并发基础知识

zhuyonge 2020-08-01

线程和锁的作用类似铆钉和工字梁在土木工程中的作用。

编写线程安全的代码,核心在于对其状态访问的操作进行管理,特别是对共享的和可变的状态的访问。

共享意味着多个线程同时访问;可变意味着变量的值在其生命周期内可以改变。重点在于控制代码不出现 一些不可控的并发访问。


一个对象是否需要线程安全,主要是取决于它是否需要被多个线程访问。

当有多个线程访问某个状态变量并且其中有一个线程执行写入操作的时候,必须使用同步机制协同这些线程对变量的访问。

同步的方法:synchronized,volatile,Lock以及原子变量。

如果没有合适的方法来进行同步一般有三种方法来修复:

1.在各个线程之间不共享这个变量

2.将状态变量变成一个不可变的变量

3.在访问状态变量的时候使用同步机制

设计线程安全的类时,良好的面向对象技术,不可修改性,以及明晰的不变性规范都能起到一定的帮助作用。


面向对象中的抽象和封装会有时候降低程序的性能


线程安全性:某个类的行为与规范完全一致;当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,主调代码中不需要任何的额外同步或者协同,这个类表现出正确的行为,那么就称这个类是安全的。

无状态对象一定是线程安全的。

无状态对象指:它不包含任何域,也不包含任何对其他类中域的引用。


原子性:一个不可分割的操作

不满足原子性就会出现一个竞态条件的问题:由于不恰当的执行时序而出现不正确的结果。

复合操作:为了保证原子性,“先检查后执行(延迟初始化)”以及“读取——修改——写入(递增)”这种操作必须是一个不可分割的原子性操作。这种操作称之为复合操作。

如果在一个类中有多个类变量,那么原子性就不再简单针对某一个单一的变量,所有在整体的状态都进行维护。

内置锁:Synchronized Block ,以Synchronized作为关键字来修饰的方法就是一种横跨整个方法体的同步代码块,其中该同步代码库的锁就是方法调用所在的对象,静态方法以Class对象锁。

Synchronized (SmartCat){
   //访问或修改由锁保护的共享状态  
}

每个Java对象都可以用做一个实现同步的锁(也叫内置锁或者监视器锁),当线程A(SmartCat)需要进入方法Test的时候,线程A就会获得一个锁,线程B去进入方法Test的时候就会被拒绝,因为线程A正在Test方法中


重入:当某个线程请求一个自己已经获得过锁的方法的时候就会直接进入。

重入意味着锁的操作的颗粒度是“线程”而不是“调用”。重入的一种实现方法就是为锁关联一个获得计数值和一个所有者线程,进入的时候记录下线程以及对应的计数值。


用锁来保护状态:由于锁能使其保护的代码路径以串行的形式来访问,因此可以通过锁来构造一些协议以实现对共享状态的独占访问。只要始终遵守这些协议,就能确保状态的一致性。仅仅将复合操作封装到一个同步代码块中是不够的,如果使用同步来协调对某个变量的访问,那么在访问这个变量的所有位置上都需要使用同步,而且,当使用锁来协调某个变量的访问时,在访问变量的所有位置上都需要使用同一个锁。
对象的内置锁与其装填之间没有内在的关联,当获取与对象关联的锁时,并不能阻止其他线程访问该对象,某个线程在获得对象的锁之后,只能阻止其他线程获得同一个锁。你需要自行构造加锁协议或同步策略来实现对共享状态的安全访问,并且在程序中自始至终地使用它们。

常见的加锁约定是,将所有的可变状态都封装在对象内部,并通过对象的内置锁对所有访问可变状态的代码路径都进行同步,当某一个变量由锁来保护时,意味着每次访问这个变量时都需要首先获得锁,这样就确保同一时刻只有一个线程可以访问这个变量。


对象的共享:要编写正确的并发程序,关键问题在于:在访问共享的可变状态时需要正确的进行管理,同步代码块和同步方法可以确保以原子性或者确定临界区的方法执行,但Synchronized还有一个作用就是内存可见性,我们不仅希望防止某个线程正在使用对象状态而另一个对象进行了改变。

可见性:多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

当线程在没有同步的情况下读取变量时,可能会得到一个失效值,但至少这个值是由之前某个线程设置的值,而不是一个随机值。这种安全性保证也被称为最低安全性。这种安全性使用绝大部分变量,除64位,64位的读写操作是需要被分成两个进行执行的


volatile变量:volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile变量的时候总会返回一个最新的值。常用于保持内存可见性和防止指令重排序

写入volatile变量相当于退出同步代码块,而读取volatile变量就相当于进入同步代码块。

volatile变量的经典用法就是:标识

volatile boolean asleep;
....
    while(!asleep)
        countSomeSheep();

volatile的原理是:

当对volatile变量进行写操作的时候,JVM会向处理器发送一条lock前缀的指令,将这个缓存中的变量回写到系统主存中。但是就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题,所以在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议

缓存一致性协议:每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作的时候,会强制重新从系统内存里把数据读到处理器缓存里。

所以,如果一个变量被volatile所修饰的话,在每次数据变化之后,其值都会被强制刷入主存。而其他处理器的缓存由于遵守了缓存一致性协议,也会把这个变量的值从主存加载到自己的缓存中。这就保证了一个volatile在并发编程中,其值在多个缓存中是可见的。

所以在当且满足以下所有条件的时候才回去使用volatile:

1.对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值

2.该变量不会与其他状态一起纳入不变性条件中

3.在访问变量时不需要加锁

相关推荐