大家好,又见面了,我是你们的朋友全栈君。
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修饰的成员变量在每次被线程访问时,都强迫从内存中重读该成员变量的值;而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存,这样在任何时刻两个不同线程总是看到某一成员变量的同一个值,这就是保证了可见性。
下面我们便来举个例子来说明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账号...