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 |