Java基础总结(三)——多线程

多线程:sychronized,Lock,volatile,CAS。每一个的实现原理和常用场景,可重入锁和公平锁的原理,java的锁优化

sychronized解决并发问题

  • **实现原理:**java自带关键字,被sychronized修饰的方法或者代码块,可保证在同一时刻只有一个线程可以执行这个代码块。同时sychronized还可保证共享变量的内存可见性,可替代volatile。

  • 不可让等待的线程自动响应中断,等待的线程会一直等待下去

  • 遇到异常时,可主动释放被线程占用的锁,不会发生死锁现象

  • 使用sychronized不可知道是否成功获取锁
    常用场景

  • 修饰普通方法。锁是当前实例对象 ,进入同步代码前要获得当前实例的锁。

  • 修饰静态方法。锁是当前类的class对象 ,进入同步代码前要获得当前类对象的锁。

  • 修饰代码块。sychronized(XXX.class)锁是括号里面的对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

Lock

  • Lock是接口
  • 可让线程自动响应中断
  • 遇到异常时,需通过unlock()才可释放锁,易发生死锁现象,需在finally块中执行unlock()
  • 通过Lock可知道是否成功获取锁
  • lock和synchronized的区别在这里插入图片描述
    常用场景
  • Synchronized和Lock并没有我们想象的有那么大差异,他们都是利用线程的阻塞(BLOCKING)来实现同步的,都是使用了基于锁的阻塞算法
  • 在一些内置锁无法满足需求的情况下,ReentrantLock可以作为一种高级工具。当需要一些高级功能时才应该使用ReentrantLock,这些功能包括:可定时的,可轮询的与可中断的锁获取操作,公平队列,以及非块结构的锁。否则,还是应该优先使用Synchronized

volatile(线程不安全)
原理:

  • Java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。
  • 在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比sychronized关键字更轻量级的同步机制。
  • 当对非 volatile 变量进行读写的时候,每个线程先从内存拷贝变量到CPU缓存中。如果计算机有多个CPU,每个线程可能在不同的CPU上被处理,这意味着每个线程可以拷贝到不同的 CPU cache 中。而声明变量是 volatile 的,JVM 保证了每次读变量都从内存中读,跳过 CPU cache 这一步。

常用场景:
只能在有限的一些情形下使用 volatile 变量替代锁。要使 volatile 变量提供理想的线程安全,必须同时满足下面两个条件:

  • 对变量的写操作不依赖于当前值。
  • 该变量没有包含在具有其他变量的不变式中。
    实际上,这些条件表明,可以被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。
    第一个条件的限制使 volatile 变量不能用作线程安全计数器。

Java 内存模型中的可见性、原子性和有序性

  • 可见性:volatile、synchronized 和 final 实现可见性。
  • 原子性:synchronized 和在 lock、unlock 中操作保证原子性。(a++是非原子性的操作)
  • 有序性: volatile 和 synchronized 两个关键字来保证线程之间操作的有序性。

CAS(Conmpare And Swap)是用于实现多线程同步的原子指令
原理

  • 它将内存位置的内容与给定值进行比较,只有在相同的情况下,将该内存位置的内容修改为新的给定值。 这是作为单个原子操作完成的。 原子性保证新值基于最新信息计算; 如果该值在同一时间被另一个线程更新,则写入将失败。 操作结果必须说明是否进行替换; 这可以通过一个简单的布尔响应(这个变体通常称为比较和设置),或通过返回从内存位置读取的值来完成。

  • 执行函数:CAS(V,E,N) 如果V值等于E值,则将V的值设为N。若V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。
    常用场景

  • 对于资源竞争较少的情况,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗cpu资源;而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。

  • 对于资源竞争严重的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized。

可重入锁和公平锁

  • 公平锁能保证:老的线程排队使用锁,新线程仍然排队使用锁。
  • 非公平锁保证:老的线程排队使用锁;但是无法保证新线程抢占已经在排队的线程的锁。
  • 如果锁具备可重入性,则称作为可重入锁。像synchronized和ReentrantLock都是可重入锁,可重入性在我看来实际上表明了锁的分配机制:基于线程的分配,而不是基于方法调用的分配。举个简单的例子,当一个线程执行到某个synchronized方法时,比如说method1,而在method1中会调用另外一个synchronized方法method2,此时线程不必重新去申请锁,而是可以直接执行方法method2。

java的锁优化
优化的方向就是减少线程的阻塞,因为挂起线程和恢复线程需要切换到操作系统的内核状态。

1.偏向锁
2.轻量级锁
3.重量级锁
4.锁消除
5.锁粗化
6.除了虚拟机,程序员自己如何优化锁

偏向锁
虚拟机的团队根据经验发现,大多数情况下,锁不仅不存在多线程竞争,而且总是有同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。

当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单的测试一下对象头的 “Mark Word” 里是否存储着指向当前线程的偏向锁。

如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下 Mark Word 中偏向锁的标识是否设置了1(表示当前还是偏向锁):如果没有设置,则使用CAS 竞争锁;如果设置了,则尝试使用CAS 将对象头的偏向锁指向当前线程。

可以说,偏向锁的 “偏”,就是偏心的 “偏”,他的意思就是这个锁会偏向于第一个获得他的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要同步。

当有另外要给线程去尝试获取这个锁时,偏向模式宣告结束,后续的操作将升级为轻量级锁。

注意:偏向锁可以提高有同步但无竞争的程序性能,他同样有缺陷:如果程序中大多数的锁总是被多个不同的线程访问,那偏向模式就是多余的。1.6之后的虚拟机默认启用偏向锁,可以使用JVM参数来关闭:-XX:-UseBiasedLocking=false;程序将默认进入轻量级锁状态。

可以看到,Mark Word 是实现偏向锁的关键。而后面的轻量级锁也是通过这个实现的。

轻量级锁
什么是轻量级锁呢? “轻量级” 是相对于使用操作系统互斥量来实现的传统锁而言的,因此传统的锁机制称为 “重量级” 锁。 首先需要强调一点,轻量级锁并不是用来代替重量级锁的,他的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能损耗。

线程在执行同步块之前,JVM 会先在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头的 Mark Word 复制到锁记录中,官方称为 Displaced Mark Word. 然后线程尝试使用CAS 将对象头中的 Mark Word 替换为指向锁记录的指针。

如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便会尝试使用自旋来获取锁,注意:这里线程并没有挂起自己,而是通过一定次数的自旋(默认10次,可以使用 -XX:PreBlockSpin 修改),防止切换到内核态导致的开销。

如果有2个以上的线程争用同一把锁,那么轻量级锁将会失效,升级到重量级锁。

那么为什么升级到重量级锁之后不能降级呢?假设一下:如果锁升级到重量级之后,拿到锁的某个线程被阻塞了,等待了很久,那么轻量级线程将会一直自旋等待,消耗CPU性能。所以,在升级到重量级锁后,就不能降级了,防止轻量级锁自旋消耗CPU。

可以看到偏向锁和轻量级锁的差别,偏向锁在第一个线程拿到锁之后,将把线程ID 存储在对象头中,后面的所有操作都不是同步的,相当于无锁。而轻量级锁,每次获取锁的时候还是需要使用CAS来修改对象头的记录,在没有线程竞争的情况下,这个操作是很轻量的,不需要使用操作系统的互斥机制。

重量级锁
相比较轻量级锁是通过自旋来获取锁的,重量级锁则是通过操作系统将线程切换到内核态并阻塞来实现的。代价十分高昂。
在这里插入图片描述
锁消除
什么是锁消除呢?指的是 JIT 编译器在运行时,对一些没有必要同步的代码却同步了的锁进行消除。可以说是一种彻底的锁优化。通过锁消除,可以节省毫无意义的请求锁时间。

锁粗化
原则上,我们在编写代码的时候,总是推荐将同步块尽可能的小。这样是为了使得需要同步的操作数量小,如果存在锁竞争,那等待锁的线程也能尽快拿到锁。

大部分情况下,这个原则是正确的。如果如果一系列连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中的,那即使没有线程竞争,频繁的同步操作也会导致不必要的性能损耗。

如果虚拟机探测到很多零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部。即加大了同步块。

除了虚拟机,程序员自己如何优化锁

  • 减小锁的持有时间 :只在必要时进行同步
  • 减小锁的粒度
    这个和我们上面说的虚拟机帮助我们粗化时反的。但是,我们说,大部分情况下,减小锁的粒度也是削弱多线程竞争的有效手段,比如 ConcurrentHashMap,他只锁住了 Hash 桶中的某一个桶,不像HashTable 一样锁住整个对象。
  • 使用读写锁替换独占锁
    我们之前在说 Java 世界的三把锁的时候说哪三把锁,内置锁,重入锁,读写锁,就是我们现在说的读写锁 ReadWriteLock,使用读写锁来替代独占锁是减小锁粒度的一种特殊情况,在读多写少的场合,读写锁对系统性能是有好处的。可以有效提高系统的并发能力。因为读操作不会影响数据的完整性和一致性,就像 ConcurrentHashMap 的 get 方法一样,根本不需要加锁,这个时候又要说说 HashTable ,该容器连 get 方法都加锁。你可以想象一下。
  • 锁分离
    如果将读写锁进一步延伸,就是锁分离,读写锁根据读写操作功能的不同,进行了有效的分离。而 JDK 的 LinkedBlockingQueue 则是锁分离的最佳实践。在进行 take 操作和 put 操作使用了两把不同的锁。因为他们之间根本没有竞争关系,或者说,使用队列的数据结构,将原本耦合的业务分离了。

参考:
https://blog.csdn.net/Dome_/article/details/86676430

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值