显式锁

上篇讲了使用synchronized关键字来定义锁,其实Java除了使用这个关键字外还可以使用Lock接口及其实现的子类来定义锁,ReentrantLock类是Lock接口的一个实现,Reentrant是“重入”的意思,因此这个锁也是支持重入的,这里就不再测试它的重入性了,感兴趣的同学可以自行测试。这种方式是Java在jdk1.5才引入进来的功能,它的功能比synchronized关键字更为强大,但也有一个缺点:使用起来比synchronized关键字更麻烦。使用Lock来实现value++,代码如下:

class Entity {
public static int value = 0;
}
class IncreaseThread implements Runnable {
private static Lock lock = new ReentrantLock();
public void run() {
for(int i=0; i <100000; i++){
lock.lock();
try {
Entity.value++;
}
finally {
lock.unlock();
}
}
}
}
public class ReentrantLockTest {
public static void main(String[] args) throws InterruptedException {
ExecutorService exec = Executors.newCachedThreadPool();
exec.execute(new IncreaseThread());
exec.execute(new IncreaseThread());
exec.shutdown();
Thread.sleep(5000);
System.out.println("Value = " + Entity.value);
}
}

  

运行5秒后输出如下结果:

Value = 200000

在定义lock时将其声明为static的,因此两个IncreaseThread对象可以共用一个lock对象;如果使用的不是同一个对象,尝试获取的就不是同一个锁了,也就不会互斥。代码执行到lock()方法时会检查锁是否被占用,如果没有被占用则直接获取锁并执行下面的代码,如果已经被占用了则阻塞当前线程,等待锁被其他线程释放。使用lock设置的临界区从调用lock()方法开始,到调用unlock()方法结束,本例在try块中定义要执行的代码,在finally块中调用unlock()方法是一种常用的方式,这样即使要执行的代码中抛出异常,也可以保证锁会被正常释放。

试图获取锁

tryLock方法给我们提供了两种尝试获取锁的方式,即根据是否获取到了锁来执行不同的策略,我们先介绍两个方法的功能。

tryLock():试图获取锁,如果锁没有被占用则得到锁、返回true、继续执行下面的代码;如果锁被占用了则立即返回false(不会被阻塞)、继续执行下面的代码。

tryLock(long time, TimeUnit unit):与tryLock()类似,这个方法可以指定等待锁的最长时间,在这段时间内当前线程会被阻塞。如果时间内获得了锁则返回true并提前结束阻塞,反之返回false。

具体代码如下:

class TryToGetLockThread implements Runnable {
public void run() {
boolean alreadyGetLock = TryLockTest.lock.tryLock();
if(alreadyGetLock) {
try {
System.out.println("新线程:我拿到锁了");
Thread.sleep(3000);//持有锁3秒
} catch (InterruptedException e) {
e.printStackTrace();
}
finally {
System.out.println("新线程:我要释放锁了 ");
TryLockTest.lock.unlock();
}
}
}
}
public class TryLockTest {
public static Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
ExecutorService exec = Executors.newCachedThreadPool();
exec.execute(new TryToGetLockThread());
exec.shutdown();
boolean alreadyGetLock = lock.tryLock(2, TimeUnit.SECONDS);
if(alreadyGetLock) {
try {
System.out.println("主线程:我拿到锁了");
}
finally {
TryLockTest.lock.unlock();
}
}
else {
System.out.println("主线程:没拿到锁,可以做点别的事情");
}
}
}

运行程序后,输出结果如下,其中第一条是立即输出的,第二句是两秒之后输出的,第三句是三秒后输出的:

新线程:我拿到锁了

主线程:没拿到锁,可以做点别的事情

新线程:我要释放锁了

本例中我们创建了一个新线程,新线程使用tryLock()方法先获得了锁,持有这个锁3秒钟。主线程使用tryLock(long time, TimeUnit unit)方法来试图获取锁,由于新线程持有锁3秒,因此主线程执行lock.tryLock(2, TimeUnit.SECONDS)等待两秒后就放弃了获取锁,返回false;如果主线程等待4秒,那么它就可以得到锁,从而得到不同的结果,感兴趣的同学可以自行测试。

锁的对象是谁

有的同学可能觉得这个锁对应的是这个锁的对象(lock),实际上不是的,这个锁没有对应的锁对象,因此synchronized关键字和显式锁之间不能产生互斥效果。

让我们做一个测试:

class LockTest {
private static Lock lock = new ReentrantLock();
public static void neverStopMethod() {
lock.lock();
try {
while(true){}//Never stop
}
finally{
lock.unlock();
}
}
public static void getLockMethod() {
synchronized(lock) {
System.out.println("我得到了class锁");
}
}
}
class NeverStopThread implements Runnable {
public void run() {
LockTest.neverStopMethod();
}
}
public class TryToGetSameLock {
public static void main(String[] args) throws InterruptedException {
ExecutorService exec = Executors.newCachedThreadPool();
exec.execute(new NeverStopThread());
exec.shutdown();
Thread.sleep(100);//等待0.1秒,确保新建的线程已经获得了锁
LockTest.getLockMethod();
}
}

程序运行后,立即输出以下结果,并且程序始终没有退出

我得到了class锁

neverStopMethod()方法使用显式锁,getLockMethod()使用内置锁来获取lock对象的锁,我们通过线程池创建了一个调用neverStopMethod()方法的线程,主线程等待0.1秒后再通过getLockMethod()方法来获取锁。如果这两个锁是互斥的,getLockMethod()会一直等待新线程释放锁,但是很遗憾,它没有等待而是直接获取了锁。

锁的公平性

公平锁:按照访问锁的先后顺序排队,先到先得,这个很好理解。

非公平锁:当一个线程想获取锁时,先试图插队,如果刚好占用锁的线程释放了锁,下一个线程还没来得及拿锁,那么当前线程就可以直接获得锁;如果锁正在被其它线程占用,则排队,排队的时候就不能再试图获得锁了,只能等到前面所有线程都执行完才能获得锁。

synchronized关键字和默认情况下的ReentrantLock都是非公平锁。非公平锁比公平锁的性能更好,假设线程一是队列中的第一个,线程二是想要插队的线程,当占用锁的线程释放了锁时JVM有两种选择:

1.阻塞线程二,启动线程一。

2.线程一的状态维持不变继续阻塞,线程二的状态也维持不变继续运行。线程状态的切换是耗费时间的,因此方案二的性能更好。

ReentrantLock类默认的构造方法是非公平锁,如果需要设置为非公平锁,只需要调用ReentrantLock(boolean fair)方法指定即可。

总结

最后我们来盘点一下synchronized关键字和ReentrantLock类各自的优势:

synchronized关键字的优势:

使用简单,不用显示释放锁,编写代码更简洁;ReentrantLock类需要显示释放锁,unlock()方法要写在finally块中,否则会有死锁的风险。

ReentrantLock类的优势:

提供了更多的特性,比如设置锁的公平性,检查锁是否被占用等功能;synchronized关键字只能是公平锁,并且没有额外的特性。

一般来讲我们不需要额外功能的时候才会使用ReentrantLock类,否则都是使用synchronized关键字。

公众号:今日说码。关注我的公众号,可查看连载文章。遇到不理解的问题,直接在公众号留言即可。

Java并发编程(五)锁的使用(下)的更多相关文章

  1. Java并发编程:锁的释放

    Java并发编程:锁的释放 */--> code {color: #FF0000} pre.src {background-color: #002b36; color: #839496;} Ja ...

  2. Java并发编程(06):Lock机制下API用法详解

    本文源码:GitHub·点这里 || GitEE·点这里 一.Lock体系结构 1.基础接口简介 Lock加锁相关结构中涉及两个使用广泛的基础API:ReentrantLock类和Condition接 ...

  3. 【Java并发编程五】信号量

    一.概述 技术信号量用来控制能够同时访问某特定资源的活动的数量,或者同时执行某一给定操作的数据.计数信号量可以用来实现资源池或者给一个容器限定边界. 信号量维护了一个许可集,许可的初始量通过构造函数传 ...

  4. Java并发编程之锁机制

    锁分类 悲观锁与乐观锁 悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改.因此对于同一个数据的并发操作,悲观锁采取加锁的形式.悲观的认为,不加锁的并发操作一定会出问题 ...

  5. java并发编程:锁的相关概念介绍

    理解同步,最好先把java中锁相关的概念弄清楚,有助于我们更好的去理解.学习同步.java语言中与锁有关的几个概念主要是:可重入锁.读写锁.可中断锁.公平锁 一.可重入锁 synchronized和R ...

  6. Java并发编程-各种锁

    安全性和活跃度通常相互牵制.我们使用锁来保证线程安全,但是滥用锁可能引起锁顺序死锁.类似地,我们使用线程池和信号量来约束资源的使用, 但是缺不能知晓哪些管辖范围内的活动可能形成的资源死锁.Java应用 ...

  7. Java并发编程 (五) 线程安全性

    个人博客网:https://wushaopei.github.io/    (你想要这里多有) 一.安全发布对象-发布与逸出 1.发布与逸出定义 发布对象 : 使一个对象能够被当前范围之外的代码所使用 ...

  8. 【Java并发编程】并发编程大合集-值得收藏

    http://blog.csdn.net/ns_code/article/details/17539599这个博主的关于java并发编程系列很不错,值得收藏. 为了方便各位网友学习以及方便自己复习之用 ...

  9. 【Java并发编程】并发编程大合集

    转载自:http://blog.csdn.net/ns_code/article/details/17539599 为了方便各位网友学习以及方便自己复习之用,将Java并发编程系列内容系列内容按照由浅 ...

  10. Java并发编程原理与实战十五:手动实现一个可重入锁

     package com.roocon.thread.ta1; public class Sequence { private MyLock lock = new MyLock(); private ...

随机推荐

  1. 手动编译Flume

    1.源码下载: 我用的是1.6版,因为加了kafka-sink,下载地址 http://www.apache.org/dyn/closer.cgi/flume/1.6.0/apache-flume-1 ...

  2. 大数据技术生态圈形象比喻(Hadoop、Hive、Spark 关系)

    [摘要] 知乎上一篇很不错的科普文章,介绍大数据技术生态圈(Hadoop.Hive.Spark )的关系. 链接地址:https://www.zhihu.com/question/27974418 [ ...

  3. Open Source BI Platform List

    资源入口: awesome-business-intelligence https://github.com/thenaturalist/awesome-business-intelligence h ...

  4. SOFA 源码分析 — 泛化调用

    前言 通常 RPC 调用需要客户端使用服务端提供的接口,而具体的形式则是使用 jar 包,通过引用 jar 包获取接口的的具体信息,例如接口名称,方法名称,参数类型,返回值类型. 但也存在一些情况,例 ...

  5. 【python进阶】深入理解系统进程2

    前言 在上一篇[python进阶]深入理解系统进程1中,我们讲述了多任务的一些概念,多进程的创建,fork等一些问题,这一节我们继续接着讲述系统进程的一些方法及注意点 multiprocessing ...

  6. prometheus alert rules文件格式化

    1.下载go(version>1.9,否则promtool工具不好使) https://golang.org/doc/install?download=go1.10.2.linux-amd64. ...

  7. Jquery浅克隆与深克隆

    Jquery浅克隆与深克隆 JavaScript部分 $("div").on('click', function() {//执行操作}) //clone处理一 $("di ...

  8. python笔记:#005#算数运算符

    算数运算符 计算机,顾名思义就是负责进行 数学计算 并且 存储计算结果 的电子设备 目标 算术运算符的基本使用 01. 算数运算符 算数运算符是 运算符的一种 是完成基本的算术运算使用的符号,用来处理 ...

  9. Leetcode_删除排序数组中的重复项

    Leetcode  删除排序数组中的重复项 题目: 给定一个排序数组,你需要在原地删除重复出现的元素,使得每个元素只出现一次,返回移除后数组的新长度. 不要使用 额外的数组空间,你必须在原地修改输入数 ...

  10. python实现四则运算和效能分析

    代码github地址:https://github.com/yiduobaozhi/-1 PSP表格: 预测时间(分钟) planning 计划 15 Estimate 估计这个任务需要多少时间 10 ...