多线程2
volatile
作用
-
保证线程可见性
使用volatile,将会强制所有线程都去堆内存中读取变量的值
-MESI(CPU的缓存一致性协议)
详细可看
-
禁止指令重排序(CPU级别的支持)
- cpu级别增加读写屏障
图解说明:
A B线程都用到一个变量,java默认是A线程中保留一份copy,这样如果B线程修改了该变量,则A线程未必知道,不知B线程何时去修改真正的变量的值,此时使用volatile关键字,会让所有线程都会读到变量的修改值
volatie和synchronized区别
-
volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
-
volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的
-
volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性
volatile并不能保证多个线程共同修改变量时所带来的不一致问题,也就是说volatile不能替代synchronized
-
volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞
-
volatile标记的变量不会被编译器优化,不会进行指令重排序;synchronized标记的变量可以被编译器优化
volatile其他
-
volatile 引用类型(包括数组)只能保证引用本身的可见性,不能保证内部字段的可见性。
public class T04_VolatileNotSync { volatile int count = 0; void m() { for(int i=0; i<10000; i++) count++; } public static void main(String[] args) { T04_VolatileNotSync t = new T04_VolatileNotSync(); List<Thread> threads = new ArrayList<Thread>(); for(int i=0; i<10; i++) { threads.add(new Thread(t::m, "thread-"+i)); } threads.forEach((o)->o.start()); threads.forEach((o)->{ try { o.join(); } catch (InterruptedException e) { e.printStackTrace(); } }); System.out.println(t.count); } } ============= 输出结果的原因是:volatile虽然能够保证count的可见性,但是不能够保障count++的原子性
输出结果
CAS(compare and swap)
什么是CAS
CAS:Compare and Swap,即比较再交换。
jdk5增加了并发包java.util.concurrent.*,其下面的类使用CAS算法实现了区别于synchronouse同步锁的一种乐观锁。JDK 5之前Java语言是靠synchronized关键字保证同步的,这是一种独占锁,也是是悲观锁。
CAS属于CPU原语级别的支持,正常程序不会被打断
CAS算法理解
CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
CAS实现原理
CAS缺点
- 循环时间太长;
- 只能保证一个共享变量原子操作;
- 会出现ABA问题;
循环时间长开销很大:
CAS 通常是配合无限循环一起使用的,我们可以看到 getAndAddInt 方法执行时,如果 CAS 失败,会一直进行尝试。如果 CAS 长时间一直不成功,可能会给 CPU 带来很大的开销。
只能保证一个变量的原子操作:
当对一个变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个变量操作时,CAS 目前无法直接保证操作的原子性。但是我们可以通过以下两种办法来解决:1)使用互斥锁来保证原子性;2)将多个变量封装成对象,通过 AtomicReference 来保证原子性。
什么是ABA问题?ABA问题怎么解决?
CAS 的使用流程通常如下:
1)首先从地址 V 读取值 A;
2)根据 A 计算目标值 B;
3)通过 CAS 以原子的方式将地址 V 中的值从 A 修改为 B。
但是在第1步中读取的值是A,并且在第3步修改成功了,我们就能说它的值在第1步和第3步之间没有被其他线程改变过了吗?
如果在这段期间它的值曾经被改成了B,后来又被改回为A,那CAS操作就会误认为它从来没有被改变过。这个漏洞称为CAS操作的“ABA”问题。Java并发包为了解决这个问题,提供了一个带有标记的类“AtomicStampedReference”,它可以通过控制变量值的版本来保证CAS的正确性。因此,在使用CAS前要考虑清楚“ABA”问题是否会影响程序并发的正确性,如果需要解决ABA问题,改用传统的互斥同步可能会比原子类更高效。
AutomicXXX(CAS+ volatile)
相关概念
-
Java从JDK1.5开始提供了java.util.concurrent.atomic包,方便程序员在多线程环境下,无锁的进行原子操作。原子变量的底层使用了处理器提供的原子指令,但是不同的CPU架构可能提供的原子指令不一样,也有可能需要某种形式的内部锁,所以该方法不能绝对保证线程不被阻塞。
-
原子类其内部实现不是简单的使用synchronized,而是一个更为高效的方式CAS (compare and swap) + volatile和native方法(同步的工作更多的交给了硬件),从而避免了synchronized的高开销,执行效率大为提升
-
虽然基于CAS的线程安全机制很好很高效,但要说的是,并非所有线程安全都可以用这样的方法来实现,这只适合一些粒度比较小,型如计数器这样的需求用起来才有效,否则也不会有锁的存在了
-
在Atomic包里一共有12个类,四种原子更新方式,分别是原子更新基本类型,原子更新数组,原子更新引用和原子更新字段。Atomic包里的类基本都是使用Unsafe实现的包装类
代码详解
主要以AtomicInteger为例
- 有参构造函数:从构造函数函数可以看出,数值存放在成员变量value中;
- 成员变量value声明为volatile类型,说明了多线程下的可见性,即任何一个线程的修改,在其它线程中都会被立刻看到
- compareAndSet方法(value的值通过内部this和valueOffset传递)
- 接收2个参数,一个是期望数据(expected),一个是新数据(new);如果atomic里面的数据和期望数据一致,则将新数据设定给atomic的数据,返回true,表明成功;否则就不设定,并返回false。