| 백기선님의 자바 라이브 스터디 10주차 주제입니다.
프로세스와 쓰레드
프로세스는 실행 중인 프로그램을 의미합니다. 프로그램을 실행하면 운영체제가 실행에 필요한 메모리를 할당해주면서 프로세스가 됩니다. 프로세스는 프로그램을 수행하는 데 필요한 데이터와 메모리 등의 자원 그리고 쓰레드로 구성되어 있으며 작업을 수행하는 것이 바로 쓰레드가 됩니다.
따라서 프로세스에는 최소한 하나 이상의 쓰레드가 존재하고 둘 이상의 쓰레드를 가진 프로세스는 멀티쓰레드 프로세스가 됩니다.
멀티쓰레드를 사용하는 이유에 대해서 알아보기 위해 멀티쓰레드 장점을 나열해보겠습니다.
멀티 쓰레딩의 장점
- CPU의 사용률을 향상시킨다
- 자원을 보다 효율적으로 사용할 수 있다
- 사용자에 대한 응답성이 향상된다
- 작업이 분리되어 코드가 간결해진다
이러한 이유로 멀티쓰레드를 사용하지만 장점만 존재하지는 않습니다. 여러 쓰레드가 자원을 공유하기 때문에 동기화,교착상태등에 대해서 고려해야됩니다.
Thread 클래스와 Runnable 인터페이스
쓰레드를 구현하는 방법은 Thread 클래스를 상속받는 방법과 Runnable 인터페이스를 구현하는 방법으로 분류 가능합니다.
public class MyThread extends Thread{
public void run(){
//작업 내용
}
}
public class MyThread implements Runnable{
public void run(){
//작업 내용
}
}
Thread 클래스를 상속받는 쪽은 run 메서드를 오버라이딩하는 방법이고 Runnable 쪽은 추상메서드인 run 메서드를 구현하는 차이가 있습니다. 클래스는 다중 상속이 불가하기 때문에 Runnable 인터페이스를 사용하는 쪽이 일반적이라고 합니다.
예제
public class MyThread extends Thread{
public void run(){
for(int i =0 ; i < 5; i++){
System.out.println("Thread");
}
}
}
public class MyRunnable implements Runnable{
@Override
public void run() {
for(int i = 0; i < 5; i++){
System.out.println("Runnable");
}
}
}
public class ThreadExample {
public static void main(String[] args) {
MyRunnable runnable = new MyRunnable();
new Thread(runnable).start();
MyThread thread = new MyThread();
thread.start();
}
}
쓰레드가 무조건 순서대로 진행되는게 아니라는걸 보여드리고 싶었는데 이렇게 설계하면 안되나봅니다. 메인에서 for문을 돌리면서 쓰레드를 돌려보도록 하겠습니다.
아무튼 제가 원하는대로 쓰레드가 순서대로 돌아가는건 아니라는건 어찌어찌 보여드렸네요. 쓰레드를 생성한 다음에는 start()를 호출해야만 비로소 작업이 수행이 되고, 이건 딱 한 번 사용가능합니다. 동일한 수행을 더 해야된다면 쓰레드를 다시 생성해서 start()를 해주셔야해요.
쓰레드의 상태
요약 | 상태 | 설명 |
객체 생성 | NEW | 쓰레드 객체가 생성, 아직 start() 메소드가 호출되지 않은 상태 |
실행 대기 | RUNNABLE | 실행 상태로 언제든지 갈 수 있는 상태 |
일시 정지 | WAITING | 다른 쓰레드가 통지할 때까지 기다리는 상태 |
TIMED_WAITING | 주어진 시간 동안 기다리는 상태 | |
BLOCKED | 사용하고자 하는 객체의 lock이 풀릴 때까지 기다리는 상태 | |
종료 | TERMINATED | 실행을 마친 상태 |
쓰레드 스케줄링 메서드
생성자 / 메서드 | 설명 |
void interrupt() | sleep()이나 join()에 의해 일시정지상태인 쓰레드를 실행대기상태로 만듬 |
void join() | 지정된 시간동안 쓰레드가 실행되도록 함 |
void resume() | suspend()에 의해 일시정지상태에 있는 쓰레드를 실행대기상태로 만듬 |
static void sleep() | 지정된 시간동안 쓰레드를 일시정지시킴 |
void stop() | 쓰레드를 즉시 종료시킴 |
void suspend() | 쓰레드 일시정지시킴 |
static void yield() | 실행 중에 다른 쓰레드에게 양보하고 실행대기상태로 만듬 |
resume,stop,suspend는 deprecated 되었습니다.
쓰레드의 우선순위
쓰레드는 우선순위라는 멤버변수를 가지고 있습니다. 이 우선순위에 따라서 특정 쓰레드가 더 많은 작업시간을 할당받을 수 있도록 할 수 있습니다.
관련 메서드와 필드를 정리해봤습니다.
void setPriority(int priority)//우선순위 변경
int getPriority()//쓰레드 우선순위 반환
MAX_PRIORITY//최대 우선순위
MIN_PRIORITY//최소 우선순위
NORM_PRIORITY//보통 우선순위
참고로 쓰레드 우선순위는 1~10의 범위를 가질 수 있습니다. 10에 가까울수록 우선순위가 높아 작업 할당을 더 많이 받을 수 있답니다.
public class MyRunnable implements Runnable{
@Override
public void run() {
for(int i= 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName());
}
}
}
public static void main(String[] args) {
Thread th1 = new Thread(new MyRunnable());
Thread th2 = new Thread(new MyRunnable());
System.out.println("name = "+ th2.getName());
th2.setPriority(10);
th1.start();
th2.start();
}
사실 저는 우선순위가 높은 쪽이 항상 먼저 실행되고 더 많은 작업을 하고 그럴줄 알았는데 for문을 5번만 돌려서 그런가 0000011111이 나올때도 꽤 있더라구요.
Main 쓰레드
우리가 알고 있는 main()도 사실 쓰레드로 동작합니다. 이를 메인 쓰레드라고 부릅니다. 메인 쓰레드는 프로그램이 시작하면 가장 먼저 실행되는 쓰레드이며, 모든 쓰레드는 메인 쓰레드로부터 생성됩니다. 다른 쓰레드를 생성해서 실행하지 않으면 메인 쓰레드가 종료되는 순간 프로그램도 종료됩니다.
쓰레드는 사용자쓰레드와 데몬쓰레드로 나뉩니다.
데몬쓰레드
데몬 쓰레드는 메인 쓰레드의 작업을 돕는 보조 쓰레드입니다. thread.setDaemon(true)를 이용해서 쓰레드를 데몬 쓰레드로 만들어줍니다. 그 다음부터는 앞에서 다룬 쓰레드처럼 start()를 호출해서 사용합니다. 메인 쓰레드의 보조이므로 메인 쓰레드가 종료가 되면 데몬 쓰레드도 덩달아 종료됩니다.
동기화
멀티 쓰레드는 한 프로세스의 자원을 공유해서 작업을 하기 때문에 서로 영향을 주게 됩니다. 여러 쓰레드가 하나의 변수에 접근해서 값을 이리저리 바꿀때 문제가 생길 수 있습니다. 따라서 한 쓰레드가 공유 자원을 사용중이라면 다른 쓰레드가 접근하지 못하도록 잠궈두는 것을 동기화라고 합니다.
critical section 임계영역이라고 하죠. 멀티 쓰레드에서 단 하나의 쓰레드만 실행할 수 있는 코드 영역을 임계영역이라고 합니다. 자바에서는 이런 임계영역을 지정하기 위해서 동기화 메서드와 동기화 블록을 제공합니다. synchronized 키워드를 붙여서 간단하게 사용할 수 있습니다.
class ThreadEx21 {
public static void main(String args[]) {
Runnable r = new RunnableEx21();
new Thread(r).start();
new Thread(r).start();
}
}
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) {}
balance -= money;
}
}
}
class RunnableEx21 implements Runnable {
Account acc = new Account();
public void run() {
while(acc.getBalance() > 0) {
int money = (int)(Math.random() * 3 + 1) * 100;
acc.withdraw(money);
System.out.println("balance:"+acc.getBalance());
}
}
}
이 코드에는 문제점이 있는데 한 번 돌려보시면 알 수 있습니다. 말로 간단히 전달하자면 돈이 음수가 나오는 경우가 나온답니다. 이를 해결하기 위해 동기화를 적용해보겠습니다.
class ThreadEx22 {
public static void main(String args[]) {
Runnable r = new RunnableEx22();
new Thread(r).start();
new Thread(r).start();
}
}
class Account {
private int balance = 1000; // private으로 해야 동기화가 의미가 있다.
public int getBalance() {
return balance;
}
public synchronized void withdraw(int money){ // synchronized로 메서드를 동기화
if(balance >= money) {
try { Thread.sleep(1000);} catch(InterruptedException e) {}
balance -= money;
}
}
}
class RunnableEx22 implements Runnable {
Account acc = new Account();
public void run() {
while(acc.getBalance() > 0) {
int money = (int)(Math.random() * 3 + 1) * 100;
acc.withdraw(money);
System.out.println("balance:"+acc.getBalance());
}
}
}
이렇게 막아주셔야 합니다.
데드락
객체에 lock을 걸어서 동기화 처리하는 것은 좋습니다. 그러나 쓰레드가 교착상태에 빠질 수 있다는 문제점이 존재합니다. 쓰레드들이 일을 처리할때 한 번에 한 자원만 가져가지는 않겠죠. 쓰레드 A와 쓰레드 B가 있다고 할때 서로 상대편이 보유하고 lock을 걸어둔 자원이 필요해요. 그럼 이거 어떡합니까. 상대가 작업이 끝나고 반환을 해야 내가 쓸 수 있는데 서로 물렸어요. 이런걸 데드락이라고 합니다.
데드락 발생 조건
Mutual Exculsion(상호 배제) : 매 순간 하나의 프로세스만이 자원을 사용할 수 있음
No preemption(비선점) : 프로세스가 스스로 자원을 반납하기 전까지 다른 프로세스가 자원을 가져갈 수 없습니다
Hold and Wait : 자원을 가진 프로세스가 다른 자원을 기다릴 때 들고있는 자원을 놓지 않음
Circular Wait : 자원을 기다리는 프로세스 간에 사이클이 형성되어야 함
public class ch04 {
static Object object1 = new Object();
static Object object2 = new Object();
public static void main(String[] args) {
User1 user1 = new User1();
User2 user2 = new User2();
user1.start();
user2.start();
}
public static class User1 extends Thread {
public void run() {
synchronized (object1){
System.out.println("User1 is holding object1");
try {
Thread.sleep(10);
} catch (Exception e) {}
System.out.println("User1 is waiting object2");
synchronized (object2){
System.out.println("User1 is holding object1 and object2");
}
}
}
}
public static class User2 extends Thread {
public void run() {
synchronized (object2){
System.out.println("User2 is holding object2");
try {
Thread.sleep(10);
} catch (Exception e) {}
System.out.println("User2 is waiting object1");
synchronized (object1) {
System.out.println("User2 is holding object1 and object2");
}
}
}
}
}
데드락 처리
데드락 예방
- 상호배제 조건의 제거 : 교착 상태는 두 개 이상의 프로세스가 공유가능한 자원을 사용할 때 발생하는 것이므로 공유 불가능한 조건을 제거하면 교착 상태를 해결할 수 있다.
- 점유와 대기 조건의 제거 한 프로세스에 수행되기 전에 모든 자원을 할당시키고 나서 점유하지 않을 때에는 다른 프로세스가 자원을 요구하도록 하는 방법이다. 자원 과다 사용으로 인한 효율성, 프로세스가 요구하는 자원을 파악하는 데에 대한 비용, 자원에 대한 내용을 저장 및 복원하기 위한 비용, 기아상태, 무한대기 등의 문제점이 있다.
- 비선점 조건의 제거 비선점 프로세스에 대해 선점 가능한 프로토콜을 만들어 준다.
- 환형 대기 조건의 제거 자원 유형에 따라 순서를 매긴다.
교착상태 회피
- 자원 할당 그래프 알고리즘 (Resource Allocation Graph Algorithm)
- 은행원 알고리즘 (Banker's algorithm)
번외, lock
제가 예전에 운영체제 강의를 들을때는 교수님께서 말씀하신 키워드는 사실 lock이었어요. 위의 대부분의 내용들을 자바의 정석 기반으로 작성을 하다보니까 synchronized만 언급했는데 얘도 추가로 찾아볼게요. lock() - unlock() 메서드를 통해서 쓰레드를 직접적으로 관리할 수 있습니다.
lock의 종류
ReentrantLock
- 재진입이 가능한 lock. 가장 일반적인 배타 lock
ReentrantReadWriteLock
- 읽기에는 공유적이고, 쓰기에는 배타적인 lock
StampedLock
- ReentrantReadWriteLock에 낙관적인 lock의 기능을 추가
출처 및 참고
자바의 정석
운영체제 강의(KOCW 반효경 교수님 강의 & 학부시절에 들은 강의)
https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/locks/Lock.html
'Java' 카테고리의 다른 글
[Java] 어노테이션 정리 (0) | 2021.06.22 |
---|---|
[JAVA] Enum 열거형 정리 (0) | 2021.06.07 |
[Java] 예외처리 (0) | 2021.05.18 |
[Java] 인터페이스에 대하여 (0) | 2021.05.10 |
[JAVA] 패키지 (0) | 2021.05.03 |