Java并发读书笔记:线程安全与互斥同步
本篇参考许多著名的书籍,形成读书笔记,便于加深记忆。
前文传送门:Java并发读书笔记:JMM与重排序
导致线程不安全的原因
当一个变量被多个线程读取,且至少被一个线程写入时,如果读写操作不遵循happens-before
规则,那么就会存在数据竞争的隐患,如果不给予正确的同步手段,将会导致线程不安全。
什么是线程安全
Brian Goetz在《Java并发编程实战》中是这样定义的:
当多个线程访问一个类时,如果不用考虑这些线程在运行时环境下的调度和交替执行,并且不需要额外的同步及在调用方代码不必做其他的协调,这个类的行为仍然是正确的,那么这个类就是线程安全的。
周志明在《深入理解Java虚拟机》中提到:多个线程之间存在共享数据时,这些数据可以按照线程安全程度进行分类:
不可变
不可变的对象一定是线程安全的,只要一个不可变的对象被正确地构建出来,那么它在多个线程中的状态就是一致的。例如用final关键字修饰对象:
- 修饰的是基本数据类型,final修饰不可变。
- 修饰的是一个对象,就需要保证其状态不发生变化。
JavaAPI中符合不可变要求的类型:String类,枚举类,数值包装类型(如Double)和大数据类型(BigDecimal)。
绝对线程安全
即完全满足上述对于线程安全定义的。
满足该定义其实需要付出很多代价,Java中标注线程安全的类,实际上绝大多数都不是线程安全的(如Vector),因为它仍需要在调用端做好同步措施。Java中绝对线程安全的类:CopyOnWriteArrayList
、CopyOnWriteArraySet
。
相对线程安全
即我们通常所说的线程安全,Java中大部分的线程安全类都属于该范畴,如Vector
,HashTable
,Collections
集合工具类的synchronizedCollection()
方法包装的集合等等。就拿Vector举例:如果有个线程在遍历某个Vector、有个线程同时在add这个Vector,99%的情况下都会出现ConcurrentModificationException
,也就是fail-fast
机制。
线程兼容
对象本身并不是线程安全的,可以通过在调用段正确同步保证对象在并发环境下安全使用。如我们之前学的分别与Vector和HashTable对应的ArrayList
和HashMap
。
对象通过synchronized关键字修饰,达到同步效果,本身是安全的,但相对来说,效率会低很多。
线程对立
无论调用端是否采取同步措施,都无法正确地在多线程环境下执行。Java典型的线程对立:Thread类中的suspend()和resume()方法:如果两个线程同时操控一个线程对象,一个尝试挂起,一个尝试恢复,将会存在死锁风险,已经被弃用。
常见的对立:System.setIn()
,System.setOut()
和System.runFinalizersOnExit()
。
互斥同步实现线程安全
互斥同步也被称做阻塞同步(因为互斥同步会因为线程阻塞和唤醒产生性能问题),它是实现线程安全的其中一种方法,还有一种是非阻塞同步,之后再做学习。
互斥同步:保证并发下,共享数据在同一时刻只被一个线程使用。
synchronized内置锁
其中使用synchronized
关键字修饰方法或代码块是最基本的互斥同步手段。
synchronized
是Java提供的一种强制原子性的内置锁机制,以synchronized
代码块的定义方式来说:
synchronized(lock){
//访问或修改被锁保护的共享状态
}
它包含了两部分:1、锁对象的引用 2、锁保护的代码块。
每个Java对象都可以作为用于同步的锁对象,我们称该类的锁为监视器锁(monitor locks),也被称作内置锁。
可以这样理解:线程在进入synchronized之前需要获得这个锁对象,在线程正常结束或者抛出异常都会释放这个锁。
而这个锁对象很好地完成了互斥,假设A持有锁,这时如果B也想访问这个锁,B就会陷入阻塞。A释放了锁之后,B才可能停止阻塞。
锁即对象
- 对于普通同步方法,锁是当前实例对象(this)。
//普通同步方法
public synchronized void do(){}
- 对于静态同步方法,锁是当前的类的Class对象。
//静态同步方法
public static synchronized void f(){}
- 对于同步方法块,锁的是括号里配置的对象。
//锁对象为TestLock的类对象
synchronized (TestLock.class){
f();
}
明确:synchronized方法和代码块本质上没啥不同,方法只是对跨越整个方法体的代码块的简短描述,而这个锁是方法所在对象本身(static修饰的方法,对象是当前类对象)。这个部分可以参考:Java并发之synchronized深度解析
是否要释放锁
释放锁的情况:
- 线程执行完毕。
- 遇到return、break终止。
- 抛出未处理的异常或错误。
- 调用了当前对象的wait()方法。
不释放锁的情况:
- 调用了Thread.sleep()和Thread.yield()暂停执行不会释放锁。
- 调用suspend()挂起线程,不会释放锁,已被弃用。
实现原理
通过对.class文件反编译可以发现,同步方法通过ACC_SYNCHRONIZED
修饰,代码块同步使用monitorenter
和monitorexit
两个指令实现。
虽然两者实现细节不同,但其实本质上都是JVM基于进入和退出Monitor对象来实现同步,JVM的要求如下:
monitorenter
指令会在编译后插入到同步代码块的开始位置,而monitorexit
则会插入到方法结束和异常处。每个对象都有一个
monitor
与之关联,且当一个monitor
被持有之后,他会处于锁定状态。线程执行到
monitorenter
时,会尝试获取对象对应monitor
的所有权。在获取锁时,如果对象没被锁定,或者当前线程已经拥有了该对象的锁(可重进入,不会锁死自己),将锁计数器加一,执行
monitorexit
时,锁计数器减一,计数为零则锁释放。获取对象锁失败,则当前线程陷入阻塞,直到对象锁被另外一个线程释放。
啥是重进入?
重进入意味着:任意线程在获取到锁之后能够再次获取该锁而不会被锁阻塞,synchronized
是隐式支持重进入的,因此不会出现锁死自己的情况。
这就体现了锁计数器的作用:获得一次锁加一,释放一次锁减一,无论获得还是释放多少次,只要计数为零,就意味着锁被成功释放。
ReentrantLock(重入锁)
ReentrantLock
位于java.util.concurrent(J.U.C)
包下,是Lock接口的实现类。基本用法与synchronized
相似,都具备可重入互斥的特性,但拥有扩展的功能。
Lock接口的实现提供了比使用synchronized方法和代码块更广泛的锁操作。允许更灵活的结构,具有完全不同的属性,并且可能支持多个关联的Condition对象。
RenntrantLock官方推荐的基本写法:
class X {
//定义锁对象
private final ReentrantLock lock = new ReentrantLock();
// ...
//定义需要保证线程安全的方法
public void m() {
//加锁
lock.lock();
try{
// 保证线程安全的代码
}
// 使用finally块保证释放锁
finally {
lock.unlock()
}
}
}
API层面的互斥锁
ReentrantLock表现为API层面的互斥锁,通过lock()
和unlock()
方法完成,是显式的,而synchronized表现为原生语法层面的互斥锁,是隐式的。
等待可中断
当持有线程长期不释放锁的时候,正在等待的线程可以选择放弃等待或处理其他事情。
公平锁
ReentrantLock锁是公平锁,即保证等待的多个线程按照申请锁的时间顺序依次获得锁,而synchronized是不公平锁。
锁绑定
一个ReentrantLock对象可以同时绑定多个Condition对象。
JDK1.6之前,ReentrantLock在性能方面是要领先于synchronized锁的,但是JDK1.6版本实现了各种锁优化技术,后续性能改进会更加偏向于原生的synchronized。
参考数据:《Java并发编程实战》、《Java并发编程的艺术》、《深入理解Java虚拟机》
Java并发读书笔记:线程安全与互斥同步的更多相关文章
- Java并发读书笔记:如何实现线程间正确通信
目录 一.synchronized 与 volatile 二.等待/通知机制 等待 通知 面试常问的几个问题 sleep方法和wait方法的区别 关于放弃对象监视器 三.等待通知典型 生产者消费者模型 ...
- Java并发读书笔记:线程通信之等待通知机制
目录 synchronized 与 volatile 等待/通知机制 等待 通知 面试常问的几个问题 sleep方法和wait方法的区别 关于放弃对象监视器 在并发编程中,保证线程同步,从而实现线程之 ...
- Java并发读书笔记:Lock与ReentrantLock
Lock位于java.util.concurrent.locks包下,是一种线程同步机制,就像synchronized块一样.但是,Lock比synchronized块更灵活.更复杂. 话不多说,我们 ...
- Java并发读书笔记:JMM与重排序
目录 Java内存模型(JMM) JMM抽象结构 重排序 源码->最终指令序列 编译器重排序 处理器重排序 数据依赖性 as-if-serial happens-before happens-b ...
- 《深入了解java虚拟机》高效并发读书笔记——Java内存模型,线程,线程安全 与锁优化
<深入了解java虚拟机>高效并发读书笔记--Java内存模型,线程,线程安全 与锁优化 本文主要参考<深入了解java虚拟机>高效并发章节 关于锁升级,偏向锁,轻量级锁参考& ...
- java并发编程笔记(三)——线程安全性
java并发编程笔记(三)--线程安全性 线程安全性: 当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些进程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现 ...
- java并发编程笔记(七)——线程池
java并发编程笔记(七)--线程池 new Thread弊端 每次new Thread新建对象,性能差 线程缺乏统一管理,可能无限制的新建线程,相互竞争,有可能占用过多系统资源导致死机或者OOM 缺 ...
- java并发编程笔记(五)——线程安全策略
java并发编程笔记(五)--线程安全策略 不可变得对象 不可变对象需要满足的条件 对象创建以后其状态就不能修改 对象所有的域都是final类型 对象是正确创建的(在对象创建期间,this引用没有逸出 ...
- java effective 读书笔记
java effective 读书笔记 []创建和销毁对象 静态工厂方法 就是“封装了底层 暴露出一个访问接口 ” 门面模式 多参数时 用构建器,就是用个内部类 再让内部类提供构造好的对象 枚举 si ...
随机推荐
- HDFS的HA集群原理分析
1.简单hdfs集群中存在的问题 不能存在两个NameNode 单节点问题 单节点故障转移 2.解决单节点问题 找额外一个NameNode备份原有的数据 会出现脑裂 脑裂:一个集群中多个管理者数据 ...
- C#反射与特性(二):探究反射
目录 1,反射的使用概述 2,获取 Type 在上一章中,我们探究了 C# 引入程序集的各种方法,这一章节笔者将探究 C# 中使用反射的各种操作和代码实践. 1,反射的使用概述 1.1 什么是反射 & ...
- Appium环境搭建超详细教程
前言: 本系列教程会从软件的基本安装开始,最终目的是通过完成几个案例后, 大家实现自由抓取App中想要的资源. 本系列以后会更的: Appium基本使用及控制真机及安卓模拟器 Mitmproxy抓包工 ...
- React useEffect的源码解读
前言 对源码的解读有利于搞清楚Hooks到底做了什么,如果您觉得useEffect很"魔法",这篇文章也许对您有些帮助. 本篇博客篇幅有限,只看useEffect,力求简单明了,带 ...
- bootstrap4.4 Stretched link的使用
Stretched link功能介绍:扩大可点击区域. 原理 .stretched-link::after { position: absolute; top: 0; right:0; bottom: ...
- bootstrap:导航下拉菜单
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <meta name ...
- 扫描器是如何判定有xss漏洞的
这个问题,看似简单,实则触及到很多人的知识盲区 我们都知道,弹窗就能判定这个页面存在xss, 那么扫描器是怎么判断的呢,或者说扫描器是怎么判断是否弹窗的呢 测试发现 当响应的头中content-typ ...
- lvs+keepalived部署k8s v1.16.4高可用集群
一.部署环境 1.1 主机列表 主机名 Centos版本 ip docker version flannel version Keepalived version 主机配置 备注 lvs-keepal ...
- vue引用外部JS的两种种方案
前言 肯定会遇到没有npm化的库 自己写的js 方法 在Vue中该怎么引用呢 第一种 如果库是es6写的 就可以用import 引入 比如我自己写的http 封装接口的方法 就可以这样子导入哦 第二种 ...
- Qt Installer Framework翻译(3-4)
更新组件 下图说明了用于更新已安装组件的默认工作流程: 本节使用在macOS上运行的Qt 5维护工具为例,来演示用户如何更新已安装组件. 启动更新程序 用户启动维护工具时,将打开"简介&qu ...