线程安全与锁优化
当多个线程同时访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用放方运行热呢其他的协调操作,调用这个对象的行为都可以获得正确的结果没那就称这个对象是线程安全的;
线程安全
Java中的线程安全
Java语言中各种操作共享的数据分为以下五类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立;
不可变
- 不可变的对象一定是线程安全的,无论是这个对象的方法实现,还是方法的调用者;
- 保证对象的行为不影响自己的状态的途径有很多种:最简单的一种就是把对象里面带有状态的变量声明成final
- Java API类库中符合不可变要求的类型,String,Number的部分子类,Long和Double等数值的包装类型、BigInteger、BigDecimal等大数据类型; Number中的子类AtomicInteger和AtomicLong则是可变的;
绝对线程安全
定义非常严格:不管运行时环境如何,调用者都不需要任何额外的同步措施,可能需要付出非常高昂的、甚至不切实际的代价;
相对线程安全
- 相对现场安全就是我们通常意义上的线程安全,他需要保证这个对象单次的操作是线程安全的,我们在调用的时候不需要进行额外的保障措施;但是对于一些特点顺序的连续调用,**还是需要一些同步手段(例下案例)**的;
- 例如(Vector、HashTable、Collections.synchronizedCollections()包装的集合)
Vector<String> vector = new Vector<>();
Thread t = new Thread(() -> {
synchronized (vector){
for (int i=0;i<vector.size();i++){
vector.remove(i);
}
}
});
线程兼容
- 是指对象本身不是线程安全的,但可以通过在调用端正确地使用同步手段保证对象在并发环境中安全的使用;
- 如ArrayList、HashMap
线程对立
- 无论调用端是否采取了同步措施,都无法在多线程环境中并发使用代码
- 例如:Thread.suspend()【中断线程】、resumer()【恢复线程】等
线程安全的实现方法
虚拟机提供的同步和锁机制起到了至关重要的作用
互斥同步(也被称为阻塞同步,悲观的并发策略)
- 互斥是实现同步的一种手段,临界区(Critical Section)、互斥量(Mutex)、**信号量(Semaphore)**都是常见的互斥实现方法;
- syncronized:字节码对应(monitorenter、monitorexit)
- 作用于对象参数,那么锁住的是这个对象的引用;
- 作用于实例方法,那么锁的是对象实例;
- 作用于Class方法,那么锁的对象是Class对象;
- 被synchronized修饰的同步块对同一条线程来说是可重入的,
- 被synchronized修饰的同步块在释放锁之前,会无条件阻塞后面其他线程的进入;
- 持有锁是一个重量级的操作,阻塞或唤醒一条线程,则需要操作系统来帮忙完成,这就陷入用户态到核心态的转换中;-
- syncronized是Java语言的一个重量级操作;concurrent.locks.Lock接口是一个全新的互斥手段,基于Lock接口(非块结构实现互斥),改为在类库的层面上实现互斥;
- 重入锁(ReentrantLock) 是Lock接口的最常见的一种实现
- ReentrantLock相对于syncronized增加了一些高级功能:
- 等待可中断:当前持有锁的线程长期不释放锁的时候,正在等待的线程可以放弃等待,改为处理其他事情;
- 公平锁: 默认与syncronized一样非公平,可通过带布尔值的构造参数要求使用公平锁,(公平锁会导致性能急剧下降)
- 锁绑定多个条件:通过newCondition()方法管理多个条件;
- 推荐 syncronized和ReentrantLock都可满足的情况下,优先使用syncronized:
- syncronized是在Java语法方面的同步;
- Lock应该在finally块中释放,同步的代码块抛出异,则有可能永远不会释放锁,syncronized由Java虚拟机确即时保出现异常,锁会释放;
- Java虚拟机更容易针对syncronized来进行优化
非阻塞同步(基于冲突检测的乐观并发策略)
不管风险,先进行操作,如果没有其他线程争用共享数据,操作就直接成功;如果共享的数据的确被争用,产生了冲突,那么再进行其他的补偿措施,最常见的补偿措施是不断的重试,直到出现没有竞争的共享数据为止,这种乐观并发策略的实现不需要把线程阻塞挂起,这种策略被称为非阻塞同步;
- 需要“硬件指令集的发展”?因为我们必须要求操作和冲突检测两个步骤具备原子性;
- 测试并设置(Test-and-Set)
- 获取并增加(Fetch-and-Increment)
- 交换(Swap)
- 比较并交换(Compare-and-Swap 简称CAS)
- 加载链接/条件存储(Load-Linked / Store-Conditional 下文简称LL/SC)
- CAS指令需要三个操作数,分别是内存位置(Java中简单理解为变量的内存地址,用V表示),旧的预期值(用A表示)和准备设置的新值(用B表示)。CAS指令执行时,当且仅当V符合A时,处理器才会用B更新V的值,否则它就不会执行更新。但是,不管是否更新了V值,都会返回V的旧值,上述操作就是一个原子操作,执行期间不会被其他线程中断。
- sun.misc.Unsafe类的compareAndSwapInt() 和compareAndSwapLong() 等几个方法包装提供。HotSpot虚拟机做了一些特殊处理,即时编译的结果是一条平台相关的处理器CAS指令;
- JDK9,Java类库开放了面向用户程序使用的CAS操作;
无同步方案
- 同步与线程安全两者没有必然的联系。同步只是保障存在共享数据争用时正确性的手段,如果一个方法本来就不涉及共享数据,那么它自然就不需要任何同步措施去保证其正确性;
- 可重入代码(纯代码)这是线程安全代码的一个真子集,
- 可重入代码有一些共同特征:(例如)不依赖全局变量、存储在堆上的数据和公用的系统资源、用到的状态量都是由参数中传入,不可调用非可重入的方法等;
- 消费队列的架构模式,都会将消费过程限制在一个线程中消费完。
- 一个请求对应一个服务器线程(线程本地存储),Java.lang.ThreadLocal类实现线程本地存储,每一个线程的Thread对象都有一个ThreadLocalMap对象,这个对象存储了一组以ThreadLocal.threadLocalHashCode为键,以本地线程变量为值的K-V值对,每一个ThreadLocla对象都包含了一个独一无二的threadLocalHashCode值,使用这个值可以找到K-V中对应的本地线程变量;
锁优化
自旋锁与自适应自旋锁
- 互斥同步对性能最大的影响是阻塞的实现,挂起线程和恢复线程的操作都要转入内核态中完成;
- 物理机器有一个以上的处理器或处理器核心,能让两个或以上的线程并行执行,让后面执行请求锁的线程"稍等一会”,但不放弃执行器的执行时间,看看持有锁的线程是否很快就会释放锁,为了让线程等待,我们只须让线程执行一个忙循环(自旋),这就是自旋
- 锁被占用的时间很短,效果就会很好,自旋默认十次,可通过-XX:PreBlockSpin自行更改;
- JDK6之后对自旋锁的优化,自适应意味着自旋的时间不固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定的;
- 如果在同一个锁对象上,自旋等待刚刚成功获取过锁,并且持有锁的线程正在运行中,虚拟机会认为这次自旋也可能再次成功,进而允许自旋等待持续相对更长的时间,比如持续进行100次忙循环;
- 如果对于某个锁,自旋很少成功获得过锁,那么之后要获取这个锁时,可能直接省略掉自旋过程,以避免浪费处理器资源;
锁消除
- 虚拟机即时编译器在运行时检测到某段程序需要同步的代码根本不可能存在共享数据竞争而实施的一种对锁进行消除的优化策略;
锁粗化
- 如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作出现在循环体之中,即使没有线程竞争,频繁地互斥同步操作也会导致不必要的性能损耗;
轻量级锁
提升性能的依据是:“对于绝大部分的锁,在整个同步期间都是不存在竞争的”
轻量级锁:在无竞争的情况下,使用CAS操作去消除同步使用的互斥量;
与偏向锁一样,首先先要了解HotSpot虚拟机对象的内存布局(尤其对象头)
虚拟机对象头分为两部分:
第一部分用于存储对象自身的运行时数据,如(哈希码、GC分代年龄),这部分数据的长度在32位和64位的虚拟机分别占有32个bit或64个bit;
第二部分用于存储指向方法区的对象类型数据的指针,如果是数组对象,还会有额外的部分用来存储数组长度;
- 轻量级锁(加锁)的工作过程:
- 在代码即将进入同步块的时候,如果此同步对象没有被锁定(锁标志位“01”状态),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间 ,用于存储锁对象目前的Mark Word的拷贝(官方加了Displaced前缀);
- 虚拟机将使用CAS操作尝试把对象的Mark Word更新为指向Lock Record的指针。如果这个更新动作成功了,代表线程拥有了这个对象的锁,并且对象Mark Word的锁标志位将转换“00”,表示此对象处于轻量级锁状态;
- 如果更新操作失败,则说明至少存在一个线程与当前线程竞争获取该对象的锁;虚拟机首先检测对象的Mark Word是否指向当前线程的栈帧,如果是,则说明当前线程已拥有这个对象的锁,直接进入同步块继续执行,否则说明是其他线程持有这个对象的锁,这时轻量级锁不在有效,必须膨胀为重量级锁,锁标志的状态值为“10”,此时Mark Word中存储的是重量级锁的指针,后面等待的线程也必须进入阻塞状态;
- 轻量级锁(解锁):
- 如果对象的Mark Word仍然指向当前的锁记录,那么就用CAS操作把对象当前的Mark Word和线程中复制的Displaced Mark Word替换回来。如果替换成功,同步过程完成,如果替换失败,则说明其他线程尝试过获取该锁,就要在释放锁的同时,恢复被挂起的线程;
偏向锁
JDK6引入的锁优化措施,目的是消除数据在无竞争情况下的同步原语,进一步提高程序的性能;
偏向锁:在无竞争的情况下,把整个同步都消除掉,连CAS操作都不做了;
意思是:这个锁会偏向于第一个获取它的线程,如果接下来的执行过程中,该锁一直没有被其他的线程获取,则持有偏向所得线程将永远不需要获得同步;
偏向锁可以提高带有同步但无竞争的程序性能,如果程序中大多数的锁总是被多个不同的线程访问,那偏向模式便是多余的;
可使用参数-XX:-UserBiasedLocking来禁止偏向锁优化反而可以提升性能;
偏向锁、轻量级锁的状态转化及对象Mark Word的关系图:
参考《深入理解Java虚拟机》第三版