synchronized
키워드는 멀티 쓰레드 환경에서 두개 이상의 쓰레드가 하나의 변수에 동시에 접근을 할 때 Race condition(경쟁상태)이 발생하지 않도록 합니다.
만약 Race condition이 발생할 수 있는 code block을 synchronized 키워드로 감싸면, 하나의 쓰레드만 이 code block에 진입할 수 있습니다. 그 외에 다른 쓰레드는 먼저 진입한 쓰레드가 이 code block을 나갈 때 까지 기다리도록 하여 Race condition이 발생하지 않도록 합니다.
이 글은 synchronized를 사용하는 방법과 효율적으로 사용하는 방법에 대해서 알아봅니다.
synchronized 키워드를 사용하는 이유
다음 코드를 보시면, Thread pool을 만들어 멀티 쓰레드에서 number
변수의 숫자를 증가시키고 있습니다.
public class SynchronizedKeywordExample {
static long number = 0;
public static void main(String args[]) {
ExecutorService service = Executors.newCachedThreadPool();
for (int i = 0; i < 1000; i++) {
service.submit(() -> {
number++;
System.out.println(number);
});
}
}
}
위 코드를 실행해보면 다음과 같이 출력이 됩니다. 처음에는 순차적으로 숫자가 증가하지만, 나중에는 순차적으로 증가하고 있지 않습니다. 사실 Java 코드 한 줄은 여러 줄의 기계어로 변환되기 때문에, 멀티 쓰레드 환경에서 순차적으로 코드가 실행된다는 것을 보장할 수 없습니다.
1
2
3
4
....
15
16
17
19
20
18
21
....
위의 코드에서 Race condition이 발생할 수 있는 부분을 synchronized 키워드로 감싸보겠습니다. 멀티쓰레드에서 동시에 접근이 가능한 변수는 number입니다. 이것을 synchronized block으로 감싸면 됩니다.
public class SynchronizedKeywordExample {
static long number = 0;
public static void main(String args[]) {
ExecutorService service = Executors.newCachedThreadPool();
for (int i = 0; i < 1000; i++) {
service.submit(() -> {
synchronized (SynchronizedKeywordExample.class) {
number++;
System.out.println(number);
}
});
}
}
}
이제 위의 코드를 실행해보면, 다음과 같이 순차적으로 숫자가 증가하는 것을 볼 수 있습니다.
synchronized block
에는 하나의 쓰레드만 진입할 수 있기 때문에, 하나의 쓰레드가 값을 변경한 이후에 다음 쓰레드가 값을 변경할 수 있습니다.
이렇게 Race condition이 발생하지 않도록 하여 의도한 대로 동작하게 만들 수 있었습니다.
1
2
3
4
.....
14
15
16
17
18
19
20
.....
일반적으로 synchronized 키워드를 사용하여, 다음과 같은 패턴으로 동기화할 수 있습니다.
- 인스턴스 메소드 동기화
- 스태틱 메소드 동기화
- 메소드 안에서 동기화
인스턴스 메소드 동기화
synchronized 키워드를 사용하여 인스턴스 단위로 동기화를 할 수 있습니다.
다음과 같이 Counter 클래스를 만들었고, 멀티 쓰레드에서 increase()
를 호출할 것입니다.
public class Counter {
private long number = 0;
public void increase() {
number++;
System.out.println(number);
}
}
만약 아래와 같이 멀티쓰레드에서 increase()
를 호출하도록 만들면 Race condition이 발생하여 문제가 될 수 있습니다.
ExecutorService service = Executors.newCachedThreadPool();
Counter counter = new Counter();
for (int i = 0; i < 1000; i++) {
service.submit(() -> {
counter.increase();
});
}
다음과 같이 메소드에 synchronized 키워드를 추가하면 메소드 단위로 동기화를 적용할 수 있습니다. 즉, 1개의 쓰레드만 이 메소드에 진입할 수 있습니다. 다른 쓰레드들은 먼저 진입한 쓰레드에서 메소드 호출이 종료될 때까지 대기하게 됩니다.
public class Counter {
private long number = 0;
public synchronized void increase() {
number++;
System.out.println(number);
}
}
주의할 점은 synchronized는 특정 메소드 단위로 동기화되는 것이 아니라, 그 객체 단위로 동기화가 되는 것입니다.
예를 들어, 아래 코드처럼 AAA라는 클래스의 increase()
와 decrease()
에 synchronized를 추가하였을 때 이 두개의 메소드는 동시에 수행될 수 없습니다.
메소드에 설정되는 synchronized는 인스턴스 단위로 동기화가 되기 때문에 이 객체 내에서 synchronized block 안으로 들어갈 수 있는 것은 1개의 쓰레드만 허용되기 때문입니다.
class AAA {
public synchronized void increase() {
}
public synchronized void decrease() {
}
}
즉, 메소드에 synchronized를 적용하는 것은 매우 비효율적일 수 있습니다.
스태틱 메소드 동기화
synchronized로 스태틱 메소드 단위로 동기화를 할 수 있습니다.
다음 코드는 static 메소드인 increase()
에 synchronized를 추가하였습니다.
public class Counter {
private static long number = 0;
public static synchronized void increase() {
number++;
System.out.println(number);
}
}
클래스 내의 static 메소드는 오직 하나만 존재합니다. 여러 객체가 생성되어도 하나의 static 메소드를 공유합니다.
따라서, 2개 이상의 Counter 객체가 생성되었고, 이 객체들이 동시에 increase()
를 호출하였을 때 1개의 쓰레드만 이 메소드에 진입할 수 있습니다.
따라서, 동일한 클래스로 만들어진 여러 객체들을 동기화할 수 있습니다.
메소드 안에서 동기화
메소드 단위로 synchronized를 적용하는 것이 비효율적일 수 있습니다. 아래 예제에서, Race condition이 발생하는 것은 number
변수 하나인데, System.out.prinln()
코드도 함께 동기화되기 때문입니다.
따라서 다음과 같이 synchronized (lock)
block으로 number에 접근하는 코드만 동기화할 수 있습니다. 여기서 lock
은 동기화되는 단위라고 생각할 수 있습니다.
this
는 인스턴스를 의미하기 때문에 객체 단위로 동기화가 됩니다.
public class Counter {
private long number = 0;
public void increase() {
long temp = 0;
synchronized (this) {
number++;
temp = number;
}
System.out.println(temp);
}
}
만약 다음과 같이 decrease()
에 synchronized가 적용되었다면, decrease()
가 수행되는 동안 increase()
내부의 synchronized block은 수행될 수 없습니다.
모두 인스턴스 객체 단위로 동기화를 하였기 때문입니다.
public class Counter {
private long number = 0;
public void increase() {
long temp = 0;
synchronized (this) {
number++;
temp = number;
}
System.out.println(temp);
}
public synchronized void decrease() {
number--;
}
}
만약 다음과 같이 lock이라는 객체를 만들고, increase 메소드의 내부 코드를 synchronized (lock)
으로 변경한다면 decrease()
와 함께 수행될 수 있습니다.
decrease()
는 인스턴스를 단위로 동기화하였고, increase()
는 lock이라는 객체를 단위로 동기화하였기 때문입니다.
public class Counter {
private long number = 0;
private Object lock = new Object();
public void increase() {
long temp = 0;
synchronized (lock) {
number++;
temp = number;
}
System.out.println(temp);
}
public synchronized void decrease() {
number--;
}
}
위의 예제에서 본 것처럼, Race condition이 발생할 수 있는 객체들을 동기화할 때 동기화되는 단위를 다르게 설정하면 효율적으로 동작할 수 있습니다.
정리
synchronized 키워드는 Race condition이 발생할 수 있는 코드를 동기화하여 1개의 쓰레드만 코드를 수행할 수 있도록 보장하는 것입니다. synchronized를 적용할 때, 동기화되는 단위를 어떤 것으로 설정하냐에 따라서 프로그램이 효율적으로 동작할 수도 있고 비효율적으로 동작할 수도 있습니다.
Related Posts
- Java - Unsupported class file major version 61 에러
- Java - String.matches()로 문자열 패턴 확인 및 다양한 예제 소개
- Java - 문자열 공백제거 (trim, replace)
- Java - replace()와 replaceAll()의 차이점
- Java - ArrayList 초기화, 4가지 방법
- Java - 배열 정렬(Sorting) (오름차순, 내림차순)
- Java - 문자열(String)을 비교하는 방법 (==, equals, compare)
- Java - StringBuilder 사용 방법, 예제
- Java - 로그 출력, 파일 저장 방법 (Logger 라이브러리)
- Java IllegalArgumentException 의미, 발생 이유
- Java - NullPointerException 원인, 해결 방법
- Seleninum의 ConnectionFailedException: Unable to establish websocket connection 해결
- Java - compareTo(), 객체 크기 비교
- Java - BufferedWriter로 파일 쓰기
- Java - BufferedReader로 파일 읽기
- Java charAt() 함수 알아보기
- Java - BigInteger 범위, 비교, 연산, 형변환
- Java contains()로 문자(대소문자 X) 포함 확인
- Java - Set(HashSet)를 배열로 변환
- Java - 문자열 첫번째 문자, 마지막 문자 확인
- Java - 문자열 한글자씩 자르기
- Java - 문자열 단어 개수 가져오기
- Java - 1초마다 반복 실행
- Java - 배열을 Set(HashSet)로 변환
- Java - 여러 Set(HashSet) 합치기
- Java - 명령행 인자 입력 받기
- Java - 리스트 역순으로 순회, 3가지 방법
- Java - 특정 조건으로 리스트 필터링, 3가지 방법
- Java - HashMap 모든 요소들의 합계, 평균 계산
- Java - 특정 조건으로 HashMap 필터링
- Java - 싱글톤(Singleton) 패턴 구현
- Java - 숫자 왼쪽에 0으로 채우기
- Java - String 배열 초기화 방법
- Java - 정렬된 순서로 Map(HashMap) 순회
- Java - HashMap에서 key, value 가져오기