본문 바로가기

JAVA

Java 멀티쓰레드 동기화 - (2) Synchronized

728x90

Java에는 멀티쓰레드 환경에서 동기화를 하기 위한 3가지 방법이 있다.

이번에는 synchronized 키워드에 대해 얘기해보겠습니다.

 

https://wjdtn7823.tistory.com/65

 

Java 멀티쓰레드 동기화 - (1) Volatile

Java에는 멀티쓰레드 환경에서 동기화를 하기 위한 3가지 방법이 있다. 그중 Volatile 키워드에 대해서 설명하겠습니다. 1. volatile 키워드 JAVA에서 volatile 키워드는  변수를 read 하고 write 할때 CPU cach

wjdtn7823.tistory.com

 

1. synchronized 키워드

 

synchronized는 여러개의 스레드가 객체에 접근하는 것을 제어하여 객체의 thread-safe를 가능케 하는 방식입니다. synchornized 키워드는 객체의 블록이나 또는 함수에 사용할수 있습니다.

여기서 주의할 점은 synchonized 키워드를 메소드에 붙일 경우 인스턴스에 적용된다는 점입니다. 서로 다른 객체에 대한 synchonized을 할 경우  쓰레드 동기화가 적용되지 않습니다. 만약, 인스턴스가 아니라 클래스에 대해서 lock을 걸려고 할 경우에는  synchronized(A.class){ ...} 와 같이 synchronized block을 사용해야 합니다.

간단한 예제를 통해 보여드리겠습니다.

public class SyncTest {
	int t;
	int cnt;
	public synchronized void SomeMethod() {
		System.out.printf("Before t = %d\n",t);
		System.out.printf("t = %d SomeMethod\n",t);
		try {
			Thread.sleep(100);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
		System.out.printf("Leaving SomeMethod t= %d\n",t);
	}
	
	public SyncTest(int t) {
		this.t =t ;
		// TODO Auto-generated constructor stub
	}
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		SyncTest s = new SyncTest(1);
		SyncTest q = new SyncTest(2);
		Thread t1 = new Thread(
		()->{
			for(int i =0 ;i < 1000;i++) {
				s.SomeMethod();
			}
		}
		);
		Thread t2 = new Thread(()-> {
			for(int i =0 ;i < 1000;i++) {
				q.SomeMethod();
			}
		});
		
		t1.start();
		t2.start();
	}

}

s와 q객체를 만들고 synchronized 된 SomeMethod를 실행해보지만,

1번쓰레드의 sync 함수가 종료되기 전에 2번 쓰레드가 치고 들어오는 걸 볼수 있습니다.

이와 같이 같은 객체를 가르키는 경우에만 synchonized가 원하는 대로 동기화됩니다.

같은 원리로 static 함수나 block에 synchornized를 적용할경우 JVM의 Method area에 같은 메모리를 가르키기 때문에 정상적으로 작동할 것입니다.

 

같은 객체를 가르키지만 서로 다른 synchonized 함수를 호출하는 경우는 어떻게 될까요?

서로 다른 SomeMethod 와 SomeOtherMethod에 synchonized 키워드를 붙이고 쓰레드 2개에 돌려보도록 하겠습니다.

public class SyncTest {
	int t;
	int cnt;
	public synchronized void SomeMethod(int c) {
		System.out.printf("Before c = %d t = %d\n",c,t);
		System.out.printf("t = %d SomeMethod\n",t);
		try {
			Thread.sleep(100);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
		System.out.printf("Leaving SomeMethod c = %d t= %d\n",c,t);
	}
	
	public synchronized void SomeOtherMethod(int c) {
		System.out.printf("Before SomeOtherMethod c = %d t = %d\n",c,t);
		System.out.printf("t = %d SomeOtherMethod\n",t);
		try {
			Thread.sleep(100);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
		System.out.printf("Leaving SomeOtherMethod c = %d t= %d\n",c,t);
	}
	
	public SyncTest(int t) {
		this.t =t ;
		// TODO Auto-generated constructor stub
	}
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		SyncTest s = new SyncTest(1);
		SyncTest q = new SyncTest(2);
		Thread t1 = new Thread(
		()->{
			for(int i =0 ;i < 1000;i++) {
			//	System.out.println("t1t1t1");
				s.SomeMethod(1);
			}
		}
		);
		Thread t2 = new Thread(()-> {
			for(int i =0 ;i < 1000;i++) {
				//System.out.println("t2t2t2");
				s.SomeOtherMethod(2); 
			}
		});
		
		t1.start();
		t2.start();
	}

}

예상하셨겠지만 동일하게 synchonized 됩니다. 이는 synchronized가 함수가 아니라 해당객체에 synchonized가 걸린다는 걸 생각하면 이해하기 쉬우실겁니다. 객체에 락이 걸리더라도 synchronized 키워드가 없는 함수 호출에 대해서는 영향을 주지는 않습니다.

 

2. Reentrant Lock vs Synchonized

Reentrant Lock 는 java.util.concurrent.locks 에 포함되는 클래스로

Synchonized 는 문법적으로는 동일한 의미를 가집니다. (임계구역에 락을 걸어 thread-safe를 한다는 점에서 말이죠.)

다만 synchonized는 객체가 단순히 함수가 시작될떄 락을 걸고 그 함수가 종료되면 락을 푸는 단순한 기능만 제공했다면 Reentrant Lock 같은 경우 타이머, 인트럽트 핸들러 같이 락을 풀수 있는 기능들을 제공하며 read, write 와 같이 읽기 쓰기에 대해서 따로 락을 거는 기능 등 다양한 기능들을 제공합니다. 또한 synchonized 가 여러 함수에 걸쳐 동기화가 이뤄지는 경우 구현하기 어려웠다면 lock은 이러한 문제들을 해결할수도 있습니다. 위와 똑같은 예제코드를 Reentrant Lock로 한번 작성해보겠습니다.  클래스에 ReadWriteLock을 하나 추가하고 한 함수에는 Read Lock 만 한 함수에는 WriteLock만 적용해보겠습니다. 당연히 서로 다른 락을 사용하기 떄문에 어떠한 제어도 되지 않을것입니다.

 

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReentrantTest {
	int t;
	ReadWriteLock readWriteLock;
	public void ReadMethod(int c) {
		System.out.printf("Before Lock c = %d t = %d\n",c,t);
		//readWriteLock.readLock().lock();
		System.out.printf("t = %d readMethod\n",t);
		try {
			Thread.sleep(100);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}finally {
		//	readWriteLock.readLock().unlock();
		}
		
		System.out.printf("Leaving readMethod c = %d t= %d\n",c,t);
		
	}
	
	public void WriteMethod(int c) {
		System.out.printf("Before WriteMethod c = %d t = %d\n",c,t);
		System.out.printf("t = %d WriteMethod\n",t);
		
		
			try {
				readWriteLock.writeLock().lock();
				Thread.sleep(100);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			
	//	} catch (InterruptedException e) {
			// TODO Auto-generated catch block
		//	e.printStackTrace();
		finally {
			readWriteLock.writeLock().unlock();
		}
		
		
		System.out.printf("Leaving WriteMethod c = %d t= %d\n",c,t);
	}
	public ReentrantTest(int t) {
		// TODO Auto-generated constructor stub
		this.t = t;
		
		readWriteLock = new ReentrantReadWriteLock(false); //fairneww
	}
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		ReentrantTest s = new ReentrantTest(1);
		Thread t1 = new Thread(
		()->{
			for(int i =0 ;i < 1000;i++) {
			//	System.out.println("t1t1t1");
				s.WriteMethod(i);
			}
		}
		);
		Thread t2 = new Thread(()-> {
			for(int i =0 ;i < 1000;i++) {
				//System.out.println("t2t2t2");
				s.ReadMethod(i);
			}
		});
		
		t1.start();
		t2.start();
	}

}

lock을 넘겨줄때 넘긴 fairness에 대한건 이 밑 링크를 참고하세요!

 

stackoverflow.com/questions/7962312/how-to-understand-the-non-fair-mode-of-reentrantreadwritelock

 

How to understand the "non-fair" mode of ReentrantReadWriteLock?

ReentrantReadWriteLock has a fair and non-fair(default) mode, but the document is so hard for me to understand it. How can I understand it? It's great if there is some code example to demo it. UP...

stackoverflow.com

 

 

3. 실제 synchonized가 적용된 자바 클래스들

 

StringBuffer

Vector

 

StringBuffer와 Vector의 함수를 보면 synchonized 블록들이 있다. 그렇기 때문에 둘은 Thread-safe합니다. 하지만 단일쓰레드에서 이 둘을 사용한다면 synchonized하는데 사용한 오버헤드 때문에 효율이 떨어질 것이다. 그러므로 같은 기능을 가지지만 Synchonized가 없는 StringBuilder 그리고 ArrayList를 사용하는게 좋을것이다.

 

 

'JAVA' 카테고리의 다른 글

ThreadLocal  (0) 2020.11.22
Java 멀티쓰레드 동기화 - (3) AtomicClass  (0) 2020.10.24
Java 멀티쓰레드 동기화 - (1) Volatile  (0) 2020.10.03
JPA 프록시와 연관관계 관리  (0) 2020.09.04
Spring Data JPA  (0) 2020.08.30