멀티쓰레드를 지원함으로써, 하나의 자바 프로그램에서 여러 개의 쓰레드를 동시에 실행할 수 있습니다. 이를 통해 프로그램의 실행 속도를 높이고, 사용자가 프로그램을 더 효율적으로 이용할 수 있게 됩니다.
자바에서 멀티쓰레드를 구현하는 방법은 두 가지가 있습니다.
첫 번째 방법은 Thread 클래스를 상속하여 쓰레드를 생성하고 실행시키는 방법입니다.
두 번째 방법은 Runnable 클래스를 구현해서 쓰레드를 생성하는 방법입니다.
먼저 Thread 클래스를 상속받아 새로운 쓰레드를 생성하려면 다음과 같이 코드를 작성할 수 있습니다.
public class MyThread extends Thread {
public void run() {
// 스레드가 실행할 코드 작성
}
}
예시 코드를 보면 Thread 클래스를 상속받아서 MyThread 클래스를 정의하고 있습니다.
이 클래스에서 run() 메서드를 오버라이딩해서 쓰레드가 실행할 코드를 작성할 수 있습니다.
쓰레드를 실행하려면 start() 메서드를 호출해야 합니다.
MyThread myThread = new MyThread();
myThread.start();
Runnable 인터페이스를 구현해서 쓰레드를 생성하는 방법은 다음과 같습니다.
public class MyRunnable implements Runnable {
public void run() {
// 스레드가 실행할 코드 작성
}
}
예시 코드를 보면 Runnable 인터페이스의 구현체인 MyRunnable 클래스를 정의하고 있습니다. 이 클래스에서는 run() 메서드를 오버라이딩해서 쓰레드가 실행할 코드를 작성할 수 있습니다.
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
마찬가지로 이 클래스를 사용해서 Thread 인스턴스를 생성하고 start() 메서드를 호출해서 쓰레드를 실행시킬 수 있습니다.
여러 쓰레드가 동시에 하나의 변수에 접근하게 되면 변수의 값이 의도치 않게 변경되는 문제가 발생합니다. 이와 같은 멀티쓰레드 프로그래밍에서 발생할 수 있는 문제들을 해결하기 위해 Java에서는 동기화(Synchronized) 기능을 제공합니다. 이를 통해 여러 쓰레드가 공유하는 변수나 자원에 대해 쓰레드 간의 접근을 조절할 수 있습니다.
synchronized 키워드나 Lock 인터페이스를 사용할 수 있습니다.
public class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
예시 코드를 보면 Counter 클래스가 하나의 멤버 변수 count를 가지고 있습니다. 이 변수에는 여러 쓰레드가 동시에 접근할 수 있습니다. 이로 인해 의도치 않게 변수의 값이 변경되는 것을 방지하기 위해서 increment 메서드와 getCount 메서드에 synchronized 키워드를 사용해서 동기화를 수행하고 있습니다.
이를 통해 해당 메서드 중 하나가 실행 중일때, 다른 쓰레드가 해당 메서드를 실행할 수 없게해서 안전하게 변수나 자원을 처리할 수 있게 됩니다.
단점은 없나요?
해당 키워드로 동기화된 메소드는 하나의 스레드만 실행할 수 있기 때문에 다른 스레드는 해당 메서드 실행이 완료되기 전까지 기다려야 합니다. 이러한 대기 상태는 멀티스레드 프로그래밍에서 성능문제가 발생할 수 있습니다.
그렇기 때문에 해당 키워드를 남용하면 성능 문제가 발생할 수 있습니다. 만약 동기화가 필요한 부분이 많아지면, CPU 코어 간의 경쟁이 증가하게 되어 성능이 저하 될 수 있습니다. 따라서 동기화가 필요한 부분에만 사용하고 너무 많이 사용하지 않도록 유의해야 합니다.
Java에서 제공하는 동기화 기능을 구현하는 인터페이스입니다.
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Counter {
private int count = 0;
private Lock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public void decrement() {
lock.lock();
try {
count--;
} finally {
lock.unlock();
}
}
public int getCount() {
return count;
}
}
해당 인터페이스가 제공하는 메서드를 통해 동기화를 구현할 수 있습니다.
Counter 클래스의 메서드를 보면 lock 메서드로 락을 얻고 unlock 메서드로 락을 반환하도록 구현되어 있습니다. 만약 락이 이미 다른 쓰레드에 의해 획득되어 있다면, 해당 쓰레드는 락을 획득할 때까지 대기합니다.
쓰레드 풀(Thread Pool) 기능을 제공하여 많은 수의 쓰레드를 생성하고 관리하는데 있어 편리함을 제공한다. 쓰레드 풀을 사용하면 필요에 따라 쓰레드를 동적으로 생성하거나 제거할 수 있으며, 쓰레드의 재사용성을 높여서 성능을 향상시킬 수 있습니다.
Java에서 쓰레드 풀은 java.util.concurrent 패키지에서 제공됩니다. 쓰레드 풀을 사용하면 쓰레드 생성과 삭제에 대한 오버헤드를 줄이고, 쓰레드의 재사용성을 높여서 프로그램의 성능을 향상시킬 수 있습니다.
예를 들어서 1000개의 작업을 처리해야하는 경우에 각각의 작업을 처리하기 위해서 쓰레드를 생성하고 삭제하는 것은 오버헤드가 큽니다. 대신 쓰레드 풀을 사용해서 미리 쓰레드를 생성하고 작업을 할당하는 방식을 사용하면, 쓰레드 생성과 삭제에 대한 오버헤드를 줄이고 성능을 개선할 수 있습니다.
그러면 어떻게 사용해요?
쓰레드 풀 Executor 인터페이스와 그 하위 인터페이스들을 통해 사용할 수 있습니다. 예를 들어 ExecutorService 인터페이스는 쓰레드 풀을 생성하고 관리하기 위한 메서드들을 제공합니다.
// 쓰레드 풀 생성
ExecutorService executor = Executors.newFixedThreadPool(5);
// 작업 할당
for (int i = 0; i < 10; i++) {
executor.submit(new MyRunnableTask(i));
}
// 쓰레드 풀 종료
executor.shutdown();
예시 코드를 보면 Executors.newFixedThreadPool(5)는 5개의 쓰레드를 가지는 쓰레드 풀을 생성합니다. 그리고 executor.submit(new MyRunnableTask(i))는 MyRunnableTask라는 Runnable 구현체를 생성하고, 쓰레드 풀에 작업을 할당합니다.