第四章:java 多线程volatile关键字 atomic类学习 java 原子性讲解,变量可见与不可见说明

第四章:java 多线程volatile关键字 atomic类学习 java 原子性讲解,变量可见与不可见说明第四章:java 多线程volatile关键字 atomic类学习 java 原子性讲解,变量可见与不可见说明

大家好,又见面了,我是你们的朋友全栈君。

  volatile概念:volatile关键字的主要作用是使变量在多个线程间可见。

       在说volatile关键字之前,先来看两个小例子:

package com.xiaoyexinxin.ThreadLearn;

public class RunThread extends Thread{
	private int num=0;
	public void setNum(int num){
		System.out.println(this.num);
		this.num=num;
	}
	public void run() {
		System.out.println(num);	
	}
	
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		RunThread r=new RunThread();
		r.setNum(10);
		r.start();
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
		RunThread r2=new RunThread();
		r2.setNum(20);
		r2.start();
	}

}

结果:

0
10
0
20

      运行结果如下,可以看到,两个线程操作的num完全没有关系,各自操作各自的。

 假如我们在num前面加上static修饰

private static int num=0;


运行的结果是:

0
10
10
20

说明两个线程操作的是同一个变量;

可以看出多个线程访问同一个变量时会有线程问题,当然我们可以使用synchronized锁,这样无论多少线程访问num变量都要一个一个执行,这样就很慢。

我们来看另外一个例子:

public class VolatileThread extends Thread{
	
	private boolean isRunning=true;
	
	private void setIsRuning(boolean isRunning){
			this.isRunning=isRunning;
	}
	
	public void run(){
		System.out.println("进入run方法。。。");
		while(isRunning==true){
			
		}
		System.out.println("线程停止");
	}

	public static void main(String[] args) throws Exception{
		VolatileThread vt=new VolatileThread();
		vt.start();
			Thread.sleep(1000);
		vt.setIsRuning(false);
		System.out.println("isRunning的值已经是false");
		Thread.sleep(2000);
		System.err.println(vt.isRunning);
	}
}

这个执行结果是:

进入run方法。。。
isRunning的值已经是false
false

可见没有打印线程停止,而且线程也没停止,陷入了死循环。可是我们后面明明把变量设置成了false,为啥还在执行呢?

这是JDK的设计造成的,JDK在设计线程的时候引入了线程工作内存机制,变量在主内存中有一份isRunning变量,在线程工作内存中存了改变量的一个副本,线程在执行的时候判断isRunning变量值的时候是从线程工作内存中去获取的,当我们在主线程中设置isRunning的值为false时,主内存中的isRunning变量的值已经变成false了,但是线程工作内存中的isRunning副本的值还是true,因此我们才会看到while循环还在一直运行的原因。JDK这样做的目的是为了避免每次获取变量值都要去主内存获取,因为这样比较消耗性能。


那么,我们应该怎样解决这个问题呢?其实方案很简单,就是给isRunning加上volatile关键字修饰,然后重新运行main方法,这次发现while循环结束了。这才是正常的运行结果。

private volatile boolean isRunning=true;


结果:

进入run方法。。。
isRunning的值已经是false
线程停止
false

 这时工作机制如下图所示。可以看到,当变量被volatile关键字修饰后,线程执行引擎就会去主内存中去读取变量值,同时主内存会把改变的变量值更新到线程工作内存当中。

package com.xiaoyexinxin.ThreadLearn;

public class VolatileNoAtomic extends Thread{  
    private static volatile int count;  
    private static void addCount(){  
        for(int i=0;i<1000;i++){  
            count++;  
        }  
        System.out.println(count);  
    }  
      
    public void run(){  
        addCount();  
    }  
      
    public static void main(String[] args){  
        VolatileNoAtomic[] arr = new VolatileNoAtomic[10];  
        for(int i=0;i<10;i++){  
            arr[i] = new VolatileNoAtomic();  
        }  
        for(int i=0;i<10;i++){  
            arr[i].start();  
        }  
    }  
}


   用volatile关键字修饰变量虽然可以让变量在多个线程间可见,但是它并不具有原子性,我们来看下面一个例子,定义了一个addCount方法,调用一次count就加1000,如果count具有原子性的话,最后的结果应该是10000。

 那么,怎样才能让变量count具有原子性呢?我们可以使用AtomicInteger,如下图所示。

package com.xiaoyexinxin.ThreadLearn;

import java.util.concurrent.atomic.AtomicInteger;

public class VolatileNoAtomic extends Thread{  
    private static AtomicInteger count=new AtomicInteger(0);  
    private static void addCount(){  
        for(int i=0;i<1000;i++){  
            count.incrementAndGet();  
        }  
        System.out.println(count);  
    }  
      
    public void run(){  
        addCount();  
    }  
      
    public static void main(String[] args){  
        VolatileNoAtomic[] arr = new VolatileNoAtomic[10];  
        for(int i=0;i<10;i++){  
            arr[i] = new VolatileNoAtomic();  
        }  
        for(int i=0;i<10;i++){  
            arr[i].start();  
        }  
    }  
}

   修改后,我们再运行下main方法,结果如下,虽然中间的过程不具有原子性,但是最终的结果一定是具有原子性的,这样做的好处是多个线程可以同时执行,中间过程可能有短暂的数据不一致,但是最终的结果一定是正确的。这样的例子也很常见,比如我们双11抢购商品,这么大的并发量,要说一下子就把所有数据都准确的统计出来是不可能的,因为并发量太大了,根本来不及统计,于是退而求其次,允许短暂的数据不一致,但是最终一定要做到数据准确、一致。


volatile关键字虽然拥有多个线程之间的可见性,但是却不具备同步性(也就是原子性),可以算上是一个轻量级的synchronized,性能要比synchronized强很多,不会造成阻塞(在很多开源的架构里,比如netty的底层代码就大量使用volatile,可见netty性能一定是非常不错的。)这里需要注意:一般volatile用于只针对于多个线程可见的变量操作,并不能代替synchronized的同步功能。实现原子性建议使用atomic类的系列对象,支持原子性操作(注意atomic类只保证本身方法原子性,并不保证多次操作的原子性)


原子性:

    刚才说了那么多次原子性,这里也说一下java的原子性是什么:

原子性是拒绝多线程操作的,不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作。简而言之,在整个操作过程中不会被线程调度器中断的操作,都可认为是原子性。例如 a=1是原子性操作,但是a++和a +=1就不是原子性操作。

因为:

(1)也就是整个过程中会出现线程调度器中断操作的现象,例如:

类似”a += b”这样的操作不具有原子性,在某些JVM中”a += b”可能要经过这样三个步骤:

(1)取出a和b

(2)计算a+b

(3)将计算结果写入内存

如果有两个线程t1,t2在进行这样的操作。t1在第二步做完之后还没来得及把数据写回内存就被线程调度器中断了,于是t2开始执行,t2执行完毕后t1又把没有完成的第三步做完。这个时候就出现了错误,相当于t2的计算结果被无视掉了。所以上面的例子在同步add方法之前,实际结果总是小于预期结果的,因为很多操作都被无视掉了。

类似的,像”a++”这样的操作也都不具有原子性。所以在多线程的环境下一定要记得进行同步操作。

可见性

    valotile修饰词是具有可见性的,也是上面我们提到过的,

多线程变量不可见:当一个线程对一变量a修改后,还没有来得及将修改后的a值回写到主存,而被线程调度器中断操作(或收回时间片),然后让另一线程进行对a变量的访问修改,这时候,后来的线程并不知道a值已经修改过,它使用的仍旧是修改之前的a值,这样修改后的a值就被另一线程覆盖掉了。

多线程变量可见:被volatile修饰的成员变量在每次被线程访问时,都强迫从内存中重读该成员变量的值;而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存,这样在任何时刻两个不同线程总是看到某一成员变量的同一个值,这就是保证了可见性。

volatile使用场景:在两个或者更多的线程访问的成员变量上使用volatile。当要访问的变量已在synchronized代码块中,或者为常量时,不必使用。
由于使用volatile屏蔽掉了JVM中必要的代码优化,所以在效率上比较低,因此一定在必要时才使用此关键字。

       下面我们便来举个例子来说明atomic类不保证多次操作原子性,代码如下(注意此时multiAdd方法前是没有synchronized修饰的)


package com.xiaoyexinxin.ThreadLearn;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

public class AtomicUse {  
    private static AtomicInteger count = new AtomicInteger(0);  
    //多个addAndGet在一个方法内是非原子性的,需要加synchronized进行修饰,保证4个  
    //addAndGet整体原子性  
    public  int multiAdd(){  
        try {  
            Thread.sleep(100);  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
        count.addAndGet(1);  
        count.addAndGet(2);  
        count.addAndGet(3);  
        count.addAndGet(4);//1+2+3+4=10,也就是说,执行一次multiAdd方法,count就加10  
        return count.get();  
    }  
      
    public static void main(String[] args){  
        final AtomicUse au = new AtomicUse();  
        List<Thread> ts = new ArrayList<Thread>();  
        for(int i=0;i<100;i++){  
            ts.add(new Thread(new Runnable() {  
                  
                public void run() {  
                    System.out.println(au.multiAdd());  
                }  
            }));  
        }  
        for(Thread t:ts){  
            t.start();  
        }  
    }  
}

   我们运行main方法,结果如下所示,如果multiAdd具有原子性的话,那么应该是整10的增加,但是我们看到中间出现了诸如223、231这样的数字,说明atomic类确实不能保证多次操作的原子性(如果只写一个addAndGet方法的话,是支持原子性的,现在是4个,因此不支持方法的原子性了)。不过,虽然不能保证multiAdd方法的原子性,但是最终的结果是正确的,那就是1000,无论运行多少次,一定有1000,这说明最终是正确的。

   如果我们要保证multiAdd方法的原子性的话,我们就给multiAdd方法添加synchronized关键字,如下图所示。

 public synchronized int multiAdd(){  

我们再运行main方法,运行结果如下(由于运行结果太长,我只截取了最后面一段),可以看到数字count确实是整10的增加的,直到1000。,不过这样明细满了许多。


版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/106148.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)


相关推荐

  • 如何实现动静分离

    如何实现动静分离一、动静分离我们的网站简单来说分为2种数据资源,一种是动态的数据,即PHP等程序语言实时吐出来的数据,在网页内容上主要是HTML代码,另一种则是静态资源,比如图片、css、js、视频等(当然,图片等资源也可能是实时动态生成的,比如PHP缩略图,这里就不展开讨论了)。一般网站初建,因为流量小、业务简单等原因,都默认将两种数据放到一台服务器上提供服务。访问量大到一定程度之后,就…

  • BIOS 和UEFI的区别

    BIOS 和UEFI的区别BIOS 和UEFI的区别

  • pycharm配置环境及安装第三方库_pycharm怎么配置

    pycharm配置环境及安装第三方库_pycharm怎么配置下载git.exe(官网太慢,可去第三方)安装选择自己的路径在pycharm里添加git(具体路径为你安装的路径下的Git/cmd/git.exe)ok

  • pycharm远程调试「建议收藏」

    pycharm远程调试「建议收藏」我的博客链接Remote篇——PyCharm远程运行、调试环境配置一般在本地无法调试远程端代码,机械性的scp传输文件十分影响工作效率,PyCharm的Pro支持远程Run,Debug,等可视化的功能。操作系统:本地MacOS,远程Linux(本地3个操作系统都是支持的,远程Linux比较稳定)IDE:最新版本PyCharmPro(不支持社区版)python虚拟环境:Anaconda,pip远程创建新项目首先在远程服务器上新建一个项目文件$mkdirYOUR-PROJEC

  • SOP是什么?SOP的作用是什么?如何编写SOP?

    SOP是什么?SOP的作用是什么?如何编写SOP?SOP是由StandardOperationProcedure这三个英文单词的首个字母组合而成。也就是以统一化的标准将操作流程的步骤和要求罗列出来,用于指导和规范日常工作。SOP的核心,就是把特定流程的关键问题细化及量化。SOP是以文件的方式归纳总结操作人员在实际生产过程中的具体操作步骤和应当要注意的事项,它是车间现场操作人员的作业指导模板,也是质量检验人员用于检测指导工作的依据。SOP的作用:1、把企业长期累积的经验技术记录归纳,汇总成简单易懂的标准化文件,即使出现操作人员变动也不会使已有的技

  • html如何设置ie6兼容性视图,IE6浏览器兼容性视图设置在哪里[通俗易懂]

    html如何设置ie6兼容性视图,IE6浏览器兼容性视图设置在哪里[通俗易懂]ie6浏览器算是旧版本了,如果你想要设置兼容性视图,该怎么设置呢?下面由学习啦小编为大家整理了IE6浏览器的兼容性视图设置在哪里的方法,希望对大家有帮助!IE6浏览器兼容性视图设置在哪里IE6兼容性视图设置的方法和步骤如下打开电脑后,在开始菜单中,选种【所有程序】,在程序列表中,会看到InternetExplorer浏览器,显示的WIN7操作系统的操作图,如图点击IE浏览器,打开浏览器后,默认登…

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号