이것이 자바다 정리 #10 멀티 스레드 1 (프로세스와 스레드, 멀티 스레드, 동시성과 병렬성, 스케줄링, 스레드 이름, 동기화 메소드, 임계 영역, 스레드의 상태)
이것이 자바다 책을 참고하였습니다.
main()
메소드를 실행하며 시작된다. main()
메소드를 실행시키는 주체는 메인 스레드이다. Runnable
인터페이스를 상속한 클래스를 만들기Thread
클래스를 상속한 클래스를 만들기둘 다 결국
run()
메소드를 오버라이드하게 되어있다.
public class ImplementRunnable implements Runnable{
@Override
public void run() {
System.out.println("ImplementRunnable.run");
}
}
어려울 것 없이 run()
메소드만 구현해주면 된다.
public class Main {
public static void main(String[] args) {
ImplementRunnable task = new ImplementRunnable();
Thread thread = new Thread(task);
thread.start();
}
}
Runnable
인터페이스를 상속한 클래스는 task
(작업)의 역할을 하게되고, 구현한 .run()
메소드는 해당 스레드의 .start()
메소드를 실행하면 수행된다.
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Main.run");
}
});
thread.start();
}
}
위와 같이 즉시 Runnable
인터페이스의 익명객체를 만들어 스레드를 만들 수도 있다.
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(() -> System.out.println("Main.run"));
thread.start();
}
}
위와 같이 더 간결하게 람다 표현식을 이용할 수도 있다.
public class BeepTask implements Runnable{
Toolkit toolkit = Toolkit.getDefaultToolkit();
@Override
public void run() {
for(int i=0; i<5; i++) {
toolkit.beep();
try{
Thread.sleep(500);
}catch (Exception e) {
}
}
}
}
public class BeepPrintTask implements Runnable{
@Override
public void run() {
for(int i=0; i<5; i++) {
System.out.println("삐");
try{
Thread.sleep(500);
}catch (Exception e) {
}
}
}
}
public class BeepExample {
public static void main(String[] args) {
BeepTask beepTask = new BeepTask();
BeepPrintTask beepPrintTask = new BeepPrintTask();
Thread beepThread = new Thread(beepTask);
Thread beepPrintThread = new Thread(beepPrintTask);
beepThread.start();
beepPrintThread.start();
}
}
실행하면
"삐"
가 출력되는 동시에 비프음도 난다.
Runnable
인터페이스를 상속하여 .run()
메소드를 구현하지 않고, Thread
클래스를 상속하여 .run()
메소드를 오버라이드하여도 된다.public class ExtendThread extends Thread {
@Override
public void run() {
System.out.println("ExtendThread.run");
}
}
public class Main {
public static void main(String[] args) {
Thread thread = new ExtendThread();
thread.start();
}
}
Thread
클래스를 상속하고 .run()
메소드를 오버라이드하여도 잘 동작한다.public static void main(String[] args) {
Thread thread = new Thread() {
@Override
public void run() {
System.out.println("Main.run");
}
};
thread.start();
}
위처럼 익명 자식 객체를 이용해도 무방하다.
public static void main(String[] args) {
Thread thread = new Thread(() -> System.out.println("Main.run"));
thread.start();
}
람다식을 이용하면 더 간결하다.
Thread-n
이라는 이름으로 명명된다.n
은 스레드의 번호이다.main
스레드의 이름은 main
이다..setName()
메소드를 이용하면 된다..getName()
메소드를 이용하면 된다.Thread.currentThread()
메소드로 현재 실행되고 있는 스레드의 참조를 얻을 수 있다.public class ThreadA extends Thread{
public ThreadA() {
this.setName("ThreadA");
}
@Override
public void run() {
for (int i = 0; i < 2; i++) {
System.out.println(this.getName() + "가 출력한 내용");
}
}
}
public class ThreadB extends Thread{
@Override
public void run() {
for (int i = 0; i < 2; i++) {
System.out.println(this.getName() + "가 출력한 내용");
}
}
}
public class Main {
public static void main(String[] args) {
Thread mainThread = Thread.currentThread();
System.out.println("프로그램 시작 스레드 이름: " + mainThread.getName());
Thread threadA = new ThreadA();
System.out.println("작업 스레드 이름: " + threadA.getName());
threadA.start();
Thread threadB = new ThreadB();
System.out.println("작업 스레드 이름: " + threadB.getName());
threadB.start();
}
}
하나의 코어에서 멀티스레드가 번갈아가며 실행되는 성질
싱클 코어 CPU를 이용한 동시성은 사용자 입장에서 병렬성처럼 보이기 쉽지만, 엄연히 동시성이다.
멀티 코어에서 개별 스레드를 동시에 실행하는 성질
.setPriority()
메소드를 이용하면 된다.가독성을 위해
Thread.MAX_PRIORITY = 10
,Thread.NORM_PRIORITY = 5
,Thread.MIN_PRIORITY = 1
등의 상수도 정의되어 있다.
public class CalcThread extends Thread{
public CalcThread(String name) {
setName(name);
}
@Override
public void run() {
for (int i = 0; i < 200000000; i++) {
}
System.out.println(getName());
}
}
public class Main {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
Thread thread = new CalcThread("thread" + i);
if(i != 9) {
thread.setPriority(Thread.MIN_PRIORITY);
} else {
thread.setPriority(Thread.MAX_PRIORITY);
}
thread.start();
}
}
}
우선순위가 가장 높은
thread9
가 먼저 출력된 것을 볼 수 있다.
public class Store {
private int value;
public void setValueAndPrint(int value) {
this.value = value;
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ": " + this.value);
}
}
public class User extends Thread{
Store userStore;
int valueToStore;
public User(Store userStore, String userName, int valueToStore) {
this.setName(userName);
this.userStore = userStore;
this.valueToStore = valueToStore;
}
@Override
public void run() {
userStore.setValue(valueToStore);
}
}
public class Main {
public static void main(String[] args) {
Store sharedStore = new Store();
Thread user1Thread = new User(sharedStore, "user1", 50);
user1Thread.start();
Thread user2Thread = new User(sharedStore, "user2", 100);
user2Thread.start();
}
}
"user1"
은 50
을 저장소에 저장하고 출력하고 싶다."user2"
는 100
을 저장소에 저장하고 출력하고 싶다.위에서 일어난 일은 "user1"
이 값을 저장하고 출력을 기다리는 사이 2초라는 시간에 "user2"
가 값을 수정해서 둘 다 100
을 출력하고 말았다. 이를 해결하기 위해서는 "user1"
이 해당 객체를 쓰고있을 때는 해당 객체를 잠궈놓거나 하는 등의 처리가 필요하다.
synchronized
) 메소드 혹은 동기화 블록을 제공한다.synchronized
키워드를 제공하고 이 키워드는 인스턴스 메소드, 정적 메소드 등 어디든 붙일 수 있다.임계 영역에 대해 배웠으니, 이제 값을 저장하고 출력하는 메소드 부분을 임계 영역으로 지정하여 처음 의도대로 프로그램을 실행시켜보자.
public class Store {
private int value;
public synchronized void setValueAndPrint(int value) {
this.value = value;
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ": " + this.value);
}
}
해당 스레드가 임계 영역을 모두 실행할 때까지 다른 스레드들은 기다리고 접근 가능한 시점에 접근한다.
public class Store {
private int value;
public void setValueAndPrint(int value) {
synchronized (this) {
this.value = value;
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ": " + this.value);
}
}
}
위와 같이, 핵심 루틴만 임계 영역으로 만들어 놓을 수도 있다.
Thread.start()
메소드를 수행하면, 스레드가 곧바로 실행 상태가 되는 것이 아니라 스레드는 실행 대기 상태가 된다..run()
메소드를 실행한다.Thread.getState()
에 의해 조회했을 때는 그냥 RUNNABLE
상태로 나온다..run()
메소드를 할당 시간 (TimeSlice) 혹은 우선 순위 (Priority) 에 맞게 실행할 것이다..run()
메소드 실행이 끝나면 실행 종료 상태가 된다.WAITING
, TIMED WAITING
, BLOCKED
3가지가 있다..getState()
메소드를 이용하여 확인할 수 있다.NEW
): 스레드 객체가 생성되고 아직 .start()
메소드가 호출되지 않은 상태RUNNABLE
): .start()
가 호출된 후, 스케줄링에 의해 선택될 때까지 실행 대기WAITING
, TIMED_WAITING
, BLOCKED
)WAITING
: 다른 스레드가 실행 대기 상태로 가라고 명령할 때까지 기다리는 상태TIMED_WAITING
: 주어진 시간만큼 대기하고 있는 상태BLOCKED
: 사용하고자 하는 객체의 락이 풀릴 때까지 기다리는 상태 (Critical Section 접근)TERMINATED
): 실행을 마친 상태public class Main {
public static void main(String[] args) throws InterruptedException {
Store sharedStore = new Store();
Thread user1Thread = new User(sharedStore, "user1", 50);
Thread.State user1ThreadStateBeforeStart = user1Thread.getState();
System.out.println("user1ThreadStateBeforeStart = " + user1ThreadStateBeforeStart);
user1Thread.start();
Thread.State user1ThreadStateAfterStart = user1Thread.getState();
System.out.println("user1ThreadStateAfterStart = " + user1ThreadStateAfterStart);
Thread user2Thread = new User(sharedStore, "user2", 100);
Thread.State user2ThreadStateBeforeStart = user2Thread.getState();
System.out.println("user2ThreadStateBeforeStart = " + user2ThreadStateBeforeStart);
user2Thread.start();
Thread.State user2ThreadStateAfterStart = user2Thread.getState();
System.out.println("user2ThreadStateAfterStart = " + user2ThreadStateAfterStart);
Thread.sleep(1000);
Thread.State user1ThreadStateAfterASecond = user1Thread.getState();
System.out.println("user1ThreadStateAfterASecond = " + user1ThreadStateAfterASecond);
Thread.State user2ThreadStateAfterASecond = user2Thread.getState();
System.out.println("user2ThreadStateAfterASecond = " + user2ThreadStateAfterASecond);
Thread.sleep(2000);
Thread.State user1ThreadStateAfterThreeSeconds = user1Thread.getState();
System.out.println("user1ThreadStateAfterThreeSeconds = " + user1ThreadStateAfterThreeSeconds);
Thread.State user2ThreadStateAfterThreeSeconds = user2Thread.getState();
System.out.println("user2ThreadStateAfterThreeSeconds = " + user2ThreadStateAfterThreeSeconds);
}
}
위와 같이 상태를 출력하는 프린트문을 많이 넣어보았다.
new
로 새로운 객체 생성 이후 .start()
이전에는 두 스레드 모두 객체 생성(NEW
) 상태이다..start()
이후에는 두 스레드 모두 실행 대기(RUNNABLE
) 상태이다.user1Thread
는 Thread.sleep(2000)
에 의해 주어진 시간동안 기다리는 일시정지 상태 (TIME_WAITING
)이다.user2Thread
는 임계 영역(Critical Section)에 접근할 수 없기 때문에 일시정지 상태 (BLOCKED
)이다.user1Thread
는 .run()
메소드를 끝마쳐 종료 상태 (TERMINATED
)이다.user2Thread
는 Thread.sleep(2000)
에 의해 주어진 시간동안 기다리는 일시정지 상태 (TIME_WAITING
)이다.
TIME_WAITING
이 끝난 후에는RUNNABLE
이 되어 나머지 실행을 마치고,TERMINATED
가 되었을 것이다.
public class StatePrintThread extends Thread{
private Thread targetThread;
public StatePrintThread(Thread targetThread) {
this.targetThread = targetThread;
}
@Override
public void run() {
while(true) {
Thread.State state = targetThread.getState();
System.out.println("타겟 스레드 상태: " + state);
if(state == State.NEW) {
targetThread.start();
}
if(state == State.TERMINATED) {
break;
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class TargetThread extends Thread {
@Override
public void run() {
for (long i = 0; i < 2000000000; i++) {
}
try {
Thread.sleep(1500);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (long i = 0; i < 2000000000; i++) {
}
}
}
public class Main {
public static void main(String[] args) {
StatePrintThread statePrintThread = new StatePrintThread(new TargetThread());
statePrintThread.start();
}
}
NEW
-> RUNNABLE
-> TIMED_WAITING
-> RUNNABLE
-> TERMINATED
를 모두 볼 수 있다. NEW
인 상태를 확인하면 targetThread.start()
를 실행한다.RUNNABLE
상태이다.Thread.sleep()
메소드 부분을 만나게 되면 TIMED_WAITING
상태가 된다.RUNNABLE
상태가 된다.TERMINATED
상태가 된다.