学习互联网架构第四课(volatile关键字)
来源:互联网 发布:大学生程序员怎么赚钱 编辑:程序博客网 时间:2024/06/07 02:55
volatile概念:volatile关键字的主要作用是使变量在多个线程间可见。
在说volatile关键字之前,先来看两个小例子
package com.internet.thread;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){ RunThread t1 = new RunThread(); t1.setNum(10); t1.start(); try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();} RunThread t2 = new RunThread(); t2.setNum(20); t2.start(); } }运行结果如下,可以看到,两个线程操作的num完全没有关系,各自操作各自的。
010020假如我们在num前面加上static修饰
private static int num = 0;下面再运行main方法,结果如下,说明这时两个线程操作的是同一个变量num。
0101020但是多个线程同时访问同一个变量a的时候,就会出现线程问题,如下图所示。针对线程问题,我们可以采取给变量a加synchronized锁,这样无论多少个线程访问变量a都要一个一个的来,其中一个线程操作a的期间其它线程不能操作变量a,但是这样有个很大的问题就是并发太低。
下面我们再来看个例子,代码如下
package com.internet.thread;public class VolatileThread extends Thread{ private boolean isRunning = true; private void setRunning(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 InterruptedException{ VolatileThread vt = new VolatileThread(); vt.start(); Thread.sleep(3000); vt.setRunning(false); System.out.println("isRunning的值已经被设置了false"); Thread.sleep(1000); System.out.println(vt.isRunning); }}运行结果如下图所示,可以看到,虽然isRunning变量的值变成了false,但是while循环依然在执行,如下图所示。这显然不合理的。
那么,为什么我们把变量值isRunning变成false而while循环却不停止呢?这其实是JDK的设计造成的,如下图所示,JDK在设计线程的时候引入了线程工作内存的机制,变量在主内存中有一份isRunning变量,在线程工作内存中存了该变量的一个副本,线程在执行的时候判断isRunning变量值的时候是从线程工作内存中去获取的,当我们在主线程中设置isRunning的值为false时,主内存中的isRunning变量的值已经变成false了,但是线程工作内存中的isRunning副本的值还是true,因此我们才会看到while循环还在一直运行的原因。JDK这样做的目的是为了避免每次获取变量值都要去主内存获取,因为这样比较消耗性能。
那么,我们应该怎样解决这个问题呢?其实方案很简单,就是给isRunning加上volatile关键字修饰,然后重新运行main方法,这次发现while循环结束了。这才是正常的运行结果。
这时工作机制如下图所示。可以看到,当变量被volatile关键字修饰后,线程执行引擎就会去主内存中去读取变量值,同时主内存会把改变的变量值更新到线程工作内存当中。
用volatile关键字修饰变量虽然可以让变量在多个线程间可见,但是它并不具有原子性,我们来看下面一个例子,定义了一个addCount方法,调用一次count就加1000,如果count具有原子性的话,最后的结果应该是10000。
package com.internet.thread;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(); } }}我们运行上面的代码,结果如下,可以看到最后的结果是8839,并不是我们期望的10000,从而可以得出结论:用volatile关键字修饰的变量并不具有原子性。
2000400030002000500062406763683978398839那么,怎样才能让变量count具有原子性呢?我们可以使用AtomicInteger,如下图所示。
修改后,我们再运行下main方法,结果如下,虽然中间的过程不具有原子性,但是最终的结果一定是具有原子性的,这样做的好处是多个线程可以同时执行,中间过程可能有短暂的数据不一致,但是最终的结果一定是正确的。这样的例子也很常见,比如我们双11抢购商品,这么大的并发量,要说一下子就把所有数据都准确的统计出来是不可能的,因为并发量太大了,根本来不及统计,于是退而求其次,允许短暂的数据不一致,但是最终一定要做到数据准确、一致。
10002000416550004724629670008903900010000volatile关键字虽然拥有多个线程之间的可见性,但是却不具备同步性(也就是原子性),可以算上是一个轻量级的synchronized,性能要比synchronized强很多,不会造成阻塞(在很多开源的架构里,比如netty的底层代码就大量使用volatile,可见netty性能一定是非常不错的。)这里需要注意:一般volatile用于只针对于多个线程可见的变量操作,并不能代替synchronized的同步功能。实现原子性建议使用atomic类的系列对象,支持原子性操作(注意atomic类只保证本身方法原子性,并不保证多次操作的原子性)
下面我们便来举个例子来说明atomic类不保证多次操作原子性,代码如下(注意此时multiAdd方法前是没有synchronized修饰的)
package com.internet.thread;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() {@Overridepublic void run() {System.out.println(au.multiAdd());}})); } for(Thread t:ts){ t.start(); } }}我们运行main方法,结果如下所示,如果multiAdd具有原子性的话,那么应该是整10的增加,但是我们看到中间出现了诸如223、231这样的数字,说明atomic类确实不能保证多次操作的原子性(如果只写一个addAndGet方法的话,是支持原子性的,现在是4个,因此不支持方法的原子性了)。不过,虽然不能保证multiAdd方法的原子性,但是最终的结果是正确的,那就是1000,无论运行多少次,一定有1000,这说明最终是正确的。
1020304060607090801001101301201401501601701802002102002232502312402603002902802703103403303213503603803703904004104304204404504605205105004964704805305405515605705966106306065926506406206707006906806707407807607707507307307318008108008308308708708708708908909109009509509509509601000970990980如果我们要保证multiAdd方法的原子性的话,我们就给multiAdd方法添加synchronized关键字,如下图所示。
我们再运行main方法,运行结果如下(由于运行结果太长,我只截取了最后面一段),可以看到数字count确实是整10的增加的,直到1000。
8308408508608708808909009109209309409509609709809901000volatile关键字我们就学习到这里。
- 学习互联网架构第四课(volatile关键字)
- 学习互联网架构第二课(脏读)
- Volatile关键字的学习
- volatile关键字学习
- volatile关键字后续学习
- 深入学习volatile关键字
- 学习volatile关键字
- volatile关键字学习总结
- 学习互联网架构第一课(线程基础)
- 学习互联网架构第三课(synchronized重入锁)
- 学习互联网架构第七课(ThreadLocal的使用)
- 学习互联网架构第八课(单例和多线程)
- 学习互联网架构第九课(同步类容器)
- 学习互联网架构第十课(并发类容器)
- 学习C中volatile关键字
- java学习之关键字volatile
- java之volatile关键字学习
- 线程学习四:volatile 关键字
- 91.使用BigDecimal进行精确运算
- python opencv入门(学习笔记no.3)-从摄像头读取视频并显示
- Hibernate(下)
- Java Servlet API中文说明文档
- OGNL表达式
- 学习互联网架构第四课(volatile关键字)
- spark 内存管理
- intval()和int()
- 剑指Offer-37
- Struts2后台校验两种方式
- android 自定义MP4播放器
- 关闭和打开Drawerlayout侧拉手势滑动的方法
- 1 小时学会 MySQL 数据库
- jquery总结