公平锁/非公平锁/可重入锁/递归锁/自旋锁

1,105 阅读6分钟

引言:更多相关请看 JAVA并发编程系列

公平锁和非公平锁

公平锁:多个线程按照申请锁的顺序来获取锁类似排队打饭,先来后到。获取不到锁的时候,会自动加入队列,等待线程释放后,队列的第一个线程获取锁。
非公平锁:在多线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取到锁,在高并发的情况下,有可能造成优先级反转或者饥饿现象。获取不到锁的时候,会自动加入队列,等待线程释放锁后所有等待的线程同时去竞争。

二者区别

并发包ReentrantLock的创建可以指定构造函数的boolean类型来得到公平锁或者非公平锁 默认是非公平锁。 Java ReentrantLock而言。
公平锁: Threads acquire a fair lock in the order in which they requested it。就是很公平,在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照FIFO(先进先出)的规则从队列中取到自己。 非公平锁: a nonfair lock permits barging: threads requesting a lock can jump ahead of the queue of waiting threads if the lockhappens to be available when it is requested。非公平锁比较粗鲁,上来就直接尝试占有锁,如果尝试失败,就再采用类似公平锁那种方式。

题外话

synchronized也是一种非公平锁,ReentrantLock默认是非公平锁。

可通过构造函数指定该锁是否是公平锁。true公平锁、false非公平锁。
非公平锁的优点在于吞吐量比公平锁大。

可重入锁(又名递归锁)

含义:同一个线程可以反复获取锁多次,然后需要释放多次。同一线程外层函数获得锁之后,内层递归函数仍然可以获取该锁的代码,在同一线程在外层方法获取锁的时候+,在进入内层方法会自动获取锁。也就是说,线程可以进入任何一个它已经拥有的锁所同步着的代码块。ReentrantLock/synchronized就是典型的可重入锁。
可重入锁最大的作用就是避免死锁。

案例

class Stu implements Runnable {

    /**
     * 学习
     */
    public synchronized void study() {
        System.out.println(Thread.currentThread().getName() + "学习方法");
        rest();
    }

    /**
     * 休息
     */
    public synchronized void rest() {
        System.out.println(Thread.currentThread().getName() + "休息方法");
    }

    @Override
    public void run() {
        sleep();
    }

    Lock lock = new ReentrantLock();

    /**
     * 睡觉
     */
    public void sleep() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "睡觉方法");
            eat();
        } finally {
            lock.unlock();
        }
    }

    /**
     * 吃饭
     */
    public void eat() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "吃饭方法");
        } finally {
            lock.unlock();
        }
    }

}

class ReenterLockDemo {

    public static void main(String[] args) {
//        synTest();
        /**
         * 执行结果如下:
         * t1学习方法
         * t1休息方法
         * t2学习方法
         * t2休息方法
         */
        reenTest();
        /**
         * 执行结果如下:
         * t4睡觉方法
         * t4吃饭方法
         * t3睡觉方法
         * t3吃饭方法
         */
    }

    static Stu stu = new Stu();

    /**
     * synchronized版可重入锁演示
     */
    public static void synTest() {
        new Thread(() -> {
            try {
                stu.study();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "t1").start();

        new Thread(() -> {
            try {
                stu.study();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "t2").start();
    }

    /**
     * ReentrantLock版可重入锁演示
     */
    public static void reenTest() {
        new Thread(stu, "t3").start();
        new Thread(stu, "t4").start();
    }
}

自旋锁SpinLock

含义:尝试获取的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。本质就是一个while/do while循环。
Unsafe类加CAS思想。如典型的Unsafe类的getAndAddInt就是一个典型的自旋锁。

手写自旋锁

class SpinDemo {

    AtomicReference<Thread> atomicReference = new AtomicReference<>();// 未设置初始值,默认为null

    public void lock() {
        System.out.println(Thread.currentThread().getName()+"进入lock方法");
        // 如果能把默认的值从空设置为线程就跳出,反之就一直在此死循环。
        while (!atomicReference.compareAndSet(null, Thread.currentThread())) {

        }
    }

    public void unlock() {
        System.out.println(Thread.currentThread().getName()+"进入unlock方法");
        atomicReference.compareAndSet(Thread.currentThread(),null);
    }

    public static void main(String[] args) {
        SpinDemo spinDemo = new SpinDemo();
        new Thread(()->{
            spinDemo.lock();
            // 休息一会
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            spinDemo.unlock();
        }, "AAA").start();

        // 休息一会
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(()->{
            spinDemo.lock();
            // 休息一会
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            spinDemo.unlock();
        }, "BBB").start();
    }
}

独占锁(写)/共享锁(读)/互斥锁

独占锁:该锁一次只能被一个线程所持有。ReentrantLock和Synchronized都是独占锁。
共享锁:该锁可被多个线程所持有。
互斥锁:对ReentrantReadWriteLock其读锁是共享锁,其写锁是独占锁读锁的共享锁可保证并发读是非常高效的,读写、写读、写写的过程是互斥的。多个线程同时操作,一个资源类没有任何问题,所以为了满足并发量。读取共享资源应该可以同时进行。但是,如果有一个线程想去写共享资源来,就不应该有其他线程可以对资源进行读或写。
小总结:读、读能共存;读、写不能共存;写、写不能共存。 写操作:原子+独占,整个过程必须是一个完成的统一整体,中间不允许被分割、被打断。

ReentrantReadWriteLock读写锁

简介:现实中有这样一种场景:对共享资源有读和写的操作,且写操作没有读操作那么频繁。在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以应该允许多个线程同时读取共享资源;但是如果一个线程想去写这些共享资源,就不应该允许其他线程对该资源进行读和写的操作了。针对这种场景,JAVA的并发包提供了读写锁ReentrantReadWriteLock,它表示两个锁,一个是读操作相关的锁,称为共享锁;一个是写相关的锁,称为排他锁/独占锁。ReentrantReadWriteLock默认是非公平锁,可通过构造函数自定义公平非公平锁。

线程进入读锁的前提条件:
1、没有其他线程的写锁。
2、没有写请求或者有写请求,但调用线程和持有锁的线程是同一个。
3、线程进入写锁的前提条件:无其他线程的读锁或写锁。
读写锁有以下三个重要的特性:
(1)公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平。
(2)重进入:读锁和写锁都支持线程重进入。
(3)锁降级:遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁。

案例

/**
 * 资源类
 */
class MyCaChe {
    /**
     * 保证可见性
     */
    private volatile Map<String, Object> map = new HashMap<>();
    private ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();

    /**
     * 写
     *
     * @param key
     * @param value
     */
    public void put(String key, Object value) {
        reentrantReadWriteLock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t正在写入" + key);
            //模拟网络延时
            try {
                TimeUnit.MICROSECONDS.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            map.put(key, value);
            System.out.println(Thread.currentThread().getName() + "\t写入完成");
        } finally {
            reentrantReadWriteLock.writeLock().unlock();
        }
    }

    /**
     * 读
     *
     * @param key
     */
    public void get(String key) {
        reentrantReadWriteLock.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t正在读取");
            //模拟网络延时
            try {
                TimeUnit.MICROSECONDS.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Object result = map.get(key);
            System.out.println(Thread.currentThread().getName() + "\t读取完成" + result);
        } finally {
            reentrantReadWriteLock.readLock().unlock();
        }
    }

}

class ReadWriteLockDemo {
    public static void main(String[] args) {
        MyCaChe myCaChe = new MyCaChe();
        for (int i = 1; i <= 5; i++) {
            final int tem = i;
            new Thread(() -> {
                myCaChe.put(tem + "", tem);
            }, String.valueOf(i)).start();
        }
        for (int i = 1; i <= 5; i++) {
            final int tem = i;
            new Thread(() -> {
                myCaChe.get(tem + "");
            }, String.valueOf(i)).start();
        }

    }
}

执行效果:

保证写入状态不会被其它打断