线程安全的实现方式和锁优化

什么是线程安全?

在Java中线程安全的场景有哪些?

不可变

绝对线程安全

相对线程安全

线程兼容

线程对立

 Java中保证线程安全的方式?

互斥同步

非阻塞同步

无同步方案

1. 可重入代码

2. 线程本地存储

锁优化

自旋锁

锁消除

锁粗化

轻量锁

偏向锁


什么是线程安全?

《Java Concurrency in Practice》作者Goetz 对于线程安全的定义:

当多个线程访问一个对象,如果不用考虑这些线程在运行环境下的调度和交替执行,也不需要额外的同步,或者调用方在进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那么这个对象就是线程安全的。

对于上面的定义,需要说明的是对象内部都会封装了保证线程安全的实现,但是这样也不容易是线程安全的,通常我们对上面的定义弱化一点,那就是限于对象行为的单次调用。因为就是线程安全的类,多次调用也可能线程不安全,需要另外同步处理。 

在Java中线程安全的场景有哪些?

这里的线程安全,是多个线程访问共享变量这个前提,Java中多线程访问共享资源的安全性由强到弱,分为下面五类。

不可变

只要一个final变量被正确构建出来(没有发生this逃逸问题),对于其他线程而言,是不可变的,是线程安全的。

如果共享数据是基本类型,只要定义为final类型就可保证不可变,如果共享变量是对象的话,不可变指的是行为不可变,定义属性为final类型。

绝对线程安全

绝对线程安全,从线程安全的定义上来看,不需要另外加上同步控制,实际上,就算是线程安全的类,在调用的时候,可能是线程不安全的,需要另外加上同步控制,保证是绝对安全。

package net.lingala.zip4j.test;

import java.util.Vector;

public class AbsoluteThreadSafety {
	private static Vector<Integer> vector = new Vector<>();
	public static void main(String[] args) {
		while(true) {
			for(int i=0;i<20;i++) {
				vector.add(i);
			}
			Thread remove = new Thread(new Runnable() {
				
				@Override
				public void run() {
					for(int i=0;i<vector.size();i++) {
						try {
							vector.remove(i);
						}catch(Exception e) {
							e.printStackTrace();
						}
					}
				}
			});
			Thread get = new Thread(new Runnable() {
				@Override
				public void run() {
					for(int i=0;i<vector.size();i++) {
						try {
							int m = vector.get(i);
						} catch (Exception e) {
							e.printStackTrace();
						}
					}
				}
			});
			remove.start();
			get.start();
			if(Thread.activeCount()>500) break;
		}
		
	}
}

运行结果:

java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 24
	at java.util.Vector.get(Vector.java:744)
	at net.lingala.zip4j.test.AbsoluteThreadSafety$2.run(AbsoluteThreadSafety.java:26)
	at java.lang.Thread.run(Thread.java:745)

 分析: size,remove,get方法都是synchronize修饰的同步方法,线程获取没有修改前的下标,操作时候下标发生改变,很容易发生越界。

解决:将remove,get线程,加上同步锁,排队执行。

package net.lingala.zip4j.test;

import java.util.Vector;

public class AbsoluteThreadSafety {
	private static Vector<Integer> vector = new Vector<>();
	private static Object obj = new Object();
	public static void main(String[] args) {
		while(true) {
			for(int i=0;i<20;i++) {
				vector.add(i);
			}
			Thread remove = new Thread(new Runnable() {
				
				@Override
				public void run() {
					synchronized (obj) {
						for(int i=0;i<vector.size();i++) {
							try {
								vector.remove(i);
							}catch(Exception e) {
								e.printStackTrace();
							}
						}
					}
					
				}
			});
			Thread get = new Thread(new Runnable() {
				@Override
				public void run() {
					synchronized (obj) {
						for(int i=0;i<vector.size();i++) {
							try {
								int m = vector.get(i);
							} catch (Exception e) {
								e.printStackTrace();
							}
						}
					}
					
				}
			});
			remove.start();
			get.start();
			if(Thread.activeCount()>500) break;
		}
		
	}
}

相对线程安全

相对线程安全,单独调用对象的方法是安全的,不需要另外的同步操作。怎么理解?也就是,对象对于自己的方法内部做了同步控,是线程安全的类,如Vector等等。

线程兼容

对象是线程不安全的,但是调用的时候,可以加上同步控制保证线程安全,如ArrayList,HashMap等等。

线程对立

不管使用什么措施,在多线程环境就是线程不安全的。

这种代码很少出现,一个例子就是Thread的suspend()和resume(),多线程调用一个线程实例的这两个方法会发生死锁。

 Java中保证线程安全的方式?

互斥同步

同步指的是多个线程访问共享数据时,保证共享数据同一时刻只能被一个线程使用。互斥是同步的实现手段,互斥的实现方式有临界区,互斥量,信号量。比较常用的是手段是synchronize关键字和lock锁。

非阻塞同步

不通过加锁的方式,而是通过乐观锁的机制(CAS)实现同步。常见的手段是Atomic类

CAS存在ABA问题, 就是一个变量被修改后,又被还原了,另一个线程认为是没有改过,Java中提供了一个类AtomicStampedReference来控制变量的版本解决这个问题,不过,大部分情况下,ABA问题不会影响并发的正确性。

无同步方案

1. 可重入代码

可重入:一段代码执行一部分后,中断,转而执行另一段代码,获得控制权后返回,执行不会有任何错误

怎么判断是否是可重入的?

一段代码不涉及到对共享资源的读写操作,执行结果是可预期的,对于每个线程而言执行结果都是一样的,就是可重入的

因此是线程安全的,例如类中没有读写实例变量或者静态变量的方法。

2. 线程本地存储

将读写的资源和每个线程进行绑定,每个线程访问的都是自己的资源,也就不存在线程安全问题,使用类ThreadLocal实现。

锁优化

jdk1.6之后提供了自旋锁、自适应自旋锁、轻量锁、偏向锁、锁粗化等手段来减少线程共享数据时候的开销。

锁存在四种状态:无锁状态、偏向锁状态、轻量锁状态、重量锁状态,这几种锁的状态是随着竞争加强而不断升级的。

注意,这几种状态,只能升级不能降级。

自旋锁

在采用互斥同步的手段保证线程安全的时候,没有获取到锁的线程只能阻塞,等待获取到锁后,再次执行。这种挂起,又再次执行给系统较大的开销。并且因为实际上锁获取、锁释放的过程耗费时间不会太长,可以让请求锁的线程不放弃CPU的执行时间,执行一个忙循环,而不是挂起,和获取到锁的线程并行执行。

定义:请求锁的时候,如果没有拿到锁,不阻塞,不放弃CPU执行时间,执行一个忙循环的手段就是自旋。

开启:-XX:+UseSpinning参数开启,默认是开启的。

缺点:如果锁占用的时间较短,自旋能够减少很多开销,但是如果锁占用时间很长,自旋白白占有CPU时间,耗费资源,因此都会限制自旋的次数,默认是10次(-XX:PreBlockPin参数可修改)

自适应自旋锁:自旋的时间不再是一个固定值,而是由上次获取到同一个锁自旋的时间和锁的状态决定。如果同一个锁对象,自旋等待成功获取到锁,认为再次自旋是可能再次成功的,虚拟机就允许自旋的时间更长,否则,如果自旋很少成功,就会取消自旋操作。

锁消除

一些代码做了同步处理,但是线程执行中不存在竞争共享数据的可能,就会消除锁,这是虚拟机自发的一种优化手段。

锁粗化

一般在做同步处理的时候,都会尽可能的将同步区域限制小,如果一段代码多次获取同一个锁,虚拟机会对同步区域进行扩大是的这段代码只需要获取一次锁,这种扩大同步区域的优化手段,就是锁粗化。

例如:Stringbuffer连续的append方法,就会将同步区域扩大到第一次append和最后一次append的范围。

轻量锁

轻量级锁不是用来代替重量级锁的,只是为了在没有多线程竞争的前提下,减少使用重量级锁互斥产生的性能消耗。

轻量级锁的加锁和解锁都是通过CAS操作完成的,并且如果存在两个以上的线程竞争同一个锁,轻量级锁会膨胀为重量级锁。

什么轻量级锁能够优化锁的性能?

对于绝大数锁而言,在同步周期内都是不存在竞争的,那么使用轻量级锁的CAS操作就能够避免互斥同步的开销。

偏向锁

轻量级锁是在无竞争的情况下使用CAS操作消除同步的互斥量,那么偏向锁就是无竞争的情况下将整个同步消除。

偏向锁,会偏向第一个获取到锁的线程,如果接下来的执行中,没有线程去竞争这个偏向锁,那么持有偏向锁的线程就会消除同步。

但是,如果存在另一个线程去竞争偏向锁,偏向锁将会失效,回复到未锁定或者轻量锁状态,不会直接升级为重量锁。

参考:《深入理解Java虚拟机-JVM高级特性和最佳实践(第二版)》

 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值