
자바에서 thread는 매우 자주 접하게 되는 core 개념이다
한국에서 자바를 쓴다고 하면 통상적으로 servlet 기반 프레임워크들이 언급되는데 이 servlet이 요청당 thread를 사용하는 처리방식인 tomcat을 웹서버로 사용하기 때문이다
그렇기에 servlet 기반 프레임워크들을 사용하다 보면 thread에대한 이해가 필요한 경우가 많다
그래서 자바에서 thread가 어떻게 사용되는지를 알아보고자한다
thread가 무엇인지 보다는 java에서 thread를 어떻게 사용하는지에 대해 중점적으로 글을 작성해보려 한다
참고로 virtual thread와 Fiber에 대해서는 추후에 공부해보려 한다

기본적으로 therad를 사용한다는 말은 JVM이 커널의 thread 자원을 사용하겠다는 말이 된다
이과정에서 JVM은 JNI를 통해 커널 영역의 thread를 JVM이 사용할 수 있도록 한다
이를 두고 커널 수준 스레드(Kernel Level Threads)와 사용자 수준 스레드(User Level Threads)라고 한다
JVM은 운영체제의 스레드 API를 사용하여 자바 스레드를 생성하고 관리한다
JNI는 자바와 네이티브 코드 간의 상호작용을 지원하며, 네이티브 스레드가 JVM과 상호작용하기 위해서는 AttachCurrentThread를 사용해야 한다
그리고 이 과정을 간략하게 코드로 확인해보겠다

Thread.class의 구현부 일부다
thread를 사용하기 위해 start를 호출하면 start0를 내부적으로 호출하게 되는데

보이는 바와 같이 native 호출을 하게되는데 이과정에서 커널의 start0을 내부적으로 호출한다
17버전을 기준으로 다음 글을 참고해보면 C++로 작성된 start0을 호출하기 위해 JNIENV를 가져온다
JNIENV를 통해 시스템 메서드인 start0를 호출한다
멀티쓰레드 모델에는 크게 세가지 종류가 있다

커널 수준 thread당 사용자 수준 thread를 하나만 만들기 때문에 오버헤드가 굉장히 크다

Green Thread Model은 다수의 thread를 하나의 커널 수준 thread가 생성하는 방식이다
이 방식에는 여러가지 문제가 있다
하나의 커널 수준 thread가 만든 사용자 수준 thread들은 한번에 하나의 사용자 수준 thread만이 커널 수준 thread에 접근 가능하기 때문에 접근한 사용자 수준 thread가 bloking되는 경우 전체 thread다 bloking되는 치명적인 단점이 있다
사용자 수준에서의 자원 사용에 실질적 동시성을 부여하지 못하기 때문에 사용자가 직접 자원 사용을 신경 써야한다
사실상 멀티프로세싱이라 봐야 할거 같다

CPU의 물리적 자원이 증가하면서 여러개의 커널 수준 thread가 동시에 여러개의 사용자 수준 thread를 생성할 수 있게 되었다
그렇기에 커널 수준 thread에 접근하는 사용자 수준 thread가 bloking 되어도 또다른 커널 수준 thread에 접근하면 되기 때문에 진정한 의미의 multithreading이 가능해진다
그리고 만들어진 사용자 수준 thread를 OS가 관리해 주기 때문에 사용자 관점에서 실질적 동기화에 개입을 할 수 있다(물론 신경쓸것도 많아진 셈이다)
이제 예시 코드를 통해 자바에서 thread를 어떻게 사용하는지 알아보자
public class Main {
public static void main(String[] args) {
Counter counter = new Counter();
Thread thread1 = new CustomThread(counter);
Thread thread2 = new CustomThread(counter);
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("count = " + counter.getValue());
}
}
class Counter {
private int value = 0;
public void increment() {
int temp = value;
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
value = temp + 1;
}
public int getValue() {
return value;
}
}
class CustomThread extends Thread {
private final Counter counter;
public CustomThread(Counter counter) {
this.counter = counter;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
counter.increment();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
코드의 흐름을 보면 최초 CustomThread를 생성할때 하나의 Counter 객체를 주입받아 사용한다
문제는 increment() 메서드를 호출할때 두 thread가 순서 없이 value에 접근한다
그렇기 때문에 동기화 문제를 해결할 방법이 필요하다
방법은 상당히 간단한데, synchronized를 메서드에 추가하면 된다
public synchronized void increment() {
int temp = value;
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
value = temp + 1;
}
또한 atomic type을 사용해서 동시성 이슈를 해결하는 방법도 있다
class Counter {
private final AtomicInteger value = new AtomicInteger(0);
public void increment() {
value.incrementAndGet();
}
public int getValue() {
return value.get();
}
}
더 나아가 조금 정교하게 동시성을 해결하기 위해 StampedLock을 사용해 lock을 하는 방법도 있다
class Counter {
private int value = 0;
private final StampedLock lock = new StampedLock();
public void increment() {
long stamp = lock.writeLock();
try {
int temp = value;
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
value = temp + 1;
} finally {
lock.unlockWrite(stamp);
}
}
public int getValue() {
long stamp = lock.tryOptimisticRead();
int currentValue = value;
if (!lock.validate(stamp)) {
stamp = lock.readLock();
try {
currentValue = value;
} finally {
lock.unlockRead(stamp);
}
}
return currentValue;
}
}
writeLock()을 통해 자원의 쓰기 작업에 대해 lock을 걸고, tryOptimisticRead()을 통해 lock 상태에서 변수에 접근 가능하도록 readLock()을 하는 방법도 있다
reference