java线程安全与锁优化(二)

上一篇文章中我们从StringBuilder与StringBuffer的区别开始引申出线程安全相关的概念与对线程安全的“安全强度”进行说明与测试。本篇文章我们将继续对java线程安全与锁优化进行学习。(不足或有错误之处,欢迎指正)

三、如何保证线程安全

   回顾: 前文中对线程安全 通过“安全强度”将其分为了 不可变、绝对线程安全、相对线程安全、线程兼容、线程对立5类。其中不可变对象在构建过程中只要不发生 this 引用逃逸 则就是线程安全的  不需要做额外的操作;绝对线程安全需要付出的代价太大;绝大多数java api 中标注了线程安全类都是相对线程安全,需要在实际操作中根据情况进行额外同步操作来保证程序在多线程环境下正常运行;线程兼容则是在本身线程不安全的情况下采用同步等操作来保证其线程安全;线程对立属于在多线程环境中无论是否采用同步操作都无法实现线程安全,应该极力避免。

     回顾了线程安全的相关知识后,我们将开始学习线程安全的实现方法。

 1、互斥同步

      互斥同步是我们实现线程安全最常见的一种方式。我们通常只说同步而不说互斥同步是因为互斥是实现同步的手段,同步是互斥的结果。在java中最基本的互斥同步操作就是synchronized 关键字,其次使用ReentrantLock也可以实现同步。

引申问题:synchronized 的实现原理

     synchronized 经过编译后会在同步块前后会形成 monitorenter 和monitorexit 两个字节码指令(这两个指令个人理解为监视器,在同步前开始监视,在同步完成后退出监视),这两个字节码都需要一个 引用类型的参数来指明要加锁和解锁的对象,这个从synchronized的使用上可以更加明白的看出来。示例:

// 同步操作使用中需要指定一个引用类型的参数vector
//Vector<Integer> vector = new Vector<Integer>();
synchronized (vector) {
	for (int j = 0; j < vector.size(); j++) {
		vector.remove(j);
	}
}

示例中:vector 是一个引用类型  作为参数传给synchronized 作为锁的对象。

在方法上使用synchronized 可不明确指明对象参数,如果没有明确指明,就根据synchronized 修饰的是实例方法还是类方法去取对应的对象实例或Class对象来作为锁对象

题外音:类方法(用static修饰的方法) ,实例方法(需要实例化对象后才能调用的方法)

有了锁对象后,在执行monitorenter指令时,首先要尝试获取对象的锁, 如果这个对象没有被加锁或当前线程已经获取了对象的锁,对锁的计数器进行+1,在执行monitorexit指令时,对锁的计数器进行-1,如果计数器为0则释放锁。如果获取锁失败就阻塞当前线程直到锁被其他线程释放为止。

特别注意:synchronized同步块对于同一线程来说是可以重入的,,不会出现把自己锁死的情况。 另外 一个是同步块在执行完成前会阻塞其他线程进入。

引申问题:为什么说synchronized是重量级的操作

       在进行同步操作时,时常被前辈告知,慎用synchronized关键字,因为它是重量级操作。为什么说它是重量级的呢?

       原因:java的线程是映射到操作系统的原生线程上的,线程在阻塞与唤醒的过程中涉及用户态与内核态的切换,状态的切换在某些时候比程序本身消耗的时间还长,因此说synchronized 是一个重量级的操作。我们常常将synchronized叫做同步,其实它的名称叫重量级锁。

大致讲完synchronized的实现原理及其使用时的一些注意事项后,再来讲另一种互斥同步的线程安全实现——ReentrantLock(重入锁)

ReentrantLock 是java 并发包(java.util.concurrent 简称J.U.C)的API。首先来看它的使用方式

ReentrantLock lock = new ReentrantLock();
try{
    lock.lock();
	//业务逻辑代码
}catch(Exception e){
	e.printStackTrace();
}finally {
	lock.unlock();
}

从上面的使用方式来看,与synchronized使用方式相差无几,不同的只在代码写法上。这是因为synchronized是原生语法上的互斥锁,ReentrantLock为API层面上的互斥锁,需要在try/finally语句来完成。

引申问题:synchronized与ReentrantLock的区别

synchronized是原生语法上的,基于JVM层面的互斥锁,属于非显式锁

ReentrantLock是API层面上的互斥锁,属于显式锁

synchronized与ReentrantLock主要的区别需要从ReentrantLock的3个主要特性上说明。

特性1:等待可中断

          synchronized在获取锁后会导致其他获取该锁的线程进入阻塞等待状态,在等待的过程中无法中断,也无法使用轮询的方式获取锁,而ReentrantLock在拥有了与synchronized相同并发性和内存语言的基础上增加了类似轮询,定时和可中断等待的特性来提高线程的执行效率。

特性2:可实现公平锁

        synchronized是不公平的锁,在锁释放时,所以的线程都可以通过竞争的方式来获取锁,这样会导致先到的线程可能一直获取不到锁(理论是这样,但JVM肯定会尽量不让这样的情况发生的),而ReentrantLock默认也是非公平锁,但通过设置参数可以让ReentrantLock实现公平锁,按照规则顺序的获取锁。

特性3:锁可以绑定多个条件

       synchronized 中 锁对象的wait() 和notify()或notifyAll()方法可以实现一个隐含的条件,如果要关联多个条件时就不得不额外添加一个锁。而ReetrantLock只需要多次调用 newCondition()方法即可。

性能方面:互斥同步主要的问题就是在线程的阻塞与唤醒过程中带来的性能问题,在jdk1.5之前 ,synchronized的使用会严重影响多线程的执行效率,但在jdk1.5之后,synchronized经过优化后,性能与ReentrantLock不相上下,已经不再是慎用的理由了。

2、非阻塞同步

         互斥同步在同步过程中会阻塞其他线程所以又称为阻塞同步。随着计算机硬件指令集的发展,对于同步有了其他的选择方案,可以通过冲突检测的并发策略进行同步,这种同步方式的核心思想是先对共享数据进行操作,然后检测操作是否会产生冲突,如果没有冲突则操作成功,如果有冲突则通过补偿措施(常见的补偿措施就是重试,直到成功为止),这种同步方式不会让线程挂起处于阻塞状态,因此被称为非阻塞同步。

引申问题:硬件指令集如何实现非阻塞同步与冲突检测

       硬件可以保证让多个操作行为只通过一个指令来完成,这类指令常用的有:

        测试并设置(TAS)

        获取并增加(FAI)

        交换(SWAP)

        比较并交换(CAS)

        加载链接/条件存储(LL/SC)

      其中最后两条是现在处理器才有的,我们通过说的非阻塞同步就是通过后两条来保证与实现的。

3、无同步方案

前两个方案都是通过同步的方式来保证线程安全,但线程安全与同步其实本来没有任何联系,同步只是保证共享数据在竞争使用权时正确性的一种手段。如果不涉及数据共享,也就不须要任何同步措施。那么什么样的代码不需要同步就能实现线程安全呢?

      3.1 可重入代码

       可重入代码也叫做纯代码。什么是可重入的代码或者如何验证代码是可重入代码。一个很简单的原则可以判断:如果一个方法的返回数据是可以预测的,只要输入了相同的数据 都能返回相同的结果,这样的代码就是可重入的。简单来说类似于工具类,只要输入的数据相同,结果必定相同。

     3.2 线程本地存储

       如果一个代码中使用的数据需要和其他代码共享,只要能保证这些共享数据在一个线程能执行就是线程安全的。

       常见的例子:

            web交互模型:一个请求对应一个服务器线程。

       如果一个数据要被多线程访问,可以使用Volatile 关键字来声明,如果一个数据要被某个线程独享,可以通过

       java.lang.ThreadLocal来实现线程本地存储功能。

 

以上是对于如何保证和实现线程安全的实现方法的总结。错误之处欢迎指正。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值