item 78 leekyunghee - JAVA-JIKIMI/EFFECTIVE-JAVA3 GitHub Wiki

synchronized ν‚€μ›Œλ“œ

  • λ©”μ„œλ“œλ‚˜ 블둝을 ν•œλ²ˆμ— ν•œ μŠ€λ ˆλ“œμ”© μˆ˜ν–‰ν•˜λ„λ‘ 보μž₯ν•˜λ €λ©΄ synchronized ν‚€μ›Œλ“œλ₯Ό μ‚¬μš©ν•˜λ©΄ λœλ‹€.
  • 동기화λ₯Ό μ œλŒ€λ‘œ μ‚¬μš©ν•˜λ©΄ μ–΄λ–€ λ©”μ„œλ“œλ„ 객체의 μƒνƒœκ°€ μΌκ΄€λ˜μ§€ μ•Šμ€ μˆœκ°„μ„ λ³Ό 수 μ—†λ‹€.
  • λ™κΈ°ν™”λœ λ©”μ„œλ“œλ‚˜ 블둝에 λ“€μ–΄κ°„ μŠ€λ ˆλ“œκ°€ 같은 락의 λ³΄ν˜Έν•˜μ— μˆ˜ν–‰λœ λͺ¨λ“  이전 μˆ˜μ •μ˜ μ΅œμ’… κ²°κ³Όλ₯Ό κ°™κ²Œ ν•œλ‹€.
  • μ‹±κΈ€ μŠ€λ ˆλ“œ 기반 ν”„λ‘œκ·Έλž¨μ΄λΌλ©΄ 동기화λ₯Ό κ³ λ €ν•˜μ§€ μ•Šμ•„λ„ λ˜μ§€λ§Œ λ©€ν‹° μŠ€λ ˆλ“œ 기반이라면 객체λ₯Ό κ³΅μœ ν•  λ•Œ 동기화λ₯Ό κ³ λ―Όν•΄μ•Ό ν•œλ‹€.

μ›μžμ (atomic)

  • μžλ°” μ–Έμ–΄μ˜ λͺ…μ„ΈμƒμœΌλ‘œ longκ³Ό double λ₯Ό μ œμ™Έν•œ λ³€μˆ˜λ₯Ό 읽고 μ“°λŠ” 것은 μ›μžμ μ΄λ‹€.
  • 즉, 동기화 없이 μ—¬λŸ¬ μŠ€λ ˆλ“œκ°€ 같은 λ³€μˆ˜λ₯Ό μˆ˜μ •ν•˜λ”λΌλ„ 항상 μ–΄λ–€ μŠ€λ ˆλ“œκ°€ μ •μƒμ μœΌλ‘œ μ €μž₯ν•œ 값을 μ½μ–΄μ˜€λŠ” 것을 보μž₯ν•œλ‹€λŠ” 것이닀.
  • ν•˜μ§€λ§Œ μŠ€λ ˆλ“œκ°€ ν•„λ“œλ₯Ό 읽을 λ•Œ 항상 β€˜μˆ˜μ •μ΄ μ™„μ „νžˆ λ°˜μ˜λœβ€™ 값을 μ–»λŠ”λ‹€ 보μž₯ν•˜μ§€λ§Œ, ν•œ μŠ€λ ˆλ“œκ°€ μ €μž₯ν•œ 값이 λ‹€λ₯Έ μŠ€λ ˆλ“œμ—κ²Œ β€˜λ³΄μ΄λŠ”κ°€β€™λŠ” 보μž₯ν•˜μ§€ μ•ŠλŠ”λ‹€. λ”°λΌμ„œ μ›μžμ  데이터λ₯Ό μ“Έ λ•Œλ„ 동기화해야 ν•œλ‹€.

잘λͺ»λœ μ½”λ“œ μ˜ˆμ‹œ: 동기화가 μ—†λ‹€.

  • 동기화가 잘λͺ» λ˜μ—ˆμ„ λ•ŒλŠ” μ–΄λ–€ 일이 λ°œμƒν•˜λŠ”μ§€ μ½”λ“œλ‘œ μ‚΄νŽ΄λ³΄μž. μ•„λž˜ μ½”λ“œλŠ” μ–Όλ§ˆλ‚˜ μ˜€λž«λ™μ•ˆ μ‹€ν–‰λ κΉŒ?
public class StopThread {
    private static boolean stopRequested;

    public static void main(String[] args) throws InterruptedException {
        Thread backgroundThread = new Thread(() -> {
            int i = 0;
            while (!stopRequested)
                i++;
        });
        backgroundThread.start();
        TimeUnit.SECONDS.sleep(1);
        stopRequested = true;
    }
}
  • μŠ€λ ˆλ“œκ°€ start 되고 1초 λ™μ•ˆμ˜ sleep이 λλ‚˜λ©΄ boolean λ³€μˆ˜μ˜ 값이 trueκ°€ λ˜μ–΄ 루프λ₯Ό λΉ μ Έλ‚˜μ˜¬ κ²ƒμœΌλ‘œ μ˜ˆμƒλœλ‹€.
  • ν•˜μ§€λ§Œ μ‹€μ œλ‘œ μ½”λ“œλ₯Ό μˆ˜ν–‰ν•΄λ³΄λ©΄ ν”„λ‘œκ·Έλž¨μ€ μ’…λ£Œλ˜μ§€ μ•ŠλŠ”λ‹€.
  • 동기화λ₯Ό ν•˜μ§€ μ•Šμ•˜κΈ° λ•Œλ¬Έμ— 메인 μŠ€λ ˆλ“œκ°€ μˆ˜μ •ν•œ boolean λ³€μˆ˜μ˜ 값이 λ°±κ·ΈλΌμš΄λ“œ μŠ€λ ˆλ“œμ—κ²Œ μ–Έμ œ λ³€κ²½λœ κ°’μœΌλ‘œ 보일지 λͺ¨λ₯Έλ‹€.
  • λ˜ν•œ 동기화 μ½”λ“œκ°€ μ—†λ‹€λ©΄ JVMμ—μ„œ μ•„λž˜μ™€ 같은 μ΅œμ ν™”λ₯Ό ν•  μˆ˜λ„ μžˆλ‹€.
// μ›λž˜ μ½”λ“œ
while (!stopRequested)
    i++;

// μ΅œμ ν™”ν•œ μ½”λ“œ
if (!stopRequested)
    while (true)
        i++;
  • μ΄λŠ” JVM이 μ‹€μ œλ‘œ μ μš©ν•˜λŠ” λŒμ–΄μ˜¬λ¦¬κΈ°(hoisting, ν˜Έμ΄μŠ€νŒ…)λΌλŠ” μ΅œμ ν™” 기법이 μ‚¬μš©λœ 것이닀.
  • 결과적으둜 응닡 λΆˆκ°€(liveness failure) μƒνƒœκ°€ λ˜μ–΄ 더 이상 μ§„ν–‰λ˜λŠ” μ½”λ“œκ°€ μ—†λ‹€. λ‹€μ‹œ κΈ°μ‘΄ μ½”λ“œλ‘œ λŒμ•„μ™€μ„œ 생각해보면, κ³΅μœ ν•˜λŠ” λ³€μˆ˜λ₯Ό λ‹€λ£° λ•Œ λ™κΈ°ν™”ν•˜λŠ” μ½”λ“œλ₯Ό λ„£μœΌλ©΄ λœλ‹€.
public class StopThread {
    private static boolean stopRequested;

    private static synchronized void requestStop() {
        stopRequested = true;
    }

    private static synchronized boolean stopRequested() {
        return stopRequested;
    }

    public static void main(String[] args) throws InterruptedException {
        Thread backgroundThread = new Thread(() -> {
            int i = 0;
            while (!stopRequested())
                i++;
        });
        backgroundThread.start();
        TimeUnit.SECONDS.sleep(1);
        requestStop();
    }
}
  • 이처럼 λ™κΈ°ν™”λŠ” 읽기와 쓰기에 λŒ€ν•΄ λͺ¨λ‘ ν•„μš”ν•˜λ‹€. μœ„ μ½”λ“œμ²˜λŸΌ 곡유 ν•„λ“œμ— λŒ€ν•œ 읽기/μ“°κΈ° λ©”μ„œλ“œ λͺ¨λ‘λ₯Ό 동기화 μ²˜λ¦¬ν•˜λ©΄ λ¬Έμ œλŠ” ν•΄κ²°λœλ‹€.

volatile

  • 배타적 μˆ˜ν–‰κ³ΌλŠ” 상관이 μ—†μ§€λ§Œ 항상 κ°€μž₯ μ΅œκ·Όμ— μ €μž₯된 값을 μ½μ–΄μ˜¨λ‹€. μ΄λ‘ μ μœΌλ‘œλŠ” CPU μΊμ‹œκ°€ μ•„λ‹Œ μ»΄ν“¨ν„°μ˜ 메인 λ©”λͺ¨λ¦¬λ‘œλΆ€ν„° 값을 μ½μ–΄μ˜¨λ‹€.
  • κ·Έλ ‡κΈ° λ•Œλ¬Έμ— 읽기/μ“°κΈ° λͺ¨λ‘κ°€ 메인 λ©”λͺ¨λ¦¬μ—μ„œ μˆ˜ν–‰λœλ‹€.
public class stopThread {
    private static volatile boolean stopRequested;

    public static void main(String[] args) throws InterruptedException {
        Thread backgroundThread = new Thread(() -> {
            int i = 0;
            while (!stopRequested)
                i++;
        });
        backgroundThread.start();
        TimeUnit.SECONDS.sleep(1);
        stopRequested = true;
    }
}
  • μœ„ μ½”λ“œμ²˜λŸΌ volatile을 μ‚¬μš©ν•˜λ©΄ 동기화λ₯Ό μƒλž΅ν•΄λ„ λœλ‹€. λ‹€λ§Œ μ£Όμ˜ν•΄μ„œ μ‚¬μš©ν•΄μ•Ό ν•œλ‹€. μ•„λž˜μ™€ 같은 μ˜ˆμ œμ—μ„œ λ¬Έμ œμ μ„ μ°Ύμ•„λ³Ό 수 μžˆλ‹€.
private static volatile int nextSerialNumber = 0;

public static int generateSerialNumber() {
    return nextSerialNumber++;
}
  • μ½”λ“œμƒμœΌλ‘œ 증가 μ—°μ‚°μž(++)λŠ” ν•˜λ‚˜μ§€λ§Œ μ‹€μ œλ‘œλŠ” volatile ν•„λ“œμ— 두 번 μ ‘κ·Όν•œλ‹€.
  • λ¨Όμ € 값을 읽고, κ·Έ λ‹€μŒμ— 1을 μ¦κ°€ν•œ ν›„ μƒˆλ‘œμš΄ 값을 μ €μž₯ν•˜λŠ” 것이닀.
  • λ”°λΌμ„œ 두 번째 μŠ€λ ˆλ“œκ°€ 첫 번째 μŠ€λ ˆλ“œμ˜ μ—°μ‚° 사이에 듀어와 곡유 ν•„λ“œλ₯Ό 읽게 되면, 첫 번째 μŠ€λ ˆλ“œμ™€ 같은 값을 보게될 것이닀.
  • 이처럼 잘λͺ»λœ κ²°κ³Όλ₯Ό κ³„μ‚°ν•΄λ‚΄λŠ” 였λ₯˜λ₯Ό μ•ˆμ „ μ‹€νŒ¨(safety failure)라고 ν•œλ‹€. 이 λ¬Έμ œλŠ” λ©”μ„œλ“œμ— synchronizedλ₯Ό 뢙이고 volatile ν‚€μ›Œλ“œλ₯Ό 곡유 ν•„λ“œμ—μ„œ μ œκ±°ν•˜λ©΄ ν•΄κ²°λœλ‹€.

atomic νŒ¨ν‚€μ§€

  • java.util.concurrent.atomic νŒ¨ν‚€μ§€μ—λŠ” 락 없이도 thread-safeν•œ 클래슀λ₯Ό μ œκ³΅ν•œλ‹€.
  • volatile은 λ™κΈ°ν™”μ˜ 효과 쀑 톡신 μͺ½λ§Œ μ§€μ›ν•˜μ§€λ§Œ 이 νŒ¨ν‚€μ§€λŠ” μ›μžμ„±(배타적 μ‹€ν–‰)κΉŒμ§€ μ§€μ›ν•œλ‹€. κ²Œλ‹€κ°€ μ„±λŠ₯도 동기화 버전보닀 μš°μˆ˜ν•˜λ‹€.
private static final AtomicLong nextSerialNum = new AtomicLong();

public static long generateSerialNumber() {
    return nextSerialNum.getAndIncrement();
}

핡심정리

  • κ²°λ‘ μ μœΌλ‘œλŠ” κ°€λ³€ 데이터λ₯Ό κ³΅μœ ν•˜μ§€ μ•ŠλŠ” 것이 동기화 문제λ₯Ό ν”Όν•˜λŠ” κ°€μž₯ 쒋은 방법이닀.
  • 즉, κ°€λ³€ λ°μ΄ν„°λŠ” 단일 μŠ€λ ˆλ“œμ—μ„œλ§Œ μ‚¬μš©ν•˜μž. ν•œ μŠ€λ ˆλ“œκ°€ 데이터λ₯Ό μˆ˜μ •ν•œ 후에 λ‹€λ₯Έ μŠ€λ ˆλ“œμ— κ³΅μœ ν•  λ•ŒλŠ” ν•΄λ‹Ή κ°μ²΄μ—μ„œ κ³΅μœ ν•˜λŠ” λΆ€λΆ„λ§Œ 동기화해도 λœλ‹€.
  • λ‹€λ₯Έ μŠ€λ ˆλ“œμ— 이런 객체λ₯Ό κ±΄λ„€λŠ” ν–‰μœ„λ₯Ό μ•ˆμ „ λ°œν–‰(safe publication)이라고 ν•œλ‹€.
  • 클래슀 μ΄ˆκΈ°ν™” κ³Όμ •μ—μ„œ 객체λ₯Ό 정적 ν•„λ“œ, volatile ν•„λ“œ, final ν•„λ“œ ν˜Ήμ€ λ³΄ν†΅μ˜ 락을 톡해 μ ‘κ·Όν•˜λŠ” ν•„λ“œ 그리고 λ™μ‹œμ„± μ»¬λ ‰μ…˜μ— μ €μž₯ν•˜λ©΄ μ•ˆμ „ν•˜κ²Œ λ°œν–‰ν•  수 μžˆλ‹€.