[Java] Thread 와 공유객체, 동기화

준우·2022년 4월 27일
1

Java

목록 보기
17/30
post-thumbnail

공유 객체란

놀이터에 그네가 하나 있고, 어린 아이들은 셋 있다. 이런 상황에서 그네를 [공유 객체], 아이들을 [Thread]라고 말할 수 있다. 하나의 객체를 여러 개의 쓰레드가 함께 사용하거나 공유한다는 것을 의미한다.

예를 들어 MusicBox 라는 공유 객체가 세 개의 메소드를 가지고 있다고 하자.
이 각각의 메소드들은 1초 이하의 시간동안 열번 반복하며 어떤 음악을 출력한다.

 public class MusicBox { 
        //신나는 음악!!! 이란 메시지가 1초이하로 쉬면서 10번 반복출력
        public void playMusicA(){
            for(int i = 0; i < 10; i ++){
                System.out.println("신나는 음악!!!");
                try {
                    Thread.sleep((int)(Math.random() * 1000));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } // for        
        } //playMusicA

        //슬픈 음악!!!이란 메시지가 1초이하로 쉬면서 10번 반복출력
        public void playMusicB(){
            for(int i = 0; i < 10; i ++){
                System.out.println("슬픈 음악!!!");
                try {
                    Thread.sleep((int)(Math.random() * 1000));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } // for        
        } //playMusicB
        //카페 음악!!! 이란 메시지가 1초이하로 쉬면서 10번 반복출력
        public void playMusicC(){
            for(int i = 0; i < 10; i ++){
                System.out.println("카페 음악!!!");
                try {
                    Thread.sleep((int)(Math.random() * 1000));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } // for        
        } //playMusicC  
    }

Thread

그리고 MusicBox를 사용하는 MusicPlayer 3명을 MusicPlayer 라는 쓰레드에 만들었다. MusicPlayer 3명은 하나의 MusicBox를 사용할 것이다. 받아들이는 타입에 따라서 musicBox 가지고 있는 메소드가 다르게 호출 되도록 run() 메소드를 오버라이딩 한다.

public class MusicPlayer extends Thread{
        int type;
        MusicBox musicBox;  
        // 생성자로 부터 musicBox와 정수를 하나 받아들여서 필드를 초기화
        public MusicPlayer(int type, MusicBox musicBox){
            this.type = type;
            this.musicBox = musicBox;
        }       
        // type이 무엇이냐에 따라서 musicBox가 가지고 있는 메소드가 다르게 호출
        public void run(){
            switch(type){
                case 1 : musicBox.playMusicA(); break;
                case 2 : musicBox.playMusicB(); break;
                case 3 : musicBox.playMusicC(); break;
            }
        }       
    }

실행 코드

위에서 만든 MusicBox 와 MusicPlayer를 이용하는 MusicBoxExam1 클래스를 아래와 같이 구현한다.

    public class MusicBoxExam1 {

        public static void main(String[] args) {
            // MusicBox 인스턴스
            MusicBox box = new MusicBox();

            MusicPlayer kim = new MusicPlayer(1, box);
            MusicPlayer lee = new MusicPlayer(2, box);
            MusicPlayer kang = new MusicPlayer(3, box);

            // MusicPlayer쓰레드를 실행합니다. 
            kim.start();
            lee.start();
            kang.start();           
        }   
    }

실행 결과

신나는 음악과 슬픈 음악, 카페 음악이 섞여서 출력되는 것을 확인할 수 있다.

🤔 그런데 만약 MusicBox의 세 개 메소드가 동시에 호출된다면, 혹은 MusicBox 내부가 고장이 난다면 어떻게 해야할까?

synchronized 메소드

이를 해결할 수 있는 방법이 바로 synchronized 키워드를 return 타입 앞에 사용하는 것이다. Synchronized를 사용하면 하나의 메소드가 실행될 때, 다른 메소드가 실행되지 못하도록 대기를 시키고 해당 메소드가 모든 실행이 완료되면 대기하던 다른 메소드가 실행되도록 만들 수 있다.

package level2;

public class MusicBox {
    public synchronized void playMusicA(){
        for(int i=0; i<10; i++){
            System.out.println("신나는 음악!!!");
            try {
                Thread.sleep((int)(Math.random()*1000));
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }


    public synchronized void playMusicB(){
        for(int i=0; i<10; i++){
            System.out.println("슬픈 음악...");
            try {
                Thread.sleep((int)(Math.random()*1000));
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }


    public synchronized void playMusicC(){
        for(int i=0; i<10; i++){
            System.out.println("카페 음악~~~");
            try {
                Thread.sleep((int)(Math.random()*1000));
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }

}

여러개의 쓰레드들이 공유객체를 사용할 때, 메소드에 synchronized가 붙어있다면 0.00001초라도 메소드가 먼저 실행이 되면 해당 객체의 사용권을 얻게 된다. 이런 사용권을 보통 모니터 락(monitor lock)이라고 한다.

이렇게 메소드에 synchronized 키워드를 붙인 상태에서 똑같이 MusicBoxExam 클래스를 실행 해보면 메소드 하나가 열번의 프린트문을 다 실행하고나서 다음 메소드가 실행되는 것을 볼 수 있다.

모니터 락은 메소드 실행이 종료되거나 wait() 같은 메소드를 만나기 전까지는 끝나지 않는다. 다른 쓰레드들은 모니터 락이 끝날때 까지 대기하고 있다가, 두번째 쓰레드가 메소드를 실행 하면서 모니터 락을 얻게되고 나중에 실행되는 쓰레드는 가장 마지막으로 모니터 락을 얻게 된다.

package level2;

public class MusicBox {
    public synchronized void  playMusicA(){
        for(int i=0; i<10; i++){
            System.out.println("신나는 음악!!!");
            try {
                Thread.sleep((int)(Math.random()*1000));
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }


    public synchronized void playMusicB(){
        for(int i=0; i<10; i++){
            System.out.println("슬픈 음악...");
            try {
                Thread.sleep((int)(Math.random()*1000));
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }


    public void playMusicC(){
        for(int i=0; i<10; i++){
            System.out.println("카페 음악~~~");
            try {
                Thread.sleep((int)(Math.random()*1000));
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }

}

MusicBox 클래스에서 PlayMusicC 메소드에서만 synchronized 키워드를 제거하면 나머지 쓰레드들이 메소드를 동시에 호출하더라도 객체를 망가뜨리지 않는다. 다른 쓰레드들이 synchronized 메소드를 실행하면서 모니터 락을 얻게 됐다고 하더라도 PlayMusicC 는 중간에 끼어들어 실행이 된다.

synchronized 블록

그런데, 메소드에 synchronized 키워드를 붙여쓴다면 메소드 코드가 길어지면서 마지막으로 대기하던 쓰레드는 실행까지 굉장히 오랜 시간을 기다려야할 수가 있다.

예시

 synchronized(this){
                //동시에 실행되면 안되는 코드
            }

이런 문제를 조금이라도 해결하기 위해 메소드 전체에 synchronized를 붙이는 게 아니라 동시에 실행되면 안되는 부분에다가만 synchronized 블록을 만드는 방법도 있다.


public class MusicBox {
    public void  playMusicA(){
        for(int i=0; i<10; i++){
            synchronized(this){
                System.out.println("신나는 음악!!!");
            }
            try {
                Thread.sleep((int)(Math.random()*1000));
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }


    public synchronized void playMusicB(){
        for(int i=0; i<10; i++){
            synchronized (this){
                System.out.println("슬픈 음악...");
            }
            try {
                Thread.sleep((int)(Math.random()*1000));
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }


    public  void playMusicC(){
        for(int i=0; i<10; i++){
            synchronized (this){
                System.out.println("카페 음악~~~");
            }
            try {
                Thread.sleep((int)(Math.random()*1000));
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }

}

위와 같이 synchronized 블록을 사용하면 객체가 모니터 락을 가질 수 있을 때만 동기화 하도록 할 수 있다. 즉, 딱 한줄로 감싸져있는 synchronized 블록이 종료될 때 대기하던 스레드가 실행되면서 다른 쓰레드들이 조금 더 빠르게 실행에 진입할 수 있게 되는 것이다.

이렇게 synchronized 키워드를 사용하면 메소드 전체가 아니라 정말 필요한 부분만 동기화 시킬 수 있다는 내용을 살펴보았다.

🙏 Reference

0개의 댓글