728x90
1. 프로세스와 쓰레드
- 용어
- 프로그램이 실행되면 프로세스가 된다.
- program
- pro:진행되는 , gram: 정보
- 저장되는 파일 형태
- 저장 공간만 존재하면 된다 - HDD, SDD
- process
- cess==go
- 작업이 실행되는 것
- 명령어가 필요한 것 -CPU
- 프로그램을 더블 클릭 후 수행이 되는 상태로 변하면 프로세스가 된다.
* 동영상같은 것은 데이터만 있기 때문에 프로그램이지만 프로세스가 아니다.
- 프로그램이 실행되면 프로세스가 된다.
- process - 쓰레드
- 프로세스 : 작업에 필요한 데이터와 메모리 등의 자원 쓰레드로 구성되어 있는 것
- 쓰레드: 프로세스의 자원을 이용해서 실제로 작업을 수행하는 것
- 싱글쓰레드 : 자원 하나에 쓰레드 한 개
- 멀티쓰레드 : 자원 하나에 쓰레드 여러 개 -> 자원을 공유하기 때문에 동기화 문제가 생긴다.
2. 멀티쓰레딩의 장단점
- 장점
- CPU의 사용률을 향상시킨다.
- 자원을 보다 효율적으로 사용할 수 있다.
- 사용자에 대한 응답성이 향상된다. - 채팅하다가 파일 보내기
- 작업이 분리되어 코드가 간결해진다.
- 단점
자원을 공유하면서 작업을 하기 때문에 발생- 동기화 문제 발생
- 교착상태 문제 발생
3. 쓰레드의 구현과 실행
- 2가지 방법 존재
- Thread class를 상속받는 방법
- Runnable 인터페이스를 구현하는 방법
- 인터페이스를 구현하는 방법이 재사용, oop, 다중상속이 가능해서 주로 사용 된다.
- 인터페이스를 구현하는 방법이 재사용, oop, 다중상속이 가능해서 주로 사용 된다.
- 메서드
- Thread currentThread() : 현재 실행중인 쓰레드의 참조를 반환
- getName() : 쓰레드의 이름을 반환
- start() : 해당 메서드를 호출해야만 쓰레드가 실행
// Thread class를 상속
class MyThread extends Thread{
public void run() {/* 작업 내용 */} // Thread class run() 오버라이딩
}
// Runnable interface 구현
class MyThread implements Runnable {
public void run() {/* 작업 내용 */} // Runnable interface의 run() 구현
}
package practice;
public class Practice {
public static void main(String[] args) {
ThreadEx1 t1 = new ThreadEx1();
Runnable r = new ThreadEx2(); // Runnable instance 생성
Thread t2 = new Thread(r); // 실제 Thread에 매개변수로 넣어서 사용
t1.start();
t2.start();
}
}
class ThreadEx1 extends Thread {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(getName()); // 조상인 Thread의 getName()을 호출
}
}
}
class ThreadEx2 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()); // 현재 실행중인 Thread를 반환
}
}
}
4. start() 와 run()
- start() : 새로운 쓰레드가 작업을 실행하는데 필요한 새로운 호출스택을 생성
- run() : start()가 깔아준 새로운 호출스택에서 run()이 실행된다
- 실행 중인 사용자 쓰레드가 하나도 없을 때 프로그램이 종료된다. -> run() 작업이 다 종료되야 main()도 종료된다.
- 참고 : main() 도 작업을 수행하는 쓰레드이다.
- main 쓰레드를 사용자 쓰레드 혹은 non데몬 쓰레드라고 한다.
- 다른 종류로 데몬 쓰레드가 존재
5. 싱글쓰레드와 멀티쓰레드
- 예시
- 싱글쓰레드 : 하나의 쓰레드로 2개의 작업을 수행하는 경우
- 멀티쓰레드 : 2개 쓰레드로 2개의 작업을 수행하는 경우
- 결과
- 싱글 쓰레드가 더 빠르다.
- 한가지 작업을 다 종료후 다음 작업을 수행한다.
- 멀티쓰레드의 경우 2가지의 작업을 쓰레드가 각각 수행하지만 서로 병행해서 작업을 수행하기 때문에 context switching이 발생해서 시간의 delay가 생긴다.
- 병행과 병렬
- 병행 : 반드시 다른 작업을 수행하는 것
- 병렬 : 같은 작업을 2개 이상 쓰레드로 수행하는 것
6. 쓰레드의 I/O blocking
- 5.의 경우 싱글쓰레드가 더 빠르다고 했지만 I/O 같은 경우는 외부로부터 들어와야하는 값으로 인해서 생기는 blocking 시간이 존재하게 된다.
- 위와 같은 경우에는 멀티쓰레드를 사용하면blocking 되는 시간에 다른 작업을 수행하면 되므로 멀티쓰레드의 작업 처리 속도가 훨씬 빠르다.
- 면접 단골 질문
- blocking
- 사용자로부터 입력을 기다리는 구간, 아무 일도 하지 않는다. - I/O blocking
- 싱글 쓰레드에서 2가지 일을 하는 경우 발생
- non blocking
- 멀티 쓰레드에서 2가지 일을 하는 경우 한 쓰레드가 I/O를 기다리는 동안 다른 쓰레드가 자신의 일을 수행한다.
- 작업 처리 속도가 빠르다
- 동기화
- 값 넣는 순서대로 수행
- 비동기화
- 값을 넣었다고 먼저 답을 주지 않는 것. ex) cgv - 표 끊기 표 100장 끊는 사람것은 마지막으로 미룬다.
- blocking
package practice;
import javax.swing.*;
public class Practice {
public static void main(String[] args) {
Runnable r = new ThreadEx();
Thread th = new Thread(r);
th.start();
String input = JOptionPane.showInputDialog("any value");
System.out.println("input = " + input);
}
}
class ThreadEx implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(i);
try {
Thread.sleep(1000);
} catch (Exception e) {}
}
}
}
7. 쓰레드 우선 순위
- java app보다 OS가 상위에 있기 때문에 희망사항일 뿐 100% 그대로 수행되지 않는다.
- java에서는 10 ~ 1 까지의 우선 순위가 있고 숫자가 클수록 우선 순위가 높다.
- 메서드
void setPriority(int newPriority) // 쓰레드의 우선순위를 지정한 값으로 변경 default = 5
int getPriority() // 쓰레드의 우선순위를 반환
8. 쓰레드 그룹
- 서로 관련된 쓰레드를 그룹으로 다루기 위한 것. class가 존재
- 보안상의 이유로 도입 : 자신이 속한 쓰레드 그룹이나 하위 쓰레드 그룹은 변경할 수 있지만 다른 쓰레드 그룹의 쓰레드를 변경할 수는 없다.
- 쓰레드 그룹에 넣는 방법 - 쓰레드 생성자를 이용해서 넣는다.
- 기본 생성자를 통해 생성한 쓰레드는 자신을 호출한 쓰레드의 그룹에 들어가게 된다.
main에서 쓰레드를 생성하면 main 쓰레드 그룹에 속한다.
// 쓰레드 생성자
Thread(ThreadGroup group, String name)
Thread(ThreadGroup group, Runnable target)
Thread(ThreadGroup group, Runnable target, String name)
Thread(ThreadGroup group, Runnable target, String name, long stackSize)
9. 데몬 쓰레드
- 중요
- 보조역할을 하는 쓰레드
- 일반 쓰레드가 모두 종료시 데몬 쓰레드는 강제적으로 자동 종료
- 예 : 가비지 컬렉터, 자동저장, 화면자동갱신
- 생성 조건
- while(true) // 항상 무한루프로 돌리기. 결국 일반쓰레드가 종료되면 자동 종료가 된다.
- 특정조건을 만족할 때 작업을 수행하는 조건문 작성
- 일반 쓰레드 수행 전 setDaemon(boolean on)을 통해서 쓰레드를 데몬 쓰레드로 변경
on이 true일 때 데몬 쓰레드로 설정 된다.
- 예제를 보면 이해가 쉽다.
package practice;
import javax.swing.*;
public class Practice implements Runnable {
static boolean autoSave = false;
public static void main(String[] args) {
Runnable r = new Practice();
Thread th = new Thread(r);
th.setDaemon(true); // start 전에 daemon 설정
th.start();
for (int i = 0; i <= 10; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {}
System.out.println(i);
if (i == 5) autoSave = true; // 조건문 작성
}
System.out.println("프로그램 종료");
}
@Override
public void run() {
while (true) { // 1. 무한 루프 -> 조건은 psvm에 작성 -> main thread 종료시 자동 종료
try {
Thread.sleep(3 * 1000);
}catch (Exception e){}
if (autoSave) autoSave(); // 2. auto save가 true일 때 3초마다 수행
}
}
// 수행할 method
public void autoSave() {
System.out.println("작업파일이 자동 저장되었습니다.");
}
}
10. 쓰레드 상태
11. 쓰레드 실행 제어
- stop(), suspend(), resume() -> 교착상태의 문제로 인해서 deprecated 되었다.
- 사용하고 싶으면 직접 구현해서 사용하기
- 실행 제어 메서드
- static
- static void sleep(long millis)
- 자기 자신을 재우는 것
- 일시 정지상태로 이동
- static void yield()
- 자기 자신이 양보를 하는 것. 자신에게 주어진 실행시간에 한해서 양보를 해준다.
- 실행 대기상태로 이동
- static void sleep(long millis)
- instance
- void interrupt()
- sleep() 이나 join()에 의해 일시정지상태인 쓰레드를 깨워서 실행대기상태로 만다.
- sleep() 중 interrupt()로 깨우면 예외가 터지면서 boolean이 false로 초기화된다. 그래서 catch문 안에 interrupt()를 작성해 예외 처리를 하면 정상 작동하게 된다.
- 사용법
- Thread class 내부 boolean isInterrupted()의 false 값을 void interrupt()는 true로 변경한다.
- true로 변경 해서 쓰레드를 깨운 후
- boolean interrupted()를 이용해서 다시 현재 상태의 boolean을 반환 후 boolean isInterrupted()를 false로 만들어 줘야한다.
- 정리
- interrupted 자체가 의미가 있는 것이아니라 이 메서드를 통해서 내부 isInterrupted boolean 값을 true로 변경 후 true 조건식을 만들어서 뭔가를 작동시킨다.
- sleep() 이나 join()에 의해 일시정지상태인 쓰레드를 깨워서 실행대기상태로 만다.
- void join()
- 지정된 시간동안 쓰레드가 실행되도록한다.
- 지정 시간이 지나거나 작업이 종료시 join()을 호출한 쓰레드로 다시 돌아와 작업을 수행
- void interrupt()
- static
- sleep 예제
package practice;
public class Practice{
public static void main(String[] args) {
Thread1 t1 = new Thread1();
Thread2 t2 = new Thread2();
t1.start();
t2.start();
// 해당 try-catch문을 주석 처리하면 main, t1, t2 상관없이 다 랜덤으로 실행
try{
Thread.sleep(2000);
}catch (InterruptedException e){
}
System.out.print("main finish");
}
// 수행할 method
public void autoSave() {
System.out.print("작업파일이 자동 저장되었습니다.");
}
}
class Thread1 extends Thread {
@Override
public void run() {
for (int i = 0; i < 300; i++) {
System.out.print("-");
}
System.out.println("t1 finish");
}
}
class Thread2 extends Thread {
@Override
public void run() {
for (int i = 0; i < 300; i++) {
System.out.print("|");
}
System.out.print("t2 finish");
}
}
- interrupt() 예제
package practice;
import javax.swing.*;
public class Practice {
public static void main(String[] args) {
Thread1 t1 = new Thread1();
t1.start();
String input = JOptionPane.showInputDialog("put string");
System.out.println("input = " + input);
t1.interrupt(); // false -> true
System.out.println("t1.isInterrupted() = " + t1.isInterrupted()); // expect : true
}
// 수행할 method
public void autoSave() {
System.out.print("작업파일이 자동 저장되었습니다.");
}
}
class Thread1 extends Thread {
@Override
public void run() {
int i = 10;
while (i != 0 && !isInterrupted()) {
System.out.println(i--);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
interrupt(); // catch문으로 잡아야한다.
}
}
System.out.println("count finish");
}
}
class Thread2 extends Thread {
@Override
public void run() {
for (int i = 0; i < 300; i++) {
System.out.print("|");
}
System.out.print("t2 finish");
}
}
- join() yield()
package practice;
public class Practice {
static long startTime = 0;
public static void main(String[] args) {
Thread1 t1 = new Thread1();
Thread2 t2 = new Thread2();
t1.start();
t2.start();
startTime = System.currentTimeMillis();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
}
System.out.println("time : " + (System.currentTimeMillis()-Practice.startTime));
}
// 수행할 method
public void autoSave() {
System.out.print("작업파일이 자동 저장되었습니다.");
}
}
class Thread1 extends Thread {
@Override
public void run() {
for (int i = 0; i < 300; i++) {
System.out.print(new String("-"));
}
}
}
class Thread2 extends Thread {
@Override
public void run() {
for (int i = 0; i < 300; i++) {
System.out.print("|");
}
}
}
12. 쓰레드의 동기화
- 멀티 쓰레드 프로세스의 경우 문제점
- 서로의 작업에 영향을 준다.
- 한 쓰레드가 특정 작업을 끝마치기 전까지 다른 쓰레드에 의해 방해받지 않도록 하는 것이 필요
- 쓰레드의 동기화
- 한 쓰레드가 진행중인 작업을 다른 쓰레드가 간섭하지 못하게 막는 것
- 임계 영역(critical section)과 잠금(lock) 개념 도입
- 임계 영역
- 간섭받으면 안되는 code 영역
- 공유 데이터(==객체)를 사용하는 코드 영역을 임계 영역으로 지정
- 임계 영역 지정시, 내부의 객체에 lock이 생성된다.
- lock
- 공유데이터(객체)가 가지고 있는 lock을 획득한 쓰레드만 이 영역 내의 코드를 수행
- 객체 1개마다 lock 1개를 가진다.
- 참고 - private
- 동기화를 하고자 하는 부분에는 분명이 공유되는 객체 혹은 기본형 값들이 존재할 것이다.
- 따라서 해당 변수들을 private으로 지정을 해주어서 다른 쓰레드에서 접근조차 못하도록 막아야 동기화 자체에 의미를 가지게 된다.
- 임계 영역
- 쓰레드의 동기화
- 임계영역
임계 영역은 좁을수록 성능면에서 좋다.- 메서드 전체를 임계 영역으로 지정 - 내부의 객체를 공유 못하는 거지만 메서드 자체를 lock을 걸었다고 생각해도 무방
- 특정한 영역을 임계 영역으로 지정
// 1. 메서드 전체를 임계 영역으로 지정
public synchronized void calcSum(){}
// 2. 특정한 영역을 임계 영역으로 지정
synchronized(객체의 참조변수){}
예제
public class Practice {
public static void main(String[] args) {
Runnable r = new Thread1();
new Thread(r).start();
new Thread(r).start();
}
}
class Account {
private int balance = 1000;
// balance라는 것을 동시성 문제로 부터 해결하기 위해서 synchronized 했는데 private이 아니면 외부에서 임의 접근히 가능해져서 의미가 없어진다
public int getBalance(){
return balance;
}
public synchronized void withdraw(int money) { // 동기화
if (balance >= money) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {}
balance -= money;
}
}
}
class Thread1 implements Runnable {
Account acc = new Account();
@Override
public void run() {
while (acc.getBalance() > 0) {
int money = (int) (Math.random() * 3 + 1) * 100;
acc.withdraw(money);
System.out.println("balance = " + acc.getBalance());
}
}
}
13. wait() & notify(), notifyAll()
- 동시성 문제 해결한 동기화의 문제점
- 특정 쓰레드가 객체의 락을 가진 상태로 오랜 시간을 보내게 되는 경우 다른 쓰레드들이 모두 해당 객체의 락을 기다린다고 다른 작업이 원활하게 이루어지지 않게 된다.
- 예시 : 만약 위의 예제의 sleep이 1일로 되어있고 내부에 돈을 넣는 과정 또한 동일 synchronize에 포함이 된다고 하면 돈이 없는 것을 확인하고 돈을 통장에 넣으려고 해도 1일 뒤에 할 수 있게 된다.
- wait(), notify(), notifyAll()을 통해서 해결
- 공통 특징
- Object에 정의
- 동기화 블록 내에서만 사용
- 효율적인 동기화를 가능하게 한다.
- wait()
- 해당 쓰레드가 lock을 반납하게 하고 일시정지상태로 들어가도록 한다.
- notify(), notifyAll() 을 통해서 깨워야지 실행대기 상태로 들어가게 된다.
- 아니면 wait()에 매개변수를 넣어서 일정시간동안만 일시정지상태를 유지한다.
- notify()
- 작업이 종료된 후 다시 일시정지된 쓰레드들 중 임의의 한 쓰레드에게 lock을 주어서 실행되게 한다.
- notifyAll()
- 작업이 종료된 후 다시 일시정지된 쓰레드들 모두를 실행대기상태로 들어가게 한 후 한 쓰레드에게 lock을 주어서 실행되게 한다.
- 비효율적으로 굳이 다 실행대기상태로 넣는 이유
- starvation(기아 현상) 방지 목적
- 임의의 한 쓰레드만 깨울 시, 운이 나쁘면 어떤 한 쓰레드는 일시정지상태에서 일어나지 않게 된다. (운없이 호출이 안됬기 때문)
- 공통 특징
14. Lock과 condition
- wait(), notify()의 비효율성
- 입금을 하기 위한 쓰레드나 출금을 하기 위한 쓰레드가 동일한 공간(waiting pool)에서 일시정지상태를 유지하게 된다.
- 그래서 입금을 위해 wait()을 출금을 일시정지시키고 입금후 다시 출금을 하는 과정이 반복이 될 때 나중에는 어떤 쓰레드가 호출 될지 모르게 된다.
- 물론 입금을 하는 쓰레드가 와야하는데 출금 쓰레드가 오게 되면 다시 입금 쓰레드로 변경이 되도록 설계가 되어있지만 문제라기 보단 비효율성이 발생한다. - 이 외에도 비효율성이 존재한다.
- 해결책
- lock() 과 condition()을 이용한 동기화 수행
- 실질적으로는 condition을 통해서 wait(), notify()를 해결하는데 lock은 왜 있냐?
-> locks 패키지에 condition, lock 의 인터페이스가 존재하고 lock의 method에 condition 생성하는 method가 존재한다. - 따라서 lock을 구현해야지 condition을 사용해서 비효율성을 해결할 수 있다.
* 참고 lock으로만 waitiong pool 분리가 가능하다. 하지만 구현하는 것 자체가 비효율적이다.
- Lock
- ReentrantLock - 구현체
- 재진입이 가능한 lock
- 원래 사용하던 lock이랑 동일
- 하지만 synchronize 내부의 lock의 자동 기능이 없기에 method를 통해서 lock 기능 수행 필요
- ReentrantReadWriteLock - 구현체
- 읽기에는 공유적이고, 쓰기에는 배타적인 lock
- read와 write를 분리 read일 때는 read를 하는 모든 쓰레드는 다 접근 가능하고 write는 접근이 불가, write는 아무도 접근 못함
- 원리
- 원래 일반적인 동기화의 lock은 true/false로 해결 - Mutex
- 해당 lock은 0, -1, 양수로 진행
사용전 상태를 0
읽기 쓰레드가 작동시 +1씩 증가 & 종료시 -1 씩 감소 -> read만 접근 가능
write일 때는 무조건 -1로 고정 -> read나 write 접근 불가
- StampedLock - 구현 안되어있음
- 낙관적인 lock
- 원래는 read인지 write인지 보고 접근해야하지만 일단 읽고(아무도 write 안하고 있겠지 마인드) 나중에 write인지 확인하는 것 - 역순으로 진행
- 하지만 빨랐죠?
- ReentrantLock - 구현체
// ReentrantLock의 생성자
ReentrantLock()
// lock method
void lock() // lock 잠금
void unlock() // lock 해지
boolean isLocked() // lock 잠겼는지 확인
// lock 사용방법
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
// 임계 영역 작성
} catch(Exception e){
} finally{
lock.unlock();
}
- condition : wait(), notify()문제 해결
- waitingPool 분리시켜준다.
- wait()과 notify()에 관련된 메서드 존재
waitingPool.method() 형식으로 작성 - 이전에는 wait()이렇게 썻었다.
// condition method
// wait()
void await()
void awaitUninterruptibly()
// wait(long timeout)
boolean await(long time, TimeUnit unit)
long awaitUntil(Date deadline)
// notify()
void signal()
// notifyAll()
void signalAll()
// 예제
private ReenTrantLock lock = new ReentrantLock(); // lock 생성
// lock으로 condition 생성
// newCondition()이 lock interface method 중 condition 생성하는 method
private Condition forCook = lock.newCondition();
private Condition forCust = lock.newCondition();
... 생략
forCook.await();
... 생략
forCust.signal();
14. 동기화 흐름
- 멀티 쓰레드 사용
-> 데이터 공유 문제 발생 - 동기화 : lock을 걸어버림 - 아무도 못들어오도록 함 (문제 해결)
-> 동기화의 문제 발생 : 아무도 못들어오니깐 정말 들어와야하는 경우 lock으로 인해 진행이 안됨 - wait(), notify() - 특정 경우 wait()으로 해당 객체의 lock을 풀고 notify()로 다시 깨워서 작동 (문제 해결)
-> notify(), wait()의 비효율성 발생 : waiting pool이 하나여서 아무거나 막 들어옴 - lock & condition : 그룹별로라도 waiting pool을 분리해서 아무거나 다음 턴에 못들어오도록 비효율성 해결 (문제 해결)
15.volatile
- 멀티 코어의 문제점 해결
- 코어는 처음에만 memory로부터 data를 읽어온 후 이후 core 내부의 cache에서부터 data를 읽는데 이때부터 다른 corerk write를 한 것이 동기화가 되지 않는다.
- 따라서 자주 write되는 code에는 volatile을 붙여주면 cpu가 해당 정보를 읽을 때마다 cache를 사용하지 않고 memory로부터 data를 읽게되어서 동기화가 이루어지게 된다.
이전 발행글 : 2023.03.23 - [java/java 기본] - 12. 지네릭스, 열거형, 애너테이션
다음 발행글 : 2023.03.24 - [java/java 기본] - 14. 람다와 스트림