乐观锁、悲观锁、公平锁、自旋锁、偏向锁、轻量级锁、重量级锁、锁膨胀...难理解?不存的!来,话不多说,带你飙车。
上一篇介绍了线程池的使用,在享受线程池带给我们的性能优势之外,似乎也带来了另一个问题:线程安全的问题。
那什么是线程的安全问题呢?
一、线程安全问题的产生
线程安全问题:指的是在多线程编程中,同时操作同一个可变的资源之后,造成的实际结果与预期结果不一致的问题。
比如:A和B同时向C转账10万元。如果转账操作不具有原子性,A在向C转账时,读取了C的余额为20万,然后加上转账的10万,计算出此时应该有30万,但还未来及将30万写回C的账户,此时B的转账请求过来了,B发现C的余额为20万,然后将其加10万并写回。然后A的转账操作继续——将30万写回C的余额。这种情况下C的最终余额为30万,而非预期的40万。
如果上面的内容您还没有理解,没关系,我们来看下面非安全线程的模拟代码:
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.
二、线程安全的解决方案
线程安全的解决方案分为以下几个维度(参考《码出高效:Java开发手册》):
- 数据单线程可见(单线程操作自己的数据是不存在线程安全问题的,ThreadLocal就是采用这种解决方案);
- 数据只读;
- 使用线程安全类(比如StringBuffer就是一个线程安全类,内部是使用synchronized实现的);
- 同步与锁机制;
解决线程安全核心思想是:“要么只读,要么加锁”,解决线程安全的关键在于合理的使用Java提供的线程安全包java.util.concurrent简称JUC。
三、线程同步与锁
Java 5 以前,synchronized是仅有的同步手段,Java 5的时候增加了ReentrantLock(再入锁)它的语义和synchronized基本相同,比synchronized更加灵活,可以做到更多的细节控制,比如锁的公平性/非公平性指定。
3.1 synchronized
synchronized 是 Java 内置的同步机制,它提供了互斥的语义和可见性,当一个线程已经获取当前锁时,其他试图获取的线程只能等待或者阻塞在那里。
3.1.1 synchronized 使用
synchronized 可以用来修饰方法和代码块。
3.1.1.1 修饰代码块
synchronized (this) {     int former = number++;     int latter = number;     //... }3.1.1.2 修饰方法
public synchronized void add() {     //... }3.1.2 synchronized 底层实现原理
synchronized 是由一对 monitorenter/monitorexit 指令实现的,Monitor 对象是同步的基本实现单元。在 Java 6 之前,Monitor的实现完全是依靠操作系统内部的互斥锁,因为需要进行用户态到内核态的切换,所以同步操作是一个无差别的重量级操作,性能也很低。但在Java 6的时候,JVM 对此进行了大刀阔斧地改进,提供了三种不同的 Monitor 实现,也就是常说的三种不同的锁:偏向锁(Biased Locking)、轻量级锁和重量级锁,大大改进了其性能。
3.1.2.1 偏向锁/轻量级锁/重量级锁
偏向锁是为了解决在没有多线程的访问下,尽量减少锁带来的性能开销。
轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。
3.1.2.2 锁膨胀(升级)原理
Java 6 之后优化了 synchronized 实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,减低了锁带来的性能消耗,也就是我们常说的锁膨胀或者叫锁升级,那么它是怎么实现锁升级的呢?
锁膨胀(升级)原理: 在锁对象的对象头里面有一个ThreadId字段,在第一次访问的时候ThreadId为空,JVM让其持有偏向锁,并将ThreadId设置为其线程id,再次进入的时候会先判断ThreadId是否尤其线程id一致,如果一致则可以直接使用,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,不会堵塞,执行一定次数之后就会升级为重量级锁,进入堵塞,整个过程就是锁膨胀(升级)的过程。
3.1.2.3 自旋锁
自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。
3.1.2.4 乐观锁/悲观锁
悲观锁和乐观锁并不是某个具体的“锁”而是一种是并发编程的基本概念。
