按照锁的不同特性进行划分,可以划分如下锁:
可重入锁和不可重入锁
如果线程获取了当前实例的锁(this),并进入方法A,这个线程在没有释放这把锁时,这个线程是否能再次进入方法A?
可重入锁 :可以再次进入,方法A递归了 (线程可以进入任何一个它已经拥有的锁所同步着的代码块。) 不可重入锁 :不可再次进入,只有等待锁被释放,才能进入方法A
在Java中,synchronized和ReentrantLock都是可重入锁。
复制 // 可重入锁,正常执行,不会出现死锁,如果是自旋锁,会发生死锁
public class UnReentrant{
Lock lock = new Lock();
public void outer(){
lock.lock();
inner();
lock.unlock();
}
public void inner(){
lock.lock();
//do something
lock.unlock();
}
}
可重入锁的基本原理,记录获取锁的线程,如果是记录的线程,就对代码放行:
复制 public class Lock{
boolean isLocked = false;
Thread lockedBy = null;
int lockedCount = 0;
public synchronized void lock() throws InterruptedException{
Thread callingThread = Thread.currentThread();
while(isLocked && lockedBy != callingThread){
wait();
}
isLocked = true;
lockedCount++;
lockedBy = callingThread;
}
public synchronized void unlock(){
if(Thread.curentThread() == this.lockedBy){
lockedCount--;
if(lockedCount == 0){
isLocked = false;
notify();
}
}
}
}
公平锁和非公平锁
公平锁 即尽量以请求锁的顺序来获取锁(内部拥有一个队列)。比如同时有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该锁,这种就是公平锁。
非公平锁 即无法保证锁的获取是按照请求锁的顺序进行的。这样就可能导致某个或者一些线程永远获取不到锁。
在Java中,synchronized 是非公平锁,无法保证等待线程获取锁的顺序
创建公平锁:
复制 ReentrantLock lock = new ReentrantLock(true) // true 表示公平锁,false表示非公平锁
可中断锁与不可中断锁
如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。
在Java中,synchronized就不是可中断锁,而Lock是可中断锁。 Lock.lockInterruptibly()
就是一种可中断锁:
复制 ReentrantLock lock = new ReentrantLock();
try {
lock.lockInterruptibly(); // 如果迟迟获取不到,可以通过interrupt方法打断等待
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
try {
lock.unlock();
} catch (IllegalMonitorStateException ignored) {
}
}
共享锁与排他锁
共享锁 :如果事务T对数据A加上共享锁后,则其他事务只能对A再加共享锁,不能加排他锁。获准共享锁的事务只能读数据,不能修改数据。
排他锁 :如果事务T对数据A加上排他锁后,则其他事务不能再对A加任任何类型的封锁。获准排他锁的事务既能读数据,又能修改数据。
读写锁
读锁是共享锁,写锁是排他锁 。在Java中,针对读写锁,提供了 ReentrantReadWriteLock
类:
复制 public class ReadWriteLockDemo {
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
public static void main(String[] args) {
final ReadWriteLockDemo demo = new ReadWriteLockDemo();
new Thread(() -> demo.get(Thread.currentThread())).start();
new Thread(() -> demo.get(Thread.currentThread())).start();
}
public void get(Thread thread) {
rwl.readLock().lock();
try {
long start = System.currentTimeMillis();
while(System.currentTimeMillis() - start <= 1) {
System.out.println(thread.getName()+"正在进行读操作");
}
System.out.println(thread.getName()+"读操作完毕");
} finally {
rwl.readLock().unlock();
}
}
}
读读不互斥,读写互斥,写写互斥
悲观锁与乐观锁
悲观锁 :总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如 行锁,表锁等,读锁,写锁 等,都是在做操作之前先上锁。Java中synchronized和ReentrantLock 等独占锁就是悲观锁思想的实现。
乐观锁 :总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法 实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中 java.util.concurrent.atomic
包 下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
悲观锁与乐观锁的使用场景 : 从上面对两种锁的介绍,我们知道两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。
自旋锁
自旋锁是一种乐观锁,因为采用CAS实现,通过不断循环获取锁来实现无锁机制: juc.atomic
下的所有类都是由CAS+while循环实现,他们都是自旋锁。
复制 public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2); // 获取对象最新的值
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); // 如果不是期望值(var5仍是当前值,说明没有被其他线程修改),返回false,如果是期望值则设置为 var5+1 目标值
return var5;
}
偏向锁
偏向锁出现在synchronized锁升级中