synchronized 与volatile
2022-04-01 10:28 shadowsky88 阅读(31) 评论(0) 编辑 收藏 举报关键字synchronized的主要作用是保证同一时刻,只有一个线程可以执行某一个方法,或是某一个代码块,synchronized可以修饰方法及代码块。随着JDK的版本升级,synchronized关键字在执行效率上得到很大提升。它包含三个特征。
1)可见性:synchronized具有可见性。
2)原子性:使用synchronized实现了同步,同步实现了原子性,保证被同步的代码段在同一时间只有一个线程在执行。
3)禁止代码重排序:synchronized禁止代码重排序。
关键字volatile的主要作用是让其他线程可以看到最新的值,volatile只能修饰变量。它包含三个特征:
1)可见性:B线程能马上看到A线程更改的数据。
2)原子性:在32位系统中,针对未使用volatile声明的long或double数据类型没有实现写原子性,如果想实现,则声明变量时添加volatile,而在64位系统中,原子性取决于具
2.3.3 禁止代码重排序的测试
使用关键字volatile可以禁止代码重排序。
在Java程序运行时,JIT(Just-In-Time Compiler,即时编译器)可以动态地改变程序代码运行的顺序,例如,有如下代码:
A代码-重耗时 B代码-轻耗时 C代码-重耗时 D代码-轻耗时
在多线程的环境中,JIT有可能进行代码重排,重排序后的代码顺序有可能如下:
B代码-轻耗时 D代码-轻耗时 A代码-重耗时 C代码-重耗时
这样做的主要原因是CPU流水线是同时执行这4个指令的,那么轻耗时的代码在很大程度上先执行完,以让出CPU流水线资源给其他指令,所以代码重排序是为了追求更高的程序运行效率。
重排序发生在没有依赖关系时,例如,对于上面的A、B、C、D代码,B、C、D代码不依赖A代码的结果,C、D代码不依赖A、B代码的结果,D代码不依赖A、B、C代码的结果,这种情况下就会发生重排序,如果代码之间有依赖关系,则代码不会重排序。
使用关键字volatile可以禁止代码重排序,例如,有如下代码:
A变量的操作 B变量的操作 volatile Z变量的操作 C变量的操作 D变量的操作
那么会有4种情况发生:
1)A、B可以重排序。
2)C、D可以重排序。
3)A、B不可以重排到Z的后面。
4)C、D不可以重排到Z的前面。
换言之,变量Z是一个“屏障”,Z变量之前或之后的代码不可以跨越Z变量,这就是屏障的作用,关键字synchronized具有同样的特性。
1.实现代码重排序的测试
虽然代码重排序后能提高程序的运行效率,但在有逻辑性的程序中就容易出现一些错误,下面通过示例验证一下。
创建测试用的项目reorderTest。
创建类代码如下:
package test; public class Test1 { private static long x = 0; private static long y = 0; private static long a = 0; private static long b = 0; private static long c = 0; private static long d = 0; private static long e = 0; private static long f = 0; private static long count = 0; public static void main(String[] args) throws InterruptedException { for (;;) { x = 0; y = 0; a = 0; b = 0; c = 0; d = 0; e = 0; f = 0; count++; Thread t1 = new Thread(new Runnable() { public void run() { a = 1; c = 101; d = 102; x = b; } }); Thread t2 = new Thread(new Runnable() { public void run() { b = 1; e = 201; f = 202; y = a; } }); t1.start(); t2.start(); t1.join(); t2.join(); String showString = "count=" + count + " " + x + "," + y + ""; if (x == 0 && y == 0) { System.err.println(showString); break; } else { System.out.println(showString); } } } }
程序运行后控制台输出最后的部分结果如下:
count=119630 0,1 count=119631 0,1 count=119632 0,1 count=119633 0,1 count=119634 0,1 count=119635 0,0
程序输出x和y都是0,这时就出现了代码重排序,重排后的顺序如下:
x = b; a = 1; c = 101; d = 102;
和
y = a; b = 1; e = 201; f = 202;
结果是x和y均为0。
2.关键字volatile之前的代码可以重排
下面通过示例验证volatile“之前”的代码可以出现代码重排的效果,重排后的顺序如下:
x = b; a = 1; c = 101; d = 102;
和
y = a; b = 1; e = 201; f = 202;
结果是x和y均为0。
程序代码如下:
package test; public class Test2 { private static long x = 0; private static long y = 0; private static long a = 0; private static long b = 0; private static long c = 0; volatile private static long d = 0; private static long e = 0; volatile private static long f = 0; private static long count = 0; public static void main(String[] args) throws InterruptedException { for (;;) { x = 0; y = 0; a = 0; b = 0; c = 0; d = 0; e = 0; f = 0; count++; Thread t1 = new Thread(new Runnable() { public void run() { a = 1; c = 101; x = b; d = 102; } }); Thread t2 = new Thread(new Runnable() { public void run() { b = 1; e = 201; y = a; f = 202; } }); t1.start(); t2.start(); t1.join(); t2.join(); String showString = "count=" + count + " " + x + "," + y + ""; if (x == 0 && y == 0) { System.err.println(showString); break; } else { System.out.println(showString); } } } }
程序运行结果如下:
count=33853 0,1 count=33854 0,1 count=33855 0,1 count=33856 0,1 count=33857 0,1 count=33858 0,0
关键字volatile之前的代码发生了重排。
3.关键字volatile之后的代码可以重排
下面通过示例验证volatile之后的代码可以出现代码重排的效果,重排后的顺序如下:
c = 101; x = b; a = 1; d = 102;
和
e = 201; y = a; b = 1; f = 202;
结果是x和y均为0。
程序代码如下:
package test; public class Test3 { private static long x = 0; private static long y = 0; private static long a = 0; private static long b = 0; volatile private static long c = 0; private static long d = 0; volatile private static long e = 0; private static long f = 0; private static long count = 0; public static void main(String[] args) throws InterruptedException { for (;;) { x = 0; y = 0; a = 0; b = 0; c = 0; d = 0; e = 0; f = 0; count++; Thread t1 = new Thread(new Runnable() { public void run() { c = 101; a = 1; d = 102; x = b; } }); Thread t2 = new Thread(new Runnable() { public void run() { e = 201; b = 1; f = 202; y = a; } }); t1.start(); t2.start(); t1.join(); t2.join(); String showString = "count=" + count + " " + x + "," + y + ""; if (x == 0 && y == 0) { System.err.println(showString); break; } else { System.out.println(showString); } } } }
程序运行结果如下:
count=20898 0,1 count=20899 0,1 count=20900 0,1 count=20901 0,1 count=20902 0,0
关键字volatile之后的代码发生了重排。
4.关键字volatile之前的代码不可以重排到volatile之后
下面通过示例验证volatile之前的代码不可以重排到volatile之后,所以x和y同时不为0的情况不会发生。
程序代码如下:
package test; public class Test4 { private static long x = 0; private static long y = 0; private static long a = 0; private static long b = 0; volatile private static long c = 0; volatile private static long d = 0; private static long count = 0; public static void main(String[] args) throws InterruptedException { for (;;) { x = 0; y = 0; a = 0; b = 0; c = 0; d = 0; count++; Thread t1 = new Thread(new Runnable() { public void run() { x = b; c = 101; a = 1; } }); Thread t2 = new Thread(new Runnable() { public void run() { y = a; d = 201; b = 1; } }); t1.start(); t2.start(); t1.join(); t2.join(); String showString = "count=" + count + " " + x + "," + y + ""; if (x != 0 && y != 0) { System.err.println(showString); break; } else { System.out.println(showString); } } } }
程序运行后,x和y同时不为0的情况不会发生,所以关键字volatile之前的代码不可以重排到volatile之后。
5.关键字volatile之后的代码不可以重排到volatile之前
下面通过示例验证volatile之后的代码不可以重排到volatile之前,所以x和y的值永远不可能同时为0。
程序代码如下:
package test; public class Test5 { private static long x = 0; private static long y = 0; private static long a = 0; private static long b = 0; volatile private static long c = 0; volatile private static long d = 0; private static long count = 0; public static void main(String[] args) throws InterruptedException { for (;;) { x = 0; y = 0; a = 0; b = 0; c = 0; d = 0; count++; Thread t1 = new Thread(new Runnable() { public void run() { a = 1; c = 101; x = b; } }); Thread t2 = new Thread(new Runnable() { public void run() { b = 1; d = 201; y = a; } }); t1.start(); t2.start(); t1.join(); t2.join(); String showString = "count=" + count + " " + x + "," + y + ""; if (x == 0 && y == 0) { System.err.println(showString); break; } else { System.out.println(showString); } } } }
程序运行后,x和y的值永远不可能同时为0,所以关键字volatile之后的代码不可以重排到volatile之前。
6.关键字synchronized之前的代码不可以重排到synchronized之后
下面通过示例验证synchronized之前的代码不可以重排到synchronized之后,所以x和y同时不为0的情况不会发生。
程序代码如下:
package test; public class Test6 { private static long x = 0; private static long y = 0; private static long a = 0; private static long b = 0; private static long count = 0; public static void main(String[] args) throws InterruptedException { for (;;) { x = 0; y = 0; a = 0; b = 0; count++; Thread t1 = new Thread(new Runnable() { public void run() { x = b; synchronized (this) { } a = 1; } }); Thread t2 = new Thread(new Runnable() { public void run() { y = a; synchronized (this) { } b = 1; } }); t1.start(); t2.start(); t1.join(); t2.join(); String showString = "count=" + count + " " + x + "," + y + ""; if (x != 0 && y != 0) { System.err.println(showString); break; } else { System.out.println(showString); } } } }
程序运行后,x和y同时不为0的情况不会发生,所以关键字synchronized之前的代码不可以重排到synchronized之后。
7.关键字synchronized之后的代码不可以重排到synchronized之前
下面通过示例验证synchronized之后的代码不可以重排到synchronized之前,所以x和y的值永远不可能同时为0。
程序代码如下:
package test; public class Test7 { private static long x = 0; private static long y = 0; private static long a = 0; private static long b = 0; private static long count = 0; public static void main(String[] args) throws InterruptedException { for (;;) { x = 0; y = 0; a = 0; b = 0; count++; Thread t1 = new Thread(new Runnable() { public void run() { a = 1; synchronized (this) { } x = b; } }); Thread t2 = new Thread(new Runnable() { public void run() { b = 1; synchronized (this) { } y = a; } }); t1.start(); t2.start(); t1.join(); t2.join(); String showString = "count=" + count + " " + x + "," + y + ""; if (x == 0 && y == 0) { System.err.println(showString); break; } else { System.out.println(showString); } } } }
程序运行后,x和y的值永远不可能同时为0,所以关键字synchronized之后的代码不可以重排到synchronized之前。
8.总结
关键字synchronized的主要作用是保证同一时刻,只有一个线程可以执行某一个方法,或是某一个代码块,synchronized可以修饰方法及代码块。随着JDK的版本升级,synchronized关键字在执行效率上得到很大提升。它包含三个特征。
1)可见性:synchronized具有可见性。
2)原子性:使用synchronized实现了同步,同步实现了原子性,保证被同步的代码段在同一时间只有一个线程在执行。
3)禁止代码重排序:synchronized禁止代码重排序。
关键字volatile的主要作用是让其他线程可以看到最新的值,volatile只能修饰变量。它包含三个特征:
1)可见性:B线程能马上看到A线程更改的数据。
2)原子性:在32位系统中,针对未使用volatile声明的long或double数据类型没有实现写原子性,如果想实现,则声明变量时添加volatile,而在64位系统中,原子性取决于具体的实现,在X86架构64位JDK版本中,写double或long是原子的。另外,针对用volatile声明的int i变量进行i++操作时是非原子的。
3)禁止代码重排序。
学习多线程与并发,要着重“外炼互斥,内修可见,内功有序”,这是掌握多线程、学习多线程和并发技术的重要知识点。
关键字volatile和synchronized的使用场景总结如下:
1)当想实现一个变量的值被更改时,让其他线程能取到最新的值时,就要对变量使用volatile。
2)当多个线程对同一个对象中的同一个实例变量进行操作时,为了避免出现非线程安全问题,就要使用synchronized。