자바 - 스레드공부..

지환·2023년 10월 21일
0

JAVA

목록 보기
35/39

스레드란?

  • 사전적 의미로 한 가닥의 실이다. 한 가지 작업을 실행하기 위해 순차적으로 실행할 코드를 실처럼 이어놓았다고 해서 유래된 이름이다.

  • 멀티프로세스는 운영체제에서 할당받은 자신의 메모리를 가지고 실행하기 때문에 각 프로세스는 서로 독립적이다.

  • 멀티스레드는 하나의 프로세스 내부에 생성되기 때문에 하나의 스레드가 예외를 발생시키면 프로세스 자체가 종료될 수 있어 다른 스레드에 영향을 미친다.

Runnable

Thread thread = new Thread(Runnable target);
  • Runnable은 작업 스레드가 실행 할 수 있는 코드를 가지고 있는 객체라고 붙어진 이름이다.

  • Runnable은 인터페이스 타입 이기 떄문에 구현 객체를 만들어 대입해야한다.

  • Runnable은 run()메소드 하나가 정의되어 있는데 구현 클래스는 run()을 재정의해서 작업 스레드가 실행할 코드를 작성해야한다.

class Task implements Runnable
{
	public void run()
    {
     ----실행할 코드----
    }

}
  • Runnable은 작업 내용을 가지고 있는 객체이지 스레드가 아니다. Runnable 구현 객체를 생성 후 -> 이것을 매개값으로 하여 Thread 생성자를 호출해야 비로소 작업 스레드가 생성된다.

(1)

Runnable task = new Runnable();
Thread thread = new Thread(task);

(2) 스레드 생성 시 익명 객체를 사용하는 법

Thread thread = new Thread(new Runnable(){
	public void run(){
    
    }

}});
  • 이 방법을 더 많이 사용한다. 작업 스레드는 생성되는 즉시 실행되는 것이 아니라, start() 메소드를 다음과 같이 호출해야 실행된다.
thread.start();

start()가 호출되면, 작업 스레드는 매개값으로 받은 Runnable의 run() 메소드를 실행하면서 자신의 작업을 처리한다.

package com.ABCDEFG;

import java.awt.Toolkit;

class BeepTask1 implements Runnable
{

	@Override
	public void run() {
		Toolkit toolkit = Toolkit.getDefaultToolkit();
		for(int i = 0; i < 5; i++)
		{
			toolkit.beep();
			try {Thread.sleep(500);} catch(Exception e){}
		}
	}
	
	}

public class BeepTask {
	public static void main(String[] args) {
		Runnable beepTask = new BeepTask1();
		Thread thread = new Thread(beepTask); 
		thread.start();//여기서 스레드가 시작되어 BeepTask1 클래스로 이동 후 구현체 메소드인 run을 실행한다.
		
		for(int i = 0; i < 5; i++)
		{
			System.out.println("띵");
			try {thread.sleep((500);}
			catch(Exception e) {}
			
		}
	}

}
------------------------------
띵
띵
띵
띵
띵

Thread 하위 클래스로부터 생성

  • 작업스레드가 실행할 작업을 Runnable로 만들지 않고 Thread의 하위 클래스로 작업 스레드를 정의하면서 작업 내용을 포함 시킬 수 있다
  • Thread 클래스를 상속 하고 run() 메소드 재정의 하여 스레드가 실행 할 코드를 작성한다.

아까와 같이 thread.start()를 하게 되면 해당 오버라이딩 한 부분에서 메소드를 실행한다.

BeepThread.java

package com.ABCDEFG;

import java.awt.Toolkit;

public class BeepThread extends Thread{
	public static void main(String[] args) {
		
	}

	@Override
	public void run() {
		Toolkit toolkit = Toolkit.getDefaultToolkit();
		for(int i = 0; i<5; i++)
		{
			toolkit.beep();
			try
			{
				Thread.sleep(500);
			}
			catch(Exception e) {}
		}
	}

}

BeepMain

package com.ABCDEFG;

public class BeepMain {
	public static void main(String[] args) {
		
		Thread thread = new BeepThread();
		// 해당 부분은 업케이스팅 된 부분이다. 그렇게 되면 thread는 BeepThread 사용 x || 부모의 메소드만 사용가능
		// 만약에 사용하려면 다운캐스팅을 진행해야한다.
		thread.start(); // 여기서 run 부분이 실행된다.
		for(int i = 0; i<5; i++)
		{
			System.out.println("띵");
			try {thread.sleep(500);}
			catch(Exception e) {}
		}
	}

}

스레드의 이름을 알고싶다. 메인 스레드는 main이라는 이름을 갖고 있고 우리가 직접 생성한 스레드는 자동적으로 Thread-n 이라는 이름으로 설정한다.

thread.setName("스레드 이름");

스레드 이름을 알고 싶은 경우에는 getName() 메소드를 호출한다.

thread.getName();

이 두개 다 Thread의 인스턴스 메소드이므로 스레드 객체의 참조가 필요하다. 만약 스레드 객체의 참조를 가지고 있지 않다면 Thread 클래스의 정적 메소드인 currentThread() 를 이용해서 현재 스레드를 참조하자.

공유객체 주의사항

User2

package com.ABCDEFG;

public class User2 extends Thread{
	
	private Calculator calculator;

	public Calculator getCalculator() {
		return calculator;
	}

	public void setCalculator(Calculator calculator) {
		this.setName("스레드 이름을 User2로 설정한다.");
		this.calculator = calculator;
	}

	public void run() {
		calculator.setMemory(50);
	}
	
	
	
	

}

User1

package com.ABCDEFG;

public class User1 extends Thread{

	private Calculator calculator;

	public Calculator getCalculator() {
		return calculator;
	}

	public void setCalculator(Calculator calculator) {
		this.setName("User1"); // 스레드 이름을 user1으로 설정
		this.calculator = calculator; 
	}
	
	public void run() {
		calculator.setMemory(100); // 공유객체인 calculator의 메모리에 100을 저장한다.
	}
	
	
}

Calculator

package com.ABCDEFG;

public class Calculator {
	private int memory;

	public int getMemory() {
		return memory;
	}

	public void setMemory(int memory) {
		this.memory = memory;
		
		try {
			Thread.sleep(2000);
		}
		catch(InterruptedException e) {}
		System.out.println(Thread.currentThread().getName() + " :" + this.memory);
	}
	

	
}

MainThreadExample

package com.ABCDEFG;

public class MainThreadExample {

	public static void main(String[] args) {
		Calculator calculator = new Calculator();
		
		User1 user1 = new User1(); // User1의 스레드 생성
		user1.setCalculator(calculator); //User1의 공유 객체 설정
		user1.start(); //user1의 스레드 시작
		
		User2 user2 = new User2();
		user2.setCalculator(calculator);
		user2.start(); // user2의 스레드 시작 
	}
}
---------------------------------------
스레드 이름을 User2로 설정한다. :50
User1 :50
  • 사람 B가 계산기로 작업을 하다가 계산 결과에 메모리에 저장한 뒤 사람 C가 계산기를 만져서 A가 메모리에 저장한 값을 다른 값으로 변경하는 것과 동일하다.

  • A는 기존의 저장된 값을 사용할 수 없고, 엉터리 값을 사용한다.

  • User1 스레드가 Calculator 객체의 memory 필드에 먼저 100을 저장하고 2초간 일시정지 상태가 된다.

  • 그 동안에 User2 스레드가 memory 필드값을 50으로 변경한다. 2초 지나 User1 스레드가 다시 실행 상태가 되어 memory 필드값을 출력하면, User2 스레드가 저장한 50이 출력된다.

  • 해당 부분을 해결하기 위해 동기화 메소드를 제공한다.
    멀티 스레드 영역에서 단 하나의 스레드만 실행할 수 있는 코드 영역을 임계영역이라고 한다. 자바는 이 임계영역을 저장하기 위해서 동기화 메소드를 사용한다.

(키워드 : Synchronized void method, 임계영역. )

package com.ABCDEFG;

public class Calculator {
	private int memory;

	public int getMemory() {
		return memory;
	}

	public Synchronized void setMemory(int memory) {
		this.memory = memory;
		
		try {
			Thread.sleep(2000);
		}
		catch(InterruptedException e) {}
		System.out.println(Thread.currentThread().getName() + " :" + this.memory);
	}
	

	
}
  • 내부의 동기화 메소드를 실행하면, 즉시 객체에 잠금을 걸어 다른 스레드가 동기화 메소드를 실행하지 못하도록 한다.

  • 만약 동기화가 여러개 있다면, 스레드가 이들 중 하나를 실행 했을 떄 다른 스레드는 해당 메소드는 물론이고, 다른 동기화 메소드도 실행할 수 없다. 하지만 이 때 다른 스레드에서 일반 메소드는 실행이 가능하다.

  • 동기화 메소드를 만들려면 다음과 같이 메소드 선언에 Synchronized 키워드를 붙이면 된다.

  • 위 코드처럼 앞에 키워드를 붙이면 User1 스레드는 Calculator 객체의 동기화 메소드인 setMemory()를 실행하는 순간 객체의 잠금을 처리한다.

  • 메인스레드에선 User2 스레드를 실행하지만 동기화 메소드인 setMemory()를 실행하지 못하고 User1이 setMemory()를 모두 실행할 동안 대기해야한다.

  • User1스레드가 setMemory()을 실행하고 나면 User2 스레드가 setMemory()을 실행한다.

스레드 제어

이미지 출처

스레드 객체를 생성하고 start() 메소드를 호출하면 바로 실행 되는 것이 아니라 실행 대기 상태 가 된다.

실행 대기 상태란 ?

언제든지 실행할 준비가 되어 있는 상태를 말한다.

실행상태

실행 대기 상태에 있는 스레드 중에서 운영체제는 하나의 스레드를 선택하고 CPU가 run()메소드를 실행하도록 한다.

실행 상태의 스레드는 run() 메소드를 모두 실행하기 전에 다시 실행 대기 상태로 돌아갈 수 있으며, 실행 대기 상태에 있는 다른 스레드가 선택되어 실행 상태가 된다.

실행 상태에서 run() 메소드의 내용이 모두 실행되면 스레드의 실행이 멈추고 종료 상태가 된다.

종료상태

실행 상태에서 run()메소드가 종료되면, 더 이상 실행할 코드가 없기 때문에 스레드의 일행은 멈추게 된다.

  • interrupt() : 일시 정지 상태의 스레드에 InterruptedException 예외를 발생시켜, 예외 처리 코드(catch)에서 실행 대기 상태로 가거나 종료 상태로 갈 수 있도록 한다.
  • sleep(long millis) , sleep(long millis, int nanos)
    : 주어진 시간 동안 스레드를 일시 정지 상태로 만든다. 주어진 시간이 지나면 자동적으로 실행 대기 상태가 된다.
  • notify() , notifyAll()
    : 동기화 블록 내에서 wait() 메소드에 의해 일시 정지 상태에 있는 스레드를 실행 대기 상태로 만든다.

public class SleepExam {
    
    public static void main(String[] args){
        
        Toolkit toolkit = Toolkit.getDefaultToolkit();
        
        for(int i=0; i<0; i++){
            toolkit.beep();
            
            try{
                Thread.sleep(3000); -- 3초동안 메인스레드를 일시정지 시킴 
            }catch(InterruptedException e){}
            
        }
    }
}

interrupt 예제

InterruptExample.java

package com.ABCDEFG;

public class InterruptExample {

	public static void main(String[] args) {
		Thread thread = new PrintThread2();
		thread.start(); // PrintThread2의 오버라이팅한 run 메소드를 실행한다. 
		
		try {Thread.sleep(1000);}
		catch(InterruptedException e){}
		thread.interrupt(); // 무한 반복 while문을 빠져 나오기 위한 방법 중 하나  스레드를 종료시키는 역할을한다.
	}
}
--------------------------
실행중
실행중
실행중
자원 정리
실행 종료

PrintThread2.java

package com.ABCDEFG;

public class PrintThread2 extends Thread{

	public void run()
	{
		try {
			while(true)
			{
				System.out.println("실행중");
				Thread.sleep(1);
			}
		}
		catch(InterruptedException e) {
			System.out.println("자원 정리");
			System.out.println("실행 종료");
			
		}
	}

}
  • 여기서 중요한 학습목표는 스레드가 실행 대기 또는 실행 상태에 있을 때 interrupt() 메소드가 실행되면 바로 InterruptedException 이 발생되지 않고, 스레드가 미래에 일시정지 상태가 되면 InterruptedException이 발생한다.

  • 다른 말로 하면 thread가 일시 정지 상태가 되지 않으면 interrupt() 메소드 호출은 아무런 의미가 없다.

  • 위 코드는 Thread.sleep(1)을 사용해서 잠시나마 일시정지를 시켰다.

그렇다면 Thread.sleep(1)을 이용하지 않고 일시정지 하는 방법은 없을까? -> 라는 생각을 해야된다.

그 때 사용할 수 있는 메소드는 2가지가 있다. interrupt() 메소드가 호출되었다면, 스레드의 interrupted() ,isInterrupted() 메소드는 true을 반환한다. 이 부분은 자바 API를 참고하면 된다.

  • interrupted() : 정적 메소드로 현재 스레드가 interrupted 되었는지 확인한다.

  • isInterrupted() : 인스턴스 메소드로 현재 스레드가 interrupted되었는지 확인한다.

package com.ABCDEFG;

public class PrintThread2 extends Thread{

	public void run()
	{
			while(true)
			{
				System.out.println("실행중");
				if(Thread.interrupted())
				{
					break;
				}
				
			}

			System.out.println("자원정리");
			System.out.println("실행종료");
			
		}
}


이런 식으로 할 수 있음

기초 체크 [break vs return의 차이]

1. break특징

  • break는 가장 가까이에 있는 반복문을 벗어나기 위해 사용한다.
  • break문이 실행되면 Loop가 전부 끝나지 않아도 해당 반복문을 탈출한다.

2. return문 특징

  1. 쓰여진 해당 함수에서의 탈출을 의미한다.
    -> return문 실행 시 반복문을 포함하는 메소드 자체를 종료시킨다.

  2. 메소드 내에서 return이 실행되면 뒷 줄에 코드가 더 있다고 하더라고 값 반환 후 종료

  3. 메소드의 출력값은 return명령어만 가능하다.

  4. return; 문(반환 값 명시 안하고 바로 세미콜론이 오는 경우) 만을 써서 메소드를 빠져나가는 방법은 리턴 자료형이 void형인 메소드에만 해당한다.
    -> 당연한 얘기지만 리턴 자료형이 명시되어 있는 경우에는 메소드에서 return; 문만 작성하면 컴파일 오류가 발생한다.

profile
아는만큼보인다.

0개의 댓글