동기화: 하나의 작업이 완료된 후 다른 작업을 수행하는 것
비동기: 하나의 작업 명령 이후 완료 여부와 관계없이 바로 다른 작업 명령을 수행하는 것.
동기화의 필요성
멀티쓰레드를 사용할 때 동기화는 필요하다. 이유를 알아보자.
왼쪽의 step3도중 오른쪽 step1이 먼저 발생하면 결과는 5가 아닌 4가 나올 것이다.
동기화를 사용하지 않았을 때 문제가 발생된다.
class MyData{
int data = 3;
public void plusData(){
int mydata = data;
try{Thread.sleep(2000);} catch (InterruptedException e){}
data = mydata + 1;
}
}
class PlusThread extends Thread{
MyData myData;
public PlusThread(MyData myData){
this.myData = myData;
}
@Override
public void run(){
myData.plusData();
System.out.println(getName()+"실행결과:"+ myData.data);
}
}
public class TheNeedForSynchronized {
public static void main(String[] args) {
// 공유 객체 생성
MyData myData = new MyData();
// plusThread1
Thread plusThread1 = new PlusThread(myData);
plusThread1.setName("plusThread1");
plusThread1.start();
try{Thread.sleep(1000);} catch (InterruptedException e){}
// plusThread2
Thread plusThread2 = new PlusThread(myData);
plusThread2.setName("plusThread2");
plusThread2.start();
}
}
결과를 보면 예상값 5가 아닌 4가 출력됨을 확인 할 수 있다.
plusThread1실행결과:4
plusThread2실행결과:4
이러한 이유는 두번째 쓰레드가 data필드를 증가시키는 시점에 아직 첫번째 쓰레드 실행이 끝나지 않았기 때문이다.
사용자가 원하는 로직은 다음과 같다.
위의 그림처럼 하나의 쓰레드가 MyData객체 내의 data 필드값을 증가시키는 연산을 완전히 끝내고 난 뒤에 다음 쓰레드가 동일한 작업을 수행하면 data의 필드값은 5의 결과를 가질 것이다.
위와 같이 하나의 쓰레드가 객체를 모두 사용 후 다음 쓰레드가 사용할 수 있도록 설정하는 것을 '동기화' 라고 한다.
동기화 방법
자바에서 동기화 방법은 크게 메서드 동기화, 블록 동기화 로 나눌 수 있다.
메서드 동기화는 2개이상의 쓰레드가 동시에 메서드를 실행할 수 없으며, 블록 동기화도 마찬가지로 동시에 해당 블록을 실행할 수 없다.
메서드 동기화
메서드를 동기화할 때는 도익화하고자 하는 메서드의 리턴 타입 앞에 synchronized 키워드만 넣으면 된다.
접근지정자 synchronized 리턴타입 메서드명(입력매개변수) {
// 동기화가 필요한 코드
}
이렇게 되면 동시에 2개이상의 쓰레드에서 해당 메서드를 실행할 수 없다.
예제 코드를 보면 plusData()메서드 앞에 synchronized 키워드를 추가하였다.
class MyData{
int data = 3;
//synchronized 추가
public synchronized void plusData(){
int mydata = data;
try{Thread.sleep(2000);} catch (InterruptedException e){}
data = mydata + 1;
}
}
class PlusThread extends Thread{
MyData myData;
public PlusThread(MyData myData){
this.myData = myData;
}
@Override
public void run(){
myData.plusData();
System.out.println(getName()+"실행결과:"+ myData.data);
}
}
public class SynchronizedMethod {
public static void main(String[] args) {
// 공유 객체 생성
MyData myData = new MyData();
Thread plusThread1 = new PlusThread(myData);
plusThread1.setName("plusThread1");
plusThread1.start();
try{Thread.sleep(1000);} catch (InterruptedException e){}
Thread plusThread2 = new PlusThread(myData);
plusThread2.setName("plusThread2");
plusThread2.start();
}
}
결과
plusThread1실행결과:4
plusThread2실행결과:5
블록 동기화
메서드 중 일부분만 동기화가 필요한 부분만 있다면 전체 메서드를 동기화를 할 필요가 없다.
성능 면에서 많은 손해를 보기 때문이다.
해당 부분만 동기화 할 수 있는데 이를 '블록 동기화' 라고 한다.
synchronized (임의의 객체){
//동기화가 필요한 코드
}
블록동기화를 사용해 앞의 예제를 수정하였다.
class MyData{
int data = 3;
public void plusData(){
synchronized(this) {
int mydata = data;
try {Thread.sleep(2000);} catch (InterruptedException e) {}
data = mydata + 1;
}
}
}
class PlusThread extends Thread{
MyData myData;
public PlusThread(MyData myData){
this.myData = myData;
}
@Override
public void run(){
myData.plusData();
System.out.println(getName()+"실행결과:"+ myData.data);
}
}
public class SynchronizedBlock {
public static void main(String[] args) {
// 공유 객체 생성
MyData myData = new MyData();
// plusThread1
Thread plusThread1 = new PlusThread(myData);
plusThread1.setName("plusThread1");
plusThread1.start();
try{Thread.sleep(1000);} catch (InterruptedException e){}
Thread plusThread2 = new PlusThread(myData);
plusThread2.setName("plusThread2");
plusThread2.start();
}
}
결과
plusThread1실행결과:4
plusThread2실행결과:5
메서드, 블록 동기화는 메서드 내의 코드가 길고, 연산 과정이 다수 포함되어있다면 메서드 전체를 동기화하는 것과 일부 코드만 동기화 하는것 사이에 성능 차이가 존재한다.
동기화의 원리
- 모든 객체는 자신만의 key값을 가지고 있다.
- 블록 동기화 코드에서 (synchronized(this{...}) 블록이 this객체가 가진 key로 잠긴다.
- 실행하는 쓰레드가 그 key를 갖게 된다
- 해당 쓰레드가 동기화 블록의 실행을 완료하기 전까지 다른 열쇠는 key를 얻을 수 없어 해당 블록을 실행할 수 없다.
- 열쇠는 모든 객체가 소유하지만 메서드를 동기화 하는 경우에 this객체의 key를 사용한다.
- 하나의 객체 내부에 여러 개의 동기화 메서드가 있다면 이 메서드 모두가 this의 key로 잠겨있어 1개의 쓰레드가 이들 중 1개의 메서드를 실행하고 있다면 다른 쓰레드는 나머지 2개의 메서드도 같이 잠겨서 사용할 수 없다.
- 즉 다른 쓰레드는 먼저 사용 중인 쓰레드가 작업을 완료하고 key를 반납할 때까지 대기한다.
3개의 동기화 영역이 동일한 key로 동기화됐을 때
class MyData {
// this 객체가 갖고 있는 하나의 key를 함께 사용
synchronized void aaa() {
for(int i=0; i<3; i++) {
System.out.println(i + "sec");
try { Thread.sleep(1000); } catch (InterruptedException e) {}
}
}
// this 객체가 갖고 있는 하나의 key를 함께 사용
synchronized void bbb() {
for(int i=0; i<3; i++) {
System.out.println(i + "초");
try { Thread.sleep(1000); } catch (InterruptedException e) {}
}
}
// this 객체가 갖고 있는 하나의 key를 함께 사용
void ccc() {
synchronized(this) {
for(int i=0; i<3; i++) {
System.out.println(i + "번째");
try { Thread.sleep(1000); } catch (InterruptedException e) {}
}
}
}
}
public class KeyTest1 {
public static void main(String[] args) {
//공유객체
MyData myData = new MyData();
//세개의 쓰레드가 각각의 메서드 호출
new Thread() {
public void run() {
myData.aaa();
};
}.start();
new Thread() {
public void run() {
myData.bbb();
};
}.start();
new Thread() {
public void run() {
myData.ccc();
};
}.start();
}
}
결과
0sec
1sec
2sec
0번째
1번째
2번째
0초
1초
2초
동기화 메서드와 동기화 블록이 다른 key를 사용할 때
class MyData {
// this 객체가 갖고 있는 하나의 key를 함께 사용
synchronized void aaa() {
for(int i=0; i<3; i++) {
System.out.println(i + "sec");
try { Thread.sleep(1000); } catch (InterruptedException e) {}
}
}
// this 객체가 갖고 있는 하나의 key를 함께 사용
synchronized void bbb() {
for(int i=0; i<3; i++) {
System.out.println(i + "초");
try { Thread.sleep(1000); } catch (InterruptedException e) {}
}
}
// Object 객체가 갖고 있는 key를 사용
void ccc() {
synchronized(new Object()) {
for(int i=0; i<3; i++) {
System.out.println(i + "번째");
try { Thread.sleep(1000); } catch (InterruptedException e) {}
}
}
}
}
public class KeyObject_2 {
public static void main(String[] args) {
//공유객체
MyData myData = new MyData();
//세개의 쓰레드가 각각의 메서드 호출
new Thread() {
public void run() {
myData.aaa();
};
}.start();
new Thread() {
public void run() {
myData.bbb();
};
}.start();
new Thread() {
public void run() {
myData.ccc();
};
}.start();
}
}
결과
0번째
0sec
1번째
1sec
2번째
2sec
0초
1초
2초
'Language > Java' 카테고리의 다른 글
[Java] 제네릭(Generic) (0) | 2022.04.08 |
---|---|
[Java] 쓰레드의 상태 (0) | 2022.04.06 |
[Java] 쓰레드의 속성 (0) | 2022.04.05 |
[Java] 쓰레드, 멀티쓰레드 (0) | 2022.04.05 |
[Java] 예외 전가 (throws) (0) | 2022.04.05 |