java의 synchronized 키워드는 동시성 이슈를 막기 위해 사용한다.
동시성 이슈에 대해 먼저 알아보자.
동시성 이슈란 공유자원에 여러 스레드가 접근하여, race condition과 같은 문제가 발생하는 것을 말한다.
결국 공유 자원에 여러 프로세스가 동시에 접근하여 자료의 일관성을 해치는 결과가 나타난다.
synchronized는 여러 해결책중 상호 배제 원칙을 통해 이 문제를 해결한다.
java에서 synchronized는 2가지 방식으로 사용할 수 있다.
우선 메소드에 선언한 synchronized 에 대해 먼저 알아보자.
public synchronized void setSharedResource(String resource) {
sharedResource = resource;
try {
long sleep = (long) (Math.random() * 100);
Thread.sleep(sleep);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if (!Objects.equals(sharedResource, resource)) {
System.out.println(sharedResource + " is not same as " + resource);
}
}
전체 코드
public class SyncTest {
private String sharedResource;
public static void main(String[] args) {
SyncTest syncTest = new SyncTest();
System.out.println("SyncTest start");
new Thread(() -> {
for (int i = 0; i < 100; i++) {
syncTest.setSharedResource("resource1");
}
}).start();
new Thread(() -> {
for (int i = 0; i < 100; i++) {
syncTest.setSharedResource("resource2");
}
}).start();
System.out.println("SyncTest end");
}
public synchronized void setSharedResource(String resource) {
sharedResource = resource;
try {
long sleep = (long) (Math.random() * 100);
Thread.sleep(sleep);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if (!Objects.equals(sharedResource, resource)) {
System.out.println(sharedResource + " is not same as " + resource);
}
}
}
메소드에 synchronized를 선언하는 방식은 문제가 있는데 상호 배제 원리를 사용하여, 스레드가 메소드에 접근해 메소드가 종료되기 전까지 락이 걸려 다른 스레드가 접근하지 못한다.
만약 list1를 사용하는 코드에는 lock이 필요하지만, list2를 사용하는 코드에는 lock이 필요없다고 가정해보자.
public void add(int value) {
/*
code that lock is not needed
*/
if (!list2.contains(value)) {
list2.add(value);
}
synchronized (this) {
if (!list1.contains(value)) {
list1.add(value);
}
}
}
위 코드에서는 synchronized(this) 를 통해 필요한 코드 부분에만 lock을 걸었다.
그런데 왜 this를 사용하는 걸까?
전체 코드
public class MethodSyncTest {
private List<Integer> list1 = new ArrayList<>();
private List<Integer> list2 = new ArrayList<>();
public static void main(String[] args) {
MethodSyncTest mSyncTest = new MethodSyncTest();
System.out.println("mSyncTest start");
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
mSyncTest.add(i);
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
mSyncTest.add(i);
}
});
thread1.start();
thread2.start();
try {
thread1.join(3000);
thread2.join(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("mSyncTest end");
System.out.println("list1 size : " + mSyncTest.list1.size()); // 항상 100
System.out.println("list2 size : " + mSyncTest.list2.size()); // 종종 100 이 넘는다.
}
public void add(int value) {
/*
code that Sync is not needed
*/
if (!list2.contains(value)) {
list2.add(value);
}
synchronized (this) {
if (!list1.contains(value)) {
list1.add(value);
}
}
}
}
코드를 실행해보면, list1의 size는 항상 100이 나오는 반면, list2의 size는 종종 100이 넘는다.
public class SyncCompareTest {
private static List<Integer> list = new ArrayList<>();
public static void main(String[] args) {
SyncCompareTest t1 = new SyncCompareTest();
SyncCompareTest t2 = new SyncCompareTest();
System.out.println("syncCompareTest start");
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
t1.add(i);
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
t2.add(i);
}
});
thread1.start();
thread2.start();
try {
thread1.join(3000);
thread2.join(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("syncCompareTest end");
System.out.println("list1 size : " + list.size());
}
}
(this) 방식을 먼저 살펴보자.
public void add(int value) {
synchronized (this) {
if (!list.contains(value)) {
list.add(value);
}
}
}
그럼 (.class) 방식은 어떨까?
public void add(int value) {
synchronized (SyncCompareTest.class) {
if (!list.contains(value)) {
list.add(value);
}
}
}