java并发编程学习(七) volatile特性详解

简介

特性

可见性

验证

不具有原子性

验证

解决volatile复合操作不具有原子性

将复合操作使用同步锁控制,控制执行复合操作每个时刻只有一个线程

使用并发包中的Atomic类操作

禁止指令重排

有什么用?

注意


简介

volatile是Java虚拟机提供的轻量级锁,在一些场景上可以用来替换synchronized锁,而且效率更高,比如多个线程共享某个变量的值时候,要求一个线程修改这个值之后,立刻对其他线程可见

特性

可见性

一个线程修改了共享变量的值,对另一个线程是能够立刻看到共享变量的修改的

根据volatile的happen before 原则

如果一个volatile的写happen-before对一个volatile的读,那么对volatile的写操作的更改对后面的volatile的读操作是可见的 

验证

public class Service {
	private boolean flag =true ;
	public void test(){
		while(flag){
		}
		System.out.println("跳出while循环执行......");
	}
	public void cancel(){
		flag = false;
	}
	public static void main(String[] args) throws InterruptedException {
		final Service s = new Service();
		Thread t = new Thread(new Runnable() {
			@Override
			public void run() {
				s.test();
			}
		});
		t.start();
		Thread.sleep(1000);
		s.cancel();
		System.out.println("已经将flag赋值为false.....");
	}
}

配置:  代码在-server模式jvm运行,eclipse中设置-server模式:

运行结果: 线程t不会输出 跳出死循环这句。。。

原因:在主线程中,将flag改为false,线程t仍然是死循环中。因为对线程t而言,flag的修改它看不到

修改:将flag 加上 volatile关键字,再次运行代码

运行结果: 线程t结束死循环...

不具有原子性

原子性,是指一个操作不能被CPU中断。volatile线程不安全的地方,主要体现在对volatile修饰的变量进行复合操作是不具有原子性的,常见的例子就是i++问题。(当然,volatile的读或写操作是原子性)

i++ 操作在内存中其实三个操作:

  1. 从主内存中读取i到工作内存中
  2. 工作内存中对i自增后,赋值给一个临时变量
  3. 将临时变量刷新回主内存中

在多个线程,执行i++操作的时候,可能存在线程A读取i值,线程B随后读取一样的值,线程A自增后,将值写会到主内存中,之后,被线程B自增的值覆盖了,这就线程不安全了

验证

public class Service {
	private volatile int i = 1 ;
	public void incre(){
		for(int j =0;j<100;j++){
			this.i++;
		}
		System.out.println("i的值:"+i);
		
	}
	public int get(){
		return i;
	}
	public static void main(String[] args) throws InterruptedException {
		final Service service = new Service();
		for(int i=0;i<100;i++){
			Thread t = new Thread(new Runnable() {
				@Override
				public void run() {
					service.incre();
				}
			});
			t.start();
		}
	}
}

运行结果: 9990

解决volatile复合操作不具有原子性

将复合操作使用同步锁控制,控制执行复合操作每个时刻只有一个线程

public synchronized void incre(){
		for(int j =0;j<100;j++){
			this.i++;
		}
	}

使用并发包中的Atomic类操作

Atomic类操作复合运算的原理的核心,运用了CAS算法

举例来说,类似i++操作,使用AtomicInteger 类的incrementAndGet自增的话,就是不断调用comparseAndSet方法直到更新成功。

对于 compareAndSet方法,有三个参数,主内存的值,本地内存的值,期望更新的值,只有当主内存的值和本地内存的值一致时,才将期望值更新到主内存中,并且返回true,否则返回false。

public final int incrementAndGet() {
        for (;;) {
            int current = get();
            int next = current + 1;
            if (compareAndSet(current, next))
                return next;
        }
    }
 public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

禁止指令重排

jmm对于volatile变量,会禁止特定的编译器或者处理器重排序

  1. 当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个确保volatile写之前的操作都不能被重排序到volatile写后面
  2. 当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序
  3. 当第一个操作是volatile写,第二个是volatile读时,不能重排序

禁止重排序,是通过内存屏障来实现的,具体如下:

1. 每一个volatile写前面加上一个storeStore屏障

加上这个内存屏障可以保证:前面的所有普通写操作对任意处理器可见,将屏障前的普通写在volatile写之前,刷新到主内存中

2. 每一个volatile 写后面加上一个storeload屏障,禁止后面的volatile读写操作重排序

3. 每一个volatile 读后面加上一个loadLoad屏障, 禁止下面的所有的普通读和上面的volatile读重排序

4. 每一个volatile 读后面加上一个loadstore屏障,禁止下面的所有普通写和上面的volatile读重排序

有什么用?

以双重检查的单例模式为例

package cn.bing.singleton;
/**
 * 双重检查锁定延迟加载
 * @author Administrator
 *
 */
public class SingleTonDoubleLock {
	private static volatile SingleTonDoubleLock instance;
	private SingleTonDoubleLock() {}
	public static SingleTonDoubleLock getInstance() {
		if(instance==null) {//1
			synchronized (SingleTonDoubleLock.class) {//2
				if(instance==null)
				instance = new SingleTonDoubleLock();
			}
		}
		return instance;
	}
}

假设线程A和线程B调用getInstance方法,线程A获取到类锁,进入2,创建对象

对象的创建分为3步骤

  1. 给对象分配内存空间
  2. 对象初始化
  3. 将引用指向对象的内存空间

jvm可能对上面的指定进行重排序,线程B看到另一个线程的执行顺序都是乱序的,可能是1,3,2的顺序

此时,线程B执行到1,看到instance的地址不为空(由于重排序,可能对象还没有初始化),直接就返回了地址,但是此时对象还没有被初始化

解决: 声明变量是volatile类型,当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个确保volatile写之前的操作都不能被重排序到volatile写后面,因此,保证另一个线程看到引用赋值的时候,一定对象是初始化了的

注意:

如果存在两个线程执行一个相同的方法,对线程本身,是顺序执行的,看别的线程执行是乱序的。

注意

volatile类型用来一般修饰单个变量的读写,如果碰到多个变量需要共享时候,可以将多个变量封装为一个对象

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值