目录
synchronized的升级策略: 无锁 --> 偏向锁 --> 轻量级锁 -->重量级锁 (这是程序在运行时,jvm做出的优化策略)
锁策略: (synchronized实现的策略)
synchronized并非只是一个简单的加锁过程,它实现的策略还是比较复杂的。
synchronized的升级策略: 无锁 --> 偏向锁 --> 轻量级锁 -->重量级锁 (这是程序在运行时,jvm做出的优化策略)
无锁:
无锁就是简单的不涉及加锁问题,没有线程安全问题。
偏向锁:
偏向锁是产生锁竞争时,不是一下子就立马加上锁了,相当于有一个中间的过程,在代码写完加锁之后,先对锁对象有一个标记,如果再有一个线程来了,和我竞争同一把锁,此时我就会咔嚓加锁,让那个线程进行阻塞等待,但是如果没有人和我竞争同一把锁,我就只是做一个标记,不会对锁对象真正的进行加锁。(加锁也是有成本的,所以非必要就不加锁)
轻量级锁:
如果此时产生了锁竞争,就进行加锁,加锁之后,就变成了一个轻量级锁(通常时自旋锁),自旋锁很快,此时锁竞争还不激烈(如两个线程竞争同一把锁),另一个进入阻塞等待的线程就会进入自旋状态(是一个while循环,一直判断竞争的锁对象是否被释放,while会循环的很快,自己一直旋转一样)。
等到锁被释放之后,能在第一时间拿到锁进行加锁。(但是一直while循环,是要耗费CPU的资源的,所以在锁竞争不激烈时是自旋锁状态)。
重量级锁:
当锁竞争非常激烈时(比如10个线程竞争同一把锁),此时就会升级成重量级锁,重量级锁,因为自旋锁是非常消耗CPU的资源的,一个线程自旋还好,如果是10个线程恐怕就对CPU不是很友好了,所以会升级成重量级锁,重量级锁是暂时不参与CPU的调度,进入阻塞等待状态,等锁被释放了,再等内核去调度这剩下的9个线程,所以很难第一时间就拿到锁,系统的调度是随机的,所以极端情况下可能需要等很长时间才会拿到锁。
锁消除:
遵非必要不加锁的策略。(这个优化还是比较保守的)。是编译时期做出的优化手段,检测当前代码是否是多线程执行的,或者判断当前代码是否有必要加锁,如果不必要,但是又把锁给写了,此时在编译的过程中会自动的把锁去掉。
(注:synchronized不应该被乱用,就像是StringBuffer就把关键的方法都加上了synchronized关键字,那如果是单线程使用StringBuffer,是不会涉及到线程安全问题的,但是也加上了锁,此时就相当于锁没有被真正的编译)
锁粗化:
首先介绍锁粒度:synchronized代码块包含的代码的多少,代码越多,锁粒度就越粗,代码越少锁粒度就越细。 一般在写代码的过程中,多数情况下,是希望锁粒度更小的,因为只要加锁,此时就是不能并发执行的,是串行;串行的代码越少,并发执行的代码就越多。
但是只是一般情况下,如果某个场景,要频繁的加锁 / 解锁,此时编译器就可能把这个加解锁的操作优化成一个粒度更粗的锁。 如下图: 尤其是每一次加锁都是有开销的,而释放锁之后又要重新进行锁竞争,所以很难保证第一时间拿到锁;此时的小路反而可能会更低。 所以会优化成只是加一次锁,最后再释放锁。
cas优化过程:(无锁编程)
就是无锁也能解决线程安全的问题。
cas的意思是:compare and swap(比较并交换),比较的是寄存器A的值和内存中M的值,如果值是相同的,就把寄存器B 的值和M中的值进行交换。(寄存器B的值大概也是来自另一个内存)
通常情况下,不考虑寄存器中的值是否改变,更关心的是内存的数值(相当于变量的值)(此种情况就是前文说过的多线程不安全问题中的不是原子操作,叫做内存可见性)。如下图: 两个线程对M中的值进行自增操作,
(1)如果不加锁,多个线程之间执行是并发的,很可能编译器从原来的内存中读变量优化成从寄存器中读变量,此时会出bug,但是引入cas的优化之后,可以不加锁,然后进行比较。
(2)如果寄存器A中的值现在加到了2,写回了内存M中,同时t2线程也正在进行操作,就先不进行++操作,先比较一下A和M中的值是否是相等的。
(3)如果是相等的就说明A已经对M中的值进行了自增,那B就是把自己寄存器中的值更新成M的值,然后在这个基础上再进行自增,此时多线程安全问题就解决了。
所以说cas是无锁编程。
cas实现原子类:
在Java标准库中,给我们提供了原子类,如AtomicInteger,就能保证在多线程情况下++和--操作是安全的。就可以在不加锁的情况下实现线程安全。同样的,现在两个线程再对num自增50000次,此时的结果就是正确的。(AtomicInteger中的参数是把num初始化成几的意思,类中提供的方法getAndIncrement是++操作,有前置++后置++还有--的操作都封装成了方法)。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.atomic.AtomicInteger;
public class ThreadDemo32 {
public static void main(String[] args) throws InterruptedException {
AtomicInteger num = new AtomicInteger(0);
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
//num++
num.getAndIncrement();
//++num
/*num.incrementAndGet();
num.getAndDecrement();
num.decrementAndGet();*/
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
num.getAndIncrement();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(num);
}
}
我们可以看一下AtomicInteger类的源码,如下图:此时方法中并没有加锁操作,那是如何实现线程安全的呢?
我们可以来看一段伪代码: 可以具体看一下这个伪代码的意思,就是这样实现线程安全的。
注:上述代码刚把oldValue赋值给value,这个值能不相等嘛,,此时是可以不相等的,因为是多线程,如果t1线程刚进行到赋值操作,此时就来了t2线程,再赋值,再比较,再自增,此时value的值就改变了,所以,如果不相等,就先进行给寄存器(oldValue)赋值,然后再进行自增操作。
(此处的CAS就是检测value是否变过,如果没变过---直接自增即可 如果变过了---先赋值再自增)
cas实现自旋锁:
我们再来看一段伪代码: 自旋锁就是这么实现的。优点:当锁被其他线程释放后,第一时间就可以拿到锁。 缺点:一直循环,进入忙等状态,消耗CPU的资源。
注:cas优化操作,并非是一段代码,而是一条CPU指令,这一条指令本身就已经是原子性的了,就能完成某些功能(比较再赋值)然后使线程是安全的了,所以cas优化是要靠CPU的支持的。
cas的a -> b -> a 问题: cas只能是比较是值是否相同,但是不能确定这个值是否中间发生过改变,如果是从a 变成了b,然后又变成了a,此时这个问题该如何解决??(经典面试题,重点掌握)
解:关键是aba反复横跳的问题,如果是约定a只能单方向变化呢,就是只能增加或者只能减小,问题就解决了。 但是如果需求是既能增加也能减小咋办? 可以引入一个版本号的变量,约定版本号只能是增加的,(注:每次修改都会增加一个版本号,此时每次增加或者减小的时候就看这个版本号变没变过,再决定当前是否自增),
只要约定的版本号只能是递增的,就可以保证不会出现aba来回变的情况了。以版本号为准,而不是以那个变量为准了。