Java并发编程实战————可重入内置锁

引言

在《Java Concurrency in Practice》的加锁机制一节中作者提到:

    Java提供一种内置的锁机制来支持原子性:同步代码块。“重入”意味着获取锁的操作的粒度是“线程”,而不是调用。当某个线程请求一个由其他线程持有的锁时,发出请求的线程就会阻塞。然而,由于内置锁时可重入的,因此如果某个线程试图获得一个已经由它自己持有的锁,那么这个请求就会成功。————《Java Concurrency in Practice》

关于上面引述的这段话,出自书中第21页,但是从书中给出的例子来看,对于这个概念真的很难理解,有很多问题blowed my mind。而最重要的关键,当然是“同一个线程”。

而锁这个东西,我们在程序中需要通过其他的线程来感知锁的存在,否则如果我用一个线程执行了一个用synchronized修饰的方法,谁来证明锁的存在?这也是测试代码的书写难点。

测试代码

public class Test {public static void main(String[] args) throws InterruptedException {// 创建一个子类对象final Child widget = new Child();// 定义线程1Thread th1 = new Thread("th1") {@Overridepublic void run() {System.out.println(super.getName() + ":start...");widget.doSometing();}};// 定义线程2Thread th2 = new Thread("th2") {@Overridepublic void run() {System.out.println(super.getName() + ":start...");/*** 下行在th1刚刚调用子类重写的父类加锁方法doSometing()时,* 另一个线程th2直接调用父类的其他加锁方法会出现等待现象,说明th1调用子类中重写的加锁方法会立刻持有父类锁,* 此时不允许调用父类其他的加锁方法*/widget.doAnother();/*** 下行在th1开始调用子类重写的父类加锁方法后,立刻通过另一个线程th2调用父类的未加锁方法doNother(),* th2会立刻执行完毕,不需要等待,也就 证明了内置锁对那些没有加锁的方法是不起作用的,也就是说这些没有加锁的方法,* 不会因为其他线程持有该类的内置锁就处于等待或阻塞的状态而无法执行*/// widget.doNother();/*** 如果说调用doAnother证明了调用重写的父类加锁方法会直接持有父类锁的话,* 那么下行就证明了调用子类的加锁方法也一定会获得该类的内置锁,就算这个方法已经* 持有了父类锁,也就是说线程th1在执行doSomething()之初就持有了子类锁和父类锁两个锁,*/// widget.doMyLike();/** th2调用doSometing()是需要等待的,并不是继承的关系,不是重入,重入是发生在一个线程中的 */// widget.doSometing();}};th1.start();Thread.sleep(100);th2.start();}
}class Father {/** 唯一被子类Child重写的方法 */public synchronized void doSometing() {System.out.println(Thread.currentThread().getName() + ":Father ... do something...");}public synchronized void doAnother() {System.out.println(Thread.currentThread().getName() + ":Father... do another thing...");}public void doNother() {System.out.println(Thread.currentThread().getName() + ":Father... do Nothing...");}
}class Child extends Father {@Overridepublic synchronized void doSometing() {try {System.out.println(Thread.currentThread().getName() + ":Child do something...");Thread.sleep(5000);System.out.println(Thread.currentThread().getName() + ":end Child do something...");} catch (InterruptedException e) {e.printStackTrace();}super.doSometing();}public synchronized void doMyLike() {System.out.println(Thread.currentThread().getName() + ":Child do my like...");}
}

这是补充了网上代码的测试code,重要的地方都写了注释,原版代码可以参考这篇文章《java内置锁synchronized的可重入性》

 

这段代码其实是书中程序清单2-7的代码扩展和补充,参考文章的例子非常给力,不过还是需要花点头脑去理解和总结(不过诸位放心,本篇文章绝对不是简单的拿来主义)。

 

测试code想要表达什么?在main方法中声明了两个线程,用th1去制造“可重入”的条件,而th2用于感知锁的存在。我们说可重入是指同一个线程而言。但是同一个线程如何才可以持有了一个类的锁后又去调用加锁代码块?

就像上述代码所示:父类的加锁方法doSomething()被子类重写了,不仅重写了,还又加了个锁。那么th1在调用子类doSomething()方法的时候,不仅会获得子类的锁,还会同时获得一个父类的锁,也就是说doSomething()在被th1的调用之初,th1就会立刻获得子类和父类两个类的内置锁。

而一个线程持有了这个类的内置锁导致的结果是,其他线程需要等待或阻塞执行该类中其他任何加锁方法,但未加锁方法不受影响!

因此,根据这个代码规则,th1此时持有了子类锁和父类锁,那么th2在执行父类的doAnother()加锁方法时就会出现阻塞执行的情况。

但是我突然又有一个想法,为何一个线程同时获得子类和父类的双重锁的条件这么多?如果子类的重写方法没有锁呢?如果父类的方法没有锁,子类重写的方法有锁呢?这些情况又会是怎样的执行结果?于是我调整了代码,将这两种情况重现测试了一下:

情况一:父类加锁,子类未加锁,th1调用子类重写方法:

父类:

子类:

执行结果:

情况二:父类方法未加锁,子类重写后加锁,th1调用子类重写的该方法。

父类:

子类:

执行结果:

总结:子类重写了父类方法时,如果子类该方法有同步锁,那么不论父类该方法是否加锁,线程在调用子类的这个方法时都会同时获得子类和父类双重锁,从而影响其他线程调用子类和父类中任何加锁方法。

(说实话,如果是gif动图,效果会更明显一些,因为在子类重写的doSomething()中,有一个5秒的线程睡眠时间,这样的测试效果是比较直观的。)

以上就是关于“重入”的引申理解,即关于线程获得双重锁的知识总结,可能有些绕,而且后面的扩展总结也比较难想到,笔者建议将上面的完整代码考下来运行一下,体会一下。而且笔者在必要的输出语句上补充了线程信息,类名信息,便于区分输出结果和执行顺序,线程的睡眠时间也做了调整,便于理解,注释也尽量做到严谨概括。其中最重要的是th2定义中的4种情况,当然可能还有其他的情况我没有列举出来,比如子类新增一个同步方法,在th1中调用的时候是否也会获得父类的内置锁呢?大家可以去尝试一下。

总之,再次强调,一旦线程获得了某个类的内置锁,其他线程便会阻塞执行该类中的任何同步方法,但该类的非同步方法是不受影响的。

如有疑问,欢迎文末留言!

=======2018.6.21 清晨更新=======================================================

经过检验测试,如果子类中新增一个同步方法,例如前面代码中的synchronized doMyLike(),线程th1调用之后依然获取了子类和父类双重锁。

这样我们就将前面的概念上升到了更广泛的高度上:

如果一个线程调用了一个对象的同步方法,那么这个线程不仅持有该类对象的锁,由于子类对象同时也是父类对象,因此其他线程不能访问父类中其他的同步方法,使其他线程进入阻塞状态。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/558983.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

java的守护进程与非守护进程

java的守护进程与非守护进程 最近重新研究Java基础知识,发现以前太多知识知识略略带过了,比较说Java的线程机制,在Java中有两类线程: User Thread(用户线程)、Daemon Thread(守护线程) ,(PS:以前忽略了&a…

双剑合璧————Spring Boot + Mybatis Plus

引言 最近在学习Mybatis Plus的使用,希望通过spring boot快速将mybatis plus整合进来。 对于springboot项目,mybatis plus团队也有自己的启动器 :mybatis-plus-boot-starter。这个依赖内部已经整合了mybatis-spring,也包括非快速…

Git初学札记(一)————Git简介与安装

前言 Git是一个开源的分布式版本控制系统,可以有效、高速地处理从很小到非常大的项目版本管理。Git是Linus Torvalds为了帮助管理Linux内核开发而开发的一个开放源码的版本控制软件。(在这里再一次致敬Linus大神)特点 分布式相比于集中式的最…

Git初学札记(二)————EGit导入远程Git仓库项目(Clone操作)

引言 我们在实际开发项目的时候,难免要使用像Eclipse或者IDEA这样的继承开发工具,除了部分“牙牙学语”的程序员需要手动输入javac去编译程序以外,在实际开发中手动编译并运行项目的“猿族”应该是已经绝种了。 我个人认为,使用gi…

Git初学札记(三)————创建Git版本库

引言 版本库即所谓的Git仓库,英文名称是Repository,可以简单理解为一个目录(.git folder),这个目录可以记录并保存直接父级及其子目录下的全部文本文件的修改操作,谓之“版本控制”! 手动建库 不…

Git初学札记(四)————Git Push的常规操作与Pull冲突解决

目录 引言 Git命令行的远程Push EGit Push操作中的冲突问题 同步 工作区与本地库同步 工作区与远程库同步 图标 重点 引言 在团队开发当中,Git Push是多人协作环节中的最重要的一环可能没有之一。同SVN一样,push操作可以看做是对远端程序的提交…

Git初学札记(五)————Branch分支管理

引言 正如之前的博客中提到的,Git区别于Svn的一个最明显的功能就是分支管理功能。 那么什么是分支?分支又能为我们的开发带来什么翻天覆地的变化呢?(为了使博客的内容更具权威性和专业性,以下部分内容摘自官方文档《Gi…

Git初学札记(六)————在远程新建本地Branch与在本地新建远程Branch

引言 本篇博客介绍将现有的本地分支以新的分支形式推送到远程库中,和以新的分支的形式从远程库中拉取一个分支。这两个功能都是比较简单的操作,但是在实际开发中,可能会在开发初期有所触及。比如我们希望将远程的dev分支拉取到本地来进行开发…

Git初学札记(七)————合并分支(merge)

目录 引言 开始Merge 1、History视图 2、Team菜单 3、Git Repositories视图 巧用Git Staging视图 放弃Merging 可能的Merge结果 引言 Git鼓励开发者使用分支来进行程序的开发。但是最终只会有一个版本发行出去,因此,我们需要将开发好的分支merg…

公钥,私钥和数字签名这样理解轻松入门!

公钥,私钥和数字签名这样理解轻松入门!参考博文:https://blog.csdn.net/21aspnet/article/details/7249401 (公钥和私钥是成对出现的,可以把他们看成锁头和钥匙的关系,公钥为锁头,私钥是钥匙&am…

Spring Boot + JSP

目录 引言 Maven依赖 JSP页面 application.properties配置 controller 结果展示 引言 在尝试使用shiro的时候需要页面登录的加持,但是长期的前后端分离工作,导致页面的知识几乎忘光。突然想通过jsp这种简单的形式来学习其他的技术,作为…

https和http的区别

https和http的区别一、基本概念(http服务器-->本地浏览器,正确快速传输;https安全套接字层,http的安全版本, httpssl层,建立一个信息安全的通道,保证数据传输的安全,确认网站的…

Markdown简明使用

#Markdown使用技巧 ##代码背景 可以使用反引号,来将特殊文字括起来,这样Markdown会自动为引号中的内容加入背景。 形如:内容 例如:Java project > team > share project ##图片上传 图片无法像正常的文本编辑器那样直接粘贴…

Spring Boot + Mybatis 快速整合

引言 最近在工作结束后抽时间学习了一下mybatis的知识,因为之前有学习过,但是经久不用,也未曾踏实地整理,因此有所淡忘。 super meeting会议管理系统是我厂最近开发的一套会议预约平台。持久层框架经讨论,选为灵活优…

SQL关联查询————LEFT JOIN关键字的使用

引言 关联查询一直是非常重要的SQL使用技巧。 在一次查询操作中&#xff0c;使用mybatis进行条件查询&#xff0c;在没有使用 LEFT JOIN 关键字的情况下是这样写的&#xff1a; <!-- 查找成员 --><select id"selectUsers" resultMap"selectUsers_Res…

Java知识点全面汇总

&#xfeff;&#xfeff; 相关概念 面向对象的三个特征 封装&#xff0c;继承&#xff0c;多态&#xff0c;这个应该是人人皆知&#xff0c;有时候也会加上抽象。 多态的好处 允许不同类对象对同一消息做出响应&#xff0c;即同一消息可以根据发送对象的不同而采用多种不…

Git初学札记(八)————版本回退

引言 不论是使用svn还是git&#xff0c;版本回退都是一个非常重要的功能。 EGit版本回退 在History视图中&#xff0c;我们可以看到我们的commit历史&#xff0c;选中任意一个commit版本&#xff0c;右键reset —>Hard &#xff0c;即可回退到指定版本。 注意&#xff0c…

Java实现用户头像上传(修改默认文件大小限制)

概述 每次说起文件上传&#xff0c;就不得不提一下前端的实现方式&#xff0c;说来也奇怪&#xff0c;本博主最热门的博客居然也是文件上传&#xff0c;3万多的访问量占了总访问量的一多半&#xff1a;《传统form表单提交方式的文件上传与文件存储》&#xff0c;而且&#xff…

垃圾回收机制和JVM垃圾回收常见算法

垃圾回收机制和JVM垃圾回收常见算法垃圾回收的好处和特点&#xff1a; 好处&#xff1a; 1. 提高编程效率&#xff1b; 2. 垃圾回收机制保护程序的完整性。特点&#xff1a; 1. 只能回收无用对象的内存空间&#xff0c;对其他物理资源无能为力&#xff1b; 2. 为了更快回收不再…

Java 多线程 —— 死锁与锁的错误用法

引言 死锁状态的大致情况是&#xff1a;Thread_1在获得A对象的锁后&#xff0c;紧接着去请求B对象的锁 &#xff0c;Thread_2在获得了B对象的锁后&#xff0c;紧接着又去请求A对象的锁&#xff0c;如下图&#xff1a; 一、模拟一个死锁 public class DeadLockDemo {static cl…