Java多线程并发笔记(3)锁机制:synchronized、Lock显式锁

前言

记录学习过程
多线程的加锁机制:synchronized、Lock显式锁

目录

  1. synchronized
  2. Lock显式锁
  3. AQS同步器
  4. ReentrantLock
  5. ReentrantReadWriteLock
  6. 总结

synchronized

前面分析执行控制时学了synchronized的使用

synchronized是一个关键字,可以在方法或代码块上修饰
修饰了后就会执行互斥机制,锁定对象
保证了修改的可见性与原子性

 	//synchronized修饰普通方法获得并锁定指定对象
    public synchronized void test(){
        
    }
    //synchronized修饰代码块获得并锁定对象
    public void test1(){
        synchronized (this) {}
    }
    //synchronized修饰静态方法会锁定class类对象
    public static synchronized void test2(){}

Lock显式锁

概括

Lock是一个接口
在Java.util.concurrent包下
Lock锁与synchronized有一定的不同
简单概括一下:

  • Lock方式来获取锁支持中断、超时不获取、是非阻塞的

  • 提高了语义化,哪里加锁,哪里解锁都得写出来

  • Lock显式锁可以给我们带来很好的灵活性,但同时我们必须手动释放锁

  • 支持Condition条件对象

  • 允许多个读线程同时访问共享资源

在这里插入图片描述
它有3个子类
在这里插入图片描述

AQS同步器

这个大佬写的很详细
JUC:Java.util.concurrent包
Lock锁接口有3个抽象类
在这里插入图片描述

AbstractQueuedSynchronizer就是AQS

如同它的英文名:抽象的队列式的同步器

AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它
例如Lock的子类都是基于AQS实现的

在这里插入图片描述

维护了一个volatile int state(代表可见性的共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列 - CLH队列)

AQS定义两种资源共享方式:Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)

获取独占锁的过程就是在acquire定义的
在这里插入图片描述
在这里插入图片描述

释放独占锁的过程在release定义的
在这里插入图片描述
在这里插入图片描述
关于AQS:

  • juc包中很多可阻塞的类都是基于AQS构建的

  • AQS可以说是一个给予实现同步锁、同步器的一个框架,很多实现类都在它的的基础上构建的

  • 在AQS中实现了对等待队列的默认实现,子类只要重写部分的代码即可实现(大量用到了模板代码)

公平锁

公平锁:线程获取锁的顺序和调用lock的顺序一样,FIFO;
非公平锁:线程获取锁的顺序和调用lock的顺序无关,全凭运气。

Lock和synchronize都是默认使用非公平锁的
公平锁为了保证线程规规矩矩地排队,需要增加阻塞和唤醒的时间开销
非公平锁直接插队获取非公平锁,跳过了对队列的处理,就是线程查看锁资源有否被占有,没有线程就直接占用,不管是否有等待线程,速度会更快

ReentrantLock(可重入锁)

ReentrantLock是Lock接口的一个重要子类

  • 是一个可重入的互斥锁,比synchronized更有伸缩性(灵活)
  • 支持公平锁(是相对公平的)
  • 使用时最标准用法是在try之前调用lock方法,在finally代码块释放锁

在这里插入图片描述
ReentrantLock有3个内部类,是AQS的子类,AQS是ReentrantLock的基础,AQS是构建锁、同步器的框架

在这里插入图片描述构造方法
在这里插入图片描述
ReentrantLock有两个构造方法,有参是确定是否为公平锁

默认是非公平锁(NonfairSync)
在这里插入图片描述

ReentrantLock操作

在这里插入图片描述
简单一个lock - unlock 内部实现其实挺复杂的

  1. 非公平Lock方法
    前面构造方法可以看出,默认是非公平Lock方法,实例化NofairSync内部类

在这里插入图片描述NofairSync的lock锁方法:
if compareAndSetState尝试获得共享资源,
else 获得失败就调用acquire(1)方法,acquire方法调用NofairSync类(实质上就是AQS)的tryAcquire方法
在这里插入图片描述在这里插入图片描述nonfairTryAcquire是Sync实例的方法
在这里插入图片描述

在这里插入图片描述
Lock方法内部运行过程:
调用构造方法实例化的NofairSync实例的lock方法,判断能否获得共享资源,得到了就锁住然后运行方法体
得不到就调用acquire(1)方法(AQS的方法),acquire内部调用tryAcquire方法,tryAcquire调用Sync实例的方法

一环扣一环

  1. 公平lock方法
    在构造方法时选择带参构造。
final ReentrantLock reentrantLock=new ReentrantLock(true);

带参构造调用的就是FairSync实例
在这里插入图片描述其他和非公平类似,
但是公平lock中tryAcquire的具体实现是在FairSync实例里
在tryAcquire方法内有一定的设置
在这里插入图片描述hasQueuedPredecessors是判断当前线程是否位于CLH同步队列中的第一个。如果是则返回flase,否则返回true
在这里插入图片描述

  1. unlock方法

在这里插入图片描述
调用Sync实例的release方法
在这里插入图片描述release调用tryRelease方法

在这里插入图片描述在这里插入图片描述
以独占模式的释放锁

  1. 可以看出底层还是AQS的几个方法
  • isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
  • tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
  • tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
  • tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
  • tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。

简单实现ReentrantLock

package com.company.Thread.ReentrantLock;

import java.util.concurrent.locks.ReentrantLock;

public class TrcketTest implements Runnable {
    private int trcket=10;
    //事先实例化reetrantLock对象
    final ReentrantLock reentrantLock=new ReentrantLock();
    public void test(){
        //获得锁
        reentrantLock.lock();
        try {
            while (true) {
                if (trcket > 0) {
                    System.out.println(Thread.currentThread().getName() + "已售票,余票" + --trcket);
                } else {
                    System.out.println(Thread.currentThread().getName() + "已售完");
                    break;
                }
            }
        }
        finally {
            //在finally中关闭锁
            reentrantLock.unlock();
        }
    }
    @Override
    public void run() {
        test();
    }

    public static void main(String[] args){
        TrcketTest test=new TrcketTest();
        new Thread(test, "1号窗口").start();
        new Thread(test,"2号窗口").start();
        new Thread(test,"3号窗口").start();

    }


}

在这里插入图片描述
reentrantLock将对象锁定,直到unlock,线程2,3才可以运行

ReentrantReadWriteLock(读写锁)

synchronized内置锁和ReentrantLock都是互斥锁(一次只能有一个线程进入到临界区(被锁定的区域))

而ReentrantReadWriteLock是一个读写锁:

  • 在读取数据的时候,可以多个线程同时进入到到临界区(被锁定的区域)

  • 在写数据的时候,无论是读线程还是写线程都是互斥的

Lock的重要子类:ReentrantReadWriteLock继承ReadWriteLock接口

在这里插入图片描述ReadWriteLock接口:有readLock、writeLock方法

在这里插入图片描述
所以ReentrantReadWriteLock也有两个实现类ReadLock、WriteLock

在这里插入图片描述
读写锁的一些要点

  • 读锁不支持条件对象,写锁支持条件对象
  • 读锁不能升级为写锁,写锁可以降级为读锁
  • 读写锁也有公平和非公平模式
  • 读锁支持多个读线程进入临界区,写锁是互斥的

读写锁要分开来使用:
在这里插入图片描述
具体的用法:



	//创建ReentrantReadWriteLock对象
    private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
 	//抽取读写锁
    private Lock readLock = rwl.readLock();
    private Lock writeLock = rwl.writeLock();
    public int getXXX(){
        readLock.lock();
        try{
            //执行操作
        }finally{
            readLock.unlock();
        }
    }
    public void setXXX(){
        writeLock.lock();
        try{
            //执行操作
        }finally{
            writeLock.unlock();
        }
    }

底层实现:

写锁:
在这里插入图片描述写锁通过acquire方法实现独占(前面有该方法的实现步骤)

在这里插入图片描述unlock通过release方法

读锁:
在这里插入图片描述acquireShared是共享方式

在这里插入图片描述releaseShared共享方式(具体看大佬的讲解

读锁可以降级为写锁:
并发编程之——读锁源码分析(解释关于锁降级的争议)

总结

  • synchronized是关键字,JVM会帮我们解决锁问题,Lock是显式锁,需要自己锁和解锁
  • synchronized随着jdk的更新已经和lock差不多了,都可以使用
  • 可以看出Lock的两个重要子类:ReentrantReadWriteLock、ReentrantLock都是基于AQS实现的
  • ReentrantReadWriteLock和ReentrantLock都支持公平和非公平模式
  • ReentrantLock与synchronized的功能类似,但更灵活
  • ReentrantReadWriteLock是一个读写锁,如果读的线程比写的线程要多很多的话,那可以考虑使用它。它使用state的变量高16位是读锁,低16位是写锁
  • 写锁可以降级为读锁,读锁不能升级为写锁
  • 写锁是互斥的,读锁是共享的

过一过,回头再学

发布了49 篇原创文章 · 获赞 0 · 访问量 1224

猜你喜欢

转载自blog.csdn.net/key_768/article/details/104313379