深入理解jvm 一 线程安全

线程安全:

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

java语言中得各种操作共享数据可以分成五类

1、不可变:不可变的对象一定是线程安全的,无论对象的方法实现还是方法的调用者都不需要在采取安全措施。如果共享数据是一个基本数据类型,那么只要在定义的时候使用final关键字修饰就可以保证它不可变。例如java.lang.String类的对象,他就是一个典型的不可变对象,不管调用substring、replace、还是concat方法都不会影响它的值,只会返回一个新构造的字符串对象。还有枚举类以及Number的部分子类如Long和double

2、绝对线程安全:绝对线程安全是不管运行是环境如何,调用者都不需要任何额外的同步措施。通常需要付出很大的甚至不切实际的代价。例如Vector是一个线程安全的容器,因为他的add get 和size方法都是被synchronize修饰的,但是他并不意味着调用它的时候永远都不在需要同步手段了在多线程环境如果不做额外的措施,一个线程在错误的时间删除一个元素,导致序号i已经不再可用,在访问i数组就会抛异常。

3、相对线程安全:就是通常意义的线程安全,确保这个对象单独的操作是操作安全的。

4、线程兼容:指的是本身对象并不是线程安全的,但是可以通过调用端的正确使用同步手段来保证对象的安全使用。

5、线程对立:无论调用端是否采取了同步措施,都无法在多线程环境中并发使用代码。


线程安全的实现方法

1、互斥同步:一种常见的并发同步保障手段,同步指的是在多个线程并发访问共享数据时,保证共享数据在同一个时刻只能被一个线程使用。互斥指的是实现同步的 一种手段,临界区互斥量和信号量都是主要的互斥实现方式。

在java中最基本的互斥同步手段就是synchronized关键字,它经过编译会形成monitorenter和monitorexit两个字节码指令,他们都需要一个reference类型参数来知名要锁定和解锁的对象。如果是明确的对象参数就是这个对象的reference,如果没有明确指出,那就是根据synchronized修饰的是实例方法还是类方法,去取对应的对象实例或class对象来作为锁对象。

monitorenter执行前首先要获取对象的锁,如果这个对象没有被锁或者当前线程已经拥有了那个对象的锁,就要把锁的计数器加1

monitorenter和monitorexit会分别将锁计时器加1和减1.一旦计数器为0时,锁就会被释放,如果获取对象锁失败,那么当前线程就会被阻塞等待,知道对象锁被另外一个线程释放为止。

synchronized同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题,其次同步块在已进入的线程执行完之前会阻塞后面其他线程的进入。但是synchronized是个重量级操作,只有在切实有必要的情况下才会用这种操作。

除了synchronized还可以用java.util.concurrent包中的重入锁ReentrantLock来实现同步,它比synchronized多了一些高级功能:等待可中断,可实现公平锁,以及锁可以绑定多个条件。

等待可中断:当一个有锁的线程长期不释放锁,其他等待的线程可以干别的事情。

公平锁:是指多个线程在等待同一个锁是必须按照申请锁的时间先后顺序来依次获得锁。

绑定多个条件:就是new多个Condition对象。


synchronized性能不如ReentrantLock,但是在后面的jdk版本中性能已经持平了,而且还是更提倡使用更贴近原生的synchronized来进行同步。


2、非阻塞同步:

因为互斥同步的观点是不去做正确的同步措施就会出现问题,因此是一种悲观的并发策略。

随着硬件指令集的发展,我们有了新的选择,基于冲突检测的乐观并发策略。通俗的讲就是先进行操作,如果没有其他线程征用共享数据,那操作就成功了,如果有共享数据争用,就产生了冲突那就采取其他的补偿措施,最常见的就是不断地重试直到成功为止。

CAS指令有三个操作数,一个是内存位置,一个是旧的预期值,另一个是新值。当地址位置的值符合旧的预期值,就会用新值替换变量的内存地址,否则就不执行更新。但是无论更新与否都会返回内存地址的旧值,这就是一个原子操作。

但是会存在这么一个问题,如果一个变量值是A,正在准备赋值的时候读取到它的值是A,但是另一个线程突然改变了这个值变成了B,之后又改回了A,那CAS操作就会误会这个类没有被改变过。

3、无同步方案:

有一些代码天生就是线程安全的,主要有两类:可重入代码和线程本地存储。

可重入代码的意思一个正在执行的代码中断,转而去执行另外一段代码,,执行完毕后返回接着执行之前的代码,原来的程序不会出现任何错误,这就是可重入的代码,他可以保证线程安全。如果有一个方法他的返回结果是可以预测的,只要输入了相同的数据都能返回相同的结果,那么他就是可以满足重入性的要求,当然也就是线程安全的。

线程本地存储就是一段代码中所需要的数据必须与其他的代码共享,那就看看这些共享数据的代码能否保证在同一个线程中执行,如果能保证就可以把共享数据的可见范围限制在同一个线程之内,这样无需同步也能保证线程之间不出现数据征用的问题。大部分使用消费队列的架构模式都会讲产品的消费过程尽量在一个线程内消费完。



猜你喜欢

转载自blog.csdn.net/qq_31615049/article/details/80303501