乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度。java
乐观锁认为对于同一个数据的并发操做,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断从新的方式更新数据。乐观的认为,不加锁的并发操做是没有事情的。算法
乐观锁在Java中的使用,是无锁编程,经常采用的是CAS算法,典型的例子就是原子类,经过CAS自旋实现原子操做的更新。编程
悲观锁认为对于同一个数据的并发操做,必定是会发生修改的,哪怕没有修改,也会认为修改。所以对于同一个数据的并发操做,悲观锁采起加锁的形式。悲观的认为,不加锁的并发操做必定会出问题。数组
悲观锁适合写操做很是多的场景,乐观锁适合读操做很是多的场景,不加锁会带来大量的性能提高。缓存
分段锁实际上是一种锁的设计,并非具体的一种锁。多线程
咱们以ConcurrentHashMap
来讲一下分段锁的含义以及设计思想,ConcurrentHashMap
中的分段锁称为Segment,它即相似于HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每一个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。当须要put元素的时候,并非对整个hashmap进行加锁,而是先经过hashcode来知道他要放在那一个分段中,而后对这个分段进行加锁,因此当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。并发
分段锁的设计目的是细化锁的粒度,当操做不须要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操做。函数
公平锁是指多个线程按照申请锁的顺序来获取锁。性能
非公平锁是指多个线程获取锁的顺序并非按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会形成优先级反转或者饥饿现象。优化
对于Java ReentrantLock
而言,经过构造函数指定该锁是不是公平锁,默认是非公平锁。非公平锁的优势在于吞吐量比公平锁大。
对于Synchronized
而言,也是一种非公平锁。因为其并不像ReentrantLock
是经过AQS的来实现线程调度,因此并无任何办法使其变成公平锁。
不可重入锁,即若当前线程执行某个方法已经获取了该锁,那么在方法中尝试再次获取锁时,就会获取不到被阻塞。必须先释放锁,才能从新获取。
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。
对于Java ReentrantLock
而言, 他的名字就能够看出是一个可重入锁,其名字是Re entrant Lock
从新进入锁。
对于Synchronized
而言,也是一个可重入锁。可重入锁的一个好处是可必定程度避免死锁。
独享锁是指该锁一次只能被一个线程所持有。
共享锁是指该锁可被多个线程所持有。
对于Java ReentrantLock
而言,其是独享锁。可是对于Lock的另外一个实现类ReadWriteLock
,其读锁是共享锁,其写锁是独享锁。读锁的共享锁可保证并发读是很是高效的,读写,写读 ,写写的过程是互斥的。
Synchronized也是独享锁。
独享锁/共享锁这是广义上的说法,互斥锁/读写锁就分别对应具体的实现。
Java中的具体实现ReentrantLock、Synchronized。
java中具体实现ReentrantReadWriteLock,容许多个读线程同时访问,但不容许写线程和读线程、写线程和写线程同时访问。读写锁内部维护了两个锁,一个用于读操做,一个用于写操做。
ReentrantReadWriteLock支持如下功能:
1)支持公平和非公平的获取锁的方式;
2)支持可重入。读线程在获取了读锁后还能够获取读锁;写线程在获取了写锁以后既能够再次获取写锁又能够获取读锁;
3)还容许从写入锁降级为读取锁,其实现方式是:先获取写入锁,而后获取读取锁,最后释放写入锁。可是,从读取锁升级到写入锁是不容许的;
4)读取锁和写入锁都支持锁获取期间的中断;
5)Condition支持,仅写入锁提供一个 Conditon 实现;读取锁不支持 Conditon,readLock().newCondition() 抛出 UnsupportedOperationException。
使用
示例一:利用重入来执行升级缓存后的锁降级。
class CachedData { Object data; volatile boolean cacheValid; //缓存是否有效 ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); void processCachedData() { rwl.readLock().lock(); //获取读锁 //若是缓存无效,更新cache;不然直接使用data if (!cacheValid) { // Must release read lock before acquiring write lock //获取写锁前须释放读锁 rwl.readLock().unlock(); rwl.writeLock().lock(); // Recheck state because another thread might have acquired // write lock and changed state before we did. if (!cacheValid) { data = ... cacheValid = true; } // Downgrade by acquiring read lock before releasing write lock //锁降级,在释放写锁前获取读锁 rwl.readLock().lock(); rwl.writeLock().unlock(); // Unlock write, still hold read } use(data); rwl.readLock().unlock(); //释放读锁 } }
示例二:使用 ReentrantReadWriteLock 来提升 Collection 的并发性
class RWDictionary { private final Map<String, Data> m = new TreeMap<String, Data>(); private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); private final Lock r = rwl.readLock(); //读锁 private final Lock w = rwl.writeLock(); //写锁 public Data get(String key) { r.lock(); try { return m.get(key); } finally { r.unlock(); } } public String[] allKeys() { r.lock(); try { return m.keySet().toArray(); } finally { r.unlock(); } } public Data put(String key, Data value) { w.lock(); try { return m.put(key, value); } finally { w.unlock(); } } public void clear() { w.lock(); try { m.clear(); } finally { w.unlock(); } } }
实现原理
ReentrantReadWriteLock含有两把锁readerLock和writerLock,其中ReadLock和WriteLock都是内部类。
ReentrantReadWriteLock 也是基于AQS实现的,它的自定义同步器(继承AQS)须要在同步状态(一个整型变量state)上维护多个读线程和一个写线程的状态,使得该状态的设计成为读写锁实现的关键。若是在一个整型变量上维护多种状态,就必定须要“按位切割使用”这个变量,读写锁将变量切分红了两个部分,高16位表示读,低16位表示写。
这里不分析具体如何获取锁和释放锁。
阻塞的代价
java的线程是映射到操做系统原生线程之上的,若是要阻塞或唤醒一个线程就须要操做系统介入,须要在用户态与内核态之间切换,这种切换会消耗大量的系统资源,由于用户态与内核态都有各自专用的内存空间,专用的寄存器等,用户态切换至内核态须要传递给许多变量、参数给内核,内核也须要保护好用户态在切换时的一些寄存器值、变量等,以便内核态调用结束后切换回用户态继续工做。
自旋锁
自旋锁是指尝试获取锁的线程不会当即阻塞,而是采用循环的方式去尝试获取锁。
自旋锁的原理很是简单,若是持有锁的线程能在很短期内释放锁资源,那么那些等待竞争锁的线程就不须要作内核态和用户态之间的切换进入阻塞挂起状态,它们只须要等一等(自旋),等持有锁的线程释放锁后便可当即获取锁,这样就避免用户线程和内核的切换的消耗。
可是线程自旋是须要消耗cup的,说白了就是让cup在作无用功,若是一直获取不到锁,那线程也不能一直占用cup自旋作无用功,因此须要设定一个自旋等待的最大时间。若是自旋超过最大时间仍然没法获取到锁,这时线程会中止自旋进入阻塞状态。
优缺点:
优势:自旋锁尽量的减小线程的阻塞,这对于锁的竞争不激烈,且占用锁时间很是短的代码块来讲性能能大幅度的提高,由于自旋的消耗会小于线程阻塞挂起再唤醒的操做的消耗,这些操做会致使线程发生两次上下文切换。
缺点:若是锁的竞争激烈,或者持有锁的线程须要长时间占用锁执行同步块,这时候就不适合使用自旋锁了,由于自旋锁在获取锁前一直都是占用cpu作无用功。
因此自旋锁适合竞争不是很激烈且执行的同步块时间较短的状况下。
这三种锁是指锁的状态或阶段,而且都是针对Synchronized
的。从jdk1.6开始为了减小得到锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”。
Synchronized
锁共有四种状态,级别从低到高分别是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。随着竞争状况锁状态逐渐升级、锁能够升级但不能降级。
偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。下降获取锁的代价。
轻量级锁是指当锁是偏向锁的时候,再有另外一个线程访问时,偏向锁就会升级为轻量级锁,其余线程会经过自旋的形式尝试获取锁,不会阻塞,提升性能。
重量级锁是指当锁为轻量级锁的时候,另外一个线程过来获取锁,此时锁被占用该线程自旋获取锁,但自旋不会一直持续下去,当自旋必定次数的时候,尚未获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其余申请的线程进入阻塞,性能下降。
锁的实现:
ReenTrantLock的实现是一种自旋锁,经过循环调用CAS操做来实现加锁。它的性能比较好也是由于避免了使线程进入内核态的阻塞状态。想尽办法避免线程进入内核的阻塞状态是咱们去分析和理解锁设计的关键钥匙。
Synchronized原始的Synchronized是依赖于JVM实现的,性能比较差,但自JDK1.6以后引入了偏向锁、轻量级锁,同时也借鉴了ReentrantLock的CAS思想实现加锁,优化以后的性能已经和ReentrantLock基本相同。官方甚至建议使用synchronized。
区别:
便利性:很明显Synchronized的使用比较方便简洁,而且由编译器去保证锁的加锁和释放,而ReenTrantLock须要手工声明来加锁和释放锁,为了不忘记手工释放锁形成死锁,因此最好在finally中声明释放锁。
锁的细粒度和灵活度:很明显ReenTrantLock优于Synchronized。
ReenTrantLock独有能力:
除了须要用到以上三点功能时其余都推荐使用synchronized方式加锁。