동기화 Synchronization
- 공유자원(객체)을 상대로 순서대로 작업이 이루어지도록 처리하는 방법
- 과도한 동기화: 전체적으로 프로그램의 성능을 저하시킴
필요한 이유
- 프로세스 내 자원을 여러개의 스레드가 공유하여 작업 진행시 문제 방지
- 임계영역(Critical Section, 문제 발생 여지가 있는 영역)에 동기화 처리를 해줌으로써 예방
동기화 처리 방법
- synchronized
- 메서드 자체에 동기화 처리
public synchronized void add() { ~ }
- 여러개의 Thread들이 공유객체의 메소드를 사용할 때 메소드에 synchronized가 붙어 있을 경우 먼저 호출한 메소드가 객체의 사용권(Monitoring Lock)을 얻는다.
- 메소드 앞에 synchronized 를 붙혀서 실행해 보면, 메소드 하나가 모두 실행된 후에 다음 메소드가 실행된다.
- 해당 모니터링 락은 메소드 실행이 종료되거나, wait()와 같은 메소드를 만나기 전까지 유지된다.
- 다른 쓰레드들은 모니터링 락을 놓을때까지 대기한다.
- 동기화 블럭
synchornized (this) { ~ }
- synchronized를 메소드에 붙혀서 사용 할 경우, 메소드의 코드가 길어지면, 마지막에 대기하는 쓰레드가 너무 오래 기다리는것을 막기위해서 블럭을 사용
- 해당 this가 동기화 처리가 됨.
- this만 동기화 블럭에 접근 가능
- 블럭빠져나오기전까지는 동기화 블럭 내 애들만 공유객체에 접근 가능
- 이때는 blocked상태이며 블럭을 빠져나오면서 runnable로 바뀌게됨
- blocked상태 = waiting상태처럼 일시정지상태
- lock 객체 이용
- 객체 생성시
private final
로 생성
- 장점: 범위를 마음대로 정해줄 수 있음
- 단점: unlock을 잘 해줘야함 안해주면 끝장난다(?)
- lock에도 여러 종류가 있지만 제일 심플한건 ReentrantLock
- lock(): 동기화 시작
- unlock(): 동기화 끝
- try ~ catch 블럭을 사용할 경우, unlock()은 finally 블럭에서
예시
T15: synchronized
1. 공유객체 클래스
class ShareObject {
private int sum = 0;
public void add() {
for (int i = 0; i < 1000000000; i++) {}
synchronized (this) {
int n = sum;
n += 10;
sum = n;
System.out.println(Thread.currentThread().getName()
+ " 합계: " + sum);
}
}
}
2. 작업을 수행하는 쓰레드 클래스
class WorkerThread extends Thread {
ShareObject sObj;
public WorkerThread(String name, ShareObject sObj) {
super(name);
this.sObj = sObj;
}
@Override
public void run() {
for (int i = 1; i < 10; i++) {
sObj.add();
}
}
}
3. 프로그램이 실행될 main()
ShareObject sObj = new ShareObject();
WorkerThread th1 = new WorkerThread("1번 쓰레드", sObj);
WorkerThread th2 = new WorkerThread("2번 쓰레드", sObj);
th1.start();
th2.start();
4. 결과
- 동기화 안할때
- 동기화 할때
T16: synchronized 예제
1. 은행의 입출금을 관리하는 클래스 (공유객체)
class SyncAccount {
private int balance;
public synchronized int getBalance() {
return balance;
}
public synchronized void setBalance(int balance) {
this.balance = balance;
}
public synchronized void deposit(int money) {
balance += money;
}
public synchronized boolean withdraw(int money) {
if(balance >= money) {
for (int i = 1; i < 1000000000; i++) {}
balance -= money;
System.out.println("메서드 안에서 balance = " + getBalance());
return true;
}else {
return false;
}
}
}
2. 은행 업무를 처리하는 쓰레드
class BankThread extends Thread {
private SyncAccount sAcc;
public BankThread(SyncAccount sAcc) {
this.sAcc = sAcc;
}
@Override
public void run() {
boolean result = sAcc.withdraw(6000);
System.out.println("쓰레드 안에서 result = " + result
+ ", balance = " + sAcc.getBalance());
}
}
3. 프로그램이 실행되는 main()
SyncAccount sAcc = new SyncAccount();
sAcc.setBalance(10000);
BankThread th1 = new BankThread(sAcc);
BankThread th2 = new BankThread(sAcc);
th1.start();
th2.start();
T17: Lock 예제
1. 입출금을 담당하는 클래스 (공유객체)
class LockAccount {
private int balance;
private final ReentrantLock lock = new ReentrantLock();
public int getBalance() {
return balance;
}
public void setBalance(int balance) {
this.balance = balance;
}
public void deposit(int money) {
lock.lock();
balance += money;
lock.unlock();
}
public boolean withdraw(int money) {
lock.lock();
boolean chk = false;
try {
if(balance >= money) {
for (int i = 0; i < 1000000000; i++) {}
balance -= money;
System.out.println("메서드 안에서 = " + getBalance());
chk = true;
}
} catch (Exception e) {
chk = false;
} finally {
lock.unlock();
}
return chk;
}
}
2. 은행 업무를 처리하는 쓰레드
class BankThread2 extends Thread {
private LockAccount lAcc;
public BankThread2(LockAccount lAcc) {
this.lAcc = lAcc;
}
@Override
public void run() {
boolean result = lAcc.withdraw(6000);
System.out.println("쓰레드 안에서 result = " + result
+ ", balance = " + lAcc.getBalance());
}
}
3. 프로그램이 실행되는 main()
LockAccount lAcc = new LockAccount();
lAcc.setBalance(10000);
BankThread2 th1 = new BankThread2(lAcc);
BankThread2 th2 = new BankThread2(lAcc);
th1.start();
th2.start();
두 방법 모두 동일한 결과
Collection 클래스 동기화처리
- Vector, HashTable등 예전부터 존재하던 Collection 클래스들은 내부에 동기화 처리가 되어있다.
- 최근에 새로 구성된 Collection들은 동기화 처리가 되어있지 않다.
- 동기화가 필요한 프로그램에서 이런 Collection들을 사용하려면 동기화 처리를 한 후에 사용해야 한다.
- 동기화 처리시: Collections의 정적 메서드 중에서 synchronized로 시작하는 메서드 이용.
T18:
private static List<Integer> list1 = new ArrayList<Integer>();
private static List<Integer> list2 = Collections.synchronizedList(new ArrayList<>());
public static void main(String[] args){
Runnable r = new Runnable() {
@Override
public void run() {
for(int i=1; i<=10000; i++){
list2.add(i);
}
}
};
Thread[] ths = new Thread[] {
new Thread(r), new Thread(r),
new Thread(r), new Thread(r), new Thread(r)
};
long startTime = System.currentTimeMillis();
for (Thread th : ths) {
th.start();
}
for (Thread th : ths) {
try {
th.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
long endTime = System.currentTimeMillis();
System.out.println("처리 시간(ms)" + (endTime - startTime));
System.out.println("list2의 개수 : " + list2.size());
}
동기화 처리 하지않은 리스트를 사용했을 때
동기화 처리가 된 리스트