JVM(五)----------------线程安全之同步机制

我觉得对知识的理解不仅是你看懂了书本的内容,还要在面试时能将你的理解完整地表述出来。我看了线程安全的知识,然后自问自己能否将它表述,结果失望地发现我连意思都表述不清。为此写这篇博客

什么是线程安全:

我看过对线程安全的一个通俗的解释是:如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,

线程安全并没有具体的定义,但虚拟机这本书里引用了一个相对来说比较准确的定义:当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以活得正确的结果,那么这个对象就是线程安全的。

Java语言中的线程安全

讨论线程安全首先得限定多个线程之间存在共享数据访问这个前提。有了这个前提,可以将JAVA语言中各种操作共享的数据分为以下5类:不可变,绝对线程安全,相对线程安全,线程兼容和线程对立

1.不可变

首先记住,不可变的对象一定是线程安全的。只要一个不可变的对象被正确地构建出来(没有发生this引用逃逸的情况)其外部的可见状态永远不会改变,永远不会看到它在多线程处于不一致的状态。(final关键字)

java.lang.String类的对象,他就是一个典型的不可变对象,不管调用substring、replace、还是concat方法都不会影响它的值,只会返回一个新构造的字符串对象。还有枚举类以及Number的部分子类如Long和double

2.绝对线程安全

绝对线程安全的定义是很严格的,大多数Java类都不是绝对线程安全的。例如java.util.Vector,虽然它的一些方法都被synchronized修饰,但在多线程下进行访问修改还是会出错。

3.相对线程安全

我们通常说的线程安全就是相对线程安全,它需要保证对这个对象的单独操作是线程安全的。对一些特定顺序的连续调用,就需要在调用端使用额外的同步手段来保证调用的正确性

如Vector,HashTable,Collections的synchronizedCollection()方法包装的集合都属于这种线程安全

5.线程对立

线程对立是指无论调用端是否采取了同步措施,都无法在多线程环境中 并发使用的代码。这种情况是很少见的,也是有害的。

这里只举一个例子:

 线程安全的实现方法

虚拟机实现线程安全主要有两种机制:同步机制和锁机制

1.互斥同步(悲观策略)

互斥同步是常见的一种并发正确性保障手段。同步是指在多个线程并发访问共享数据时,保证共享数据在同一时刻只被一个线程使用。互斥是实现同步的手段,由操作系统知识可知,临界区,互斥量,和信号量都是主要的互斥实现方式。

互斥是实现同步的一种手段。以下是实现互斥的手段:

  • 临界区
  • 互斥量
  • 信号量

互斥是因,同步是果,互斥是方法,同步是目的。

互斥在代码中是如何体现的?

synchronized 
经过编译之后,会在同步块的前后分别形成monitorenter和monitorexit两个字节码指令,这两个字节码都需要一个reference类型的参数来指明要锁定和解锁的对象。如果Java程序中的synchronized明确指定了对象参数,那就是这个对象的reference:如果没有明确指定,那就根据synchronized修饰的是实例还是类方法,去取对应的对象实例或Class对象来作为锁对象。

主要表现为以下特征:

  • 对同一线程是可重入的
  • 同步块在已进入的线程执行完之前,会阻塞后面其他线程的进入
  • synchronized是重量级操作,会执行比较长的时间,阻塞或唤醒一个线程,都需要操作系统来帮忙完成,这需要从用户态转换到核心态中。
  • 虚拟机会对synchronized进行一些优化

ReentrantLock 
与synchronized功能类似,主要表现以下特征:

  • 等待可中断。当持有锁的线程长期不释放锁,正在等待的线程可以选择放弃等待,改为处理其他事情
  • 公平锁。多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁;synchronized的锁是非公平的
  • 锁定绑定多个条件。一个ReentrantLock对象可以同时绑定多个Condition对象,而synchronized中只能有一个条件

想要明白什么是可重入锁与不可重入锁,什么是公平锁与非公平锁可以看下这篇文章:

https://blog.youkuaiyun.com/yanyan19880509/article/details/52345422 

2.非阻塞同步(乐观策略)

互斥同步总是认为只要不去做正确的同步措施(如加锁),那就肯定会出现问题。所以无论数据是否会出现竞争,它都要进行等操作。而非阻塞同步是一种基于冲突检测的乐观并发策略,就是先进行操作,如果没有其他线程争用共享数据,那操作就成功了;如果共享数据有争用产生了冲突,那就再采取其他补偿措施(最常见的补偿措施就是不断地重试,直到成功为止)。非阻塞同步主要是为了解决、克服线程阻塞和唤醒所带来的性能问题。

主要的实现:
CAS指令 (这是一条硬件指令,因为能保证从语义行看起来需要多次操作的行为只通过一条处理器指令就能完成)

需要有三个操作数,分别是内存位置(在Java中可以简单理解为变量的内存地址,用V表示)、旧的预期值(用A表示)和新值(用B表示)。CAS指令执行时,当且当V符合旧预期值A时,处理器用新值B更新V的值,否则它就不执行更新,但是无论是否更新了V的值,都会返回V的旧值,上述的处理过程是一个原子操作。

当多个线程使用CAS同时更新一个变量值时,只有其中一个线程能够更新成功,其他的线程都将失败。但是,失败的线程不会被挂起(但如果获取锁失败,线程将被挂起),而是返回失败状态,调用者线程可以选择是否需要再一次尝试(如果是在一些竞争激烈的情况下,更好的方式是在重试之前等待一段时间或者回退,从而避免活锁问题–不断重试,不断失败),或者执行一些恢复操作,也可以什么都不做。

JDK1.5之后,JAVA程序中才可以使用CAS操作,该操作由sun.misc.Unsafe里面的几个方法包装提供,虚拟机在内部对这些方法做了特殊处理,即时编译出来的结果就是一条平台相关的处理器CAS指令,没有方法调用过程,或者可以认为是无条件内联进去。只有启动类加载器加载的类才能访问。

以上是书本的定义,说一下自己对CAS指令的理解:首先我们第一次从内存地址V读取到一个值A,过了一段时间后(这段时间可能极短,因为内存地址V的值随时都可能被更新)第二次从内存地址V 读取到一个值,将后一次读取的值与A进行比较,如果相等,则将新值B赋予内存地址V,即将内存地址V的值更新为B。为什么说值A是预期值,那时因为我们希望两次从内存地址V读取到的值都不变 ,也就是希望第二次读取到的值仍等于A,这样才可以赋予更新值B。

综合这个理解,就很容易明白CAS操作的“ABA”问题:如果经过了第一次从内存地址V取得值A后,第二次从内存地址V取值前,内存地址V被更新为其他值,然后这个其他值又再次更新为A值,那么第二次从内存地址V取得的值仍为A,此时CAS操作就会认为它从来没有被改变过。这漏洞就是CAS的“ABA”问题。

3.无同步方案

.(1)如果一个方法本来就不涉及共享数据,那它自然就无须任何同步措施去保证正确性。

(2)可重入代码 
在代码执行的任何时刻中断它,转而去执行另一段代码,在控制权返回后,原来的程序不会出现任何错误

  • 不依赖存储在堆上的数据和公用的系统资源,用到的状态量都由参数传入、不调用非可重入方法等
  • 如果一个方法,它的返回结果是可以预测的,只要输入了相同的数据,就都可能返回相同的结果

(3)线程本地存储

  • 把共享数据的可见范围控制在同一个线程之内,这样无须同步也能保证线程之间不出现数据争用问题
  • 消费队列的架构模式
  • ThreadLocal类
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值