线程安全与锁优化

参考整理自《深入理解Java虚拟机》第13章

一 线程安全

1.1 什么是线程安全?

如果对于一个对象可以安全地被多个线程同时使用,那么它就是线程安全的。

 

1.2 Java语言中的线程安全

在这里讨论线程安全,就限定于多个线程之间存在共享数据访问这个前提。

将Java语言中各种操作共享的数据分为5类:

(1)不可变

不可变对象一定是线程安全的(没有发生this引用逃逸的情况下,不会被其他线程操作,线程私有)。

如果共享数据是一个基本数据类型,那么只要在定义时使用final关键字修饰它就可以保证它是不可变的。

如果共享数据是一个对象,那么就需要保证对象的行为不会对其状态产生任何影响即可。(如String类,调用它的substring()、replace()、concat()这些方法不会影响它原来的值)

保证对象的状态不可变最简单的方法就是把对象中带有状态的变量都声明为final。

(2)绝对线程安全

很多Java API中标注自己是线程安全的类,大多数都不是绝对的线程安全,在多线程情况下,如果不在方法调用端做额外的同步措施的话(如Synchronized),仍然是不安全的。

要达到绝对的线程安全,即“不管运行环境如何,调用者都不需要任何额外的同步措施”通常需要付出很大的甚至是不切实际的开销。

(3)相对线程安全

在Java语言中,大部分的线程安全类都属于这种类型。如Vector、HashTable

(4)线程兼容

指对象本身不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全使用。

(5)线程对立

指无论调用端是否采取了同步措施,都无法在多线程环境中并发使用的代码。

 

1.3 线程安全的实现方法

1.3.1 互斥同步(阻塞同步)

同步:指多个线程并发访问共享数据时,保证数共享数据在同一时刻只被一个线程(或者是一些,使用信号量的时候)使用。

互斥是实现同步的一种手段,互斥是因,同步是果。

(1)synchronized关键字

在Java中,最基本的互斥同步手段就是使用synchronized关键字。

原理:synchronize关键字经过编译以后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令,这两个字节码都需要一个reference类型的参数来指明要锁定和解锁的对象。

锁定的对象:如果synchronized明确指定了对象参数,那就是这个对象的reference,如果没有明确指定,根据synchronized修饰的是实例方法还是类方法,去取对应的对象实例或Class对象来作为锁对象。

过程:在执行monitorenter指令时,首先要尝试获取对象的锁。如果这个对象没有被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加1,相应的,在执行monitorexit指令时会将锁计数器减1,当计数器为0时,锁就被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到对象锁被另外一个线程释为止。

重量级锁:Java的线程是映射到操作系统的原生线程上的,如果要阻塞或唤醒一个线程,都需要操作系统来帮忙完成,这就需要从用户状态转换到核心态中,因此状态转换需要消耗很多的处理器时间。对于简单的同步块,状态转换消耗的时间有可能比用户代码执行的时间还要长,所以synchronized是Java语言中的一个重量级操作。所以在必要的情况下才使用这种操作,而虚拟机本身也会做一些优化,比如在通知操作系统阻塞线程之前加入一段自旋等待过程,避免频繁地切入到核心状态。

(2)ReentrantLock

还可以使用java.util.concurrent包中的重入锁ReentrantLock来实现同步(lock()和unlock()方法配合try/finally使用)。

区别:相比synchronized,ReentrantLock增加了一些高级功能,主要有以下3项:等待可中断、可实现公平锁(通过带布尔值的构造函数,默认情况下是非公平的)、以及锁可以绑定多个条件

PS : 公平锁非公平锁

公平锁:指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。

非公平锁:在锁被释放时,任何一个等待锁的线程都有机会获得锁。

synchronized中的锁是非公平的,ReentrantLock默认情况下也是非公平的,但可以实现公平锁。

 

1.3.2 非阻塞同步

互斥同步最主要的问题是进行线程阻塞和唤醒带来的性能问题。因此这种同步也叫做阻塞同步。

(1)基于冲突检测的乐观并发策略

互斥同步的问题: Synchronized互斥锁属于悲观锁,它有一个明显的缺点,它不管数据存不存在竞争都加锁,随着并发量增加,且如果锁的时间比较长,其性能开销将会变得很大。

解决的办法:基于冲突检测的乐观乐观并发策略。这种模式下,已经没有所谓的锁概念了,每条线程都直接先去执行操作,计算完成后检测是否与其他线程存在共享数据竞争,如果没有则让此操作成功,如果存在共享数据竞争则可能不断地重新执行操作和检测,直到成功为止,这种并发策略的而许多实现都不需要把线程挂起,因为这种同步操作称为非阻塞同步。

靠硬件保证:随着硬件指令集的发展,可以保证操作和冲突检测这两个操作具有原子性,这类指令有CAS(Compare and Swap )、LL/SC等。

CAS指令: 有3个操作数,分别是内存位置(V)、旧值(A)、新值(B)。如果A == V中的值,那么用B更新V的值。

实现:Java程序中可以使用CAS操作,由sun.misc.Unsafek类里面的compareAndSwapInt()和compareAndSwapLong()等几个方法包装提供。虚拟机在内部对这些方法做了特殊处理,即时编译出来的结果就是一条平台相关的CAS指令,没有方法调用的过程。

CAS的逻辑漏洞:如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍为A值,那我们就能说它的值没有被其线程改过了吗?可能存在一种情况,在这段期间它的值被改成了B,后来有被改回A,那么CAS操作就误认为它从来没改变过。这种漏洞称为CAS操作的“ABA”问题。大部分情况下ABA问题不会影响程序并发的正确性。

PS :  悲观锁乐观锁

悲观锁:拿数据时,总是认为数据会被其他线程修改,如果不做同步措施,总会出现问题,所以无论数据是否出现竞争,都要进行加锁,用户态和心态转换,维护锁计数器,检查是否有被阻塞的线程需要唤醒等操作。

乐观锁:每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在最后更新的时候会判断一下在此期间别人有没有去更新这个数据,如果有则更新失败,反复执行操作和检测。

 

1.3.3 无同步方案

如果一个方法本来就不涉及共享数据,自然就无须任何同步措施去保证正确性,有些代码天生就是线程安全的。

 

二 锁优化

2.1 自旋锁 和 自适应自旋

2.1 锁消除

2.2 锁粗化

2.3 轻量级锁

2.4 偏向锁

 

 

 

 

2 可重入锁

可重入锁又名递归锁,是指在同一个线程在外层方法获取

http://www.cnblogs.com/qifengshi/p/6831055.html

 

posted @ 2017-09-04 00:18  小猫慢慢爬  阅读(260)  评论(0编辑  收藏  举报