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 ...
随机推荐
- C语言判断一个数能否被3和5整除
#include <stdio.h> /* 判断一个数能不能同时被3和5整除 --------soulsjie 20170525----- */ void main(){ int inpu ...
- 九度oj 题目1052:找x
题目1052:找x 时间限制:1 秒 内存限制:32 兆 特殊判题:否 提交:9901 解决:5098 题目描述: 输入一个数n,然后输入n个数值各不相同,再输入一个值x,输出这个值在这个数组中的下标 ...
- 扫描局域网内所有主机和MAC地址的Shell脚本
#!/bin/bash #author: InBi #date: 2011-08-16 #website: http://www.itwhy.org/2011/08-20/939.html ##### ...
- hdu 5044 树链剖分
转载:http://blog.csdn.net/qinzhenhua100/article/details/39716851 二种操作,一种更新结点值,一种更新路径值,最后输出更改后的结点值和路径值. ...
- codeforces 359A
#include<stdio.h> #define N 60 int map[N][N]; int main() { int n,m,i,j,flag; while(scanf(&qu ...
- Gym 100801 J. Journey to the “The World’s Start” DP+单调队列优化+二分
http://codeforces.com/gym/100801 题目大意:有从左到右有n个车站,有n-1种车票,第i种车票一次最多可以坐 i 站(1<=i<=n) 每种票有固定的价钱 ...
- pkill有的时候并不能杀死进程?
pkill的用法:http://man.linuxde.net/pkill 根据进程命令行,杀死进程 如下intellij.go代码为一个代理服务器,把本地请求转向一个代理 package main ...
- centos下性能分析工具perf的安装和简单使用
1.安装: cat /etc/redhat-releaseCentOS release 6.6 (Final) sudo yum install perf 2.
- maven打包插件maven-shade-plugin简单介绍
作用: 1.可以把依赖打入jar包,然后直接使用这个jar包,从而不用担心依赖问题 2.通过设置MainClass,创建一个可以执行的jar包 3.Java工程经常会遇到第三方 Jar 包冲突,使用 ...
- uva 1411 Ants (权值和最小的完美匹配---KM算法)
uva 1411 Ants Description Young naturalist Bill studies ants in school. His ants feed on plant-louse ...