常州网站建设制作工作室,深圳龙岗招聘网,sketch做网站线框图,沈阳建设厅网站首页乐观锁、悲观锁、公平锁、自旋锁、偏向锁、轻量级锁、重量级锁、锁膨胀…难理解#xff1f;不存的#xff01;来#xff0c;话不多说#xff0c;带你飙车。 
上一篇介绍了线程池的使用#xff0c;在享受线程池带给我们的性能优势之外#xff0c;似乎也带来了另一个问题不存的来话不多说带你飙车。 
上一篇介绍了线程池的使用在享受线程池带给我们的性能优势之外似乎也带来了另一个问题线程安全的问题。 
那什么是线程的安全问题呢 
一、线程安全问题的产生 
线程安全问题指的是在多线程编程中同时操作同一个可变的资源之后造成的实际结果与预期结果不一致的问题。 比如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() {Overridepublic void run() {threadSafeSample.add();}});Thread threadB  new Thread(new Runnable() {Overridepublic void run() {threadSafeSample.add();}});threadA.start();threadB.start();threadA.join();threadB.join();}
}我电脑运行的结果 非相等  former5555 latter6061 
可以看到仅仅是两个线程的低度并发就非常容易碰到 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 乐观锁/悲观锁 
悲观锁和乐观锁并不是某个具体的“锁”而是一种是并发编程的基本概念。 
悲观锁认为对于同一个数据的并发操作一定是会发生修改的哪怕没有修改也会认为修改。因此对于同一个数据的并发操作悲观锁采取加锁的形式。悲观的认为不加锁的并发操作一定会出问题。 
乐观锁则与 Java 并发包中的 AtomicFieldUpdater 类似也是利用 CAS 机制并不会对数据加锁而是通过对比数据的时间戳或者版本号来实现乐观锁需要的版本判断。 
3.1.2.5 公平锁/非公平锁 
公平锁是指多个线程按照申请锁的顺序来获取锁。 
非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序有可能后申请的线程比先申请的线程优先获取锁。 
如果使用 synchronized 使用的是非公平锁是不可设置的这也是主流操作系统线程调度的选择。通用场景中公平性未必有想象中的那么重要Java 默认的调度策略很少会导致 “饥饿”发生。非公平锁的吞吐量大于公平锁。 
非公平锁吞吐量大于公平锁的原因 
比如A占用锁的时候B请求获取锁发现被A占用之后堵塞等待被唤醒这个时候C同时来获取A占用的锁如果是公平锁C后来者发现不可用之后一定排在B之后等待被唤醒而非公平锁则可以让C先用在B被唤醒之前C已经使用完成从而节省了C等待和唤醒之间的性能消耗这就是非公平锁比公平锁吞吐量大的原因。 
3.2 ReentrantLock 
ReentrantLock只能修饰代码块使用ReentrantLock必须手动unlock释放锁不然锁永远会被占用。 
3.2.1 ReentrantLock 使用 
ReentrantLock reentrantLock  new ReentrantLock(true); // 设置为true为公平锁默认是非公平锁
reentrantLock.lock();
try {}finally {reentrantLock.unlock();
}3.2.2 ReentrantLock 优势 具备尝试非阻塞地获取锁的特性当前线程尝试获取锁如果这一时刻锁没有被其他线程获取到则成功获取并持有锁  能被中断地获取锁的特性与synchronized不同获取到锁的线程能够响应中断当获取到锁的线程被中断时中断异常将会被抛出同时锁会被释放  超时获取锁的特性在指定的时间范围内获取锁如果截止时间到了仍然无法获取锁则返回。  
3.2.3 ReentrantLock 注意事项 
在finally中释放锁目的是保证在获取锁之后最终能够被释放不要将获取锁的过程写在try块内因为如果在获取锁时发生了异常异常抛出的同时也会导致锁无故被释放ReentrantLock提供了一个newCondition的方法以便用户在同一锁的情况下可以根据不同的情况执行等待或唤醒的动作 
3.3 synchronized和ReentrantLock区别 
从性能角度synchronized 早期的实现比较低效对比 ReentrantLock大多数场景性能都相差较大。但是在 Java 6 中对其进行了非常多的改进在高竞争情况下ReentrantLock 仍然有一定优势。在大多数情况下无需太纠结于性能还是考虑代码书写结构的便利性、可维护性等。 
主要区别如下 
ReentrantLock使用起来比较灵活但是必须有释放锁的配合动作ReentrantLock必须手动获取与释放锁而synchronized不需要手动释放和开启锁ReentrantLock只适用于代码块锁而synchronized可用于修饰方法、代码块等 
参考资料 
《码出高效Java开发手册》 
Java核心技术36讲http://t.cn/EwUJvWA 
Java中的锁分类https://www.cnblogs.com/qifengshi/p/6831055.html