可重入锁
同一个线程进入外层方法获取锁之后,进入内层方法就无需在获取锁了(前提是锁的是同一个对象或者Class),可以在一定程度上避免死锁。ReentrantLock和synchronized都是可重入锁。
不可重入锁
同一个线程在外层获取锁后,在内层再次获取锁时,会阻塞。它在获取锁时,会判断status的值,如果不为0,直接阻塞;为0则可以获取锁。所以外层获取锁之后,想要在内层获取锁,就必须先释放外层的锁,置status为0,之后才能获取内层的锁,而要释放外层锁,必须离开外层锁控制的范围,也就是要继续执行代码获取内层锁。因此,就会产生死锁。
公平锁
线程按照队列中排队的顺序获得锁。
优点是线程不会饿死。
缺点是吞吐效率低于非公平锁,CPU每次都要唤醒所有线程,开销大
非公平锁
多个线程加锁时直接尝试获得锁,获取不到在排队获取。线程可能可以无需阻塞便直接获得锁
优点是减少唤醒线程的开销,吞吐效率高。
缺点是等待队列中的线程可能会饿死
Synchronized和Lock锁默认实现的都是非公平锁
共享锁
锁可被多个线程所持有。加共享锁前先判断同步资源是否有锁,如果有而且是共享锁,则可以继续加共享锁,如果是排它锁,则需等待排它锁被释放后才能加共享锁。如果没有锁,则可以直接加共享锁,且之后的线程只能加共享锁。获得共享锁的线程只能读数据,不能修改数据。读锁的就是共享锁的一种具体实现。
排他锁
锁一次只能被一个线程所持有。如果同步资源加上排它锁后,其他线程不能再加任何类型的锁。获得排它锁的线程即能读数据又能修改数据。Synchronized,lock锁(读锁除外)都是排它锁的具体实现
独占锁
同排他锁
死锁
两个或者两个以上线程在执行过程中产生了交叉闭环申请,互相都在等对方先释放锁,然后自己才能释放,因此程序就停滞了
死锁产生的四个条件:
1)互斥。共享资源同时只能被一个线程访问。
2)占有且等待。线程T1在取得共享资源A的时候,请求等待资源B的时候并不释放资源A。
3)不可抢占。其他线程不能强行抢占线程的资源。
4)循环等待条件。线程T1在持有资源A1,同时在请求等待获取资源B,线程T2在持有资源B,然后在请求等待线程T1的持有资源,形成了交叉闭环申请。
悲观锁
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。适用于多写场景
乐观锁
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,如果被更新了,则按照实现方式会有对应的处理方法,如抛出异常、重试等。乐观锁在Java中采用无锁编程来实现,可以使用版本号机制和CAS算法实现,原子类中的递增操作就是通过CAS自旋实现的。乐观锁比较适用于多读场景(少写),可以节省上锁的开销,加大吞吐量。
自旋锁
如果线程获取同步资源的锁失败,表示资源被占用,当前线程会进行自旋(循环等待,但线程会一直处于用户态,不用进入内核态),如果自旋完成后占用同步资源的线程已经释放了锁,那么当前线程就可直接获得锁,省去了线程开销的过程;如果还是失败,则继续自旋直至超过一定次数。缺点是自旋需要消耗CPU,如果占用资源的线程阻塞了,发生死锁了,碰到异常了,那自旋操作就无用了,反而白白耗费了CPU资源。所以自旋锁适用于锁竞争不激烈,并且持有锁的线程占用锁时间较短的场景,若是锁竞争激烈,则可能存在某个线程一直自旋但一直被其他线程抢占锁,导致自旋无用
读写锁
ReadWriteLock,接口内部提供读锁和写锁两个方法,实现类为ReetrantReadWriteLock,默认是非公平的。
读锁是共享锁,同步资源可以持有多个读锁。写锁是排它锁,在获取写锁前,如果同步资源已经获得锁,则线程被阻塞直至之前的锁被释放。
写锁可以降级为读锁,获取写锁->获取读锁->释放写锁。
读线程获取读锁之后,可以再次获取读锁。写线程获取写锁后可以再次获取写锁,也能获取读锁
无锁
针对synchronized关键字下的锁的状态。无锁没有对资源进行锁定,一般采用CAS算法加版本号来实现,线程不断尝试修改同步资源,失败了就不断重试直至成功。
偏向锁
jdk1.6以后。它会偏向于第一个访问锁的线程,如果在运行过程中,只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,这种情况下,就会给线程加一个偏向锁(在Mark Word里存储锁偏向的线程ID)。在线程进入和退出同步块时不再通过CAS操作来加锁和解锁,而是检测Mark Word里是否存储着指向当前线程的偏向锁。如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。
优点是减少不必要的轻量级锁执行路径,加锁和解锁不需要额外消耗,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令即可。和非同步方法相比,仅有纳秒级差距。
缺点是如果存在锁竞争,会有额外的锁撤销的消耗。
适用于只有一个线程访问同步资源的场景。
偏向锁应该可以说是可重入锁的一种具体实现方式,只是他是jvm层面的,实现方式不同于jdk层面的reentrantlock
轻量级锁
jdk1.6以后。当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。
加锁过程:
1、在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。
2、拷贝对象头中的Mark Word复制到锁记录中。
3、拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word。
4、如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态
5、如果轻量级锁的更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行,否则说明多个线程竞争锁。若当前只有一个等待线程,则该线程通过自旋进行等待。但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。
优点是线程不会阻塞,程序响应速度提高
缺点是如果始终得不到锁的线程,自旋会一直消耗CPU;如果锁竞争激烈,轻量级锁将会很快膨胀为重量级锁,那维持轻量级锁的过程(未获取锁的线程先自旋等待(自旋锁),然后膨胀为重量级锁)就白费了
适用于追求响应速度,同步块执行速度快的场景
重量级锁
如果多个线程竞争锁,轻量级锁就要膨胀为重量级锁,后面等待锁的线程也要进入阻塞状态,而当前线程便尝试使用自旋来获取锁。
优点是线程竞争不消耗资源,没有自旋了
缺点是如果没有获得锁,线程会阻塞,响应速度变慢
适用于追求吞吐量,同步块执行速度慢的场景