본문으로 바로가기

Java의 동기화 Synchronized 개념 정리#2

category 개발이야기/Java 2017. 11. 20. 00:30
반응형

이번엔 singleton과 static 함수에서 synchronized가 어떻게 동작하는지 확인해 보겠습니다.

Singleton 객체에서의 동기화

singleton은 객체를 한개만 생성하여 사용하도록 합니다.

따라서 #1번 글에서 사용했던것과 예제나 개념은 크게 다르지 않습니다.
public class Singleton {

	public ArrayList<Integer> mList = new ArrayList<>();

	private Singleton() {
		//
	}

	private static class Holder {
		public static final Singleton sHolderSingleton = new Singleton();
	}

	public static Singleton getInstance() {
		return Holder.sHolderSingleton;
	}

	public static void main(String[] agrs) throws InterruptedException {

		System.out.println("Test start!");
		Thread t1 = new Thread(() -> {
			for (int i = 0; i < 10000; i++) {
				Singleton.getInstance().add(i);
			}
		});

		Thread t2 = new Thread(() -> {
			for (int i = 0; i < 10000; i++) {
				Singleton.getInstance().add(i);
			}
		});

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

		t1.join();
		t2.join();

		System.out.println(Singleton.getInstance().mList.size());
		System.out.println("Test end!");

	}

	public synchronized void add(int val) {
		if (mList.contains(val) == false) {
			mList.add(val);
		}
	}
}

#1번에서 사용했던 예제중에 객체를 생성하는 부분만 singleton으로 바꿨습니다.

1. holder pattern으로 singleton 객체를 생성하도록 합니다. (singleton 객체 생성에 동기화 이슈를 해결)

2. 각 thread에서 getInstance()를 통하여 객체를 얻고 10000번 add() 함수를 부릅니다.

3. add함수는 synchronized가 되어있으므로 this를 lock으로 사용하게 됩니다.


이전 "Java의 동기화 Synchronized 개념 정리#1" 글을 충분히 이해하고 오셨다면, 사실 singleton이라고 해서 다르지 않다는걸 알것 같습니다.

따라서 singleton 객체는 multi-thread를 사용하는 환경에서는 그닥 좋은 패턴은 아닙니다.

singleton에 synchronized 함수가 많을수록 multi-thread는 병목현상을 겪게 되므로 애껏, multi-thread로 만들어 놓고 singleton을 만나면서 single thread처럼 동작하게 되는것이지요.

마지 8차선 고속도로를 뚫어놓고, 톨게이트는 한개만 만들어 놓는꼴입니다.

어차피 톨게이트에선 차 한대씩만 통과하면서 지연이 생기겠지요.


따라서 multi-thread환경에서는 공유해야 할 객체가 아니라면, new로 객체를 새로 생성하는것을 추천합니다.

필요할때마다 new 생성해서 쓰고 버리면 됩니다.

버리는건 GC가 알아서 해주니까요.

그나마 싱글톤으로 동기화 처리를 잘 만드려면 synchronized block으로 object를 따로 사용하여 lock이 걸려야하는 부분을 잘 그룹핑 해줘야 합니다.

그래도..new 로 생성해서 쓰는게 더 좋습니다!!


사실 singleton은 생성할때 multi-thread로 인한 문제가 더 많이 발생합니다.

따라서 multi-thread에서 생성시에 문제가 생기지 않도록 생성방법을 선택해야 합니다.

생성방법에 대한 정리는 여기서 확인 하시면 됩니다.

http://blog.naver.com/pionio2/220788958222


static 함수의 synchronization

앞에서는 계속 객체를 대상으로 lock 걸린다고 설명했습니다.

그럼 객체없이 호출하는 static 함수는 synchronized keyword 사용시 어떻게 동작 할까요?

public class StaticFunction {

	private static String HERO;

	public static void main(String[] agrs) {		
		System.out.println("Test start!");
		new Thread(() -> {
			for (int i = 0; i < 1000000; i++) {
				StaticFunction.batman();
			}
		}).start();

		new Thread(() -> {
			for (int i = 0; i < 1000000; i++) {
				StaticFunction.superman();
			}
		}).start();
	}

	public static synchronized void batman() {
		HERO = "batman";

		try {
			long sleep = (long) (Math.random() * 100);
			Thread.sleep(sleep);
			if ("batman".equals(HERO) == false) {
				System.out.println("synchronization broken - batman");
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

	public static synchronized void superman() {
		HERO = "superman";

		try {
			long sleep = (long) (Math.random() * 100);
			Thread.sleep(sleep);
			if ("superman".equals(HERO) == false) {
				System.out.println("synchronization broken - superman");
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}

#1번 글에서 사용했던 hero 예제 입니다.

1. static synchronized 함수가 두개인 Singleton 클래스를 만듭니다.

2. 두개의 thread가 static synchronized 함수인 batman() 과 superman()을 각각 백만번 호출 합니다.

3. batman(), superman() 함수는 "HERO"란 static 변수에 각각 다른 값을 세팅하고, 랜덤하게 sleep한 후에 값이 변했는지 체크합니다.

4. 값이 변했다면 "synchronization broken" 메시지를 찍습니다.


이 예제는 #1글에서 객체를 생성해서 사용했던것과 마찮가지로 절대 "synchronized broken...."로그가 찍히지 않습니다.

즉 static 함수라도 함수간 동기화가 잘 지켜 집니다.

static 함수의 경우 해당 class에 lock을 겁니다.

따라서 static synchronized 함수간 lock이 공유되면서 동시에 호출되는것을 막습니다.


그럼 여기에 일반 sycnhronized함수를 추가하면 어떻게 될까요??


static synchronized 함수와 synchronized 함수의 혼용

위 예제에 ironman() 함수를 추가해 보겠습니다.
synchronized 함수이긴 하지만 static 함수가 아니라서 객체를 생성해서 써야 합니다.
public class StaticFunction {

	private static String HERO;

	public static void main(String[] agrs) {		
		System.out.println("Test start!");
		new Thread(() -> {
			for (int i = 0; i < 1000000; i++) {
				StaticFunction.batman();
			}
		}).start();

		new Thread(() -> {
			for (int i = 0; i < 1000000; i++) {
				StaticFunction.superman();
			}
		}).start();
		
		StaticFunction sfunction = new StaticFunction();
		new Thread(() -> {
			for (int i = 0; i < 1000000; i++) {
				sfunction.ironman();
			}
		}).start();
		System.out.println("Test end!");

	}

	public static synchronized void batman() {
		HERO = "batman";

		try {
			long sleep = (long) (Math.random() * 100);
			Thread.sleep(sleep);
			if ("batman".equals(HERO) == false) {
				System.out.println("synchronization broken - batman");
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

	public static synchronized void superman() {
		HERO = "superman";

		try {
			long sleep = (long) (Math.random() * 100);
			Thread.sleep(sleep);
			if ("superman".equals(HERO) == false) {
				System.out.println("synchronization broken - superman");
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
	
	public synchronized void ironman() {
		HERO = "ironman";

		try {
			long sleep = (long) (Math.random() * 100);
			Thread.sleep(sleep);
			if ("ironman".equals(HERO) == false) {
				System.out.println("synchronization broken - ironman");
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}


위 코드는 일반 synchronized 함수인 ironman()을 추가하고 해당 함수를 사용하는 thread를 하나더 추가했습니다.

즉 3개의 스레드가 백만번씩 돌면서 각 함수를 호출 합니다.

1번 쓰레드 -> static synchronized batman() 호출 (백만번)

2번 쓰레드 -> static synchronized superman() 호출 (백만번)

3번 쓰레드 -> synchronized ironman() 호출 (백만번)


결과가 어떻게 나올지 예상이 되시나요?

정답은 아래와 같습니다.


미친듯이 로그를 찍어댑니다.

batman()superman()은 class를 lock으로 사용하지만 ironman()은 생성된 객체를 기준으로 lock을 잡기 때문에 서로 따로 놀게 됩니다.

따라서 ironman()이 "HERO" 변수를 계속 바꿔치기 하기때문에 전부 lock이 틀어지는거죠.


결론적으로 이렇게 혼용해서 쓴다면 이유도 모르고 간헐적으로 동기화 이슈가 발생합니다.

더군다나 실제 코드에선 코드라인이 길어지면서 호출하는 스레드가 제각각이기 때문에 찾아내기도 힘듭니다.

못고치는 이슈가 될 가능성이 크죠.ㅠ.ㅠ


누누히 강조하지만 multi-thread 환경에서는 new로 객체를 생성해서 쓰는게 좋습니다.

그게 아니라면 synchronized 이외에도 많은 동기화 기법을 자바에서 지원하고 있으니 잘 활용하면 됩니다.

(동기화를 지원하는 자료구조라든가, Atomic이라든가, java8의 stream이나 completableFuture를 이용해서 low level의 동기화는 지양하는것도 좋은 방법입니다.


반응형