赞
踩
本篇文章是基于《深入理解Java虚拟机》一书的读书笔记,针对线程安全以及同步锁的相关知识做了介绍。上一篇文章Java 内存模型与线程关注的是虚拟机如何实现并发以及并发控制,本篇文章的关注点是高效并发。文章结构如下所示:
并发能够更加充分地利用计算机资源,同时处理多个任务。但是并发首先我们需要确保的应当是正确性,其次才是实现高效的性能,并发的正确性所涉及到的就是线程安全:
定义:当多个线程访问一个对象时,如果不考虑线程在运行时环境下的调度和交替执行,也不需要额外的同步,或者在调用方进行任何其他操作时,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的。
线程安全的代码都必须具备的特征:代码本身封装了所有必要的正确性保障手段(如互斥同步),令调用者无需关系多线程问题,更无需自己采取任何措施来保证多线程的正确调用。
分类:按照“安全程度”由强至弱可分为以下 5 类:
Ⅰ. 不可变:一定是线程安全的,外部的可见状态永远也不会改变,永远也不会看到其在多个线程之中处于不一致的状态。
final
关键字即可保证它是不可变的。final
。Ⅱ. 绝对线程安全:满足上述定义的线程安全级别,即要达到“无论运行时环境如何,调用者都不需要任何额外的同步措施”的效果。但绝对线程安全的要求很严苛,即使是诸如 Java内置的 Vectors
类也无法达到这种绝对的安全!
Ⅲ. 相对线程安全:通常意义上的线程安全,需要保证对对象单独的操作是线程安全的,在调用时无需做额外的保障措施,但对于一些特定顺序的连续调用,可能仍然需要额外的同步手段保障调用的正确性。Java中大部分线程安全类属于这个类型,例如 Vectors
、HashTable
等。4
Ⅳ. 线程兼容:本身不是线程安全的对象通过使用正确的同步手段达到线程安全的效果。我们平常使用的绝大多数类是属于线程兼容的,例如 ArrayList
、HashMap
等。
Ⅴ. 线程对立:无论是否采用同步措施,都无法在多线程环境中并发使用的代码。Java语言天生具备多线程特性,线程对立这种排斥多线程的代码很少出现。Thread
类中的 suspend()
和 resume()
方法就是典型的线程对立的例子,但是这两个方法已经被废弃了。
suspend() 和 resume() 分别用于暂停和恢复线程,当两个线程同时持有一个线程对象时,如果一个尝试去暂停线程,另一个尝试去恢复线程,在并发的情况下,无论调用时是否进行了同步,目标线程都存在死锁的风险。
线程安全的实现可分为代码编程实现以及虚拟机的内部实现,本篇文章侧重点是虚拟机的实现,下面是虚拟机实现线程安全的运作过程:
同步:指在多个线程并发访问共享数据时,保证共享数据在同一时刻只被一个线程使用。
互斥:是实现同步的一种手段,临界区、互斥量和信号量都是主要的互斥实现方式。
互斥是因,同步是果;互斥是方法,同步是目的。
悲观策略:互斥同步也成为阻塞同步,属于悲观的并发策略,认为只要不去做正确的同步措施,看到会出现问题,无论共享数据是否真的存在竞争,它都要进行加锁。
Java中的实现手段:
synchronized
关键字:
synchronized
经过编译之后,会在同步块前后形成 monitorenter 和 monitorexit 字节码指令。如果 synchronized
明确指定了锁定对象,那么锁定的就是该对象;否则就需要根据修饰的是实例方法还是类方法来获取对应的对象实例或 Class对象作为锁对象。synchronized
关键字是一种可重入锁,不会出现自己把自己锁死的情况。同步块在已进入的线程执行完之前,会阻塞后面的线程进入。并且由于 Java的线程是映射到操作系统的元素线程之上,因此唤醒和阻塞线程的操作都需要从用户态切换到内核态中,这种切换状态需要耗费很多时间。因此 synchronized
在 Java中属于一个重量级操作。ReentrantLock
类:
ReentrantLock
同样为一个可重入锁,通过提供 lock()/unlock()
方法来实现同步的操作,它和 synchronized
有以下区别:
synchronized
属于非公平锁,即后阻塞的线程也能先获得锁,有可能造成线程饥饿现象。ReentrantLock
默认也是非公平锁,但可以通过构造方法传递参数改为公平锁。一般而言非公平锁的效率会比公平锁的效率快。synchronized
关键字时,如果要和多于一个的条件关联的时候,就不得不额外地添加锁。而 ReentrantLock
可以同时绑定多个 Condition
对象,只需要多次调用 newCondition()
方法即可。ReentrantLock
和 synchronized
的选择:提倡使用 synchronized
关键字,虽然它属于重量级锁,但是 Java 已经对其做了相当多的优化用于提升性能。ThreadLocal
类实现本地存储的功能,它的内部是一个 Map,通过以 ThreadLocal.threadLocalHashCode
为键,本地线程变量为值的 K-V 值对进行本地变量的存储,每个线程都有独一无二的 ThreadLocal.threadLocalHashCode
值,使用这个值就可以找回对应的本地线程变量。Android 异步消息处理机制中的 Looper
对象就是通过 ThreadLocal
的方式存储的。锁优化是为了实现高效并发,在线程之间更高效地共享数据以及解决竞争问题,从而提高程序的执行效率,以下是 5 种锁优化技术:
Ⅰ. 自适应自旋(Adaptive Spinning):
Ⅱ. 锁消除(Lock Elimination):
Ⅲ. 锁粗化(Lock Coarsening):
StringBuilder
类的 append()
方法进行字符串的拼接时,虚拟机探测到有这样一串零碎的操作都是对同一个对象加锁,将会把加锁同步的范围粗化到整个操作序列的外部,即将锁扩展到第一个 append()
之前直至最后一个 append()
之后,这样只需加锁一次即可。Ⅳ. 轻量级锁(Lightweight Locking):
存储内容 | 标志 | 状态 |
---|---|---|
对象哈希码、对象分代年龄 | 01 | 未锁定 |
指向锁记录的指针 | 00 | 轻量级锁定 |
指向重量级锁的指针 | 10 | 膨胀(重量级锁定) |
空,不需要记录信息 | 11 | GC标记 |
偏向线程ID、偏向时间戳、对象分代年龄 | 01 | 可偏向 |
Ⅴ. 偏向锁(Biased Locking):
本篇文章参考自:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。