일단 해보는 코딩/Java

[Java] Thread 종류와 Thread 동기화 하는방법

eun_zoey2 2022. 8. 1. 15:47
728x90
Thread (스레드) 란?

    Thread는 독립적인 프로세스 실행 단위이다. Thread는 우리가 한글 문서를 작성하면서 프린트로 인쇄를 동시에 할 수 있는 것이나 인터넷을 하면서 음악을 듣는 것처럼 한 번에 두 가지 이상의 프로세스를 실행 가능하게 해 준다. 하지만 실제로 동시에 두 개가 실행되는 것은 아니고, 운영체제 내부에서 CPU의 프로세스를 쪼개서 각각을 Time Slot에 넣고 번갈아 실행하는 것이다.  동시에 돌아가는 것처럼 보이도록 아주 빠르게 Thread가 실 행되는 것인데 사람은 느끼지 못할 뿐이다.

 

Thread 종류

 

    1. Single Thread

      - 하나의 프로세스에서 오직 하나의 스레드로만 실행하기 때문에 하나의 레지스터와 스택으로 표현이 가능하다.

더보기
package Java11;

class ThreadEx extends Thread { // nested Thread class를 inherited( 내장된 쓰레드클래스를 상속)
	public void run() {
		for (int i=0; i<=10; i++) {
			System.out.println("Thread activation");
		}
	}
}
public class Single_Thread {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		ThreadEx t = new ThreadEx();
		t.run();	// t.start()를 사용해서 병렬 쓰레드를 실행시킬 수 있다. 
		System.out.println("End of Main class");
	}

}

    2. Multi Thread

       - CPU의 최대 활용을 위해 프로그램의 둘 이상을 동시에 실행하는 기술이다.

더보기
package Java11;

class Thread_mul1 extends Thread{
	@Override
	public void run() {
		for (int i=0; i<=10; i++) {
			System.out.print("1");
			try {
			Thread.sleep(1);
		}	catch (InterruptedException e) {
			e.printStackTrace();
			}
		}
	}
}
class Thread_mul2 extends Thread{
	@Override
	public void run() {
		for (int i=0; i<=10; i++) {
			System.out.print("2");
			try {
			Thread.sleep(2);
		}	catch (InterruptedException e) {
			e.printStackTrace();
			}
		}
	}
}
public class Multi_Thread {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		Thread_mul1 mt1 = new Thread_mul1();
		Thread_mul2 mt2 = new Thread_mul2();
		mt1.start();	//object.start() will start multi_thread
		mt2.start();	//object.run() will start single_thread

	}

}

 

    3. Runnable Interface Thread

      - 재사용성이 높고,  (일단 Thread 클래스를 상속받으면 다른 클래스를 상속받기 어렵기 때문에 여러 클래스를 쉽게 상속받을 수 있는 인터페이스가 좋을 수 있다.) Runnable 코드의 일관성을 유지할 수 있어서 일부 수행에서는 Thread 보다 더 효율적일 수 있다. 

더보기

run( ), start( ) 사용

 

Thread 클래스(java,lang, Thread) 에는 많은 메서드가 있는데 그중에서 단일 스레드는 run( )을 사용하고 , 멀티 스레드는 start( )를 오버라이드 해서 사용할 수 있는데 start( )를 사용하면 내부적으로 run( )도 실행된다. 멀티 스레드를 처리할 때에는 시차를 주기 위해서 object.sleep(sec); 을 주어서 두 스레드 사이에 격차를 두고 실행시키기도 한다.

=> Thread 클래스와 동일한 개념으로 사용될 수 있는 것으로 Runnable 인터페이스를 사용할 수 있다.  

class Thread_Ex extends Thread {... } =>  class Runnable_Ex implements Runnable {... } 식으로 변경

이 Runnable 인터페이스에서는 단일 쓰레드만 빠르게 취급하므로 run( ) 하나만 사용하면 된다.  start( )를 사용한다면 객체를 생성해서 사용하면 된다. 

 

run( )을 호출하는 것은 생성된 쓰레드를 실행하는 것이 아니라 클래스의 메서드를 실행하는 것이지만, start()를 사용하면 실행에 필요한 스택(stack:메모리)을 확보한 뒤 run( )을 각각 호출해서 사용하기 때문에  멀티스레드가 가능한 것이다.

 

예제 1) 

 

 

package Java11;

class Thread_Ex2 implements Runnable {	// class Thread_Ex2 extends Thread { 식으로했음.
	// 하단에서 run ( ) 메서드를 실행하라는 경고
	int temp[];
	public Thread_Ex2() {
		temp = new int[10];
		for (int i=0; i<temp.length; i++) {
			temp[i] = i;
	}
}	
	@Override
	public void run() {	// 이 부분을 기술하면 상단의 경고가 사라진다.
	for (int i=0; i<temp.length; i++) {
		try {
		Thread.sleep(1000);
		} catch (Exception e) {
			e.printStackTrace();
			}
		System.out.println("temp : " + temp[i]);
		}	
	}
}
public class Test05 {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		Thread_Ex2 te1= new Thread_Ex2();
		Thread te2 = new Thread(te1);	// Thread_Ex2 te2 = new Thread_Ex2();
		te2.start();
	}
	
}

 

예제 2)

 

package Java11;

public class Test06 implements Runnable{

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		System.out.println("Starting main class");
		Test06 tt1 = new Test06();	// Test06 클래스에서 객체 생성
		Thread tt2 = new Thread(tt1);	// Thread 객체를 넣어서 새로은 객체 생성
		tt2.start();	// 내부적으로 run( ) 호출
		System.out.println("Ending main class");
	}
	public void run() {
		System.out.println("run()");
		first();	// 메서드 호출
	}
	public void first() {
		System.out.println("first()");
		second();	// 메서드 호출
	}
	public void second() {
		System.out.println("second()");
	}
}

 4. Daemon Thread

      - 다른 일반 쓰레드의 작업을 돕는 보조하는 역할을 수행한다. 함께 구동 중인 일반 Thread가 종료되면 DaemonThread도 함께 종료된다. 예를 들어 문서를 작성하는 도중에 3초 간격으로 자동 세이브가 필요한 경우 이런 Daemon Thread를 사용할 수 있다. 

더보기
package Java12;

public class Test01 implements Runnable {  // ~ extends Thread {, 추상 메써드 필요
	static  boolean autoSave = false;   // 불리언 변수 생성과 초기값 설정
	public static void main(String[] args) {
		Test01 dm = new Test01(); 
		Thread th = new Thread(dm); 
	    th.setDaemon(true);				// 추상 메써드 
	    th.start();                    // 내부적으로 run() 자동 실행
		for (int i=1; i<=15; i++) {
			try { 
			Thread.sleep(1000);
		    } catch (Exception e) {
//		    	e.printStackTrace();
		    }
	    System.out.println();
	    if (i==3)  					   // 3초 뒤에 자동 세이브
	      autoSave = true;
		}
		System.out.println("프로그램 종료");
}
		public void run() {
			while (true) {
				try {
				Thread.sleep(3000);
			    } catch (Exception e) {
			      }
			    if (autoSave == true)
			   	    System.out.println("자동 저장됩니다");
			}
	}
}

    5. Join Thread Join 

      - 하나의 Thread가 끝난 뒤 다시 이어서 (새로운 | 기존 계속) 작업하는 것이 Join이다. 

더보기

예제 1)

 

package Java12;

public class Test02 implements Runnable {

	public static void main(String[] args) {
		System.out.println("main class start");
		Test02 tj = new Test02();
		Thread th = new Thread(tj);
//		Runnable th1 = new Test02();
		th.start(); 	// 추상 메서드 들어감 
		try {
			th.join();	// 추상 메서드 
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}	
		System.out.println("main class close");
	}
	@Override
	public void run () {
		System.out.println("run()");
		first();
	}
	public void first() {
		System.out.println("first()");
		second();
	}
	public void second() {
		System.out.println("second()");
	}
}

 

Q1 ) 키보드에서 숫자를 입력받고 쓰레드에서 입력받은 숫자가 1씩 감소하다가 0이 되었을 때 "종료" 라는 메세지와 함께 쓰레드를 빠져나오게 하시오.

 

package Java12;

import java.util.Scanner;

public class Q1 implements Runnable {
	private int n;
	public Q1(int n) {
		this.n = n;
	}
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		System.out.print("정수를 입력하세요 : ");
		Scanner scan = new Scanner(System.in);
		Q1 tc = new Q1(scan.nextInt());
		Thread th = new Thread(tc);
	    th.start();
	}
	public void run() {
		for(int i=n; i>=0; i--) {
			System.out.println(i);
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		System.out.println("프로그램 종료");
	}
}

 

Thread Synchronized (스레드 동기화)란?

    휴대폰으로 음악이나 영상을 동기화 하게 되면, 동기화가 끝날 때까지 휴대폰은 다른 직업을 할 수 없는데 이런 구조가 바로 동기롸 쓰레드의 구조이다. 두 개 이상의 쓰레드가 하나의 자원을 공유할 경우 동기화 문제가 발생된다. 변수는 하나인데 두 개의 쓰레드가 동시에 한 변수의 값을 변경하려고 하면 오류가 발생한다. 이를 막기 위해서 내가 컴퓨터로 작업을 하다가 잠시 자리를 비운 사이 내 작업이 끝날 때까지 다른 사람이 손대지 못하도록 컴퓨터를 잠가둘 필요가 있다. 

이처럼 특정 스레드들이 공유하는 한개의 자원이 사용 중일 때, 현재 자원을 사용하지 않는 다른 스레드가 작업을 수행하지 못하게 동기화가 필요하다. 

 

Thread 동기화 방법

 

    1. synchronized

1. 메서드 전체를 임계 영역으로 지정
public synchronized void calcSum() {
	...
	}
2. 특정한 영역을 임계 영역으로 지정
synchronized (객체의 참조변수) {
	...
	}

 

    2. Setter와 Getter

    객체 지향 프로그래밍에서 객체의 데이터는 객체 외부에서 직접적으로 접근하는 것을 막는데 객체 데이터를 외부에서 읽고 변경하면 객체의 무결성(ie, 자동차의 속도가 -)이 깨질 수 있기 때문이다. 따라서 객체지향 프로그래밍에서는 메써드를 통해서 데이터를 변경하는 방법을 선호한다. 데이터는 외부에서 접근하지 못하게 하고 메써드는 공개해서 외부에서 메써드를 통하게 해서 데이터에 접근시킨다. 이런 메써드가 Setter이다.
또한 객체 외부에서 객체 필드 값을 사용하지 못하게 할 수도 있는데 이럴 때에는 메써드로 필드 값을 가공해서 외부로 전달하는데 이렇게 해서 값을 가져오는 메써드가 Getter이다.
=>클래스를 선언할 때 가능하면 필드 멤버들을 private로 선언해서 외부로부터 보호하고, 필드에 대한 Setter/Getter 메써드를 작성해서 필드 값을 안전하게 설정(변경)/사용하는 것이 좋다.

더보기

예제 1)

 

package Java13;

class SyncEx implements Runnable {
	private long money = 10000; // 잔액

	@Override
	public void run() {
		synchronized (SyncEx.class) { // SyncEx.class는 this.SyncEx의 의미
			// synchronized 키워드를 사용하면 해당 키워드가 명시되어 있는 영역의 
            //처리가 마무리 될 때 까지 다른 쓰레드에서 접근하지 못하게 된다.
			for (int i = 1; i < 10; i++) {
				try {
					Thread.sleep(500);
				} catch (Exception e) {
					e.printStackTrace();
				}
				if (getMoney() <= 0)
					break;
				outMoney(1000);
			}
		}
	}

	public long getMoney() { // Getter() 메써드 설정
		return money;
	}

	public void setMoney(long money) { // Setter() 메써드 설정
		this.money = money;
	}

	public void outMoney(long howMuch) {
		String threadName = Thread.currentThread().getName();
		if (getMoney() > 0) { // 잔액이 있다면
			money -= howMuch; // 출금액
			System.out.println(threadName + " - 잔액 : " + getMoney() + "원");
		} else {
			if (getMoney() == 0) {
				System.out.println(threadName + " 잔액이 없습니다.");

			}
		}
	}
}

public class Test01 {
	public static void main(String[] args) {

		SyncEx atm = new SyncEx();
		Thread mom = new Thread(atm, "엄마");
		Thread son = new Thread(atm, "아들");
		mom.start();
		son.start();
		// start()를 실행하면 내부적으로 run()이 실행되므로
	}
}

 

    3. wait(  )과 notify(  )

    현재 실행중인 쓰레드가 진행 중에 wait() 메써드를 만나면 일시적으로 정지되어 해당 쓰레드가 일시적으로 대기상태로 보내지고 제어권을 다른 쓰레드에게 넘긴다. 
그리고 wait()을 만나서 대기상태에 빠진 쓰레드는 notify() 메써드를 만나면 재 구동된다. 이런 기법을 이용하면 두 개 이상의 쓰레드가 구동 중일 때 한 개의 동기화 쓰레드가 작업을 진행하면 이 작업을 완전히 마칠 때까지 기다렸다가 다른 쓰레드의 작업이 수행되는 것이 아니라, 하나의 쓰레드 동기화가 진행 중일 때에도 일시적으로 해당 쓰레드를 정지시키고 다른 쓰레드가 작업을 할 수 있게 만들 수 있다.

더보기

예제 1)

package Java13;

import java.util.Random;
import java.util.Scanner;

class Account{
	int balance = 1000; // 잔액
	public synchronized void withdraw(int money) {
		if(balance < money ) {
			try {
				wait();  // 현재 쓰레드 일시 정지 => 대기 상태로 감
			} catch (InterruptedException e) {
			}
		}
		balance -= money;S
	}
	public synchronized void deposit(int money) {
		balance += money;
		notify();  // 정지된 쓰레드 재실행
		
	}
}
class AccountThread implements Runnable{
	Account acc;  // Account 객체 acc준비
	public AccountThread(Account acc) {  //생성자 정의
		this.acc = acc;
	}
	@Override
	public void run() {
		while (true) {
			try {
				Thread.sleep(500);
			} catch (InterruptedException e) {
			}
			int money = (new Random().nextInt(3)+1) * 100; 
			// 랜덤함수는 0~1사이의 값을 반환하기 때문에 *100 해서 정수화 해줌 
		acc.withdraw(money);
		System.out.println("잔액 : "+acc.balance);
		}
		
	}
}
public class Test02 {

	public static void main(String[] args) {
		Account acc =new Account();
		Runnable rr = new AccountThread(acc);
		Thread td = new Thread(rr);
		td.start();
		while(true) {
			Scanner scan = new Scanner(System.in);
			int n = scan.nextInt();
			acc.deposit(n);
			scan.close();
		}
	}

}