谈对公平锁/非公平锁/可重入锁/递归锁/自旋锁的理解?请手写一个自旋锁

公平和非公平锁

是什么

公平锁: 是指多个线程按照申请锁的顺序来获取锁, 类似排队打饭, 先来后到。

非公平锁: 是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发的情况下, 有可能会造成优先级反转或者饥饿现象

两者的区别

公平锁 / 非公平锁

并发包中 ReentrantLock 的创建可以指定构造函数的 boolean 类型来得到公平锁或非公平锁,默认是非公平锁

/**
     * Creates an instance of {@code ReentrantLock}.
     * This is equivalent to using {@code ReentrantLock(false)}.
     */
    public ReentrantLock() {
        sync = new NonfairSync();
    }

关于两者区别:
公平锁: 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 lock happens to be available when it is requested.
非公平锁比较粗鲁,上来就直接尝试占有锁,如果尝试失败,就再采用类似公平锁那种方式。

题外话

Java ReentrantLock而言,
通过构造函数指定该锁是否是公平锁, 默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。

对于 Synchronized 而言, 也是一种非公平锁

可重入锁(又名递归锁)

是什么

可重入锁(也叫做递归锁)

指的是同一线程外层函数获得锁之后, 内层递归函数仍然能获取该锁的代码,在同一个线程在外层方法获取锁的时候, 在进入内层方法会自动获取锁

也即是说, 线程可以进入任何一个它已经拥有的锁所同步着的代码块。

ReentrantLock / Synchronized 就是一个典型的可重入锁

可重入锁最大的作用是避免死锁

ReentrantLockDemo 代码演示

package com.brian.interview.study.thread;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * Copyright (c) 2020 ZJU All Rights Reserved
 * <p>
 * Project: JavaSomeDemo
 * Package: com.brian.interview.study.thread
 * Version: 1.0
 * <p>
 * Created by Brian on 2020/2/12 11:01
 */

class Phone implements Runnable{
    public synchronized void sendSMS() throws Exception {
        System.out.println(Thread.currentThread().getName() + "\t invoked sendSMS()");
        sendEmail();
    }

    public synchronized void sendEmail() throws Exception {
        System.out.println(Thread.currentThread().getName() + "\t #####invoked sendEmail()");
    }

    // ==========================================================
    Lock lock = new ReentrantLock();
    @Override
    public void run() {
        get();
    }

    public void get() {
        lock.lock();
        lock.lock();   // 只要两两配对几把锁都可以
        try {
            // 线程可以进入任何一个它已经拥有的锁
            //
            // 所同步着的代码块
            System.out.println(Thread.currentThread().getName() + "\t invoked get()");
            set();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
            lock.unlock();
        }
    }

    public void set() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t #####invoked set()");
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
}

/**
 * 可重入锁(也叫做递归锁)
 *
 * 指的是同一线程外层函数获得锁之后, 内层递归函数仍然能获取该锁的代码,
 * 在同一个线程在外层方法获取锁的时候, 在进入内层方法会自动获取锁
 *
 * 也即是说, 线程可以进入任何一个它已经拥有的锁所同步着的代码块。
 *
 * case one synchronized 就是一个典型的可重入锁
 * t1	 invoked sendSMS()          t1线程在外层方法获取锁的时候
 * t1	 #####invoked sendEmail()   t1进入内层方法会自动获取锁
 *
 * t2	 invoked sendSMS()          t2线程在外层方法获取锁的时候
 * t2	 #####invoked sendEmail()   t2进入内层方法会自动获取锁
 *
 *
 * case two ReentrantLock 就是一个典型的可重入锁
 * t3	 invoked get()
 * t3	 #####invoked set()
 * t4	 invoked get()
 * t4	 #####invoked set()
 */
public class ReentrantLockDemo {
    public static void main(String[] args) {
        Phone phone = new Phone();

        new Thread(() -> {
            try {
                phone.sendSMS();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "t1").start();

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

        Thread t3 = new Thread(phone, "t3");
        Thread t4 = new Thread(phone, "t4");

        t3.start();
        t4.start();
    }
}

自旋锁 (spinlock)

是指尝试获取锁的线程不会立即阻塞, 而是采用循环的方式去尝试获取锁, 这样的好处是减少线程上下文切换的消耗, 缺点是循环会消耗CPU
在这里插入图片描述

package com.brian.interview.study.thread;

/**
 * Copyright (c) 2020 ZJU All Rights Reserved
 * <p>
 * Project: JavaSomeDemo
 * Package: com.brian.interview.study.thread
 * Version: 1.0
 * <p>
 * Created by Brian on 2020/2/12 11:47
 */

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

/**
 * 题目:实现一个自旋锁
 * 自旋锁好处:循环比较获取直到成功为止, 没有类似 wait 的阻塞。
 *
 * 通过CAS操作完成自旋锁, A线程先进来调用 myLock 方法自己持有锁5秒钟, B随后进来后发现
 * 当前有线程持有锁, 不是 null, 所以只能通过自旋等待, 直到A释放锁后B随后抢到。
 */
public class SpinLockDemo {

    // 原子引用线程
    AtomicReference<Thread> atomicReference = new AtomicReference<>();

    public void myLock(){
        Thread thread = Thread.currentThread();
        System.out.println(Thread.currentThread().getName() + "\t come in (⊙o⊙)");
        while (!atomicReference.compareAndSet(null, thread)){

        }
    }

    public void myUnlock(){
        Thread thread = Thread.currentThread();
        atomicReference.compareAndSet(thread, null);
        System.out.println(Thread.currentThread().getName()+"\t invoked myUnlock()");
    }

    public static void main(String[] args) {
        SpinLockDemo spinLockDemo = new SpinLockDemo();

        new Thread(() -> {
            spinLockDemo.myLock();

            // 暂停一会儿线程
            try {
                TimeUnit.SECONDS.sleep(5);
            }catch (InterruptedException e) {
                e.printStackTrace();
            }

            spinLockDemo.myUnlock();
        }, "AA").start();

        // 暂停一会儿线程
        try {
            TimeUnit.SECONDS.sleep(1);
        }catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -> {
            spinLockDemo.myLock();

            try {
                TimeUnit.SECONDS.sleep(1);
            }catch (InterruptedException e) {
                e.printStackTrace();
            }

            spinLockDemo.myUnlock();
        }, "BB").start();
    }
}

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

独占锁:指该锁一次只能被一个线程所持有。对 ReentrantLock 和 Synchronized 而言都是独占锁

共享锁:指该锁可被多个线程所持有。
对 ReentrantReadWriteLock 其读锁是共享锁, 其写锁是独占锁。
读锁的共享锁可保证并发读是非常高效的, 读写, 写读, 写写的过程是互斥的。

package com.brian.interview.study.thread;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * Copyright (c) 2020 ZJU All Rights Reserved
 * <p>
 * Project: JavaSomeDemo
 * Package: com.brian.interview.study.thread
 * Version: 1.0
 * <p>
 * Created by Brian on 2020/2/12 13:44
 */

class MyCache {  // 资源类
    private volatile Map<String, Object> map = new HashMap<>();
    private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();

    public void put(String key, Object value) {

        rwLock.writeLock().lock();
        try{
            System.out.println(Thread.currentThread().getName() + "\t 正在写入:" + key);
            // 暂停一会儿线程
            try {
                TimeUnit.MILLISECONDS.sleep(300);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
            map.put(key, value);
            System.out.println(Thread.currentThread().getName() + "\t 写入完成");
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            rwLock.writeLock().unlock();
        }
    }

    public void get(String key) {
        rwLock.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t 正在读取");
            // 暂停一会儿线程
            try {
                TimeUnit.MILLISECONDS.sleep(300);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
            Object result = map.get(key);
            System.out.println(Thread.currentThread().getName() + "\t 读取完成:" + result);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            rwLock.readLock().unlock();
        }
    }

    public void clearMap(){
        map.clear();
    }
}

/**
 * 多个线程同时读一个资源类没有任何问题, 所以为了满足并发量, 读取共享资源应该可以同时进行。
 * 但是
 * 如果有一个线程想去写共享资源来, 就不应该再有其它线程可以对该资源进行读或写
 * 小总结:
 *     读-读能共存
 *     读-写不能共存
 *     写-写不能共存
 *
 *     写操作:原子+独占, 整个过程必须是一个完整的统一体, 中间不许被分割, 不许被打断
 */
public class ReadWriteLockDemo {
    public static void main(String[] args) {
        MyCache myCache = new MyCache();

        for (int i = 1; i <= 5; i++) {
            final int tempInt = i;
            new Thread(() -> {
                myCache.put(tempInt+"", tempInt+"");
            }, String.valueOf(i)).start();
        }

        for (int i = 1; i <= 5; i++) {
            final int tempInt = i;
            new Thread(() -> {
                myCache.get(tempInt+"");
            }, String.valueOf(i)).start();
        }
    }
}

发布了77 篇原创文章 · 获赞 79 · 访问量 5707

猜你喜欢

转载自blog.csdn.net/qq_35340189/article/details/104570079