多线程的各种锁

    在学习多线程的时候,我们经常会听到可重入锁/不可重入锁、公平锁/非公平锁、读写锁现在我们就逐一它们的神秘面纱。

Lock包下的层级结构
Lock包下的层级结构

      1.可重入锁/非重入锁:大部分jdk提供的都是可重入锁,如syncronized,reentrantLock 都是可重入,代表单个(也可以说同一个)线程可以多次获得该锁,如果单个线程拿到锁没有释放,你再去拿,拿到则是重入锁 ,拿不到则是非重入锁。

 public static void main(String[] args) {
        //KodyLock lock = new KodyLock ();
        Lock lock = new ReentrantLock ();
        lock.lock ();
        System.out.println ("获得锁");
        lock.lock ();
        System.out.println ("再次获得锁");
    }

        如上,可以发现ReentranLock是可重入的,那非重入锁呢?下面是一个简单的非重入锁的实现

public class NCrLock {

    boolean isLock = false;

    public synchronized void  lock() throws InterruptedException{
        while (isLock){
            wait ();
        }
        isLock = true;
    }

    public synchronized void unlock(){
        isLock = false;
        notify ();
    }

    public static void main(String[] args) throws InterruptedException{
        NCrLock nCrLock = new NCrLock ();
        nCrLock.lock ();
        System.out.println ("获得锁");
        nCrLock.lock ();
        System.out.println ("再次获得锁");
    }

}

       2.读写锁:我们知道ReentrantLock是一种排它锁,同一时间内只允许一个线程访问,而ReentrantReadWriteLock允许多个读线程同时访问,但不允许读线程和写线程、写线程和写线程同时访问,在实际应用中,大部分共享数据(缓存)的访问都是读操作远多于写操作这时候ReentrantReadWrite就比排它锁提供了更好的并发性和吞吐量。

 public static void main(String[] args) {
        ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock ();
        new Thread (() -> {
            reentrantReadWriteLock.readLock ().lock ();
            long startTime = System.currentTimeMillis ();
            while (System.currentTimeMillis () - startTime < 5){
                //记录5ms内 读线程做的事情
                System.out.println (Thread.currentThread ().getName () + ":正在进行读操作" );
            }
            reentrantReadWriteLock.readLock ().unlock ();
        },"读锁线程").start ();

        new Thread (() -> {
            reentrantReadWriteLock.readLock ().lock ();
            long startTime = System.currentTimeMillis ();
            while (System.currentTimeMillis () - startTime < 5){
                //记录5ms内 读线程做的事情
                System.out.println (Thread.currentThread ().getName () + ":正在进行读操作" );
            }
            reentrantReadWriteLock.readLock ().unlock ();
        },"读锁线程2").start ();


    }

两个读线程控制台打印信息如下:多试几次因为可能cpu调度切换这其中时间差导致先执行某个线程

读锁线程2:正在进行读操作
读锁线程:正在进行读操作
读锁线程2:正在进行读操作
读锁线程2:正在进行读操作
........

改成读线程与写线程控制台打印信息如下:

读锁线程:正在进行读操作
读锁线程:正在进行读操作
读锁线程:正在进行读操作
写锁线程2:正在进行读操作
写锁线程2:正在进行读操作
写锁线程2:正在进行读操作
写锁线程2:正在进行读操作
.......

    3.公平锁与非公平锁:java中自带的关键字syncronized和ReentrantLock都能事先对方法或者代码块进行加锁,前者只能是非公平锁后者默认是非公平锁,公平锁和非公平锁都是基于锁内部维护的一个双向链表,表节点Node的值就是每一个请求当前锁的线程。公平锁则是每次依次从队首取值,所以能保证获取锁的顺序性,而非公平锁则是有机会去抢锁,可能会导致线程抢锁求而不得。

      所以总结下里就是:

  1. 公平锁获取锁的顺序是按照加锁的顺序来分配的,即FIFO先进先出,非公平锁与公平锁不一样的就是它是随机获取锁,先来的不一定得到锁,这个方式可能造成线程拿不到锁。

      非公平锁:基于CAS尝试将state(锁数量)从0改为1,如果设置成功则设置当前线程为独占锁线程,如果设置失败还会在获取一次锁的数量:

  1. 如果锁的数量为0在基于CAS尝试将state从0设置为1,如果设置成功则设置当前线程为独占锁线程
  2. 如果锁的数量不为0或者上面尝试失败了,则查看当前锁是不是独占锁线程,如果是则将当前线程锁的数量+1,如果不是则将线程封装在一个Node内部,并加入到等待队列,等待被其前一个线程节点唤醒

   公平锁:获取一次锁的数量

  1. 如果锁的数量为0,如果当前线程是等待队列的头节点,基于CAS尝试将state从哪个0设置为1一次,设置成功则将当前线程设置为独占锁线程
  2. 如果锁的数量不为0或者当前线程不是等待队列中的头节点或者上面的尝试失败了,查看当前线程是不是已经是独占锁线程,如果是则将当前锁的数量+1,如果不是则将该线程封装在一个Node内,并加入到等待队列,等待被其前面一个线程唤醒。
  public static void main(String[] args) throws InterruptedException{
        Lock lock = new ReentrantLock (false);
        //分别依次启动5个线程,观察它的执行情况
        IntStream.range (0,5).forEach (value -> {
            new Thread (() -> {
                System.out.println (Thread.currentThread ().getName () + "线程开始运行");
                lock.lock ();
                System.out.println (Thread.currentThread ().getName () + "拿到锁");
                lock.unlock ();
            },String.valueOf (value)).start ();
        });
    }

可以看出线程拿到锁的顺序为0,1,4,3,2线程执行的顺序为0,1,3,4,2明显是无序的

0线程开始运行
1线程开始运行
0拿到锁
3线程开始运行
1拿到锁
4线程开始运行
4拿到锁
2线程开始运行
3拿到锁
2拿到锁

改为公平锁的执行情况,线程拿到锁的顺序为0,1,2,3,4程执行的顺序为0,1,2,3,4线程拿到锁顺序与线程执行的顺序一致,当然这里拿到锁的顺序也并非全是0到4顺序来的,这个得看cpu调度切换到谁。

public static void main(String[] args) throws InterruptedException{
        Lock lock = new ReentrantLock (true);
        //分别依次启动5个线程,观察它的执行情况
        IntStream.range (0,5).forEach (value -> {
            new Thread (() -> {
                System.out.println (Thread.currentThread ().getName () + "线程开始运行");
                lock.lock ();
                System.out.println (Thread.currentThread ().getName () + "拿到锁");
                lock.unlock ();
            },String.valueOf (value)).start ();
        });
    }
0线程开始运行
0拿到锁
1线程开始运行
1拿到锁
2线程开始运行
2拿到锁
3线程开始运行
3拿到锁
4线程开始运行
4拿到锁
发布了73 篇原创文章 · 获赞 18 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qq_40826106/article/details/101704900