学习互联网架构第四课(volatile关键字)

       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完全没有关系,各自操作各自的。

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

private static int num = 0;
       下面再运行main方法,结果如下,说明这时两个线程操作的是同一个变量num。

0
10
10
20
        但是多个线程同时访问同一个变量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关键字修饰的变量并不具有原子性。

2000
4000
3000
2000
5000
6240
6763
6839
7839
8839
        那么,怎样才能让变量count具有原子性呢?我们可以使用AtomicInteger,如下图所示。


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

1000
2000
4165
5000
4724
6296
7000
8903
9000
10000
        volatile关键字虽然拥有多个线程之间的可见性,但是却不具备同步性(也就是原子性),可以算上是一个轻量级的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() {
				
				@Override
				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,这说明最终是正确的。

10
20
30
40
60
60
70
90
80
100
110
130
120
140
150
160
170
180
200
210
200
223
250
231
240
260
300
290
280
270
310
340
330
321
350
360
380
370
390
400
410
430
420
440
450
460
520
510
500
496
470
480
530
540
551
560
570
596
610
630
606
592
650
640
620
670
700
690
680
670
740
780
760
770
750
730
730
731
800
810
800
830
830
870
870
870
870
890
890
910
900
950
950
950
950
960
1000
970
990
980
       如果我们要保证multiAdd方法的原子性的话,我们就给multiAdd方法添加synchronized关键字,如下图所示。


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

830
840
850
860
870
880
890
900
910
920
930
940
950
960
970
980
990
1000
         volatile关键字我们就学习到这里。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值