JAVA/HIGH JAVA

[JAVA] 스레드 - 공통 객체 / 동기화

아잠만_ 2024. 5. 1. 11:45

스레드에서 객체를 공통으로 사용하는 예제


원주율을 계산하는 스레드와 계산이 완료되면 계산된 원주율을 출력하는 스레드가 있다
원주율을 저장하는 객체가 필요하다 이 객체를 두 스레드에서 공통으로 사용한다
public class ThreadTest14 {
	public static void main(String[] args) {
		// 공통으로 사용할 객체를 생성한다
		ShareData data = new ShareData();
		
		// 스레드 객체를 생성하고 공통으로 사용할 객체를 각각의 스레드에 주입한다
		CalcPIThread cal = new CalcPIThread(data);
		
		PrintPIThread print = new PrintPIThread();
		print.setData(data);

		cal.start();
		print.start();
	}
}

// 원주율을 계산하는 스레드
class CalcPIThread extends Thread {
	private ShareData data;

	public CalcPIThread(ShareData data) {
		this.data = data;
	}

	@Override
	public void run() {
		/*
		 * 원주율 = (1/1 - 1/3 + 1/5 - 1/7 + 1/9 ... ) * 4 
		 *           1     -3    5     -7    9 
		 *  (2의 몫) 0      1    2      3    4
		 */
		double sum = 0.0;
		for (int i = 1; i < 1_000_000_000; i += 2) {
			if ((i / 2) % 2 != 0) {
				sum -= (1.0 / i);
			} else { // 2로 나눈 몫이 짝수일 때
				sum += (1.0 / i);
			}
		}
		data.result = sum * 4;
		data.isOk = true;
	}
}

// 계산된 원주율을 출력하는 스레드
class PrintPIThread extends Thread {
	private ShareData data;

	// Setter
	public void setData(ShareData data) {
		this.data = data;
	}

	@Override
	public void run() {
		while (true) {
			if (data.isOk) {
				break;
			} else {
				Thread.yield();
			}
		}
		System.out.println();
		System.out.println("결과 : " + data.result);
		System.out.println("PI : " + Math.PI);
	}
}

// 원주율을 관리하는 클래스 (공통으로 사용할 클래스)
class ShareData {
	public double result; // 계산된 원주율이 저장될 변수

	public boolean isOk = false; // 계산이 완료되었는지 여부를 나타내는 변수
}

동기화 synchronized

멀티스레드 환경에서 반드시 스레드간 동기화 문제를 해결해야함

(예시로 atm기에서 돈을 동시에 인출할 때 인출하는 과정 속에 A라는 사람이 인출을 시도하고 나서 B 사람이 인출을 시도한다면 A 인출 중간에 들어오면서 A가 인출한 결과가 적용되지 않아 잔액보다 더한 돈이 인출될 수 있음

> 이에 대한 예시는 2번째에 존재)

자바에서는 synchronized 키워드를 제공해 스레드 간 동기화를 시켜 data의 thread-safe를 가능케함

  1. 메서드에서 사용하는 경우
    public synchronized void 메소드이름(){   }
  2. 객체 변수에 사용하는 경우(block문)
    private Object 객체이름 = new Object();
    public void 메소드 이름(){ synchronized (객체이름) {     }}

public class ThreadTest15 {
	
	public static void main(String[] args) {
		ShareObject sObj = new ShareObject();
		TestThread th1 = new TestThread("Test1", sObj);
		TestThread th2 = new TestThread("Test2", sObj);
		
		th1.start();
		th2.start();
	}
	
}

class TestThread extends Thread{
	private ShareObject sObj;
	
	// 생성자
	public TestThread(String name, ShareObject sObj) {
		super(name);	// 스레드의 name 설정
		this.sObj = sObj;
	}


	@Override
	public void run() {
		for(int i=1; i<=10; i++) {
			sObj.add();
		}
	}
}

// 공통 클래스
class ShareObject{
	private int sum = 0;
	
	// 동기화하기
	// lock을 걸어 실행하는 동안 다른 스레드가 사용하지 못하게함
	
	// 단순히 합계에서 10더해주는 메서드
	// 다른 스레드에게 넘어갈 수 있는 환경을 제공하기 위해 단계를 나눠서 사용
//	public synchronized void add() { // 방법 1 : 메서드에 동기화 설정하기
	public void add() {
		synchronized (this) {	// 방법 2 : 동기화 블럭을 이용하여 설정하기
			// 동기화로 처리할 내용을 넣어주기
			
			int n = sum;
			
			n+= 10;
			
			sum = n;
			
			System.out.println(Thread.currentThread().getName() + " 합계 : "+sum);
		}
	}
}

은행의 입출금을 스레드로 처리하는 예제

public class ThreadTest16 {
	private int balance; // 잔액이 저장될 변수

	public int getBalance() {
		return balance;
	}

	public void setBalance(int balance) {
		this.balance = balance;
	}

	// 입금을 처리하는 메서드
	public void deposit(int money) {
		balance += money;
	}

	// 출금을 처리하는 메서드 (반환값 => 출금 성공 :true, 출금 실패 : false)
//	public synchronized boolean withdraw(int money) {	// 동기화 처리 유무
	public boolean withdraw(int money) {
		synchronized (this) {

			if (balance < money) {
				return false;
			} else {

				for (int i = 1; i <= 1000000000; i++) { // 브레이크 걸기(시간 소비)
					int k = i + 1;
				}

				balance -= money;
				System.out.println("메서드 안에서 balance : " + balance);
				return true;
			}
		}
	}

	public static void main(String[] args) {
		// 공통 객체 생성
		ThreadTest16 account = new ThreadTest16();
		account.setBalance(10000); // 잔액을 10000원으로 설정

		// 익명 구현체로 스레드 구현
		Runnable runTest = new Runnable() {

			@Override
			public void run() {
				boolean result = account.withdraw(6000); // 6000원 출금하기

				System.out.println(
						Thread.currentThread().getName() + "에서 result = " + result + ", 잔액 = " + account.balance);
			}
		};

		Thread th1 = new Thread(runTest);
		Thread th2 = new Thread(runTest);

		th1.start();
		th2.start();

		// 동기화 처리하지 않은 결과
//		메서드 안에서 balance : -2000
//		메서드 안에서 balance : -2000
//		Thread-0에서 result = true, 잔액 = -2000
//		Thread-1에서 result = true, 잔액 = -2000

		// 동기화 처리한 결과
//		메서드 안에서 balance : 4000
//		Thread-0에서 result = true, 잔액 = 4000
//		Thread-1에서 result = false, 잔액 = 4000
	}
}

List 동기화 처리

private static List<타입> 리스트이름 = Collections.synchronizedList(new ArrayList<타입>());

Vector, Hashtable등과 같이 자바의 초창기부터 존재하던 Collection객체들은
내부에 동기화 처리가 되어있다
 
그런데 새로 구성된 Collection들은 동기화 처리가 되어 있지 않다.
그래서, 동기화가 필요한 프로그램에서 이런 Collection들을 사용하려면
동기화 처리를 한 후에 사용해야 한다.


import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Vector;

public class ThreadTest17 {
	private static Vector<Integer> vec = new Vector<Integer>();
	
	// 동기화 처리가 되지 않은 List
	private static List<Integer> list1 = new ArrayList<>();

	// 동기화 처리를 한 List
	private static List<Integer> list2 = 
			Collections.synchronizedList(new ArrayList<Integer>());
	
	public static void main(String[] args) {
		// 익명 구현체로 스레드 구현
		Runnable r = new Runnable() {
			
			@Override
			public void run() {
				for(int i=0; i<10000; i++) {
//					vec.add(i);
//					list1.add(i);
					list2.add(i);
				}
			}
		};
		
		Thread[] thArr = new Thread[] {
				new Thread(r), new Thread(r), new Thread(r), new Thread(r), new Thread(r)
		};
		
		for(Thread th : thArr) {
			th.start();
		}
		
		for(Thread th : thArr) {
			try {
				th.join();
			} catch (InterruptedException e) {
				// TODO: handle exception
			}
		}
		
//		System.out.println("vec의 개수 : "+vec.size()); // vec의 개수 : 50000
//		System.out.println("list의 개수 : "+list1.size()); // list의 개수 : 10162
// Exception in thread "Thread-4" Exception in thread "Thread-2" Exception in thread "Thread-3" Exception in thread "Thread-1" java.lang.ArrayIndexOutOfBoundsException: 22
// 기존의 데이터가 덮어씌어지면서 발생하는 현상
		
		System.out.println("list의 개수 : "+list2.size()); // list의 개수 : 50000
	}
}

동기화 - wait(), notify(), notifyAll()

wait() : 객체의 lock을 풀고 해당 객체의 스레드를 waiting pool에 넣는다

notify() : waiting pool에서 대기중인 스레드 중의 하나를 깨운다

notifyAll() : waiting pool에서 대기중인 모든 스레드를 깨운다.


public class ThreadTest18 {

	public static void main(String[] args) {
		WorkObject wObj = new WorkObject();
		WorkThread01 wth1 = new WorkThread01(wObj);
		WorkThread02 wth2 = new WorkThread02(wObj);
		
		wth1.start();
		wth2.start();

	}
}

// WorkObject의 method01()메서드만 호출하는 스레드
class WorkThread01 extends Thread {
	private WorkObject wObj;

	public WorkThread01(WorkObject wObj) {
		super();
		this.wObj = wObj;
	}

	@Override
	public void run() {
		for (int i = 0; i < 10; i++) {
			wObj.method01();
		}
		synchronized (wObj) {
			wObj.notify();	// 동기화 영역이 아님
		}
	}
}

// WorkObject의 method02()메서드만 호출하는 스레드
class WorkThread02 extends Thread {
	private WorkObject wObj;
	
	public WorkThread02(WorkObject wObj) {
		super();
		this.wObj = wObj;
	}
	
	@Override
	public void run() {
		for (int i = 0; i < 10; i++) {
			wObj.method02();
		}
		synchronized (wObj) {
			wObj.notify();	// 동기화 영역이 아님
		}
	}
}

// 공통으로 사용할 객체
class WorkObject {
	public synchronized void method01() {
		System.out.println("method01()메서드 실행 중...");

		notify();

		try {
			wait();
		} catch (InterruptedException e) {
			// TODO: handle exception
		}
	}

	public synchronized void method02() {
		System.out.println("method02()메서드 실행 중...");

		notify();

		try {
			wait();
		} catch (InterruptedException e) {
			// TODO: handle exception
		}
	}
}

public class ThreadTest19 {
	public static void main(String[] args) {
		DataBox dataBox = new DataBox();
		ProducerThread th1 = new ProducerThread(dataBox);
		ConsumerThread th2 = new ConsumerThread(dataBox);
		
		th1.start();
		th2.start();
	}
}

// 데이터를 공통으로 사용하는 클래스
class DataBox {
	private String value;

	// 저장된 데이터를 가져가는 메서드
	// 추가적인 기능 포함
	// ==> value 변수의 값이 null이면 value변수에 문자열이 채워질 때까지 기다리고,
	// value변수에 값이 있으면 해당 문자열을 반환한다.
	// 반환 후에는 value변수값을 null로 만든다.
	public synchronized String getValue() {
		if (value == null) {
			try {
				wait();
			} catch (InterruptedException e) {
				// TODO: handle exception
			}
		}
		// value변수에 데이터가 있을 때의 처리 내용
		String temp = value;
		System.out.println("스레드가 읽은 데이터 : " + temp);

		value = null;
		notify();
		return temp;
	}

	// 데이터를 저장하는 메서드
	// ==> value변수에 값이 있으면 value변수가 null이 될 때까지 기다림
	// value변수값이 null이 되면 새로운 데이터를 저장한다.
	public synchronized void setValue(String value) {
		if (this.value != null) {
			try {
				wait();
			} catch (InterruptedException e) {
				// TODO: handle exception
			}
		}
		// this.value변수에 새로운 데이터를 저장
		notify();
		this.value = value;
	}
}

// 데이터를 꺼내서 사용하는 스레드
class ConsumerThread extends Thread {
	private DataBox dataBox;

	public ConsumerThread(DataBox dataBox) {
		super();
		this.dataBox = dataBox;
	}

	@Override
	public void run() {
		for (int i = 0; i < 4; i++) {
			System.out.println(dataBox.getValue());
		}
	}
}

// 데이터를 넣는 스레드
class ProducerThread extends Thread {
	private DataBox dataBox;

	public ProducerThread(DataBox dataBox) {
		super();
		this.dataBox = dataBox;
	}

	@Override
	public void run() {
		String nameArr[] = { "홍길동", "이순신", "강감찬", "이몽룡" };
		for (String str : nameArr) {
			String value = str;
			dataBox.setValue(value);
		}
	}
}