锁机制
在java中的锁分为以下(其实就是按照锁的特性和设计来划分
1、公平锁/非公平锁
2、可重入锁
3、独享锁/共享锁
4、互斥锁/读写锁
5、乐观锁/悲观锁
6、分段锁
7、偏向锁/轻量级锁/重量级锁
8、自旋锁(java.util.concurrent包下的几乎都是利用锁)
从底层角度看常见的锁也就两种:Synchronized和Lock接口以及ReadWriteLock接口(读写锁)
从类关系看出Lock接口是jdk5后新添的来实现锁的功能,其实现类:ReentrantLock、WriteLock、ReadLock。
其实还有一个接口ReadWriteLock,读写锁(读读共享、读写独享、写读独享、写写独享)。
Lock接口与synchronized关键字本质上都是实现同步功能。
区别:ReentrantLock:使用上需要显示的获取锁和释放锁,提高可操作性、可中断的获取获取锁以及可超时的获取锁,默认是 非公平的但可以实现公平锁,悲观,独享,互斥,可重入,重量级锁。
ReentrantReadWriteLock:默认非公平但可实现公平的,悲观,写独享,读共享,读写,可重入,重量级锁。
synchronized:关键字,隐式的获取锁和释放锁,不具备可中断、可超时,非公平、互斥、悲观、独享、可重入的重量级Lock的使用也很简单:
Lock lock = new ReentrantLock(); lock.lock(); try{ }finally{ lock.unlock(); } //注意:不要将lock方法写在try块中,因为如果在获取锁的时候发生异常,异常抛出的同时也会导致锁无故的释 //放 否则会程序会报监视状态异常 Exception in thread "线程一" java.lang.IllegalMonitorStateException at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:155) at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1260) at java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:460) //ReentrantLock必须要在finally中unlock(), 否则,如果在被加锁的代码中抛出了异常,那么这个锁将会永远无法释放. //synchronized就没有这样的问题, 遇到异常退出时,会释放掉已经获得的锁。
Lock接口提供的 ,synchronized关键字所不具备的特性
以下测试代码,测试
Lock lock = new ReentrantLock(); final MMT m = new MMT(lock); Thread tt = new Thread(new Runnable() { @Override public void run() { System.out.println("线程一 开始执行。。。"); try { m.update("张三"); } catch (InterruptedException e) { System.out.println(Thread.currentThread().getName()+"被中断(锁释放)。。。"); } System.out.println("线程一 结束执行。。。"); } },"线程一"); Thread tt2 = new Thread(new Runnable() { @Override public void run() { System.out.println("线程二 开始执行。。。"); try { m.update("李四"); } catch (InterruptedException e) { // TODO Auto-generated catch block System.out.println(Thread.currentThread().getName()+"被中断(锁释放)。。。"); } System.out.println("线程二 结束执行。。。"); } },"线程二"); tt.start(); tt2.start(); //中断线程 tt.interrupt(); try { tt.join(); tt2.join(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } class MMT { String name; Lock lock=null; public MMT(Lock lock) { this.lock=lock; } public void update(String name) throws InterruptedException{ // lock.lock(); // boolean tryLock = lock.tryLock();//尝试获取锁 //中断只是在当前线程获取锁之前,或者当前线程获取锁的时候被阻塞 // lock.lockInterruptibly(); lock.tryLock(3000, TimeUnit.SECONDS); try{ setName(name); System.out.println(Thread.currentThread().getName()+" 变换后的姓名为"+name); }finally{ lock.unlock(); } } public void setName(String name) { this.name = name; } public String getName() { return name; } }
可实现公平锁
对于ReentrantLock而言,可实现公平锁 ,通过构造函数指定是否需要公平,默认是非公平,区别在与非公平随机性,并且高并发下吞吐量大,公平的话根据请求锁等待的时间长短,等待的长了优先,类似FIFO,吞吐量降低了。
锁绑定多个条件
指ReentrantLock对象可以同时绑定多个Condition条件对象,而在Synchroized中,锁对象的wait方法、notify方法、和notifyall方法可以实现一个隐含条件,如果需要多个,得额外的添加一个锁对象。在ReentrantLock中不需要,只需要创建多个条件对象即可(new Condition()),对应的await()、siganl()、signalAll()。
synchronized的优势
synchronized是在JVM层面上实现的,不但可以通过一些监控工具监控synchronized的锁定,而且在代码执行时出现异常,JVM会自动释放锁定,但是使用Lock则不行,lock是通过代码实现的,要保证锁定一定会被释放,就必须将unLock()放到finally{}中
应用场景:
在资源竞争不激烈的情况下,synchronized关键字的性能优与ReentrantLock,相反,ReentrantLock的性能保持常态,优于关键字。
按照其性质划分:
公平锁/非公平锁
公平锁指多个线程按照申请锁的顺序来依次获取锁。非公平锁指多个线程获取锁的顺序并不是按照申请锁的顺序来获取,有可能后申请锁的线程比先申请锁的线程优先获取到锁,此极大的可能会造成线程饥饿现象,迟迟获取不到锁。由于ReentrantLock是通过AQS来实现线程调度,可以实现公平锁,,但是synchroized是非公平的,无法实现公平锁。
/** * 公平锁与非公平锁测试 */ public class FairAndUnFairThreadT { public static void main(String[] args) throws InterruptedException { //默认非公平锁 final Lock lock = new ReentrantLock(true); final MM m = new MM(lock); for (int i=1;i<=20 ;i++){ String name = "线程"+i; Thread tt = new Thread(new Runnable() { @Override public void run() { for(int i=0;i<2;i++){ m.testReentrant(); } } },name); tt.start(); } } } class MM { Lock lock = null; MM(Lock lock){ this.lock = lock; } public void testReentrant(){ lock.lock(); try{ Thread.sleep(1); System.out.println(Thread.currentThread().getName() ); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } public synchronized void testSync(){ System.out.println(Thread.currentThread().getName()); } }
但是未必绝对就是按照顺序,可能因为CPU准备原因,可能个别会不是公平的。
乐观锁与悲观锁
不是指什么具体类型的锁,而是指在并发同步的角度。悲观锁认为对于共享资源的并发操作,一定是发生xi修改的,哪怕没有发生修改,也会认为是修改的,因此对于共享资源的操作,悲观锁采取加锁的方式,认为,不加锁的并发操作一定会出现问题。乐观锁认为对于共享资源的并发操作是不会发生修改的,在更新数据的时候,会采用尝试更新,不断重试的方式更新数据。乐观的认为,不加锁的并发操作共享资源是没问题的。从上面的描述看除,乐观锁不加锁的并发操作会带来性能上的提升,悲观锁的使用就是利用synchroized关键字或者lock接口的特性。乐观锁在java中的使用,是无锁编程常常采用的是CAS自旋锁,典型的例子就是并发原子类,通过CAS自旋(spinLock)来更新值。
独享锁与共享锁
独享锁是指该锁一次只能被一个线程所持有。共享锁是指可被多个线程所持有。在java中,对ReentrantLock对象以及synchroized关键字而言,是独享锁的。但是对于ReadWriteLock接口而言,其读是共享锁,其写操作是独享锁。读锁的共享锁是可保证并发读的效率,读写、写写、写读的过程中都是互斥的,独享的。独享锁与共享锁在Lock的实现中是通过 AQS(抽象队列同步器)来实现的。
互斥锁与读写锁
互斥锁与读写锁就是具体的实现,互斥锁在java 中的体现就是synchronized关键字以及Lock接口实现类ReentrantLock,读写锁在java中的具体实现就是ReentrantReadWriteLock。
可重入锁
又名递归锁,是指同一个线程在外层的方法获取到了锁,在进入内层方法会自动获取到锁。对于ReentrantLock和synchronized关键字都是可重入锁的。最大的好处就是能够避免一定程度的死锁。
public sychrnozied void test() { //执行逻辑,调用另一个加锁的方法 test2(); } public sychronized void test2() { //执行业务逻辑 }
在上面代码中,sychronized关键字加在类方法上,执行test方法获取当前对象作为监视器的对象锁,然后又调用test2同步方法。
一、如果锁是可重入的话,那么当前线程就在调用test2时并不需要再次获取当前锁对象,可以直接进入test2方法。
二、如果锁是不具备可重入的话,那么该线程在调用test2前会等待当前对象锁的释放,实际上该对象锁已被当前线程所持有不可能再此获得。那么就会发生死锁。
按照设计方案来分类(目的对锁的进一步优化)
自旋锁与自适应自旋锁(或者说是自旋锁的变种TicketLock、MCSLock、CLHLock)
底层采用CAS来保证原子性,自旋锁获取锁的时候不会阻塞,而是通过不断的while循环的方式尝试获取锁。优点:减少线程上下文切换的消耗,缺点是会消耗CPU。如果锁被占用的时间很短,自旋等待的效果就会非常好,反之,如果锁被占用的时间很长,那么自旋的线程只会白白消耗处理器资源,而不会做任何有用的工作,反而会带来性能上的浪费。
偏向锁、轻量级锁、重量级锁
这三种锁是指锁的状态,并且是针对Synchronized,在java通过引入锁升级的机制来实现高校的synchronized。锁的状态是通过对象监视器在对象头中的字段来表明的。
偏向锁:指一段同步代码一直被同一个线程s所访问,那么该线程会自动的获取锁。降低获取锁的代价。
轻量级锁:当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
重量级锁:当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没获取到锁就会进入阻塞,该锁膨胀为重量级锁。重量级会让其他申请线程阻塞,性能降低。
偏向所锁,轻量级锁都是乐观锁,重量级锁是悲观锁。一个对象刚开始实例化的时候,没有任何线程来访问它的时候。它是可偏向的,意味着,它现在认为只可能有一个线程来访问它,所以当第一个线程来访问它的时候,它会偏向这个线程,此时,对象持有偏向锁。偏向第一个线程,这个线程在修改对象头成为偏向锁的时候使用CAS操作,并将对象头中的ThreadID改成自己的ID,之后再次访问这个对象时,只需要对比ID,不需要再使用CAS在进行操作。
一旦有第二个线程访问这个对象,因为偏向锁不会主动释放,所以第二个线程可以看到对象时偏向状态,这时表明在这个对象上已经存在竞争了,检查原来持有该对象锁的线程是否依然存活,如果挂了,则可以将对象变为无锁状态,然后重新偏向新的线程,如果原来的线程依然存活,则马上执行那个线程的操作栈,检查该对象的使用情况,如果仍然需要持有偏向锁,则偏向锁升级为轻量级锁,(偏向锁就是这个时候升级为轻量级锁的)。如果不存在使用了,则可以将对象回复成无锁状态,然后重新偏向。
轻量级锁认为竞争存在,但是竞争的程度很轻,一般两个线程对于同一个锁的操作都会错开,或者说稍微等待一下(自旋),另一个线程就会释放锁。 但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁,重量级锁使除了拥有锁的线程以外的线程都阻塞,防止CPU空转。
分段锁
分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。我们以ConcurrentHashMap来说一下分段锁的含义以及设计思想,ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。
当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在那一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。
但是,在统计size的时候,可就是获取hashmap全局信息的时候,就需要获取所有的分段锁才能统计。分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。