운영체제에서 배우는 Thread의 개념을 숙지하고 자바에서는 어떻게 구현되는지 알아봅시다!
Thread 구현
- Thread 클래스 상속
- Runnable 인터페이스 구현
Thread 클래스 상속의 경우 run() 메소드를 오버라이딩하고 Runnable 인터페이스 구현의 경우 run() abastract 메소드를 구현해주는 방식이다. 이전에 배웠겠지만 클래스 상속보다는 인터페이스 구현이 좀 더 선호된다. (자바는 단일 상속만 가능하기 때문)
다만 Runnable 자체로는 start() 메소드를 호출할 수 없다. (start 와 run의 차이는 다음에!) 따라서 Runnable 인터페이스 구현체를 Thread 생성자에 넣어 새로운 쓰레드로 생성해주고 사용하도록 한다.
public class ThreadTest {
public static void main(String[] args) {
Runnable runnable = new RunnableImpl();
Thread1 thread1 = new Thread1();
Thread thread2 = new Thread(runnable);
thread1.start();
thread2.start();
}
static class Thread1 extends Thread {
@Override
public void run() {
for(int i = 0; i < 300; i++) {
System.out.println("-");
}
}
}
static class RunnableImpl implements Runnable {
@Override
public void run() {
for(int i = 0; i < 300; i++) {
System.out.println("|");
}
}
}
}
데몬 쓰레드
쉽게 설명하자면 보조쓰레드이다. 일반 쓰레드가 모두 종료되면 강제적으로 자동종료되는 것이 당연한 쓰레드이다.
setDaemon(true) 로 간단하게 설정 가능하다.
public final void setDaemon(boolean on) {
checkAccess(); // 현재 쓰레드가 해당 쓰레드의 daemon 여부를 변경할 수 있는 권한이 있는지 체크
if (isAlive()) { // 현재 쓰레드가 이미 진행중이라면 에러
throw new IllegalThreadStateException();
}
daemon = on;
}
쓰레드 동기화
1. synchronized
코드 자체를 critical section 으로 설정하여 쓰레드 간의 동기화를 하는 방법
아래의 두 가지 방법 중에 하나를 택할 수 있다.
public synchronized void add(String dish) {
System.out.println("Dishes: " + dishes.toString());
}
public void add(String dish) {
synchronized(this) {
System.out.println("Dishes: " + dishes.toString());
}
}
this 의 뜻은 객체 인스턴스 그 자체이다. 코드를 보도록 하자
public class A {
public void test() {
synchronized (this) {
System.out.println("lock " + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {}
System.out.println("unlock " + Thread.currentThread().getName());
}
}
}
public class ThreadTest {
public static void main(String[] args) throws Exception {
A a = new A();
Thread t1 = new Thread(() -> {
a.test();
});
Thread t2 = new Thread(() -> {
a.test();
});
t1.start();
t2.start();
}
}
synchronized(this) 는 이 객체 자체를 lock 걸겠다는 뜻이다. 그래서 같은 인스턴스에 대해 락이 잘 걸리는걸 확인할 수 있다. 만약에 인스턴스가 아니라 클래스 자체로 락을 걸고 싶다면 다른 방법이 필요하다.
public class A {
static Object lock = new Object();
public void test() {
synchronized (lock) {
System.out.println("lock " + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {}
System.out.println("unlock " + Thread.currentThread().getName());
}
}
}
public class ThreadTest {
public static void main(String[] args) throws Exception {
Thread t1 = new Thread(() -> {
new A().test();
});
Thread t2 = new Thread(() -> {
new A().test();
});
t1.start();
t2.start();
}
}
synchronized(lock) 에서 lock 변수를 통해 동기화가 진행되고 있다.
A 인스턴스들은 모두 static 변수 Object lock = new Object() 를 공유하고 있고 이걸 동기화시킨다. 모든 A 인스턴스들은 공유된 변수 lock 를 통해 동기화가 가능해진다.
참고! 메소드에 synchronized를 한 경우 메소드 끼리 하나의 인스턴스에 대해 동기화를 한다.
public class A {
public synchronized void test1() {
System.out.println("lock " + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {}
System.out.println("unlock " + Thread.currentThread().getName());
}
public synchronized void test2() {
System.out.println("lock " + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {}
System.out.println("unlock " + Thread.currentThread().getName());
}
}
public class ThreadTest {
public static void main(String[] args) throws Exception {
A a = new A();
Thread t1 = new Thread(() -> {
a.test1();
});
Thread t2 = new Thread(() -> {
a.test2();
});
t1.start();
t2.start();
}
}
2. volatile
volatile 를 알려면 컴퓨터 구조를 알아야한다. 요즘 컴퓨터는 기본적으로 멀티 코어 프로세서를 갖추는데 이때 코어는 메인 메모리에서 데이터를 캐시에 복사해오고 연산을 진행한다. 문제는 캐시와 메인 메모리가 데이터 불일치성이 생길 때 문제가 되는 것이다. 그래서 volatile 로 변수를 선언하면 캐시가 아닌 무조건 메인 메모리에서 읽어오도록 한다.
그렇다고 해서 volatile이 동기화를 위해 등장한 것은 아니다. 원래 의미는 변수의 read/write를 원자화 함에 있다. int 와 int 보다 작은 타입들 (4바이트 이하) 는 하나의 명령어로 read/write가 가능하다. (원래 작업의 최소 단위가 4바이트이기 때문) 그러나 long과 double 과 같이 4바이트 보다 큰 변수들은 하나의 명령어로 실행이 해결이 안 되는 크기다. 즉 최소 2개의 명령어가 필요하여 두 개의 명령어가 실행되는 사이에 다른 쓰레드가 간섭한다면 데이터 불일치가 생긴다. 그래서 하나의 액션으로 묶어 락을 걸어서 다른 쓰레드가 데이터 불일치를 만들지 않도록 하는 것이다.
'📗Java' 카테고리의 다른 글
[Java] Garbage Collection (0) | 2023.09.07 |
---|---|
[Java] 자바 가상 머신 (JVM) 과 GC (1) | 2023.09.06 |
[Java] String 클래스 메소드, StringBuffer, StringBuilder (0) | 2023.07.23 |
[Java] 예시와 함께 알아보는 추상화 - 추상클래스와 인터페이스 (0) | 2023.07.20 |
[Java] Chained Exception (0) | 2023.07.20 |