728x90
자바에서 동기화와 관련된 keyword에는 synchronized와 volatile이 있다
이번에는 volatile에 대해 공부해보자
synchronized와 volatile의 차이는 뭔가요?
synchronized
- 상호 배제와 가시성을 모두 보장
- 코드 block과 메서드에서 사용 가능
- 스레드 메모리와 메인 메모리 사이의 "모든" 변수 값을 동기화
- volatile보다 overhead 크다
- null 객체는 동기화 X(링크)
- null 객체를 동기화하고, 다른 곳에서 해당 객체를 사용하게 된다면 NPE발생
- lock을 obtain하고 release 한다(이유는 아래에서)
volatile
- "atomic"이 아닌 가시성만을 보장
- 필드에서만 사용 가능
- 스레드 메모리와 메인 메모리 사이의 volatile 변수 값을 동기화
- volatile 변수 null 가능
- lock을 obtain하거나 release 하지 않는다
synchronized는 lock을 obtain하고 release하지만 volatile은 필요하지 않을까요?
차이점에서 보다시피 volatile은 "atomic"을 보장하지 않는다
따라서 상호 배제를 보장하지 않는 volatile은 lock을 얻을 필요가 없다
volatile이 뭔가요?
- 클래스를 thread-safe 상태로 만들기 위함
- 변수의 가시성을 보장함(메서드, 클래스에 사용 불가)
- volatile로 선언된 변수를 메인 메모리에 저장한다는 의미
- read
- CPU cahce에서 read하지 않고 메인 메모리에서 읽는다
- write
- writer할 때마다 메인 메모리까지 도달하여 작성함
- read
- read 스레드는 write 스레드에 의해 volatile 값이 변하게 되면 읽는다(아래 코드에서 확인)
volatile이 왜 사용하나요?
- non-volatile은 성능 향상을 위해 각 스레드에 존재하는 CPU cache에 저장
- 멀티 스레드 환경에서 각 CPU cache에 저장돼있는 변수가 다를 수 있다
- 변수 값 불일치 문제 발생
- 위 문제를 해결하기 위해 "volatile" 사용
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을 사용하게 되면 이와 같은 문제를 방지할 수 있음
volatile를 사용 시 write thread가 왜 한개만 존재해야하나요?
- volatile은 atomic을 보장하지 않음
- 변경 사항을 다른 thread에게 알리기 전에 다른 스레드가 간섭하여 변경할 수 있음
- 단일 write thread, 여러 read thread에 유용
- synchronized은 volatile과 다르게 read & write 연산 atomic 보장한다
가시성 보장이 무슨 말인가요?
- thread에서 변경한 값이 메인 메모리에 저장되지 않아서 다른 thread가 이 값을 볼 수 없는 상황을 가시성 문제라고 한다
- 따라서 volatile를 사용하게 되면 메인 메모리에 저장되기 때문에 다른 thread가 현 thread에서 변경한 겂을 알 수 있다
non-volatile
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
*/
volatile(변수 값을 변경하지 않을 경우)
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가 실행되지 않는다
결론
read 스레드는 write 스레드에 의해 volatile 값이 변하게 되면 값을 read 한다
volatile(값을 변경할 경우)
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가 실행됨을 알 수 있다
volatile(read thread가 여러 개가 되면 안되는 이유)
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이 보장되지 않기 때문에 값이 일치하지 않게 된다
REFERENCES
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 |