(十一)线程安全与锁优化

线程安全与锁优化

本章主要内容:

  1. 线程安全所涉及的概念和分类
  2. 同步实现的方法及虚拟机的底层的运作与阿里
  3. 虚拟机为了实现高效并发所采取的一系列锁优化措施

线程安全

面向过程:会把数据和过程看成两个独立的部分分别处理,数据表示问题空间的客体,过程用于处理这些数据
面向对象:数据和行为都是对象的一部分,更符合现实世界的思维方式

什么是线程安全?

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

JAVA中线程安全的5个级别

讨论线程安全,就限定于多个线程之间存在共享数据访问这个前提,因为如果一段代码根本不会与其他线程共享数据,那么从线程安全的角度来看,程序是串行执行还是多线程执行对它来说是完全没有区别的。我们将Java语言中各种操作共享的数据分为以下5类:不可变、绝对线程安全、相对线程安全、线程兼容和线程独立。

  1. 不可变:在Java语言中,不可变的对象一定是线程安全的。基本数据类型采用final关键字修饰,如果是对象则需要保证自己的行为不会影响状态,例如String的replace()等方法都是产生新的对象。
    例如,JDK中Integer类的构造函数:
1
2
3
4
5
6
7
8
9
10
11
12
private final int value;  

/**
* Constructs a newly allocated {@code Integer} object that
* represents the specified {@code int} value.
*
* @param value the value to be represented by the
* {@code Integer} object.
*/
public Integer(int value) {
this.value = value;
}
  1. 绝对线程安全:绝对的线程安全就是前面的提到的定义,这个定义很严格,一个类要达到「不管运行时环境如何,调用者都不需要任何额外的同步措施」通常需要付出很大的,甚至有时候是不切实际的代价。在Java API中标注自己是线程安全的类,大多数都不是绝对的线程安全。
    例如Vector,加入同步以保证Vector访问的线程安全性:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Thread removeThread = new Thread(new Runnable() {  
@Override
public void run() {
synchronized(vector) {
for (int i = 0; i < vector.size(); i++) {
vector.remove(i);
}
}
}
});

Thread printThread = new Thread(new Runnable() {
@Override
public void run() {
synchronized(vector) {
for (int i = 0; i < vector.size(); i++) {
System.out.println(vector.get(i));
}
}
}
});
  1. 相对的线程安全:就是我们通常意义上所讲的线程安全,它需要保证对这个对象单独的操作是线程安全的,我们在调用的时候不需要做额外保障措施。在Java语言中,大部分的线程安全类都属于这种类型,例如Vector、HashTable等。
  2. 线程兼容:线程兼容是指对象本身并不是线程安全的,但是可以通过调用端正确地使用同步手段来保证对象在并发环境中可以安全的使用,我们平常说一个类不是线程安全的,绝大多数时候指的是这一种情况。
  3. 线程对立:指无论调用端是否采取了同步措施,都无法在多线程环境中并发使用的代码。一个线程对立的例子是Thread类的suspend()和resume()方法,如果并发进行的话,无论调用时是否进行了同步,目标线程都是存在死锁风险的,如果suspend()中断的线程就是即将要执行resume()的那个线程,那肯定就要产生死锁了。

如何实现线程安全?

  1. 互斥同步:互斥是因,同步是果;互斥是方法,同步是目的。
  2. 非阻塞同步:解决互斥同步进行线程阻塞和唤醒所带来的性能问题
  3. 无同步方案:如果一个方法本来就不涉及共享数据,那它就无须任何同步措施去保证正确性。

可重入锁是什么意思?

线程可以进入任何一个它已经拥有的锁所同步着的代码块。意味着可以进行递归

ReentrantLock和Synchronized区别?

synchronized是原生语法层面的实现。ReentrantLock是API层面,使用lock()和unlock()方法配合try/finally语句块来实现。

ReentrantLock有三个高级特性:

  1. 等待可中断:当持有锁的线程长期不释放锁时,正在等待的线程可以选择放弃等待,改为处理其他事情。可中断特性对处理执行时间非常长的同步块很有帮助。
  2. 可实现公平锁:公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁;而非公平锁则不保证这一点,在锁被释放时,任何一个等待锁的线程都有机会获得锁。
  3. 锁可以绑定多个条件:一个ReentrantLock对象可以同时绑定多个Condition对象,而在synchronized中,锁对象的wait()和notify()或notifyAll()方法可以实现一个隐含的条件,如果要和多于一个的条件关联的时候,就不得不额外地添加一个锁,而ReentrantLock则无须这样做,只需要多次调用newCondition()方法即可。

如何理解阻塞同步?

通俗理解:到我的工作时间,却因为拿不到资源而无法工作,只能转让别人,而这个转让的手续又比较复杂。

什么是悲观和乐观?

悲观并发策略:认为只要不作同步,就一定会出现问题
乐观并发策略:认为一般情况下都没有数据竞争,可以不作同步,若真发生共享数据争用,再采取补偿措施

为什么乐观策略需要“硬件指令集的发展”?

乐观策略需要在更新时判断是否发生数据冲突,这两步操作需要合成一个原子指令(称为CAS操作),这需要硬件指令集的支持

当然也可以用互斥同步,但是这样就失去了乐观策略的意义

CAS的“ABA”问题是什么?如何解决?

如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然为A值,那我们就能说它的值没有被其他线程改变过了吗?如果在这段期间它的值曾经被改成了B,后来又被改回为A,那CAS操作就会误认为它从来没有被改变过。

如何判断一个方法是可重入的?

返回结果是可预测的,只要输入相同,返回也就相同

锁优化

自旋锁解决什么问题?如何工作?

线程阻塞的时候,让等待的线程不放弃cpu执行时间,而是执行一个自旋(一般是空循环),这叫做自旋锁。

自旋等待本身虽然避免了线程切换的开销,但它是要占用处理器时间的,因此,如果锁被占用的时间很短,自旋等待的效果就非常好,反之,如果锁被占用的时间很长,那么自旋的线程只会白白消耗处理器资源,带来性能上的浪费。因此,自旋等待的时间必须要有一定的限度。

锁消除解决什么问题?如何工作?

虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。一般根据逃逸分析的数据支持来作为判定依据。

锁粗化解决什么问题?如何工作?

原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小——只在共享数据的实际作用域中才进行同步,这样是为了使需要同步的操作数量尽可能变小,如果存在锁竞争,那等待锁的线程也能尽快拿到锁。

但如果一系列操作频繁对同一个对象加锁解锁,或者加锁操作再循环体内,会耗费性能,这时虚拟机会扩大加锁范围。

轻量级锁和偏向锁解决什么问题?如何工作?

轻量级锁是JDK 1.6之中加入的新型锁机制。它的作用是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。
偏向锁也是JDK1.6中引入的锁优化,它的目的是消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能。如果说轻量级锁是在无竞争的情况下使用CAS操作去消除同步使用的互斥量,那偏向锁就是在无竞争的情况下把整个同步都消除掉,连CAS操作都不做了。

mark word

锁的状态转换

本文标题:(十一)线程安全与锁优化

文章作者:Sun

发布时间:2019年12月24日 - 11:12

最后更新:2019年12月24日 - 11:12

原始链接:https://sunyi720.github.io/2019/12/24/JVM/(十一)线程安全与锁优化/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。