-
Java에서 동시성 관리 - SynchronizationJava 2020. 12. 29. 22:35
개요
최근 출시되는 하드웨어는 거의 대부분 멀티 코어가 가능하도록 설계됩니다. 코어란 CPU 안에서 연산 처리를 하는 장치입니다. 이러한 코어를 최대한 활용하는 것이 프로그램의 성능에도 당연히 좋겠죠. 그러려면 여러 스레드나 프로세스를 동시에 실행시켜서 처리해야합니다. 여기서 발생하는 문제가 동시성 문제입니다.
은행 통장 입출금 예제를 통해 동시성 문제에 대해 살펴보겠습니다. 프로그램은 계좌 클래스, 스레드의 Task를 수행할 ThreadTask 클래스, 그리고 이를 토대로 두 개의 스레드를 생성시켜 은행 계좌를 잔고가 0원이 될때까지 출금을 하는 메인 함수로 구성되있습니다. 출금은 100, 200, 300원 중 랜덤한 값으로 출금합니다.
public class Main { public static void main(String[] args) { ThreadTask threadTask = new ThreadTask(); Thread customer1 = new Thread(threadTask); Thread customer2 = new Thread(threadTask); customer1.start(); customer2.start(); } } class Account { private int balance; public Account(int balance) { this.balance = balance; } public void withDraw(int money) { if (balance >= money) { try { Thread thread = Thread.currentThread(); System.out.println(thread.getName() + " money: " + money); thread.sleep(1000); this.balance -= money; System.out.println(thread.getName() + " balance: " + balance); } catch (InterruptedException error) { error.printStackTrace(); } } } public int getBalance() { return this.balance; } } class ThreadTask implements Runnable { private Account account = new Account(2000); @Override public void run() { while (account.getBalance() > 0) { int money = (int) ((Math.random() * 3) + 1) * 100; account.withDraw(money); } } }
위의 프로그램은 balance라는 값에 대한 동시성 문제가 발생합니다. 두 스레드가 해당 변수에 접근하는 시점과 출금을 하는 시점이 일치하지 않기 때문입니다. 프로그램을 실행시켜보면,
화면과 같이 최종적으로 -100이 출력되는 것을 확인할 수 있습니다. 물론 실행을 여러번하면 제대로 출력될 때도 있습니다. 하지만 이러한 결과가 나오는 것은 thread 0이 200원을 출금하려는 순간, thread 1이 100원을 추가로 출금하려고 입력을 넣어서, thread 0의 Task가 실행된 후에 다시 thread 1의 Task 결과가 반영이 되는 것입니다. 이런식으로 두 스레드가 공유하는 변수가 예상치 못한 결과값이 멀티 스레드에서 발생할 수 있습니다. 스레드가 더 많아지면 그만큼 더 제어가 힘들어지겠죠.
Syncronized를 이용한 동시성 제어
Lock이라는 개념을 이용하면 공유 자원에 대한 통제가 가능합니다. 두 개의 스레드가 있다고 가정하면, 공유 자원을 획득한 스레드가 Lock을 가진다고 하고, 자원을 다 쓰고 반환하면 Lock을 반환한다고 합니다. 따라서, 반드시 한개의 스레드만 공유 자원을 사용하게 되는 것이죠. 이러한 로직을 구현하기 위해 자바는 Synchronized라는 예약어를 제공합니다. Synchronized 블록을 생성하거나 메소드 앞에 붙일 수가 있습니다.
Synchronized는 Lock 획득, 반환에 대한 기능을 제공한다.
예제의 출금 메소드에 Synchronized를 붙여보겠습니다.
public class Main { public static void main(String[] args) { ThreadTask threadTask = new ThreadTask(); Thread customer1 = new Thread(threadTask); Thread customer2 = new Thread(threadTask); customer1.start(); customer2.start(); } } class Account { private int balance; public Account(int balance) { this.balance = balance; } public synchronized void withDraw(int money) { if (balance >= money) { try { Thread thread = Thread.currentThread(); System.out.println(thread.getName() + " money: " + money); thread.sleep(1000); this.balance -= money; System.out.println(thread.getName() + " balance: " + balance); } catch (InterruptedException error) { error.printStackTrace(); } } } public int getBalance() { return this.balance; } } class ThreadTask implements Runnable { private Account account = new Account(2000); @Override public void run() { while (account.getBalance() > 0) { int money = (int) ((Math.random() * 3) + 1) * 100; account.withDraw(money); } } }
Synchronized를 하면 withDraw 메소드에 반드시 하나의 스레드만 접근할 수 있습니다. 따라서 동시에 출금하게 되는 일이 없어지게 되는 것이죠. 단, Synchronized를 사용하면 그만큼 멀티 코어, 멀티 스레드의 장점을 잃어 성능을 떨어드릴 수 있습니다. 따라서, 반드시 필요할 때만 사용하는 것이 중요합니다. 동시성 제어엔 다른 여러 방법이 있는데, 다음 포스팅에서 다뤄보도록 하겠습니다.
'Java' 카테고리의 다른 글
Java의 Singleton Pattern에 대하여 (0) 2021.05.26 Abstract Class와 Interface의 차이 (0) 2021.04.13 Java GC와 Object간 Reference (0) 2020.10.18 Java로 구현하는 DFS 알고리즘 (0) 2020.02.24