java并发(二)线程安全的实现方法

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qwkxq/article/details/57120966

一、互斥同步(synchronization 互斥是方式,同步是目的)

互斥同步的实现方式主要有3种:互斥量、信号量、同步区

在java中,最基本的互斥手段就是synchronize关键字,synchronized在编译后会在代码前后形成monitorenter和monitorexit2个字节码指令,而这2个指令都需要一个reference类型的参数来指明要锁定和解锁的对象。

如果明确指定了参数,那传入指令的参数就是该对象;
如果没有明确指定,则根据修饰在静态或非静态方法来确定是Class对象或对象实例。

根据java虚拟机要求,

当执行monitorenter指令时,首先获取对象的锁,如果对象没有被锁定或当前线程已拥有对象的锁,则锁的计数器加1,对应的在执行monitorexit指令时将锁的计数器减1,当计数器为0时,锁即被释放。
如果获取锁失败,则当前线程陷入阻塞,需要等待。直到其它线程释放当前线程持有的对象锁。

注意:

1.synchronized同步块对于同一个线程是可重入的,不会出现自己锁死自己的问题;
2.synchronized同步快在已进入线程执行完之前,会阻塞后面线程的进入,而java的多线程技术依赖于操作系统原生技术,如果要阻塞或者唤醒一个线程,必须操作系统帮忙,这就需要从用户态转换到核心态中,对于代码简单的同步块,可能状态转换耗费的时间比用户代码执行时间还要长。因此synchronized是一个重量级操作,只有在确实必要的情况下才使用它。当然虚拟机本身也做了一些优化,诸如:通知操作系统阻塞线程前,加入一段自旋式等待过程,避免频繁的转换到核心态中。

除了synchronized关键字外,还可以使用java.util.concurrent(J.U.C)包下的ReentrantLock(重入锁)来实现互斥锁,它通过lock()和unlock()加上try/catch来实现互斥锁。且它还提供了以下3种高级功能:
1.等待可中断
陷入等待的线程可以放弃等待,改为做其它事情,这对于某些执行时间较长的同步块很有益。
2.公平锁
和synchronized一样,ReentrantLock的锁默认也是不公平的,即锁被释放时,任何一个等待锁的线程都有机会获得锁。而公平锁则是指当多个线程等待同一个锁时,必须按照申请锁的顺序依次获得锁,这一点可以通过带boolean类型的构造函数来实现这一点。
3.锁可绑定多个条件
synchronized中,锁对象的wait(),notify(),notifyAll()方法可以实现一个隐含的条件,如果要和多于一个的条件关联时,就必须额外添加一个锁。而ReentrantLock则不需要这样做,只需要多次调用newCondition()即可。

ReentrantLock相对于synchronized实现互斥同步,性能是优于后者的(jdk1.5之后),尤其在线程越多的情况越明显。(这决定于synchronized并没有太多的优化,后续java会优先优化synchronized,所以建议在synchronized能够完成的情况下,尽量使用synchronized

二、非阻塞式同步(Non Blocking Synchronization)

互斥同步最大的问题就是进行线程阻塞和唤醒带来的性能问题,在处理问题的方式上,互斥同步是一种悲观的并发策略,无论共享数据是否真的会竞争,这种策略都认为必须进行加锁(实际上虚拟机会优化掉不必要的加锁)、用户核心态转换、维护锁技术、检查是否有阻塞线程等待被唤醒等操作。

随着硬件指令集的发展,我们有了另一个选择,基于冲突检测的乐观并发策略,即:如果没有共享数据的竞争,那操作就成功了;如果有共享数据的竞争,产生冲突,在采取其它补偿性措施(最常用的就是不断重试,直到成功为止),这种乐观的并发策略都不需要将线程挂起,因此这种策略称为非阻塞式同步。

非阻塞式同步要求操作和冲突检测这2个步骤是不可分割的整体即具备原子性,这一点如果在使用互斥同步来保证就失去意义了,所以只能通过硬件来完成,硬件能保证看似多条语句才能完成的行为只需一条处理器指令就可以完成,这类指令常用的有:
这里写图片描述
这里写图片描述

CAS操作在jdk1.5之后被支持,它由sun.misc.Unsafe类里的compareAndSwapInt(),compareAndSwapLong()等几个方法包提供,它们经过虚拟机特殊处理,使得编译出来的结果就是一条平台相关的处理器指令,没有方法调用的过程,可以认为是无条件内联进去了。

Unsafe类只有启动类加载器加载的Class才能访问,所以不通过反射,我们只能通过其它javaAPI简介访问,如J.U.C包下的整数原子类,其中的compareAndSet(),getAndIncrement(),incrementAndGet()等方法使用了Unsafe方法的CAS操作。

CAS操作虽然看起来不错,但是它远不能覆盖互斥同步的所有使用场景,而且存在一个逻辑漏洞(ABA漏洞),在一个变量v初次读取时A值,到准备赋值时依然是A值即认为v没有被其它线程修改是CAS指令的基本逻辑,但实际上很有可能在这个过程中,v先是A,然后被修改成了B,后来又被改会A。虽然为了解决这个问题,J.U.C包提供了一个带标记的原子引用类AtomicStampedReference通过变量值的版本保证CAS的正确性,但目前来看这个类比较鸡肋,解决ABA问题,还是使用互斥同步相比原子类更为高效。

三、无同步方案

1.可重入代码:一个简单的原则可以判断可重入代码,即:如果是一个方法,它的返回结果是可以预测的,只要输入相同的数据,都能返回相同的结果,那么就满足重入性要求,当然就是线程安全的。

可重入代码一定是线程安全的,线程安全的不一定是可重入代码

2.线程本地存储:如果一段代码中的变量必须与其它线程共享,那就要看共享数据的代码能否保证在同一个线程中执行,如果可以保证,那么就把共享数据的可见范围限制在一个线程中了,这样无须同步也能保证线程之间不会出现数据竞争的问题。多使用在消费队列的架构模式(如生产者-消费者模式),最为经典的就是web交互模式中的“一个请求对应一个服务器线程”的处理方式,在java中,提供java.lang.ThreadLocal类来实现线程本地存储。

猜你喜欢

转载自blog.csdn.net/qwkxq/article/details/57120966
今日推荐