1.讲一讲AtomicInteger类,为什么要用CAS而不是synchronized?
1.1 CAS(全称为compare-And-Swap)是什么?
它是一条CPU并发原语,它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新值,这个过程是原子的 。
CAS并发原语体现在JAVA语言中就是sun.misc.Unsafe类中的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。
// 5是初始值
AtomicInteger atomicInteger=new AtomicInteger(5);
//5是期望值,如果现在主内存里的值也是5就改为2019
System.out.println(atomicInteger.compareAndSet(5, 2019)+"==update=="+atomicInteger.get());
5是期望值,如果现在主内存里的值也是5就改为1024
System.out.println(atomicInteger.compareAndSet(5, 1024)+"==update=="+atomicInteger.get());
执行结果为:
1.2 为什么AtomicInteger不加synchronized能实现原子性?
示例volatile不保证原子性,而AtomicInteger能实现原子性。
volatile不保证原子性如下:
public class test{
public static void main(String[] args) throws InterruptedException {
MyData myData=new MyData();
for(int i=1;i<=20;i++){
new Thread(()->{
for(int k=1;k<=1000;k++){
myData.add();
}
},String.valueOf(i)).start();
}
//需要等待上面20个线程全部计算完成后,再用main线程取得最终的结果值是多少
//activeCount()>2为什么是大于2因为默认两个线程,一是main线程二是gc线程
//大于2时让出cpu让上面20个线程执行
while (Thread.activeCount()>2){
Thread.yield();
log.info("Thread.activeCount()=={}",Thread.activeCount());
}
System.out.println(Thread.currentThread().getName()+"finally i
value"+myData.num);
}
}
class MyData{
volatile int num;
public void add(){
num++;
}
}
执行结果为,期望得到20000,实际得到的值是19713:
用AtomicInteger保证原子性:
public class test{
public static void main(String[] args) throws InterruptedException {
MyData myData=new MyData();
for(int i=1;i<=20;i++){
new Thread(()->{
for(int k=1;k<=1000;k++){
myData.add();
myData.addAtomic();
}
},String.valueOf(i)).start();
}
//需要等待上面20个线程全部计算完成后,再用main线程取得最终的结果值是多少
//activeCount()>2为什么是大于2因为默认两个线程,一是main线程二是gc线程
//大于2时让出cpu让上面20个线程执行
while (Thread.activeCount()>2){
Thread.yield();
log.info("Thread.activeCount()=={}",Thread.activeCount());
}
System.out.println(Thread.currentThread().getName()+"int type value"+myData.num);
System.out.println(Thread.currentThread().getName()+"atomic type value"+myData.atomicInteger);
}
}
class MyData{
volatile int num;
public void add(){
num++;
}
AtomicInteger atomicInteger=new AtomicInteger();
public void addAtomic(){
atomicInteger.getAndIncrement();
}
}
执行结果为 :用AtomicInteger加1的结果是20000是正确的
16:10:07.575 [main] INFO com.hy.controller.ResController - Thread.activeCount()==18
main int type value19880
main atomic type value20000
AtomicInteger执行过程源码:
AtomicInteger atomicInteger=new AtomicInteger();
public void addAtomic(){
atomicInteger.getAndIncrement();
}
// getAndIncrement方法 this是当前对象valueOffset是内存偏移量,加1
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
//Unsafe类的getAndAddInt方法,该Unsafe类是在jdk里的rt.jar包
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
//var1对象里的var2这个内存偏移量的值
var5 = this.getIntVolatile(var1, var2);
//var1 AtomicInteger 对象本身
//var2 该对象值得引用地址
//var4 需要变动的数量 +1
//var5 是用var1,var2找出的主内存中真实的值
//用该 对象当前的值与var5比较
//如果相同,更新var5+var4并且返回true
//如果不同,继续取值然后 再比较,直到更新完成
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4))的执行流程: 假设线程A和线程B两个线程同时执行getAndAddlnt操作(分别跑在不同CPU上) 1 AtomicInteger里 面的value原始值为3,即主内存中AtomicInteger的value为3,根据JvM模型,线程A和线程B各自持有一 份值为3的value的副本分别到各自的工作内存。 2 线程A通过getlIntVolatile(var1, var2)拿到value值3, 这时线程A被挂起。 3 线程B也通过getlntVolatile(var1, var2)方法获取到value值3,此时刚好线程B没有被挂起并执行compareAndSwaplnt方法,比较内存值也为3,成功修改内存值为4,线程B打完收工,一切OK。 4 这时线程A恢复,执行compareAndSwapInt方法比较,发现自己手里的值数字3和主内存的值数字4不一致,说明该值已经被其它线程抢先一步修改过了,那A线程本次修改失败,只能重新读取重新来一遍了。 5 线程A重新获取value值, 因为变量value被volatile修饰, 所以其它线程对它的修改,线程A总是能够看到,线程A继续执行compareAndSwapInt进行比较替换,直到成功。
AtomicInteger 源码:
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
// 创建Unsafe 对象
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
//得到Unsafe 的内存偏移量
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
//getAndIncrement方法里加1就是对value对1,因为是volatile 所以对其它线程可见
private volatile int value;
}
为什么AtomicInteger不加synchronized能实现原子性?答案:
1.Unsafe是CAS的核心类,由于java方法无法直接访问底层系统,需要 通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据,Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为java中CAS操作的执行依赖于Unsafe类的方法。注意,Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务。
2.变量valueOffset表示该变量值在内存中偏移地址,因为Unsafe就是根据内存偏移地址获取数据的
// getAndIncrement方法 this是当前对象valueOffset是内存偏移量,加1
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
3.变量value用volatile修饰,保证了多线程之间的可见性
DCL(双端检锁)机制不一定线程安全,原因是有指令重排序的存在,加入volatile可以禁止指令重排序。
原因在于某一个线程执行到第一次检测,读取到的instance不为null时,instance的引用对象可能还没有完成初始化,instance=new SingletonDemo();可以分为以下3步完成
memory=allocate() //1分配对象内存空间
instance(memory); //2初始化对象
instance=memory; //3设置instance指向刚分配的内存地址,此时instance!=null
步骤2和步骤3不存在数据依赖关系,可能会指令重排序如下:
memory=allocate() //1分配对象内存空间
instance=memory; //3设置instance指向刚分配的内存地址,此时instance!=null,但是对象还没有初始化完成
instance(memory); //2初始化对象
指令重排只会保证串行语义的执行一致性(单线程),但并不会关心多线程间的语义一致性
所以当一条线程访问instance不为null时,由于instance实例未必已初始化完成,也就造成了线程安全问题。
以下代码是DCL(双端检锁):
class SingletonDemo{
private SingletonDemo(){
System.out.println("我是一个构造函数");
}
private static SingletonDemo instance=null;
public static SingletonDemo getInstance(){
if(instance==null){
synchronized (SingletonDemo.class){
if(instance==null){
instance=new SingletonDemo();
}
}
}
return instance;
}
}
禁止指令重排加volatile:
class SingletonDemo{
private SingletonDemo(){
System.out.println("我是一个构造函数");
}
private static volatile SingletonDemo instance=null;
public static SingletonDemo getInstance(){
if(instance==null){
synchronized (SingletonDemo.class){
if(instance==null){
instance=new SingletonDemo();
}
}
}
return instance;
}
}
CAS缺点: 循环时间长,开销大 只能保证一个共享变量的原子操作。 会导致ABA问题,所谓ABA就是线程A把自己工作内存里的值改成别的值同步到主内存,然后又改回原来的值同步到主内存,过后线程B拿自己工作内存的值和主内存的值比较是一样然后更改。线程B并不知道A中间已经改过一次。用 AtomicStampedReference的时间戳版本号解决ABA问题,就是版本号比较,类似于Svn
public class ABADemo {
static AtomicReference<Integer> atomicReference=new AtomicReference<Integer>(100);
static AtomicStampedReference<Integer> atomicStampedReference=new AtomicStampedReference<Integer>(100,1);
public static void main(String[] args) {
System.out.println("=======以下是ABA问题的产生=======");
new Thread(new Runnable() {
public void run() {
atomicReference.compareAndSet(100,101);
atomicReference.compareAndSet(101,100);
}
},"t1").start();
new Thread(new Runnable() {
public void run() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("更改是否成功="+atomicReference.compareAndSet(100,2019)+"=="+atomicReference.get());
}
},"t2").start();
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("=======以下是ABA问题的解决=======");
new Thread(new Runnable() {
public void run() {
int stamp=atomicStampedReference.getStamp(); //版本号
System.out.println(Thread.currentThread().getName()+"第一次版本号:"+stamp);
try {
Thread.sleep(1); //为了让t4拿到 为1的版本号
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicStampedReference.compareAndSet(100,101,stamp,stamp+1);
System.out.println(Thread.currentThread().getName()+"第二次版本号:"+atomicStampedReference.getStamp());
atomicStampedReference.compareAndSet(101,100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName()+"第三次版本号:"+atomicStampedReference.getStamp());
}
},"t3").start();
new Thread(new Runnable() {
public void run() {
int stamp=atomicStampedReference.getStamp(); //版本号
System.out.println(Thread.currentThread().getName()+"第一次版本号:"+stamp);
try {
Thread.sleep(3); //为了让t3执行完修改操作
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean b = atomicStampedReference.compareAndSet(100, 2019, stamp, stamp + 1);
System.out.println(Thread.currentThread().getName()+"修改是否成功:"+b+"版本号为:"+atomicStampedReference.getStamp());
System.out.println(Thread.currentThread().getName()+"值为:"+atomicStampedReference.getReference());
}
},"t4").start();
}
}
自旋锁:
是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU
自己实现一个自旋锁:
class MyData{
AtomicReference<Thread> atomicReference=new AtomicReference<>();
public void myLock(){
Thread thread=Thread.currentThread();
System.out.println(Thread.currentThread().getName()+"===come in");
//先t1 期望值是null,确实是null返回true,把值设置为自己的thread
//然后t2进来 期望值是null,这时候已经被t1改了,所以返回false,然后一直while循环
//直到t1 执行myUnlock()方法把值改为null.这时候返回true
while (!atomicReference.compareAndSet(null,thread)){
}
}
public void myUnlock(){
Thread thread=Thread.currentThread();
atomicReference.compareAndSet(thread,null);
System.out.println(Thread.currentThread().getName()+"===invoke myUnlock");
}
public static void main(String[] args) {
MyData myData=new MyData();
new Thread(()->{
myData.myLock();
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
myData.myUnlock();
},"t1").start();
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
myData.myLock();
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
myData.myUnlock();
},"t2").start();
}
}
执行结果:
t1===come in
t2===come in
t1===invoke myUnlock
t2===invoke myUnlock