티스토리 뷰

JAVA/정리

Thread

란텔 2016. 1. 14. 23:27

Thread


쓰레드는 프로세스에 포함되는 자원이다.

프로세스란 실행중인 프로그램이며, 프로그램을 실행할 때 필요한 자원이 하나 이상의 쓰레드이다.


하나의 프로세스에 하나의 쓰레드만 있다면 싱글 쓰레드 프로세스라 부르고, 하나의 프로세스에 하나 이상의 쓰레드가 있다면 멀티 쓰레드 프로세스라고 부른다.

옛날에 쓰던 DOS환경을 보통 싱글쓰레드, 요즘 쓰는 windows환경을 멀티쓰레드로 이해하면 되겠다. 


자바에서 콘솔 프로그램을 짤 때 public staitc void main(String[] args)라는 메서드가 있는데 이것조차도 하나의 쓰레드이다.



아래의 쓰레드 예제는 쓰레드를 생성하는 두가지 방법을 코딩한 것이다.

쓰레드를 생성하는 방법은 Runnble인터페이스를 구현하는 방법과 Thread클래스를 상속하는 방법이 있다.

어느것을 많이 쓴다고는 말을 못하지만..

아무래도 객체지향언어가 클래스끼리는 단일 상속만을 지원하기 때문에, 제약이 없는 Runnable인터페이스를 구현하여 쓰레드를 다루는 방법이 더 나은듯 하다.



위 코드를 보면 run()을 호출하지 않고 start()라는 메서드를 호출하고 있다.

run()을 호출한다는 것은 그냥 일반 메서드를 호출하는 것과 같은 것이지, 쓰레드를 시작하는 것은 아니다.

start()를 호출해야 쓰레드를 위한 호출스택이 하나더 생성되고, 생성된 호출스택에 run()이 호출되며 쓰레드를 수행하게 된다.





우선순위

쓰레드에는 우선순위가 존재한다. 1~10까지 있는데 숫자가 높을 수록 더 높은 우선순위를 갖는다.

우선순위는 상대적이기 때문에

1우선순위, 2우선순위와 

9우선순위, 10우선순위를 가지는 쓰레끼리는 차이가 없다.


//Thread 상속
class PlusThread extends Thread{
	public void run(){
		Thread.currentThread().setPriority(7);
		System.out.println("in PlusThread>>"+Thread.currentThread().getName());
		for(int i=0; i<20; i++)
		System.out.print("+");
	}
}

//Runnable 구현
class MinusThread implements Runnable{
	
	public void run(){
		for(int i=0; i<20; i++)
			System.out.print("-");
	}
	
}

public class ThreadEx {
	public static void main(String[] args){
		//Thread 상속클래스로 Thread생성
		System.out.println(Thread.currentThread().getName());  //현재 쓰레드는 main쓰레드
		System.out.println(Thread.currentThread().getPriority()); //main쓰레드의 기본 우선순위는 5
		Thread.currentThread().setPriority(3); //main쓰레드의 우선순위를 3으로.
		System.out.println(Thread.currentThread().getPriority()); //3으로 우선순위가 변경된 main쓰레드
		
		
		PlusThread t = new PlusThread();  //
		System.out.println(t.getName());  //PlusThread의 이름 출력
		System.out.println(t.getPriority()); //PlusThread의 우선순위는 3
		
		
		//Runnbale구현 클래스로 쓰레드 생성
		Runnable r = new MinusThread();
		Thread t1 = new Thread(r);
		
		t.start();
		t1.start();
		//t1.sleep(1000);	
		
		System.out.println(t.getPriority()); //우선순위가 3이되었다 7이되었다한다.
		
		
	}
}





쓰레드는 동기화가 되어있지 않다. 싱글 쓰레드 프로그램으 사용한다면 동기화가 필요 없지만 멀티 쓰레드 프로그램을 사용한다면 적절한 쓰레드 스케쥴링이 필요하다.





import java.util.ArrayList;
import java.util.List;

//휘발유 클래스 쓰레드
class Gasoline implements Runnable {
	long amount = 5000L; // 휘발유 양을 5000으로 한다.
	List<Car> car;

	// 인스턴스 생성시 자동차 목록들을 넣는다.
	public Gasoline(List<Car> car) {
		this.car = car;
	}

	public void run() {
		// 기름 넣기
		try {
			Thread.sleep(1000);
		} catch (Exception e) {
		}
		Car c = car.get((int) (Math.random() * car.size()));
		synchronized (this) {
			while (this.amount > c.amount) {
				refuel(c);
			}
		}
	}

	// 기름 넣기
	public void refuel(Car c) {

		// Car c = car.get((int)(Math.random() * car.size()));

		System.out.println(c.carName + "주유시작");

		this.amount -= c.amount;

		System.out.println(c.carName + "주유끝 >>>>> 주유소 남은 휘발유" + this.amount);
	}

}

// 차량 생성 클래스
class Car extends Thread {
	String carName;
	long amount;

	Car(String carName, long amount) {
		this.carName = carName;
		this.amount = amount;
	}

	public void run() {

	}

}

public class SynchronizedTest {
	public static void main(String[] args) {

		Car c1 = new Car("소울", 200);
		Car c2 = new Car("소나타", 400);
		Car c3 = new Car("벤츠", 500);

		List<Car> list = new ArrayList<Car>();
		list.add(c1);
		list.add(c2);
		list.add(c3);

		Runnable r = new Gasoline(list);
		Thread t1 = new Thread(r);
		Thread t2 = new Thread(r);
		Thread t3 = new Thread(r);

		t1.start();
		t2.start();
		t3.start();

	}
}

위의 예는 차량에 휘발유를 넣는 것을 생각해서 코딩한 것인데..

휘발유가 다 소모될 때까지 차량에 기름을 넣는다. 


run() 메서드를 보면 synchronized(this){} 블록이 보이는데 이것이 의미하는 바는 현재 실행상태인 자기 자신의 쓰레드 작업이 끝나기 전에 다른 쓰레드가 이 블록내로 접근하지 못하게 하는 것이다.

t1이 블록내의 구간을 수행 중이면 t1이나 t2는 접근을 하지 못한다.


synchronized키워드는 메서드 레벨에서도 사용할 수 있다.


public synchronized  void refuel()

처럼 사용 할 수 있으며, 이것이 의미하는 것은 현재 실행 상태인 쓰레드가 refuel메서드의 작업이 끝나기 전에는 다른 쓰레드를 접근하지 못하게 하는 것이다.







wait()와 notify() 그리고 notifyAll()


synchronized를 사용해서 동기화 쓰레드를 구현하다 보면 특정 상황에서 A 쓰레드가 요구조건이 만족되지 않아 계속 락을 차지하고 있는 경우가 있다. 그렇기 때문에 이 동기화 구간에 다른 쓰레드들(B, C, D.....)이 접근하지 못하게 되서 오랜시간 동안 A 쓰레드를 기다려야 하는 상황이 발생한다.


이같은 상황을 완화하기 위해서 wait()와 notify()가 존재한다.


wait와 notify는 Object클래스에 정의 되어 있으며, 동기화(synchronized) 블록 내에서만 사용할 수 있다.



쓰레드에 대해서 wait()를 호출하면 그 쓰레드는 즉시 작업을 멈추고 wating pool이라는 곳에 들어가서 기다리게 된다. 그래서 다른 쓰레드에게 제어권이 넘어가 동기화 구간을 수행하게 한다. 그러다가 notify()를 호출하면 wating pool에 대기하고 있는 쓰레드들을 깨우게 되어 다음 쓰레드 대상이 될 수 있는 실행대기 상태로 변하게 된다.


notify()를 사용하면 wating pool에 들어있는 쓰레드 중에 하나만 깨우고, notifyAll()을 호출하면 waiting pool의 전부를 깨운다. 


정말 운이 나쁘면 A라는 쓰레드가 wating pool에 들어가 있더라도 같은 종류의 쓰레드인 B라는 쓰레드가 락(lock)을 차지하고 다시 또 wating pool에 들어가고 하는 반복적인 상황이 발생할 수 있는데..

이런 것을 '기아현상' 이라고 한다.


예를 들어 비품구매 비용을 지급하는 사장 쓰레드가 있다고 하자. 비품이 그때 그때 필요한 직원들은 수십명이 있다.

직원이 필요할 때 사장이 돈을 주지만 사장은 한 자리에 계속있지 않는다. 외부에(wating pool) 업무 차 가기도 한다. 돈을 받아서 비품을 구매한 직원도 사무실 자기자리(wating pool)에 업무를 보러 간다.


여기서 사장이나 직원 둘 다 wating pool에 있다.

notify()를 사용한다면 wating pool에 있는 하나의 쓰레드만 대기열로 복귀시킨다. 그게 사장이 될지 각각의 직원들이 될지 알 수 없기 때문에 재수 없으면 직원들만 계속 나와서 비품비용을 요구하지만 사장은 자리에 없고 외부(wating pool)에 있다. 그렇기 때문에 오랜기간 동안 사장은 비품 비용을 지급할 수 없는 상황이 발생한다.

이같은 상황을 '기아현상' 이라고 한다.


기아현상을 피하는 방법은 notifyAll()을 사용하면 된다. notifyAll()은 wating pool의 모든 쓰레드를 꺼내서 대기열로 보내기 때문에 직원뿐만이 아니라 사장도 다음 쓰레드 실행의 대상이 될 수 있다.


다음은 이 같은 상황을 비슷하게 코드로 표현한 것이다.

실행 해본 결과 

notify()를 사용한 경우 : 19초 19초 18초 15초 20초

notifyAll()를 사용한 경우 : 10초 9초 17초 12초 10초

로 notifyAll()을 사용한 경우에 더 빨리 작업을 수행한다.

package 쓰레드; import java.text.SimpleDateFormat; import java.util.Date; import java.util.concurrent.locks.ReentrantLock; class MoneyAndFixtures extends Thread { private int money = 0; //직원이 가지는 비품 비용 int bossMoney = 24000; // 사장이 지급할 수 있는 총 비품 비용 final int COM_MONEY = 4000; // 한번에 지급가능한 비품비 boolean isMoney = true; //사장이 돈이 없을 경우 false Fixtures[] p = Fixtures.values(); //ReentrantLock lock = new ReentrantLock(); @Override public void run() { long time1 = System.currentTimeMillis(); Fixtures p = null; while (isMoney) { synchronized (this) { notifyAll(); //다른 쓰레드가 시작 되는 순간 notifyAll()을 호출해서 wating pool의 쓰레드를 전부 깨운다. if (isMoney == false) { break; } String name = Thread.currentThread().getName(); if (name.equals("boss")) { boolean is = allocateMoney(); if (!is) { System.out.println("사장의 돈 지급이 끝났습니다."); isMoney = false; long time2 = System.currentTimeMillis(); System.out.println((time2 - time1) / 1000.0 + "초 걸림"); break; } else { try { wait(); //wating pool로 사장 쓰레드를 보낸다. continue; } catch (InterruptedException e) { e.printStackTrace(); } } } else { p = buyFixtures(); if (p.getPrice() > money) { if (isMoney == false) { System.out.println("더이상 돈이 없어 끝냅니다. 현재돈:" + money); // isMoney = false; long time2 = System.currentTimeMillis(); System.out.println((time2 - time1) / 1000.0 + "초 걸림"); break; } System.out.println(p.name() + "을 사기엔 돈이 모자랍니다."); try { wait(); //wating pool로 직원 쓰레드를 보낸다. continue; } catch (InterruptedException e) { e.printStackTrace(); } } money = money - p.getPrice(); System.out.println(Thread.currentThread().getName() + "가 " + p.name() + "(" + p.getPrice() + ")을 사고" + money + "가 남았습니다."); } // end if } // end synchronized } // end while loop } public boolean allocateMoney() { if (bossMoney >= COM_MONEY) { money = money + COM_MONEY; bossMoney = bossMoney - COM_MONEY; System.out.println("돈을 지급했습니다."); return true; } else { return false; } } // enum Fixtures객체 임의로 하나의 오디널 선택하기 public Fixtures buyFixtures() { try { Thread.sleep(200); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } int pid = (int) (Math.random() * 2); // 이넘에 맞는 임의의 수 생성 Fixtures product = null; for (Fixtures item : p) { // System.out.println(item.ordinal()); int ordinal = item.ordinal(); if (ordinal == pid) { product = item; break; } } return product; } } //비품 이넘 타입 enum Fixtures { A4PAPER(4000), COFFEE(5000); private final int value; private Fixtures(int value) { this.value = value; } public int getPrice() { return value; } } public class ThreadEx { public static void main(String[] args) { MoneyAndFixtures mf = new MoneyAndFixtures(); //사장 포함 10개의 쓰레드를 생성 Thread[] t = new Thread[10]; for (int i = 0; i < t.length; i++) { if (i == t.length - 1) { t[i] = new Thread(mf, "boss"); t[i].start(); } else { t[i] = new Thread(mf, "staff" + i); t[i].start(); } } } }






Lock과 Condition


- synchronized키워드를 이용한 동기화 외에도 lock클래스를 통해 동기화가 가능하다.

- java.util.concerrent.locks에 관련된 클래스들이 있다.



그중에서도 ReentrantLock과 ReentrantReadWriteLock을 살펴보겠다.



ReentrantReadWriteLock은 ReadWriteLock인터페이스를 구현하고 있고, 또한 기능적 측면에서 ReentrantLock의 기능을 지원한다.


ReadWriteLock은 한 쌍의 락을 가지고 있는데 오직 읽기만 가능한 lock과 쓰기만 가능한 lock이다.


사용방법은 우선 ReentrantReadWriteLock의 인스턴스를 생성한 뒤 readLock()이나 writeLock()을 호출해서 lock()락을 얻거나 unlock()해제한다.


ReentrantReadWriteLock은 공정한 모드와 비공정한 모드를 지원하는데 비공정성 모드가 기본이며, 비공정성 모드는 하나 이상 또는 더 많은 읽기 또는 쓰기 쓰레드가 서로 락을 얻으려고 하는 습성이 있긴 하지만 공정성 모드 보다 높은 처리량을 보인다.

공정성 모드는 new ReentrantReadWriteLock(true)로 인스턴스를 생성하면 공정성 모드가 된다. 공정성 모드일 때, 쓰레드들은 거의 정확한 순서대로 각각의 쓰레드가 락을 얻으며, 현재 점유중인 쓰레드가 작업을 끝내고 lock이 해제될 때

가장 긴 시간동안 대기중인 하나의 쓰기 쓰레드에 쓰기 락이 할당 되거나 

대기하고 있는 모든 쓰기 쓰레드보다 더 오래 대기중인 읽기 쓰레드의 그룹이 있다면 그 그룹에 읽기 락이 할당된다







Lock과 Condition을 이용한 동기화

앞서 물품비용을 지급하는 사장 쓰레드와 물품이 필요해서 사장에게서 지급된 돈으로 물품을 구매하는 직원 쓰레드에 빗대어 코딩을 했었다.

여기서는 wait()와 notify()를 사용했는데 wait()를 사용함으로써 현재 lock을 얻고 있는 쓰레드가 waiting pool이라는 쓰레드 대기 풀에 진입해서 대기한다고 하였다. 그 상태에서 notify()를 호출하면 wating pool에서 대기하고 있는 하나의 쓰레드를 호출해서 호출된 쓰레드가 락을 얻을 수 있는 상태를 만든다고 하였는데, 여기서 문제점은 wating pool에서 대기하는 대상이 사장과 직원이 둘 다 될 수 있는데에 문제점이 있다.


그 문제점이란 다음과 같다.

직원이 물품 비용이 부족해서 사장에게 비용을 받아야 함에도 프로그램이 notify()로 가져온 쓰레드가 직원이 될 경우,

재수 없으면 한동안 notify()로 호출된 쓰레드가 직원이 될 수 있다. 이는 비효율적이므로 성능상의 문제가 될 수 있다.



이런 문제를 해결할 수 있는 것이 Condition클래스이다.

Condition클래스 타입의 참조변수는 Lock클래스타입의 인스턴스에서 newCondition()메서드를 통해서 참조할 수 있다.

Condition은 서로 다른 wating pool에 각각의 클래스들을 분리해서 대기 시킬 수 있기 때문에 사장 쓰레드에 대해 lock을 얻고 싶다면 사장 쓰레드를 담은 Condition에서, 직원 쓰레드에 대해 lock을 얻고 싶다면 직원 쓰레드를 담은 Condition에서 메서드를 통해 다음 순번에 lock을 얻도록 할 수 있다.



Lock클래스 인스턴스를 생성하고 Condition을 만드는 방법

ReentrantLock lock = new ReentrantLock();
Condition bossCondition = lock.newCondition();
Condition staffCondition = lock.newCondition();

타입의 참조변수를 생성하고 인스턴스를 만들었다면, 임계영역(쓰레드가 lock을 얻어야 되는 전체 구간)의 시작과 끝에

lock.lock()으로 잠그고, lock.unlock()으로 해제한다.

그리고 Condition클래스의 (await() : 해당 Condition의 wating poll에 대기, signal() : 해당 Condition의 wating pool에 대기중인 쓰레드를 다음 실행 대기 상태로 함.) 메서드를 통해 원하는 쓰레드를 가지고 있는 Condition에 대하여 호출할 수 있다.



다음은 wait()와 notify()로 위에서 코딩한 것을 Lock과 Condition사용을 통해 재구성 해 본 코드이다.

class MoneyAndFixtures extends Thread {

    private int money = 0; // 직원이 가지는 비품 비용
    int bossMoney = 24000; // 사장이 지급할 수 있는 총 비품 비용
    final int COM_MONEY = 4000; // 한번에 지급가능한 비품비
    boolean isMoney = true; // 사장이 돈이 없을 경우 false
    Fixtures[] p = Fixtures.values();
    ReentrantLock lock = new ReentrantLock();
    Condition bossCondition = lock.newCondition();
    Condition staffCondition = lock.newCondition();

    @Override
    public void run() {
        long time1 = System.currentTimeMillis();

        Fixtures p = null;

        while (true) {

            lock.lock();

            String name = Thread.currentThread().getName();

            if (isMoney == false) {
                break;
            }

            
            if (name.equals("boss")) {
                boolean is = allocateMoney();
                // System.out.println(is);
                if (is == false) {
                    System.out.println("사장의 돈 지급이 끝났습니다. bossMoney="
                            + bossMoney);
                    isMoney = false;
                    long time2 = System.currentTimeMillis();
                    System.out.println((time2 - time1) / 1000.0 + "초 걸림");
                    break;
                } else {
                    try {
                        staffCondition.signal(); //staffCondition에 대기하고 있는 쓰레드 하나를 깨운다.
                        bossCondition.await(); //그리고 bossCondition에 대기하고 있는 쓰레드를 대기시킨다.
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                        System.out.println("exception ocurr");
                    }
                }
            } else {
                p = buyFixtures();

                if (p.getPrice() > money) {

                    if (bossMoney < COM_MONEY) {
                        System.out.println("더이상 돈이 없어 끝냅니다. 현재돈:" + money);
                        // isMoney = false;
                        long time2 = System.currentTimeMillis();
                        System.out.println((time2 - time1) / 1000.0 + "초 걸림");
                        break;
                    }

                    System.out.println(p.name() + "을 사기엔 돈이 모자랍니다.");
                    try {
                        bossCondition.signal();
                        staffCondition.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                } else {

                    money = money - p.getPrice();
                    System.out.println(Thread.currentThread().getName() + "가 "
                            + p.name() + "(" + p.getPrice() + ")을 사고" + money
                            + "가 남았습니다.");
                }

            } // end if

            lock.unlock();

        } // end while loop

    }

    public boolean allocateMoney() {
        if (bossMoney >= COM_MONEY) {
            money = money + COM_MONEY;
            bossMoney = bossMoney - COM_MONEY;
            System.out.println("돈을 지급했습니다.");
            return true;
        } else {
            return false;
        }
    }

    // enum Fixtures객체 임의로 하나의 오디널 선택하기
    public Fixtures buyFixtures() {

        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        int pid = (int) (Math.random() * 2); // 이넘에 맞는 임의의 수 생성
        Fixtures product = null;
        for (Fixtures item : p) {
            // System.out.println(item.ordinal());
            int ordinal = item.ordinal();
            if (ordinal == pid) {
                product = item;
                break;
            }
        }
        return product;
    }

}

// 비품 이넘 타입
enum Fixtures {
    A4PAPER(4000), COFFEE(5000);

    private final int value;

    private Fixtures(int value) {
        this.value = value;
    }

    public int getPrice() {
        return value;
    }
}

public class ThreadEx {

    public static void main(String[] args) {

        MoneyAndFixtures mf = new MoneyAndFixtures();
        // 사장 포함 10개의 쓰레드를 생성
        Thread[] t = new Thread[10];
        for (int i = 0; i < t.length; i++) {
            if (i == t.length - 1) {
                t[i] = new Thread(mf, "boss");
                t[i].start();
            } else {
                t[i] = new Thread(mf, "staff" + i);
                t[i].start();
            }
        }

    }

}

경과 시간을 보면 평균 5초가 넘지 않는다. notify()를 사용할 때는 기아현상(wating pool에서 가져오는 쓰레드가 사장일지 직원일지 알 수없음), notifyAll()을 사용할 때는

경쟁(wating pool에서 모든 스레드를 깨우지만 실행대기열에서 서로 lock을 얻기 위해 경쟁하기 때문에 역시 필요한 쓰레드가 다음에 실행 될것이라 장담할 수 없음)상태를 해소한 결과를 볼 수 있다.





'JAVA > 정리' 카테고리의 다른 글

[generics] 제네릭스  (0) 2016.04.21
익명 클래스  (0) 2016.04.07
clone() 에 대하여...  (0) 2015.07.04
자바7  (0) 2014.11.26
[코딩]List안에 Map자료구조의 데이터 정렬  (0) 2014.06.08
Comments
최근에 올라온 글
최근에 달린 댓글
TAG
more
Total
Today
Yesterday