异同
volatile:重点在于告诉JVM被标记变量在线程的私有工作内存中的值是不确定的,每次都需要从主存中读取。
synchronized:对某一对象上锁,被保护的代码块无法并发执行。
二者都使用了内存屏障,保证“读之前”或“写之后”所有的CPU操作都同步刷新到主存中。
实验
该测试在windows10,JDK1.8.0_271下执行:
volatile的错误用法
首先测试 volatile标记的整型变量在两个线程下做累加操作:
public class Test {
volatile static long a = 0;
public static void main(String[] args) {
Runnable r = () -> {
for (int i = 0; i < 100_000_000; i++) {
a++;
}
};
Thread t1 = new Thread(r);
Thread t2 = new Thread(r);
long start = System.currentTimeMillis();
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println(a);
System.out.println(end - start);
}
}
volatile标记变量测试结果
因为volatile只能保证原子操作的原子性,a++实际上为复合操作,包括读取,相加,写入三部分,因此并没有得到正确的预期结果。两个线程交替进行a++操作,进行了大量的线程内存和主存的同步操作和数据拷贝,因此花费时间很长。
synchronized的正确用法
使用synchronized对当前类对象上锁测试累加结果:
public class Test {
static long a = 0;
public static void main(String[] args) {
Runnable r = () -> {
synchronized (Test.class) {
for (int i = 0; i < 100_000_000; i++) {
a++;
}
}
};
Thread t1 = new Thread(r);
Thread t2 = new Thread(r);
long start = System.currentTimeMillis();
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println(a);
System.out.println(end - start);
}
}
synchronized保护代码块测试结果
可见synchronized获取了正确的实验结果且时间明显变短(因为同步操作只在synchronized加锁前后进行,且同一线程可能多次连续加锁成功,因此数据在线程工作内存和主存之间的拷贝次数明显减少,因而花费时间较短)。
volatile的正确用法
import java.util.concurrent.TimeUnit;
public class Test {
// volatile static boolean a = true;
static boolean a = true;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while (a) {}
});
Thread t2 = new Thread(() -> {
a = false;
});
t1.start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
}
}
a未加volatile修饰,该程序在线程t2中修改了a的值,无法同步到线程t1的工作内存中,导致t1无法终止,程序无法结束。如果在a的前面加上volatile修饰,则可以将t2线程修改的值刷新到主存,同时t1能读取到值的修改,程序能正常结束。
网友评论