자바는 stack, heap, static 영역으로 이루어져 있음. 자바의 스레드끼리는 static 하고 heap 영역을 공유하기 때문에, 공유 자원에 대한 동기화 문제를 신경써야 한다.
동기화 문제를 해결하는 방법 중 하나인 Synchronized 키워드
sychronized 는 lock 을 통해 동기화를 시키며 4가지 사용법이 있다
public class SyncClass {
public synchronized void syncMethod1(String msg) {
System.out.println(msg + "의 syncMethod1 실행중 " + LocalDateTime.now());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized void syncMethod2(String msg) {
System.out.println(msg + "의 syncMethod2 실행중 " + LocalDateTime.now());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class App {
public static void main(String[] args) {
SyncClass syncClass = new SyncClass();
Thread thread1 = new Thread(() -> {
System.out.println("thread1 start" + LocalDateTime.now());
syncClass.syncMethod1("thread1");
System.out.println("thread1 end" + LocalDateTime.now());
});
Thread thread2 = new Thread(() -> {
System.out.println("thread2 start" + LocalDateTime.now());
syncClass.syncMethod2("thread2");
System.out.println("thread2 end" + LocalDateTime.now());
});
thread1.start();
thread2.start();
}
}
SyncClass 인스턴스를 하나 생성하고 두 스레드를 만들어 각각 syncMethod1, syncMethod2 를 호출했다
// 결과
thread1 start2022-11-27T15:01:07.204569
thread2 start2022-11-27T15:01:07.204888
thread1의 syncMethod1 실행중 2022-11-27T15:01:07.230202
thread1 end2022-11-27T15:01:08.250856
thread2의 syncMethod2 실행중 2022-11-27T15:01:08.250880
thread2 end2022-11-27T15:01:09.256507
스레드1 이 syncMethod1 를 호출하고 종료된 다음 스레드2가 syncMethod2 를 호출하고 종료
인스턴스를 두개 만들고 실행시키면 결과값은 아래와 같다.
public class App {
public static void main(String[] args) {
SyncClass syncClass1 = new SyncClass();
SyncClass syncClass2 = new SyncClass();
Thread thread1 = new Thread(() -> {
System.out.println("thread1 start" + LocalDateTime.now());
syncClass1.syncMethod1("thread1");
System.out.println("thread1 end" + LocalDateTime.now());
});
Thread thread2 = new Thread(() -> {
System.out.println("thread2 start" + LocalDateTime.now());
syncClass2.syncMethod2("thread2");
System.out.println("thread2 end" + LocalDateTime.now());
});
thread1.start();
thread2.start();
}
}
thread2 start2022-11-27T15:10:46.957119
thread1 start2022-11-27T15:10:46.957458
thread1의 syncMethod1 실행중 2022-11-27T15:10:46.983359
thread2의 syncMethod2 실행중 2022-11-27T15:10:46.983279
thread1 end2022-11-27T15:10:48.005838
thread2 end2022-11-27T15:10:48.006050
lock 을 공유하지 않아, 스레드간 동기화가 발생하지 않는다.
synchronized method 는 인스턴스에 lock 을 건다. 인스턴스 접근 자체에 lock 을 거는게 아니라 synchronized 키워드가 붙은 메소드에 lock을 거는 것.
static 키워드가 포함되면 synchronized 메소드는 인스턴스가 아닌 클래스 단위로 lock을 공유한다.
public class Basic {
private static Basic basic;
public static Basic getInstance() {
if (Objects.isNull(basic)) {
basic = new Basic();
}
return basic;
}
}
싱글 스레드 환경에서는 싱글톤 객체를 쉽게 위와 같은 방식으로 반환할 수 있지만, 멀티 스레드 환경에서는 getInstance 가 동시에 불릴 수 있어 동기화 문제가 발생한다.
getInstance에 synchronized
를 붙이면 동기화 이슈를 해결할 수 있지만, synchronized
메소드가 많으면 멀티 스레드는 병목현상이 발생할 수 있다. (기껏 멀티 스레드를 사용하는데, 싱글 스레드처럼 동작할 수 있게된다)
라는 방법이 있는데… 굳이 알아야 하나?? 싶네..
private static int count= 0;
@Test
void threadTest() throws InterruptedException {
int maxCnt = 1000;
for (int i=0; i<maxCnt; i++) {
new Thread(() -> {
count++;
System.out.println(count);
}).start();
}
Thread.sleep(1000);
assertThat(count).isEqualTo(maxCnt);
}
생성된 각 쓰레드들은 결국 전역변수 count를 참조하고 있는데,
count++ 연산이 원자성을 보장하지 않음 → 동시성 문제 발생할 수 있음
private static int count= 0;
public synchronized void plus() { // 임계 영역임이 보장됨
count++;
System.out.println();
}
@Test
void threadTest() throws InterruptedException {
int maxCnt = 10000;
for (int i=0; i<maxCnt; i++) {
new Thread(this::plus).start();
}
Thread.sleep(100);
assertThat(count).isEqualTo(maxCnt);
}
1) 가시성 문제
2) 원자성 문제
(다시 보기)
스레드가 요청이 올 때마다 생성하면 새로 생성하는 데 비용이 많이 든다.
왜?? 커널 레벨에서 다루기에 비용이 크게 발생한대
그래서 풀이라는 개념이 있는데
클라이언트가 작업을 요청하면 이 작업은
Task Queue에 해당 작업을 넣는다.
쓰레드 풀은 Task Queue에 존재하는 작업을 꺼내 쓰레드에게 실행하도록 함
장점:
비용적 측면, 컨텍스트 스위칭 딜레이 줄일 수 있음
단점:
너무 많은 양의 스레드를 만들면 메모리 낭비가 심해질 수 있음