자바에서 우리는 synchronized 라는 키워드를 쉽게 만날 수 있다.
이러한 키워드를 왜 사용하며 어떤 방식으로 사용하면 좋을 지에 대한 것을 정리해보겠다.
이 내용의 일부 자바 성능 튜닝이라는 책을 참고하여 작성하였다.
이번에는 사용법에 대한 내용을 정리하겠지만, 나중에는 성능에 대한 것도 정리 예정이다.
synchronized 란?
동시에 어떤 작업에 접근을 하게 되었을 때 접근을 제어하는 역할을 하는 키워드 이다.
예를 들어 진행을 해보겠다.
- 하나의 기부를 받는 단체 클래스가 있다고 해보자. 이러한 단체에 동시에 여러 명의 사람(기부를 하는 사람) 이 기부를 하게 되었을 때 어떤 결과가 나오게 될까?
- 기부 단체 클래스
//기분 단체 클래스
public class Contribution {
private int amount = 0;
public void donate(){
amount++;
}
public int getTotal(){
return amount;
}
}
- 기부 하는 사람 클래스
public class Contributor extends Thread {
private Contribution myContribution;
private String myName;
public Contributor(Contribution myContribution, String myName) {
this.myContribution = myContribution;
this.myName = myName;
}
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
myContribution.donate();
}
System.out.format(" %s total = %d\n" , myName,myContribution.getTotal());
}
}
- 테스트 클래스
public class ContributeTest {
public static void main(String[] args) {
Contributor[] contributors = new Contributor[10];
Contribution contribution = new Contribution();
for (int i = 0; i < 10; i++) {
contributors[i] = new Contributor(contribution, " Contributor" + i);
}
for(int loop= 0 ; loop <10 ; loop++){
contributors[loop].start();
}
}
}
결과 코드
Contributor2 total = 2181
Contributor3 total = 3181
Contributor1 total = 2181
Contributor0 total = 2181
Contributor4 total = 4181
Contributor5 total = 5181
Contributor6 total = 6181
Contributor7 total = 7181
Contributor8 total = 8181
Contributor9 total = 9181
- 결과를 보면 알겠지만, 분명 우리는 10명의 사람이 동시에 1개의 단체에 동시에 접근을 해서 기부를 하게 되었다.
- 상식적으로 생각을 해보면, 알 수 있지만 한 명 당 1000원씩 10명이 1개의 단체에 기부를 한 것이기 때문에 10000원이 되는 것이 맞다고 할 수 있다.
- 하지만 우리의 결과와는 다르게 9181원이라는 결과가 나오게 되었다.
이게 무슨 일일까?
Thread 의 동시 접근
스레드란 무엇인지 부터 생각을 해보아야 한다.
- 스레드는 하나의 프로세스(프로그램에) 여러 개의 스레드를 만들고 접근할 수 있게 해주는 방법이다.
- 스레드는 다른 말로 Lightweight Process(LWP) 가벼운 프로세스라고도 한다.
- 즉, 가벼운 프로세스이며, 프로세스에서 만들어 사용하고 있는 메모리를 공유한다.
- 그래서 별개의 프로세스가 하나 씩 뜨는 것보다는 성능이나 자원 사용에 있어서 많은 도움이 된다.
다시 보면 알겠지만 기부자를 나타내는 Contributor
클래스는 Thread
로 구현이 되어있다.
이 Contributor
는 public void run()
를 실행하게 되었을 때 Contribution
동시에 접근을 할 수 있다.
public class Contributor extends Thread {
private Contribution myContribution;
private String myName;
public Contributor(Contribution myContribution, String myName) {
this.myContribution = myContribution;
this.myName = myName;
}
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
myContribution.donate();
}
System.out.format(" %s total = %d\n" , myName,myContribution.getTotal());
}
}
그럼 이게 왜 문제일까?
그냥 동시에 접근하면 하면 되는 거 아닌가? 라고 생각할 수 있지만 이게 문제가 될 수 있다.
//기분 단체 클래스
public class Contribution {
private int amount = 0;
public void donate(){
amount++;
}
public int getTotal(){
return amount;
}
}
사실 amount++
연산은 사실상 세 가지 단계로 이루진다.
- 현재 값 읽기 (Read): 현재 변수의 값을 읽어온다.
- 값 증가 (Increment): 읽어온 값에 1을 더한다.
- 결과로 새로운 값을 저장 (Write): 새로운 값을 변수에 저장한다.
여러 스레드에서 동시에 donate
메서드를 호출할 때, 두 스레드가 동시에 amount++
을 수행하면 아래와 같은 문제가 발생할 수 있다.
- 두 스레드가 동시에 현재 값을 읽어와서 각각 1을 더하고, 그 결과를 저장하려고 할 때, 예상치 못한 동작이 발생할 수 있습니다.
- 두 스레드가 동시에 읽어온 값이 동일하면, 실제로 증가된 값은 1이 아니라 2가 되어야 하지만, 이러한 예상치 못한 결과가 발생할 수 있다.
- 이게 무슨 말이나면 동시에 접근하게 되었을 때 두 스레드가 동시에 3을 읽었다고 해보자.
각자 읽어 온 값이 3이기 때문에 4로 증가 시키고 저장을 하면 사실 5가 되어야 하는 값이 1만 증가해서 4 라는 숫자를 저장하기 때문에 동시성에 대한 문제가 생긴다는 것이다.
이러한 이유로 여러 스레드에서 동시에 값을 변경하는 경우, 즉각적으로 읽고 쓰는 연산에 대해서는 동기화를 사용하는 것이 좋다.
따라서 위의 문제를 해결하기 위해서 다시 코드를 작성해 보겠다.
예시 코드 수정 후 완성
- 기부자 코드
public class Contributor extends Thread {
private Contribution myContribution;
private String myName;
public Contributor(Contribution myContribution, String myName) {
this.myContribution = myContribution;
this.myName = myName;
}
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
myContribution.donate();
}
System.out.format(" %s total = %d\n" , myName,myContribution.getTotal());
}
}
- 기부 단체 코드
synchronized
키워드를 붙여 주었다.
public class Contribution {
private int amount = 0;
public synchronized void donate(){
amount++;
}
public int getTotal(){
return amount;
}
}
- 테스트 코드
```java
public class ContributeTest {
public static void main(String[] args) {
Contributor[] contributors = new Contributor[10];
Contribution contribution = new Contribution();
for (int i = 0; i < 10; i++) {
contributors[i] = new Contributor(contribution, " Contributor" + i);
}
for(int loop= 0 ; loop <10 ; loop++){
contributors[loop].run();
}
}
}
- 결과를 실행해본 결과 10000원 이라는 결과를 나타내었다.
- 사실 여기서 Thread 에 대한 동작 방식, 성능을 더 공부하면 좋지만 여기서는 다루지 않겠다.
Contributor0 total = 2687
Contributor2 total = 6587
Contributor1 total = 6886
Contributor4 total = 6907
Contributor7 total = 8787
Contributor3 total = 8963
Contributor8 total = 9107
Contributor5 total = 9519
Contributor6 total = 9759
Contributor9 total = 10000
이렇게 synchronized
를 사용해 동시성 문제를 해결하는 것을 알아보았다.
끝
추가 문법 정리
동기화된 메서드: 메서드를 동기화하려면 메서드 선언에 synchronized
키워드를 추가합니다. 스레드가 동기화된 메서드에 진입하면 해당 메서드의 객체에 연결된 락을 얻어 다른 스레드가 동일한 객체의 어떤 동기화된 메서드에도 진입하지 못하게 합니다.
public synchronized void synchronizedMethod() {
// 메서드 내용 }
동기화된 블록: 메서드 내에서 동기화된 블록을 사용하여 특정 코드 섹션을 보호할 수 있습니다. 이 방법은 전체 메서드가 아닌 메서드 일부만 동기화하려는 경우 유용합니다. 동기화된 블록을 만들려면 락으로 사용할 객체를 지정하고 한 번에 한 스레드만 락을 보유할 수 있습니다.
public void 어떤메서드() {
// 비동기화된 코드
synchronized (락객체) {
// 동기화된 코드 블록
}
}
'Java' 카테고리의 다른 글
[JAVA] JAVA 네이티브 언어와 통신하기 위한 JNI 개념 (0) | 2024.08.11 |
---|---|
Java - array, arrayList 에서 크기 가변의 내부 동작 (0) | 2023.12.09 |
Java - LinkedList (feat. vs ArrayList) (0) | 2023.12.03 |
interface기반 구현(프로그래밍) 이란? (0) | 2023.11.29 |
String, StringBuilder, StringBuffer (0) | 2023.11.20 |