본문 바로가기
블로그 이미지

방문해 주셔서 감사합니다! 항상 행복하세요!

  
   - 문의사항은 메일 또는 댓글로 언제든 연락주세요.
   - "해줘","답 내놔" 같은 질문은 답변드리지 않습니다.
   - 메일주소 : lts06069@naver.com


Java(자바)

Java synchronized, ReentrantLock, semaphore

야근없는 행복한 삶을 위해 ~
by 마샤와 곰 2019. 6. 13.

 

 

자바에서는 동기화에 대한 지원을 여러방법으로 가능토록 하는데 대표적으로 synchronized와 semaphore를 통해서 쉽게 구현하게 해 주고 있다.

synchronized는 메소드나 문장구간등에 선언하여 해당 행동이 끝날 때 까지 다른 쓰레드가 접근을 못하도록 하는데 반해 semaphore는 좀 더 유연하다

semaphore는 접근할 수 있는 쓰레드의 수와 외부에서 락 또는 릴리즈 등에 대한 컨트롤도 가능 할 수 있게 해준다.

 

일반적인 동기화의 예제코드이다.

public class Tester {
	public static void main(String args[]){
		Tester mm = new Tester();
		try {
			new Thread(()->{
				try {
					mm.looper(1234);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}	
			}).start();			
			new Thread(()->{
				try {
					mm.looper(5678);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}	
			}).start();
		} catch (Exception e) {
			e.printStackTrace();
		}		
	}

	private synchronized void looper(int aaa) throws InterruptedException{
		while(true){
			Thread.sleep(1000);
			System.out.println("abcd : " + aaa);
		}
	}
}

위 코드를 실행하면 두번째 쓰레드는 동기화에 의해서 접근을 계속 못하는 현상이 발생한다.

타이머처럼 어떤 쓰레드가 돌면서 동기화된 구간을 일정시간이 지나면 풀어 줄 수 있는 방법이 없을까? 라는 생각이 들었다.

그래서 이렇게 한번 바꾸어 보았다.

public class Tester {

	public static void main(String args[]){
		Tester mm = new Tester();
		try {
			new Thread(()->{
				try {
					mm.looper(1234);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}	
			}).start();			
			new Thread(()->{
				try {
					mm.looper(5678);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}	
			}).start();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	private synchronized void looper(int aaa) throws InterruptedException{
        Object that = this;
		new Thread(()->{
			for(int a=0; a < 5;a++){
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			that.notify();  //깨어나줘!!
		}).start();

		while(true){
			Thread.sleep(1000);
			System.out.println("abcd : " + aaa);
		}
	}
}

중간에 다른 쓰레드를 만들어서 notify메소드를 실행시켜서 다음쓰레드가 접근 가능 할 수 있도록 코드를 변경했다고 생각했었는데.. 아래와 같은 오류가 났다.

앗차...저문법이 아니지..

아...뭔가 코드를 만들때부터 이상하다 싶더니만..

위 오류의 원인은 해당 객체가 동기화되지 않았는데 뭘 깨우는지 몰라서 발생한 오류이다.

생각해보니 notify는 wait이라는 메소드랑 같이 쓰였던 것 같았는데 말이다.

 

그리하여 외부에서도 해당 동기화된 메소드를 풀 수 있는 방법이 없는지 시도하여 보았다.

아래 코드를 살펴보자.

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Tester {

	public static void main(String args[]){
		Tester mm = new Tester();
		try {
			new Thread(()->{
				try {
					mm.looper(1234);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}	
			}).start();			
			new Thread(()->{
				try {
					mm.looper(5678);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}	
			}).start();
		} catch (Exception e) {
			e.printStackTrace();
		}
		
	}
	
	private static final Lock lock = new ReentrantLock();  //락이라는 인터페이스다.
	
	private void looper(int aaa) throws InterruptedException{
		lock.lock();
		System.out.println(lock.tryLock());  //잠겼나? true가 나오는군..
		new Thread(()->{			
			for(int a=0; a < 5;a++){
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			System.out.println(lock.tryLock());  //엥? 여기서는 false가 나오네?
			lock.unlock();  //오류발생구간..
		}).start();

		while(true){
			Thread.sleep(1000);
			System.out.println("abcd : " + aaa);
		}
	}		
}

ReentrantLock라는 클래스는 동기화의 시작, 끝지점을 지정할 수 있는 클래스이다.

ReentrantLock클래스는 lock 메소드를 호출한 시점부터 unlock 시점까지는 일반 절차식 로직에서는 문제가 없는데, 타이머 역할을 하는 쓰레드가 unlock을 실행하면 동일한 오류가 나는 것이다.

그러한 이유는 비슷하게도 ReentrantLock가 동기화를 보는 대상이 없기 때문이다.( 기존 클래스에서 타이머 쓰레드로 바뀌었기 때문이다 )

그리하여 마지막으로 Semaphore를 활용하였다.

import java.util.concurrent.Semaphore;

public class Tester {

	public static void main(String args[]){
		Tester mm = new Tester();
		try {
			new Thread(()->{
				try {
					mm.looper(1234);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}	
			}).start();			
			new Thread(()->{
				try {
					mm.looper(5678);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}	
			}).start();
		} catch (Exception e) {
			e.printStackTrace();
		}
		
	}
	
	private static final Semaphore smp = new Semaphore(1);  //1은 접근 가능 갯수 이다.
	private void looper(int aaa) throws InterruptedException{
		smp.acquire();  //잠겨라
        System.out.println(smp.availablePermits());  //접근 가능 갯수
		new Thread(()->{			
			for(int a=0; a < 5;a++){
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			smp.release();  //풀려라
			System.out.println(smp.availablePermits());  //접근 가능 갯수
		}).start();

		while(true){
			Thread.sleep(1000);
			System.out.println("abcd : " + aaa);
		}
	}
}

세마포어를 활용하니 간단하게도 타이머 역할을 하는 쓰레드가 잘 동작하는 것을 볼 수 있었다.

간단하게 정리를 해보자면,

1. synchronized 문법 : 엄격한 동기화, 1개의 접근만 허용한다.

2. ReentrantLock 클래스 : 동기화의 시작/끝점에 대해 명시적 지정 가능, 선언한 객체 내에서만 동작

3. semaphore 클래스 : 동기화의 시작/끝점에 대해서 명시적 지정 가능, 조금 더 자유로운 시작/종료 가능

반응형
* 위 에니메이션은 Html의 캔버스(canvas)기반으로 동작하는 기능 입니다. Html 캔버스 튜토리얼 도 한번 살펴보세요~ :)
* 직접 만든 Html 캔버스 애니메이션 도 한번 살펴보세요~ :)

댓글