[프로그래머스] 자바 중급 - 쓰레드
자바 쓰레드(Thread)
Thread는 무엇인가? OS적 관점과 자바에서 다루는 쓰레드에 대해알아보자!
OS에서 process, thread에 대한 기본 지식은 생략한다.
쓰레드란
정의
- 일단 세부적인 내용을 process, thread, 자원, 할당, 교착 등 다루기 시작하면 끝이 없다,, 그리고 무조건 '제대로', '깊게' 알아야 하기 때문에 java를 통해 역으로 깨닫는 방식은 너무나 옳지 않다!
CS지식 찍어먹기 멈춰!
- 가볍게 읽어보자, 그리고 이것 또한, 그리고 이것도
- 프로세스는 현재 실행되고 있는 프로그램의 단위이고, 이 프로그램 단위(프로세스 안)에 쓰레드가 존재한다!
- 여러개 쓰레드가 하나의 프로세스 안에서 작동이 된다! 멀티 스레딩이 된다!!
자바의 메인쓰레드?
- 쓰레드는 동시성과 병렬성이 있어야 한다.
- 프로세스 시작 시 최초 실행되는 static main 메서드는 JVM에 의해 생성된 main() 스레드에서 실행된다.
- 자바 어플리케이션 자체는 운영체제에게 자원을 할당받은 프로세스. -> 컴파일된 자바 클래스는 JVM 위에서 동작
- JVM은 프로그램의 시작점인 main() 메서드를 찾아서 메인 스레드를 생성하고 메인 스레드의 run() 메서드를 통해 해당 main() 메서드를 실행합니다.
- 이때 main(String[] args) 매개변수인 String 배열에 명령행에서 입력한 파라미터를 넣어 호출하는 것! -> 이 시점에서 스레드는 메인 스레드 하나이고 당연히 처리 흐름도 하나입니다.
자바에서 쓰레드 만들기
Thread 클래스를 상속받기
- java.lang.Thread클래스를 상속받는다. 그리고 Thread가 가지고 있는 run()메소드를 오버라이딩한다.
public class MyThread1 extends Thread {
String str;
public MyThread1(String str){
this.str = str;
}
public void run(){
for(int i = 0; i < 10; i ++){
System.out.print(str);
try {
// 너무 빠르기 때문에 수행결과를 잘 확인 할 수 없어서 Thread.sleep() 메서드를 이용해서 조금씩
// 쉬었다가 출력할 수 있게한다.
Thread.sleep((int)(Math.random() * 1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
- 위 class의 인스턴스(object)를 만들어서 메인쓰레드에서 사용해보자!
public class ThreadExam1 {
public static void main(String[] args) {
// MyThread인스턴스를 2개 만듭니다.
MyThread1 t1 = new MyThread1("*");
MyThread1 t2 = new MyThread1("-");
t1.start();
t2.start();
System.out.print("!!!!!");
}
}
- Thread 클래스를 상속받은 MyThread1을 사용하는 클래스
- Thread를 상속 받았으므로 MyThread1은 Thread 이다.
- 쓰레드를 생성하고, Thread 클래스가 가지고 있는 start() 메소드를 호출 한다.
- start메소드를 호출하지 않으면 run하지 않는다!
- 메인 스레드가 죽는다고 프로세스가 죽는 건 아니다! -> 하나의 쓰레드라도 run 중이면 종료되지 않는다! -> 모든 쓰레드가 죽어야 프로세스도 죽는다!
Runnable인터페이스를 구현하기
- Runable 인터페이스가 가지고 있는 run()메소드를 구현한다.
public class MyThread2 implements Runnable {
String str;
public MyThread2(String str){
this.str = str;
}
@Override
public void run(){
for(int i = 0; i < 10; i ++){
System.out.print(str);
try {
Thread.sleep((int)(Math.random() * 1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
- 위 class의 인스턴스(object)를 만들어서 메인쓰레드에서 사용해보자!
public class ThreadExam2 {
public static void main(String[] args) {
MyThread2 r1 = new MyThread2("*");
MyThread2 r2 = new MyThread2("-");
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r2);
t1.start();
t2.start();
System.out.print("!!!!!");
}
}
- Runable 인터페이스를 구현한 MyThread2을 사용하는 클래스
- MyThread2는 Thread를 상속받지 않았기 때문에 Thread가 아니다.
- Thread를 생성하고, 해당 생성자에 MyThread2를 넣어서 Thread를 생성한다. -> thread를 상속받고 만드는 방법과 차이 체크!
- Thread 클래스가 가진 start()메소드를 호출한다.
- 자바는 단일 상속만 지원하기 때문에 Runable 인터페이스를 지원한다!
- 즉 다중상속이 안되기 때문에 다른 class를 extend하고 있으면 인터페이스를 사용하면 된다!
public class Bus extends Car implements Runnable
- 생성자도 따로 이용할 수 있다는게 장점이다!
- 좀 더 다양하고 많이 사용되는 방법이다.
쓰레드와 공유객체
공유객체?
Thread는 무엇인가? OS적 관점과 자바에서 다루는 쓰레드에 대해알아보자!
OS에서 process, thread에 대한 기본 지식은 생략한다.
public class MyThread1 extends Thread {
String str;
public MyThread1(String str){
this.str = str;
}
public void run(){
for(int i = 0; i < 10; i ++){
System.out.print(str);
try {
// 너무 빠르기 때문에 수행결과를 잘 확인 할 수 없어서 Thread.sleep() 메서드를 이용해서 조금씩
// 쉬었다가 출력할 수 있게한다.
Thread.sleep((int)(Math.random() * 1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class ThreadExam1 {
public static void main(String[] args) {
// MyThread인스턴스를 2개 만듭니다.
MyThread1 t1 = new MyThread1("*");
MyThread1 t2 = new MyThread1("-");
t1.start();
t2.start();
System.out.print("!!!!!");
}
}
- Thread를 상속 받았으므로 MyThread1은 Thread 이다.
- 쓰레드를 생성하고, Thread 클래스가 가지고 있는 start() 메소드를 호출 한다.
- start메소드를 호출하지 않으면 run하지 않는다!
public class MyThread2 implements Runnable {
String str;
public MyThread2(String str){
this.str = str;
}
@Override
public void run(){
for(int i = 0; i < 10; i ++){
System.out.print(str);
try {
Thread.sleep((int)(Math.random() * 1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class ThreadExam2 {
public static void main(String[] args) {
MyThread2 r1 = new MyThread2("*");
MyThread2 r2 = new MyThread2("-");
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r2);
t1.start();
t2.start();
System.out.print("!!!!!");
}
}
- MyThread2는 Thread를 상속받지 않았기 때문에 Thread가 아니다.
- Thread를 생성하고, 해당 생성자에 MyThread2를 넣어서 Thread를 생성한다. -> thread를 상속받고 만드는 방법과 차이 체크!
- Thread 클래스가 가진 start()메소드를 호출한다.
- 즉 다중상속이 안되기 때문에 다른 class를 extend하고 있으면 인터페이스를 사용하면 된다!
public class Bus extends Car implements Runnable
- 생성자도 따로 이용할 수 있다는게 장점이다!
- 좀 더 다양하고 많이 사용되는 방법이다.
- 쓰레드의 공유자원에 대해 좀 더 자세히 먼저 알아야한다
- 하나의 객체를 여러개의 Thread가 사용한다는 것을 의미
- 예제 코드로 바로 살펴보자!
public class MusicBox {
//신나는 음악!!! 이란 메시지가 1초이하로 쉬면서 10번 반복출력
public void playMusicA(){
for(int i = 0; i < 10; i ++){
System.out.println("신나는 음악!!!");
try {
Thread.sleep((int)(Math.random() * 1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
} // for
} //playMusicA
//슬픈 음악!!!이란 메시지가 1초이하로 쉬면서 10번 반복출력
public void playMusicB(){
for(int i = 0; i < 10; i ++){
System.out.println("슬픈 음악!!!");
try {
Thread.sleep((int)(Math.random() * 1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
} // for
} //playMusicB
//카페 음악!!! 이란 메시지가 1초이하로 쉬면서 10번 반복출력
public void playMusicC(){
for(int i = 0; i < 10; i ++){
System.out.println("카페 음악!!!");
try {
Thread.sleep((int)(Math.random() * 1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
} // for
} //playMusicC
}
// MusicBox를 가지는 Thread객체 MusicPlayer
public class MusicPlayer extends Thread{
int type;
MusicBox musicBox; // "공유 객체"
// 생성자로 부터 musicBox와 정수를 하나 받아들여서 필드를 초기화
public MusicPlayer(int type, MusicBox musicBox){
this.type = type;
this.musicBox = musicBox;
}
// type이 무엇이냐에 따라서 musicBox가 가지고 있는 메소드가 다르게 호출
public void run(){
switch(type){
case 1 : musicBox.playMusicA(); break;
case 2 : musicBox.playMusicB(); break;
case 3 : musicBox.playMusicC(); break;
}
}
}
// 메인 쓰레드가 있는 클래스로 위 클래스 인스턴스를 쓰레드화
public class MusicBoxExam1 {
public static void main(String[] args) {
// 공유 객체로 쓸 MusicBox 인스턴스
MusicBox box = new MusicBox();
// MusicPlayer object는 모두 동일한 MusicBox obj를 가진다
MusicPlayer kim = new MusicPlayer(1, box);
MusicPlayer lee = new MusicPlayer(2, box);
MusicPlayer kang = new MusicPlayer(3, box);
// MusicPlayer쓰레드를 실행합니다.
kim.start();
lee.start();
kang.start();
}
}
- 위 흐름은 아래와 같다
- 공통으로 사용할 데이터를 클래스로 정의하고 공통으로 사용할 클래스의 인스턴스를 만든다.
- 그리고 이 인스턴스를 각각의 쓰레드에 넘겨 준다.
- 각각의 쓰레드는 이 인스턴스의 참조값을 저장한 변수를 이용하여 공통 데이터를 사용한다.
- 값이 어떻게 바뀔까? 예측이 가능한가? 어떻게 실행이 되는가?
- ps) 원래 쓰레드 환경에서는 기본적으로 예측이 불가능하다 -> 가능하게 하는 방법도 있지만, 멀티쓰레딩 목적은 '예측 가능과 불가능'이 아니라, 서로 간 실행 순서가 상관 없을때 사용되어야 하는 것!
그래서 뭐? volatiole!
- 왜 공유 객체? -> 메모리 절약과 최적화 관점
- volatile에 좀 더 자세히
- 당연히 하나의 쓰레드가 아닌 여러 쓰레드가 R/W하는 상황에서는 적합하지 X
- 그렇게 하기 위해서는 synchronized를 통해 변수 R/W(읽고 쓰기라고 하자)의 원자성(atomic)을 보장해야한다!
동기화 메소드와 동기화 블록
동기화
- 서로간 영향을 주고받는 데이터들간에 데이터의 일관성이 유지될 수 있도록 해주는 것이 동기화이다.
- 위 예제에서 music box를 생각하자. 3개의 메소드가 '동시에' 호출이 되어서 고장이 난다면?!
- 하나의 메서드가 실행되고 있을때는 다른 메서드를 사용하지 않도록 대기 시켜야한다.
- 실행이 끝나면 대기하던 메서드를 실행시키면 된다.
- 즉, 간단하게 공유객체가 가진 메소드를 동시에 호출 되지 않도록 하는 방법
- 메소드 앞에 synchronized 를 붙힌다.
- 여러개의 Thread들이 공유객체의 메소드를 사용할 때 메소드에 synchronized가 붙어 있을 경우 먼저 호출한 메소드가 객체의 사용권(Monitoring Lock)을 얻는다.
- 위 예제에서 메서드에 synchronized 붙여서 실행해보자! => 메소드 하나가 모두 실행된 후에 다음 메소드가 실행된다.
모니터링 락(Monitoring Lock)
- 해당 모니터링 락은 메소드 실행이 종료되거나, wait()와 같은 메소드를 만나기 전까지 유지된다.
- 다른 쓰레드들은 모니터링 락을 놓을때까지 대기한다.
- synchronized를 붙히지 않은 메소드는 다른 쓰레드들이 synchronized메소드를 실행하면서 모니터링 락을 획득했다 하더라도, 그것과 상관없이 실행된다.
- synchronized를 메소드에 붙혀서 사용 할 경우, 메소드의 코드가 길어지면, 마지막에 대기하는 쓰레드가 너무 오래 기다리는것을 막기위해서 메소드에 synchronized를 붙이지 않고, 문제가 있을것 같은 부분만 synchronized블록을 사용한다.
- 깊게 알면 알수록 좋다
public void playMusicB(){
for(int i = 0; i < 10; i ++){
// 해당 객체에 대해 모니터링 락가졌니? (여기서는 this)
synchronized (this) {
System.out.println("슬픈 음악!!!");
}
try {
Thread.sleep((int)(Math.random() * 1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
} // for
} //playMusicB
쓰레드와 상태제어
쓰레드의 상태
- 쓰레드는 어떻게 실행될까? 시간에 지나감에 따라 여러 쓰레드가 동시다발적으로 실행이 된다. run, wait, stop 정도로 간단하게 생각해 볼 수 있다.
- 쓰레드가 3개가 있다면 JVM은 시간을 잘게 쪼갠 후에 한번은 쓰레드1을, 한번은 쓰레드 2를, 한번은 쓰레드 3을 실행합니다. 이것에 빠르게 일어나다 보니 쓰레드가 모두 동작하는 것처럼 보이는 것이다.
- 쓰레드는 실행가능상태인 Runnable과 실행상태인 Running상태로 나뉜다.
- 실행되는 쓰레드 안에서 Thread.sleep()이나 Object가 가지고 있는 wait()메소드가 호출이 되면 쓰레드는 블록상태가 된다. blocking vs nonblocking
- Thread.sleep()은 특정시간이 지나면 자신 스스로 블록상태에서 빠져나와 Runnable이나 Running상태가 된다.
- Object가 가지고 있는 wait()메소드는 다른 쓰레드가 notify()나 notifyAll()메소드를 호출하기 전에는 블록상태에서 해제되지 않는다. -> 일이 끝났다는 걸 '알린다' 생각하자!
- wait()메소드는 호출이 되면 모니터링 락을 놓게 된다. 그래서 대기중인 다른 메소드가 실행한다.
- 쓰레드가 고유 객체의 sync block이나 method가 실행되었는데 이미 다른 쓰레드가 모니터링 락을 획득한 상태라면, 락 풀에서 블럭된 상태가 된다. 이 역시 block되었다고 표현 할 수 있다.
- 쓰레드의 run메소드가 종료되면, 쓰레드는 종료된다. 즉 Dead상태가 된다.
- Thread의 yeild메소드가 호출되면 해당 쓰레드는 다른 쓰레드에게 자원을 양보하게 된다. -> 즉 다른 쓰레드가 좀 더 빠르게 실행되게 할 수 있다.
- Thread가 가지고 있는 join메소드를 호출하게 되면 해당 쓰레드가 종료될 때까지 대기하게 된다.
join
- 쓰레드가 멈출때까지 기다리게 한다. (Thread가 가지고 있는 join메소드를 호출하게 되면 해당 쓰레드가 종료될 때까지 대기)
- 일단 0.5초씩 쉬면서 숫자를 출력하는 MyThread5를 작성해 보자
public class MyThread5 extends Thread{
public void run(){
for(int i = 0; i < 5; i++){
System.out.println("MyThread5 : "+ i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} // run
}
// 위 해당 쓰레드를 실행하고, 해당쓰레드가 종료될때까지 기다린 후, 내용을 출력하는 JoinExam클래스
public class JoinExam {
public static void main(String[] args) {
/*
MyThread5 thread = new MyThread5();
thread.start();
System.out.println("Thread 시작.");
System.out.println("Thread 종료.");
*/
MyThread5 thread = new MyThread5();
// Thread 시작 -> (Thread 상속 받았기 때문에 바로 시작 가능)
thread.start();
System.out.println("Thread가 종료될때까지 기다립니다.");
try {
// 해당 쓰레드가 멈출때까지 멈춤
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread가 종료되었습니다.");
}
}
- 실행 결과 -> 즉 main 쓰레가 해당 쓰레드 끝날때까지 wait상태가 된 것!
[ 주석 안 코드 먼저 ]
Thread 시작.
Thread 종료.
MyThread5 : 0
MyThread5 : 1
MyThread5 : 2
MyThread5 : 3
MyThread5 : 4
[ join이 들어간 메인 코드 ]
Thread가 시작되었습니다.
Thread가 종료될때까지 기다립니다.
MyThread5 : 0
MyThread5 : 1
MyThread5 : 2
MyThread5 : 3
MyThread5 : 4
Thread가 종료되었습니다.
wait와 notify
- wait와 notify는 동기화된 블록안에서 사용해야 한다.
- wait를 만나게 되면 해당 쓰레드는 해당 객체의 모니터링 락에 대한 권한을 가지고 있다면 모니터링 락의 권한을 놓고 대기한다.
public class ThreadB extends Thread{
// 해당 쓰레드가 실행되면 자기 자신의 모니터링 락을 획득
// 5번 반복하면서 0.5초씩 쉬면서 total에 값을 누적
// 그후에 notify()메소드를 호출하여 wiat하고 있는 쓰레드를 깨움
int total;
@Override
public void run(){
synchronized(this){ // 동기화 블럭!!
for(int i=0; i<5 ; i++){
System.out.println(i + "를 더합니다.");
total += i;
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
notify(); // 알린다! -> wait 풀어 임마!
}
}
}
// ThreadB를 사용하며 wait하는 클래스 작성
public class ThreadA {
public static void main(String[] args){
// 앞에서 만든 쓰레드 B를 만든 후 start
// 해당 쓰레드가 실행되면, 해당 쓰레드는 run메소드 안에서 자신의 모니터링 락을 획득
ThreadB b = new ThreadB();
b.start();
// b에 (thread 상속받는 object) 대하여 동기화 블럭을 설정
// 만약 main쓰레드가 아래의 블록을 위의 Thread보다 먼저 실행되었다면 wait를 하게 되면서 모니터링 락을 놓고 대기
synchronized(b){
try{
// b.wait()메소드를 호출.
// 메인쓰레드는 정지
// ThreadB가 5번 값을 더한 후 notify를 호출하게 되면 wait에서 깨어남
System.out.println("b가 완료될때까지 기다립니다.");
b.wait(); // 잠들어서 기다려 -> 즉 다른 쓰레드가 notify 할때까지 기다린다!
}catch(InterruptedException e){
e.printStackTrace();
}
//깨어난 후 결과를 출력
System.out.println("Total is: " + b.total);
}
}
}
- 실행 결과 ->
b가 완료될때까지 기다립니다.
0를 더합니다.
1를 더합니다.
2를 더합니다.
3를 더합니다.
4를 더합니다.
Total is: 10
데몬 쓰레드 (Daemon Thread)
- 데몬? : 데몬(Daemon)이란 보통 리눅스와 같은 유닉스계열의 운영체제에서 백그라운드로 동작하는 프로그램을 말한다. -> window 계열에서는 보통 service(서비스)라고 한다.
- 자바에서 데몬과 유사하게 동작하는 쓰레드가 데몬쓰레드다!
- 데몬쓰레드를 만드는 방법은 쓰레드에 데몬 설정을 하면 된다.
- 이런 쓰레드는 자바프로그램을 만들 때 백그라운드에서 특별한 작업을 처리하게 하는 용도로 만든다.
- ex) 주기적으로 자동 저장 / 일정 시간에 맞춤법 검사 ....
- 데몬쓰레드는 일반 쓰레드(main 등)가 모두 종료되면 강제적으로 종료되는 특징을 가지고 있다.
- setDaemon 메서드는 반드시 start()를 호출하기 전에 실행되어야 한다. 그렇지 않으면 IllegalThreadStateException이 발생한다!
// Runnable을 구현하는 DaemonThread클래스를 작성
public class DaemonThread implements Runnable {
// 무한루프안에서 0.5초씩 쉬면서 데몬쓰레드가 실행중입니다를 출력하도록 run()메소드를 작성
@Override
public void run() {
while (true) {
System.out.println("데몬 쓰레드가 실행중입니다.");
try {
Thread.sleep(500); // 0.5초씩 쉬면서
} catch (InterruptedException e) {
e.printStackTrace();
break; // Exception발생시 while 문 빠찌도록
}
}
}
public static void main(String[] args) {
// Runnable을 구현하는 DaemonThread를 실행하기 위하여 Thread 생성
Thread th = new Thread(new DaemonThread());
th.setDaemon(true); // 데몬쓰레드로 설정!!
th.start(); // 쓰레드를 실행
// 메인 쓰레드가 1초뒤에 종료되도록 설정.
// 데몬쓰레드는 다른 쓰레드가 모두 종료되면 자동종료.
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("메인 쓰레드가 종료됩니다. ");
}
}
Author And Source
이 문제에 관하여([프로그래머스] 자바 중급 - 쓰레드), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@qlgks1/프로그래머스-자바-중급-쓰레드저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)