연관 게시물
자바에서의 Thread-Safe
1. Lock
synchronized
아래 코드는 Synchronized 키워드를 사용하여 스레드의 안전성을 보장했다.
@ThreadSafe
public class Lock {
@GuardedBy("this") private int nextValue;
public synchronized int getNext() {
return nextValue++;
}
}
Lock 클래스의 getNext()메서드의 동시성을 보장한다.
getNext()에 접근할 수 있는 스레드를 단 한개만 보장한다는 뜻이며, 수많은 스레드들이 동시에 getNext()에 접근하더라도, 정확한 nextValue값을 보장받을 수 있다.
아래 코드는 synchronized 블록을 사용한 monitor pattern의 예제이다.
public class Lock2 {
private final Object myLock = new Object();
@GuardedBy("myLock") Widget widget;
void someMethod() {
synchronized (myLock) {
}
}
}
자바에서 모든 객체는 monitor를 가질 수 있는데, monitor는 여러 스레드가 동시에 객체로 접근하는 것을 막는 역할을 한다.
heap영역에 있는 객체는 모든 스레드에서 공유가 가능한데, 스레드가 monitor를 가지면 monitor를 가지는 객체에 lock를 걸 수 있다.
그렇게되면 다른 thread들이 해당 객체에 접근할 수 없게 된다.
motinor를 가질수 있는 것은 synchronized 키워드 내에서 가능한데, 여기서 myLock이 파라미터로 들어가면서, thread가 해당 객체의 monitor를 가질 수 있게 되고 해당 객체는 thread에 의해 공유될 수 있는 객체가 된다.
이해가 될 것 같으면서도 애매해서 구글링을 해서 가장 이해가 잘 된 예제를 더 들고왔다.
class Hello implement Runnable {
@Override
public void run() {
String hello = "hello";
synchronized (hello) {
System.out.println(hello);
}
}
}
위의 코드를 디컴파일 한 결과
synchronized구문을 기점으로 monitorenter와 exit가 존재한다.
스레드가 monitorenter을 가리키면 monitor의 소유권을 얻게 되며
monitorexit를 가리키면 monitor를 놓아준다. 이 때 다른 스레드들이 monitorenter가 가능해지면서 monitor를 얻게된다.
즉, monitor의 생명주기는 synchronized 키워드에 의존되며, 객체가 가지는 wait(), notify(), notifyAll()메서드는 모두 synchronized 블록에서만 유의미하고, 해당 메서드로부터 스레드를 waitset에 넣거나 불러올 수 있게 된다.
2. 자료구조
선택이 가능하다면 synchronized보다는 더 좋은 방법이다.
Hashtable, ConcurrentHashMap, AtomicInteger, BlockingQueue등이 있다.
어떤 자료구조를 사용하느냐에 따라 성능이 저하될 수 있기 때문에 상황에 맞는 자료구조를 사용하는 것이 바람직하다.
public synchronized V get(Object key) {
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
return (V)e.value;
}
}
return null;
}
...
public synchronized V put(K key, V value) {
// Make sure the value is not null
if (value == null) {
throw new NullPointerException();
}
// Makes sure the key is not already in the hashtable.
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry<K,V> entry = (Entry<K,V>)tab[index];
for(; entry != null ; entry = entry.next) {
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}
}
addEntry(hash, key, value, index);
return null;
}
...
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
int h = spread(key.hashCode());
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
...
public V put(K key, V value) {
return putVal(key, value, false);
}
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
위는 Hashtable, 아래는 ConcurrentHashMap의 메서드들이다.
Hashtable의 주요 메서드들은 전부 synchronized 키워드에 감싸진 상태로 동시성을 보장하기 때문에,
메서드를 호출할 수 있는 스레드는 반드시 1개라는 뜻이다.
ConcurrentHashMap의 get()메서드는 volatile 변수를 사용해 가시성을 보장하도록 되어있고 put()은 부분적으로 synchronized를 사용한다.
자바 1.5부터 Hashtable의 성능을 향상시키기 위해 ConcurrentHashMap을 도입했다.
3. Stack 한정 프로그래밍
모든 스레드가 각자 고유한 스택과 지역변수를 가진다는 특성을 잘 이해하면 동시성을 보장하도록 할 수 있다.
public class Animals {
Ark ark;
Species species;
Gender gender;
public int loadTheArk(Collection<Animal> candidates) {
SortedSet<Animal> animals;
int numPairs = 0;
Animal candidate = null;
animals = new TreeSet<Animal>(new SpeciesGenderComparator());
animals.addAll(candidates);
for (Animal a : animals) {
if (candidate == null || !candidate.isPotentialMate(a))
candidate = a;
else {
ark.load(new AnimalPair(candidate, a));
++numPairs;
candidate = null;
}
}
return numPairs;
}
}
animals 인스턴스는 지역변수로 선언되어 있다.
candidates 팔미터를 복사(addAll)한 뒤에 추가적인 작업을 실행하는데, 지역변수 내부(Stack)에서만 사용했기 때문에 동시성을 보장할 수 있다.
4. ThreadLocal
지역변수를 사용해 동시성을 보장하는 방법은 간결하고 이해하기 쉽다.
하지만 저 메서드 스택을 벗어나는 순간 animals 변수의 참조가 없어지기 때문에
다른 곳에서는 animals를 사용할 수 없다.
위와 같은 단점을 해결하기 위해 자바에서는 ThreadLocal 클래스를 제공하고 있다.
ThreadLocal을 사용하여 스레드 영역에 변수를 설정할 수 있기 때문에, 특정 스레드가 실행하는 모든 코드에서 그 스레드에 설정된 변수 값을 사용할 수 있게 된다.
대표적으로 SpringSecurity에서 제공하는 SecurityContextHolder가 있다.
ThreadLocal<UserInfo> local = new ThreadLocal<UserInfo>();
local.set(currentUser);
UserInfo userInfo = local.get();
ThreadLocal 객체를 생성할 수 있다.
set(), get(), remove()를 이용해 현재 쓰레드의 로컬변수에 값을 저장하고 읽고, 삭제할 수 있다.
자세한 활용방법은 아래를 참조하여 공부를 진행했다.
5. 불변객체 사용(final)
자바에서 대표적인 불변 객체(완전히 생성된 후에도 내부 상태가 유지되는 객체)
는 String이며, String은 스레드에 안전하다.
import java.util.Date;
/**
* Always remember that your instance variables will be either mutable or immutable.
* Identify them and return new objects with copied content for all mutable objects.
* Immutable variables can be returned safely without extra effort.
* */
public final class ImmutableClass
{
/**
* Integer class is immutable as it does not provide any setter to change its content
* */
private final Integer immutableField1;
/**
* String class is immutable as it also does not provide setter to change its content
* */
private final String immutableField2;
/**
* Date class is mutable as it provide setters to change various date/time parts
* */
private final Date mutableField;
//Default private constructor will ensure no unplanned construction of class
private ImmutableClass(Integer fld1, String fld2, Date date)
{
this.immutableField1 = fld1;
this.immutableField2 = fld2;
this.mutableField = new Date(date.getTime());
}
//Factory method to store object creation logic in single place
public static ImmutableClass createNewInstance(Integer fld1, String fld2, Date date)
{
return new ImmutableClass(fld1, fld2, date);
}
//Provide no setter methods
/**
* Integer class is immutable so we can return the instance variable as it is
* */
public Integer getImmutableField1() {
return immutableField1;
}
/**
* String class is also immutable so we can return the instance variable as it is
* */
public String getImmutableField2() {
return immutableField2;
}
/**
* Date class is mutable so we need a little care here.
* We should not return the reference of original instance variable.
* Instead a new Date object, with content copied to it, should be returned.
* */
public Date getMutableField() {
return new Date(mutableField.getTime());
}
@Override
public String toString() {
return immutableField1 +" - "+ immutableField2 +" - "+ mutableField;
}
}
setter메서드가 없고, 생성자가 private로 선언되어있으며, 실제 인스턴스는 팩토리 메서드를 통해 생성하도록 public이다. 또한 모든 멤버변수들은 final로 선언되어 있는데, Integer나 String은 기본적으로 불변이기 때문에 반환할 때도 그대로 리턴해도 되지만, Date의 경우 가변객체이기 때문에 반드시 방어적 복사본을 생성하는 방법으로 프로그래밍을 해야 동시성을 보장받을 수 있다.
적절한 final키워드도 별다른 동기화작업 없이 동시성환경에서 자유롭게 사용할 수 있다. 불변 객체와 비슷한 관점으로 초기화된 이후에 변경될 수 없기 때문에 여러 스레드가 동시에 접근해도 동일한 값을 보장받을 수 있다.
스레드에 대한 지식이 부족하기 때문에 당장 이해를 100%하지못했고
스레드에 대한 이해도를 넓히고
참조글과 여러 공식문서들을 참조하면서
thread-safe한 코드를 작성할 수 있는 개발자가 되어야겠지요...? 화이팅... 갈길이 멀다
참조
https://howtodoinjava.com/java/basics/how-to-make-a-java-class-immutable/
https://javacan.tistory.com/entry/ThreadLocalUsage
https://sup2is.github.io/2021/05/03/thread-safe-in-java.html
https://blog.e-zest.com/java-monitor-pattern/
https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.1
2023.04 ~ 백엔드 개발자의 기록
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!