单机单线程时代没有锁的概念,自从出现了资源竞争,人们才意识到需要对部分场景执行加锁,java中锁的种类很多本小结来学习一下
一、java并发包中的锁类
通过java.util.concurrent.locks包中基础类的解析来看锁的特性
Lock接口方法
void lock(): 执行此方法时, ✏️如果锁处于空闲状态, 当前线程将获取到锁
boolean tryLock():如果锁可用, 则获取锁, 并立即返回 true, 否则返回 false. 该方法和lock()的区别在于,tryLock()只是"试图"获取锁, 如果锁不可用, 不会导致当前线程被禁用
void unlock():🚩执行此方法时, 当前线程将释放持有的锁. 锁只能由持有者释放, 如果线程 并不持有锁, 却执行该方法, 可能导致异常的发生.
锁的特性
- 互斥性
- 不可见性
Lock是JUC包的顶层接口
,它的实现并未用到synchronized,而是利用了volatile的可见性,先通过Lock来了解JUC包的一些基础类
这里纠正一下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
从上面我们可以看出:
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 方法");
}
}
synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用
java中的悲观锁就是Synchronized
问题
AQS使用了LockSupport类阻塞和唤醒线程,为什么不使用Object的wait/notify呢?
相比Object的wait/notify,LockSupport有两大优势:
-
LockSupport不需要在同步代码块里 。所以线程间也不需要维护一个共享的同步对象了,实现了线程间的解耦。
-
unpark函数可以先于park调用,所以不需要担心线程间的执行的先后顺序
评论区