Java与线程
线程的实现
Java语言提供了在不同硬件和OS下对线程操作的统一处理,Thread类中的关键方法都是Native的,即方法没有使用或无法使用平台无关的手段来实现。即平台相关,因此是线程的实现,而不是Java线程的实现。
内核线程实现
内核线程KLT:直接由操作系统内核支持的线程,内核通过操纵调度器对线程进行调度,并负责将线程的任务映射到各个处理器上。
程序一般不会直接使用内核线程,而是使用内核线程的一种高级接口:轻量级进程LWP,即线程。每个轻量级进程都由一个内核线程支持,这种轻量级进程与内核线程的1:1关系称为一对一的线程模型。
- 每个轻量级进程都成为一个独立的调度单元,即使由一个轻量级进程在系统调用中阻塞了,也不会影响整个进程继续工作。
- 基于内核线程实现,所以各自操作都需要系统调用,需要在用户态与内核态间相互切换,代价高。
- 每个轻量级线程都有一个内核线程支持,因此需要消耗一定内核资源,如栈空间,因此一个系统可支持的线程数量有限。
使用用户线程实现
广义:一个线程只要不是内核线程,就可以认为是用户线程。
狭义:完全建立在用户空间的线程库,内核不能感知线程的存在。
- 不需要系统内核支援,操作快速,低消耗,可以支持规模更大的并发
- 没有系统内核支援,所有线程操作都需要用户自己考虑。
- 操作系统只把CPU分配给进程,阻塞处理、映射等问题解决非常困难。
这种进程与用户线程间1:N的关系称为1对多的线程模型。
使用用户线程加轻量级进程实现
Java线程的实现
依据平台而定,windows与linux是一对一的线程模型实现,一条Java线程就映射到一条轻量级进程中。
Java线程调度
协同式线程调度
线程的执行时间由线程本身控制,线程在自己的工作执行完后,主动通知系统切换到另一个线程,没有同步问题。
线程的执行时间不可控制,可能出现线程永久阻塞,使得整个系统不稳定。
抢占式线程调度
线程由系统分配执行时间,线程的切换不由线程本身决定(yield让出执行时间)。
设置线程优先级可以建议系统给某些线程多分配时间,但是并不可靠,因为Java的线程是通过映射到系统的原生线程实现的,所有线程的调度还是取决于操作系统。
锁优化
自旋锁与自适应自旋
通常一些情况共享数据的锁定只持续很短时间,为此进行挂起与恢复并不值得。
让后面请求锁的线程稍等一会,但不放弃处理器执行时间,看持有锁的线程是否会释放锁。使线程执行一个忙循环(自旋,默认10次),JDK1.6当中默认开启。
自适应自旋:由前一次在同一个锁上的自选时间以及锁拥有者状态决定。
- 如果同一个锁,自旋刚刚成功获得锁,那么此次自旋也有可能成功,允许自旋等待持续相对更长的时间。
- 如果这个锁自旋很少成功获得,那么以后可能忽略掉自旋过程。
锁消除
虚拟机即时编译器允许时,对一些代码上要求同步,但被检测到不可能存在共享数据竞争的锁进行消除。
判定依据:逃逸分析的数据支持,如果判断一段代码中,堆上所有的数据都不会逃逸出去从而被其他线程访问到,那就可以把它们当做栈上数据对待,认为它们是线程私有的,同步加锁就无须进行了。
对于一些看起来没有加锁的代码,其实隐式的加了很多锁。例如下面的字符串拼接代码就隐式加了锁:
1 | public static String concatString(String s1, String s2, String s3) { |
String是一个不可变的类,编译器会对String的拼接自动优化。在JDK 1.5之前,会转化为StringBuffer对象的连续append()
操作:
1 | public static String concatString(String s1, String s2, String s3) { |
每个append()
方法中都有一个同步块。虚拟机观察变量sb,很快就会发现它的动态作用域被限制在concatString()
方法内部。也就是说,sb的所有引用永远不会逃逸到concatString()
方法之外,其他线程无法访问到它,因此可以进行消除。
锁粗化
推荐同步块的作用范围限制尽量小,但如果一系列连续操作都对同一个对象反复加锁和解锁。这时虚拟机探测到这些时,将会将加锁同步的范围扩展到整个操作序列的外部。
轻量级锁
JDK 1.6引入了偏向锁和轻量级锁,从而让锁拥有了四个状态:无锁状态(unlocked)、偏向锁状态(biasble)、轻量级锁状态(lightweight locked)和重量级锁状态(inflated)。
在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。
无竞争下使用CAS操作去消除同步使用的互斥量:
- HotSpot虚拟机的对象内存布局:对象头包括两部分:
- 存储对象自身的运行时数据,如哈希码,GC分代年龄等,在64位虚拟机中为64Bits,称为Mark Word,是轻量级锁与偏向锁的关键。
- 存储指向方法区对象类型数据的指针,如果是数组对象,则还会存储数组长度。
- 轻量级锁执行过程:
- 代码进入同步块时,如果此同步对象没有被锁定(标志位01),虚拟机将在当前线程的栈帧建立一个名为锁记录的空间,存储锁对象目前的Mark Word拷贝。
- 虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针:
- 如果更新成功,则线程拥有了该对象的锁,锁标志位变为00。
- 更新失败,虚拟机首先检查对象的Mark Word是否指向当前线程的栈帧。
- 如果只说明当前线程已经拥有了这个对象的锁,那么就可以直接进入同步块继续执行。
- 否则说明这个锁对象已经被其他线程抢占。
- 如果两条以上的线程争用同一个锁,则锁膨胀为重量级锁,标志位为10,Mark Word存放指向重量级锁的指针。
- 依据:对于绝大部分锁,整个同步周期内是不存在竞争的:
- 如果存在竞争,则额外CAS操作。
- 不存在竞争,则避免了开销。
偏向锁
消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能。在无竞争的情况下,把整个同步都消除掉。
锁会偏向于第一个获得它的线程,在接下来执行过程中,该锁没有被其他线程获取,则持有偏向锁的线程永远不需要再进行同步。
- 当锁第一次被线程获取时,会把对象头中的标志设置为01。使用CAS操作把获取到这个锁的线程的ID记录在对象的Mark Word中。
- 如果CAS操作成功,持有偏向锁的线程每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作。
- 当另一个线程尝试获取这个锁,偏向模式宣告结束。
- 转为轻量级锁。