필기노트

자바 Thread, Runnable, Synchronized 본문

JAVA

자바 Thread, Runnable, Synchronized

우퐁코기 2022. 7. 12. 06:46
반응형
목차

1. 쓰레드를 생성하는 방법

2. 쓰레드를 생성하는 두 번째 방법

3. synchronized 기반 동기화 메소드

4. wait, notify, notifyAll에 의한 실행순서의 동기화

1. 쓰레드를 생성하는 방법

쓰레드란? 쓰레드는 하나의 프로세스 내에서 둘 이상의 프로그램 흐름을 형성할 수 있다.

class ShowThread extends Thread	// 1행
{
    String threadName;
    
    public ShowThread(String name)
    {
        threadName=name;
    }
    
    public void run()		// 10행
    {
        for(int i=0; i<100; i++)
        {
            System.out.println("안녕하세요. "+threadName+"입니다.");
            try
            {
                sleep(100);	// 17행
            }
            catch(InterruptedException e)
            {
                e.printStackTrace();
            }
        }
    }
}

class ThreadUnderstand
{
    public static void main(String[] args)
    {
        ShowThread st1 = new ShowThread("멋진 쓰레드");	// 31행
        ShowThread st2 = new ShowThread("예쁜 쓰레드");	// 32행
        st1.start();		// 33행
        st2.start();		// 34행
    }
}
  • 1행 : 여기 정의되어 있는 클래스는 Thread라는 이름의 클래스를 상속하고 있는데, 자바에서는 쓰레드도 인스턴스로 표현을 한다. 때문에 쓰레드의 표현을 위한 클래스가 정의되어야 하며, 이를 위해서는 Thread라는 이름의 클래스를 상속해야 한다.
  • 10행 : 쓰레드는 별도의 프로그램 흐름을 구성한다고 하지 않았는가? 즉 쓰레드는 쓰레드만의 main 메소드를 지닌다. 단 이름이 main은 아니다. 여기 10행에서 보이듯이 쓰레드의 main 메소드 이름은 run이다. 참고로 10행의 run 메소드는 thread 클래스의 run 메소드를 오버라이딩 한 것이다.
  • 17행 : sleep은 Thread 클래스의 static 메소드로서, 실행흐름을 일시적으로 멈추는 역할을 한다.
  • 31, 32행 : Thread 클래스를 상속하는 ShowThread의 인스턴스를 두 개 생성하고 있다. 이로써 두 개의 쓰레드가 생성된 셈이다.
  • 33, 34행: 31행과 32행을 통해서 쓰레드 인스턴스를 생성했으니, 이제 이들이 별도의 프로그램 흐름을 형성하도록(run 메소드가 호출되도록) 해야 한다. 그런데 별도의 흐름을 형성하는 방법은 매우 쉽다. Thread 클래스에 정의되어 있는 start 메소드만 호출하면 되기 때문이다.

run 메소드를 직접 호출하는 것도 불가능한 일은 아니다. 단 이러한 경우에는 단순한 메소드의 호출일 뿐, 쓰레드의 생성으로 이어지지는 않는다. 쓰레드는 자신만의 메모리 공간을 할당 받아서 별도의 실행흐름을 형성한다. 따라서 자바 가상머신은 start 메소드의 호출을 요구하는 것이다. 메모리 공간의 할당 등 쓰레드의 실행을 위한 기반을 마련한 다음에 run 메소드를 대신 호출해 주기 위해서 말이다. 

 


2. 쓰레드를 생성하는 두 번째 방법

쓰레드 클래스의 정의를 위해서는 Thread 클래스를 상속해야만 한다. 때문에 쓰레드 클래스가 상속해야 할 또 다른 클래스가 존재한다면, 이는 문제가 아닐 수 없다. 따라서 자바는 쓰레드를 생성하는 또 다른 방법을 제시하고 있다. 바로 인터페이스의 구현을 통한 방법인데, 인터페이스를 통한 다중상속의 효과에 해당하는 예로도 볼 수 있다.

class Sum
{
    int num;
    public Sum() { num=0; }
    public void addNum(int n) { num+=n; }
    public int getNum() { return num; }
}

class AdderThread extends Sum implements Runnable	// 9행
{
    int start, end;
    
    public AdderThread(int s, int e)
    {
        start=s;
        end=e;
    }
    public void run()					// 18행
    {
        for(int i=start; i<=end; i++)
            addNum(i);
    }
}

class RunnableThread
{
    public static void main(String[] args)
    {
        AdderThread at1 = new AdderThread(1, 50);	// 29행
        AdderThread at2 = new AdderThread(51, 100);	// 30행
        Thread tr1 = new Thread(at1);			// 31행
        Thread tr2 = new Thread(at2);			// 32행
        tr1.start();					// 33행
        tr2.start();					// 34행
        
        try
        {
            tr1.join();					// 38행
            tr2.join();					// 39행
        }
        catch(InterruptedException e)
        {
            e.printStackTrace();
        }
        
        System.out.println("1~100까지의 합 : "+(at1.getNum()+at2.getNum()));
    }
}
  • 9, 18행 : Sum 클래스를 상속하면서 Runnable 인터페이스를 구현하고 있다. 이 인터페이스는 run 메소드 하나로 이뤄져 있다.
  • 29, 30행 : Runnable 인터페이스를 구현하는 AdderThread 클래스의 인스턴스 두 개를 생성하고 있다. 하지만 이 인스턴스를 대상으로 start 메소드를 호출할 순 없다. start 메소드는 Thread 클래스의 메소드이기 때문이다.
  • 31, 32행 : Runnable 인터페이스를 구현하는 인스턴스를 대상으로 쓰레드의 생성방법을 보이고 있다. 이렇듯 Runnable 인터페이스를 구현하는 인스턴스의 참조 값을 전달받을 수 있는 생성자가 Thread 클래스에 정의되어 있다. 바로 이 생성자를 통해서 클래스 인스턴스를 생성해야 한다. 결국은 Thread 클래스의 인스턴스가 생성되었다는 점에 주목하자.
  • 33, 34행 : start 메소드의 호출을 통해서 최종으로 쓰레드를 생성 및 실행시키고 있다. 이로 인해서 생성자를 통해 전달된 인스턴스의 run 메소드가 호출된다.
  • 38, 39행 : 쓰레드 인스턴스를 대상으로 join 메소드를 호출하고 있다. 이는 해당 쓰레드가 종료될 때까지 실행을 멈출 때 호출하는 메소드이다. 결과적으로 tr1과 tr2가 참조하는 두 쓰레드가 종료되어야 비로소 46행을 실행하게 된다.

 


3. synchronized 기반 동기화 메소드

정말 필요한 부분에, 최소한의 형태로 동기화를 하는 개발자가 정말로 동기화를 잘하는 개발자이다. 과도한 동기화를 통해서 성능에 상관없이 결과만 보이는 것은 누구나 할 수 있는 동기화이다.

class IHaveTwoNum
{
    int num1=0;			// 3행
    int num2=0;			// 4행
    
    public void addOneNum1()	// 6행
    {
        synchronized(this)	// 8행
        {
            num1+=1;
        }
    }
    public void addTwoNum1()	// 13행
    {
        synchronized(this)
        {
            num1+=2;
        }
    }
    public void addOneNum2()	// 20행
    {
        synchronized(key)
        {
            num2+=1;
        }
    }
    public void addTwoNum2()	// 23행
    {
        synchronized(key)
        {
            num2+=2;
        }
    }
    
    public void showAllNums()
    {
        System.out.println("num1 : "+num1);
        System.out.println("num2 : "+num2);
    }
    
    Object key = new Object();	// 41행
}

class AccessThread extends Thread
{
    IHaveTwoNum twoNumInst;
    
    public AccessThread(IHaveTwoNum inst)
    {
        twoNumInst=inst;
    }
    
    public void run()
    {
        twoNumInst.addOneNum1();
        twoNumInst.addTwoNum1();
        twoNumInst.addOneNum2();
        twoNumInst.addTwoNum2();
    }
}

class SyncObjectKey
{
    public static void main(String[] args)
    {
        IHaveTwoNum numInst = new IHaveTwoNum();
        
        AccessThread at1 = new AccessThread(numInst);	// 68행
        AccessThread at2 = new AccessThread(numInst);	// 69행
        
        at1.start();
        at2.start();
        
        try
        {
            at1.join();
            at2.join();
        }
        catch(InterruptedException e)
        {
            e.printStackTrace();
        }
        numInst.showAllNums();
    }
}
  • 68, 69행 : 두 개의 쓰레드 인스턴스 생성과정에서 42행에서 생성한 인스턴스의 참조 값이 전달되었다. 즉 IHaveTwoNum의 인스턴스는 두 개의 쓰레드에 의해 접근이 이뤄지기 때문에, 3행과 4행에 접근하는 문장들은 동기화 처리가 되어야 한다.
  • 8행 : 동기화의 대상을 메소드 전부가 아니라 코드 블록 일부로 제한할 필요가 있다. 메소드 전체의 실행이 완료될 때까지 열쇠가 반납되지 않기 때문에, 이에 대한 성능 감소가 이만 저만이 아니다.
  • 41행 : 첫 번째 필요한 열쇠는 this로부터 얻고 열쇠가 더 필요할 때 Object 인스턴스를 생성해서 동기화의 '열쇠'로 사용하였다.
  • 6, 13, 20, 27행 : 6행과 13행의 메소드, 그리고 20행과 27행의 메소드에서 동기화를 위해 사용된 열쇠가 다르기 때문에, at1 쓰레드가 num1에 접근하는 메소드를 실행하는 중간에, at2 쓰레드에 의해서 num2에 접근하는 메소드를 호출할 수 있으므로써 하나의 열쇠를 대상으로 동기화가 되는 것보다 과도한 동기화로 인한 성능의 저하는 발생하지 않는다.

 


4. wait, notify, notifyAll에 의한 실행순서의 동기화

class NewsPaper
{
    String todayNews;
    boolean isTodayNews=false;
    
    public void setTodayNews(String news)	// 6행
    {
        todayNews=news;
        isTodayNews=true;
        
        synchronized(this)
        {
            notifyAll();	// 모두 일어나세요!
        }
    }
    
    public String getTodayNews()		// 17행
    {
        if(isTodayNews==false)
        {
            try
            {
                synchronized(this)
                {
                    wait();	// 한숨 자면서 기다리겠습니다.
                }
            }
            catch(InterruptedException e)
            {
                e.printStackTrace();
            }
        }
        
        return todayNews;
    }
}

class NewsWriter extends Thread
{
    NewsPaper paper;
    
    public NewsWriter(NewsPaper paper)
    {
        this.paper=paper;
    }
    public void run()
    {
        paper.setTodayNews("자바의 열기가 뜨겁습니다.");
    }
}

class NewsReader extends Thread
{
    NewsPaper paper;
    
    public NewsReader(NewsPaper paper)
    {
        this.paper=paper;
    }
    public void run()
    {
        System.out.println("오늘의 뉴스 : "+paper.getTodayNews());
    }
}

class SyncNewsPaper
{
    public static void main(String[] args)
    {
        NewsPaper paper=new NewsPaper();
        NewsReader reader1=new NewsReader(paper);
        NewsReader reader2=new NewsReader(paper);
        NewsWriter writer=new NewsWriter(paper);
        
        try
        {
            reader1.start();
            reader2.start();
            
            Thread.sleep(1000);		// 80행
            writer.start();		// 81행
            
            reader1.join();
            reader2.join();
            writer.join();
        }
        catch(InterruptedException e)
        {
            e.printStackTrace();
        }
    }
}
  • 6행 : 데이터를 가져다 놓는 쓰레드를 위한 메소드이다. 데이터를 가져다 놓은 다음, notifyAll 메소드의 호출을 통해서 혹시라도 잠을 자고 있는 쓰레드 전부를 깨우고 있다. 여기서 중요한 점은 1행에 정의된 NewsPaper 클래스의 인스턴스에 걸쳐서 잠을 자고 있는 쓰레드를 대상으로 잠을 깨운다는 사실이다.
  • 17행 : 데이터를 가져가는 쓰레드를 위한 메소드이다. isTodayNews가 false라면, 아직 데이터가 도착하지 않은 상황이니, 25행의 wait 메소드 호출을 통해서 낮잠에 들어간다. 여기서 중요한 점은 1행에 정의된 NewsPaper 클래스의 인스턴스에 걸쳐서 잠을 자게 된다는 사실이다.
  • 80, 81행 : 데이터를 가져다 놓는 쓰레드의 실행을 늦추고 있다. 이는 데이터를 가져가는 쓰레드가 wait 메소드를 호출할 때까지 기다리게 하기 위한 인위적인 조작이다.

 


출처 : 난 정말 JAVA를 공부한 적이 없다구요! - Chapter 23. 쓰레드(Thread)와 동기화

반응형
Comments