열 일곱번째, 스레드
개요: 필자가 스레드를 공부하면서 정리한 게시물이다.
스레드(thread)
개념은 코드의 실행 흐름을 말한다. 프로세스 내에 스레드가 두 개라면 두 개의 코드 실행 흐름이 생긴다는 의미이다.
- 멀티 프로세스들은 서로 독립적이므로 하나의 프로세스에서 오류가 발생해도 다른 프로세스에게 영향을 미치지 않는다.
- 멀티 스레드는 프로스세 내부에서 생성되기 때문에 다른 스레드에게 영향을 미친다.
메인 스레드
모든 자바 프로그램은 메인 스레드가 main() 메소드를 실행하면서 시작된다. 메인 스레드는 main() 메소드의 첫
코드부터 순차적으로 실행하고 마지막 코드를 실행하거나 return문을 만나면 종료된다.
이때 메인 스레드가 작업 스레드보다 먼저 종료되더라도 작업 스레드가 계속 실행 중이라면 프로세스는 종료되지 않는다.
작업 스레드 생성과 실행
자바는 작업 스레드도 객체로 관리하므로 클래스가 필요하다.
Thread thread =new Thread(new Runnable() {
@Override
public void run() {
System.out.println("ad");
}
});
thread.start();
Runnable은 스레드가 작업을 실행할 때 사용하는 인터페이스이다.
이때 run()은 스레드가 실행할 코드를 가지고 있어야 된다. 문법은 다음과 위와 같다.
public class BeepPrintExample {
public static void main(String[] args) {
Thread thread=new Thread() {
@Override
public void run() {
Toolkit toolkit = Toolkit.getDefaultToolkit();
for (int i = 0; i < 5; i++) {
toolkit.beep();
System.out.println("a");
try {
Thread.sleep(500);
} catch (Exception e) {
}
}
}
};
thread.start();
for (int i = 0; i < 5; i++) {
System.out.println("띵");
try {
Thread.sleep(500);
} catch (Exception e) {
}
}
}
}
public class BeepPrintExample {
public static void main(String[] args) {
Thread thread=new Thread(new Runnable() {
@Override
public void run() {
Toolkit toolkit = Toolkit.getDefaultToolkit();
for (int i = 0; i < 5; i++) {
toolkit.beep();
System.out.println("a");
try {
Thread.sleep(500);
} catch (Exception e) {
}
}
}
});
thread.start();
for (int i = 0; i < 5; i++) {
System.out.println("띵");
try {
Thread.sleep(500);
} catch (Exception e) {
}
}
}
}
비프음을 발생시켜주는 코드를 다음과 같이 2가지 방법을 볼 수 있는데 첫 번째 방법인 runnable 불러오지 않는것이 많이 쓰인다고 한다.
코드결과는 다음과 같다.
스레드 이름
package test.java0.beepPrintExample;
public class ThreadNameExample {
public static void main(String[] args) {
Thread mainThread = Thread.currentThread();
System.out.println(mainThread.getName());
for(int i=0; i<3; i++){
Thread threadA=new Thread(){
@Override
public void run() {
System.out.println(getName());
}
};
threadA.start();
}
Thread thread=new Thread(){
@Override
public void run() {
System.out.println(getName());
}
};
thread.setName("chltmdgh");
thread.start();
}
}
다음과 같이 getName()을 이용해 이름을 불러오고 setName()을 이용해 이름을 수정할 수 있다. 또한 메인스레드는 main이라는 이름을 가지고 있다. 기본적으로 작업 스레드 이름은 Thread-n을 가지고 있다.
스레드 상태
스레드 객체를 생성하고 start() 메소드를 호출하면 곧바로 실행 되는 것이 아니라 실행 대기 상태(RUNNABLE)가 된다. 이때 run() 메소드를 실행하는 이때를 실행 상태(RUNNING)라고 한다. 하지만 스케줄링에 의해 다시 실행 대기 상태로 돌아갈 수 있다. run() 메소드를 종료 시키면 종료 상태(TERMINATED)라고 한다.
실행 상태에서 일시 정지상태로 가고 일시 정지 상태는 실행대기 상태로 간다.
다음과 같이 메소스들을 보자!
일시정지로 보내는 메소드
sleep() : 주어진 시간 동안 스레드를 일시 정지 상태로 만든다. 주어진 시간이 지나면 자동적으로 실행 대기 상태가 된다.
join(): 이 메소드를 호출한 스레드는일시 정지 상태가 된다. 실행 대기 상태가 되려면 이 메소드를 가진 스레드가 종료 되어야 한다.
wait(): 동기화 블록 내에서 스레드를 일시 정지 상태로 만든다.
일시 정지에서 벗어나는 메소드
interrupt(): 일시 정지 상태일 경우, interruptedException을 발생시켜 실행 대기 상태 또는 종료 상태로 만든다.
notify(),notifyAll(): wait() 메소드로 인해 일시 정지 상태인 스레드를 실행 대기 상태로 만든다.
실행대기로 보내는 메소드
yield(): 실행 상태에서 다른 스레드에게 실행을 양보하고 실행 대기 상태가 된다.
package test.java0.workthread;
public class WorkThread extends Thread{
public boolean work=true;
public WorkThread(String name){
setName(name);
}
@Override
public void run() {
while(true){
if(work){
System.out.println(getName() + "작업처리");
}else{
Thread.yield();
}
}
}
}
package test.java0.workthread;
public class YieldExample {
public static void main(String[] args) {
WorkThread workThreadA=new WorkThread("workThreadA");
WorkThread workThreadB=new WorkThread("workThreadB");
workThreadA.start();
workThreadB.start();
try{
Thread.sleep(5000);
}catch(InterruptedException e){
workThreadA.work=false;
}
try{
Thread.sleep(10000);
}catch(InterruptedException e){
workThreadA.work=true;
}
}
}
예시를 잘보자! 헷갈린다.
스레드 동기화
스레드가 사용중인 객체를 스레드가 변경할 수 없도록 하려면 스레드 작업이 끝날 때까지 객체에 잠금을 걸면 된다.
이를 자바는 동기화 메소드와 블록을 제공한다.
public synchronized void method(){
//단 하나의 스레드만 실행하는 영역
}
public void method1(){
synchronized (/*공유객체*/){
//단 하나의 스레드만 실행하는 영역
}
// 여러 스레드가 실행할 수 있는 영역
}
동기화 메소드와 블록의 문법은 다음과 같다.
package test.java0.calculator;
public class Calculator {
private int memory;
public int getMemory() {
return memory;
}
public synchronized void setMemory1(int memory) {
this.memory = memory;
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
System.out.println(e);
}
System.out.println(Thread.currentThread().getName() + ":" + this.memory);
}
public void setMemory2(int memory) {
synchronized (this) {
this.memory = memory;
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
System.out.println(e);
}
System.out.println(Thread.currentThread().getName() + ":" + this.memory);
}
}
}
package test.java0.calculator;
public class User1Thread extends Thread{
private Calculator calculator;
public User1Thread(){
setName("User1Thread");
}
public void setCalculator(Calculator calculator){
this.calculator=calculator;
}
@Override
public void run() {
calculator.setMemory1(100);
}
}
package test.java0.calculator;
public class User2Thread extends Thread{
private Calculator calculator;
public User2Thread(){
setName("User2Thread");
}
public void setCalculator(Calculator calculator){
this.calculator=calculator;
}
@Override
public void run() {
calculator.setMemory2(150);
}
}
package test.java0.calculator;
public class SynchronizedExample {
public static void main(String[] args) {
Calculator calculator=new Calculator();
User1Thread user1Thread= new User1Thread();
user1Thread.setCalculator(calculator);
user1Thread.start();
User2Thread user2Thread= new User2Thread();
user2Thread.setCalculator(calculator);
user2Thread.start();
}
}
다음과 같은 코드를 통해 2초간 일시 정지를 해도 객체가 잠궈줘 100 150이라는 값이 안전하게 출력이 된다.
스레드 안전 종료
방법은 interrupt()와 조건이용방법이다.
package test.java0.printthread;
public class PrintThread extends Thread{
private boolean stop;
public void setStop(boolean stop) {
this.stop = stop;
}
@Override
public void run() {
while(!stop){
System.out.println("실행");
}
System.out.println("리소스 정리");
System.out.println("실행 종료");
}
}
package test.java0.printthread;
import test.java0.workobject.ThreadA;
public class SafeStopExample {
public static void main(String[] args) {
PrintThread thread =new PrintThread();
thread.start();
try{
Thread.sleep(3000);
}catch (InterruptedException e){}
thread.setStop(true);
}
}
조건은 간단하다.
public class PrintThread extends Thread{
@Override
public void run() {
try {
while (true) {
System.out.println("실행");
Thread.sleep(1); //일시 정지
}
}catch (InterruptedException e){}
System.out.println("리소스 정리");
System.out.println("실행 종료");
}
}
package test.java0.printerthread2;
public class InterruptExample {
public static void main(String[] args) {
PrintThread thread= new PrintThread();
thread.start();
try{
Thread.sleep(1000);
}catch(InterruptedException e){
System.out.println(e);
}
thread.interrupt();
}
}
interrupt 메서드 방식은 이러하다.
데몬 스레드
메인 스레드가 종료되면 다른 보조적인 역할을 수행하는 스레드도 종료하라는 의미이다. 예시는 다음과 같다.
package test.java0.daemon;
public class AutoSaveThread extends Thread{
void save(){
System.out.println("작업 내용 저장 ");
}
@Override
public void run() {
while(true){
try{
Thread.sleep(1000);
}catch (InterruptedException e){
break;
}
save();
}
}
}
package test.java0.daemon;
import ex.java3.A;
public class DaemonExample {
public static void main(String[] args) {
AutoSaveThread autoSaveThread = new AutoSaveThread();
autoSaveThread.setDaemon(true);
autoSaveThread.start();
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
}
System.out.println("메인 스레드 종료");
}
}
autoSaveThread.setDaemon(true)가 키워드이다. 이 키워드가 데몬 스레드를 만든다.
스레드폴
병렬 작업 증가로 인한 스레드의 폭증을 막으려면 이것을 사용하는 것이 좋다.
이것은 작업 처리에 사용되는 스레드를 제한된 개수만큼 정해 놓고 작업 큐에 들어오는 작업들을 스레드가 하나씩 맡아 처리하는 방식이다.