volatile=与=CAS

volatile

实际写程序,volatile 基本用不着,就是用来干面试的。

两个特性

  • 保证线城可见性
    java里面是堆内存的,堆内存是所有线程共享的内存。除了共享内存之外,每个线程都有自己的工作区域,叫做专属内存。如果在共享内存里有一个值的话,如果有两个线程同时访问这个值,他们会把它copy一份到自己的工作空间。然后如果对这个值进行改变,首先会在自己的工作里面改变,然后等到某一个CPU会把这个值写回到共享内存。但是什么时候写回,不好控制。此时如果另外一个线程直接读取它工作空间内的已经过时的值,显然就有问题了。此时缺少一个线程间的通知机制,叫做线程不可见。加了volatile之后就能保证一个线程改变了这个值,另一个线程马上就能看到。
    volatile能保证线程可见性,底层用到了CPU的缓存一致性协议MESI
  • 禁止指令重排序
    多核CPU为了提高计算效率,会把指令并行执行,称之为流水线式的执行。如果想要充分的利用这一点,就要求编译器把源码编译成指令之后呢,可能进行一个重新排序。逻辑上比如a=3,b=5,有可能被重排序为b=5,a=3;细节上就是汇编的指令重排序。(禁止指令重排序有什么好处呢?)

底层实现(阿里面试题)

JVM 层面的实现

在这里插入图片描述
在这里插入图片描述

  • volatile 写操作S:
    如果上面有写操作S,肯定不能和当前写操作S互换
    如果下面有读操作L,肯定也不能和当前写操作S交换
  • volatile 读操作L:
    如果想面有读操作L,肯定不能和当前读操作L互换
    如果下面有读操作S,肯定也不能和当前写操作L交换

操作系统层级的实现

无非两种:

  • CPU原语:lfence、sfence、mfence
  • 总线锁 lock 汇编指令

具体怎么实现由JVM的实现决定
volatile 在 HotSpot 里采用的 lock 指令。

volatile 修饰对象,怎么加屏障

对象的整个内存区域都会加上屏障,对对象的所有改动包括对它属性的改动,都是收屏障保护的。

DCL单例+volatile

禁止指令重排序有什么好处呢?通常会举这个例子。

1、单例模式

  • 饿汉式(推荐使用,如果没有内存容量的限制,利用类加载器创建单例,这种做法线程安全)

    public class Singleton {
    	private static Singleton instance = new Singleton();
    	private Singleton() {}
    	public Singlenton getInstance() {
    		return instance;
    	}
    }
    
  • 懒汉式(最简模式)

    public class Singleton {
    	private static Singleton instance;
    	private Singleton() {}
    	public synchronized Singlenton getInstance() {
    		if (instance == null) {
    			instance = new Singleton();
    		}
    		return instance;
    	}
    }
    

2、DCL单例

DCL(double check lock)单例属于懒汉式的终极模式,可防止多线程并发访问时造成重复创建对象

public class Singleton {
	private static volatile Singleton instance;
	private Singleton() {}
	public Singlenton getInstance() {
		if (instance == null) {
			synchronized(Singlenton.class) {
				if (instance == null) {
					instance = new Singleton();
				}
			}
		}
		return instance;
	}
}

在这里插入图片描述
在这里插入图片描述

3、对象的创建过程(汇编创建过程)

  • 1、为对象申请内存(半初始化)
  • 2、对象的成员变量进行初始化
  • 3、把对象的赋值给变量指针
Object o = new Object();

编译成汇编指令:

new #2 <java/lang/Object <init>>
dup
invokespecial #1 <java/lang/Object <init>>
astore_1
return

(astore_0 指向 this)
在创建对象的过程中,如果发生指令重排序,有可能会发生第2、3步进行交换。这样如果当一个线程创建对象时,执行完第1步先执行第3步,此时另一个线程也来访问这个对象,发现已经有值,便直接返回并使用,此时由于前一个线程还没有执行第2步,对象没有进行初始化,导致第二个线程在用这个对象是,取到的对象成员变量为0或false或null,并没有被初始化。

如果加了volatile,那么对对象的指令重排序就不允许存在了。

4、总结

  • volatite 可以禁止指令重排序,这是 synchronized 做不到的。synchronized 只能保证原子性。
  • 读屏障和写屏障是防止指令重排序用的。

volatile vs synchronized

  • volatile 并不能保证多个线程共同修改running变量时所带来的不一致问题,也就是说 volatile 不能替代 synchronized

运行下面的程序,并分析结果

package com.mashibing.juc.c_012_Volatile;

import java.util.ArrayList;
import java.util.List;

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);
	}
}

运行结果:小于100000
分析:虽然第一个线程count++变成了1,此时第二个线程读到的是1,第三个线程读到的也是1,但是此时第二和第三个线程都执行count++变成了2,此时第二个线程写回了一个2,第三个线程也写回了一个2,结果就是少加了一个1
在这里插入图片描述

  • 加了 volatile 虽然保证了 count 的可见性,但 count++ 本身并不是一个原子性的操作。(问:count++不是一句话吗,为什么没有原子性?答:编译成汇编以后会变成好几条操作指令,类似new Object()也只是一条语句,但是变成汇编以后就有好几条操作指令)。

要解决这个问题,用synchronized就可以。

package com.mashibing.juc.c_012_Volatile;

import java.util.ArrayList;
import java.util.List;

public class T05_VolatileVsSync {
	/*volatile*/ int count = 0;

	synchronized void m() { 
		for (int i = 0; i < 10000; i++)
			count++;
	}

	public static void main(String[] args) {
		T05_VolatileVsSync t = new T05_VolatileVsSync();

		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);
	}
}

synchronzed的优化

锁的细化(fine lock)

在锁竞争激烈的情况下,应该让锁尽量细化,这样可以让更多的线程不至于阻塞,比如DCL单例就比同步方法的懒汉式单例,更适合高并发情形。

锁的粗化(coars lock)

如果一个方法里头有好多好多的细锁,可以把它粗化为一个大锁,这样可以避免频繁的锁竞争。

锁定同一个对象时的注意点

  • 锁定某对象o,如果o的属性发生改变,不影响锁的使用
  • 但是如果o变成另外一个对象,则锁定的对象发生改变
  • 应该避免将锁定对象的引用变成另外的对象,可以加 final
package com.mashibing.juc.c_017_MoreAboutSync;

import java.util.concurrent.TimeUnit;

public class SyncSameObject {
	
	final Object o = new Object();

	void m() {
		synchronized(o) {
			while(true) {
				try {
					TimeUnit.SECONDS.sleep(1);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println(Thread.currentThread().getName());
			}
		}
	}
	
	public static void main(String[] args) {
		SyncSameObject t = new SyncSameObject();
		//启动第一个线程
		new Thread(t::m, "t1").start();
		
		try {
			TimeUnit.SECONDS.sleep(3);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		//创建第二个线程
		Thread t2 = new Thread(t::m, "t2");
		
		t.o = new Object(); //锁对象发生改变,所以t2线程得以执行,如果注释掉这句话,线程2将永远得不到执行机会
		
		t2.start();
	}
}

不要用String、Long等基本类型来作为锁

不要以字符串常量作为锁定对象,因为并不是因为语法上的问题,而是由于这些对象在堆内存的常量池内都是独一份,如果和某些第三方类库不小心使用了同一个对象,造成诡异的死锁,将是极难排查出问题的。

package com.mashibing.juc.c_017_MoreAboutSync;

public class DoNotLockString {
	
	String s1 = "Hello";
	String s2 = "Hello";

	void m1() {
		synchronized(s1) {
			
		}
	}
	
	void m2() {
		synchronized(s2) {
			
		}
	}
}

CAS

一篇关于CAS文章

面试重灾区,号称“无锁优化 自旋 乐观锁”,理解含义就行。

  • 比较+更新 整体是一个原子操作
  • 由于某些特别常见的操作,老是来回加锁,所以干错Java提供了一些常见操作的类,这些类的内部就自动带了锁,但这些锁不是sync重量级锁,而是CAS,号称无锁。
  • 在JUC的atomic包下,很多以Atomic开头的类,都是以CAS操作来保证线程安全的类。最常见的类就是AtomicInteger,用法如下:
package com.mashibing.juc.c_018_00_AtomicXXX;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

public class T01_AtomicInteger {
	/*volatile*/ //int count1 = 0;
	
	AtomicInteger count = new AtomicInteger(0); 

	/*synchronized*/ void m() { 
		for (int i = 0; i < 10000; i++)
			//if count1.get() < 1000
			count.incrementAndGet(); //count1++
	}

	public static void main(String[] args) {
		T01_AtomicInteger t = new T01_AtomicInteger();

		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);
	}
}

Compare And Set

  • CAS操作可以想象成一个方法,如下:
    cas(V, Expected, NewValue):参数1:V的当前值,参数2:V的期望值(旧值),参数3:V要改成的新值
  • 下面的模型,请参考 VarHandler 的 compareAndSet() 操作
CAS(V, Expected, NewValue) {
	if V == Expected: // 如果V是我期望的值E,则V做修改改成NewValue
		V = NewValue 
	otherwise try again or fail // 否则说明有其他线程改了这个值了,那么当前线程就通过自旋再试一下,此时E为上一次的V
}

在这里插入图片描述
在这里插入图片描述

  • CPU原语支持:所以在CAS操作期间,语句的执行不会被打断
  • 期望值怎么得到,就是再CAS操作之前先读一下,如果再CAS操作期间V发生了改变,那么就在CAS进行自旋操作的时候重新读
expected = read V
newValue = expected +/-* dolta
CAS(V, expected, newValue)

ABA问题

在一个线程1对变量进行cas操作时的期望值是a,在它还没有操作之前另一个线程2将变量改为b又改回为a,然后线程1执行cas操作时虽然期望值还是a,但其实已经被改过。

解决办法:

  • 一种情况,如果对于像int型变量,这种值的变化无所谓嘛,完全可以忽略
  • 对于像指向对象的变量,可能会有问题(你的女朋友虽然和你复合,但是期间她可能经历了很多别的男人)
  • 想彻底解决ABA问题,可以加版本号,每次操作,版本号加1

Unsafe类(= c/c++的指针)

CAS怎么做到可以不加锁进行操作的,就是用到类Unsafe这个类。这个类只需了解就行,这个类里面的方法非常非常多。

  • 在JDK8以前除了反射可以用之外,是不能直接使用的。原因与ClassLoader是有关系的。
  • JDK11以后,Unsafe可以通过getUnsafe() 来获得,是个单例(饿汉式)
  • Unsafe是用来干嘛的呢?它可以直接操纵JVM的内存。
  • 所有Atomic打头的类,都是通过Unsafe里的compareAndSwap()和compareAndSet()这一类的原子性操作来实现的

CAS(实现数量自增的几种方式以及比较)

为什么要用CAS操作,因为效率比sync同步锁效率高得多。怎么证明?

Atomic Vs Synchronized

举个例子,有多个线程对一个long型变量进行++操作,这在我们实际工作当中非常常见,比如秒杀,有几种方式:(它们之间的效率高低并不一定是确定的)

  • sychronized
  • AtomicLong
  • LongAdder
package com.mashibing.juc.c_018_00_AtomicXXX;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAdder;

public class T02_AtomicVsSyncVsLongAdder {
    static long count2 = 0L;
    static AtomicLong count1 = new AtomicLong(0L);
    static LongAdder count3 = new LongAdder();

    public static void main(String[] args) throws Exception {
        Thread[] threads = new Thread[1000];

        for(int i=0; i<threads.length; i++) {
            threads[i] =
                    new Thread(()-> {
                        for(int k=0; k<100000; k++) count1.incrementAndGet();
                    });
        }

        long start = System.currentTimeMillis();

        for(Thread t : threads ) t.start();

        for (Thread t : threads) t.join();

        long end = System.currentTimeMillis();

        //TimeUnit.SECONDS.sleep(10);

        System.out.println("Atomic: " + count1.get() + " time " + (end-start));
        //-----------------------------------------------------------
        Object lock = new Object();

        for(int i=0; i<threads.length; i++) {
            threads[i] =
                new Thread(new Runnable() {
                    @Override
                    public void run() {

                        for (int k = 0; k < 100000; k++)
                            synchronized (lock) {
                                count2++;
                            }
                    }
                });
        }

        start = System.currentTimeMillis();

        for(Thread t : threads ) t.start();

        for (Thread t : threads) t.join();

        end = System.currentTimeMillis();

        System.out.println("Sync: " + count2 + " time " + (end-start));

        //----------------------------------
        for(int i=0; i<threads.length; i++) {
            threads[i] =
                    new Thread(()-> {
                        for(int k=0; k<100000; k++) count3.increment();
                    });
        }

        start = System.currentTimeMillis();

        for(Thread t : threads ) t.start();

        for (Thread t : threads) t.join();

        end = System.currentTimeMillis();

        //TimeUnit.SECONDS.sleep(10);

        System.out.println("LongAdder: " + count1.longValue() + " time " + (end-start));

    }

    static void microSleep(int m) {
        try {
            TimeUnit.MICROSECONDS.sleep(m);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

结果:
LongAdder > AmoticLong > sychronized
由此可见LongAdder效率最快,但是如果线程数量减少,LongAdder未必有优势,甚至 AmoticLong也未必比 sychronized快。为什么?

  • AmoticLong为什么 sychronized快?因为 AmoticLong只需进行CAS操作,而 sychronized可能需要向操作系统取申请重型锁(锁升级的过程)。
  • LongAdder为什么 AmoticLong快?因为LongAdder内部有一个分段锁的概念。

LongAdder

AmoticLong这个CAS操作有没有问题呢?肯定是有的。比如说大量的线程同时并发修改一个 AmoticLong,可能有很多线程会不停的自旋,进入一个无限重复的循环中。

  • 分段锁 原理戳这里
    Java 8推出了一个新的类,LongAdder,他就是尝试使用分段CAS以及自动分段迁移的方式来大幅度提升多线程高并发执行CAS操作的性能!
发布了40 篇原创文章 · 获赞 0 · 访问量 417

猜你喜欢

转载自blog.csdn.net/weixin_43780400/article/details/105417182