본문 바로가기

Java

15 - 4 장 쓰레드의 동기화

1. 동기화의 개념

 

 동기화 (synchoronized)

  - 하나의 작업이 완전히 완료된 후 다른 작업을 수행하는 것

  - 한 쓰레드가 객체를 모두 사용해야 다음 쓰레드가 사용할 수 있도록 설정

  - 한 객체를 두 쓰레드가 동시에 사용할 수 없도록 설정

 

 비동기 (asynchronous)

 - 하나의 작업 명령 이후 완료 여부와 상관없이 바로 다른 작업 명령을 수행하는 것

 

 2. 동기화의 필요성 

 

 - 2개의 쓰레드가 하나의 객체를 공유할 때 동기화의 필요성 

 

- 객체 내부의 data 필드에 3의 값을 저장하고 있는 MyData 객체 1개 

- MyData 객체를 동일한 작업을 수행하는 2개의 plusThread 쓰레드가 공유 

- 2개의 쓰레드가 동시에 MyData객체 내의 데이터값을 1씩 증가

  => 결과값 5? no 

 

- 1개의 쓰레드에서의 값을 증가시키는 동작

 step 1. MyData  객체 내의 data 필드값을 읽어 CPU에 전달

 step 2. CPU는 이 값을 1 증가 

 step 3. 연산 결과를 다시 MyData 객체의 data 필드값에 저장 

 

- 만일 왼쪽 쓰레드의 step 3보다 오른쪽 쓰레드의 step1이 먼저 일어나면 결과는 5가 아닌 4가 나옴

 

- 동기화를 사용하지 않았을 때 문제 발생

package Synchronized;

//공유 객체
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(); //객체 내부의 plusData() 호출
		System.out.println(getName() +"실행 결과:"+myData.data);
	}
}

public class Synchronized {
	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) {} //1초 기다림
		
		//plusThread2
		Thread plusThread2 = new PlusThread(myData);
		plusThread2.setName("plusThread2");
		plusThread2.start();
	}
}

 plusData() 메서드 

 - data 필드를 가져와 2초 후에 값을 1만큼 증가 

 

 PlusThread 쓰레드 

 - 생성자의 매개변수로 MyData 객체를 입력받아 객체 내부의 plusData() 메서드를 호출함으로써 자신의 이름과 MyData 객체의 data 필드값을 출력

 

main() 메서드에서 2개의 PlusThread 객체를 생성하고, 각각의 이름을 지정한 후 1초 간격으로 실행

 

-2개의 쓰레드가 각각 MyData 객체의 data 필드값을 1씩 증가시켰는데도 두 쓰레드 모두 4의 결과값을 가짐 

 -> 두번째 쓰레드가 data 필드를 증가시키는 시점에 아직 첫 번째 쓰레드의 실행이 끝나지 않았기 때문 

- 동기화가 적용됐을 때 각 쓰레드의 단계별 실행 순서

 

 - 하나의 쓰레드가 MyData 객체 내의 data 필드값을 완전히 증가시키고 난 후 다음 쓰레드가 동일한 작업을 수행

 -> data 필드값은 5의 결과값을 가짐

 

3. 동기화 방법

 

1) 메서드 동기화 

 

 - 2개의 쓰레드가 동시에 메서드를 실행할 수 없는 것

 - 메서드의 리턴 타입 앞에 synchronized 키워드 

접근 지정자 synchronized 리턴 타입 메서드명 (입력매개변수) {
	//동기화가 필요한 코드 
}
class MyData {
	int data = 3;
    public synchronized void plusData() {
    	//data 필드의 값을 +1 수행
     }
}

 - 동시에 2개의 쓰레드에서 해당 메서드를 실행할 수 없게 됨 

 - MyData 객체 내의 data 필드를 1 증가시키는 메서드인 plusData() 메서드를 동기화 시키면 하나의 쓰레드가 +1 연산을 완전히 종료한 후에만 다른 쓰레드가 이 메서드를 실행시킬 수 있음

 

package Synchronized;

//공유 객체
class MyData {
	int data =3;
	
	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(); //객체 내부의 plusData() 호출
		System.out.println(getName() +"실행 결과:"+myData.data);
	}
}

public class Synchronized {
	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) {} //1초 기다림
		
		//plusThread2
		Thread plusThread2 = new PlusThread(myData);
		plusThread2.setName("plusThread2");
		plusThread2.start();
	}
}

 

2) 블록 동기화 

 

 -2개의 쓰레드가 동시에 해당 블록을 실행할 수 없는 것 

 - 동기화 영역은 꼭 필요한 부분에 한정해 적용하는 것이 좋음

 - 메서드 전체 중에 동기화가 필요한 부분이 일부라면 굳이 전체 메서드를 동기화할 필요 없이 해당 부분만 동기화 가능

 

synchronized (임의의 객체) {
	//동기화가 필요한 코드
}

 - 임의의 객체 

  -> Key를 가진 객체 

  ->모든 객체는 저마다의 Key 하나를 갖고 있음

  -> 일반적으로 클래스 내부에서 바로 사용할 수 있는 객체인 this를 사용 

class MyData {
	int data = 3;
    public void plusData() {
    	synchronized(this) {
        	//data 필드의 값을 +1 수행
         }
    }
}
package 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(); //객체 내부의 plusData() 호출
		System.out.println(getName() +"실행 결과:"+myData.data);
	}
}

public class Synchronized {
	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) {} //1초 기다림
		
		//plusThread2
		Thread plusThread2 = new PlusThread(myData);
		plusThread2.setName("plusThread2");
		plusThread2.start();
	}
}

 

4. 동기화의 원리 

 

- 모든 객체는 자신만의 열쇠(key)를 하나씩 갖고 있다.

- 동기화를 사용하면 처음 사용하는 쓰레드가 key 객체의 열쇠를 가짐

- key 객체 

  동기화 메서드 => 자기 자신의 객체 (this)

  동기화 블록 => synchronized(key 객체){}에서 사용한 key 객체 

- 다른 쓰레드는 먼저 사용 중인 쓰레드가 작업을 완료하고 열쇠를 반납할 때 까지 대기 (blocked)

 

ex)

class MyData {
	Object keyObject = new Object();
    synchronized void abc() {
    	//동기화 메서드 
    }
    synchronized void bcd() {
    	//동기화 메서드 
    }
    void cde() {
    	synchronized(this) {
        	//동기화 블록
        }
     }
     void def() {
     	synchronized(keyObject) [
        	//동기화 블록
        }
    }
    void efg() {
    	synchronized(keyObject) {
        	//동기화 블록
         }
    }
 }

 - Object 타입의 KeyObject 객체는 오직 열쇠의 역할을 하기 위해 생성한 객체 

 - abc(), bcd(), cde()는 모두 this 객체의 열쇠를 사용하기 때문에 이들 중 1개의 메서드가 실행되는 도중에는 다른 어떤 메서드도 동시에 실행할 수 없다.

 - def(), efg() 메서드는 abc(),bcd(),cde()와 다른 열쇠를 사용하기 때문에, 이들 중 하나랑 동시에 실행 가능

 - def()와 efg()도 동일한 열쇠를 사용하기 때문에 이들 두 메서드를 동시에 실행할 수 없다.

 

- 3개의 동기화 영역이 동일한 열쇠로 동기화됐을 때 

 -> abc(), bcd(), cde()는 this 객체가 갖고 있는 하나의 열쇠를 함께 사용 

package Synchronized;

class MyData{
	synchronized void abc() {
		for(int i=0;i<3; i++) {
			System.out.println(i+"sec");
			try {Thread.sleep(1000);} catch(InterruptedException e) {}
		}
	}
	
	synchronized void bcd() {
		for(int i =0; i<3; i++) {
			System.out.println(i+"초");
			try {Thread.sleep(1000);} catch(InterruptedException e) {}
		}
	}
	
	void cde() {
		synchronized(this) {
			for(int i =0; i<3; i++) {
				System.out.println(i+"번째");
				try {Thread.sleep(1000);} catch(InterruptedException e) {}
			}
		}
	}
}
public class Synchronized {
	public static void main(String[] args) {
		//공유 객체 
		MyData myData = new MyData();
		
		//3개의 쓰레드가 각각의 메서드 호출 
		new Thread() {
			public void run() {
				myData.abc();
			};
		}.start();
		
		new Thread() {
			public void run() {
				myData.bcd();
			};
		}.start();
		
		new Thread() {
			public void run() {
				myData.cde();
			};
		}.start();
	}
}

 

- 동기화 메서드와 동기화 블록이 다른 열쇠를 사용할 떄 

 -> abc(), bcd()는 this 객체가 갖고 있는 하나의 열쇠를 함께 사용

 -> cde()는 Object 객체가 갖고 있는 열쇠를 사용

 

package Synchronized;

class MyData{
	synchronized void abc() {
		for(int i=0;i<3; i++) {
			System.out.println(i+"sec");
			try {Thread.sleep(1000);} catch(InterruptedException e) {}
		}
	}
	
	synchronized void bcd() {
		for(int i =0; i<3; i++) {
			System.out.println(i+"초");
			try {Thread.sleep(1000);} catch(InterruptedException e) {}
		}
	}
	
	void cde() {
		synchronized(new Object()) {
			for(int i=0; i<3; i++) {
				System.out.println(i+"번째");
				try {Thread.sleep(1000);} catch(InterruptedException e) {}
			}
		}
	}
}
public class Synchronized {
	public static void main(String[] args) {
		//공유 객체 
		MyData myData = new MyData();
		
		//3개의 쓰레드가 각각의 메서드 호출 
		new Thread() {
			public void run() {
				myData.abc();
			};
		}.start();
		
		new Thread() {
			public void run() {
				myData.bcd();
			};
		}.start();
		
		new Thread() {
			public void run() {
				myData.cde();
			};
		}.start();
	}
}

'Java' 카테고리의 다른 글

15 - 3장 쓰레드의 속성  (0) 2023.06.14
15 -2장 쓰레드의 생성 및 실행  (0) 2023.06.14
15 - 1장 프로그램, 프로세스, 쓰레드  (0) 2023.06.13
14 - 4장 사용자 정의 예외 클래스  (0) 2023.06.13
14 - 3장 예외 전가  (0) 2023.06.13