5、线程安全的实现方式

2021-07-04 05:05

阅读:549

标签:ant   unsafe   critical   执行   申请   场景   其它   需要   用户态   

  了解了什么是线程安全之后,接下来就是如何实现线程安全。那么了解虚拟机提供的同步机制以及锁机制也就非常重要了。
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交互模型中的“一个请求对应一个服务器线程”的处理方式。

5、线程安全的实现方式

标签:ant   unsafe   critical   执行   申请   场景   其它   需要   用户态   

原文地址:https://www.cnblogs.com/nevermorewang/p/9614092.html


评论


亲,登录后才可以留言!