자바에서 멀티스레드를 이용하면 여러작업을 동시에 처리할 수 있기 때문에 작업효율이 좋아질 수도 있다.
하지만 하나의 공유자원을 여러 스레드에서 동시에 접근하여 사용하게 되면 때때로 우리가 예상치 못한 결과를 마주할 수 있다.
쓰레드가 동시성으로 실행될 때 여러 쓰레드가 동시에 접근 가능한 자원을 공유자원이라고 한다.
어떤 문제가 생길 수 있는지 예제를 보면서 이해해보자.
잔고가 1000원이 있다.
잔고에서 랜덤한 금액을 출금하는 메서드를 호출하여 출금한다. 이때 잔고에 남은 금액이 출금하려는 돈보다 크거나 같아야 출금할 수 있어야 한다.
class Account{
private int balance = 1000;
public int getBalance(){
return balance;
}
public void withdraw(int money){
if(balance >= money){ // 잔고의 금액이 출금하려는 돈보다 크거나 같아야
try{Thread.sleep(1000)
}catch(InterruptedException e){}
balance -= money;
}
}
}
class RunnableEx implements Runnable{
Account acc = new Account();
public void run(){
while(acc.getBalance()>0){
int money = (int)(Math.random()+3+1)+100;
acc.withdraw(money);
System.out.println(acc.getBalance());
}
}
}
class ThreadEx {
public static void main(String args[]){
RunnableEx r = new RunnableEx();
new Thread(r).start();
new Thread(r).start();
}
}
900
600
500
200
-100
음수가 나오면 안되는 로직인데 음수가 나왔다.
어떻게 된 것일까?
프로세스는 이랬을 것이다.
이런 눈물나는 오류가 생길 수 있기 때문에 한 스레드가 진행중인 작업을 다른 스레드가 간섭하지 못하게 막아야 한다.
그래서 스레드 동기화란,
👉 멀티스레드 환경에서 여러 스레드가 하나의 공유자원에 동시에 접근하지 못하도록 막는것
앞서 말한 문제를 syncronized 를 사용해서 스레드를 동기화 할 수 있다.
동기화하려면 간섭받지 않아야하는 문장들을 임계영역(critical section)으로 설정한다.
⭐ synchronized로 지정된 임계영역은 한 스레드가 이 영역에 접근하여 사용할때 lock이 걸림으로써 다른 스레드가 접근할 수 없게 된다.
⭐ 임계영역은 lock 을 얻은 단하나의 스레드만 출입이 가능하다. (1객체 1락)
이후 해당 스레드가 이 임계영역의 코드를 다 실행 후 벗어나게되면 unlock 상태가 되어 그때서야 대기하고 있던 다른 스레드가 이 임계영역에 접근하여 다시 lock을 걸고 사용할 수 있게 된다.
synchronized 로 임계영역을 설정하는 방법은 두가지가 있다.
1) 메소드에 synchronized 설정하기
메소드 이름 앞에 synchronized 키워드를 사용하면 해당 메소드 전체를 임계영역으로 설정한다.
public synchronized void increase() {
count++;
System.out.println(count);
}
임계영역은 한 스레드만 출입할 수 있기 때문에 영역을 최소화 해야한다.
=> 임계영역이 많을 수록 성능이 떨어짐, 갯수와 영역도 최소화 하는게 좋음
2) 코드블럭에 synchronized 설정하기
꼭 필요한 부분에만 블럭을 지정하여 임계영역으로 설정할 수 있다.
예제와 같이 synchronized(this)로 지정하게 되면 참조변수(this) 객체의 lock을 사용하게 된다.
void increase() {
synchronized(this) {//객체의 참조변수
count++;
}
System.out.println(count);
}
그럼 위 잔고 예제에 syncronized 를 걸어보자.
class Account{
private int balance = 1000; // 동기화해놨어도 다른 스레드가 건드리면 의미가 없게 되니 private으로 선언
//-----임계영역-----//
public syncronized int getBalance(){ //잔고를 읽을 때에도 방해받지 않게 동기화를 걸어줬다
return balance;
}
//-----임계영역-----//
public syncronized void withdraw(int money){
if(balance >= money){
try{Thread.sleep(1000)
}catch(InterruptedException e){}
balance -= money;
}
}
}
class RunnableEx implements Runnable{
Account acc = new Account();
public void run(){
while(acc.getBalance()>0){
int money = (int)(Math.random()+3+1)+100;
acc.withdraw(money);
System.out.println(acc.getBalance());
}
}
}
class ThreadEx {
public static void main(String args[]){
RunnableEx r = new RunnableEx();
new Thread(r).start();
new Thread(r).start();
}
}
900
300
200
0
0
내 잔고를 안전하게 지킬 수 있게 되었다!