자바생
article thumbnail
Published 2022. 4. 18. 10:12
Java volatile keyword Java
728x90

자바에서 동기화와 관련된 keyword에는 synchronized와 volatile이 있다

이번에는 volatile에 대해 공부해보자

 

0.1. synchronized와 volatile의 차이는 뭔가요?

0.1.1. synchronized

  • 상호 배제와 가시성을 모두 보장
  • 코드 block과 메서드에서 사용 가능
  • 스레드 메모리와 메인 메모리 사이의 "모든" 변수 값을 동기화
    • volatile보다 overhead 크다
  • null 객체는 동기화 X(링크)
    • null 객체를 동기화하고, 다른 곳에서 해당 객체를 사용하게 된다면 NPE발생
  • lock을 obtain하고 release 한다(이유는 아래에서)

0.1.2. volatile

  • "atomic"이 아닌 가시성만을 보장
  • 필드에서만 사용 가능
  • 스레드 메모리와 메인 메모리 사이의 volatile 변수 값을 동기화
  • volatile 변수 null 가능
  • lock을 obtain하거나 release 하지 않는다

 

synchronized는 lock을 obtain하고 release하지만 volatile은 필요하지 않을까요?

차이점에서 보다시피 volatile은 "atomic"을 보장하지 않는다
따라서 상호 배제를 보장하지 않는 volatile은 lock을 얻을 필요가 없다

 

0.2. volatile이 뭔가요?

  • 클래스를 thread-safe 상태로 만들기 위함
  • 변수의 가시성을 보장함(메서드, 클래스에 사용 불가)
  • volatile로 선언된 변수를 메인 메모리에 저장한다는 의미
    • read
      • CPU cahce에서 read하지 않고 메인 메모리에서 읽는다
    • write
      • writer할 때마다 메인 메모리까지 도달하여 작성함
  • read 스레드는 write 스레드에 의해 volatile 값이 변하게 되면 읽는다(아래 코드에서 확인)

 

0.3. volatile이 왜 사용하나요?

  • non-volatile은 성능 향상을 위해 각 스레드에 존재하는 CPU cache에 저장
  • 멀티 스레드 환경에서 각 CPU cache에 저장돼있는 변수가 다를 수 있다
    • 변수 값 불일치 문제 발생
  • 위 문제를 해결하기 위해 "volatile" 사용

 

0.4. volatile는 언제 사용하는게 적합한가요?

  • 변수 값 일치를 "보장"해야하는 경우에 사용하는 것이 좋다
    • 무분별한 사용 금지. CPU cache보다 Main memory에 접근하는 비용이 더 크기 때문
  • 하나의 Thread가 write하고 나머지 Thread가 읽는 상황일 경우
    • 왜 ??
    • 아래의 사진을 보면 두 thread가 count 값을 1씩 늘린다
    • 모두 Main Memory에 반영되기 전이다
    • 여기서 둘 다 모두 1을 증가시킨 후에 main memory에 반영시키면, count에는 1 값이 저장된다
      (원래는 2를 원했던 것)

  • long과 double형 변수를 atomic하게 read & write 처리
    • long과 double은 64비트이다. 기본적으로 write 연산은 해당 변수들을 atomic하지 않게 처리한다
    • 많은 플랫폼들은 long과 double을 32비트씩 2번 쓰기를 하게 되는데 이때 동시성 이슈 발생할 수 있다
    • volatile을 사용하게 되면 이와 같은 문제를 방지할 수 있음

 

0.5. volatile를 사용 시 write thread가 왜 한개만 존재해야하나요?

  • volatile은 atomic을 보장하지 않음
    • 변경 사항을 다른 thread에게 알리기 전에 다른 스레드가 간섭하여 변경할 수 있음
  • 단일 write thread, 여러 read thread에 유용
  • synchronized은 volatile과 다르게 read & write 연산 atomic 보장한다

 

0.6. 가시성 보장이 무슨 말인가요?

  • thread에서 변경한 값이 메인 메모리에 저장되지 않아서 다른 thread가 이 값을 볼 수 없는 상황을 가시성 문제라고 한다
  • 따라서 volatile를 사용하게 되면 메인 메모리에 저장되기 때문에 다른 thread가 현 thread에서 변경한 겂을 알 수 있다

 

0.7. non-volatile

<java />
public class Main { private static int COUNT = 0; public static void main(String[] args) { new CheckVal().start(); new ChangeVal().start(); } static class CheckVal extends Thread { @Override public void run() { int local = COUNT; while (local < 5) { if (local != COUNT) { System.out.println(Thread.currentThread().getName()); System.out.println("Got change " + COUNT); local = COUNT; } } } } static class ChangeVal extends Thread { @Override public void run() { int local = COUNT; while (local < 5) { System.out.println(Thread.currentThread().getName()); System.out.println("Incrementing " + (COUNT + 1)); local = ++COUNT; try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } } } } /* Thread-1 Incrementing 1 Thread-1 Incrementing 2 Thread-1 Incrementing 3 Thread-1 Incrementing 4 Thread-1 Incrementing 5 */

 

0.8. volatile(변수 값을 변경하지 않을 경우)

<java />
public class Main { private volatile static int COUNT = 0; public static void main(String[] args) { new CheckVal().start(); new ChangeVal().start(); } static class CheckVal extends Thread { @Override public void run() { int local = COUNT; while (local < 5) { if (local != COUNT) { System.out.println(Thread.currentThread().getName()); System.out.println("Got change " + COUNT); local = COUNT; } } } } static class ChangeVal extends Thread { @Override public void run() { int local = COUNT; while (local < 5) { System.out.println(Thread.currentThread().getName()); System.out.println("Incrementing " + (COUNT + 1)); local = COUNT; try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } } } } /* Thread-1 Incrementing 1 Thread-1 Incrementing 1 Thread-1 Incrementing 1 Thread-1 Incrementing 1 Thread-1 Incrementing 1 Thread-1 Incrementing 1 Thread-1 Incrementing 1 */
  • volatile이 뭔가요? 에 대한 질문에 "read 스레드는 write 스레드에 의해 volatile 값이 변하게 되면 읽는다" 증명
  • 앞서 non-volatile의 코드와 다른 점은 ChangeVal에서 COUNT 값을 증가시키지 않았다는 점
  • 따라서 volatile 변수 값을 변경하지 않았으므로 read thread인 CheckVal가 실행되지 않는다

 

0.8.1. 결론

read 스레드는 write 스레드에 의해 volatile 값이 변하게 되면 값을 read 한다

 

0.9. volatile(값을 변경할 경우)

<code />
public class Main { private volatile static int COUNT = 0; public static void main(String[] args) { new CheckVal().start(); new ChangeVal().start(); } static class CheckVal extends Thread { @Override public void run() { int local = COUNT; while (local < 5) { if (local != COUNT) { System.out.println(Thread.currentThread().getName()); System.out.println("Got change " + COUNT); local = COUNT; } } } } static class ChangeVal extends Thread { @Override public void run() { int local = COUNT; while (local < 5) { System.out.println(Thread.currentThread().getName()); System.out.println("Incrementing " + (COUNT+1)); local = ++COUNT; try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } } } } /* Thread-1 Incrementing 1 Thread-0 Got change 1 Thread-1 Incrementing 2 Thread-0 Got change 2 Thread-1 Incrementing 3 Thread-0 Got change 3 Thread-1 Incrementing 4 Thread-0 Got change 4 Thread-1 Incrementing 5 Thread-0 Got change 5 */
  • read thread인 CheckVal가 실행됨을 알 수 있다

 

0.10. volatile(read thread가 여러 개가 되면 안되는 이유)

<code />
public class Main { private volatile static int COUNT = 0; public static void main(String[] args) { new CheckVal().start(); new ChangeVal().start(); new ChangeVal2().start(); } static class CheckVal extends Thread { @Override public void run() { int local = COUNT; while (local < 5) { if (local != COUNT) { System.out.println(Thread.currentThread().getName()); System.out.println("Got change " + COUNT); local = COUNT; } } } } static class ChangeVal extends Thread { @Override public void run() { int local = COUNT; while (local < 5) { System.out.println(Thread.currentThread().getName() + " " +"changeVal"); System.out.println("Incrementing " + (COUNT+1)); local = ++COUNT; try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } } } static class ChangeVal2 extends Thread { @Override public void run() { int local = COUNT; while (local < 5) { System.out.println(Thread.currentThread().getName() + " " + "changeVal2"); System.out.println("Incrementing " + (COUNT+1)); local = ++COUNT; try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } } } } /* Thread-2 changeVal2 Thread-1 changeVal Incrementing 1 Incrementing 1 Thread-0 Got change 2 Thread-2 changeVal2 Thread-1 changeVal Incrementing 3 Incrementing 3 Thread-0 Got change 4 Thread-1 changeVal Thread-2 changeVal2 Incrementing 5 Incrementing 5 Thread-0 Got change 6 */
  • atomic이 보장되지 않기 때문에 값이 일치하지 않게 된다

 


1. REFERENCES

null 객체는 동기화 X

difference between synchronized and volatile

volatile 사용 시 write thread가 1개만 존재해야하는 이유

코드

 

728x90

'Java' 카테고리의 다른 글

java 연산자  (0) 2022.05.06
Primitive Type, Reference Type, Literal  (0) 2022.05.03
final 키워드  (0) 2022.03.05
스트림  (0) 2022.01.04
==와 equals의 차이  (0) 2021.05.25
profile

자바생

@자바생

틀린 부분이 있다면 댓글 부탁드립니다~😀

검색 태그