锁的分类与对应的Java实现

按照锁的不同特性进行划分,可以划分如下锁:

  • 可重入锁/不可重入锁

可重入锁和不可重入锁

如果线程获取了当前实例的锁(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锁升级中

最后更新于