秋招准备-Java-并发编程-volatile和线程安全类(八)

1.volatile

2.线程安全类

3.CAS


1.volatile

    先简单了解一下Java内存模型

    1.Java内存模型

    和JVM的运行时数据分区相区别,Java的内存模型是用来描述线程工作的(定义程序中各个变量的访问规则)。可以说是一个抽象概念。

    其规定,所有变量(数据)存储在主内存中。(这里的变量指非线程私有的实例字段、静态字段、数组元素~)

    每个线程有自己的工作内存,其中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行。

    这样,线程无法直接读写主内存中的变量,不同线程之间也无法直接访问对方工作内存中的变量。

    因此整个模型聚焦的问题就是内存间的交互,即数据交换,要实现的内容是,一个变量如何从主存拷贝到工作内存,如何从工作内存同步回住内存这样。Java内存模型提供了一些原子指令。

    保证了上述操作之后,那么线程的工作就是,在工作内存中执行run()方法,并且涉及到的一些共享变量在读取时会通过一些指令从主存拷贝到工作内存,在写入时再通过一些指令到主存进行改写。


    因此,线程对主存数据的读写操作,就涉及到首先是从主存读入数据,然后再在工作内存操作,最后在写入主存。

    那么如果是多线程访问共享变量的情况,如果没有同步性,就会出现一些问题。

    比如:

多个线程对变量i自加的时候
i=10;
//线程1读入10
i++;存入11

//线程2读入10
i++;存入11

两次自加后,本来i=12,现在i=11
甚至可能,如果线程1调用两次线程2才完成,那么线程1都使i=12了,最后线程2又使i=11

    线程1和线程2在读入i时,都是从主存中读取,这时候主存数据自然是10,则两者的工作内存里,i就为10,然后自加后,再通过指令写回主存,主存中的i的值被两次更新为11。

    或者:

//线程1
tag=true;
while(tag)
{
	operation……
}

//线程2
tag=false;

当唯一用线程2来关闭线程1的循环时,如果线程2改写了主存的tag,但线程2因为一些原因,
没有向主存中写入这个tag的更新值,那么线程1的循环就死了。

    因此,这时候就需要保证共享变量的可见性,即一个线程改变了某个变量,其他线程能够立即得知这个修改。


    2.volatile的可见性

    当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,然后会导致其他线程的这个共享变量值失效,当其它线程需要用到该变量时,会去主存读取。

    而普通变量修改后,什么时候被写入主存是不确定的。

    volatile可见性最常用到的场景就是中断循环。

volatile boolean tag;
//线程1
tag=true;
while(tag)
{
	operation……
}

//线程2
tag=false;

    当用volatile修饰tag时,如果线程2修改了tag为false,可以保证在下一次循环判断时,线程1会退出循环。

    另外:synchronized和Lock实现同步时,是可以保证其中涉及到的共享数据的可见性的,在解锁之前,会将更改的变量修改值存入主存。

    

    3.volatile的有序性

    JVM的指令重排序保证了对单个线程自己而言,内部的代码是有序的,能保证得到其想要的正确结果,并且可能通过重排序获得处理效率上的提升。

    然后对于多线程而言,涉及到共享变量之后,指令重排序就会导致一些问题。如:

boolean tag = false;
//线程1
if(tag)
{
	operation……
}

//线程2
initial();//1
tag=true;//2

    两个线程用以实现,线程2完成初始化,通知线程1可以进行if块里的操作。但线程2的1,2语句不构成happens-before关系,可以发生重排序,那么2如果在1之前,就可能发生还没初始化,线程1就开始if块的操作了。

    因此,volatile提供了一定有序性的保证。它一定层面上为指令重排序增加了一些限制:

    1.程序执行到volatile变量的读写操作时,其前面的语句一定已经执行完毕,并且结果对后面的语句可见。其后面的语句一定还没有执行。

    2.以volatile变量的读写语句为分界,其前面的代码可以重排序,但不能排到分界的后面去,其后面的代码也是一样,不能排到分界前面去执行。

    因此,在上述代码中将tag用volatile来修饰,则在看线程2的语句,volatile变量的读写语句是tag=true。那么执行这句时,前面的initial()方法一定执行完毕,因此当线程1的tag=true进入if块时,初始化一定已经完成。


    4.volatile不能保证原子性

    在实现上,原子性是一直需要考虑的东西,除非用粗粒度的锁,不然就算用原子类,用线程安全类,用不变类,都得再在具体中去考虑,毕竟一个原子操作再随便加点什么,又可能不能保证原子性了。

    但volatile是本身就不带原子性的,因此要用它来实现线程安全,就需要搭配上一点别的工具,如原子类之类的。

    写一个计数。

public class Main
{
	private static volatile AtomicInteger I = new AtomicInteger();
	public static void main(String[] args)throws Exception 
	{
		for(int i=0;i<10;i++)
		{
			Thread t = new Thread(new Runnable() {
				public void run() {
					for(int j=1;j<=1000;j++)
					{
						I.getAndIncrement();
					}
				}
			});
			t.start();
		}
		Thread.sleep(3000);
		System.out.println(I.get());
	}
}//输出10000

    实际上这个测试代码有点不对题,volatile在这里根本没起啥作用,不加volatile原子类自身也可以实现。而如果不用原子类,单纯用volatile没什么用。

    在使用volatile的时候,去考虑线程之间每一条语句的执行时序,我的理解是,volatile只保证涉及到共享数据的读取是可见的,也就是说在读取时是主存中的值,读完以后,即使其他线程再来修改,那如果没有了读取操作,该线程接下来的操作也会用未更新的值来处理。

    那么线程1和线程2对一个volatile修饰的i=10做自加,和没加volatile的其实差不多。

    在底层上,同样会发生线程1和线程2先读入数据,volatile保证此时读入的是最新数据10,然后就没了,之后的指令操作,i=10++,和i发生改变后立刻写入主存,都没问题,然后还是在主存里对i赋了两次11。

    区别只是,如果线程1完成了修改后,如果此时线程2还没进行读入,但之前它可能已经有其他语句将i=10存入工作内存了,那么这种情况,能够保证读入的一定是11。

    因此,综合来说,volatile的可见性要单用的话,一般也就用做标记,这种情况是一个线程读一个线程写的,因此能够很好的利用可见性。


2.线程安全类

    1.设计线程安全的类

    三个要素:

    1.找出构成对象状态的所有变量

    2.找出约束状态变量的不变性条件

    3.建立对象状态的并发访问管理策略

    同步策略规定了将不可变性、线程封闭、加锁机制等结合起来以实现线程安全性。

    线程安全的类使得在使用其对象时,能够允许多个线程同时访问其对象与对象中的数据。

    这些数据要么是不可变的,要么是共享可变的,如果是可变的,则需要使得它的可变性是建立在正确性之上的,即保证每个线程使用时对象的方法其语义是正确的。

    而这就需要同步策略来实现。

    因此可以总结为,对于一个线程安全类而言,找出它的所有变量,找出任何可以访问到这些变量的域,然后保证在这些域中访问变量不会出错。(支持多线程同时调用相同或者不同方法之类的)

    那么显然,

    1.变量越不可变,就越不用操心(改变变量引起线程间语义错误),当然,把变量设为不可变需要数据语义本身不变。

    2.变量访问的区域越小,需要考虑的区域越小,显然,弄个public变量就完全没得考虑线程安全了,因此,要在符合要求的情况下,尽量缩小变量的访问范围。

    3.而在关键的功能方法部分,一般也是多线程会同时访问的方法里,就需要考虑线程安全的问题了(原子、可见、有序),接下来就是解决,原子类、不变类、加锁同步、非阻塞同步等等。


    2.不变类(immutable object)

    如果一个类的数据只在初始化时定义好,之后无法改变,那么它在只允许多个线程访问它的数据而不让它们修改的同时,也保证了多线程之间一定不会相互干扰,因此不可变一定是线程安全的。

    一个类如果要定义成不可变类,需要满足以下条件:

    1.对象创建以后其状态就不能修改。(没有setValue()之类的方法)

    2.对象的所有域都是final型。

    3.对象是正确创建的。(在对象的创建期间,this引用没有逸出)

    关于状态不能修改,又可以详细一点描述为。

    1.可以直接将类定义为final,防止继承与重写。

    2.对于类中的基本类型与不可变类对象,初始化后则不用担心它们被更改,返回这些数据时可以直接返回。但如果类中有可变类对象,则需要在初始化时获得克隆,在返回时也返回其克隆值。不然就有其他域存在指向其对象的指针,并且存在修改的可能性。


    3.原子类(java.util.concurrent.atomic包)

    原子类提供了一种简单实现线程安全的方式,通过使用并发包中的原子类,搭配volatile,可是在某些场景下不加锁保证线程安全。

    原子类保证同一时刻只有一个线程能够修改其对象的值,本身是基于CAS来实现的,下面简单介绍几种原子类的使用。

    <1>AtomicInteger
class AtomicInteger
{
    public AtomicInteger(int initialValue);//无参初始值0
    int get();//获取当前值
    //自加自减操作
    int getAndDecrement()//value--
    int getAndIncrement()//value++
    int decrementAndGet()//--value
    int incrementAndGet()//++value
    //other
    int addAndGet(int d)//与给定值相加,之后再返回值
    int getAndSet(int new)//更新为新值,返回旧值
    boolean compareAndSet(int expect,int update)//如果预期==当前,更新为update
    //上述操作除了get()全部为原子操作
}

    <2>AtomicReference
class AtomicReference
{
	public AtomicReference(V initialValue);
	public final V getAndSet(V newValue);
	public final boolean compareAndSet(V expect, V update);//比较的是引用相等
}
    
    <3>AtomicBoolean
class AtomicBoolean
{
	public AtomicBoolean(boolean initialValue)
	public final boolean getAndSet(boolean newValue);
	public final boolean compareAndSet(boolean expect, boolean update);
}

    原子变量类,是一种比锁粒度更细的线程安全实现方式,将发生的竞争范围缩小到单个变量上,性能通常也要好一些。



3.CAS(compare and swap)

    悲观锁实现同步:默认每次都会发生冲突,因此对每个操作都会有加锁保证,synchronized实现即为悲观锁。

    乐观锁实现同步:每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。其实现用到的就是CAS。

    CAS在底层上是通过操作系统的指令来完成的,而在描述上则是:

    其包括三个操作数:内存位置(V),预期原值(A)和新值(B),如果内存位置的值和预期值匹配,则将该位置的值更新为B值。

public final int incrementAndGet() {  
        for (;;) {  
            int current = get();  
            int next = current + 1;  
            if (compareAndSet(current, next))  
                return next;  
        }  
    }

    如AtomicInteger的具体实现。CAS处于一个不断尝试的循环中,期望的则是一个不发生冲突的循环。

    而这个循环应该是这样子的,先获得当前值current,那么自加后就应该时next,这时候只需要将next赋给内存就完成了,但此时必须确保不发生冲突,即其他线程没有更改过当前值,那么做个判断,compareAndSet(current,next),即是内存位置的值仍然是current时,将其改为next。

    CAS的问题:

    1.显然,CAS处在一个不断循环试探中,那么如果一直得不到想要的结果,就会一直自旋,这部分会有性能损耗。

    2.ABA问题,当获得current并且计算出next后,实际上得确保在这段时间内存值没被改动过,而判断时则时通过值来判断的。但如果这个值发生过改变,但又改回来了。

    这其实从实现上好像也行,并且如果就是按值相等进行更改的话,也会判断出更改后的值仍然时原值且继续更新的结果。

    但实际上是违反操作原子性的。因此,有增加一个版本号,来解决ABA问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值