본문 바로가기

JAVA/JAVA 객체지향

[Java] 스레드

실행 중인 프로그램 가리켜 프로세스라고 한다.

프로세스 내에서 하나의 프로그램의 실행 흐름을 나타내는 것을 스레드라고 한다.

 

만약에 하나의 프로세스 안에서 3개의 스레드가 있다는 것은 스레드 3개가 각각의 실행 흐름을 가지고 있다는 것을 말한다.

 

우리는 스레드를 직접 만들지 않았지만 사용하고 있었다.

main()메서드의 호출과 실행을 담당하기 위한 스레드가 자동 생성 되었던 것이다.

자바 프로그램을 실행 시키면 하나의 프로세스가 생성이 되고 그 안에 하나의 스레드가 만들어 져서 이 스레드에 의해서 main() 메서드가 실행이 되었던 것이다.

 

스레드

  • 프로그램 내에서 실행 흐름을 이루는 최소의 단위
  • main 메서드의 실행도 하나의 스레드에 의해 진행이 된다.

스레드의 이해와 스레드의 생성 방법

1
2
3
4
5
6
7
8
9
public class CurrentThreadName {
    public static void main(String[] args) {
        
        Thread ct = Thread.CurrentThreadName();
        String name = ct.getName(); //스레드의 이름 반환
        System.out.println(name);
    }
}
 
 
 

main() 메서드의 스레드 이름 반환.

 

스레드를 생성하는 첫 번째 방법

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class CurrentThreadName {
    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();//스레드 생성 및 실행
    }
    //Runnable
}
 
 
  1. Runnable 인터페이스를 구현한 인스턴스 생성( run()메서드 구현, 스레드에게 일을 시키기 위한 메서드) 
  2. Thread 인스턴스 생성, 인자로 task 인스턴스 전달
  3. start 메서드 호출

스레드 인스턴스의 생성은 스레드를 생성하기 위한 준비과정이다.

스레드를 생성하기 위해서는 start()메서드를 호출해야 한다.

 

실행 순서 main() 메서드 -> start() 메서드 호출 시 Runnable 실행

 

둘 이상의 스레드를 생성한 예

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class ThreadTest1 {
    
    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);  //0.1초간 정지
                }
            } 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); //0.1초간 정지
                }
            } catch (InterruptedException e2) {
e2.printStackTrace();
            }
            
        };
        
        Thread t1 = new Thread(task1);
        Thread t2 = new Thread(task1);
        
t1.start();
t2.start();
 
 
    }
        
}
 

멀티 스레드 : 하나의 프로세스 안에 둘 이상의 스레드가 존재하는 경우

 

task1 task2의 Runnable 인스턴스 2개를 생성하고 두 개의 스레드를 생성했다

main() 메서드의 스레드 하나와 두 개의 스레드 3개의 스레드가 생성됐다.

 

t1의 스레드는 0,2,4,6,8…

t2의 스레드는 1,3,5,7,9…의 짝수, 홀수를 출력하면서 0.1초씩 멈췄다가 진행한다.

 

나는 0,1,2,3,4,5,6,7,8…처럼 순서대로 출력이 될 것이라고 생각 했다.

하지만 순서대로 출력 되지 않는 경우도 생겼다.

대부분 순서대로 출력이 될 것이지만 컴퓨터에 따라서 순서대로 출력이 되지 않을 때도 있다.

 

cpu에 3개의 코어가 있다고 가정해보자.

3개의 코어는 여러개의 프로세스가 실행될 경우 돌아가면서 사용을 하게 된다.

만약 windows에서 하나의 스레드가 필요하고 예제에서 처럼 3개의 스레드가 필요한 경우, 총 4개의 스레드는 운영체제에서 정의해 놓은 규칙대로 코어를 돌아가면서 사용하게 된다.

 

t1의 스레드가 실행하려고 했을 때 windows에서 필요로 해 먼저 사용할 경우 순서대로 출력이 되지 않을 수 있다!

 

결론 : 스레드의 실행 흐름은 먼저 생성하고 실행했다고 해서 먼저 종료되지는 않는다.

실행의 시간을 예측해서 프로그래밍을 하면 안되고 실행 했을 때 종료된다는 사실만을 가지고 작성해야 한다.

 

 

스레드를 생성하는 두 번째 방법(첫 번째 방법을 더 많이 사용)

스레드를 상속하고 run메서드를 오버라이딩 해서 사용하는 방법

람다식이 더 간결하고 편하기 때문에 첫 번째 방법을 더 많이 사용한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Task extends Thread{
    @Override
    public void run() { //Thread의 fun 메서드 오버라이딩
        int n1 = 10;
        int n2 = 20;
        String name = Thread.currentThread().getName();
        System.out.println(name + ": "+(n1+n2));
    }
}
 
public static void main(String[] args) {
        
    Task t1 = new Task();
    Task t2 = new Task();
    
    t1.start();
    t2.start();
    System.out.println("End "+Thread.currentThread().getName());
}
 
 
  • Thread를 상속하는 클래스의 정의와 인스턴스 생성
  • start 메소드 호출

 

스레드의 동기화(Synchronization)

 

동기화 : 하나의 메서드를 하나의 스레드가 사용할 경우 다른 스레드가 접근하는 것을 막는다.

 

동기화 블록 : 특정 블록 '{ 블록 내용 }' 을 둘 이상의 스레드가 접근하는 것을 막기 위한 선언을 가르켜 동기화 블록 이라고 한다.

 

동기화 메서드 : 특정 메서드 전체의 두 개이상의 스레드가 접근하는 것을 막기 위한 선언을 동기화 메서드라고 한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
class Counter{
    int count = 0;
    synchronized public void increment(){
        count++;
    }
    synchronized public void decrement(){
        count--;
    }
    public int getCount(){
        return count;
    }
}
 
 
 

 

메서드에서 synchronized를 선언하면 동기화 메서드가 된다.

 

위에 예제에서 int 형 count 변수를 두 개의 동기화 메서드가 사용중이다.

메서드는 여러 스레드의 접근을 막았지만 하나의 변수를 사용하기 때문에 문제가 발생할 수 있다.

count 변수는 동기화 선언을 하지 않았기 때문에 메서드에서 계속 불러 사용할 수 있어 값의 변경이 뒤죽박죽이 되어 버린다.

 

그럼 동기화 메서드가 하나씩 실행 되게 하려면 어떻게 해야할까?

자바에서는 클래스 안에 선언된 동기화 메서드가 여러 개라도 하나의 동기화 메서드가 실행될 때 다른 동기화 메서드들은 대기한다.

 

만약 increment 메서드와 decrement메서드의 코드의 길이가 200~300처럼 매우 길고 동기화 코드는 단 한줄이라고 생각해볼 때 하나의 동기화 메서드가 끝날 때까지 대기하는 건 매우 비효율적일 것이다.

이런 경우에 동기화 블록을 사용한다.

 

동기화 블록 사용 예제

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Counter{
    int count = 0;
    public void increment(){
    ...
        synchronized(this){ //동기화 블럭
            count++;
        }   
    ...
    }
    public void decrement(){
    ...
        synchronized(this){ //동기화 블럭
            count--;
        }   
    ...
    }
    public int getCount(){
        return count;
    }
}
 
 
 

 

동기화 블럭에서의 this는 객체의 참조변수로 이때 참조벼수는 락을 걸고자 하는 객체를 참조하는 것이여야 한다.

위의 예제에서는 Counter 인스턴스가 this가 된다.

 

스레드 풀 모델

 

 

 

 

스레드 풀(Thread Pool) : 스레드의 저장소, 생성된 스레드를 저장해 놓는다.

스레드의 생성 소멸은 리소스 소모가 많은 작업으로 스레드 풀은 스레드의 재활용을 위한 모델이다.

 

스레드 풀 기반의 예제1

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ThreadTest {
    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));
     };
     ExecutorService exr = Executors.newSingleThreadExecutor();
     exr.submit(task);  //스레드 풀에 작업을 전달한다.
     System.out.println("End "+ Thread.currentThread().getName());
     exr.shutdown();    //스레드 풀과 그 안에 있는 스레드의 소멸
    }
}
 
 
 

 

newSingleThreadExecutor() 메서드 호출로 하나의 싱글 스레드 저장소를 만들어 준다.

submit() 메서드에 인자로 작업할 내용을 전달해주면 하나의 스레드는 그 작업을 처리해준다.

다 실행이 되고나면 스레드가 소멸 되는 것이 아니라 스레드 저장소로 돌아가기 때문에 스레드를 소멸하고 싶다면 shutdown() 메서드를 호출해야한다.

 

스레드의 작업이 다 완료되기 전에 shutdown() 메서드를 호출해도 바로 소멸하는 것이 아니라 모든 작업이 끝난 뒤에 스레드는 소멸한다.

 

스레드 풀의 유형

  • newSingleThreadExecutor
    • 풀 안에 하나의 스레드만 생성하고 유지한다.
  • newFixedThreadPool
    • 풀 안에 인자로 전달된 수의 스레드를 생성하고 유지한다.
    • 인자로 3을 전달하면 3개의 스레드가 생성되고 대기한다.
  • newCachedThreadPool
    • 풀 안의 스레드의 수를 작업의 수에 맞게 유동적으로 관리한다.

 

스레드 풀 기반의 예제2

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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();    //스레드 풀과 그 안에 있는 스레드의 소멸
    }
 
 
 

실행결과

pool-1-thread-1 : 12

pool-1-thread-2 : 2

pool-1-thread-1 : 35

 

newFixedThreadPool(2) 메서드를 호출해 두 개의 스레드를 생성했다.

task1과 task2를 각각 thread-1과 thread-2가 처리하였고 마지막 람다식을 thread-1이 실행한 것을 알 수 있다.

결국 스레드 풀을 생성하고 스레드를 저장시켜 놓으면 스레드를 다시 재활용 한다는 것을 알 수있다.

 

Callable Future

Runnable 인터페이스의 run() 메서드는 반환형이 void 이기 때문에 반환 받을 수 없다.

스레드를 사용하다 보면 스레드의 결과를 반환 받아야 할 경우가 생긴다.

이럴 경우 Callble 인터페이스를 이용해 call() 메서드로 반환 받을 수 있다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 public static void main(String[] args) throws InterruptedException, ExecutionException{
        
        Callable<Integer> task = ()->// Integer형
            int sum = 0;
            for(int i = 0; i <10 ; i++)
                sum +=i;
                return sum; //값 반환
        };
 
        ExecutorService exr = Executors.newSingleThreadExecutor();
        Future<Integer> fur = exr.submit(task);
        Integer r = fur.get();  //get()메서드 호출로 스레드의 반환 값
        System.out.println("result : "+r);
        exr.shutdown();
    }
 
 

 

Synchronized를 대신하는 ReentrantLock

1
2
3
4
5
6
7
8
class MyClass {
    ReentrantLock criticObj = new ReentrantLock();
    void myMethod(int arg) {
       criticObj.lock() ;    // 문을 잠근다.
       .... // 한 쓰레드에 의해서만 실행되는 영역
       criticObj.unlock();    // 문을 연다.
    }
 }
 
 

 

위에 예제 코드보다는 try~catch 구분을 이용해서 아래 코드처럼 사용하는 것이 권고된다

 

1
2
3
4
5
6
7
8
9
10
11
class MyClass {
    ReentrantLock criticObj = new ReentrantLock();
    void myMethod(int arg) {
       criticObj.lock() ;    // 문을 잠근다.
       try {
          ....    // 한 쓰레드에 의해서만 실행되는 영역
       } finally {
          criticObj.unlock();    // 문을 연다.
       }
    }
 }
 

 

lock() 메서드를호출한 부분에서 부터 unlock() 메서드를 호출하는 시점까지의 영역은 하나의 쓰레드에 의해서만 실행된다.(동기화 블록을 사용한 것과 같은 의미)

 

컬렉션 인스턴스 동기화

컬렉션 인스턴스도 두 개 이상의 스레드가 접근할 경우 문제가 생길 수 있다.

컬렉션 인스턴스는 하나의 스레드가 접근한다는 것을 가정하고 만들어 졌기 때문이다.

스레드를 사용하기 위해서는 동기화 처리를 해서 스레드가 접근할 수 있도록 해야한다.

컬렉션 인스턴스 동기화의 예

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class SyncArrayList {
    public static List<Integer> lst = Collections.synchronizedList(new ArrayList<Integer>());
    //컬렉션 인스턴스 동기화 생성
 
    public static void main(String[] args) throws InterruptedException {
       for(int i = 0; i < 16; i++
          lst.add(i); //0에서 15까지 ArrayList에 값을 넣는다.
       System.out.println(lst);
    
       Runnable task = () -> {
          ListIterator<Integer> itr = lst.listIterator(); //반복자 생성
          while(itr.hasNext())
             itr.set(itr.next() + 1);
       };
       
       ExecutorService exr = Executors.newFixedThreadPool(3);
       exr.submit(task);
       exr.submit(task);
       exr.submit(task);
       
       exr.shutdown();
       exr.awaitTermination(100, TimeUnit.SECONDS); //100초동안 기다리겠다.
       System.out.println(lst);
    }
 }
 

 

 

List형 컬렉션 인스턴스를 생성한 뒤 main() 메서드에서 0~15까지 값을 넣고 있다.

그 다음으로 Runnable 인터페이스에서 반복자를 통해 0~15의 값을 하나씩 꺼내서 +1시키고 다시 집어 넣고 있다.

 

스레드 풀을 생성한 뒤 3개의 스레드를 만들고 각각의 스레드에 task를 실행 시킨다.

스레드를 3번 실행 시키기 때문에 0은3이 될 것이고 1은 4가… 15는 18이 될것이라고 예측을 할 수 있는데 제대로 값이 증가 되지 않았다.

 

왜 제대로 값이 증가 되지 않았을까?

그 이유는 lst라는 참조 변수로 List형 컬렉션 인스턴스를 참조하고 있는 접근 방법에만 동기화가 되어 있고 인스턴스에 접근하는 것은 반복자이기 때문이다.

반복자도 하나의 인스턴스이기 때문에 반복자로 접근하면 동기화가 되지 않는다. 그렇기 때문에 반복자 자체도 동기화를 시켜 주어야 한다.

 

1
2
3
4
5
6
7
Runnable task = () -> {
    synchronized(lst) {
       ListIterator<Integer> itr = lst.listIterator();
       while(itr.hasNext())
          itr.set(itr.next() + 1);
    }
 };
 

반복자를 이용해서 List에 접근할 경우에는 synchroized로 동기화 블럭을 이용해야 반복자도 동기화로 사용할 수 있다.

만약 반복자를 사용하지 않고 lst 참조변수만을 이용해 사용할 경우에는 동기화 블럭을 이용하지 않아도 된다.

 

 


윤성우의 열혈 JAVA - https://cafe.naver.com/cstudyjava?iframe_url=/MyCafeIntro.nhn%3Fclubid=19799898