Java

스레드(1)

hongdangmoo 2022. 3. 14. 21:14

◎ 프로세스 

-> 운영체제에서 실행 중인 하나의 애플리케이션

 

◎ 스레드

-> 프로세스 내부에서 코드의 실행 흐름

-> 한 프로세스 내에 스레드가 2개 존재하면 2개의 코드 실행 흐름이 생긴다.

 

멀티 스레드 vs 멀티 프로세스

-> 멀티 프로세스는 운영체제에서 할당 받은 자신의 메모리를 가지고 실행하기 때문에 각 프로세서는 서로 독립적이다. 따라서 하나의 프로세스에서 오류가 발생해도 다른 프로세스에 영향을 미치지 않는다. 

-> 멀티 스레드는 하나의 프로세스 내부에서 동시에 생성되기 때문에 하나의 스레드에서 예외가 발생하면 다른 스레드에도 영향을 미친다.

☞ 멀티 프로세스는 워드 작업 동시에 메모장을 켜고 작성을 하는 것을 생각하면 되고, 멀티 스레드는 하나의 프로그램에서 채팅 기능과 동시에 파일 전송 기능을 제공하는 메신저를 생각하면 된다.

 

◎ 메인 스레드

-> 자바의 모든 애플리케이션은 메인 스레드가 main()메소드를 실행하면서 시작한다. 메인 스레드는 main()메소드의 첫 줄 부터 아래로 순서대로 실행하고, main()메소드의 마지막 코드를 실행하거나, return문을 만나면 종료된다.

-> 메인 스레드는 작업 스레드들을 만들어 병렬로 코드를 실행하는 멀티 스레드를 생성해서 멀티 태스킹 작업을 수행할 수 있다.

-> 싱글 스레드

-> 멀티 스레드

☞ 멀티 스레드로 실행하는 애플리케이션을 개발하려면 몇 개의 작업을 병렬로 실행할지 결정하고 각 작업별 스레드를 생성한다.

☞ 자바는 작업 스레드도 객체로 생성된다. 따라서 클래스가 필요하다. java.lang.Thread 클래스를 직접 객체화하여 사용하는 것도 가능하고, Thread클래스를 상속해서 하위 클래스를 만들어 생성하는 것도 가능하다.

☞ java.lang.Thread로 작업 스레드 객체를 생성하려면 Thread thread = new Thread(Runnable a);로 Runnable을 매개값으로 하는 생성자를 호출한다.

☞ Runnable은 작업 스레드가 실행할 수 있는 코드를 가지고 있는 객체다. 또한 인터페이스 타입이기 때문에 구현 객체를 만들어 대입한다. Runnable은 run()메소드가 정의되어 있기 때문에 구현 클래스에 run()을 재정의하여 작업 스레드를 작성해야 한다.

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

 

-> 메인 스레드만 이용

import java.awt.Toolkit;

public class ThreadTest1 {
	public static void main(String[] args) {
		Toolkit toolkit = Toolkit.getDefaultToolkit(); // Toolkit객체 얻기
		for(int i = 0; i<4; i++) {
			toolkit.beep();
			try {
				Thread.sleep(1000); // 1초간 일시 정지
			}catch(Exception e) {}
	} 
		
		String[] s = new String[4];
		s[0] = "하나";
		s[1] = "둘";
		s[2] = "셋";
		s[3] = "야";
		
		for(int i = 0; i<4; i++) {
			System.out.println(s[i]);
			try {
				Thread.sleep(1000);
			}catch(Exception e) {}
		}
	}
}

☞ 위 코드는 1초 주기로 비프음을 발생시키면서 동시에 출력 작업을 하는 코드이다. 위와 같이 메인 스레드만 작성하면 비프음이 먼저 나오고, 출력을 한다.

 

-> 결과(영상)

☞ 비프음 발생과 동시에 출력을 하려면 하나의 작업은 메인 스레드가 아닌 작업 스레드에서 실행해야한다.

 

-> 비프음이 나오는 작업

import java.awt.Toolkit;

public class BeepTest implements Runnable{

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

}

-> 메인 스레드와 작업 스레드 동시 실행

public class ThreadTest2 {

	public static void main(String[] args) {
		Runnable beepTest = new BeepTest();
		Thread thread = new Thread(beepTest);
		thread.start();
		
		String[] s = new String[4];
		s[0] = "하나";
		s[1] = "둘";
		s[2] = "셋";
		s[3] = "야";
		
		for(int i = 0; i<4; i++) {
			System.out.println(s[i]);
			try {
				Thread.sleep(1000);
			}catch(Exception e) {}
		}

	}

}

-> 결과(영상)

 

 

◎ Thread 하위 클래스로부터 생성

-> 작업 스레드가 실행할 작업을 Runnable로 만들지 않고, Thread의 하위 클래스로 정의하여 작업할 수 있다.

public class Thread extends Thread{
	@Override
	public void run() {
		실행 코드
	}
}
Thread thread = new Thread();

 

◎ 스레드 이름

-> 스레드는 디버깅할 때 어떤 스레드가 어떤 작업을 하는지 알아볼 목적으로 사용되는 이름이 있다.

-> 메인 스레드는 main이라는 이름을 가지고 있고, 직접 생성한 스레드는 Thread-n이라는 이름으로 생성된다.

-> thread.setName("스레드 이름")으로 이름을 변경할 수 있다.

-> thread.getName();으로 스레드의 이름을 알 수 있다.

-> setName()과 getName()은 Thread클래스의 인스턴스 메소드로 스레드 객체의 참조가 필요하다. 만약 스레드 객체의 참조를 가지고 있지 않다면, Thread클래스의 정적 메소드 currentThread()를 이용해 현재 스레드의 참조를 얻을 수 있다.

☞ Thread thread = Thread.currentThread();

 

-> 예제

public class ThreadName {
	public static void main(String[] args) {
		Thread thread = Thread.currentThread();
		System.out.println("프로그램 시작 스레드 이름 : " + thread.getName());
		
		ThreadA threadA = new ThreadA();
		System.out.println("작업 스레드 이름 : " + threadA.getName());
		threadA.start();
		
		ThreadB threadB = new ThreadB();
		System.out.println("작업 스레드 이름 : " + threadB.getName());
		threadB.start();
	}

}

-> ThreadA 클래스

public class ThreadA extends Thread {
	public ThreadA() {
		setName("ThreadA");
	}
	
	public void run() {
		for(int i = 0; i<2; i++) {
			System.out.println(getName() + "...");
		}
	}

}

-> ThreadB 클래스

public class ThreadB extends Thread {
	public void run() {
		for(int i = 0; i<2; i++) {
			System.out.println(getName() + "...");
		}
	}

}

-> 결과

☞ ThreadA는 setName()으로 ThreadA라는 이름으로 변경하였고, ThreadB는 변경을 하지 않아 기본 이름인 Thread-1로 출력되었다.

 

◎ 공유 객체 사용과 문제점

-> 멀티 스레드 프로그램에서 스레드들이 객체를 공유해서 사용할 때 스레드A가 사용하던 객체를 스레드B가 사용하여 값이 변경될 수 있어 기대와 다른 결과가 나올 수 있다.

-> 공유 객체 사용 예제

public class MainThread {

	public static void main(String[] args) {
		Calculator cal = new Calculator();
		
		UserA userA = new UserA();
		userA.setCal(cal);
		userA.start();
		
		UserB userB = new UserB();
		userB.setCal(cal);
		userB.start();

	}

}

-> 공유 객체

public class Calculator {
	
	private int memory;
	
	public int getMemory() {
		return memory;
	}
	
	public void setMemory(int money) {
		this.memory = money;
		try {
			Thread.sleep(2000);
		}catch(Exception e) {}
		
		System.out.println(Thread.currentThread().getName() + " : " + this.memory);
	}

}

-> UserA 스레드

public class UserA extends Thread{
	
	private Calculator cal;
	
	public void setCal(Calculator cal) {
		this.setName("UserA");
		this.cal = cal;
	}
	
	public void run() {
		cal.setMemory(100);
	}

	
}

-> UserB 스레드

public class UserB extends Thread{
	
	private Calculator cal;
	
	public void setCal(Calculator cal) {
		this.setName("UserB");
		this.cal = cal;
	}
	
	public void run() {
		cal.setMemory(50);
	}
}

-> 결과

☞ 결과로 UserA : 100, userB : 50을 기대했지만 둘 다 50이 출력이 되었다. 이는 공유 객체를 사용하여 값이 변경이 되었기 때문이다.

 

◎ 동기화 메소드

-> 스레드가 사용 중인 객체를 다른 객체가 사용하지 못하도록 하려면 스레드 작업이 끝날 때까지 객체에 잠금을 걸어 다른 스레드가 사용하지 못하게 한다.

 

-> 멀티 스레드 프로그램에서 하나의 스레드만 실행할 수 있는 코드 영역을 임계영역이라 한다. 자바는 임계 영역을 지정하기 위해 동기화 메소드를 사용한다. 스레드가 객체 내부의 동기화 메소드를 실행하면 객체에 잠금을 걸어 다른 스레드가 동기화 메소드를 사용하지 못하게 한다.

-> 메소드 선언할 때 synchronized를 붙이면 동기화 메소드가 된다.

public synchronized void setMemory(int money) {
		...
}

-> 동기화 메소드 사용

public class Calculator {
	
	private int memory;
	
	public int getMemory() {
		return memory;
	}
	
	public synchronized void setMemory(int money) {
		this.memory = money;
		try {
			Thread.sleep(2000);
		}catch(Exception e) {}
		
		System.out.println(Thread.currentThread().getName() + " : " + this.memory);
	}

}

-> 결과