JAVA_스레드 (Thread)

JW__1.7·2022년 8월 18일
0

JAVA 공부일지

목록 보기
28/30

프로세스 (Process)

  • 실행중인 프로그램
  • 완전히 독립적인 수행 단위

스레드 (Thread)

  • 프로세스의 세부 실행 단위
  • 자바 실행의 기본 단위(main 스레드)
  • main 스레드 이외의 스레드 추가 가능
  • 한 프로그램에 여러개의 스레드가 존재할 수 있다. 스레드가 1개라면 단일 스레드, 2개 이상이라면 다중 스레드
  • 프로그램 코드를 1줄씩 실행하는 것이 스레드의 역할(=실행제어)

멀티 스레드

  • 멀티 스레드 = multi thread = 다중 스레드
  • 여러개의 스레드를 이용하는 프로그램
  • 다중 스레드에서 각각의 스레드는 하나의 독립적인 프로세스처럼 작업 수행

스레드 사용 이유

메모리 절약

OS마다 다르지만, 무슨 작업을 수행하려고 할 때 JVM은 32~64MB 메모리를 점유한다.
스레드 경우에는 1MB 이내의 메모리만 점유한다. 그래서 스레드를 경량 프로세스라고 부른다.

프로세스 Context Switching에 비해 오버헤드 절감

멀티 프로세스로 실행되는 작업을 멀티 스레드로 실행하게 되면 프로세스를 생성하여 자원을 할당하는 과정도 줄어들뿐더러 프로세스를 Context Switching에 비해 오버헤드를 줄일 수 있다.

작업들 간 통신 비용 절감

프로세스 간의 통신 비용보다 하나의 프로세스 내에서 여러 스레드 간의 통신 비용이 훨씬 적어서 작업들 간의 통신 부담을 줄일 수 있다.

스레드 생성

  • Thread 클래스 상속 extends Thread
  • Runnable 인터페이스 구현 implements Runnable

Thread 클래스 상속

Thread 클래스 상속을 받거나 extends Thread Runnable 인터페이스를 구현하게 되면, public void run() 메소드를 오버라이드해서 수행할 작업 작성한다.

스레드 실행

  • start() 메소드를 호출 ★
  • start() 메소드를 호출하면 run() 메소드에 오버라이드 한 내용이 실행
  • Thread의 동작은 run() 안에 작성해야 한다.

JAVA의 Process

예제 1

Thread 클래스를 상속 받는다.

Process 자식 클래스

public class Process extends Thread {
	
	private String name;

	public Process(String name) {
		super();
		this.name = name;
	}
	
	@Override
	public void run() {
    	// millies = 1/1000
		try {
		Thread.sleep(3000); // 3초 일시중지 (1000 = 1초)
		System.out.println(name + " 작업 실행");
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}

sleep() 메소드를 사용하면 1/1000초를 기다렸다가 Thread를 실행할 수 있다.
예외처리는 만약 Thread끼리 부딪히거나 겹치게 되면 무한 대기상태가 생길 수 있는데 InterruptedException을 이용하면 그것에 대한 예외처리를 해준다.

Main 클래스

public class Main {

	public static void main(String[] args) {

		System.out.println("main 시작");
		
		Process process1 = new Process("연산");
		process1.start();	// Process 클래스의 오버라이드된 run() 메소드가 호출
		
		Process process2 = new Process("제어");
		process2.start();	// 제어가 먼저 시작할 수도 있다. 시작하세요 하면 바로 시작 안할 수 있다.
							// 스케줄러에 의해서 늦게 시작할 수도 있다.
		
		System.out.println("main 종료");
		// 메인은 프로세스한테 너 시작해 알려주고 내려와서 종료함
		// 스레드는 특정 시작을 알려주면 그 때 시작
	}
}
main 시작
main 종료
// // wait()에 지정해준 대로 3초 뒤에 출력된다.
연산 작업 실행
제어 작업 실행

Thread 클래스 상속받기

Thread 클래스를 상속받게 되면, run() 메소드를 오버라이드하고, 그 안에 Thread의 행동을 명시해야 한다.

Soldier 클래스

public class Soldier extends Thread {

	private String name;
	private Gun gun;
	
	public Soldier(String name, Gun gun) {
		super();
		this.name = name;
		this.gun = gun;
	}
	
	public void shoot() {
		System.out.print("[" + name + "]" + " : ");
		gun.shoot();
	}
	
	@Override
	public void run() {
		
		try {
			// 1초에 한 발씩 쏘기
			while(gun.getBullet() != 0) {
				shoot();
				Thread.sleep(1000);
			}
		// 작업 도중에 가로채기 당해서 대기 상태
		} catch (InterruptedException e) {	// 인터럽트 : 가로채기
			e.printStackTrace();
		}
	}
}

Gun 클래스

public class Gun {

	private int bullet;

	public Gun(int bullet) {
		super();
		this.bullet = bullet;
	}
	
	public void shoot() {
		if(bullet == 0) {
			System.out.println("총알 없음");
			return;
		}
		bullet--;
		System.out.println("탕! " + bullet + "발 남음");
	}

	public int getBullet() {
		return bullet;
	}
	public void setBullet(int bullet) {
		this.bullet = bullet;
	}
}

Main 클래스

public class Main {

	public static void main(String[] args) {

		// 스레드 우선순위
		System.out.println("가장 높은 우선 순위 : " + Thread.MAX_PRIORITY);
		System.out.println("가장 낮은 우선 순위 : " + Thread.MIN_PRIORITY);
		System.out.println("보통 우선 순위 : " + Thread.NORM_PRIORITY);
		
		// 스레드 2개(s1, s2)
		Soldier s1 = new Soldier("김상사", new Gun(6));
		Soldier s2 = new Soldier("장병장", new Gun(10));

		// 각 스레드의 우선 순위
		System.out.println("s1 우선 순위 : " + s1.getPriority());
		System.out.println("s2 우선 순위 : " + s2.getPriority());
		
		// 우선 순위가 높은 스레드를 (최대한) 먼저 실행
		// 우선 순위 조정
		s1.setPriority(Thread.MIN_PRIORITY);	// 가장 낮은 우선 순위
		s2.setPriority(Thread.MAX_PRIORITY);	// 가장 높은 우선 순위
		
		// 스레드 실행
		s1.start();	// 김상사가 먼저 쏘지 않을수도 있다.
		s2.start();	
	}
}

setPriority() 메소드를 이용해서 우선순위를 최대한 조정할 수 있다.

Runnable 인터페이스를 구현하여 Thread 구현하기

WashRobot 클래스

public class WashRobot extends Robot implements Runnable {

	private String name;

	public WashRobot(String name) {
		super();
		this.name = name;
	}
	
	@Override
	public void run() {
		System.out.println(name + " 빨래중");
	}
	
}

Robot 클래스

public class Robot {

}

Main 클래스

public class Main {

	public static void main(String[] args) {

		Runnable robot1 = new WashRobot("로봇1");
		Thread thread1 = new Thread(robot1);
		
		WashRobot robot2 = new WashRobot("로봇2");
		Thread thread2 = new Thread(robot2);
		
		Thread thread3 = new Thread(new WashRobot("로봇3"));
		// A a = new A();
		// B b = new B();
		// B b = new B(new A());
		
		thread1.start();
		thread2.start();
		thread3.start();
	}
}

Runnable 인터페이스를 구현한 클래스는 Thread로 바꿔야 start() 메소드를 호출할 수 있다.

join() 메소드를 이용하여 계산기 출력

Calculator 클래스

public class Calculator implements Runnable {
	
	// 필드
	private long total;	// 초기화 안해도 자동으로 0
	private long begin; 
	private long end;
	
	// 생성자
	public Calculator(long begin, long end) {
		this.begin = begin;
		this.end = end;
	}
	
	public void add() {
		for(long n = begin; n <= end; n++) {
			total += n;
		}
	}

	public long getTotal() {
		return total;
	}
	public void setTotal(long total) {
		this.total = total;
	}
	
	@Override
	public void run() {
		add();
	}
}

Main 클래스

public class Main {

	public static void main(String[] args) {

		// Calculator를 2개 준비
		// 작업을 반으로 나눠서 진행
		
		// Calculator가 동시에 연산을 수행하려면 Calculator를 스레드로 처리해야 함
		
		Calculator calc1 = new Calculator(1, 50000);
		Thread thread1 = new Thread(calc1);
		thread1.start(); 	// 1번째 계산기 동작 시작
		
		Calculator calc2 = new Calculator(50001, 100000);
		Thread thread2 = new Thread(calc2);
		thread2.start();	// 2번째 계산기 동작 시작

total 값이 0이 나오는 이유는 Main() 메소드가 종료되면 바로 결과를 도출하기 때문에 0이 반환된다.

그래서 join() 메소드를 활용하여 Thread의 각각의 작업이 다 끝날 때까지 기다렸다가 출력해야 한다.

		// 모든 계산기의 동작이 끝날때까지 기다린다.
		// join() : 스레드가 종료(die)될 때까지 기다린다.
		try {
			thread1.join();
			thread1.join();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		
		System.out.println(thread1.isAlive());
		System.out.println(thread2.isAlive());
		
		System.out.println(calc1.getTotal() + calc2.getTotal());
	}
}

join() 메소드를 사용하면, 정상적으로 50005000 값을 반환한다.

wait() 메소드와 notify() 메소드 이용하여 화장실/방 청소하기

  • Cleaner 1개 : 공유 자원
  • Cleaner를 사용할 Robot 2개 : 스레드
  • Robot이 Cleaner를 차지하기 위한 쟁탈전이 벌어진다.

synchronized

  • 스레드 충돌 방지하기 위해서 한 번에 한 스레드만 접근할 수 있도록 허용
  • 공유 자원의 일관성을 보장
  • 한 번에 한 스레드만 접근할 수 있는 영역을 임계 영역(Critical Section)이라고 한다.

RoomRobot 클래스

public class RoomRobot extends Thread {

	private Cleaner cleaner;
	
	public RoomRobot(Cleaner cleaner) {
		super();
		this.cleaner = cleaner;
	}

	@Override
	public void run() {
		for(int i = 0; i < 5; i++) {
			cleaner.roomCleaning();
		}
	}
}

ToiletRobot 클래스

public class ToiletRobot extends Thread {

	private Cleaner cleaner;
	
	public ToiletRobot(Cleaner cleaner) {
		super();
		this.cleaner = cleaner;
	}

	@Override
	public void run() {
		for(int i = 0; i < 5; i++) {
			cleaner.toiletCleaning();
		}
	}
}

Robot 클래스

public class Robot {

}

Cleaner 클래스

  • Object 클래스의 notify() 메소드

    • 다른 스레드를 깨움
    • notifyAll() 메소드로 모든 스레드를 깨움
  • Object 클래스의 wait() 메소드

    • 스레드가 대기 상태가 됨
    • 다른 스레드가 꺠울 때까지 대기함
public class Cleaner {

	public synchronized void toiletCleaning() {
		try {
			System.out.println("화장실 청소");
			notify();	// "나 화장실 청소 끝났다"고 알림
			wait(); 	// 잠깐 쉼
		} catch (InterruptedException e) {	// wait() 메소드는 예외처리 필요
			e.printStackTrace();
		}
	}
	
	public synchronized void roomCleaning() {
		try {	
			System.out.println("방 청소");
			notify();	// "나 방 청소 끝났다"고 알림
			wait();		// 잠깐 쉼
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}

Main 클래스

public class Main {

	public static void main(String[] args) {

		// 클리너 1개
		Cleaner cleaner = new Cleaner();
		
		// 로봇 2개(동일한 클리너를 가짐)
		ToiletRobot robot1 = new ToiletRobot(cleaner);
		RoomRobot robot2 = new RoomRobot(cleaner);
		
		// 청소 시작
		robot1.start();
		robot2.start();
	}

}

반환값

화장실 청소
방 청소
화장실 청소
방 청소
화장실 청소
방 청소
화장실 청소
방 청소
화장실 청소
방 청소

0개의 댓글