文章目录
CAS(乐观锁)
cas属于轻量级锁(无锁,自旋锁)
底层实现是lock cmpxchg
CAS,是Compare and Swap的简称,在这个机制中有三个核心的参数:
主内存中存放的共享变量的值:V(一般情况下这个V是内存的地址值,通过这个地址可以获得内存中的值)
工作内存中共享变量的副本值,也叫预期值:A
需要将共享变量更新到的最新值:B
如上图中,主存中保存V值,线程中要使用V值要先从主存中读取V值到线程的工作内存A中,然后计算后变成B值,最后再把B值写回到内存V值中。多个线程共用V值都是如此操作。CAS的核心是在将B值写入到V之前要比较A值和V值是否相同,如果不相同证明此时V值已经被其他线程改变,重新将V值赋给A,并重新计算得到B,如果相同,则将B值赋给V。
值得注意的是CAS机制中的这步步骤是原子性的(从指令层面提供的原子操作),所以CAS机制可以解决多线程并发编程对共享变量读写的原子性问题。
AtomicInteger
Unsafe
Unsafe是位于sun.misc包下的一个类,主要提供一些用于执行低级别、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等,这些方法在提升Java运行效率、增强Java语言底层资源操作能力方面起到了很大的作用。但由于Unsafe类使Java语言拥有了类似C语言指针一样操作内存空间的能力,这无疑也增加了程序发生相关指针问题的风险。在程序中过度、不正确使用Unsafe类会使得程序出错的概率变大,使得Java这种安全的语言变得不再“安全”,因此对Unsafe的使用一定要慎重。
AtomicInteger中使用unsafe
AtomicInteger 的源码
public class AtomicInteger extends Number implements java.io.Serializable {
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
//用于获取value字段相对当前对象的“起始地址”的偏移量
valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
//返回当前值
public final int get() {
return value;
}
//递增加detla
public final int getAndAdd(int delta) {
//三个参数,1、当前的实例 2、value实例变量的偏移量 3、当前value要加上的数(value+delta)。
//增加的时候遇到其他线程使用会自旋,见下文unsafe类的介绍
return unsafe.getAndAddInt(this, valueOffset, delta);
}
//递增加1
public final int incrementAndGet() {
//增加的时候遇到其他线程使用会自旋,见下文unsafe类的介绍
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
...
}
unsafe中的cas,
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
//通过var1和var5找到主内存中的值
var5 = this.getIntVolatile(var1, var2);
//比较var5是否改变,如果没变就直接赋值并返回,改变了继续循环
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
在cpp中compareAndSwapInt是这样的
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
UnsafeWrapper("Unsafe_CompareAndSwapInt");
oop p = JNIHandles::resolve(obj);
jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END
最终c++层是lock cmpxchg
cmpxchg不是原子性的,因为compare的时候有可能其他cpu修改内存值
lock能够保证原子性
所以说cas属于轻量级锁(无锁,自旋锁),可以说他不是锁,因为确实没有上锁,但是又有锁的效果,说他是自旋锁,是因为他呈现出来的就是自旋的样子
锁升级
偏向锁
当一个线程使用未被访问过的资源的时候,给资源贴上标签,属于这个线程。而这种情况下也是这个线程访问这个资源,无竞争。而这个贴标签的过程就是使用一个54位的指针指向当前线程
轻量级锁
只要发生任何竞争,就从偏向锁升级成轻量级锁。首先会撤销偏向锁,然后每个线程栈中增加一个LR(Lock Record)指针,每个线程通过自旋的方式去访问资源。具体情况如下:
假设线程A和线程B
线程A先获取公共锁到自己的内存中,假设此时锁的指针是指向1
然后A把自己的LR指针赋值到锁中,此时锁指向A
A再去获取一下公共锁的指针,此时1的状态是可改变,就把当前的指向A的锁放回去。否则如果获取的锁改变,就重新来,自旋。
类似于上面的cas图片
自适应自旋锁
竞争的线程太多,或者某个线程占用的时间太长,导致其他锁自旋的时间太长,消耗cpu资源。
jdk1.6之前,如果自旋次数超过10次,或者自旋线程数量超过cpu核数的一半的时候,就变成重量级锁。
jdk1.6之后引入了自适应自旋锁
重量级锁
竞争的线程太多,或者某个线程占用的时间太长,导致其他锁自旋的时间太长,消耗cpu资源。升级成重量级锁。重量级锁需要向操作系统申请
锁消除
public void add(String a, String b){
StringBuffer sb = new StringBuffer();
sb.append(a).append(b);
}
例如stringbuffer,是线程安全的,如果在使用他的时候,正常内部会上锁。但是如果JVM判断sb并不会被其他代码引用,则将该代码带默认为线程独有的资源,并不需要同步,所以执行了锁消除操作。
锁粗化
public void test(String a){
int i = 0;
StringBuffer sb = new StringBuffer();
while(i < 100){
sb.append(a);
i++
}
return sb.toString();
}
锁会加到while上
根据例子学习锁
Object在内存对象中占用多少字节?
markword -> 8
classpointer -> 4(compressed)
null对齐 -> 4(凑到8的倍数,8字节64位)
o引用本身 -> 4
一共20字节
classpointer和简单对象指针都进行了压缩,从8变成4
超线程
JMM(java内存模型)
多线程中就是三个性质1.可见性, 2.原子性,3.有序性。下面的锁对应不同的特性
锁 | 原理 | 使用场景 |
---|---|---|
final | 实现了Java内存模型的,有序性 | 一个变量要被多个线程访问(类私有) |
volatile | 实现了Java内存模型的,可见性、有序性 | 一个变量要被多个线程访问(存放在共享内存内,共享) |
synchronized | 实现了Java内存模型的,原子性、可见性、有序性 | |
Atomic | CAS | 一个变量要被多个线程访问 |
synchronized
synchronized是重量级锁
synchronized实现了多线程的有序性,可见性,原子性
代码块使用锁block,其他代码块需要使用block的时候需要等待锁释放。
在代码块处的锁可以是this
当前实例。
可以是某个类AnyClass.class
,这样所有的实例公用一把锁。
synchronized (block) {
System.out.println("block1锁,我是线程" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("block1锁,"+Thread.currentThread().getName() + "结束");
}
synchronized (this) {
...
}
synchronized (AnyClass.class) {
...
}
synchronized用在普通方法上,默认的锁就是this,当前实例,不同的实例可以同时访问。
如果作用在static修饰的方法上那就代表着任何实例访问都有一个公共锁。
public synchronized void method() {
System.out.println("我是线程" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "结束");
}
public static synchronized void method() {
...
}
注意事项
性能问题:多线程竞争一个锁时,其余未得到锁的线程只能不停的尝试获得锁,而不能中断。高并发的情况下会导致性能下降。
1.锁对象不能为空,因为锁的信息都保存在对象头里
2.作用域不宜过大,影响程序执行的速度,控制范围过大,编写代码也容易出错
3.避免死锁
4.在能选择的情况下,既不要用Lock也不要用synchronized关键字,用java.util.concurrent包中的各种各样的类,如果不用该包下的类,在满足业务的情况下,可以使用synchronized关键,因为代码量少,避免出错
缺陷
1.效率低:锁的释放情况少,只有代码执行完毕或者异常结束才会释放锁;试图获取锁的时候不能设定超时,不能中断一个正在使用锁的线程,相对而言,Lock可以中断和设置超时
2.不够灵活:加锁和释放的时机单一,每个锁仅有一个单一的条件(某个对象),相对而言,读写锁更加灵活
3.无法知道是否成功获得锁,相对而言,Lock可以拿到状态,如果成功获取锁,…,如果获取失败,…
volatile
volatile实现了可见性,有序性,但是不能保证原子性比如i++在多线程中i使用volatile就不行
线程间可见性
如下代码,如果running不加上volatile,那么程序就会卡住。如果加上了,主线程和t1线程之间就可共享running值。修改会重新写入内存,读取会重新从内存中拿
有序性(happens-before)
//假设线程A执行writer方法,线程B执行reader方法
class VolatileExample {
int a = 0;
volatile boolean flag = false;
public void writer() {
a = 1; // 1 线程A修改共享变量
flag = true; // 2 线程A写volatile变量
}
public void reader() {
if (flag) { // 3 线程B读同一个volatile变量
int i = a; // 4 线程B读共享变量
……
}
}
}
根据程序次序规则:1 happens-before 2 且 3 happens-before 4。
根据 volatile 规则:2 happens-before 3。
根据 happens-before 的传递性规则:1 happens-before 4。
上面这个happens-before规则是怎么来的呢,想要深入理解,还得先看下文
DCL例子,禁止重排序
下面的例子为例
多线程下的单例模式(DCL)下的双重检查
public class Singleton {
public static volatile Singleton singleton;
/**
* 构造函数私有,禁止外部实例化
*/
private Singleton() {};
public static Singleton getInstance() {
//检查1
if (singleton == null) {
synchronized (singleton.class) {
//检查2
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
可以模拟一下两个线程访问未初始化的资源,最终这种双重检查不会出现问题。
这种可以不使用volatile吗?你看我这个是单例模式,多线程下访问的也只有这一个singleton对象,为什么还要加入volatile呢?
答案是,通过内存屏障防止重排序
Java中的指令重排序有两次,第一次发生在将字节码编译成机器码的阶段,第二次发生在CPU执行的时候,也会适当对指令进行重排。
重排序意思就是,代码执行的顺序可能不是你自己定义的,可能是一个乱序,只不过执行的逻辑不变。单线程没问题,但是多线程就会有问题。
上面的代码重排序可能发生在对象new的时候,字节码编译成机器码时重排序。比如正常的new过程是先半初始化Singleton类为null(0),然后再初始化赋值(4),最后singleton变量和实例关联(7)
如果4和7被重排了,那么多个线程获取singleton的时候有可能有获取值为null的可能,这是另一个线程在初始化的时候还没执行完,同时被重排指令了。这个时候再看上面的代码,就可能new 两个单例变量
什么是内存屏障呢?
详细文章看看:https://segmentfault.com/a/1190000038355290
前后两个操作之间的屏障。
简单理解就是屏障是墙,阻挡前后。volatile读不可以和下面的任何写重排序。volatile写不可以和下面的读,上面的写重排序。
下面是具体的规则
在每个 volatile 写操作的前面插入一个 StoreStore 屏障。
在每个 volatile 写操作的后面插入一个 StoreLoad 屏障。
在每个 volatile 读操作的后面插入一个 LoadLoad 屏障。
在每个 volatile 读操作的后面插入一个 LoadStore 屏障。
内存屏障 | 说明 |
---|---|
StoreStore 屏障 | 禁止上面的普通写和下面的 volatile 写重排序。 |
StoreLoad 屏障 | 防止上面的 volatile 写与下面可能有的 volatile 读/写重排序。 |
LoadLoad 屏障 | 禁止下面所有的普通读操作和上面的 volatile 读重排序。 |
LoadStore 屏障 | 禁止下面所有的普通写操作和上面的 volatile 读重排序。 |
final
final基础部分比较容易理解
写重排序规则
写final域的重排序规则可以确保:在对象引用为任意线程可见之前,对象的final域已经被正确初始化过了
其中变量a可以重排序到初始化函数之外,而变量b一定在初始化函数之内,因为构造函数return之前,插入一个storestore屏障。并且在reader都b之前保证先初始化了。
public class FinalDemo {
private int a; //普通域
private final int b; //final域
private static FinalDemo finalDemo;
public FinalDemo() {
a = 1; // 1. 写普通域
b = 2; // 2. 写final域
}
public static void writer() {
finalDemo = new FinalDemo();
}
public static void reader() {
FinalDemo demo = finalDemo; // 3.读对象引用
int a = demo.a; //4.读普通域
int b = demo.b; //5.读final域
}
public static void main(String[] args){
//读写线程
Tread(FinalDemo::writer,"t1").start();
Tread(FinalDemo::reader,"t2").start();
}
}
读重排序规则
读final域的重排序规则可以确保:在读一个对象的final域之前,一定会先读这个包含这个final域的对象的引用。
还是上面的代码,意思是在执行int b = demo.b
的时候一定先执行FinalDemo demo = finalDemo
,这个是final变量,读取引用和读取变量之间有一个LoadLoad屏障。
相反int a = demo.a
可以窜到FinalDemo demo = finalDemo
前面执行(不是很理解)
final是否有线程可见性?
最后想说一点,final无法保证线程间可见性
举例:线程线程A执行wirterOne方法,执行完后线程B执行writerTwo方法,然后线程C执行reader方法
public class FinalReferenceDemo {
final int[] arrays;
private FinalReferenceDemo finalReferenceDemo;
public FinalReferenceDemo() {
arrays = new int[1]; //1
arrays[0] = 1; //2
}
public void writerOne() {
finalReferenceDemo = new FinalReferenceDemo(); //3
}
public void writerTwo() {
arrays[0] = 2; //4
}
public void reader() {
if (finalReferenceDemo != null) { //5
int temp = finalReferenceDemo.arrays[0]; //6
}
}
}
其中可以保证temp读取的内容=1,但是arrays[0] = 2在哪块执行就未必了
参考:
https://segmentfault.com/a/1190000022904663
https://pdai.tech/md/java/thread/java-thread-x-key-volatile.html#volatile-%E7%9A%84-happens-before-%E5%85%B3%E7%B3%BB
https://segmentfault.com/a/1190000038355290