目 录CONTENT

文章目录

Java中锁的学习笔记(一)

在水一方
2022-01-20 / 0 评论 / 0 点赞 / 745 阅读 / 2,647 字 / 正在检测是否收录...

单机单线程时代没有锁的概念,自从出现了资源竞争,人们才意识到需要对部分场景执行加锁,java中锁的种类很多本小结来学习一下

一、java并发包中的锁类

通过java.util.concurrent.locks包中基础类的解析来看锁的特性

Lock接口方法

void lock(): 执行此方法时, ✏️如果锁处于空闲状态, 当前线程将获取到锁

boolean tryLock():如果锁可用, 则获取锁, 并立即返回 true, 否则返回 false. 该方法和lock()的区别在于,tryLock()只是"试图"获取锁, 如果锁不可用, 不会导致当前线程被禁用

void unlock():🚩执行此方法时, 当前线程将释放持有的锁. 锁只能由持有者释放, 如果线程 并不持有锁, 却执行该方法, 可能导致异常的发生.

锁的特性

  • 互斥性
  • 不可见性

Lock是JUC包的顶层接口,它的实现并未用到synchronized,而是利用了volatile的可见性,先通过Lock来了解JUC包的一些基础类

image.png

这里纠正一下ReentrantLock是一个类应该是实现Lock接口,而不是继承,由于是直接截图的,先这样放着

Sync继承了AbstractOwnableSynchronizer(AQS),它是JUC包实现同步的基础工具,在AQS类中定义了volatile int state变量作为共享资源,如果线程获取资源失败,则同步进入FIFO队列中进行等待,如果成功获取资源就执行临界区代码执行完释放资源时,会通知同步队列中的等待线程来获取资源后出队列并执行

AQS是抽象类,内置自旋锁实现同步队列,这部分的内容目前学习起来有一定难度后续再完善

AQS使用了模板方法模式

AQS为什么重要

AQS 在 ReentrantLock、ReentrantReadWriteLock、Semaphore、CountDownLatch、ThreadPoolExcutor 的 Worker 中都有运用(JDK 1.8),AQS 是这些类的底层原理。所以说 JUC 包里很多重要的工具类背后都离不开 AQS 框架,因此 AQS 的重要性不言而喻

AQS原理

AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中

可重入锁有

  • synchronized
  • ReentrantLock

使用ReentrantLock的注意点

ReentrantLock 和 synchronized 不一样,需要手动释放锁,所以📣📣📣使用 ReentrantLock的时候一定要手动释放锁,并且加锁次数和释放次数要一样

ReentantLock除了能完成synchronized所能完成的所有工作外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等避免多线程死锁的方法

二、Synchronized

synchronized 它可以把任意一个非 NULL 的对象当作锁。他属于独占式的悲观锁,同时属于可重入锁

Synchronized作用范围

同步代码块

同步代码块一般使用java的synchronized关键字来实现
解决的是多个线程之间访问资源的同步性,其特性由JVM来实现,java底层通过监视锁来实现synchronized同步的,监视锁即monitor,是每个对象与生俱来的一个隐藏字段,使用synchronized时,JVM会根据synchronized的当前使用环境,找到对应对象的monitor,再根据monitor的的状态进行加锁、解锁的判断

两种对方法加锁的方式:

  • 在方法签名处添加Synchronized关键字
  • 使用Synchronized(类或对象)进行同步

synchronized 关键字的底层原理

synchronized 同步语句块的情况

public class SynchronizedDemo {
    public void method() {
        synchronized (this) {
        System.out.println("synchronized");
        }
     }
}

cmd窗口首先切换到类的对应目录执行 javac SynchronizedDemo.java 命令生成编译后的 .class 文件,然后执行 javap -c -s -v -l
SynchronizedDemo.class
image.png

image.png

从上面我们可以看出:
synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。 当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权.当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。

synchronized 修饰方法的的情况


public class SynchronizedDemo2 {
    public synchronized void method() {
    System.out.println("synchronized 方法");
    }
}

image.png

synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用

java中的悲观锁就是Synchronized

问题

AQS使用了LockSupport类阻塞和唤醒线程,为什么不使用Object的wait/notify呢?
相比Object的wait/notify,LockSupport有两大优势:

  • LockSupport不需要在同步代码块里 。所以线程间也不需要维护一个共享的同步对象了,实现了线程间的解耦。

  • unpark函数可以先于park调用,所以不需要担心线程间的执行的先后顺序

0

评论区