자바/자바 입문 공부일지

자바 기초 공부 일지 54. 쓰레드Thread 생성과 쓰레드 동기화 방법

Tomitom 2022. 11. 8. 14:27
반응형

 

 

쓰레드Thread란  프로세스의 작업 단위입니다. 

프로그램 내에서 실행을 흐르는 이루는 최소 단위이며 

main 메소드의 실행도 하나의 쓰레드에 의해 진행이 됩니다. 

(프로세스가 여러가지 행동들을 실행해야 할 때 쓰레드에 할당을 하여 실행이 됩니다. ) 

쓰레드의 존재는 자바 프로그램 안에서도 존재합니다. 

 

class CurrentThreadName {

   public static void main(String[] args) {

      Thread ct = Thread.currentThread();   // 참조할 수 있는 현재 쓰레드 선언 

      String name = ct.getName();    // 쓰레드의 이름을 반환 

      System.out.println(name);

   }

}

 

쓰레드를 생성할 때 메인 쓰레드는 만들지 않아도 생성되어 있습니다.

쓰레드를 추가 생성하는 방법을 알아볼게요. 

 

• 1단계  Runnable을 구현한 인스턴스 생성

• 2단계  Thread 인스턴스 생성

• 3단계  start 메소드 호출

 

 

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("End " + Thread.currentThread().getName());
}    
// Runnable         void run()

 

public static void main(String[] args) {

 

   Runnable task = () -> { // 쓰레드가 실행하게 할 내용, Runnable은 인터페이스입니다. 

// 함수형 인터페이스이므로 메소드가 존재합니다. 

// 하나의 추상메소드가 있고, 그 추상메소드의 역할은 쓰레드에 작업을 전달하는 역할입니다. 

 

      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("End " + Thread.currentThread().getName());

}   

// Runnable         void run()

 

 

모든 쓰레드가 일을 마쳐야 프로그램이 종료되고, 출력하면 상기의 이미지와 같이 결과가 출력됩니다.

 

두 개 이상의 쓰레드를 생성해서 실행하는 과정을 살펴볼게요.

 

	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.print(i + " ");
					Thread.sleep(100);  // 0.1초간 잠(대기)를 하다가 출력한다. 
				}
			}catch(InterruptedException e) {
				e.printStackTrace();
			}
		};

 

 

홀수를 작성하는 쓰레드와 짝수를 작성하는 쓰레드 두 개를 작성했습니다. 

이제 실제로 작업을 할 때에는 생성한 쓰레드를 실행까지 해야 합니다. 

 

Thread t1 = new Thread(task1);
		Thread t2 = new Thread(task2);
		
		t1.start(); 
		t2.start();

	}

}

 

이렇게 start();  를 입력해서 실행을 시키면 다음과 같은 결과 값이 출력 됩니다. 

 

따란..

쓰레드간에는 싱크가 맞지 않을 수도 있습니다. 기본적으로 쓰레드 간에는 동기화가 되어 있지 않습니다. 

다음의 코드는 1000번을 더하고, 1000번을 빼는 작업입니다.

 

package day22;


class Counter {
	   int count = 0;  // 공유되는 변수
	   
	   public void increment() {
	      count++;  // 첫 번째 쓰레드에 의해 실행
	   }
	   public void decrement() {
	      count--;  // 또 다른 쓰레드에 의해 실행
	   }
	   public int getCount() { return count; }
	}


class Test {
	   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(); // 값을 1 증가
	      };
	   
	      Runnable task2 = () -> {
	         for(int i = 0; i < 1000; i++)
	            cnt.decrement(); // 값을 1 감소
	      };
	      Thread t1 = new Thread(task1);
	      Thread t2 = new Thread(task2);
	      t1.start();
	      t2.start();
	      t1.join();   // t1이 참조하는 쓰레드의 종료를 기다림
	      t2.join();   // t
	      2가 참조하는 쓰레드의 종료를 기다림
	      System.out.println(cnt.getCount());
	   }
	}

 

이것을 실행했을 때 우리가 기대하는 결과는 0 이지만 실상은 동기화가 되지 않기 때문에 다른 값이 출력 됩니다. 

쓰레드들은 동시에 각자의 값을 연산하기 시작하기 때문에 완벽한 싱크로가 되지 않기 때문이에요. 

 

이렇듯 둘 이상의 쓰레드가 하나의 메모리에 접근하는 것은 아주 위험합니다. 

이것을 해결하기 위해 동기화를 할 것이지만, 그래도 오류 가능성이 있고, 

동기화를 하지 않아도 될 부분까지 함께 동기화가 되기 때문에 가급적이면 지양하는 것이 좋을 것 같아요. 

 

 

동기화를 하는 방법은 synchronized  를 입력하면 간단하게 해결됩니다. 

우선 동기화 되지 않은 코드를 작성해볼게요. 

아까는 천 번을 연산했다면 이번에는 플러스 마이너스 8000번을 해볼게요.

 

package day22;


class Counter01{
	int count = 0 ;
	
	public void incre() { count++;}
	public void decre() { count--;}
	public int getCnt() {return count;}
}

public class Syncronized01 {

	static Counter01 myCnt = new Counter01();
	public static void main(String[] args)throws Exception	{
	
		Runnable task1 = () -> { 
			for(int i = 0 ; i < 8000; i ++ ) 	{myCnt.incre();}
		};
		Runnable task2 = () -> { 
			for(int i = 0 ; i < 8000; i ++ ) 	{myCnt.decre();}
		};
		
		
		Thread t1 = new Thread(task1);
		Thread t2 = new Thread(task2);
		t1.start(); t2.start(); 
		t1.join(); t2.join();
	
		
		System.out.println(myCnt.count);
		

	}

}

상기 코드를 실행했을 때에는 다음과 같이 출력 결과가 매 순간 달라집니다. 

(동기화 되기 전) 

이제 각각의 쓰레드에 동기화를 할 수 있는 synchronized  를 입력해볼게요. 

 

class Counter01{
	int count = 0 ;
	
	synchronized public void incre() { count++;}
	synchronized public void decre() { count--;}
	public int getCnt() {return count;}
}

몇 번을 실행해도 똑같은 값인 0 이 나옵니다. 

8000 번을 더하고 빼는 연산의 결과 값은 0 이니까요. 

 

 

쓰레드의 생성 및 소멸은 프로세스 작업 중 비교적 부하가 큰 작업에 속합니다.

 


 

지금까지 썼던 쓰레드는 쓰레드를 생성하고 소멸시키는 방법이었기 때문에 부담이 되었다면, 

지금부터 볼 쓰레드 풀 모델은 쓰레드들을 사용한 뒤에 다시 저장하는, 재활용을 위한 모델입니다.

즉, 쓰레드 풀은 쓰레드의 재사용이 가능합니다.  

 

 

출처 : 윤성우의 열혈 자바 프로그래밍 공부집

 

쓰레드 풀의 모든 쓰레드가 작업을 하고 있고, 남아있는 대기 쓰레드가 없다면 남은 작업들은 대기 상태에 놓이게 됩니다. 

 

쓰레드 풀의 형식은 다음과 같습니다. 

 

• newSingleThreadExecutor  

  풀 안에 하나의 쓰레드만 생성하고 유지합니다. 

• newFixedThreadPool  

  풀 안에 인자로 전달된 수의 쓰레드를 생성하고 유지합니다. 

• newCachedThreadPool  

  풀 안의 쓰레드의 수를 작업의 수에 맞게 유동적으로 관리합니다. 

 

아래는 쓰레드 풀의 사용 예제입니다.

 

package day22;

import java.util.concurrent.*;

public class ThreadPool01 {

	public static void main(String[] args) {
		
		Runnable task1 = () -> {
			String tname = Thread.currentThread().getName();
			System.out.println(tname + "\t사과 한 입 먹음. 포만감 " + 100 + "증가..." ) ; 
		};
		
		Runnable task2 = () -> {
			String tname = Thread.currentThread().getName();
			System.out.println(tname + "\t사과 두 입 먹음. 포만감 " + 200 + "증가..." ) ; 
		};
		
		ExecutorService exr = Executors.newFixedThreadPool(2);
		
		exr.submit(task1); 	exr.submit(task2);
		exr.shutdown();

	}

}

어떤 쓰레드가 먼저 시작하고 끝나는지는 고정되어 있지 않습니다. 

반응형