volatile二三事2---非原子性

1.原子性:

 一次操作是不可分割的,常见的能保证原子性的操作有院子类AtomicInteger、AtomicLong等在i++过程,载入Lock,synchronized,而volatile是非原子性的。

Java要求load read assign use store write lock unlock 8项操作是原子的,但是对于64位的long和double,允许jvm将没有volatile修饰的long和double读写操作划分成两个32位的操作。允许long和double在read load store write四个操作上是非原子的,这就是long和double的非原子性协定。不过在多线程下对为声明volatile操作的long double出现半个变量的情况是非常罕见的,故只要知道有这么回事就行了,不用担心也不用特意加上volatile

2.volatile非原子性

(1)i++:非原子性的典型例子就是i++.这其实是三条操作获取i的值,i+1,在赋值给i,而volatile修饰的变量在++过程中,如果是多线程访问可能会不是我们预想的样子,多线程访问时是不安全的,出现脏读现象。解决方案synchronized或者AtomicInteger

(2)volatile并不处理数据的原子性,只是将数据的读写强制到主内存中,

(3)原子类也不是完全能够保证原子性的,比如调用某个原子类方法是原子性的,但是先后调用两个原子类方法,这两个方法间的操作就不一定是原子性的

3.例子

 

class T2 extends Thread{
	volatile public static int count;//未加volatile修饰,数据出现不安全
	public static void add(){
		for(int i=0;i<100;i++){
			count+=i;
		}
		System.out.println("count="+count);
	}
	
	public void run(){
		add();
	}
}

 

 

count=405900  //未加volatile修饰
count=405900
count=405900
count=405900
count=495000
count=485100
count=480150
count=445500
count=405900//加volatile修饰
count=400950
count=410850
count=415800
count=420750
count=425700
count=430650
count=435600

 

此外synchronized实现原子性

 

class T2 extends Thread{
    public static int count;
	synchronized public static void add(){//synchronized锁定类,达到同步效果
		for(int i=0;i<100;i++){
			count+=i;
		}
		System.out.println("count="+count);
	}
	
	public void run(){
		add();
	}
}

 

 

 

原子类的原子操作与非原子操作

 

class T2 extends Thread{
    private AtomicInteger ai=new AtomicInteger(0);//初始值为0的原子类
	
	
	public void run(){
		for(int i=0;i<100;i++){
			System.out.println(Thread.currentThread().getName()+"\t"+ai.incrementAndGet());//++功能
		}
	}
}

 

Thread-46	96//安全结果
Thread-46	97
Thread-46	98
Thread-46	99
Thread-46	100
Thread-4	95
Thread-4	96
Thread-4	97
Thread-4	98
Thread-4	99
Thread-4	100
Thread-6	90
Thread-6	91
Thread-6	92
Thread-6	93
Thread-6	94
Thread-6	95

 

class T2 extends Thread{
    public static AtomicLong ai=new AtomicLong(0);
	public void run(){
		//两个原子的方法放在一起,操作并不是原子的,由结果可以看到并不是先+1再+100,两个函数的执行时异步的
			System.out.println(Thread.currentThread().getName()+"\t加1  "+ai.addAndGet(1));//++功能
			System.out.println(Thread.currentThread().getName()+"\t加100  "+ai.addAndGet(100));//
		
	}
}
/**   
 * @Title: Run   
 * @Description: TODO(用一句话描述该文件做什么)  
 */
public class Run {
	public static void main(String[] args){
		T2[] t2=new T2[100];
		for(int i=0;i<100;i++){
			t2[i]=new T2();
		}
		for(int i=0;i<100;i++){
			t2[i].start();
		}
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		System.out.println(T2.ai.get());
	}
}

 

 

Thread-83	加100  9295
Thread-82	加100  7276
Thread-78	加1  7175
Thread-78	加100  9395
Thread-77	加1  7173
Thread-86	加100  9195
Thread-90	加100  8894
Thread-91	加100  8794
Thread-98	加1  8394
Thread-98	加100  9595
Thread-77	加100  9495
Thread-89	加1  9596
Thread-89	加100  9697
Thread-92	加1  9698


4.禁止指令重排序              

                                                        
普通的变量仅仅会保证在该方法的执行过程中所以后依赖赋值的结果的地方顺序执行,但其他的不一定,比如int i; int j; i=1; j=1;普通变量只会保障int i在i=1前面,并不一定保证i=1在int j后面。而volatile会保证变量禁止重排序,保证该变量赋值顺序和程序中写的顺序是一样的。

硬件架构上将,重排序是CPU采用了策略将多条指令不按照程序的顺序分开给各电路单元处理

volatile的读操作与普通变量没有什么差别,但写操作会慢一些,因为他需要在本地代码中插入许多内存屏障来保障指令处理器不重排序。不过大多数情况下volatile的性能还是很优的,我们判断是否使用它的标准是是否保障变量的可见性。

线程内表现为串行,在本线程内观察所有操作都是顺序的,但在另一个线程中观察则是乱序的。

 

使用volatile变量还可以禁止JIT编译器进行指令重排序优化,这里使用单例模式来举个例子:

变量在volatile修饰之后,对变量的修改会有一个内存屏障的保护,使得后面的指令不能被重排序到内存屏障之前的位置。volalite变量的读性能与普通变量类似,但是写性能要低一些,因为它需要插入内存屏障指令来保证处理器不会发生乱序执行。即便如此,大多数场景下volatile的总开销仍然要比锁低,所以volatile的语义能满足需求时候,选择volatile要优于使用锁。

 

 

 

 

/**
 * 单例模式一
 */
public class Singleton_1 {

	private static Singleton_1 instance = null;

	private Singleton_1() {
	}

	public static Singleton_1 getInstacne() {
		/*
		 * 这种实现进行了两次instance==null的判断,这便是单例模式的DCL双检锁。
		 * 第一次检查是说如果对象实例已经被创建了,则直接返回,不需要再进入同步代码。
		 * 否则就开始同步线程,进入临界区后,进行的第二次检查是说:
		 * 如果被同步的线程有一个创建了对象实例, 其它的线程就不必再创建实例了。
		 */
		if (instance == null) {
			synchronized (Singleton_1.class) {
				if (instance == null) {
					/*
					 * 仍然存在的问题:下面这句代码并不是一个原子操作,JVM在执行这行代码时,会分解成如下的操作:
					 * 1.给instance分配内存,在栈中分配并初始化为null
					 * 2.调用Singleton_1的构造函数,生成对象实例,在堆中分配 
					 * 3.把instance指向在堆中分配的对象
					 * 由于指令重排序优化,执行顺序可能会变成1,3,2,
					 * 那么当一个线程执行完1,3之后,被另一个线程抢占,
					 * 这时instance已经不是null了,就会直接返回。
					 * 然而2还没有执行过,也就是说这个对象实例还没有初始化过。
					 */
					instance = new Singleton_1();
				}
			}
		}
		return instance;
	}
}

 

public class Singleton_2 {

	/*
	 * 为了避免JIT编译器对代码的指令重排序优化,可以使用volatile关键字,
	 * 通过这个关键字还可以使该变量不会在多个线程中存在副本,
	 * 变量可以看作是直接从主内存中读取,相当于实现了一个轻量级的锁。
	 */
	private volatile static Singleton_2 instance = null;

	private Singleton_2() {
	}

	public static Singleton_2 getInstacne() {
		if (instance == null) {
			synchronized (Singleton_2.class) {
				if (instance == null) {
					instance = new Singleton_2();
				}
			}
		}
		return instance;
	}
}

 

 

 

 

 

 

5.先行发生原则

这个原则非常重要,因为Java里面的所有有序操作并不是靠添加volatile和synchronized实现的。该原则判断数据是否发生竞争、线程是否安全的依据。先行发生定义的是两个操作的偏序关系,如果A先行发生于B,表示A发生产生的影响B看得到,这里产生的影响包括修改共享变量的值,发送消息,调用方法等。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值