乐观锁、悲观锁、公平锁、自旋锁、偏向锁、轻量级锁、重量级锁、锁膨胀...难理解?不存的!来,话很少说,带你飙车。html
上一篇介绍了线程池的使用,在享受线程池带给咱们的性能优点以外,彷佛也带来了另外一个问题:线程安全的问题。java
那什么是线程的安全问题呢?编程
线程安全问题:指的是在多线程编程中,同时操做同一个可变的资源以后,形成的实际结果与预期结果不一致的问题。安全
好比:A和B同时向C转帐10万元。若是转帐操做不具备原子性,A在向C转帐时,读取了C的余额为20万,而后加上转帐的10万,计算出此时应该有30万,但还将来及将30万写回C的帐户,此时B的转帐请求过来了,B发现C的余额为20万,而后将其加10万并写回。而后A的转帐操做继续——将30万写回C的余额。这种状况下C的最终余额为30万,而非预期的40万。bash
若是上面的内容您尚未理解,不要紧,咱们来看下面非安全线程的模拟代码:多线程
public class ThreadSafeSample {
public int number;
public void add() {
for (int i = 0; i < 100000; i++) {
int former = number++;
int latter = number;
if (former != latter-1){
System.out.printf("非相等 former=" + former + " latter=" + latter);
}
}
}
public static void main(String[] args) throws InterruptedException {
ThreadSafeSample threadSafeSample = new ThreadSafeSample();
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
threadSafeSample.add();
}
});
Thread threadB = new Thread(new Runnable() {
@Override
public void run() {
threadSafeSample.add();
}
});
threadA.start();
threadB.start();
threadA.join();
threadB.join();
}
}
复制代码
我电脑运行的结果: 非相等 => former=5555 latter=6061
并发
能够看到,仅仅是两个线程的低度并发,就很是容易碰到 former 和 latter 不相等的状况。这是由于,在两次取值的过程当中,其余线程可能已经修改了number.ide
线程安全的解决方案分为如下几个维度(参考《码出高效:Java开发手册》):性能
解决线程安全核心思想是:“要么只读,要么加锁”,解决线程安全的关键在于合理的使用Java提供的线程安全包java.util.concurrent简称JUC。优化
Java 5 之前,synchronized是仅有的同步手段,Java 5的时候增长了ReentrantLock(再入锁)它的语义和synchronized基本相同,比synchronized更加灵活,能够作到更多的细节控制,好比锁的公平性/非公平性指定。
synchronized 是 Java 内置的同步机制,它提供了互斥的语义和可见性,当一个线程已经获取当前锁时,其余试图获取的线程只能等待或者阻塞在那里。
synchronized 能够用来修饰方法和代码块。
synchronized (this) {
int former = number++;
int latter = number;
//...
}
复制代码
public synchronized void add() {
//...
}
复制代码
synchronized 是由一对 monitorenter/monitorexit 指令实现的,Monitor 对象是同步的基本实现单元。在 Java 6 以前,Monitor的实现彻底是依靠操做系统内部的互斥锁,由于须要进行用户态到内核态的切换,因此同步操做是一个无差异的重量级操做,性能也很低。但在Java 6的时候,JVM 对此进行了大刀阔斧地改进,提供了三种不一样的 Monitor 实现,也就是常说的三种不一样的锁:偏向锁(Biased Locking)、轻量级锁和重量级锁,大大改进了其性能。
偏向锁是为了解决在没有多线程的访问下,尽可能减小锁带来的性能开销。
轻量级锁是指当锁是偏向锁的时候,被另外一个线程所访问,偏向锁就会升级为轻量级锁,其余线程会经过自旋的形式尝试获取锁,不会阻塞,提升性能。
重量级锁是指当锁为轻量级锁的时候,另外一个线程虽然是自旋,但自旋不会一直持续下去,当自旋必定次数的时候,尚未获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其余申请的线程进入阻塞,性能下降。
Java 6 以后优化了 synchronized 实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,减低了锁带来的性能消耗,也就是咱们常说的锁膨胀或者叫锁升级,那么它是怎么实现锁升级的呢?
锁膨胀(升级)原理: 在锁对象的对象头里面有一个ThreadId字段,在第一次访问的时候ThreadId为空,JVM让其持有偏向锁,并将ThreadId设置为其线程id,再次进入的时候会先判断ThreadId是否尤为线程id一致,若是一致则能够直接使用,若是不一致,则升级偏向锁为轻量级锁,经过自旋循环必定次数来获取锁,不会堵塞,执行必定次数以后就会升级为重量级锁,进入堵塞,整个过程就是锁膨胀(升级)的过程。
自旋锁是指尝试获取锁的线程不会当即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减小线程上下文切换的消耗,缺点是循环会消耗CPU。
悲观锁和乐观锁并非某个具体的“锁”而是一种是并发编程的基本概念。
悲观锁认为对于同一个数据的并发操做,必定是会发生修改的,哪怕没有修改,也会认为修改。所以对于同一个数据的并发操做,悲观锁采起加锁的形式。悲观的认为,不加锁的并发操做必定会出问题。
乐观锁则与 Java 并发包中的 AtomicFieldUpdater 相似,也是利用 CAS 机制,并不会对数据加锁,而是经过对比数据的时间戳或者版本号,来实现乐观锁须要的版本判断。
公平锁是指多个线程按照申请锁的顺序来获取锁。
非公平锁是指多个线程获取锁的顺序并非按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。
若是使用 synchronized 使用的是非公平锁,是不可设置的,这也是主流操做系统线程调度的选择。通用场景中,公平性未必有想象中的那么重要,Java 默认的调度策略不多会致使 “饥饿”发生。非公平锁的吞吐量大于公平锁。
非公平锁吞吐量大于公平锁的缘由:
好比A占用锁的时候,B请求获取锁,发现被A占用以后,堵塞等待被唤醒,这个时候C同时来获取A占用的锁,若是是公平锁C后来者发现不可用以后必定排在B以后等待被唤醒,而非公平锁则可让C先用,在B被唤醒以前C已经使用完成,从而节省了C等待和唤醒之间的性能消耗,这就是非公平锁比公平锁吞吐量大的缘由。
ReentrantLock只能修饰代码块,使用ReentrantLock必须手动unlock释放锁,否则锁永远会被占用。
ReentrantLock reentrantLock = new ReentrantLock(true); // 设置为true为公平锁,默认是非公平锁
reentrantLock.lock();
try {
}finally {
reentrantLock.unlock();
}
复制代码
具有尝试非阻塞地获取锁的特性:当前线程尝试获取锁,若是这一时刻锁没有被其余线程获取到,则成功获取并持有锁;
能被中断地获取锁的特性:与synchronized不一样,获取到锁的线程可以响应中断,当获取到锁的线程被中断时,中断异常将会被抛出,同时锁会被释放;
超时获取锁的特性:在指定的时间范围内获取锁;若是截止时间到了仍然没法获取锁则返回。
从性能角度,synchronized 早期的实现比较低效,对比 ReentrantLock,大多数场景性能都相差较大。可是在 Java 6 中对其进行了很是多的改进,在高竞争状况下,ReentrantLock 仍然有必定优点。在大多数状况下,无需太纠结于性能,仍是考虑代码书写结构的便利性、可维护性等。
主要区别以下:
参考资料
《码出高效:Java开发手册》
Java核心技术36讲:t.cn/EwUJvWA
Java中的锁分类:www.cnblogs.com/qifengshi/p…
课程推荐: