0x01 线程实现
方式一:使用内核线程实现
内核线程(Kernel-Level Thread,KLT)直接由操作系统内核支持。它的线程切换是内核通过调度器对线程进行调度,并将线程任务映射到各个处理器上
轻量级进程(Light Weight Process,LWP)就是通常意义上的线程,每个轻量级进程都有一个内核进程支持。
缺点:
- 基于内核进程,所以各种线程操作,如创建、析构和同步都需要系统调用,而系统调用代价高,需要在用户态和内核态来回切换
- 每个轻量级线程都要一个内核进程支持,会消耗一定内核资源,一个系统支持轻量级线程数量有限
方式二:使用用户线程实现
用户线程(User Thread,UT)建立在用户空间,系统内核无法感知。线程创建、同步、销毁和调用完全在用户态完成。
缺点:
- 没有系统内核支持,所有线程操作需要自行处理
方式三:使用用户线程+轻量级进程混合实现
轻量级进程作为用户线程与内核线程沟通的桥梁,这样可以使用内核提供的线程调度以及处理器映射,而用户线程的创建、切换、析构依旧廉价。
0x02 线程调度
方式一:协同式线程调度
线程执行时间由线程控制,结束执行后通知系统切换到另一个线程
优点:实现简单、无线程同步问题
缺点:线程执行时间不可控,可能造成阻塞
方式二:抢占式线程调度
线程执行时间由系统分配
0x03 线程安全
线程是否安全完全根据数据是否共享来确定,如果数据不是共享的根本就不存在线程安全问题。
不可变
boolean i = false; // 操作1
final boolean j = true; // 操作2
final String s = "123"; // 操作3
操作2中的变量j是不可不变变量,即使作为共享变量,但是由于final的不可变特性,任何线程访问都是线程安全的
操作3是一个对象,那么要保证该对象的方法不会对s进行修改。如果String中的某个方法会修改s的值,那么要保证该方法的线程安全(即该方法要保证其他线程获取到s的值永远都是最新值)
绝对线程安全
不管运行环境如何,调用者都不需要任何额外同步措施。
假设Vector集合,线程1调用remove方法,线程2调用get方法,虽然remove和get方法都是synchronized的,但是不同线程操作同一个vector集合。
比如进行了复合操作
相对线程安全
单独调用该对象的时候无需做额外保障,但是在进行连续调用(复合操作),需要额外的手段进行保障
举例如:Vector、HashTable、Collections的synchronizedCollection方法包装的集合
线程兼容
对象本身不是线程安全的,但是可以在调用时做处理。如ArrayList、HashMap等
线程对立
不论是否采用了同步措施,都无法在多线程中使用。如Thread的suspend
和resume
方法、System的setIn
、setOut
、runFinalizersOnExit
0x04 线程安全实现方法
方式一:互斥同步
同步
是指多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一个线程使用。
互斥
是实现同步的方式,如临界区
,互斥量
,信号量
等
在java中的体现是synchronized
关键字、JUC包中的类。
方式二:非阻塞同步
互斥同步也称作阻塞同步,任何时候都要加锁进行操作。而非阻塞同步是在出现共享数据冲突才进行检测。
非阻塞同步需要硬件支持,因为原子性操作不能依靠加锁来实现,那么就需要硬件来实现。通常称为CAS指令。JDK1.5之后才可以使用
CAS缺点:ABA问题,在此期间可能被其他线程修改过了,但是又改回旧值。可通过AtomicStampedReference
,但推荐使用互斥同步
方式三:无同步方案
可重入代码:即在运行期间调用其他代码也不会导致出错,简单讲就是不需要堆内存(共享内存、主内存)中的数据。所有数据都是在工作内存中的
线程本地存储:共享数据是否在同一个线程中,如生产者-消费者。如web的一个请求对应一个线程
0x05 锁优化
自旋锁与自适应锁
场景:某些请求在获取锁之后导致其他线程阻塞,但是其实该请求持有锁时间短,只要其他线程稍等一下就可以获取到锁,因此无需阻塞又重新恢复(前面讲过线程的挂起和恢复都进入内核态,消耗性能)
解决方法:通过-XX:+UseSpinning
开启自旋锁,线程会自旋等待获取锁,需要指定次数(-XX:PreBlockSpin
指定)避免消耗cpu性能
自适应锁:在自旋锁的基础上,无需指定自旋次数,会根据上次自旋是否获得锁来判定本次自旋能否获得锁
适用场景:持有锁时间短的操作
锁消除
根据逃逸分析技术
确定数据不是共享数据,那么无需加锁
public void test() {
StringBuffer sb = new StringBuffer();
sb.append("test");
}
StringBuffer是线程安全的,append方法是同步方法,但是这个变量只在线程中有效,其他线程无法访问,不是共享数据,生命周期随着线程生死,那么jvm就会进行优化,去掉锁
锁粗化
如果对同一个对象加锁解锁多次,即使没有线程竞争,也会浪费性能消耗,例如在循环体中
for (int i = 0; i < 100; i++) {
synchronized (this) {
// do something
}
}
// 锁粗化
synchronized (this) {
for (int i = 0; i < 100; i++) {
// do something
}
}
轻量级锁
传统的锁,如synchronized
是重量级锁,而轻量级锁是为了在没有多线程竞争前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗
实现原理:HotSpot对象头(Mark Word)中存储锁信息
加锁过程:
- 进入同步块的时候,如果同步对象没有被锁定(Mark Word锁标记01)
- 虚拟机在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前Mark Word的拷贝
- 虚拟机使用CAS操作将对象的Mark Word指向栈帧中的锁记录(Lock Record)
- 如果更新成功,该线程拥有该对象的锁,Mark Word锁标记变成00,此时锁处于轻量级锁状态
- 如果更新失败,先检查对象的Mark Word是否指向当前线程的栈帧
- 如果当前线程拥有该对象锁,则进入同步块继续执行
- 否则该锁对象被其他线程获取,则轻量级锁变成重量级锁,锁标记变成10
解锁过程:
- 如果对象的Mark Word仍然指向线程的锁记录,那就用CAS把对象当前的Mark Word和线程中复制的Displaced Mark Word替换回来
- 如果替换成功,整个同步过程完成了
- 如果替换失败,说明其他线程尝试过获取该锁,就要在释放锁的同时,唤醒被挂起的线程
优点:大部分锁在同步期间不存在竞争,所以使用CAS避免了使用互斥量得开销,如果存在锁竞争,除了互斥量的开销,还额外发生了CAS,因此在有竞争的情况下,比重量级锁慢
偏向锁
偏向锁总是偏向第一个获取它的线程,如果在接下来的操作中该锁没有被其他线程获取,则持有该偏向锁的线程永远不需要同步
目的:消除数据在无竞争情况下的同步原语
比较轻量级锁:轻量级锁在无竞争情况下通过CAS消除同步使用的互斥量;偏向锁在无竞争情况下把整个同步都消除,连CAS操作也去除
加锁过程:
- 当锁对象第一次被线程获取的时候,虚拟机将对象头标识设置为01,同时使用CAS操作把获取到这个锁的进程ID记录在对象头中
- 如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块,都不需要同步操作
- 当另一个线程尝试获取该锁,偏向模式结束,撤销偏向恢复到未锁定或轻量级锁状态
优点:偏向锁可以提高带同步但无竞争的程序性能