쓰래드 이해와 쓰레드의 생성 방법
쓰레드는 실행중인 프로그램 내에서 “또 다른 실행 흐름을 형성하는 주체:
public class Main1 {
public static void main(String[] args) {
Thread ct = Thread.currentThread();
String name = ct.getName();
System.out.println(name);
}
}
main 메소드를 실행하는 쓰레드를 지정 -> 인스턴스 참조를 얻을 수 있다.
Thread ct = Thread.currentThread();
main 메소드를 실행하는 쓰레드를 가리켜 main 쓰레드라고 한다.
쓰레드를 사용하는 방법
쓰레드를 추가하면 추가한 수만큼 프로그램 내에서는 다른 실행의 흐름이 만들어진다.
public class Main2 {
public static void main(String[] args) {
Runnable task = () -> {
int n1 = 10;
int n2 = 20;
String name = Thread.currentThread().getName();
System.out.println(name + (n1 + n2));
};
Thread t = new Thread(task);
t.start();
System.out.println(Thread.currentThread().getName());
}
}
쓰레드의 생성을 위해 제일 먼지 하는 일은 Runnable 인터페이스를 구현. Runnable 다음 추상 메소드는 하나만 존재하는 함수형 인터페이스이다.
void run()
Runnable task = () -> {
int n1 = 10;
int n2 = 20;
String name = Thread.currentThread().getName();
System.out.println(name + (n1 + n2));
};
람다식 기반으로 구현된 메소드는 생성되는 쓰레드에 의해 실행되는 메소드이다.
Runnable 구현을 마쳤다면, Thread 인스턴스를 생성해야 한다.
Thread t = new Thread(task);
인스턴스 생성시 run 메소드 구현 내용 전달.
Thread 인스턴스를 대상으로 start 메소드를 호출
t.start();
쓰레드를 생성해서 Thread 인스턴스 생성시 전달된 run 메소드를 실향.
public Thread(Runnable target, String name)
main 쓰레드가 일을 끝냈다고 해서 프로그램이 종료되지는 않는다. 모든 쓰레드가 일을 마치고 소멸되어야 프로그램이 종료된다.
“쓰레드는 자신의 일을 마치면 자동 소멸”
public class Main3 {
public static void main(String[] args) {
Runnable task1 = () -> {
try {
for (int i = 0; i < 20; i++) {
if (i % 2 == 0)
System.out.println(i);
Thread.sleep(100);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
};
Runnable task2 = ()-> {
try {
for (int i = 0; i < 20; i++) {
if (i % 2 == 1)
System.out.println(i);
Thread.sleep(100);
}
}
catch (InterruptedException e){
e.printStackTrace();
}
};
Thread t1 = new Thread(task1);
Thread t2 = new Thread(task2);
t1.start();
t2.start();
}
}
두개의 쓰레드를 생성, 동시에 실행되는 상황을 조금더 자세히 보기 위해 쓰레드 클래스의 호출을 조금 늦췄다.
Thread.sleep(100);
보통 쓰레드는 CPU의 코어 하나가 할당되어, 동시에 실행. 쓰레드 별로 코어가 하나씩 할당되는 상황은 일반 경우. 실행흐름을 조절하거나, 예측은 -> 잘못된 결과로 이어질 수 있다. 쓰레드가 처한 상황에 따라서, 또는 운영체제가 코어를 쓰레드에 할당하는 방식에 따라서 두 쓰레드의 실행 속도에 차이가 있을 수 있기 때문.
각각의 쓰레드는 독립적으로 자신의 일을 실행해 나간다.
만약 코어가 하나이고, 쓰레드가 둘 이상이면 코어를 나누어 실행.
쓰레드를 생성하는 두번째 방법
1. Runnable 인터페이스 구현
2. Thread 인스턴스 생성
3. start 메소드 호출
또다른 방법
1. Thread를 상속하는 클래스 정의와 인스턴스 생성
2. start 메소드 호출
3. class Tasks extends Thread{
public void run(){
int n1 = 20;
int n2 = 30;
String name = Thread.currentThread().getName();
System.out.println(name);
}
}
public class Main4 {
public static void main(String[] args) {
Tasks t1 = new Tasks();
Tasks t2 = new Tasks();
t1.start();
t2.start();
System.out.println(Thread.currentThread().getName());
}
}
쓰레드 동기화
쓰레드 메모리 접근 방식과 그에 따른 문제점
둘 이상 쓰레드가 하나의 메모리 공간에 접근 시 발생하는 문제
class Counter{
int count = 0; // 두 쓰레드에 의해 공유 되는 변수
public void increment(){
synchronized (this) {
count++; // 첫 쓰레드에 의해 실행
System.out.println("1증가");
}
}
synchronized public void decrement(){
count --; // 또다른 쓰레드에 의해 실행 되는 문장
}
public int getCount(){
return count;
}
}
public class Main5 {
public static Counter cnt = new Counter();
public static void main(String[] args) throws InterruptedException {
Runnable task1 = () -> {
for (int i = 0; i < 1000; i++)
cnt.increment();
};
Runnable task2 = () -> {
for (int i = 0; i < 1000; i++)
cnt.decrement();
};
Thread t1 = new Thread(task1);
Thread t2 = new Thread(task2);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(cnt.getCount());
}
}
여기 호출되는 join 메소드는 특정 쓰레드가 실행이 완료되기를 기다리는 호출 메소드
두 쓰레드 실행이 완료되기를 기다리기 위해서 join 메소드 호출. 실행할 때마다 출력 값이 다른데, “둘 이상의 쓰레드가 동일한 변수에 접근하는 것은 문제가 될 수 있다.”
둘 이상의 쓰레드가 동일한 메모리 공간에 접근해도 문제가 발생하지 않도록 동기화 하는 것이 중요
동일한 메모리 공간에 접근하는 것이 문제가 왜 되는가?
둘 이상의 쓰레드가 하나의 변수 또는 메모리 공간에 접근을 하면 문제 발생.
ex) 변수에 저장된 값을 1씩 증가시키는 연산을 두 쓰레드가 동시에 진행. 값의 증가는 코어를 통한 연산이 필요. 실행이 되고 그 값은 변수에 가져다 놓는다. => 변수 값 저장. thread1 thread2 도 값을 증가시키기 위해 변수에 저장된 값을 가져갔다. 두 쓰레드가 동시에 값을 증가시켰지만. 두 쓰레드 모두 값이 같다. => 이 문제가 발생한 이유는 두 쓰레드가 동시에 같은 변수에 접근 했기 때문이다.
동기화
synchronized public void increment() { -> 한순간에 한 쓰레드의 접근만 허용. 이 메소드를 두 쓰레드가 동시에 호출하면, 조금이라도 빠르게 호출된 쓰레드가 실행이 되고, 다른 쓰레드는 대기, 그리고 나서 우선 호출한 쓰레드 종료되면 다음 쓰레드가 실행.
int count = 0;
public void increment(){
synchronized (this) {
count++;
System.out.println("1증가");
}
}
synchronized public void decrement(){
count --;
}
public int getCount(){
return count;
}
}
synchronized 메소드가 선언되면 동시에 쓰레드 실행 X
동기화 블록
동기화 메소드 기반의 동기화는 메소드 전체에 동기화를 걸어야 한다는 단점
public void increment(){
synchronized (this) {//동기화 블록
count++;//동기화 필요한 문장
System.out.println("1증가"); // 불필요한 동기화 문장
}
}
public void increment(){
synchronized (this) { //동기화 블록
count++;
System.out.println("1증가");
}
}
public void decrement(){
synchronized (this) { //동기화 블록
count--;
System.out.println("1감소");
}
}
인스턴스 내에 위차힌 두 동기화 블록은 둘 이상의 쓰레드에 의해 동시에 실행될 수 없도록 동기화
StringBuffer는 “쓰레드에 안전” -> StringBuffer가 동기화 되어있어서, 인스턴스를 대상으로 둘 이상의 쓰레드가 동시에 접근해도 문제가 되지 않음.
쓰레드를 생성하는 더 좋은 방법
쓰레드 풀이라는 것을 미리 만들고, 미리 제한된 수의 쓰레드를 생성해두고 이를 재활용.
각각의 쓰레드 생성되고, 이렇게 생성된 쓰레드는 작업이 끝나면 자동으로 소멸되어 리소스 소모가 많았다. 멀티쓰레드 프로그래밍에서 쓰레드 풀은 활용 중요하다.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Main6 {
public static void main(String[] args) {
Runnable task = () -> {
int n1 = 10;
int n2 = 20;
String name = Thread.currentThread().getName();
System.out.println(name);
};
ExecutorService exr = Executors.newSingleThreadExecutor();
exr.submit(task);
System.out.println(Thread.currentThread().getName());
exr.shutdown();
}
}
ExecutorService exr = Executors.newSingleThreadExecutor();
exr.submit(task);
쓰레드 풀에 작업을 전달
exr.shutdown();
쓰레드 풀과 그 안에 있는 쓰레드 소멸
생성된 쓰레드 풀에 다음과 같이 submit 메소드를 호출을 통해 작업이 전달되면, 풀에서 대기하고 있던 쓰레드가 일을 생성. 그리고 작업이 끝나면 해당 쓰레드는 다시 쓰레드 풀에 돌아가서 다른 작업이 전달되기를 기다린다.
Executors.newSingleThreadExecutor();
메소드 호출을 통해 쓰레드 풀을 생성했지만, Excutors 클래스의 다음 메소드를 통해서 다양한 유형의 쓰레드 풀을 생성 할 수 있다.
newSingleThreadExcutor -> 풀 안에 하나의 쓰레드만 생성하고 유지
하나의 코어를 기준으로 코어의 활용도를 높인 풀
newFixedThreadPool -> 풀 안에 인자로 전달된 수의 쓰레드를 생성 유지
newCachedThreadPool -> 풀 안에 쓰레드의 수를 작업의 수에 맞게 유동적으로 관리
생성하는 풀은 전달된 작업의 수에 근거하여 쓰레드의 수를 늘리기도 하고 줄이기도 한다. 작업에 수에 비례하여 쓰레드가 생성될 수 이슨 관계로 앞서 언급한 빈번한 쓰레드의 생성과 소멸로 이어질 수 있다.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Main7 {
public static void main(String[] args) {
Runnable task1 = () -> {
String name = Thread.currentThread().getName();
System.out.println(name + (5 + 7));
};
Runnable task2 = () -> {
String name = Thread.currentThread().getName();
System.out.println(name + (7 -5));
};
ExecutorService exr = Executors.newFixedThreadPool(2);
exr.submit(task1);
exr.submit(task2);
exr.submit(() -> {
String name = Thread.currentThread().getName();
System.out.println(name + (5 * 7));
});
exr.shutdown();
}
}
두개의 쓰레드가 존재하는 쓰래드 풀을 생성했다.
ExecutorService exr = Executors.newFixedThreadPool(2);
그리고 이 풀을 대상을 세 개의 작업을 전달했는데 세번째 작업의 전달 방식은
exr.submit(() -> {
String name = Thread.currentThread().getName();
System.out.println(name + (5 * 7));
});
Callable & Future
Runnable에 위치한 추상 메소드 run 반환형이 void이기 때문에 작업의 결과 return 불가.
Callable tast = () -> {
int sum = 0;
for (int i = 0; i < 10; i++)
sum += i;
return sum;
};
메소드 반환 값을 다음과 같이 Future(V) 형 참조변수에 저장.
Future fur = ex.submit(tast);
Future 타입인자는 Callable의 타입 인자와 일치
Integer r = fur.get();
쓰레드의 반환 값 획득
Synchronized를 대신 ReentrantLock
ReentrantLock criticObj = new ReentrantLock();
public void increment(){
criticObj.lock(); //문 잠근다.
try {
cnt ++; // 한 쓰레드에서 실행
} finally {
criticObj.unlock(); //문을 연다.
}
}
public void decrement(){
criticObj.lock();
try {
cnt --;
} finally {
criticObj.unlock();
}
}
한 쓰레드가 lock 메소드를 호출, -> 다음 문장을 실행하기 시작한 상태에서 다른 쓰레드가 lock 메소드를 호출, 이 쓰레드는 lock 메소드를 반환 X 그 자리에서 대기. 먼저 lock 메소드를 호출한 쓰레드가 unlock 매소드를 호출할 때까지 대기, lock 메소드의 호출 문장과 unlock 메소드의 호출 문장 사이에는 하나의 쓰레드만이 실행 가능. 그런데 lock 메소드를 호출한 쓰레드가 unlock 메소드를 호출하지 않은 코드 상에서 실수 발생.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
class Counter1{
int cnt = 0;
ReentrantLock criticObj = new ReentrantLock();
public void increment(){
criticObj.lock();
try {
cnt ++;
} finally {
criticObj.unlock();
}
}
public void decrement(){
criticObj.lock();
try {
cnt --;
} finally {
criticObj.unlock();
}
}
public int getCnt(){
return cnt;
}
}
public class Main9 {
public static Counter1 cnt = new Counter1();
public static void main(String[] args) throws InterruptedException {
Runnable task1 = () -> {
for (int i = 0; i < 1000; i++) {
cnt.increment();
}
};
Runnable task2 = () -> {
for (int i = 0; i < 1000; i++) {
cnt.decrement();
}
};
ExecutorService ex = Executors.newFixedThreadPool(2);
ex.submit(task1);
ex.submit(task2);
ex.shutdown();
ex.awaitTermination(100, TimeUnit.SECONDS);
System.out.println(cnt.getCnt());
}
}
ex.awaitTermination(100, TimeUnit.SECONDS);
쓰레드 풀에 전달된 작업이 끝나기를 1000초 기다린다.
하지만 생각과 달리 메소드는 바로 반환. 즉 쓰레드 풀에 전달된 작업이 마무리되면, 풀을 패쇄하라고 명령. 기다려주지 않는다. 그래서 쓰레드 풀에 전달된 작업의 최종 결과을 확인 ->
awaitTermination메소드 호출은 블로킹 상태에 놓이게 된다.
쓰레드 풀에 전달된 모든 작업이 완료
작업이 완료되지는 않았지만 초를 기준으로 100을 셈
컬렉션 인스턴스 동기화
동기화는 성능의 저하를 수반한다. 컬랙션 프레임워크의 클래스 대부분도 동기화 처리가 되어 있지 않는다. 따라서 쓰레드의 동시 접근에 안전하지 않다. 대신에 Collections의 다음 메소드들을 통한 동기화 방법을 제공
sychronizedSet/ synchronizedList/ synchronizedMap/ synchronizedCollection
List lst = Collections.synchronziedList
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.ListIterator;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class Main10 {
public static List list =
Collections.synchronizedList(new ArrayList());
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 16; i++) {
list.add(i);
System.out.println(list);
Runnable task = () -> {
synchronized (list) {
ListIterator<Integer> itr = list.listIterator();
while (itr.hasNext())
itr.set(itr.next() + 1);
}
};
ExecutorService sc = Executors.newFixedThreadPool(2);
sc.submit(task);
sc.submit(task);
sc.shutdown();
sc.awaitTermination(100, TimeUnit.SECONDS);
System.out.println(list);
}
}
}
Runnable task = () -> {
synchronized (list) {
ListIterator itr = list.listIterator();
while (itr.hasNext())
itr.set(itr.next() + 1);
}
};
인스턴스에 저장된 값을 1씩 증가시키는 방법
우선 컬랙션 인스턴스 자체에 대한 동기화에는 문제가 없다. 문제는 반복자이다. 컬랙션 인스턴스가 동기화 되었다 해도 이를 기반으로 생성된 반복자까지 동기화가 이뤄지는 것은 아니다. 따라서 반복자를 통해 접근할 때에는
synchronized (list) { //list 작업중 일때는 다른 쓰레드가 list에 접근 x
를 추가 해줘여 한다.
“동기화 블록의 내부를 실행 할 때 list에 다른 쓰레드 접근을 허용하지