// 방법 1. 익명 내부 클래스를 이용한 생성
Thread threadOne = new Thread(new Runnable() {
public void run() {
System.out.println("Hello Thread!");
}
});
// 방법 2. 람다식을 이용한 생성
Thread threadTwo = new Thread(() -> {
System.out.println("Hello Again, Thread!");
});
// 방법 3. 스레드를 클래스로 만들어서 생성
class MyThread extends Thread {
@Override
public void run() {
System.out.println("Hello Again Again, Thread!");
}
}
Thread threadThree = new MyThread();
// 방법 4. 구현 후 즉시 실행
new Thread(()->{
System.out.println("IDEA");
}).start();
// 객체에 대한 참조를 가지고 있지 않기 때문에 이렇게 하면 JOIN등 활용이 어려움.
// 씽크가 상관이 없다면 이렇게도 사용가능.
쓰레드 객체는 1회용이며, start()로 실행한다.
신입 개발자들이 무한 루프에서 스레드를 스타트하는 실수를 많이 한다.
Thread threadOne = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.print("1");
}
});
Thread threadTwo = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.print("2");
}
});
threadOne.run();
// 이렇게도 동작 가능. 멀티스레드랑 상관없이 클래스의 run()를 그냥 콜한 것이라 상관없음
threadOne.start(); // 가능.
threadOne.run(); // 그냥 메소드콜이니깐 다시 동작해도 잘 된다.
threadOne.start(); // 스레드 객체는 1회용이므로, start()가 재실행될 수 없다.
// 신입개발자들이 무한루프에서 스레드를 스타트하는 실수를 많이 한다.
threadTwo.start();
System.out.println("Done!");
스레드의 상태
getState() 메소드로 스레드의 상태를 확인할 수 있다.
열거형 상수 | 설명 |
---|---|
NEW | start() 메소드가 아직 호출되지 않음 |
RUNNABLE | JVM에 의해 실행 가능한 상태 |
BLOCKED | 객체가 블락된 상태 |
WAITING | sleep(), wait(), join() 등에 의해 무한히 대기 중인 상태 |
TIMED_WAITING | sleep(), wait(), join() 등에 의해 정해진 시간 동안 대기 중인 상태 |
TERMINATE | run() 메소드가 종료된 상태 |
스레드의 우선순위 제어
public class Main {
public static void main(String[] args) {
Thread p1 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
System.out.print("~");
}
});
Thread p2 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
System.out.print("*");
}
});
// 우선순위 - 값이 높을 수록 우선순위가 높다.
System.out.println(p1.getPriority()); // default 5
p1.setPriority(Thread.MAX_PRIORITY); // MAX = 10
p2.setPriority(Thread.MIN_PRIORITY); // MIN =1
p1.start();
p2.start();
// 한스레드가 일정시간을 점유하고 다른 스레드가 점유하는 식으로 진행된다,
// 왔다 갔다한다. 예측하기 어려움.
}
}
메소드 | 설명 |
---|---|
void setPriority(int newPriority) | 새로운 우선순위로 설정한다. |
int getPriority() | 우선순위를 반환한다. |
sleep()
을 이용한 제어
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread p1 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 일반스레드에서 sleep을 사용할 때는 try/catch를 해줘야한다.
// throws를 해줄 수 있지만 나중에 Interrupt 때문에 try/catch를 해주는게 좋다.
System.out.println("Hello Thread");
}
}); // run이라는 단일 메소드를 동작시키는 객체
Thread p2 = new Thread(()->{
System.out.println("Thread by lambda");
});
p2.start();
Thread.sleep(100);
// 100미리 세컨드 동안 쉬고 동작을 한다. sleep을 이용해서 시간차를 줄 수있는 방법!
// 사용률을 감소시킴, 그래서 느려진다. 메인에서 하면 메인 쓰레드를 sleep시킨다.
System.out.println("Main thread ended");
}
}
join()
을 이용한 스레드 조인
public class Main {
public static void main(String[] args) throws InterruptedException {
// 방법 1. 익명 내부 클래스
Thread p1 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 일반스레드에 쓸때는 try/catch를 해줘야한다.
// throws를 해줄 수 있지만 트라이캐치를해줘야한다.
System.out.println("Hello Thread");
}
}); // run이라는 단일 메소드를 동작시키는 객체
Thread p2 = new Thread(()->{
System.out.println("Thread by lambda");
while (true){
}
});
p2.start();
p2.join();
// 한스레드가 동작을 끝내면 p2동작이 끝나면 그때 조인을해서 p1이 실행이 된다.
// sleep 없이 join으로 맞출 수 있다. 무언갈 동작하다 막혀있는게 blocking 동작
p2.join(100);
// p2가 무한루프가 돌고 있을 대 안에 100을 주면
// 100밀리세컨트 뒤에 다음 스레드가 동작될 수 있게 한다.
// 하지만 p2는 계속 돌고있음.
// 100ms 기다렸다가 동작을 할수 있게 설정한다.
// p2끝날 때까지 기다렸다가 시작이 된다.
// 그전까지 대기상태로 들어가 있지 않은 상태다.
p1.start();
// start를 해줘야 동작한다. 실제로 OS에 스레드 동작을 요청한다.
// main이 아닌 새로운 스레드를 동작한다.
p1.join();
Thread.sleep(100);
// 100미리 세컨드동안 쉬고 동작을 한다.
// sleep을 이용해서 시간차를 줄 수있는 방법!
// 사용률을 감소시킴, 그래서 느려진다. 메인에서 하면 메인쓰레드를 sleep시킨다.
System.out.println("Main thread ended");
}
}
interrupt()
을 이용항 대기 중지
방해를 의미하긴 하는데 컴퓨터 사이언스적인 용어로 기존 동작을 방해하고 반응을 강제하는 메소드.
주로 임베디드에서 많이 사용. 별로 쓸일은 없다. 스레드 동작을 이해하는데 필요하다 잘 못 스립된 동작을 깨워준다.
의도적으로 쓸 일은 많지 않다.
public class Main {
public static void main(String[] args) {
// try/catch를 쓰는 이유는 만약 10초동안 쉬고 있는데
// 다른 동작이 끝나고 쉬고있는 동작을 interrupte로 깨워줄 수 있다.
Thread p1 = new Thread(()->{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("Interrupted");
}
System.out.println("p1!!");
});
Thread p2 = new Thread(()->{
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("p2!!");
p1.interrupt();
});
p1.start();
p2.start();
}
}
yield()
를 이용한 상태 제어
sleep()
을 이용하면 오버헤드가 엄청 크다. Running 상태에서 Timed_Waiting 상태로 이동 그 다음에 실행 가능 상태로 넘어간다.
public class Main {
public static void main(String[] args) {
Thread p1 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
System.out.print("~");
Thread.yield();
// try {
// Thread.sleep(1);
// 오버헤드가 엄청 큼! Running 상태에서 Timed_Waiting 상태로 이동
// 그 다음에 실행가능상태로 넘어감
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
}
});
Thread p2 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
System.out.print("*");
Thread.yield();
// try {
// Thread.sleep(1);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
}
});
System.out.println(p1.getPriority());
p1.setPriority(Thread.MAX_PRIORITY);
p2.setPriority(Thread.MIN_PRIORITY);
p1.start();
p2.start();
}
}
스레드의 종료
run()
메소드의 종료stop()
메소드 호출 (deprecated)class AutoSaver extends Thread{
public AutoSaver() {
this.setDaemon(true);
}
@Override
public void run() {
while (true){
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// save something...
System.out.println("Auto save done!");
}
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
new AutoSaver().start();
for (int i = 0; i < 15; i++) {
Thread.sleep(1000);
System.out.println("working");
}
// 메인에서 1초간격으로 동작을 해서 15번이 실행이되면 autosaver는 같이 종료됨
}
}
자바의 모든 객체(Object)는 고유 락을 가지고 있다.
synchronized를 이용하면 객체의 고유 락의 소유권을 가져올 수 있다.
// 1. 멀티스레드 동작에 취약한 구현
class Counter{
private int count = 0;
public int increaseCount(){
return ++count; // 읽고, 수정하고, 쓰는 작업
// 동작들이 중복이 될 수 있다, 도중에 다른 스레드가 작업을 하게되면
// 경쟁적으로 동작하다보면, 읽고 수정하고 쓰기 전에 다른 쓰레드가 읽는 경우가 발생
}
public int getCount() {
return count;
}
}
// 2. Object 객체의 Intrinsic Lock을 이용한 구현 - 굳이 이렇게 할 필요 없음.
class Counter{
private Object lock = new Object();
private int count = 0;
public int increaseCount(){
synchronized (lock){
// lock이라는 객체를 소유해야 내부 블록을 동작시킬 수 있다.
// lock은 한번에 한 스레드만 소유할 수 있다.
return ++count; // 읽고, 수정하고, 쓰는 작업
}
}
public int getCount() {
return count;
}
}
// 3. this 객체의 Intrinsic Lock을 이용한 구현(모든 객체는 고유 락을 가지고 있기 때문에)
// 가장 좋은 구현 방법.
// Intrinsic Lock의 범위가 넓어질 수록 성능이 점점 떨어진다.
class Counter{
private int count = 0;
public int increaseCount(){
synchronized (this){
return ++count; // 읽고, 수정하고, 쓰는 작업
}
}
public int getCount() {
return count;
}
}
// 4. 메소드에 synchronized 키워드를 사용
// synchronized 키워드가 사용된 메소드를 호출하기 위해서는
// 해당 객체를 소유해야만 호출이 가능, 소유하지 못하면 Blocking 동작을 하고 있으면,
// nonBlocking 멈춰 있으면 Blocking
class Counter{
private int count = 0;
public synchronized int increaseCount(){
return ++count; // 읽고, 수정하고, 쓰는 작업
}
public int getCount() {
return count;
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
Counter c = new Counter();
// 고유락을 사용하는 것은 한번에 하나만 동작하게 하기위해서 제한을 해두는 것이다
for (int i = 0; i < 100; i++) {
new Thread(()->{
// synchronized(c){ // 이렇게 싱크를 하면, 병렬 동작이 전혀 이루어지지 않는다.
// 하나의 스레드가 100번을 수행하고 다음 스레드가 100번 수행하는 형태로 동작한다.
// 가장 안전하지만 가장 효율이 떨어지는 코드가 된다.
for (int j = 0; j < 100; j++) {
// c라는 shared object 공유객체 가 있을 때
// 멀티스레드로부터 안전한 영역을 생성하는 방법이다.
synchronized (c) {
// 5. c의 고유 락을 획득해야만 동작. {}영역안에서는 다른 스레드가 접근하지 못함
c.increaseCount();
// }
}
}
}).start();
}
Thread.sleep(1000);
System.out.println(c.getCount());
}
}
두 개의 스레드를 번갈아 동작할 수 있게 하는 방법
notify()
- 대기중인 다른 스레드를 하나 동작상태로 만든다. wait()중인 다른스레드가 들어와도 된다. 하나한테만 알려준다.notifyAll()
- 나머지 전체한테 알려준다.wait()
- synchronized안에서만 호출이 가능하다. Lcok을 반환하고, 대기상태로 들어간다. notify()
와 wait()
의 순서가 중요하다 순서를 바꾸게 되면 한 번 실행하고 대기 상태로 들어가기 때문에 notify()
를 기다리는 데드락 상태가 된다. class WorkObject{
// A가 먼저 실행된다고 할 때 wait()이되면 lock을 반환하고
// B가 실행되고 notify를 날리고 동작한다음에 wait()이되고
// lock을 반환하고 A가 실행되는 왔다갔다 이렇게 동작함!
public synchronized void methodA(){
System.out.println("methodA() called");
// 처음 스레드가 실행되는건 notify가 효과가 없다.
notify();
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 위에 것과 같음
// public void methodASAME(){
// synchronized (this){{
// System.out.println("methodA() called");
// notify();
// try {
// wait();
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// }}
// }
public synchronized void methodB(){
System.out.println("methodB() called");
notify();
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class MyThread extends Thread{
private final WorkObject workObject;
private boolean isA;
public MyThread(WorkObject workObject, boolean isA){ // Dependency Injection
this.workObject = workObject;
this.isA = isA;
}
@Override
public void run(){
for (int i = 0; i < 10; i++) {
if (isA){
workObject.methodA();
} else{
workObject.methodB();
}
}
}
}
public class Main {
public static void main(String[] args) {
WorkObject sharedObj = new WorkObject();
Thread p1 = new MyThread(sharedObj, true);
Thread p2 = new MyThread(sharedObj, false);
p1.start();
p2.start();
}
}
사전적 의미 횟대(깃발)
n개의 깃발을 놓고, 여러 스레드가 경쟁하도록 하는 sync 기법
n = 1 이면, BinarySemphore라고 하며, Lock(락은 하나만 존재하니깐)과 유사하게 동작
자원관리를 할 수 있다. 충분히 무언가 쌓였을 때 동작할 수 있게 구현할 수 있다.
acquire()
- 세마포의 허용권을 가져오는 메소드
release()
- 세마포의 허용권을 증가시키는 메소드, ()안에 숫자를 써주면 그 숫자만큼 증가한다.
availablePermits()
- 허용권이 몇개 있는지 체크할 때 사용하는 메소드
tryAcquire()
- 시도를 해서 성공하면 true, 실패하면 false를 반환하고 Blocking하지 않고 바로 다음코드를 진행한다.
sem.tryAcquire(2000, TimeUnit.MILLISECONDS)
)public class Main {
public static void main(String[] args) {
Semaphore sem = new Semaphore(1); // 세마포 개수를 설정한다.
sem.release();
System.out.println(sem.availablePermits());
// sem.release(11);
// try {
//// sem.acquire(12); release가 11개인데 12개를 가져오려고 하면 blocking이 걸림
// sem.acquire(); // 세마포를 획득하는 과정
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// sem.acquireUninterruptibly(); // acquire와 비슷한데 인터럽트에 반응하지 않음
System.out.println(sem.tryAcquire());
// 시도를해서 성공하면 true를 반환함 blocking하지 않는다.
// 실패하면 false를 반환하고 넘어간다.
// blocking하지 않는다는 건 기다리지 않고 다음걸 진행한다.
// try {
// System.out.println(sem.tryAcquire(2000, TimeUnit.MILLISECONDS));
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// 일정 시간은 블락킹할 수 있다. 안에 시간을 넣을 수 있다.(타임아웃)
System.out.println(sem.availablePermits()); // 가능한 허용권을 체크할 수 있다
sem.release();
}
}
package com.company.s14.p08;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
class Philosopher extends Thread{
private final int id;
private final Fork left;
private final Fork right;
public Philosopher(int id,Fork left, Fork right) { // DI
this.id = id;
this.left = left;
this.right = right;
}
@Override
public void run() {
while (true) {
try {
left.acquire();
System.out.println(id + ": left taken.");
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
// right.acquire();
// 1초동안기다리다가 오른쪽 포크를 못 집으면 왼쪽 포크를 내려놓는다.
if (!right.tryAcquire(1000, TimeUnit.MILLISECONDS)) {
left.release();
Thread.yield(); // 왼쪽 포크를 내려놓고 컨티뉴되서 다시 반복문이돌아서 다시 집어든다 좀더 양보를해서 다른 스레드가 획득할 기회를 더 준다.
continue;
}
System.out.println(id + ": right taken.");
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
System.out.println(id + " is eating.");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
left.release();
right.release();
Thread.yield(); // 먹은 다음에 양보함.
}
}
}
class Fork extends Semaphore{
public Fork() {
super(1);
}
}
public class DinigPhillisopher {
public static void main(String[] args) {
Philosopher[] phils = new Philosopher[5];
Fork[] forks = new Fork[5];
for (int i = 0; i < 5; i++) {
forks[i] = new Fork();
}
// for (int i = 0; i < 5; i++) {
// phils[i] = new Philosopher(i,forks[i],forks[(i+1) % 5]);
// }
for (int i = 0; i < 5-1; i++) {
phils[i] = new Philosopher(i,forks[i],forks[(i+1) % 5]);
}
phils[4] = new Philosopher(4, forks[0],forks[4]);
// 이건 오른쪽을 먼저 들기 때문에 다른사람들이 먹는 확률이 올라감
// 근데 먹는사람만 먹는 단점이 생김
for (int i = 0; i < 5; i++) {
phils[i].start();
}
}
}
package com.company.s14.p09;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Vector;
import java.util.stream.Collectors;
public class Main {
public static void main(String[] args) throws InterruptedException {
List<Integer> list1 = new Vector<>();
List<Integer> list2 = new ArrayList<>();
List<Integer> list3 = Collections.synchronizedList(list2);
// 어레이리스트인데 싱크된다. 이게 훨씬 빠르다.
// list3 : list2의 싱크된 버전
for (int i = 0; i < 100; i++) {
new Thread(() -> {
for (int j = 0; j < 100; j++) {
list1.add(1);
}
}).start();
}
for (int i = 0; i < 100; i++) {
new Thread(() -> {
for (int j = 0; j < 100; j++) {
// synchronized (list2) { // 이렇게 해줬을 때 위에 보다 느림??
list3.add(1);
// }
}
}).start();
}
Thread.sleep(1000);
// 멀티스레드여서 이렇게 기다려야 더 정확해진다? 뭐지 생각해봐
// 3개의 스레드가 동작한다 메인, 스레드 1, 스레드2 메인스레드에서 동작을 하면서
// 스레드1,2가 동작을 하는데 이 스레드들이 동작이 다끝나지 않은 상태에서
// 메인스레드가 종료될 수 있으니깐 메인스레드에
// sleep을 줘서 시간차를 준다음에 사이즈를 출력하게 하기 위해서이다.
System.out.println(list1.size());
System.out.println(list2.size());
}
}
ExecutorService pool = Executors.newCachedThreadPool();
ExecutorService pool = Executors.newFixedThreadPool(10); // 입력받음
ExecutorService es = new ThreadPoolExecutor(
10, // 코어 스레드 개수
100, // 최대 스레드 개수
120, // 스레드가 이 시간 동안 일하지 않으면 제거 (대기 시간)
TimeUnit.SECONDS, // 시간 단위를 지정
new SynchronousQueue<Runnable>() // 작업을 요청하면 -> 작업을 쌓아둘 큐 -> 스레드 풀로 할당해서 넘어가서 동작한다.
// 이게 없으면 작업 요청하면 실제 스레드로 들어가서 동작하기 전까지 메인 스레드가 멈춰있어야 한다. 그래서 큐에 던져 놓고
// 메인스레드는 동작하게 하는 것이다.(존재한다.)
);
ExecutorService pool1 = Executors.newCachedThreadPool();
class Work implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(i);
}
}
}
class CallableWork implements Callable<String>{
@Override
public String call() throws Exception {
return "작업 종료";
}
}
Future<String> future = null;
future = pool1.submit(new CallableWork());
for (int i = 0; i < 100; i++) {
pool1.submit(new Work()); // 리턴이 있음
}
shutdown() - Thread.join()과 비슷하게 작업이 끝나기를 기다려서 종료
shutdownNow()는 바로 종료시키는 것
cancle() - 스레드를 종료시킬 때 사용하는 또 다른 메소드로 여기서는 실행중인 Callable 객체를 강제 종료할 수 있다. (mayInterruptIfRunning)안에 true, false 매개값을 줄 수 있다.
get() - Blocking Method로 CallableWork()의 객체의 작업이 다 끝나고 future로 값이 들어올 때까지 기다리다가 값이 들어오면 작업을 수행한다.
isCancelled() - cancle() 됬는지 확인할 때 사용하는 메소드
isDonle() - 작업이 잘 끝났는지 확인하는 메소드이다.
Callable가 어떻게 작업하는지 모르기때문에 외부에서 컨트롤할 수 있게 하기위해 위의 메소드들을 제공한다.
pool1.shutdown(); // Thread.join()과 비슷하게 작업이 끝나기를 기다려서 종료
Thread.sleep(1000);
// 다른 스레드와 동기화를 맞추기 위해(여기서는 pool1이 작업을 진행중) 잠시 기다렸다가 밑에 코드가 진행되니깐 작업완료가 마지막에 출력됨!
future.cancel(true);
try {
System.out.println(future.get());
future.isCancelled(); //캔슬 됬는지 확인
future.isDone(); // 작업이 잘 끝났는지 확인
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}