지구정복

[JAVA] 11/26 | 멀티스레드, 작업스레드, 스레드상태, 스레드를 이용한 구구단, 스레드 이름, 스레드 우선순위, 스레드 상태제어, 데몬스레드, 스레드 동기화,스레드그룹, 공유객체, 숫자야구게임.. 본문

데이터 엔지니어링 정복/JAVA & JSP

[JAVA] 11/26 | 멀티스레드, 작업스레드, 스레드상태, 스레드를 이용한 구구단, 스레드 이름, 스레드 우선순위, 스레드 상태제어, 데몬스레드, 스레드 동기화,스레드그룹, 공유객체, 숫자야구게임..

eeaarrtthh 2020. 11. 27. 16:02
728x90
반응형

앞으로 배울 내용

데이터 저장
	임시
		변수/ 상수
		->Collection
	영구
		로컬
		원격(네트워크)
		
Java 기본
	file

데이터베이스
	mariadb
Java 나머지
미니프로젝트
web - html/css/js
	*시험 - 간단한 프로그램 작성후 스크린캡처

 

12.1 멀티스레드 개념

12.1.1 프로세스와 스레드

576

운영체제에서는 실행 중인 하나의 애플리케이션을 프로세스라고 한다.

멀티태스킹은 두 가지 이상의 작업을 동시에 처리하는 것을 말하고 운영체제는 멀티 태스킹을 할 수 있도록

CPU 및 메모리 자원을 프로세스마다 적절히 할당해주고, 병렬로 실행시킨다.

 

그럼 어떻게 하나의 프로세스가 두 가지 이상의 작업을 처리할 수 있을까? 그 방법은 멀티 스레드에 있다.

하나의 스레드는 하나의 코드실행 흐름이기 때문에 한 프로세스 내에 스레드가 두 개라면 두 개의 코드 실행흐름이 생긴다는 의미이다.

멀티 프로세스가 애플리케이션 단위의 멀티 태스킹이라면 

멀티 스레드는 애플리케이션 내부에서의 멀티 태스킹이라고 할 수 있다.

 

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

따라서 하나의 프로세스에서 오류가 발생해도 다른 프로세스에게 영향을 미치지 않는다.

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

 

 

12.1.2 메인 스레드

577

모든 자바 애플리케이션은 메인 스레드가 메인메소드를 실행하면서 시작된다.

멘인 스레드는 필요에 따라 작업 스레드들을 만들어서 병렬로 코드를 실행할 수 있다.

즉 멀티 스레드를 생성해서 멀티 태스킹을 수행한다.

멀티 스레드 애플리케이션에서는 실행 중인 스레드가 하나라도 있다면, 프로세스는 종료되지 않는다.

자바는 JVM이 프로세스이고 class가 메인 스레드이다.

 

12.2 작업스레드 생성과 실행

579

작업스레드 생성은 두 가지 방법이 있다.

1. java.lang.Tread 클래스로부터 만드는 방법

2. Runnable 인터페이스를 구현해서 만드는 방법

 

먼저 하나의 패키지 안에 아래와 같은 클래스를 작성한다.

이는 메인스레드의 순차처리를 보여준다.

package threadEx;

public class Go {
	public void run() {
		for(int i=0; i<=10; i++) {
			System.out.println("go : " + i);
		}
	}
}
package threadEx;

public class Come {
	public void run() {
		for(int i=0; i<=10; i++) {
			System.out.println("come : " + i);
		}
	}
}
package threadEx;

public class ThreadEx01 {

	public static void main(String[] args) {
		Go g = new Go();
		Come c = new Come();
		
		//메인스레드의 순차처리 go가 실행되고 come이 순차적으로 실행된다.
		g.run();
		c.run();
	}
}


go : 0
go : 1
go : 2
go : 3
go : 4
go : 5
go : 6
go : 7
go : 8
go : 9
go : 10
come : 0
come : 1
come : 2
come : 3
come : 4
come : 5
come : 6
come : 7
come : 8
come : 9
come : 10

 

이번에는 새로운 패키지를 만들고 스레드 클래스를 상속해서 클래스를 만든다.

스레드가 실행됨을 알 수 있다.

여기서 run()을 사용하지 않고 Thread 클래스의 start() 메서드를 사용하는 것이 중요하다.

package Pack2;

public class Go extends Thread {
	@Override
	public void run() {
		// 스레드에서 작업할 (병렬처리할) 내용 작성
		for(int i=0; i<=10; i++) {
			System.out.println("go : " + i);
		}
	}
}
package Pack2;

public class Come extends Thread {
	@Override
	public void run() {
		for(int i=0; i<=10; i++) {
			System.out.println("come : " + i);
		}
	}
}
package Pack2;

public class ThreadEx01 {

	public static void main(String[] args) {
		Go g = new Go();
		Come c = new Come();
		
		//스레드 메서드 실행하는 메서드는 start()이다.
		g.start();
		c.start();
	}
}


go : 0
go : 1
go : 2
go : 3
go : 4
come : 0
come : 1
come : 2
come : 3
come : 4
come : 5
come : 6
go : 5
come : 7
come : 8
come : 9
come : 10
go : 6
go : 7
go : 8
go : 9
go : 10

 

 

12.5 스레드 상태

597

스레드 객체를 생성하고 start() 메소드를 호출하면 실행 대기 상태가 된다. 실행 대기 상태란 아직 스케줄링이 되지 않아서 실행을 기다리고 있는 상태를 말한다. 실행 대기 상태에 있는 스레드 중에서 스레드 스케줄링으로 선택된 스레드가 비로소 CPU를 점유하고 run() 메소드를 실행한다. 이때를 실행 상태라고 한다.

실행 상태의 스레드는 run()메소드를 모두 실행하기 전에 스레드 스케줄링에 의해 다시 실행 대기 상태로 돌아갈 수 있다. 그리고 실행 대기 상태에 있는 다른 스레드가 선택되어 실행 상태가 된다. 이렇게 스레드는 실행 대기 상태와 실행 상태를 번갈아가면서 자신의 run() 메소드를 조금씩 실행한다. 실행 상태에서 run() 메소드가 종료되면, 더 이상 실행할 코드가 없기 때문에 스레드의 실행은 멈추게 된다. 이 상태를 종료 상태라고 한다.

 

위의 코드를 아래처럼 사용해서 결과를 확인하자.

이는 먼저 프린트문 출력이후에 스레드가 종료될때까지 기다리는 것이다.

즉 스레드 안에서는 순차적인 순서가 있지만

병렬 스레드에서는 순서라는 개념이 없고 단지 CPU가 처리하는 순서에 따라 스레드가 처리된다.

package Pack2;

public class ThreadEx01 {

	public static void main(String[] args) {
		Go g = new Go();
		Come c = new Come();
		
		//스레드 메서드 실행하는 메서드는 start()이다.
		System.out.println("시작");
		g.start();
		c.start();
		System.out.println("종료");
	}
}


시작
종료
go : 0
go : 1
go : 2
go : 3
go : 4
go : 5
go : 6
go : 7
go : 8
go : 9
go : 10
come : 0
come : 1
come : 2
come : 3
come : 4
come : 5
come : 6
come : 7
come : 8
come : 9
come : 10

 

 

이번에 Runable 인터페이스를 구현해서 만들어보자.

package Pack3;

public class Go implements Runnable {
	@Override
	public void run() {
		for(int i=0; i<=50; i++) {
			System.out.println("go : " + i);
		}
	}
}
package Pack3;

public class Come implements Runnable {

	@Override
	public void run() {
		for(int i=0; i<=50; i++) {
			System.out.println("come : " + i);
		}
	}
}
package Pack3;

public class ThreadEx03 {

	public static void main(String[] args) {
		Go g = new Go();
		Come c = new Come();
		
		Thread t1 = new Thread(g);
		Thread t2 = new Thread(c);
		
		System.out.println("시작");
		t1.start();
		t2.start();
		System.out.println("끝");
	}
}

 

이번에는 Go와 Come 클래스를 만들기 귀찮으므로 익명 클래스를 이용해서 만들어보자.

익명 클래스는 인터페이스를 사용해서 만든다.

package Pack4;

public class ThreadEx01 {

	public static void main(String[] args) {
		// 익명 클래스는 인터페이스를 사용해서 만든다.
		Thread t1 = new Thread(new Runnable() {
			@Override
			public void run() {
				for(int i=0; i<=50; i++) {
					System.out.println("go : " + i);
				}
			}
		});
		Thread t2 = new Thread(new Runnable() {
			@Override
			public void run() {
				for(int i=0; i<=50; i++) {
					System.out.println("come : " + i);
				}
			}
		});
		
		t1.start();
		t2.start();
	}
}

 

-문제: 구구단 단수를 입력받아서 출력하는 스레드 클래스 작성

구구단 스레드 클래스

package Pack5;

public class Gugudan implements Runnable {
	private int dan;
	
	public Gugudan(int dan) {
		this.dan = dan;
	}

	@Override
	public void run() {
		for(int i=1; i<=9; i++) {
			System.out.printf("%s X %s = %s%n", dan, i, (dan*i));
		}
	}
}
package Pack5;

public class ThreadEx01 {

	public static void main(String[] args) {
		Thread t1 = new Thread(new Gugudan(3));
		Thread t2 = new Thread(new Gugudan(6));
		
		t1.start();
		t2.start();
	}
}



3 X 1 = 3
6 X 1 = 6
6 X 2 = 12
6 X 3 = 18
6 X 4 = 24
6 X 5 = 30
6 X 6 = 36
6 X 7 = 42
6 X 8 = 48
6 X 9 = 54
3 X 2 = 6
3 X 3 = 9
3 X 4 = 12
3 X 5 = 15
3 X 6 = 18
3 X 7 = 21
3 X 8 = 24
3 X 9 = 27

 

12.2.3 스레드의 이름

스레드는 자신의 이름을 가지고 있다. 자주 사용하지는 않지만 디버깅할 때 어떤 스레드가 어떤 작업을 하는지 조사할 목적으로 가끔 사용된다.

이름을 설정할 때는 setName()

이름을 읽을 때는 getName() 을 사용한다.

package Pack5;

public class Gugudan implements Runnable {
	private int dan;
	
	public Gugudan(int dan) {
		this.dan = dan;
	}

	@Override
	public void run() {
		System.out.println(Thread.currentThread().getName()+ "시작");
		
		for(int i=1; i<=9; i++) {
			System.out.printf("%s X %s = %s%n", dan, i, (dan*i));
		}
	}
}
package Pack5;

public class ThreadEx01 {

	public static void main(String[] args) {
		Thread t1 = new Thread(new Gugudan(3));
		Thread t2 = new Thread(new Gugudan(6));
		
		t1.setName("3단 구구단");
		t2.setName("6단 구구단");
		
		t1.start();
		t2.start();
	}
}

 

 

스레드이름 사용하는 책 예제

package Pack5;

public class ThreadA extends Thread {
	public ThreadA() {
		setName("ThreadA");	//스레드 이름 설정
	}
	
	@Override
	public void run() {
		for(int i=0; i<2; i++) {
			System.out.println(getName() + "가 출력한 내용");
		}
	}
}
package Pack5;

public class ThreadB extends Thread {
	@Override
	public void run() {
		for(int i=0; i<2; i++) {
			System.out.println(getName() + "가 출력한 내용");
		}
	}
}
package Pack5;

public class ThreadNameExample {

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

 

 

12.3 스레드 우선순위

멀티스레드는 동시성(Concurrency) 또는 병렬성(Parallelism)으로 실행된다. 

동시성은 멀티 작업을 위해 하나의 코어에서 멀티 스레드가 번갈아가며 실행하는 성질을 말하고,

병렬성은 멀티작업을 위해 멀티 코어에서 개별 스레드를 동시에 실행하는 성질을 말한다.

싱글 코어 CPU를 이용한 멀티 스레드 작업은 병렬적으로 실행되는 것처럼 보이지만, 사실은 번갈아가며 실행하는 동시성 작업이다.

 

스레드의 개수가 코어의 수보다 많을 경우, 스레드를 어떤 순서에 의해 동시성으로 실행할 것인가를 결정해야 하는데, 이것을 스레드 스케줄링이라고 한다.

스레드 스케줄링에 의해 스레드들은 아주 짧은 시간에 번갈아가면서 그들의 run() 메소드를 조금씩 실행한다.

 

자바의스레드 스케줄링은 우선순위 방식과 순환 할당 방식을 사용한다.

우선순위 방식은 우선순위가 높은 스레드가 실행 상태를 더 많이 가지도록 스케줄링하는 것을 말한다.

순환 할당 방식은 시간 할당량을 정해서 하나의 스레드를 정해진 시간만큼 실행하고 다시 다른 스레드를 실행하는 방식을 말한다.

스레드 우선 순위 방식은 스레드 객체에 우선순위 번호를 부여할 수 있기 때문에 개발자가 코드로 제어할 수 있다.

하지만 순환 할당 방식은 자바 가상 기계에 의해서 정해지기 때문에 코드로 제어할 수 없다.

 

우선순위 방식에는 우선순위는 1에서부터 10까지 부여되는데, 1이 가장 낮고, 10이 가장 높다.

기본적인 우선순위는 5이다.

우선순위를 변경하고 싶다면 Thread 클래스가 제공하는 setPriority()메소드를 이용한다.

thread.setPriority(우선순위);

 

아래 예제를 살펴보자.

package Pack5;

public class CalcThread extends Thread {
	public CalcThread(String name) {
		setName(name);
	}
	
	@Override
	public void run() {
		for(int i=0; i<2000000000; i++) {
		}
		System.out.println(getName());
	}
}
package Pack5;

public class PriorityExample {

	public static void main(String[] args) {
		for(int i=1; i<=10; i++) {
			Thread thread = new CalcThread("thread" + i);
			if (i != 10) {
				thread.setPriority(Thread.MIN_PRIORITY);	//i가 10이 아니면 모두 낮은순위
			} else {
				thread.setPriority(Thread.MAX_PRIORITY);	//i가 10이면 가장 높은 순위
			}
			thread.start();
		}
	}
}



thread10
thread1
thread7
thread9
thread2
thread3
thread6
thread5
thread8
thread4

 

 

 

12.6 스레드 상태 제어

실행 중인 스레드의 상태를 변경하는 것을 스레드 상태 제어라고 한다.

여기에는 여러가지 메소드가 있다.

 

12.6.1 sleep(밀리세컨드초)

밀리세컨드초 동안 쓰레드를 중지시키고 후에 쓰레드를 다시 실행

package Pack6;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;

public class ThreadEx01 {

	public static void main(String[] args) {
		
		System.out.println("시작");

		Thread.sleep(5000);
		
		System.out.println("끝");

	}
}

 

 

-문제: 쓰레드를 이용해서 1초마다 현재 시간:분:초 를 출력해주는 무한루프 프로그램 만들기

package Pack6;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;

public class ThreadEx01 {

	public static void main(String[] args) {
		
		System.out.println("시작");
		while(true) {
			try {
				Thread.sleep(1000);
				Date date = new Date();
				SimpleDateFormat f = new SimpleDateFormat("HH:mm:ss", Locale.KOREA);
				String d = f.format(date);
				System.out.println(d);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}
}



시작
15:07:17
15:07:18
15:07:19
15:07:20

 

12.6.2 yield()

다른 스레드에게 실행 양보

아래 예제를 보면 처음에는 ThreadA, ThreadB모두 실행하다가 3초뒤에 B만 실행하고

다시 3초 뒤에 A,B 모두 실행하고 또 3초 뒤에 두 개의 스레드가 종료된다.

package Pack6;

public class ThreadA extends Thread {
	public boolean stop = false; //종료 플래그
	public boolean work = true;	//작업 진행 여부 플래그
	
	@Override
	public void run() {
		while( !stop ) {
			if(work) {
				System.out.println("ThreadA 작업 내용");
			} else {
				Thread.yield();
			}
		}
		System.out.println("ThreadA 종료");
	}
}
package Pack6;

public class ThreadB extends Thread {
	public boolean stop = false; //종료 플래그
	public boolean work = true;	//작업 진행 여부 플래그
	
	@Override
	public void run() {
		while( !stop ) {
			if(work) {
				System.out.println("ThreadB 작업 내용");
			} else {
				Thread.yield();
			}
		}
		System.out.println("ThreadB 종료");
	}
}
package Pack6;

public class YieldExample {

	public static void main(String[] args) {
		ThreadA threadA = new ThreadA();
		ThreadB threadB = new ThreadB();
		
		threadA.start();
		threadB.start();
		
		try { Thread.sleep(3000); } catch(InterruptedException e) {}
		threadA.work = false;	//ThreadB만 실행
		
		try { Thread.sleep(3000); } catch(InterruptedException e) {}
		threadA.work = true;	//ThreadA, ThreadB 모두 실행
		
		try { Thread.sleep(3000); } catch(InterruptedException e) {}
		threadA.stop = true;	//ThreadA, ThreadB 모두 종료
		threadB.stop = true;
		
	}
}

 

12.6.3 join()

다른 스레드의 종료를 기다림

 

다음 예제는 메인 스레드는 SunThread가 계산 작업을 모두 마칠 때까지 일시 정지 상태에 있다가 SumThread가 최종 계산된 결과값을 산출하고 종료하면 결과값을 받아 출력한다.

package Pack6;

public class SumThread extends Thread {
	private long sum;
	
	public long getSum() {
		return sum;
	}
	
	public void setSum(long sum) {
		this.sum = sum;
	}
	
	@Override
	public void run() {
		for(int i=1; i<=100; i++) {
			sum += i;
		}
	}
}
package Pack6;

public class JoinExample {

	public static void main(String[] args) {
		SumThread sumThread = new SumThread();
		sumThread.start();
		
		try {
			sumThread.join();    //SumThread가 종료될때까지 메인스레드를 일시 정지시킨다.
		} catch (InterruptedException e) {}
		
		System.out.println("1~100까지의 합: " + sumThread.getSum());
	}
}



1~100까지의 합: 5050

 

 

12.6.4 wait(), notify(), notifyAll()

스레드 간의 협업

 

경우에 따라서는 두 개의 스레드를 교대로 번갈아가며 실행해야 할 경우가 있다. 정확한교대 작업이 필요할 경우, 

자신의 작업이 끝나면 상대방 스레드를 일시 정지 상태에서 풀어주고, 자신은 일시 정지 상태로 만드는 것이다.

 

 

12.7 데몬 스레드

618

데몬스레드는 주 스레드의 작업을 돕는 보조적인 역할을 수행하는 스레드이다. 주 스레드가 종료되면 데몬 스레드는 강제적으로 자동 종료되는데, 그 이유는 주 스레드의 보조역할을 수행하므로 주 스레드가 종료되면 데몬 스레드의 존재 의미가 없어지기 때문이다.

 

 

다음 예제는 1초 주기로 save() 메소드를 자동 호출하도록 AutoSaveThread를 작성하고, 메인 스레드가 3초 후 종료되면 AutoSaveThread도 같이 종료되도록 데몬스레드를 만들었다.

package Pack6;

public class AutoSaveThread extends Thread {
	public void save() {
		System.out.println("작업 내용을 저장함");
	}
	
	@Override
	public void run() {
		while(true) {
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				break;
			}
			save();
		}
	}
}
package Pack6;

public class DaemonExample {

	public static void main(String[] args) {
		AutoSaveThread auto = new AutoSaveThread();
		auto.setDaemon(true);
		auto.start();
		
		try {
			Thread.sleep(3000);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		System.out.println("메인 스레드 종료");
	}

}

만일 여기서 데몬 스레드를 만들지 않으면 save()는 계속 실행된다.

 

 

12.5 스레드 상태

스레드 상태를 출력하는 예제를 확인해보자.

 

타켓스레드의 상태를 출력하는 스레드

package Ex013;

public class StatePrintThread extends Thread {
	private Thread targetThread;
	
	public StatePrintThread(Thread targetThread) {
		this.targetThread = targetThread;
	}
	
	public void run() {
		while(true) {
			//스레드 상태 얻기
			Thread.State state = targetThread.getState();
			
			//객체 생성 상태일 경우 실행 대기 상태로 만든다.
			if(state == Thread.State.NEW) {
				targetThread.start();
			}
			
			//종료 상태일 경우 while문을 종료한다.
			if (state == Thread.State.TERMINATED) {
				break;
			}
			try {
				//0.5초간 일시 정지
				Thread.sleep(500);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}
}

타겟스레드

package Ex013;

public class TargetThread extends Thread {
	@Override
	public void run() {
		for(long i=0; i<100000000; i++) {}
		
		try {
			//1.5초간 일시 정지
			Thread.sleep(1500);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
		for(long i=0; i<100000000; i++) {}
	}
}

실행클래스 

package Ex013;

public class ThreadStateExample {

	public static void main(String[] args) {
		StatePrintThread s = new StatePrintThread(new TargetThread());
		s.start();
	}
}





타겟 스레드 상태: NEW
타겟 스레드 상태: TIMED_WAITING
타겟 스레드 상태: TIMED_WAITING
타겟 스레드 상태: TIMED_WAITING
타겟 스레드 상태: TERMINATED

 

 

12.4 동기화 메소드와 동기화 블럭

12.4.1 공유객체를 사용할 때의 주의할 점

싱글스레드 프로그램에서는 한 개의 스레드가 객체를 독차지해서 사용하면 되지만, 멀티 스레드 프로그램에서는 스레드들이 객체를 공유해서 작업해야 하는 경우가 있다.

이 경우 스레드1를 사용하던 객체가 스레드2에 의해 상태가 변경될 수 있기 때문에 스레드1이 의도했던 것과는 다른 결과를 산출할 수 있다.

 

스레드1과 2를 공유하는 객체를 공유객체라고 한다.

스레드 1에서 공유객체 a를 100으로 선언하고 2초간 일시정지한다.

스레드 2에서 1초 뒤에 공유객체 a를 50으로 선언하고 2초간 일시정지한다.

전체시간 중 2초가 지난뒤 스레드 1에서 a를 출력하면 100이 아닌 50이 출력된다.

 

아래예제를 보면 Client 객체를 공유해서 통장잔고가 마이너스 값으로 바뀐다.

package Ex013;

public class Account {
	private int balance = 1000;

	public int getBalance() {
		return balance;
	}

	public void withdraw(int money) {
		if( balance >= money ) {
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			balance -= money;
		} else {
			System.out.println("잔고가 없습니다.");
		}
	}
}
package Ex013;

public class Client implements Runnable {
	private Account account;
	
	public Client(Account account) {
		this.account = account;
	}

	@Override
	public void run() {
		while( account.getBalance() > 0 ) {
			int money = (int)(Math.random() * 3 + 1 ) * 100;
			
			account.withdraw(money);
			System.out.println("통장잔고: " + account.getBalance());
		}
	}
}
package Ex013;

public class NameEx {

	public static void main(String[] args) {
		Account account = new Account();
		Client client1 = new Client(account);
		Client client2 = new Client(account);
		
		Thread t1 = new Thread(client1);
		Thread t2 = new Thread(client2);
		
		t1.start();
		t2.start();
	}

}



통장잔고: 700
통장잔고: 700
통장잔고: 300
통장잔고: 300
통장잔고: -300
통장잔고: -300

 

12.4.2 동기화 메소드 및 동기화 블록

 

스레드가 사용중인 객체를 다른 스레드가 변경할 수 없도록 하려면 스레드 작업이 끝날 때까지 객체에 잠금을 걸어서 다른 스레드가 사용할 수 없도록 해야 한다.

멀티 스레드 프로그램에서 단 하나의 스레드만 실행할 수 있는 코드 영역을 임계 영역(critical section)이라고 한다.

자바는 임계영역을 지정하기 위해 동기화(synchronized) 메소드와 동기화 블록을 제공한다.

 

스레드가 객체 내부의 동기화 메소드 또는 블록에 들어가면 즉시 객체에 잠금을 걸어 다른 스레드가 임계영역 코드를 실행하지 못하도록 한다.

동기화 메소드를 만드는 방법은 다음과 같이 메소드 선언을 하면 된다.

public synchronized void method() { }

 

동기화 메소드는 메소드 전체 내용이 임계 영역이므로 스레드가 동기화 메소드를 실행하는 즉시 객체에는 잠금이 일어나고, 스레드가 동기화 메소드를 실행 종료하면 잠금이 풀린다.

메소드 전체 내용이 아니라, 일부 내용만 임계영역으로 만들고 싶다면 다음과 같이 동기화 블록을 사용한다.

public void method() {
    //여러 스레드가 실행 가능한 영역
    
    synchronized( 공유객체 ) {
        //임계영역 : 단 하나의 스레드만 실행
    }
    
    //여러 스레드가 실행 가능한 영역
}

동기화 블록의 외부 코드들은 여러 스레드가 동시에 실행할 수 있지만, 동기화 블록의 내부 코드는 임계 영역이므로 한 번에 한 스레드만 실행할 수 있고 다른 스레드는 실행할 수 없다.

 

 

동기화 메소드를 이용해서 위의 예제를 다시 실행하면 다음과 같다.

package Ex013;

public class Account {
	private int balance = 1000;

	public int getBalance() {
		return balance;
	}
	
    //동기화 메소드 사용
	public synchronized void withdraw(int money) {
		if( balance >= money ) {
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			balance -= money;
		} else {
			System.out.println("잔고가 없습니다.");
		}
	}
}
package Ex013;

public class NameEx {

	public static void main(String[] args) {
		Account account = new Account();
		Client client1 = new Client(account);
		Client client2 = new Client(account);
		
		Thread t1 = new Thread(client1);
		Thread t2 = new Thread(client2);
		
		t1.start();
		t2.start();
	}

}


통장잔고: 800
통장잔고: 700
통장잔고: 500
통장잔고: 300
통장잔고: 100
통장잔고: 0
잔고가 없습니다.
통장잔고: 0

 

공유객체를 사용해서 스레드를 사용해보자.

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

그동안에 User2스레드가 memory 필드값을 50으로 변경한다. 2초가 지나 User1 스레드가 다시 실행 상태가 되어

memory 필드의 값을 출력하면 User2가 저장한 50이 나온다.

//공유 객체

package Ex013;

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);
	}
}
//User1 스레드

package Ex013;

public class User1 extends Thread {
	private Calculator cal;
	
	public void setCalculator(Calculator cal) {
		this.setName("User1");	//스레드 이름을 User1로 설정
		this.cal = cal;			//공유객체인 Cal를 필드에 저장
	}
	
	@Override
	public void run() {
		cal.setMemory(100);		//공유객체인 Cal의 메모리에 100을 저장
	}
}
//User2 스레드

package Ex013;

public class User2 extends Thread {
	private Calculator cal;
	
	public void setCalculator(Calculator cal) {
		this.setName("User2");	//스레드 이름을 User2로 설정
		this.cal = cal;			//공유객체인 Cal를 필드에 저장
	}
	
	@Override
	public void run() {
		cal.setMemory(50);		//공유객체인 Cal의 메모리에 100을 저장
	}
}
//메인스레드

package Ex013;

public class MainThreadExample {

	public static void main(String[] args) {
		Calculator cal = new Calculator();
		
		User1 user1 = new User1();	//User1 스레드 생성
		user1.setCalculator(cal);	//공유 객체 설정
		user1.start();				//User1 스레드 시작
		
		User2 user2 = new User2();	//User2 스레드 생성
		user2.setCalculator(cal);	//공유 객체 설정
		user2.start();				//User2 스레드 시작
	}
}

User1: 50
User2: 50

 

이번엔 동기화 메소드로 공유 객체를 수정해서 사용해보자.

//동기화 메소드 사용한 공유 객체

package Ex013;

public class Calculator2 {
	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);
	}
}

메인 클래스에서 실행하면 아래와 같은 결과가 나온다.

User1: 100
User2: 50

 

12.6.4의 스레드간 협업을 다시 살펴보자.

611참고

데이터를 저장하는 스레드(생산자 스레드)가 데이터를 저장하면, 데이터를 소비하는 스레드(소비자 스레드)가 데이터를 읽고 처리하는 교대 작업을 구현하자.

 

생산자 스레드는 소비자 스레드가 읽기 전에 새로운 데이터를 두 번 생성하면 안되고(setData() 메소드를 두 번 실행x)

소비자 스레드는 생산자 스레드가 새로운 데이터를 생성하기 전에 이전 데이터를 두 번 읽어서도 안 된다.

(getData() 두번 실행 x)

 

구현 방법은 공유객체(DataBox)에 데이터를 저장할 수 있는 data 필드의 값이 null이면 생산자 스레드를 실행 대기 상태로 만들고, 소비자 스레드를 일시 정지 상태로 만드는 것이다.

반대로 data 필드의 값이 null이 아니면 소비자 스레드를 실행 대기 상태로 만들고, 생산자 스레드를 일시 정지 상태로 만들면 된다.

 

12.8 스레드 그룹

620

스레드 그룹은 관련된 스레드를 묶어서 관리할 목적으로 이용된다. JVM이 실행되면 system 스레드 그룹을 만들고,

JVM 운영에 필요한 스레드들을 생성해서 system 스레드 그룹에 포함시킨다. 그리고 system의 하위 스레드 그룹으로 main을 만들고 메인 스레드를 main 스레드 그룹에 포함시킨다. 

스레드는 반드시 하나의 스레드 그룹에 포함되는데, 명시적으로 스레드 그룹에 포함시키지 않으면 기본적으로 자신을 생성한 스레드와 같은 스레드 그룹에 속하게 된다.

 

12.8.1 스레드 그룹 이름 얻기

ThreadGroup group = Thread.currentThread().getThreadGroup();
String groupName = group.getName();


//모든 스레드에 대한 정보 얻기
Map<Thread, StackTraceElement[]> map = Thread.getAllStackTraces();

 

다음 예제는 현재 실행하고 있는 스레드의 이름과 데몬 여부 그리고 속한 스레드 그룹 이름이 무엇인지 출력한다.

package Ex013;

import java.util.Map;
import java.util.Set;

public class ThreadInfoExample {

	public static void main(String[] args) {
		AutoSaveThread autoSaveThread = new AutoSaveThread();
		autoSaveThread.setName("AutoSaveThread");
		autoSaveThread.setDaemon(true);
		autoSaveThread.start();
		
		Map<Thread, StackTraceElement[]> map = Thread.getAllStackTraces();
		Set<Thread> threads = map.keySet();
		
		//스레드를 하나씩 가져와 루핑시킨다.
		for(Thread thread : threads) {
			System.out.println("Name: "+thread.getName() +
					( (thread.isDaemon()) ? "(데몬)" : "(주)" ));
			System.out.println("\t" + "소속그룹: " + thread.getThreadGroup().getName());
			System.out.println();
		}
	}
}



Name: Signal Dispatcher(데몬)
	소속그룹: system

Name: Attach Listener(데몬)
	소속그룹: system

Name: AutoSaveThread(데몬)
	소속그룹: main

Name: Reference Handler(데몬)
	소속그룹: system

Name: Common-Cleaner(데몬)
	소속그룹: InnocuousThreadGroup

Name: Finalizer(데몬)
	소속그룹: system

Name: main(주)
	소속그룹: main

 

12.8.2 스레드 그룹 생성

아래와 같이 그룹을 생성한다.

ThreadGroup tg = new Threadgroup(String name);
Threadgroup tg = new ThreadGroup(ThreadGroup parent, String name);

 

12.8.3 스레드 그룹의 일괄 interrupt()

622

스레드를 스레드 그룹에 포함시키면 스레드 그룹에서 제공하는 interrupt() 메소드를 이용하면 그룹 내에 포함된 모든 스레드들을 일괄 interrupt 할 수 있다. 

예를 들어 10개의 스레드들을 모두 종료시키기 위해 각 스레드에서 interrupt() 메소드를 10번 호출하는 것보다 효율적이다.

이 메소드 외에도 여러개의 메소드를 동시에 사용할 수 있다.

 

다음 예제는 스레드 그룹을 생성하고, 정보를 출력해본다. 그리고 3초 후에 스레드 그룹의 interrupt() 메소드를 호출해서 스레드 그룹에 포함된 모든 스레드들을 종료시킨다.

//InterruptException이 발생할 때 스레드가 종료되도록 함

package Ex013;

public class WorkThread extends Thread {
	public WorkThread(ThreadGroup threadGroup, String threadName) {
		super(threadGroup, threadName);
	}
	
	@Override
	public void run() {
		while(true) {
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				System.out.println(getName() + " interrupted");
				break;
			}
		}
		System.out.println(getName() + "종료됨.");
	}
}
//스레드 그룹을 이용한 일괄 종료 예제

package Ex013;

public class ThreadGroupExample {

	public static void main(String[] args) {
		ThreadGroup myGroup = new ThreadGroup("myGroup");
		WorkThread workThreadA = new WorkThread(myGroup, "workThreadA");
		WorkThread workThreadB = new WorkThread(myGroup, "workThreadB");
		
		workThreadA.start();
		workThreadB.start();
		
		System.out.println("main 스레드 그룹의 list() 메소드 출력 내용");
		ThreadGroup mainGroup = Thread.currentThread().getThreadGroup();
		mainGroup.list();
		System.out.println();
		
		try { Thread.sleep(3000); } catch (InterruptedException e) {}
		
		System.out.println("myGroup 스레드 그룹의 interrupt() 메소드 호출");
		myGroup.interrupt();
	}

}

실행결과를 보면 list() 메소드는 현재 스레드 그룹의 이름과 최대 우선순위를 헤더로 출력하고, 그 아래에 현재 스레드 그룹에 포함된 스레드와 하위 스레드 그룹의 내용을 보여준다.

스레드는 [스레드이름, 우선순위, 소속그룹명] 으로 출력되는 것을 볼 수 있다.

또한 myGroup.interrupt() 를 호출하면 myGroup 에 포함된 두 스레드에서 InterruptedException이 발생되어

스레드가 모두 종료되는 것을 알 수 있다.

 

 

 

-문제: 숫자 야구 게임 만들기

1. 컴퓨터는 중복되지 않는 1~9까지의 임의의 수 세 자리를 저장한다

2. 사용자는 1~9까지의 임의의 수 세 자리를 입력한다

    2.1. 자리 상관없이 컴퓨터의 숫자와 사용자의 숫자가 일치하면 ball 1 증가

    2.2. 같은 자리의 숫자가 일치하면 strike 1 증가 

3. 총 9회차까지 있고 9회차 안에 strike가 3이 나오면 사용자 승리

  나오지 못하면 컴퓨터 승리

 

ex) 

1회차

컴퓨터 : 1 5 3

사용자 입력 첫 번째 값 : 5

사용자 입력 두 번째 값 : 2

사용자 입력 세 번째 값 : 3

strike : 1 / ball : 1

 

2회차

컴퓨터 : 4 5 6

사용자 입력 첫 번째 값 : 4

사용자 입력 두 번째 값 : 6

사용자 입력 세 번째 값 : 5

strike : 1 / ball : 2

 

3회차

컴퓨터 : 7 8 9

사용자 입력 첫 번째 값 : 4 

사용자 입력 두 번째 값 : 6

사용자 입력 세 번째 값 : 5

strike : 0 / ball : 0

 

4회차

컴퓨터 : 1 2 3

사용자 입력 첫 번째 값 : 1

사용자 입력 두 번째 값 : 2

사용자 입력 세 번째 값 : 3

strike : 3 / ball : 0

사용자가 승리했습니다.

package Ex013;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Random;

public class Baseball {

	public static void main(String[] args) throws Exception {
		System.out.println("숫자 야구 게임을 시작합니다.");
		int x = 1;
		loop: while ( x <= 6 ) {
			System.out.println(x + "회차");
			HashSet<Integer> baseBallResult = new HashSet<Integer>(3);
			ArrayList<Integer> arr = new ArrayList<Integer>();
			Random random = new Random();
			
			//컴퓨터가 임의의 숫자 3개를 저장
			while( baseBallResult.size() != 3 ) {
				baseBallResult.add(random.nextInt(9)+1);
			}
			arr.addAll(baseBallResult);
			System.out.println(arr.toString());
			
			//-----사용자에게 임의의 숫자 3개를 입력받음-----
			BufferedReader result = null;
			int strike = 0;
			int ball = 0;
			
			for(int i=1; i<=3; i++) {
				result = new BufferedReader(new InputStreamReader(System.in));
				System.out.print(i+"번째 수(1~9) : ");
				
				//사용자에게 입력받은 숫자를 저장
				int data1 = result.read()-'0';
				
				//컴퓨터 숫자와 사용자 숫자 비교
				if ( data1 == arr.get(i-1) ) {
					strike += 1;
				} else if (arr.contains(data1)) {
					ball += 1;
				} else {
					continue;
				}	
				
				if(strike == 3) {
					System.out.println("당신이 이겼습니다.");
					break loop;
				}
				
			}
			System.out.println("Strike : " + strike + " / Ball : " + ball);
			x++;
		}
	}
}


숫자 야구 게임을 시작합니다.
1회차
[8, 1, 6]
1번째 수(1~9) : 1
2번째 수(1~9) : 2
3번째 수(1~9) : 3
Strike : 0 / Ball : 1
2회차
[8, 9, 2]
1번째 수(1~9) : 9
2번째 수(1~9) : 2
3번째 수(1~9) : 8
Strike : 0 / Ball : 3
3회차
[4, 9, 6]
1번째 수(1~9) : 5
2번째 수(1~9) : 5
3번째 수(1~9) : 5
Strike : 0 / Ball : 0
4회차
[4, 5, 6]
1번째 수(1~9) : 1
2번째 수(1~9) : 2
3번째 수(1~9) : 3
Strike : 0 / Ball : 0
5회차
[4, 8, 2]
1번째 수(1~9) : 8
2번째 수(1~9) : 8
3번째 수(1~9) : 2
Strike : 2 / Ball : 1
6회차
[5, 6, 7]
1번째 수(1~9) : 5
2번째 수(1~9) : 4
3번째 수(1~9) : 2
Strike : 1 / Ball : 0

 

 

 

 

 

728x90
반응형
Comments