zoukankan      html  css  js  c++  java
  • 5、线程安全的实现方式

      了解了什么是线程安全之后,接下来就是如何实现线程安全。那么了解虚拟机提供的同步机制以及锁机制也就非常重要了。
    1、互斥同步
      互斥同步是指多个线程并发访问共享数据时,保证共享数据在同一时刻只被一个线程使用。在这个地方,互斥是因,同步是果;同步的实现方式是通过互斥来实现的;常见的互斥实现方式有:临界区(critical selection),互斥量(mutex)和信号量(semaphore);
    java中最基本的互斥手段就是synchronized关键字,synchronized经过编译之后,会在同步块前后分别形成monitorenter跟monitorexit两个字节码命令,这两个字节码都需要一个reference类型的参数来指明需要锁定和解锁的对象。如果synchronized明确指出了对象参数,那就是这个对象的引用,如果没有,那就根据所修饰的是实例方法还是类方法,去获取相应的对象实例或者class对象作为锁对象。
      synchronized对同一线程是可重入的,不会出现自己把自己锁死的情况。同步块在执行结束前,会阻塞其它线程的进入。由于java的线程是要映射到操作系统的原生线程的,如果阻塞或者唤醒一个线程,都需要操作系统来帮助完成,这就需要从用户态转换为核心态,这个转换要消耗大量cpu时间,对于代码简单的同步块,可能这个时间要大于执行时间。因此说,synchronized是一个重量级操作,一般只在确实必要的情况下使用。当然,虚拟机对此也有一些优化,比如阻塞线程前先进性一段自旋等待。
    我们还可以通过java.util.concurrent下的ReentrantLock来实现同步。相比synchronized,ReentrantLock提供了一些比较灵活高级的功能,主要有:
      等待可中断:持有锁的线程长期不释放锁的时候,等待的线程可以选择放弃等待;
      公平锁:多个线程获取锁的时候,必须按照申请锁的时间来依次获取锁;(严格排队,防止有的线程一直获取不到锁)
      绑定多个条件:一个ReentrantLock对象可以绑定多个Condition对象,而在synchronized中,锁对象的wait()跟notify()可以实现一个隐含的条件,如果要和多余一个的条件关联时,就不得不额外加锁;ReentrantLock只需要多次new Condition()就可以了。
      性能上,1.5的时候ReentrantLock略好,jdk1.6以后两者基本持平,而且虚拟机在优化上偏向于synchronized,因此性能不再是两者的考虑因素,都可以实现的情况下,优先synchronized。
    2、非阻塞同步
      互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,因此这种实现方式也叫阻塞同步。从处理问题的方式上来说,互斥同步属于一种悲观的并发策略。
    随着硬件指令集的发展,我们有了另外的一个选择:基于冲突检测的乐观并发策略。也就是先进行操作,如果没有其它线程使用共享数据,那就操作成功;如果有,那就再采取补偿措施。这种方式不需要把线程挂起,因此称为:非阻塞同步(Non-Blocking Synchronization)。
      所谓“硬件指令集”,就是指现代指令集中新增的CAS指令(Compare-And-Swap,比较并交换)。jdk1.5以后才有cas的相关操作,比如sun.misc.Unsafe类里边的compareAndSwapInt()方法等,虚拟机内部对这些方法做了处理,编译出来的结果就是一条平台相关的处理器 CAS指令。
      尽管CAS看起来很美,但这种操作无法涵盖互斥同步的所有使用场景,并且CAS从语义上来说并不是完美的,因为存在一个逻辑漏洞:如果变量V初次读取的时候是a值,并且在准备赋值的时候还是a值,那我们就能判断V没有被其它线程修改过吗?显然不能,如果在这期间有线程把V修改为了b,又重新修改为了a,CAS就会认为没有被修改过,这个漏洞称为CAS的“ABA问题”。concurrent包提供了一个AtomicStampedReference来解决此问题,原理是通过设置版本号来保证正确性(数据库乐观锁,是不是很相似),不过目前来说这个类有点“鸡肋”,大部分的ABA问题不会影响并发的正确性,如果需要解决ABA问题,使用互斥同步可能会比原子类更高效。
    3、无同步方案
      要保证线程安全,并不一定要进行同步。两者之间没有因果关系。同步只是一种保证共享数据争用时正确性的手段而已。有些代码是天生线程安全的,比如:
      可重入代码:这种代码也叫纯代码。可以在代码执行的任何时刻中断它,转而执行另一段代码,而在控制权返回后,原程序不会有任何错误。可重入代码都有一些共同特征,比如:不依赖存储在堆上的数据和公用的系统资源、用到的状态量都由参数传入、不调用非可重入的方法等。
      线程本地存储:如果一段代码中所需要的数据都完全包含在同一个线程中,如果能保证这一点,那就不会因为跟其它线程争抢修改资源而导致数据不一致,也就没有线程风险,是线程安全的。我们平常web开发中基本不考虑多线程干扰,就是因为web交互模型中的“一个请求对应一个服务器线程”的处理方式。

  • 相关阅读:
    QFramework 使用指南 2020(二):下载与版本介绍
    QFramework 使用指南 2020 (一): 概述
    Unity 游戏框架搭建 2018 (二) 单例的模板与最佳实践
    Unity 游戏框架搭建 2018 (一) 架构、框架与 QFramework 简介
    Unity 游戏框架搭建 2017 (二十三) 重构小工具 Platform
    Unity 游戏框架搭建 2017 (二十二) 简易引用计数器
    Unity 游戏框架搭建 2017 (二十一) 使用对象池时的一些细节
    你确定你会写 Dockerfile 吗?
    小白学 Python 爬虫(8):网页基础
    老司机大型车祸现场
  • 原文地址:https://www.cnblogs.com/nevermorewang/p/9614092.html
Copyright © 2011-2022 走看看