多线程的这些锁知道吗?手写一个自旋锁?

多线程中的各种锁

1. 公平锁、非公平锁

1.1 概念:

公平锁就是先来后到、非公平锁就是允许加塞 Lock lock = new ReentrantLock(Boolean fair); 默认非公平

  • 公平锁是指多个线程按照申请锁的顺序来获取锁,类似排队打饭。
  • 非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程优先获取锁,在高并发的情况下,有可能会造成优先级反转或者节现象。

1.2 两者区别?

  • 公平锁:

    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

    非公平锁比较粗鲁,上来就直接尝试占有额,如果尝试失败,就再采用类似公平锁那种方式。

1.3 如何体现公平非公平?

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

image-20210707143515840

其中ReentrantLock和Synchronized默认都是非公平锁,默认都是可重入锁

2. 可重入锁(递归锁)

前文提到ReentrantLock和Synchronized默认都是非公平锁,默认都是可重入锁,那么什么是可重入锁?

2.1 概念

指的时同一线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码,在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁,也就是说,线程可以进入任何一个它已经拥有的锁所同步着的代码块

2.2 为什么要用到可重入锁?

  1. 可重入锁最大的作用是避免死锁
  2. ReentrantLock/Synchronized 就是一个典型的可重入锁

2.3 代码验证可重入锁?

首先我们先验证ReentrantLock

package com.yuxue.juc.lockDemo;


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

/**
 * 尝试验证ReentrantLock锁的可重入性的Demo
 * */
public class ReentrantLockDemo {
    public static void main(String[] args) {

        mobile mobile = new mobile();

        new Thread(mobile,"t1").start();
        new Thread(mobile,"t2").start();
    }
}

/**
 * 辅助类mobile,首先继承了Runnable接口,可以重写run方法
 * 内部主要有两个方法
 * run方法首先调用第一个方法
 * */
class mobile implements Runnable {

    Lock lock = new ReentrantLock();

    //run方法首先调用第一个方法
    @Override
    public void run() {
        testMethod01();
    }
    //第一个方法,目的是首先让线程进入方法1
    public void testMethod01() {
        //加锁
        lock.lock();
        try {
            //验证线程进入方法1
            System.out.println(Thread.currentThread().getName() + "\t" + "get in the method1");
            //休眠
            Thread.sleep(2000);
            //进入方法2
            testMethod02();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    //第二个方法。目的是验证ReentrantLock是否是可重入锁
    public void testMethod02() {
        lock.lock();
        try {
            System.out.println("==========" + Thread.currentThread().getName() + "\t" + "leave the method1 get in the method2");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

因为同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁,也就是说,线程可以进入任何一个它已经拥有的锁所同步着的代码块

之后观察输出结果为:

t1	get in the method1
==========t1	leave the method1 get in the method2
t2	get in the method1
==========t2	leave the method1 get in the method2

意味着线程t1进入方法1之后,再进入方法2,也就是说进入内层方法自动获取锁,之后释放方法2的那把锁,再释放方法1的那把锁,这之后线程t2才能获取到方法1的锁,才可以进入方法1

同样地,如果我们在方法1中再加一把锁,不给其解锁,也就是

image-20210707153757001

那么结果会是怎么呢?我们运行代码可以得到

image-20210707153858881

我们发现线程是停不下来的,线程t1进入方法1加了两把锁,之后进入t2,但是退出t1的方法过程中没有解锁,这就导致了t2线程无法拿到锁,也就验证了锁重入的问题

那么为了验证是同一把锁,我们在方法1对其加锁两次,方法2对其解锁两次可以吗?这锁是相同的吗?也就意味着:

image-20210707154054167

我们再次运行,发现结果为:

t1	get in the method1
==========t1	leave the method1 get in the method2
t2	get in the method1
==========t2	leave the method1 get in the method2

也就侧面验证了加锁的是同一把锁,更验证了我们的锁重入问题

那么对于synchronized锁呢?代码以及结果如下所示:

package com.yuxue.juc.lockDemo;

public class SynchronizedDemo {
    public static void main(String[] args) throws InterruptedException {
        phone phone = new phone();
        new Thread(()->{
            phone.phoneTest01();
        },"t1").start();
        Thread.sleep(1000);
        new Thread(()->{
            phone.phoneTest01();
        },"t2").start();
    }
}
class phone{
    public synchronized void phoneTest01(){
        System.out.println(Thread.currentThread().getName()+"\t invoked phoneTest01()");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        phoneTest02();
    }
    public synchronized void phoneTest02() {
        System.out.println(Thread.currentThread().getName()+"\t -----invoked phoneTest02()");
    }
}

结果为:

t1	 invoked phoneTest01()
t1	 -----invoked phoneTest02()
t2	 invoked phoneTest01()
t2	 -----invoked phoneTest02()

上述两个实验可以验证我们的ReentrantLock以及synchronized都是可重入锁!

3. 自旋锁

3.1 自旋锁概念

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

就是我们CAS一文当中提到的这段代码:

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

其中while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4))这行代码更体现出了自旋锁的核心,也就是当我尝试去拿锁的时候,一直循环,直到拿到锁为止

3.2 手写一个自旋锁试试?

下面的AtomicReference可以去看CAS那篇有详细讲解

package com.yuxue.juc.lockDemo;

import java.util.concurrent.atomic.AtomicReference;

/**
 * 实现自旋锁
 * 自旋锁好处,循环比较获取直到成功为止,没有类似wait的阻塞
 *
 * 通过CAS操作完成自旋锁,t1线程先进来调用mylock方法自己持有锁2秒钟,
 * t2随后进来发现当前有线程持有锁,不是null,所以只能通过自旋等待,直到t1释放锁后t2随后抢到
 */

public class SpinLockDemo {
    public static void main(String[] args) {
        //资源类
        SpinLock spinLock = new SpinLock();
        //t1线程
        new Thread(()->{
            //加锁
            spinLock.myLock();
            try {
                //休眠
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //解锁
            spinLock.myUnlock();
        },"t1").start();

        //这里主线程休眠,为了让t1首先得到并加锁
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //t2线程
        new Thread(()->{
            spinLock.myLock();
            try {
                //这里休眠时间较长是为了让输出结果更加可视化
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            spinLock.myUnlock();
        },"t2").start();
    }
}

class SpinLock {
    //构造原子引用类
    AtomicReference<Thread> atomicReference = new AtomicReference<>();

    //自己的加锁方法
    public void myLock() {
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName() + "\t" + "come in myLock");
        //自旋核心代码!!当期望值是null并且主内存值也为null,就将其设置为自己的thread,否则就死循环,也就是一直自旋
        while (!atomicReference.compareAndSet(null, thread)) {

        }
    }

    public void myUnlock() {
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName() + "\t" + "===== come in myUnlock");
        //解锁,用完之后,当期望值是自己的thread主物理内存的值也是自己的,也就是被自己的线程占用
        //用完之后解锁,将主物理内存中Thread的地方设置为空,供其他线程使用
        atomicReference.compareAndSet(thread, null);
    }
}

结果为:

//t1以及t2同时进入myLock方法,争夺锁使用权
t1	come in myLock
t2	come in myLock
//t1使用完首先释放锁
t1	===== come in myUnlock
//t2使用完释放锁,但是在5秒之后,因为在程序中我们让其休眠了5s
t2	===== come in myUnlock

4. 读写锁

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

4.1 概念

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

  • 共享锁:只该锁可被多个线程所持有

    ReentrantReadWriteLock其读锁是共享锁,写锁是独占锁

  • 互斥锁:读锁的共享锁可以保证并发读是非常高效的,读写、写读、写写的过程是互斥的

4.2 代码

首先是没有读写锁的代码,定义了一个资源类,里面底层数据结构为HashMap,之后多个线程对其写以及读操作

package com.yuxue.juc.lockDemo;

import java.util.HashMap;
import java.util.Map;

class MyCache {
    //定义缓存当中的数据结构为Map型,键为String,值为Object类型
    private volatile Map<String, Object> map = new HashMap<>();
    //向Map中添加元素的方法
    public void setMap(String key, Object value) {
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName() + "\t" + " put value:" + value);
        try {
            Thread.sleep(300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //底层的map直接put
        map.put(key, value);
        System.out.println(thread.getName() + "\t" + "put value successful");
    }
    //向Map中取出元素
    public void getMap(String key) {
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName() + "\t" + "get the value");
        try {
            Thread.sleep(300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //底层的map直接get,并且返回值为Object类型
        Object retValue = map.get(key);
        System.out.println(thread.getName() + "\t" + "get the value successful, is " + retValue);
    }

}

/**
 * 多个线程同时读一个资源类没有任何问题,所以为了满足并发量,读取共享资源应该可以同时进行。 * 但是
 * 如果有一个线程想取写共享资源来,就不应该允许其他线程可以对资源进行读或写
 * 总结
 * 读读能共存
 * 读写不能共存
 * 写写不能共存
 */
public class ReadWriteLockDemo {
    public static void main(String[] args) {
        //创建资源类
        MyCache myCache = new MyCache();
        //创建5个线程
        for (int i = 0; i < 5; i++) {
            //lambda表达的特殊性,需要final变量
            final int tempInt = i;
            new Thread(() -> {
                //5个线程分别填值
                myCache.setMap(tempInt + "", tempInt + "");
            }, "thread-" + i).start();
        }
        //创建5个线程
        for (int i = 0; i < 5; i++) {
            final int tempInt = i;
            new Thread(() -> {
                //5个线程取值
                myCache.getMap(tempInt + "");
            }, "thread-" + i).start();
        }
    }
}

结果为:

thread-0	 put value:0
thread-1	 put value:1
thread-2	 put value:2
thread-3	 put value:3
thread-4	 put value:4
thread-0	get the value
thread-1	get the value
thread-2	get the value
thread-3	get the value
thread-4	get the value
thread-0	put value successful
thread-1	put value successful
thread-2	put value successful
thread-3	put value successful
thread-4	put value successful
...

上述执行结果看似没有问题,但是违背了写锁最核心的本质,也就是如果有一个线程想取写共享资源来,就不应该允许其他线程可以对资源进行读或写

所以出现问题,此时就需要用到我们的读写锁,我们对我们自己的myLock()以及myUnlock()方法进行修改为使用读写锁的版本:

class MyCache {
    //定义缓存当中的数据结构为Map型,键为String,值为Object类型
    private volatile Map<String, Object> map = new HashMap<>();

    //读写锁
    ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    /**
     *  写操作:原子+独占
     *  整个过程必须是一个完整的统一体,中间不许被分割,不许被打断 *
     * @param key
     * @param value
     * */
    //向Map中添加元素的方法
    public void setMap(String key, Object value) {
        try {
            readWriteLock.writeLock().lock();
            Thread thread = Thread.currentThread();
            System.out.println(thread.getName() + "\t" + " put value:" + value);
            Thread.sleep(300);
            //底层的map直接put
            map.put(key, value);
            System.out.println(thread.getName() + "\t" + "put value successful");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readWriteLock.writeLock().unlock();
        }
    }

    //向Map中取出元素
    public void getMap(String key) {
        try {
            readWriteLock.readLock().lock();
            Thread thread = Thread.currentThread();
            System.out.println(thread.getName() + "\t" + "get the value");
            Thread.sleep(300);
            //底层的map直接get,并且返回值为Object类型
            Object retValue = map.get(key);
            System.out.println(thread.getName() + "\t" + "get the value successful, is " + retValue);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readWriteLock.readLock().unlock();
        }
    }
}

之后运行结果变成:

thread-1	 put value:1
thread-1	put value successful
thread-0	 put value:0
thread-0	put value successful
thread-2	 put value:2
thread-2	put value successful
thread-3	 put value:3
thread-3	put value successful
thread-4	 put value:4
thread-4	put value successful
thread-0	get the value
thread-1	get the value
thread-2	get the value
thread-4	get the value
thread-3	get the value
thread-2	get the value successful, is 2
thread-3	get the value successful, is 3
thread-0	get the value successful, is 0
thread-4	get the value successful, is 4
thread-1	get the value successful, is 1

也就对应上了写锁独占,必须当每一个写锁对应写操作完成之后,才可以进行下一次写操作,但是对于读操作,就可以多个线程共享,一起去缓存中读取数据

posted @ 2021-07-08 21:59  y浴血  阅读(543)  评论(0编辑  收藏  举报