Java中的锁机制,你真的了解吗?
学到锁说明你已经学过多线程了,只有在多线程并发的情况下才会涉及到锁,相信大家用的最多的要数synchronized了,因为这个也是最简单的,直接加在方法上就可以使一个方法同步。那么除了synchronized之外,还有没有其他的锁呢,这个还真有。我们来看看:

这个是Java里边锁相关的一些类,顶级接口有三个,
- Lock
- Condition
- ReadWriteLock
我们来看Lock接口的一些方法:
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
可以看到,他提供了6中方法。我们接下来看Condition接口提供的一些方法:
public interface Condition {
void await() throws InterruptedException;
void awaitUninterruptibly();
long awaitNanos(long nanosTimeout) throws InterruptedException;
boolean await(long time, TimeUnit unit) throws InterruptedException;
boolean awaitUntil(Date deadline) throws InterruptedException;
void signal();
void signalAll();
}
他里边提供了7中方法,下面我们来看ReadWriteLock的方法:
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
他里边只提供了2中方法,分别是ReadLock和WriteLock。
首选我们来看Lock的实现类一共有三种,分别是:
ReentrantLock
ReentrantLock重入锁,是实现Lock接口的一个类,也是在实际编程中使用频率很高的一个锁,支持重入性,表示能够对共享资源能够重复加锁,即当前线程获取该锁再次获取不会被阻塞。在java关键字synchronized隐式支持重入性,synchronized通过获取自增,释放自减的方式实现重入。与此同时,ReentrantLock还支持公平锁和非公平锁两种方式。那么,要想完完全全的弄懂ReentrantLock的话,主要也就是ReentrantLock同步语义的学习:
- 重入性的实现原理
- 公平锁和非公平锁

除此之外,ReentrantLock 提供了丰富的接口用于获取锁的状态,比如可以通过isLocked()查询 ReentrantLock 对象是否处于锁定状态, 也可以通过getHoldCount()获取 ReentrantLock 的加锁次数,也就是重入次数等。而 synchronized 仅支持通过Thread.holdsLock查询当前线程是否持有锁。另外,synchronized 使用的是对象或类进行加锁,而 ReentrantLock 内部是通过 AQS 中的同步队列进行加锁,这一点和 synchronized 也是不一样的。
说了这么多可能大家对可重入这个词还不是很理解,我们往下看:
void m1() {
lock.lock();
try {
// 调用 m2,因为可重入,所以并不会被阻塞
m2();
} finally {
lock.unlock()
}
}
void m2() {
lock.lock();
try {
// do something
} finally {
lock.unlock()
}
}
假如 lock 是不可重入锁,那么上面的示例代码必然会引起死锁情况的发生。这里请大家思考一个问题,ReentrantLock 的可重入特性是怎样实现的呢?简单说一下,ReentrantLock 内部是通过 AQS 实现同步控制的,AQS 有一个变量 state 用于记录同步状态。初始情况下,state = 0,表示 ReentrantLock 目前处于解锁状态。如果有线程调用 lock 方法进行加锁,state 就由0变为1,如果该线程再次调用 lock 方法加锁,就让其自增,即 state++。线程每调用一次 unlock 方法释放锁,会让 state--。通过查询 state 的数值,即可知道 ReentrantLock 被重入的次数了。这就是可重复特性的大致实现流程。
那什么公平什么是非公平呢?
公平与非公平指的是线程获取锁的方式。公平模式下,线程在同步队列中通过 FIFO 的方式获取锁,每个线程最终都能获取锁。在非公平模式下,线程会通过“插队”的方式去抢占锁,抢不到的则进入同步队列进行排队。默认情况下,ReentrantLock 使用的是非公平模式获取锁,而不是公平模式。不过我们也可通过 ReentrantLock 构造方法ReentrantLock(boolean fair)调整加锁的模式。
既然既然有两种不同的加锁模式,那么他们有什么优缺点呢?答案如下:
公平模式下,可保证每个线程最终都能获得锁,但效率相对比较较低。非公平模式下,效率比较高,但可能会导致线程出现饥饿的情况。即一些线程迟迟得不到锁,每次即将到手的锁都有可能被其他线程抢了。
在激烈竞争的情况下,非公平锁的性能高于公平锁的性能的一个原因是:在恢复一个被挂起的线程与该线程真正开始运行之间存在着严重的延迟。假设线程 A 持有一个锁,并且线程 B 请求这个锁。由于这个线程已经被线程 A 持有,因此 B 将被挂起。当 A 释放锁时,B 将被唤醒,因此会再次尝试获取锁。与此同时,如果 C 也请求这个锁,那么 C 很有可能会在 B 被完全唤醒前获得、使用以及释放这个锁。这样的情况时一种“双赢”的局面:B 获得锁的时刻并没有推迟,C 更早的获得了锁,并且吞吐量也获得了提高。
公平锁对应的逻辑是 ReentrantLock 内部静态类 FairSync
+--- ReentrantLock.FairSync.java
final void lock() {
// 调用 AQS acquire 获取锁
acquire(1);
}
+--- AbstractQueuedSynchronizer.java
/**
* 该方法主要做了三件事情:
* 1. 调用 tryAcquire 尝试获取锁,该方法需由 AQS 的继承类实现,获取成功直接返回
* 2. 若 tryAcquire 返回 false,则调用 addWaiter 方法,将当前线程封装成节点,
* 并将节点放入同步队列尾部
* 3. 调用 acquireQueued 方法让同步队列中的节点循环尝试获取锁
*/
public final void acquire(int arg) {
// acquireQueued 和 addWaiter 属于 AQS 中的方法,这里不展开分析了
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
+--- ReentrantLock.FairSync.java
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
// 获取同步状态
int c = getState();
// 如果同步状态 c 为0,表示锁暂时没被其他线程获取
if (c == 0) {
/*
* 判断是否有其他线程等待的时间更长。如果有,应该先让等待时间更长的节点先获取锁。
* 如果没有,调用 compareAndSetState 尝试设置同步状态。
*/
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
// 将当前线程设置为持有锁的线程
setExclusiveOwnerThread(current);
return true;
}
}
// 如果当前线程为持有锁的线程,则执行重入逻辑
else if (current == getExclusiveOwnerThread()) {
// 计算重入后的同步状态,acquires 一般为1
int nextc = c + acquires;
// 如果重入次数超过限制,这里会抛出异常
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
// 设置重入后的同步状态
setState(nextc);
return true;
}
return false;
}
+--- AbstractQueuedSynchronizer.java
/** 该方法用于判断同步队列中有比当前线程等待时间更长的线程 */
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
/*
* 在同步队列中,头结点是已经获取了锁的节点,头结点的后继节点则是即将获取锁的节点。
* 如果有节点对应的线程等待的时间比当前线程长,则返回 true,否则返回 false
*/
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
ReentrantLock 中获取锁的流程并不是很复杂,上面的代码执行流程如下:
- 调用 acquire 方法,将线程放入同步队列中进行等待
- 线程在同步队列中成功获取锁,则将自己设为持锁线程后返回
- 若同步状态不为0,且当前线程为持锁线程,则执行重入逻辑
分析完公平锁相关代码,下面再来看看非公平锁的源码分析,如下:
+--- ReentrantLock.NonfairSync
final void lock() {
/*
* 这里调用直接 CAS 设置 state 变量,如果设置成功,表明加锁成功。这里并没有像公平锁
* 那样调用 acquire 方法让线程进入同步队列进行排队,而是直接调用 CAS 抢占锁。抢占失败
* 再调用 acquire 方法将线程置于队列尾部排队。
*/
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
+--- AbstractQueuedSynchronizer
/** 参考上一节的分析 */
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
+--- ReentrantLock.NonfairSync
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
+--- ReentrantLock.Sync
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
// 获取同步状态
int c = getState();
// 如果同步状态 c = 0,表明锁当前没有线程获得,此时可加锁。
if (c == 0) {
// 调用 CAS 加锁,如果失败,则说明有其他线程在竞争获取锁
if (compareAndSetState(0, acquires)) {
// 设置当前线程为锁的持有线程
setExclusiveOwnerThread(current);
return true;
}
}
// 如果当前线程已经持有锁,此处条件为 true,表明线程需再次获取锁,也就是重入
else if (current == getExclusiveOwnerThread()) {
// 计算重入后的同步状态值,acquires 一般为1
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
// 设置新的同步状态值
setState(nextc);
return true;
}
return false;
}
非公平锁的实现也不是很复杂,其加锁的步骤大致如下:
- 调用 compareAndSetState 方法抢占式加锁,加锁成功则将自己设为持锁线程,并返回
- 若加锁失败,则调用 acquire 方法,将线程置于同步队列尾部进行等待
- 线程在同步队列中成功获取锁,则将自己设为持锁线程后返回
- 若同步状态不为0,且当前线程为持锁线程,则执行重入逻辑
ReentrantLock类常用API:
- getHoldCount() 查询当前线程保持此锁的次数,也就是执行此线程执行lock方法的次数
- getQueueLength()返回正等待获取此锁的线程估计数,比如启动10个线程,1个线程获得锁,此时返回的是9
- getWaitQueueLength(Condition condition)返回等待与此锁相关的给定条件的线程估计数。比如10个线程,用同一个condition对象,并且此时这10个线程都执行了condition对象的await方法,那么此时执行此方法返回10
- hasWaiters(Condition condition)查询是否有线程等待与此锁有关的给定条件(condition),对于指定contidion对象,有多少线程执行了condition.await方法
- hasQueuedThread(Thread thread)查询给定线程是否等待获取此锁
- hasQueuedThreads()是否有线程等待此锁
- isFair()该锁是否公平锁
- isHeldByCurrentThread() 当前线程是否保持锁锁定,线程的执行lock方法的前后分别是false和true
- isLock()此锁是否有任意线程占用
- lockInterruptibly()如果当前线程未被中断,获取锁
- tryLock()尝试获得锁,仅在调用时锁未被线程占用,获得锁
- tryLock(long timeout TimeUnit unit)如果锁在给定等待时间内没有被另一个线程保持,则获取该锁
tryLock和lock和lockInterruptibly的区别:
- tryLock能获得锁就返回true,不能就立即返回false,tryLock(long timeout,TimeUnit unit),可以增加时间限制,如果超过该时间段还没获得锁,返回false
- lock能获得锁就返回true,不能的话一直等待获得锁
- lock和lockInterruptibly,如果两个线程分别执行这两个方法,但此时中断这两个线程,前者不会抛出异常,而后者会抛出异常
下面我们来看一个简单的案例:
package com.xz.day03;
import java.util.concurrent.locks.ReentrantLock;
public class TestLock1 {
static int a = 0;
public static void main(String[] args) {
ReentrantLock rlock = new ReentrantLock();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
rlock.lock();
try {
System.out.println(Thread.currentThread().getName() + "---" + ++a);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
} finally {
rlock.unlock();
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
rlock.lock();
try {
System.out.println(Thread.currentThread().getName() + "---" + ++a);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
} finally {
rlock.unlock();
}
}
}
}).start();
}
}
第二种实现Runnable的写法:
package com.xz.day03;
import java.util.concurrent.locks.ReentrantLock;
public class TestLock2 {
static int a = 0;
public static void main(String[] args) {
ReentrantLock rlock = new ReentrantLock();
//这里的rlock可以传进去,也可以在线程里边定义成局部变量
F f = new F(rlock,a);
new Thread(f).start();
new Thread(f).start();
}
}
class F implements Runnable{
private ReentrantLock rlock = new ReentrantLock();
private Integer a;
public F(ReentrantLock rlock,Integer a) {
// this.rlock = rlock;
this.a = a;
}
@Override
public void run() {
while(true) {
try {
rlock.lock();
++a;
System.out.println(Thread.currentThread().getName()+"--"+a);
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}finally {
rlock.unlock();
}
}
}
}
有问题可以在下面评论,技术问题可以私聊。
Java中的锁机制,你真的了解吗?的更多相关文章
- JAVA中关于锁机制
本文转自 http://blog.csdn.net/yangzhijun_cau/article/details/6432216 一段synchronized的代码被一个线程执行之前,他要先拿到执行这 ...
- Java 中的锁机制
多个进程或线程同时(或着说在同一段时间内)访问同一资源会产生并发(线程安全)问题.解决并发问题可以用锁. java的内置锁: 每个java对象都可以用做一个实现同步的锁,这些锁称为内置锁.线程进入同步 ...
- 【转载】Java中的锁机制 synchronized & 偏向锁 & 轻量级锁 & 重量级锁 & 各自优缺点及场景 & AtomicReference
参考文章: http://blog.csdn.net/chen77716/article/details/6618779 目前在Java中存在两种锁机制:synchronized和Lock,Lock接 ...
- Java中的锁机制
1.在Java中锁的分类 其实就是按照锁的特性分类的 公平锁,非公平锁 可重入锁 独享锁,共享锁 互斥锁,读写锁 乐观锁,悲观锁 分段锁 偏向锁,轻量级锁,重量级锁 自旋锁 相关资料:思维导图 使用场 ...
- 【Todo】【转载】Java中的锁机制2 - Lock
参考这篇文章 http://blog.csdn.net/chen77716/article/details/6641477 上一篇 (http://www.cnblogs.com/charlesblc ...
- 深入浅出Java并发包—锁机制(一)
前面我们看到了Lock和synchronized都能正常的保证数据的一致性(上文例子中执行的结果都是20000000),也看到了Lock的优势,那究竟他们是什么原理来保障的呢?今天我们就来探讨下Jav ...
- Java并发指南4:Java中的锁 Lock和synchronized
Java中的锁机制及Lock类 锁的释放-获取建立的happens before 关系 锁是java并发编程中最重要的同步机制.锁除了让临界区互斥执行外,还可以让释放锁的线程向获取同一个锁的线程发送消 ...
- Java并发编程:Java中的锁和线程同步机制
锁的基础知识 锁的类型 锁从宏观上分类,只分为两种:悲观锁与乐观锁. 乐观锁 乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新 ...
- AQS:Java 中悲观锁的底层实现机制
介绍 AQS AQS(AbstractQueuedSynchronizer)是 Java 并发包中,实现各种同步组件的基础.比如 各种锁:ReentrantLock.ReadWriteLock.Sta ...
随机推荐
- 九度oj 题目1058:反序输出
题目1058:反序输出 时间限制:1 秒 内存限制:32 兆 特殊判题:否 提交:9677 解决:3495 题目描述: 输入任意4个字符(如:abcd), 并按反序输出(如:dcba) 输入: 题目可 ...
- 【ZJOI2017 Round1游记】
DAY0: 中午12点出发,下午5点到 酒店意外豪华 晚上和MG,LYY们定了个寿司套餐 没什么学习就睡觉了 DAY1: 听说RYZ在ZJ的OIer中影响颇深 讲STL的小哥真是对不住因为我是P党 D ...
- hdu - 1689 Just a Hook (线段树区间更新)
http://acm.hdu.edu.cn/showproblem.php?pid=1698 n个数初始每个数的价值为1,接下来有m个更新,每次x,y,z 把x,y区间的数的价值更新为z(1<= ...
- 1sting 大数 递推
You will be given a string which only contains ‘1’; You can merge two adjacent ‘1’ to be ‘2’, or lea ...
- Spring在Java Filter注入Bean为Null的问题解决
在Spring的自动注入中普通的POJO类都可以使用@Autowired进行自动注入,但是除了两类:Filter和Servlet无法使用自动注入属性.(因为这两个归Web容器管理)可以用init(集承 ...
- spring mvc 访问静态资源404
访问比如css js出现404提示 在spring的配置文件中加上如下代码即可 <!-- 静态资源404 --> <mvc:resources location="/res ...
- springmvc 中model中放入枚举类型
我们直接看样例: Map<String, String> mallMap = new HashMap<String, String>(); mallMap.put(MallSt ...
- seajs入门使用
使用 Sea.js 进行模块化开发还能够带来非常多优点: 模块的版本号管理. 通过别名等配置,配合构建工具,能够比較轻松地实现模块的版本号管理. 提高可维护性.模块化能够让每一个文件的职责单一,很有利 ...
- Python进阶系列之怎么写出pythonic的代码
使用 in/not in 检查key是否存在于字典中 判断某个key是否存在于字典中时,一般的初学者想到的方法是,先以列表的形式把字典所有的key返回,在判断该key是否存在于key列表中 d = { ...
- Creo二次开发—内存处理
#include <ProDisplist.h> ProError ProDisplistInvalidate(ProMdl model) Invalidates the two- or ...