线程安全与锁优化

线程安全

定义: 线程安全的定义:当多个线程访问一个对象时,如果不用考虑这些线程在运行时的环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方法进行任何其它的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象的线程就是安全的。

  • 按“安全程度”由强至弱排序分为五类:
    • 不可变
      • 不可变的对象一定是安全的,即用final关键字修饰的变量和对象一定是安全的,如String类中的substring()方法
    • 绝对线程安全
      • 绝对线程安全满足线程安全的定义,即java中声明了线程安全的类,如果Vector就是一个线程安全的容器,他的的add()等方法都是被synchronsized修饰的。
    • 相对线程安全
      • 需要保证对这个对象单独操作是线程安全的
      • 在java中如Vector,HashTable等
    • 线程兼容
      • 指线程本身并不安全,但是可以通过调用正确的同步手段来保证并发环境中可以安全的使用
    • 线程对立
      • 不论采用什么手段都不安全,并不常见

线程安全的实现方法

  1. 互斥同步
  • 定义:互斥同步是常见的一种并发正确性保障手段
  • 注意:互斥是因(信号量,临界区等),同步是果;互斥是方法,同步是目的
  • 方法:synchronized关键字,java.util.concurrent包中的重入锁ReentrantLock来实现同步
  • synchronized关键字
    • 在字节码前后分别形成monitorenter和monitorexit两个两个字节码指令
    • 执行流程:
      • 执行monitorenter指令时,尝试获取对象的锁,如果对象没被锁定,或者该线程已经持有了那个对象的锁,锁计数器就加一,当执行到monitorexit时,锁计数器就减一,当计数器为0时,锁就被释放,如果获取对象失败,当前线程就要阻塞等待,直到线程锁被释放。
    • 注意点:
      • synchronzied同步块对于一条线程是可重入的,不不会出现自己把自己锁死。
      • 由于java的线程是映射到操作系统原生线程上的,所以阻塞和唤醒线程都会涉及到用户态和核心态之间的转换,因此资源的消耗非常大,被称为为重量级锁,尤其是同步特别简单的代码块,比如getter,setter方法
  • ReentrantLock(java.util.concurrent)
    • 区别:ReentrantLock表现为API层面的锁,运用Lock和unLock方法配合try和finally语句来完成,而synchronzied表现为原生语法层面的锁
    • 特点:
    1. 等待可中断
    • 持有锁的线程长期不释放锁的时候,正在等待的线程可以放弃等待去干其它的事情(重要)
    1. 公平锁
    • 根据申请锁的时间顺序来依次获得锁,但是性能会下降,影响吞吐量
    1. 锁绑定多个条件
    • 一个ReentrantLock对象可以同时绑定多个条件对象,在synchronized中,使用wait和notify来进行实现隐含条件,如果条件多就会加很多锁,但是ReentrantLock一个说可以绑多个条件
  1. 阻塞同步和非阻塞同步
  • 乐观策略
  • 步骤:进行操作和冲突检测需要具备原子性,需要靠硬件来保证原子性
  • CAS
    • CAS指令有三个操作数,分别是内存位置(java中变量的内存地址 V),旧的预期值(A),准备设置的新值(B),当且仅当V符合A时,处理器才会用B更新V,否则就不更新(这是一个原子过程)
    • ABA问题
  1. 无同步方案
  • 线程本地存储:尝试将共享数据的代码保证在同一个线程中执行,因此无须同步也能保证线程之间不出现数据争用的问题

锁优化

1.1. 自旋锁与自适应自旋

  • 前提: 互斥同步需要通过阻塞实现,而挂起线程和恢复线程都需要转入内核态实现,而内核态和用户态的转换需要消耗很多处理器的时间,同时共享数据的锁定状态只需要很短的一段时间,阻塞线程的两太转换需要消耗太长时间,不值得,由此提出了自旋锁。
  • 方案:让后面请求锁的线程稍等一下,但不放弃处理器资源,即线程执行一个忙循环,这就是自旋锁。
  • 特点:如果锁被占用的时间比较短,自旋等待的效果就很好,如果锁被占用的时间长,如果锁被占用的时间长,就会浪费很多处理器资源。
  • 优化方案:给自旋等待时间设置一定的限度,如果自旋次数超过了一定限度还没有获得锁,就将该线程挂起(自旋默认10次)
    1.2 自适应自旋
  • 思想:由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定
  • 例如:如果自旋等待刚刚成功了,就会认为下次又会成功,因此允许自选等待持续相对长的时间,如果总是失败,就直接挂起线程
  1. 锁清除
  • 定义:虚拟机即时编译器在运行时,对一些代码上要求同步,如果检测到了不可能出现数据竞争的线程,就会对其锁进行消除
  1. 锁粗化
  • 定义:一系列连续操作对同一个对象反复加锁解锁,或加锁操作出现在循环体中,就会频繁的加锁解锁,出现性能的巨大损耗,因此通过扩大锁的粒度,只需要加一次锁就能解决问题。
  1. 轻量级锁
    在这里插入图片描述
  2. 偏向锁
    在这里插入图片描述

偏向所锁,轻量级锁及重量级锁

  • 偏向所锁,轻量级锁都是乐观锁,重量级锁是悲观锁。
  • 一个对象刚开始实例化的时候,没有任何线程来访问它的时候。它是可偏向的,意味着,它现在认为只可能有一个线程来访问它,所以当第一个线程来访问它的时候,它会偏向这个线程,此时,对象持有偏向锁。偏向第一个线程,这个线程在修改对象头成为偏向锁的时候使用CAS操作,并将对象头中的ThreadID改成自己的ID,之后再次访问这个对象时,只需要对比ID,不需要再使用CAS在进行操作。一旦有第二个线程访问这个对象,因为偏向锁不会主动释放,所以第二个线程可以看到对象时偏向状态,这时表明在这个对象上已经存在竞争了,检查原来持有该对象锁的线程是否依然存活,如果挂了,则可以将对象变为无锁状态,然后重新偏向新的线程,如果原来的线程依然存活,则马上执行那个线程的操作栈,检查该对象的使用情况,如果仍然需要持有偏向锁,则偏向锁升级为轻量级锁,(偏向锁就是这个时候升级为轻量级锁的)。如果不存在使用了,则可以将对象回复成无锁状态,然后重新偏向。轻量级锁认为竞争存在,但是竞争的程度很轻,一般两个线程对于同一个锁的操作都会错开,或者说稍微等待一下(自旋),另一个线程就会释放锁。 但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁,重量级锁使除了拥有锁的线程以外的线程都阻塞,防止CPU空转。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值