Java内存模型系列(四)线程与锁

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的suspendresume方法、System的setInsetOutrunFinalizersOnExit

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操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块,都不需要同步操作
  • 当另一个线程尝试获取该锁,偏向模式结束,撤销偏向恢复到未锁定或轻量级锁状态

优点:偏向锁可以提高带同步但无竞争的程序性能

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值