第四章-锁的优化及注意事项

4.1 有助于提高锁性能的几点建议 162

4.1.1 减少锁持有时间 162

  • 系统持有锁时间越长锁竞争程度就越激烈,只对需要同步的代码块加锁,可以减小锁持有时间进而提高锁性能。
  • 减少锁的持有时间有助于降低锁冲突的可能性,进而提高锁的并发能力。

4.1.2 减小锁粒度 163

减小锁粒度就是指缩小锁定对象的范围,从而减小锁冲突的可能性,进而提高并发能力。

例如:如ConcurrentHashMap中并不是对整个HashMap进行加锁,而是对其分段,每段分别加锁。(JDK1.8已经取消)使用这个方法的一个缺点是:当系统需要取得全局锁时,其消耗的资源会比较多,需要取得所有子段的锁。

4.1.3 用读写分离锁来替换独占锁 165

读操作和读操作可以并行,读写操作才需要加锁。使用读写分离锁(允许多线程同时读)可以减少操作之间相互等待,可以有效的提高性能。

4.1.4 锁分离 165

将读写锁的思想进一步延伸,就是锁分离。例如:LinkedBlockingQueue中,take() 和put() 函数分别实现从队列中取数据和往队列中增加数据的功能。虽然两个函数都对队列做了修改,但LinkedBlockingQueue是基于链表的,两个操作分别位于队列的首端和尾端,两者并不冲突。因此,在JDK的实现中,用两把不同的锁分离了这两个函数,是两者在真正意义上实现了并发。

4.1.5 锁粗化 168

如果一个锁不停的被进行请求、同步和释放,其本身也会消耗系统宝贵的资源,反而不利于性能优化。

虚拟机在遇到一连串连续的对同一锁不断进行请求和释放的操作时,便会把所有的操作整合成对锁的一次请求,从而减少锁的请求同步次数,这个操作叫锁粗化。例如在for循环中,频繁申请锁和释放锁,应该改为直接在for循环之外加一次锁。

4.2 Java虚拟机对锁优化所做的努力 169

4.2.1 锁偏向 169

  • 如果一个线程获得了锁,那么锁就进入了偏向模式。当这个线程再次请求锁时,无需再做任何同步操作。在锁竞争比较激烈的场合,效果不佳。
  • 使用Java虚拟机参数-XX:USerBiasedLocking可以开启偏向锁。

4.2.2 轻量级锁 169

如果偏向锁失败,虚拟机并不会立即挂起线程,而是使用一种称为轻量级锁的优化手段。

轻量级锁只是简单地将对象头部作为指针指向持有锁的线程堆栈的内部,来判断一个线程是否持有对象锁。

如果线程获得轻量级锁成功,则可以顺利进入临界区。如果轻量级锁加锁失败,则表示其他线程抢先争夺到了锁,那么当前线程的锁请求就膨胀为重量级锁。

4.2.3 自旋锁 170

锁膨胀后,为了避免线程在操作系统层面挂起,虚拟机会使用自旋锁。当前线程无法获得锁,此时不把线程挂起,而是进行几个空循环(这也是自旋的含义)再尝试获得锁。

4.2.4 锁消除 170

锁消除是一种更彻底的锁优化。Java虚拟机在JIT编译时,通过对运行上下文的扫描去除掉不可能存在共享资源竞争的锁。如:程序员在一个不存在并发竞争的场合使用了Vector。

锁消除设计逃逸分析技术。

逃逸分析技术:逃逸分析就是观察某一个变量是否会逃出某一个作用域。

4.3 人手一支笔:ThreadLocal 171

4.3.1 ThreadLocal的简单使用 171

ThreadLocal是线程的局部变量,只有当前线程才可以访问,因此是线程安全的。

ThreadLocal只是起到容器的作用,为每一个线程分配不同对象需要应用层面保证。即在应用层调用ThreadLocal的set方法。

4.3.2 ThreadLocal的实现原理 173

public void set(T value) {
Thread t = Thread. currentThread() ;
ThreadLocalMap map = getMap(t) ;
if (map != null)
map. set (this, value) ;
else
createMap(t, value) ;
}

设置到ThreadLocal 中的数据,也正是写入了threadLocals 的这个Map。其中,key为ThreadLocal当前对象,value 就是我们需要的值。

public T get() {
Thread t = Thread . currentThread() ;
ThreadLocalMap map = getMap(t) ;
if
(map != null) {
ThreadLocalMap. Entry e = map. getEntry(this) ;
if (e!= null)
return (T)e. value;
   }
return setInitialValue() ;
}

get()方法先取得当前线程的ThreadLocalMap对象,然后通过将自己作为key取得内部的实际数据。

注意:只要线程不退出,对象的引用将一直存在,这在线程池中容易产生内存泄漏的可能。因此建议使用:ThreadLocal.remove()方法将这个变量移除。

ThreadLocal的回收机制,如下图:

ThreadL ocalMap的实现使用了弱引用。弱引用是比强引用弱得多的引用。Java虛拟机
在垃圾回收时,如果发现弱引用,就会立即回收。ThreadLocalMap 内部由一系列Entry 构
成,每一个Entry都是WeakReference<ThreadLocal>。

当ThreadLocal的外部强引用被回收时,ThreadLocalMap 中的key就会变成null。当系统进行
ThreadLocalMap清理时,就会将这些垃圾数据回收。

4.3.3 对性能有何帮助 179

如果共享对象对于竞争的处理容易引起性能损失,我们应该考虑使用ThreadLocal为每个线程分配单独的对象。一个典型的案例就是在多线程下产生随机数。

4.4 无锁 182

锁的策略使用一种叫作比较交换(CAS,Compare And Swap)的技术来鉴别线程冲突,一旦检测到冲突产生,就重试当前操作直到没有冲突为止。

4.4.1 与众不同的并发策略:比较交换 182

  • 包含三个参数CAS(V,E,N),V表示要被修改的变量,E表示期望值,N表示要设置的值。
  • 如果V的值和期望值相等则将V的值设置为N,如果不相等表示V的值已经被其他线程修改了,当前线程不做任何操作。
  • 当有多个线程访问的时候只有一个线程成功,其他线程会被告知失败,其他线程可以选择再次执行或者是放弃。

4.4.2 无锁的线程安全整数:AtomicInteger 183

JDK并发包中有一个atomic包,里面实现了一些直接使用CAS操作的线程安全的类型。

其中,最常用的一个类就是AtomicInteger,可以把它看做一个整数。与Integer不同,它是可变的,并且是线程安全的。对其进行的任何操作都是用CAS指令进行的。

和AtomicInteger类似的类还有:AtomicLong用来代表Long型数据;AtomicBoolean表示Boolean型数据;AtomicReference表示对象引用。

4.4.3 Java中的指针:Unsafe类 185

指针是不安全的,因此Java中去除了指针,但是通过Unsafe类封装了一些不安全操作。我们无法使用这个类。

4.4.4 无锁的对象引用:AtomicReference 187

AtomicReference和AtomicInteger比较类似,但是AtomicReference的作用是对”对象”进行原子操作,但是会丢失修改过程的状态信息(如果当你获得对象当前数据后,在准备修改为新值前,对象的值被其他线程连续修改了两次,而经过这两次修改后,对象的值又恢复为旧值。这样,当前线程就无法正确判断这个对象究竟是否被修改过)

JDK使用下面的AtomicStampedReference解决这个问题。

4.4.5 带有时间戳的对象引用:AtomicStampedReference 190

AtomicStampedReference内部不仅维护了对象值,还维护了一个时间戳(或者叫版本戳,实际上它可以使任何一个整数来表达状态值)。只要时间戳发生变化,就能防止不恰当的写入。AtomicStampedReference对应的数值被修改时,除了更新数据本身外,还必须要更新时间戳。当AtomicStampedReference设置对象值时,对象值及时间戳都必须满足期望值,写入才会成功。因此,即使对象值被反复读写,写回原值,只要时间戳发生变化,就能防止不恰当的写入。

4.4.6 数组也能无锁:AtomicIntegerArray 193

除提供基本数据类型以外,JDK还为我们准备了数组等复合结构。当前可用的原子数组有: AtomicIntegerArray、 AtomicLong Array和AtomicReferenceArray,分别表示整数数组、long型数组和普通的对象数组。
以AtomicIntegerArray为例:AtomicIntegerArray本质上是对int[]类型的封装,使用Unsafe类通过CAS的方式控制int[]在多线程下的安全性。

4.4.7 让普通变量也享受原子操作:AtomicIntegerFieldUpdater 194

AtomicIntegerFiedUpdater它可以让你在不改动原有代码的基础上,让普通的变量也享受CAS操作带来的线程安全性。根据数据类型不同,Updater 有三种,分别是AtomicIntegerFieldUpdater、AtomicLongFieldUpdater和AtomicReferenceFieldUpdater。顾名思义,它们分别可以对int、long 和普通对象进行CAS修改。

例:

public static class Candidate{
int id;
volatile int score;    
}
public final static AtomicIntegerFieldUpdater<Candidate> socoreUpdater =
          AtomicIntegerFieldUpdater.newUpdater(Candidate.class, "score");

4.4.8 挑战无锁算法:无锁的Vector实现 196

[–]

4.4.9 让线程之间互相帮助:细看SynchronousQueue的实现 201

[–]

4.5 有关死锁的问题 205

死锁:通俗地说,死锁就是两个或者多个线程相互占用对方需要的资源,而都不进行释放,导致彼此之间相互等待对方释放资源,产生了无限制等待的现象。

举例:哲学家就餐问题

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值