Search
Duplicate

synchronized

한 쓰레드가 진행 중인 작업을 다른 쓰레드가 간섭하지 못하도록 막는 것을 쓰레드의 동기화(synchronization)이라 한다.

개요

멀티 쓰레드 환경에서 하나의 자원이 여러 쓰레드에 의해 참조될 수 있다면, 그 쓰레드의 갯수만큼의 위/변조 위험이 있다는 것고 동일하다. 예를들어 은행 어플리케이션에서 은행잔고라는 자원을 가지고 멀티쓰레드 환경이 돌아갈 때 1번 쓰레드에서 은행자원을 사용하려는 상황에서 2번 쓰레드가 그 사이에 은행자원을 다 가져다 써서 0이된다면 1번 쓰레드는 이미 다 써버렸지만, 있다고 가정해서 사용하다가 심각한 에러가 발생할 수 있다.
이런 상황에서 여러 쓰레드에 의해 경쟁상태가 되는 자원을 사용하는 영역을 임계영역(critical section)이라 하고, 이런 자원에 접근을 막는 것을 잠금(Lock)이라 한다.
즉, 소스 코드에서 이러한 공유 자원을 사용하는 부분을 임계영역으로 지정한 뒤 쓰레드가 공유가능한 공유데이터를 사용하는 시점에서 잠금(lock)을 걸어버리면 해당 쓰레드가 임계영역 내의 모든 코드를 수행하기전까진 다른 쓰레드가 해당 임계영역에 접근하지 못하고, 잠금이 풀릴 때까지 기다리게 된다.
이렇게 공유자원의 대한 동시접근을 막는 것을 쓰레드의 동기화(synchronized)라 한다.
참고 synchronized뿐 아니라 JDK 1.5부터 java.util.concurrent.locks, java.util.concurrent.atomic 패키지를 통해 이러한 동기화 기법에 필요한 기능들을 제공한다.

Lock의 범위

임계영역을 설정해서 잠금(Lock)을 할 수 있고 이를 통해 멀티쓰레드에서 자원공유로 인한 데이터 오염의 문제를 해결해보았다. 그럼 이러한 잠금(Lock)은 어느 범위까지 잠금이 되는 것일까?
메서드에 synchronized키워드를 추가하면 그 함수가 포함된 객체(this)에 잠금(Lock)이 걸린다.
블록에 synchronized키워드를 추가하면 괄호안에 인자로 넣는 객체가 잠금(Lock)대상이 된다.
즉, synchronized 키워드가 메서드에 붙는다면 해당 메서드가 있는 객체(this)자체가 잠금 객체 대상이 된다.
그렇기에 때문에 this 키워드를 사용할 수도 있고 다른 객체를 잠금 객체로 사용할 수도 있지만, 이런 경우 동기화작업이 제대로 되지 않을 수 있기 때문에 락 객체는 하나만 사용하는게 좋다.
한편, 메서드의 블록 내부에서 synchronized 블록을 사용하는 경우 해당 객체와 메서드까지는 여러 스레드에서 동시에 접근할 수 있다. 하지만, synchronized 블록에 다다르면 모든 스레드는 동작을 멈추고 자신의 차례를 기다리게 된다.

사용법

다음과 같은 두 가지 방법으로 간단하게 임계 영역 설정및 잠금을 할 수 있는데, 자원들은 이 임계영역을 들어가면서 쓰레드가 잠금(lock)을 얻게되고, 블럭을 벗어나면서 자동으로 잠금(lock)을 반환되는데 이는 모두 자동적으로 이뤄지기 때문에 임계영역 설정만 해주면 된다.
하지만, 임계 영역은 멀티쓰레드 프로그램의 성능에 영향을 끼치기 때문에 최대한 좁은 범위로 임계영역을 설정해야 하고 그렇기에 메서드 전체보다는 특정 범위에 synchronized 블럭으로 임계 영역을 설정하는게 좋다.
1.
메서드 전체를 임계 영역으로 지정하는 방법
public synchronized void usingAmount(long amount) { //...로직 생략 }
Java
복사
⇒ 블럭 내부 전체를 임계 영역(critical section)이라 한다.
2.
특정 영역을 임계 영역으로 지정하는 방법
public void usingAmount(...) { synchronized (객체의 참조변수){ //...로직 생략 } }
Java
복사
⇒ 로직내에서도 synchronized 블럭 내부만을 임계 영역(critical section)이라 한다.

예제 코드

다음은 쇼핑몰에서 상품구매에 관련된 코드로 상품의 재고가 멀티쓰레드 환경에서 synchronized 키워드를 통해 안전하게 관리되도록 하는 코드이다.
public class ShoppingApplication { public static void main(String[] args) throws ExecutionException, InterruptedException { ShoppingMall shoppingMall; shoppingMall = new ShoppingMall("홈플러스"); CompletableFuture<Void> future1 = CompletableFuture.runAsync(() -> { try { shoppingMall.order("마스크", 100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + ": " + shoppingMall.getQuantityByProduct("마스크")); }); CompletableFuture<Void> future2 = CompletableFuture.runAsync(() -> { try { shoppingMall.order("마스크", 40); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + ": " + shoppingMall.getQuantityByProduct("마스크")); }); CompletableFuture.allOf(future1, future2).get(); } private static class ShoppingMall { private final String name; private final Map<String, Product> productList = new ConcurrentHashMap<>(); public ShoppingMall(String name) { this.name = name; } public boolean addProduct(String name, int quantity) { if (productList.containsKey(name)) { return false; } productList.put(name, new Product(name, quantity)); return true; } public void order(String name, int quantity) throws InterruptedException { if (!productList.containsKey(name)) { throw new IllegalArgumentException(); } synchronized (productList) { // 임계영역 설정 블록 Product product = productList.get(name); int productQuantity = product.quantity; Thread.sleep(1000L); productQuantity += (quantity * -1); if (productQuantity < 0) { throw new IllegalArgumentException(); } product.changeQuantity(productQuantity); } } public String getQuantityByProduct(String name) { return productList.get(name).toString(); } private static class Product { private final String name; private int quantity; public Product(String name, int quantity) { this.name = name; this.quantity = quantity; } public void changeQuantity(int quantity) { this.quantity = quantity; } @Override public String toString() { return "[" + name + ", " + quantity + "]"; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Product)) return false; Product product = (Product) o; return quantity == product.quantity && Objects.equals(name, product.name); } @Override public int hashCode() { return Objects.hash(name, quantity); } } } }
Java
복사
⇒ 위 코드에서 제품 주문 메서드(order)에서 synchronized 키워드가 없다면 어떻게 될까? 먼저 접근한 Thread에서도 제품의 재고수량을 조회해온 뒤 Thread.sleep(1000L)로 인해 기다리게되는동안 다른 쓰레드에서도 해당 자원에 접근해서 제품의 재고 수량을 가져온다. 그럼 양 측 쓰레드는 모두 최초의 재고수량(100개)를 가지고 계산을 하게되는데, future1에서는 100개, future2에서는 40개를 주문하게된다.
쇼핑몰의 마스크 총 재고는 100개지만, future1, future2가 주문한 총 수량 140개가 오류를 발생하지않고 주문이 되었을 뿐 아니라 둘 중 무엇이 더 늦게 동작하냐에따라 재고마저도 60개가 될 수도 있다.
그래서 이런 코드부분에 임계영역 설정(synchronized)을 한다면, 해당 자원에 대한 잠금이 걸리면서 위와같은 동시성 문제를 해결할 수 있게 된다.

관련글