1.锁之公平锁与非公平锁

2.可重入锁(递归锁)

3.自旋锁

4.读锁写锁

1.锁之公平锁与非公平锁
我们先来了解一下,最基本的公平锁和非公平锁:

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

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

而对于JAVA常用的ReentrantLock和synchronize锁而言,是公平锁还是非公平锁呢?

我们先来看一下ReentrantLock的构造方法

Lock lock = new ReentrantLock();
/**
     * Creates an instance of {@code ReentrantLock}.
     * This is equivalent to using {@code ReentrantLock(false)}.
     */
    public ReentrantLock() {
    //默认为非公平锁
        sync = new NonfairSync();
    }

    /**
     * Creates an instance of {@code ReentrantLock} with the
     * given fairness policy.
     *
     * @param fair {@code true} if this lock should use a fair ordering policy
     */
    public ReentrantLock(boolean fair) {
    //如果传入为true,则是公平锁,注释说会使用先来后到的排序策略
        sync = fair ? new FairSync() : new NonfairSync();
    }

接下来我们再看一下synchronize关键字是公平锁还是非公平锁:

我们先定义一个synchronize方法

    public static synchronized void method01() {
        System.out.println(Thread.currentThread().getName() + "method01");
    }
    public static synchronized void method01() {
        System.out.println(Thread.currentThread().getName() + "method01");
    }

接下来我们循环启动多个线程,如果线程的编号是按照顺序执行的,则证明synchronize是公平锁,如果线程的编号是乱序执行的,则证明synchronize是非公平锁。

        for(int i =1;i<=10;i++){
            new Thread(()->{method01();},"t"+i).start();
        }

执行结果如下:
image.png
我们可以得出,synchronized也是非公平锁。

2.可重入锁(递归锁)
可重入锁 递归锁
可重入锁又叫递归锁,指的是同一线程在外层获取锁的时候,在进入内层方法会自动获取锁。是一种不会对其自身进行阻塞的锁,我知道这么说比较抽象,接下来我们距离进行说明。

我们先写两个同步方法A和B,其中A调用B,如果锁是不可重入锁,因为线程调用方法A时,已经获取锁,就没有办法获取方法B的锁了,但是可重入锁的话,线程调用同步方法A,方法A调用同步方法B,此时自动获取锁。


    public static synchronized void method01() {
        System.out.println(Thread.currentThread().getName() + "method01");
        method02();
    }

    public static synchronized void method02() {
        System.out.println(Thread.currentThread().getName() + "method02");
    }
    
new Thread(()->{method01();},"t1").start();

此时,线程不会造成死锁,而是顺利执行方法A和方法B
image.png

接下来,我们用ReentrantLock来示范一下可重入锁:

//我们使用这种模板来使用ReentrantLock
        lock.lock();
        try {
            //这里是方法体
        } finally {
            lock.unlock();
        }

我们新建一个phone对象,使用ReentrantLock,然后再用一个同步方法调用另外一个同步方法

public class Phone implements Runnable {

    Lock lock = new ReentrantLock();

    public void methodA() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "method01");
            methodB();
        } finally {
            lock.unlock();
        }

    }

    public void methodB() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "method02");
        } finally {
            lock.unlock();
        }
    }

    @Override
    public void run() {
        methodA();
    }
}
        Phone phone = new Phone();
        new Thread(phone, "t1").start();
        new Thread(phone, "t2").start();
        new Thread(phone, "t3").start();
        new Thread(phone, "t4").start();

运行结果为:
image.png

可以看出,是可重入锁,而且并没有死锁。

但是如果此时,我们修改一下方法会怎么样?

        //两个lock
  public void methodA() {
        lock.lock();
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "method01");
            methodB();
        } finally {
        //两个unlock
            lock.unlock();
            lock.unlock();
        }
    }

此时也能正常运行,但是如果加锁lock和解锁unlock的次数不一样,那就没有办法继续运行了,程序会死锁。
image.png

3.自旋锁

自旋锁我们在之前在介绍CAS的时候 JAVA并发编程——CAS概念以及ABA问题 介绍过,他指的是尝试获取锁的线程不会立即阻塞,而是采用循环的方式去获取锁,这样的好处是减少上下文切换的消耗,缺点是会消耗CPU。

   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));

        return var5;
    }

接下来我们将用代码来自行模拟一把自旋锁:

首先,我们需要一个原子引用类:

    AtomicReference<Thread> atomicReference = new AtomicReference<>();

因为我们等等要用到atomicReference的compareAndSet方法,再者对锁进行加锁和解锁的主体是线程,因为是线程获取锁,所以泛型是线程。

接下来定义一个加锁和解锁方法,利用自旋的方式加锁:

     public void myLock() {
        Thread thread = Thread.currentThread();
        //先获取当前线程
        //1.如果引用类没有线程,则交换
        //交换后返回值为true,因为加了取反,导致为false,跳出循环,加锁成功
        //2.如果原子引用类有线程,则返回false
        //取反后获得true,就可以有一直循环自旋的效果
        while (!atomicReference.compareAndSet(null, thread)) {

        }
        System.out.println(thread.getName() + " \t get lock!");
    }

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

接下来我们新建两个线程运行一下

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

        new Thread(() -> {
            spinLockDemo.myLock();//加锁
            try {
               //五秒钟之类,第二个线程无法进入
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            spinLockDemo.unLock();//解锁
        }, "t1").start();

        new Thread(() -> {
            spinLockDemo.myLock();//加锁
            spinLockDemo.unLock();//解锁
        }, "t2").start();

    }

运行结果为:

image.png

4.读锁写锁

此时我们引入一个新的概念:读写锁。

从前面几种锁的使用和介绍情况来看,我们每次只允许一个线程通过,其实效率还是挺多的,从真实的开发业务场景进行分析,其实很多时候,只要保证写操作的排他性,无需保证读操作的排他性。
接下来我们来模拟一遍读写锁,模拟一个键值对的缓存,使用读写锁

我们要用到一个及其重要的类:

//它有一个lock.writeLock().lock()和lock.readLock().lock()
//方法,可以保证读操作的共享性和写操作的排他性。
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

我们来书写一个Map,用来当缓存空间,然后读写锁对该缓存进行操作。

//因为是多线程操作,记得保证它的可见性,必须采用volatile。
private volatile Map<String, Object> map = new HashMap<>();

接下来就是最重要的读和写操作:

 /**
     * 写操作  原子性 独占性
     *
     * @param key
     * @param value
     * @throws InterruptedException
     */
    public void put(String key, Object value) throws InterruptedException {
        //使用写锁
        lock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t 正在写入" + key);
            Thread.sleep(1000);
            map.put(key, value);
            System.out.println(Thread.currentThread().getName() + "\t 写入完成");
        } finally {
            lock.writeLock().unlock();
        }
    }

    /**
     * 共享性,时间不等
     *
     * @param key
     * @return
     * @throws InterruptedException
     */
    public Object get(String key) throws InterruptedException {
        //使用读锁
        lock.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t 正在读取" + key);
            Thread.sleep(1000);
            Object object = map.get(key);
            System.out.println(Thread.currentThread().getName() + "\t 读取完成" + "对象为" + object.toString());
            return object;
        } finally {
            lock.readLock().unlock();
        }
    }

接下来我们试着运行一下:

 public static void main(String[] args) {
        CacheDemo cacheDemo = new CacheDemo();
        //五个线程写,五个线程读,保证读互斥,写共享
        for (int i = 0; i < 5; i++) {
            final int temp = i;
            new Thread(() -> {
                try {
                    cacheDemo.put(temp + "", temp + "");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }, "t1").start();
        }
        for (int i = 0; i < 5; i++) {
            final int temp = i;
            new Thread(() -> {
                try {
                    cacheDemo.get(temp + "");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }, "t2").start();
        }
    }

image.png

从运行结果发现,写操作时,会一个一个线程进行写入,因为使用了sleep方法,会有明显的间隔,完成一个再运行下一个,但是读线程因为是共享锁,就会一口气全部执行,按照时间片轮转法,获取时间片的线程随意进行读取。

总结:
总1.公平锁:指多个线程按申请锁的顺序来获取锁,类似排队打饭,先来后到。
**2.非公平锁:指多个线程获取锁的顺序并不是按照申请的顺序,有可能后申请的线程优先获取锁,在高并发的情况下,可能会造成优先级反转或者饥饿现象(饥饿现象就是线程永远获取不到锁)。
3.可重入锁:可重入锁又叫递归锁,指的是同一线程在外层获取锁的时候,在进入内层方法会自动获取锁。是一种不会对其自身进行阻塞的锁。
4.自旋锁:他指的是尝试获取锁的线程不会立即阻塞,而是采用循环的方式去获取锁,这样的好处是减少上下文切换的消耗,缺点是会消耗CPU。
5.读写锁:保证写操作的排他性,无需保证读操作的排他性,保证系统吞吐量。


苏凌峰
73 声望38 粉丝

你的迷惑在于想得太多而书读的太少。