赞
踩
volatile关键字在使用上有3个特性:
1、可见性:B线程能马上看到A线程更改的数据。
2、原子性:volatile原子性体现在赋值原子性,在32位JDK中对64位数据类型执行赋值操作时会写两次,高32位和低32位,如果写两次的这组操作被打断,导致写入的数据被其他线程的写操作所覆盖,获得错误的结果,就是非原子性的。如果写两次的操作是连续的,不允许被打断,就是原子性的。在32位JDK中针对未使用volatile声明的long或double64位数据类型没有实现写原子性,如果想实现,需要在声明变量时添加volatile。而在64位JDK中,是否具有原子性取决于具体的实现,在X86架构64位JDK版本中,写double或long是原子性的。针对用volatile声明的int变量进行i++操作时是非原子性的。这些会在下面的内容验证。
3、禁止代码重排序。
volatile关键字具有可见性,可以提高软件(系统)的灵敏度。具体测试过程如下:
- public class PrintString {
- private boolean isContinuePrint = true;
-
- public boolean isContinuePrint() {
- return isContinuePrint;
- }
-
- public void setContinuePrint(boolean continuePrint) {
- isContinuePrint = continuePrint;
- }
-
- public void printStringMethod(){
- try {
- while(isContinuePrint = true){
- System.out.println("运行 printStringMethod()方法的线程是:" + Thread.currentThread().getName());
- Thread.sleep(1000);
- }
- }catch (InterruptedException e){
- e.printStackTrace();
- }
- }

- public class Run1 {
- public static void main(String[] args) {
- PrintString printString = new PrintString();
- printString.printStringMethod();
- System.out.println("我要停止它。停止的线程是:" + Thread.currentThread().getName());
- printString.setContinuePrint(false);
- }
- }

出现死循环的原因是main线程一直处理while循环,导致程序不能继续执行后面的代码,解决办法是使用多线程。
- public class PrintString implements Runnable{
- private boolean isContinuePrint = true;
-
- public boolean isContinuePrint() {
- return isContinuePrint;
- }
-
- public void setContinuePrint(boolean continuePrint) {
- isContinuePrint = continuePrint;
- }
-
- public void printStringMethod(){
- try {
- while(isContinuePrint == true){
- System.out.println("运行 printStringMethod()方法的线程是:" + Thread.currentThread().getName());
- Thread.sleep(100);
- }
- }catch (InterruptedException e){
- e.printStackTrace();
- }
- }
-
- @Override
- public void run() {
- printStringMethod();
- }
- }

- public class Run1 {
-
- public static void main(String[] args) throws InterruptedException {
- PrintString printString = new PrintString();
- new Thread(printString).start();
- System.out.println("我要停止它。停止的线程是:" + Thread.currentThread().getName());
- printString.setContinuePrint(false);
- }
- }

- public class RunThread extends Thread{
- private boolean isRunnint = true;
-
- public boolean isRunnint() {
- return isRunnint;
- }
-
- public void setRunnint(boolean runnint) {
- isRunnint = runnint;
- }
-
- @Override
- public void run(){
- System.out.println("进入run方法");
- while (isRunnint == true){
-
- }
- System.out.println("线程被停止了");
- }
- }

- public class Run2 {
- public static void main(String[] args) {
- try {
- RunThread thread = new RunThread();
- thread.start();
- Thread.sleep(1000);
- thread.setRunnint(false);
- System.out.println("已经赋值为false了");
- }catch (InterruptedException e){
- e.printStackTrace();
- }
- }
- }

是什么原因导致死循环呢?在启动线程时,因为变量private boolean isRunning = tue;分别存储在公共内存和线程的私有内存中,线程运行后再线程的私有内存中取得isRunning的值一直是true,而代码 thread.setRunning(false) 虽然被执行了,却是把公共内存中的isRunning变量改成false,操作的是两块内存地址中的数据,所以一直处于死循环的状态。内存结果如下图:
这个问题其实就是私有内存中的值与公共内存中的值不同导致的,可以通过使用volatile关键字来解决,volatile的主要作用就是当线程访问isRunning变量时,强制地从公共内存中取值。修改RunThread.java文件
- public class RunThread extends Thread{
- volatile private boolean isRunnint = true;
-
- public boolean isRunnint() {
- return isRunnint;
- }
-
- public void setRunnint(boolean runnint) {
- isRunnint = runnint;
- }
-
- @Override
- public void run(){
- System.out.println("进入run方法");
- while (isRunnint == true){
-
- }
- System.out.println("线程被停止了");
- }
- }


通过使用volatile关键字,强制地从公共内存读取变量的值,在同步到线程的私有内存中,内存结结构如下图。并且使线程被正确的终止了,这种方式就是之前提到的一种停止线程的方法:使用退出标志使线程正常退出。

总结:使用volatile关键字是增加了实例变量在多个线程之间的可见性。
synchronzied关键字可以使多个线程访问同一个资源时,具有可见性,也可以使线程私有内存中的变量与公共内存中的变量同步,也就是可见性,下面对其进行验证。
- public class Service {
- private boolean isContinue = true;
- public void runMethod(){
- while(isContinue == true){
-
- }
- System.out.println("停下来了");
- }
- public void stopMethod(){
- isContinue = false;
- }
- }
- public class ThreadA extends Thread{
- private Service service;
-
- public ThreadA(Service service) {
- this.service = service;
- }
-
- @Override
- public void run(){
- service.runMethod();
- }
- }
- public class ThreadB extends Thread{
- private Service service;
-
- public ThreadB(Service service) {
- this.service = service;
- }
- @Override
- public void run(){
- service.stopMethod();
- }
- }
- public class Run3 {
- public static void main(String[] args) {
- try {
- Service service = new Service();
- ThreadA a = new ThreadA(service);
- a.start();
- Thread.sleep(1000);
- ThreadB b = new ThreadB(service);
- b.start();
- System.out.println("已经发起停止的命令了");
- }catch (InterruptedException e){
- e.printStackTrace();
- }
- }
- }

运行时出现死循环。原因是各个线程间的数据值没有可见性造成的,而synchronzied关键字可以使数据具有可见性,修改Service.java代码:
- public class Service {
- private boolean isContinue = true;
- public void runMethod(){
- String s = new String();
- while(isContinue == true){
- synchronized (s){
-
- }
- }
- System.out.println("停下来了");
- }
- public void stopMethod(){
- isContinue = false;
- }
- }

synchronzied关键字会把私有内存中的数据同公共内存同步,使私有内存中的数据和公共内存中的数据一致。
在32位JDK中针对未使用volatile声明的long或double的64位数据类型没有实现赋值与原子性,如果想实现,声明变量时添加volatile。如果在64位JDK中,是否原子取决于具体的实现,在X86架构64位JDK版本中,写double或long是原子的。
另外,volatile最致命的缺点是不支持运算原子性,也就是多个线程对用volatile修饰的变量i执行 i ++/i --操作时,还是会被分成三步,造成非线程安全问题。
- public class MyThread extends Thread{
- volatile public static int count;
- private static void addCount(){
- for (int i = 0;i<100;i++){
- count ++;
- }
- System.out.println("count = " + count);
- }
- @Override
- public void run(){
- addCount();
- }
-
- }
- public class Run {
- public static void main(String[] args) {
- MyThread[] array = new MyThread[100];
- for (int i = 0; i < 100; i++) {
- array[i] = new MyThread();
- }
- for (int i = 0; i < 100; i++) {
- array[i].start();
- }
- }
- }

运行结果不是10000,说明在多线程环境下,volatile 修饰的变量的 ++运算是非原子性的。
修改 MyThread.java如下:
- public class MyThread extends Thread{
- volatile public static int count;
- synchronized private static void addCount(){
- for (int i = 0;i<100;i++){
- count ++;
- }
- System.out.println("count = " + count);
- }
- @Override
- public void run(){
- addCount();
- }
-
- }

在本示例中,如果在方法private static void addCount()前加入synchronzied同步关键字,就没有必要在使用volatile关键字来声明count变量了。
volatile关键字主要是在多个线程中可以感知实例变量被更改了,并且可以获得最新的值时使用,也就是增加可见性时使用。例如,在32位JDK中增加赋值操作的原子性。
volatile关键字提示线程每次从公共内存中区读取变量,而不是从私有内存中去读取,这样就保证了同步数据的可见性。但需要注意的是:如果修改实例变量中的数据,比如i++,则这样的操作其实并不是一个原子操作,也就是非线程安全的。表达式i++的操作步骤分解如下:
1、从内存中取出i的值。
2、计算i的值。
3、将i的值写到内存中。
假设在第二步计算的时候,另一个线程也修改i的值,那么结果就是错误的,解决办法是使用synchronzied关键字保证原子性。所以,volatile本身并不处理int i ++ 运算操作的原子性。
总结:volatile保证数据在线程之间的可见性,但不保证同步性,同时在32位JDK中保证赋值操作的原子性。
除了在i++操作时使用synchronzied关键字实现同步外,还可以使用AtomicInteger原子类实现。原子操作是不可分割的整体,没有其他线程能够中断或检查正在原子操作中的变量。一个原子(atomic)类型就是一个原子操作可用的类型,它可以在没有锁的情况下做到线程安全。
- public class AddCountThread extends Thread{
- private AtomicInteger count = new AtomicInteger(0);
- @Override
- public void run(){
- for (int i = 0; i < 10000; i++) {
- System.out.println(count.incrementAndGet());
- }
- }
- }
- public class Run1 {
- public static void main(String[] args) {
- AddCountThread addCountThread = new AddCountThread();
- Thread t1 = new Thread(addCountThread);
- t1.start();
- Thread t2 = new Thread(addCountThread);
- t2.start();
- Thread t3 = new Thread(addCountThread);
- t3.start();
- Thread t4 = new Thread(addCountThread);
- t4.start();
- Thread t5 = new Thread(addCountThread);
- t5.start();
- }
- }

成功累加到50000。
即使在有逻辑性的情况下,原子类的输出结果也具有随机性。
- public class MyService {
- public static AtomicLong aiRef = new AtomicLong();
- public void addNum(){
- System.out.println(Thread.currentThread().getName() + "加了100之后的值是:" + aiRef.addAndGet(100L));
- aiRef.addAndGet(1L);
-
- }
- }
- public class MyThread extends Thread{
- private MyService myService;
-
- public MyThread(MyService myService) {
- this.myService = myService;
- }
-
- @Override
- public void run(){
- myService.addNum();
- }
- }
- public class Run1 {
- public static void main(String[] args) {
- try {
- MyService service = new MyService();
- MyThread[] threads = new MyThread[5];
- for (int i = 0; i < threads.length; i++) {
- threads[i] = new MyThread(service);
- }
- for (int i = 0; i < threads.length; i++) {
- threads[i].start();
- }
- Thread.sleep(1000);
- System.out.println(service.aiRef.get());
- }catch (InterruptedException e){
- e.printStackTrace();
- }
-
-
- }
- }


执行结果如上图,输出顺序错了,应该每加1次100再次加1,出现这种情况的原因是addAndGet()方法是原子的,但方法和方法之间的调用确实非原子的,此时可以用同步解决该问题。更改MyService.java
- public class MyService {
- public static AtomicLong aiRef = new AtomicLong();
- synchronized public void addNum(){
- System.out.println(Thread.currentThread().getName() + "加了100之后的值是:" + aiRef.addAndGet(100L));
- aiRef.addAndGet(1L);
-
- }
- }

从运行结果可以看到,输出信息依次加100再加1,是正确的计算过程。
volatile关键字可以禁止代码重排序。什么是重排序?在Java程序运行时,JIT(即时编译器)为了优化程序的运行,可以动态地改变程序代码运行的顺序。比如有如下代码:
- A 代码 - 重耗时
- B 代码 - 轻耗时
- C 代码 - 重耗时
- D 代码 - 轻耗时
在多线程的环境中,JIT有可能进行代码重排,重排后的代码顺序可能如下:
- B 代码 - 轻耗时
- D 代码 - 轻耗时
- A 代码 - 重耗时
- C 代码 - 重耗时
这样做的主要原因是CPU流水线中这4个指令时同时执行的,轻耗时的代码在很大程度上会先执行完,以让出CPU流水线资源供其他指令使用,所以代码重排是为了追求更好的程序运行效率。
重排发生在没有依赖关系时,比如上面ABCD代码,相互之间没有依赖关系时就会发生重排,如果存在依赖则代码不会重排序。
而volatile关键字可以禁止代码重排序,比如下面这段代码:
- A 变量的操作
- B 变量的操作
- volatile Z 变量的操作
- C 变量的操作
- D 变量的操作
根据以上代码,会有下面2种情况
1、AB可以重排,但不能排到Z的后面
2、CD可以重排,但不能排到Z的前面
也就是说,变量Z是一道屏障,Z变量之前或之后的代码不能跨越Z。同样,synchronzied关键字也具有同样的特性。
总结:
synchronzied关键字的主要作用是保证同一时刻,只有一个线程可以执行某一个方法或者某一个代码块。synchronzied可以修改方法以及代码块,随着JDK版本的升级,synchronzied在执行效率上得到很大提升,它包含三个特征:可见性、原子性和禁止代码重排序。
volatile关键字的主要作用是让其他线程可以看到最新的值,volatile只能修改变量。同样也具有可见性、原子性和禁止代码重排序三个特征。
synchronzied和volatile使用场景:
1、当想实现一个变量的值被更改,而其他线程能取到最新的值时,就要对变量使用volatile。
2、如果多个线程对同一个对象中的同一个实例变量进行写操作,为了避免出现非线程安全问问题,就要使用synchronzied。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。