《深入理解Java虚拟机》读书笔记10

第13章 线程安全与锁优化
线程安全的定义
多个线程 访问一个对象时,如果不用考虑这些 线程在运行时环境下的调度和交替执行 ,也不需要进行 额外的同步 ,或者在 调用方法进行任何其他的协调操作,调用这个对象的行为都可以获取正确的结果 ,那这个对象是线程安全的。

Java语言中操作共享的数据分为5类:
  • 不可变(在Java语言里,不可变的对象一定是线程安全的,只要一个不可变的对象被正确构建出来,那其外部的可见状态永远也不会改变,永远也不会在多个线程中处于不一致的状态。例如:final关键字修饰、java.lang.String类的对象、枚举类型等
  • 绝对线程安全(完全符合线程安全的定义)
  • 相对线程安全(保证对这个对象的单独操作是线程安全的,大部分线程安全类型都属于该类)
  • 线程兼容(对象本身并不是线程安全的,但可以通过调用端正确地使用同步手段来保证对象在并发环境中的安全使用)
  • 线程对立(不管调用端是否采取了同步措施,都无法在多线程环境中并发使用的代码)

线程安全的实现方法:
虚拟机提供了同步与锁机制。
  • 阻塞同步(互斥同步)
  • 非阻塞同步

同步
1、阻塞同步(互斥同步) (转载内容)
互斥 是实现同步的一种手段, 临界区 互斥量 信号量 都是主要的互斥实现方式。Java中最基本的同步手段就是 synchronized 关键字,其编译后会在同步块的前后分别形成 monitorenter monitorexit 两个字节码指令。这两个字节码都需要一个Reference类型的参数指明要锁定和解锁的对象。如果Java程序中的synchronized明确指定了对象参数,那么这个对象就是Reference;如果没有明确指定,那就根据synchronized修饰的是实例方法还是类方法,去获取对应的对象实例或Class对象作为锁对象。 

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

除了synchronized之外,还可以使用java.util.concurrent包中的 重入锁 ReentrantLock )来实现同步。ReentrantLock比synchronized 增加了高级功能 :等待可中断、可实现公平锁、锁可以绑定多个条件。
  • 等待可中断:当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,对处理执行时间非常长的同步块很有用。
  • 公平锁:多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。synchronized中的锁是非公平的。
  • 锁可以绑定多个条件:一个ReentrantLock对象可以同时绑定多个Condition对象(多次调用newCondition()方法)

2、非阻塞同步 (转载内容)
互斥同步 最大的问题,就是进行 线程阻塞和唤醒所带来的性能问题 ,是一种 悲观的并发策略 。总是认为只要不去做正确的同步措施(加锁),那就肯定会出问题,无论共享数据是否真的会出现竞争,它都要进行加锁、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程需要被唤醒等操作。

随着硬件指令集的发展,我们可以使用 基于冲突检测的乐观并发策略 先进行操作 ,如果没有其他线程征用数据,那操作就成功了;如果共享数据有征用,产生了冲突,那就再进行其他的补偿措施。这种乐观的并发策略的许多实现 不需要线程挂起 ,所以被称为 非阻塞同步

3、无同步方案
前言:要保证线程安全并不一定要同步,两者没有因果关系。同步只是保障共享数据争用时的正确手段,若一个方法本来就不涉及共享数据,自然无需同步措施保证正确性,因此有一些代码天生是线程安全的。如:
可重入代码 (Reentrant Code)——也叫纯代码(Pure Code),所有的可重入代码是线程安全的,反过来不一定。
线程本地存储 ——如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码能否保证在同一个线程执行?若能,就可以把共享数据的可见范围限制在同一个线程池内,这样,无需同步也能保证线程间不出现数据争用问题。

锁优化(转载) http://cmsblogs.com/?p=2071
锁优化是在JDK的哪个版本?
JDK1.6的一个重要主题,就是 高效并发 。HotSpot虚拟机开发团队在这个版本上,实现了各种锁优化:
  • 适应性自旋
  • 锁消除
  • 锁粗化
  • 轻量级锁
  • 偏向锁

为什么要提出 自旋锁?
互斥同步对性能最大的影响是 阻塞的实现 挂起线程 恢复线程 的操作都需要 转入内核态 中完成,这些操作给系统的并发性带来很大压力。 同时很多应用共享数据的锁定状态, 只会 持续很短的一段时间 ,为了这段时间去挂起和恢复线程并不值得。先不挂起线程,等一会儿。

自旋锁的原理?
如果物理机器有 一个以上的处理器 ,能让两个或以上的线程同时并行执行,让后面请求锁的线程稍等一会,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放。为了让线程等待,我们只需让线程执行一个忙循环(自旋)。

自旋的缺点?
自旋等待本身虽然避免了线程切换的开销,但它要占用处理器时间。 所以如果锁被占用的时间很短,自旋等待的效果就非常好;如果时间很长,那么自旋的线程只会白白消耗处理器的资源。所以自旋等待的时间要有一定的限度,如果自旋超过了限定的次数仍然没有成功获得锁,那就应该使用传统的方式挂起线程了。

什么是自适应自旋?
自旋的时间不固定 了,而是由 前一次在同一个锁上的自旋时间 锁的拥有者的状态 来决定。
  • 如果一个锁对象,自旋等待刚刚成功获得锁,并且持有锁的线程正在运行,那么虚拟机认为这次自旋仍然可能成功,进而运行自旋等待更长的时间。
  • 如果对于某个锁,自旋很少成功,那在以后要获取这个锁,可能省略掉自旋过程,以免浪费处理器资源。
有了自适应自旋,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测就会越来越准确,虚拟机也会越来越聪明。

锁消除
锁削除是指虚拟机 即时编译器在运行时 对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行削除 (主要判定依据来源于 逃逸分析 的数据支持,如果判断到一段代码中,在堆上的所有数据都不会逃逸出去被其他线程访问到,那就可以把它们当作栈上数据对待,认为它们是线程私有的,同步加锁自然就无须进行)。

程序员怎么会在明知道不存在数据竞争的情况下使用同步呢?很多不是程序员自己加入的。

锁粗化
原则上,同步块的作用范围要尽量小。但是如果一系列的连续操作都对 同一个对象 反复加锁和解锁,甚至加锁操作在循环体内,频繁地进行互斥同步操作也会导致不必要的性能损耗。 
如果虚拟机探测到有这样一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(膨胀)到整个操作序列的外部(由多次加锁变成只加锁一次)。
锁粗化就是 增大锁的作用域

轻量级锁
轻量级锁并 不是用来代替 重量级锁(传统锁机制,如互斥等)的,目的是在没有多线程竞争的前提下, 减少传统的重量级锁 使用操作系统互斥量产生的 性能消耗。

偏向锁
消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能 。即在无竞争的情况下,把整个同步都消除掉。 这个锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要同步。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值