JVM学习记录-线程安全与锁优化(一)
前言
线程:程序流执行的最小单元。线程是比进程更轻量级的调度执行单位,线程的引入,可以把一个进程的资源分配和执行调度分开,各个线程既可以共享进程资源(内存地址、文件I/O等),又可以独立调度(线程是CPU调度的基本单位)。
Java语言定义了5中线程状态,在任意一个时间点,一个线程只能有且只有其中的一种状态,5中状态如下。
新建(New):创建后尚未启动的线程处于这种状态。
运行(Runnable):Runnable包括了操作系统线程状态中的Running和Ready,也就是处于此状态的线程可能正在执行,也可能正在等待着CPU为它分配执行时间。
无限期等待(Waiting):处于这种状态的线程不会被分配CPU执行时间,它们要等待被其他线程显示地唤醒。
让线程进入无限等待的方法有如下几个:
- 没有设置Timeout参数的Object.wait()方法。
- 没有设置Timeout参数的Thread.join()方法。
- LockSupport.park()方法。
限期等待(Timed Waiting):处于这种状态的线程也不会被分配CPU执行时间,不过无须等待被其他线程显式地唤醒,在一定时间之后它们会由系统自动唤醒。
让线程进入限期等待状态的方法有如下几个:
- Thread.sleep()方法。
- 设置了Timeout参数的Object.wait()方法。
- 设置了Timeout参数的Thread.join()方法。
- LockSupport.parkNanos()方法。
- LockSupport.parkUntil()方法。
阻塞(Blocked):线程被阻塞了,“阻塞状态”是在等待着获取到一个排他锁,这个事件将在另一个线程放弃这个锁的时候发生;更通俗的解释就是一个线程正在干着一件事,没资源干其他的事,当来了其他的事时就只能阻塞的等着线程能腾出时间来处理。
结束(Terminated):已终止线程的线程状态,线程已经结束执行。
这5种状态在遇到特定的事件的时候会相互转换。
线程安全
一个比较严谨线程安全定义:当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确地结果,那么这个对象就是线程安全的。
Java语言中的线程安全
研究线程安全,需要限定于多个线程之间存在共享数据访问这个前提。Java语言中各种操作共享数据分为以下5类:
不可变
在JDK1.5以后,Java语言中不可变的对象一定是线程安全的,无论是对象的方法实现还是方法的调用者,都不需要再采取任何的线程安全保障措施。如果一个基本数据类在定义时使用final关键字修饰它,就可以保证它时不可变的。如果final修饰的是一个对象,需要保证对象的方法不会对其状态产生影响才行。例如:String类的substring()、concat()这些方法不会影响原来的值,只会生成一个新的字符串。
保证对象方法不会对其状态产生影响的实现方式有很多,最简单是将对象中带有状态的属性用final修饰。
例如Integer类中的实现代码:
/**
* The value of the {@code Integer}.
*
* @serial
*/
private final int value; /**
* Constructs a newly allocated {@code Integer} object that
* represents the specified {@code int} value.
*
* @param value the value to be represented by the
* {@code Integer} object.
*/
public Integer(int value) {
this.value = value;
}
Java中除了String类、Integer类,还有其他的Long、Double等包装类,以及BigInteger和BigDecimal等大数据类型,都符合不可变要求的类型。
绝对线程安全
绝对线程安全,是指绝对的符合前面提到的线程安全的定义,多线程永远调用对象时永远都能获得正确的结果。但是为了实现这个绝对要付出的代价是很大的,在Java中标注自己是线程安全的类,绝大多数都不是绝对线程安全的,例如Vector类,java.util.Vector是一个线程安全类,它的add()、get()、size()等都是被synchronized修饰的,但这并不能保证它是绝对安全的。
如下代码:
public class Test { private static Vector<Integer> vector = new Vector<Integer>(); public static void main(String[] args){ while (true){
for(int index = 0;index < 10;index++){
vector.add(index);
}
//移除元素的线程
Thread removeThread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i<vector.size(); i++){
vector.remove(i);
}
}
}); //打印元素的线程
Thread printThread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i<vector.size(); i++){
System.out.println(vector.get(i));
}
}
}); removeThread.start();
printThread.start();
//别创建太多线程,出现异常就手动停止运行吧,不然会一直执行下去。
while (Thread.activeCount()>5);
} } }
运行结果:
Exception in thread "Thread-229" java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 15
at java.util.Vector.get(Vector.java:748)
at com.eurekaclient2.client2.shejimoshi.JVM.Test$2.run(Test.java:38)
at java.lang.Thread.run(Thread.java:748)
尽管Vector的方法都是同步的,但是在多线程环境下,若不在调用方法端做额外的同步措施的话,仍然不是线程安全的,因为若另一线程恰好在错误的时间里删除了一个元素,导致序号i已经不再可用的话,再用i访问数组就会抛出一个ArrayIndexOutOfBoundsException。
解决方法如下(将移除和打印都设置为同步):
public class Test { private static Vector<Integer> vector = new Vector<Integer>(); public static void main(String[] args){ while (true){
for(int index = 0;index < 10;index++){
vector.add(index);
}
//移除元素的线程
Thread removeThread = new Thread(new Runnable() {
@Override
public void run() {
synchronized(vector){
for (int i = 0; i<vector.size(); i++){
vector.remove(i);
}
}
}
}); //打印元素的线程
Thread printThread = new Thread(new Runnable() {
@Override
public void run() {
synchronized(vector){
for (int i = 0; i<vector.size(); i++){
System.out.println(vector.get(i));
}
}
}
}); removeThread.start();
printThread.start(); while (Thread.activeCount()>5);
} } }
相对线程安全
我们通常所讲的线程安全就是指的相对线程安全,需要保证对象单独的操作时线程安全的,不需要做额外的保障措施。但若是对于一些特定属性的连续调用,就可能会需要在调用端添加额外的同步措施。Java语言中,大部分的线程安全类都是相对线程安全的,例如Vector、HashTable以及Collections的synchronizedCollection()方法包装的集合等。
线程兼容
线程兼容是指对象本身不是线程安全的,但是可以通过在调用端使用同步措施来保证对象在并发环境中可以安全的使用。Java中大部分类都是线程兼容的,如ArrayList、HashMap等等。
线程对立
线程对立指无论调用端是否采用了同步措施,都无法在多线程环境中并发使用代码。这种代码是有害的,应尽量避免。常见的线程对立操作有System.setIn()、System.setOut()和System.runFinalizersOnExit()等。
线程安全的实现方法
线程安全的实现主要有以下几个方法:
互斥同步
通过互斥来实现同步,临界区、互斥量、信号量都是主要的互斥实现方法。在Java中最基本的互斥同步手段就是synchronized关键字,synchronized关键字通过编译后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令,这两个字节码都需要一个reference类型的惨呼是来指明要锁定和解锁的对象。若在程序中为synchronized指明了对象参数,那就是这个对象的reference,若没有指明,则根据synchronized修饰的是实例方法或类方法,来获取对应的对象或Class对象来作为锁对象。
在虚拟机规范中要求,在执行monitorernter指令时,首先要尝试获取对象的锁。如若此对象没被锁定或当期线程已经拥有了此对象的锁,则把锁的计数器加1,响应的在执行monitorexit指令时会将锁计数器减1,当计数器为0时,锁就会被释放。若获取锁失败,那么当前线程就要进入阻塞状态,直到对象锁被另外一个线程释放为止。
有两点需要注意的是:
- synchronized同步快对于同一条线程来说是可重入的,不会出现自己把自己锁死的问题。
- 同步块在已进入的线程执行完之前,会阻塞后面其他线程的进入。
除了synchronized之外,还可以使用java.util.concurrent包中的重入锁(ReentrantLock)来实现同步。用法很相似,只是代码写法上有区别,ReentrantLock表现为API层面的互斥锁(lock()和unlock()方法配合try/finally()语句块来完成),synchronized表现为原生语法层面的互斥锁。不过ReentrantLock比synchronized增加了一些高级功能,主要有以下3项:等待可中断、可实现公平锁,以及锁可以绑定多个条件。
- 等待可中断是指当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情,可中断特性对处理执行时间非常长的同步块很有帮助。
- 公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁;非公平锁,在释放时任何一个等待线程都有机会获得锁。synchronized是非公平锁,ReentrantLock默认情况下也是非公平的,但可以通过带布尔值的构造函数要求使用公平锁。
- 锁绑定多个条件是指一个ReentrantLock对象可以同时绑定多个Condition对象,而synchronized中,锁对象的wait()和notify()或notifyAll()方法可以实现一个隐含的条件,如果要和多于一个的条件关联的时候,就不得不额外的添加一个锁,而ReentrantLock则无须这样做,只需要多次调用newCondition()方法即可。
非阻塞同步
互斥同步最主要的问题就是进行现场阻塞和唤醒锁带来的性能问题,因此这种同步也称为阻塞同步(Block Synchronization)。从处理问题的方式上来说,互斥同步属于一种悲观的并发策略,那么相对而言的就有了另一种基于冲突检测的乐观并发策略,通俗的解释就是先执行操作,如果没有其他线程争用共享数据,那操作就成功了;如果有线程争用共享数据,那就再采取其他补偿措施(常见的补偿措施就是不断重试,直到成功为止),这种乐观的并发策略不需要把线程挂起,因此也被称为非阻塞同步(Non-Block Synchronization)。
在进行操作和冲突检测时,需要保证这两个步骤的原子性,这个时候如果靠同步互斥,那就也成悲观并发了,所以只能靠硬件来完成这个保证,硬件保证一个从语义上开起来需要多次操作的行为只通过一条处理器指令就能完成,此类指令常用的有:
- 测试并设置(Test-and-Set)。
- 获取并增加(Fetch-and-Increment)。
- 交换(Swap)。
- 比较并交换(Compare-and-Swap)CAS。
- 加载链接/条件存储(Load-Linked/Store-COnditional)。
无同步方案
可重入代码
如果一个方法,它的返回结果是可以预测的,只要输入了相同的数据,就都能返回相同的结果,那它就满足可重入性的要求,当然也就是线程安全的。这个方法就是可重入代码,在这段代码可以在执行的任何时刻中断它,转而去执行另外一段代码,而在控制权返回后,原来的程序不会出现任何错误。
线程本地存储
如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行?如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内,这样,无须同步也能保证线程之间不出现数据争用的问题。例如大部分的消息队列的架构模式(生产者-消费者)都符合这个特点。
JVM学习记录-线程安全与锁优化(一)的更多相关文章
- JVM学习记录-线程安全与锁优化(二)
前言 高效并发是程序员们写代码时一直所追求的,HotSpot虚拟机开发团队也为此付出了很多努力,为了在线程之间更高效地共享数据,以及解决竞争问题,HotSpot开发团队做出了各种锁的优化技术常见的有: ...
- 【JVM.12】线程安全与锁优化
一.概述 面向过程的编程思想极大地提升了现代软件开发的生产效率和软件可以达到的规模,但是现实世界与计算机世界之间不可避免地存在一些差异,本节就如何保证并发的正确性和如何实现线程安全讲起. 二.线程安全 ...
- Java并发编程学习:线程安全与锁优化
本文参考<深入理解java虚拟机第二版> 一.什么是线程安全? 这里我借<Java Concurrency In Practice>里面的话:当多个线程访问一个对象,如果不考虑 ...
- jvm(13)-线程安全与锁优化(转)
0.1)本文部分文字转自“深入理解jvm”, 旨在学习 线程安全与锁优化 的基础知识: 0.2)本文知识对于理解 java并发编程非常有用,个人觉得,所以我总结的很详细: [1]概述 [2]线程安全 ...
- jvm(13)-线程安全与锁优化
[0]README 0.1)本文部分文字转自“深入理解jvm”, 旨在学习 线程安全与锁优化 的基础知识: 0.2)本文知识对于理解 java并发编程非常有用,个人觉得,所以我总结的很详细: [1]概 ...
- 深入理解JVM(7)——线程安全和锁优化
Java中的线程安全 按照线程安全的“安全程度”由强至弱来排序,可以将Java语中各种操作共享的数据分为以下5类:不可变. 绝对线程安全. 相对线程安全. 线程兼容和线程对立. 1.不可变 不变的对象 ...
- 深入理解JVM - 线程安全与锁优化 - 第十三章
线程安全 当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方法进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那么这个对 ...
- JVM之java并发 ——线程安全与锁优化
概述 人们很难想象现实中的对象在一项工作进行期间,会被不停地中断和切换,对象的属性(数据)可能会在中断期间被修改和变“脏”,而这些事情在计算机世界中则是很正常的事情.有时候,良好的设计原则不得不向现实 ...
- 《深入了解java虚拟机》高效并发读书笔记——Java内存模型,线程,线程安全 与锁优化
<深入了解java虚拟机>高效并发读书笔记--Java内存模型,线程,线程安全 与锁优化 本文主要参考<深入了解java虚拟机>高效并发章节 关于锁升级,偏向锁,轻量级锁参考& ...
随机推荐
- bootstrap datetimepicker
一.datepicker 早期的 二.datetimepicker 适用于bootstrap2,3兼容性不太好 三.在github上找了个不错的:https://github.com/Eonasdan ...
- Allegro中常见的文件格式
allegro/APD.jrl : 记录开启 Allegro/APD 期间每一个执行动作的 command .产生在每一次新开启 Allegro/APD 的现行工作目录下 .env : 存在 pcbe ...
- RxSwift学习笔记6:Subjects/PublishSubject/BehaviorSubject/ReplaySubject/Variable
// 从前面的几篇文章可以发现,当我们创建一个 Observable 的时候就要预先将要发出的数据都准备好,等到有人订阅它时再将数据通过 Event 发出去. // 但有时我们希望 Observabl ...
- 1.mybatis入门
一:创建表 CREATE TABLE `country` ( `id` ) NOT NULL AUTO_INCREMENT, `countryname` varchar() DEFAULT NULL, ...
- hdu 1226 超级密码
超级密码 Time Limit: 20000/10000 MS (Java/Others) Memory Limit: 65536/32768 K (Java/Others) Problem D ...
- 【文文殿下】P3740 [HAOI2014]贴海报
题解 一开始想到离散化,然后暴力模拟.但是存在一种hack数据: [5,7] [1,5] [7,9] 这样会错误的认为第一个区间被覆盖了(因为两个端点被覆盖).所以我们设置一个玄学调参系数,在一个区间 ...
- FunDA(17)- 示范:异常处理与事后处理 - Exceptions handling and Finalizers
作为一个能安全运行的工具库,为了保证占用资源的安全性,对异常处理(exception handling)和事后处理(final clean-up)的支持是不可或缺的.FunDA的数据流FDAPipeL ...
- C# 中 DataTable转换成IList
在用C#作开发的时候经常要把DataTable转换成IList:操作DataTable比较麻烦,把DataTable转换成IList,以对象实体作为IList的元素,操作起来就非常方便. 注意:实体的 ...
- Storm入门示例
开发Storm的第一步就是设计Topology,为了方便开发者入门,首先我们设计一个简答的例子,该例子的主要的功能就是把每个单词的后面加上Hello,World后缀,然后再打印输出,整个例子的Topo ...
- WebDriver高级应用实例(7)
7.1在测试中断言失败的步骤进行屏幕截图 目的:在测试过程中,在断言语句执行失败时,对当前的浏览器进行截屏,并在磁盘上新建一个yyyy-mm-dd格式的目录,并在断言失败时新建一个已hh-mm-ss格 ...