线程的安全问题

线程的安全问题

原因:操作共享数据

临界区

  • 一个程序运行多个线程本身是没有问题的
  • 问题出在多个线程访问共享资源
    • 多个线程读共享单元也没有问题
    • 在多个线程对共享资源读写操作是发生指令交错,就会出现问题
  • 一段代码块内如果存在对共享资源的多线程读写操作,

解决方法()

  • 阻塞式的解决方案:synchronized,Lock
  • 非阻塞式的解决方案原子变量

阻塞式的解决方法

synchronized实际上是使用对象锁保证了临界区内代码的原子性,代码区内的代码是不可分割的,不会被线程切换打断

1
2
3
synchronized(Object){

}

方法一:同步代码块

synchronized(同步监视器){}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class MyThread extends Thread{
private int ticket = 100;
public void run() {
synchronized (MyThread.class){
if(ticket > 0 ){
ticket--;
}
}
}
}
class MyThread2 implements Runnable{
private int ticket = 100;
public void run() {
synchronized (this){
if(ticket > 0 ){
ticket--;
}
}
}
}
  1. 操作共享数据的代码,即为需要被同步的代码
  2. 共享数据:多个线程共同操作的变量
  3. 同步监视器,俗称:锁,任何一个对象都可以充当锁。要求是每个人都要共用同一把锁
  4. 不能包含少了,也不能包含多了

优点:

  • 解决了线程的安全性问题

局限性:

  • 相当于一个单线程的事,效率低

方法二:同步方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class MyThread3 extends Thread{
private static int ticket = 100;
public void run() {
show();
}
public static synchronized void show() {

if(ticket > 0 ){
ticket--;
}
}
}
class MyThread4 implements Runnable{
private int ticket = 100;
public synchronized void run() {

if(ticket > 0 ){
ticket--;
}
}
}

总结:

  1. 同步方法依然涉及到同步监视器,只是不需要显式的声明
  2. 继承使用的是当前类,实现使用的是当前对象

线程八锁

情况一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Test8Lock {
public static void main(String[] args) {
Number number = new Number();
new Thread(number::a).start();
new Thread(number::b).start();
}
}

@Slf4j
class Number {
public synchronized void a(){ //this
log.debug("a");
}
public synchronized void b(){
log.debug("b");
}
}

结果是 a b或者 b a

情况二

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Test8Lock {
public static void main(String[] args) {
Number number = new Number();
new Thread(number::a).start();
new Thread(number::b).start();
}
}

@Slf4j
class Number {
public synchronized void a(){
Thread.sleep(1000);
log.debug("a");
}
public synchronized void b(){
log.debug("b");
}
}

结果是 一秒后 a b或者 b 一秒后 a

情况三

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Test8Lock {
public static void main(String[] args) {
Number number = new Number();
new Thread(number::a).start();
new Thread(number::b).start();
new Thread(number::c).start();
}
}

@Slf4j
class Number {
public synchronized void a(){
Thread.sleep(1000);
log.debug("a");
}

public synchronized void b(){
log.debug("b");
}

public void c(){
log.debug("c");
}
}

结果是 一秒后 a b或者 b 一秒后 a,c在任何时候都可能打印

情况四

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Test8Lock {
public static void main(String[] args) {
Number number = new Number();
Number number1 = new Number();
new Thread(number::a).start();
new Thread(number1::b).start();
}
}

@Slf4j
class Number {
public synchronized void a(){
Thread.sleep(1000);
log.debug("a");
}
public synchronized void b(){
log.debug("b");
}
}

结果是 b 一秒后 a

情况五

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Test8Lock {
public static void main(String[] args) {
Number number = new Number();
new Thread(Number::a).start();
new Thread(number::b).start();
}
}

@Slf4j
class Number {
public static synchronized void a(){ //静态方法的锁是Class类
Thread.sleep(1000);
log.debug("a");
}
public synchronized void b(){
log.debug("b");
}
}

结果是 b 一秒后 a

情况六

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Test8Lock {
public static void main(String[] args) {
Number number = new Number();
new Thread(number::a).start();
new Thread(number::b).start();
}
}

@Slf4j
class Number {
public static synchronized void a(){
sleep(1000);
log.debug("a");
}
public static synchronized void b(){
log.debug("b");
}
}

结果是 一秒后 a b或者 b 一秒后 a

情况七

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Test8Lock {
public static void main(String[] args) {
Number number = new Number();
new Thread(Number::a).start();
new Thread(number::b).start();
}
}

@Slf4j
class Number {
public static synchronized void a(){
sleep(1000);
log.debug("a");
}
public synchronized void b(){
log.debug("b");
}
}

结果是结果是 b 一秒后 a

情况八

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Test8Lock {
public static void main(String[] args) {
Number number = new Number();
Number number1 = new Number();
new Thread(Number::a).start();
new Thread(number::b).start();
}
}

@Slf4j
class Number {
public static synchronized void a(){
sleep(1000);
log.debug("a");
}
public synchronized void b(){
log.debug("b");
}
}

结果是结果是 b 一秒后 a

变量的线程安全分析

成员变量和静态变量是否线程安全

  • 如果没有共享,则线程安全
  • 如果他们被共享了,则根据他们的状态是否能够被改变分为两种情况
    • 如果只有读操作,则线程安全
    • 如果有读写操作,则这段代码是临界区,需要考虑线程安全

局部变量是线程安全

  • 局部变量是线程安全的
  • 但局部变量引用的对象未必
    • 如果对象没有逃离方法的作用访问,它是线程安全的
    • 如果该对象逃离方法的作用范围,需要考虑线程安全

Monitor概念

Java对象头

以32位虚拟机为例

普通对象

image-20201103104533467

数组对象

image-20201103110532794

Mark words的组成部分

synchronized原理

synchronized原理
  • 对象obj管理操作系统中一个monitor。
  • 当Thread1试图进入synchronized代码块时,会从Object的MarkWord找到该对象对应的monitor,并将monitor的owner指向该线程。
  • 在Thread1上锁过程中,其他Thread看到monitor有主人,则进入等待队列。
  • Thread1执行完后,会进行非公平的竞争锁。
  • 图中waitset是获得过锁但是条件不满足进入waiting状态的线程。

synchronized进阶

轻量级锁

轻量级锁的使用场景:如果一个对象虽然有多线程访问,但所线程访问的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来进行优化。

轻量级锁对于使用者是透明的,但是语法仍然是synchronized。

假设有两个方法同步方法块,利用同一个对象加锁。

  • 创建锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录的结构,内部可以储存锁定对象的Mark word: JVM层面
synchronized进阶1
  • 让锁记录中的Object指向锁对象,并尝试使用CAS替换Object的Mark Word,将Mark Word的值存入锁记录。
synchronized进阶-第 2页
  • 如果CAS替换成功,对象头中存储了锁记录地址和状态00,表示由该线程给对象加锁,这时图示如下:
synchronized进阶-第 3页
  • 如果CAS失败,有两种情况
    • 如果是其他线程已经持有了该Object的轻量级锁,这时表示由竞争,进入锁膨胀过程
    • 如果是自己执行了synchronized锁重入,那么再添加一条Lock Record作为重入的计数
synchronized进阶-第 4页
  • 当退出synchronized代码块(解锁时)如果有取值为null的锁记录,表示有重入,这时重置锁记录,表示重入计数器减一
synchronized进阶-第 3页
  • 当退出synchronized代码块锁记录不为null时,这时使用CAS将Mark Word的值恢复给对象头
    • 成功则解锁成功
    • 失败说明轻量级锁已经进行了锁膨胀,或者升级成为了重量级锁,进入重量级锁的解锁流程。

锁膨胀

如果在尝试加轻量锁的过程中,CAS操作无法成功,这时一种情况就是有其他对象为该对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。

1
2
3
4
5
6
7
static Object obj = new Object();
public static void method1() {
synchronized( obj ) {
//同步块
}
}

  • 当新Thread进行轻量级加锁时,Thread 1 已经对该对象进行了轻量级锁
synchronized进阶-第5页
  • 这时新Thread加轻量级锁失败进入锁膨胀流程
    • 即为Object对象申请Monitor锁,让Object指向重量级锁地址
    • 然后自己进入Monitor的EntryList的BLOCKED
synchronized进阶-第6页

自旋优化

重量级锁进行竞争时,还可以使用自选来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞

自旋重试成功的情况

线程1 ( CPU1上 ) 对象 Mark 线程2 ( CPU2上 )
- 01 (无锁) -
访问同步块,获取Monitor 10 (重量锁) -
成功 (加锁) 10 (重量锁) -
执行同步块 10 (重量锁) -
执行同步块 10 (重量锁) 访问同步块,获取Monitor
执行同步块 10 (重量锁) 自旋重试
执行完毕 10 (重量锁) 自旋重试
成功 (解锁) 01 (无锁) 自旋重试
- 10 (重量锁) 成功 (加锁)
- 10 (重量锁) 执行同步块

自旋重试失败的情况:

线程1 ( CPU1上 ) 对象 Mark 线程2 ( CPU2上 )
- 01 (无锁) -
访问同步块,获取Monitor 10 (重量锁) -
成功 (加锁) 10 (重量锁) -
执行同步块 10 (重量锁) -
执行同步块 10 (重量锁) 访问同步块,获取Monitor
执行同步块 10 (重量锁) 自旋重试
执行同步块 10 (重量锁) 自旋重试
执行同步块 10 (重量锁) 自旋重试
执行同步块 10 (重量锁) 自旋重试
执行同步块 10 (重量锁) 阻塞

偏向锁

轻量级锁在没有竞争的时候,每次重入仍然需要执行CAS操作。

Java6中引入了偏向锁来做进一步的优化:只有第一次使用CAS将线程ID设置到对象的MarkWord头,之后发现这个线程ID是自己的就表示没有竞争,不用重新CAS。以后只要不发生竞争,这个对象就归该线程所有。

偏向状态

一个对象创建时

  • 如果开启了偏向锁(默认开启),那么对象创建后,MarkWord最后0x05即最后三位为101,这时它的thread,epoch,age都为0
  • 偏向锁是默认延迟加载的,不会再程序启动时立即生效,如果想要避免延迟,可以加VM参数 -xx:BiasedLockingStartupDelay=0来禁用延迟
  • 如果没有开启偏向锁,那么对象创建后,MarkWord最后0x05即最后三位为001,这时它的hashcode,age都为0,hashcode在第一次获取时修改。

撤销偏向锁

调用了对象的Hashcode,但是偏向锁的对象的MarkWord中存储的是线程id,如果调用Hashcode会导致偏向锁被撤销。

  • 调用hashcode会使得对象不可偏向,从101变成001.
    • 轻量锁会在锁记录中记录Hashcode
    • 重量级锁会在Monitor中记录Hashcode
  • 当有其他线程使用锁对象时,会将偏向锁升级成为轻量级锁
  • 调用wait、notify会升级偏向锁

批量冲偏向

批量撤销

当撤销偏向锁阈值超过40次之后,jvm会觉得自己确实偏向错了,根本就不该偏向,于是整个类的所有对象都会变为不可偏向,新建的对象也是不可偏向的。

锁消除

wait notify

synchronized原理
  • Owner 线程发现条件不满足,调用wait方法,即可进入WaitSet变成WAITING状态
  • BLOCKED和WAITING的线程都处于阻塞状态,不占用CPU时间片
  • BLOCKED线程会在Owner线程释放是唤醒
  • WAITING线程会在Owner调用notify或notifyAll时唤醒,但唤醒后并不意味着立刻获得所,仍需要进入EntryList重新竞争

API介绍

obj.wait()让进入Object监视器的线程到waitSet等待,会释放锁

obj.notify()在Object上正在waitset中等待的线程中挑一个唤醒

obj.notifyAll()在Object上正在waitset中等待的线程全部唤醒

他们都是在线程之间进行协作的手段,都属于Object对象的方法,必须获取此对象的锁,才能调用这几个方法

上述的方法必须使用在同步代码块或者同步方法之中。

上述的方法的调用者必须是同步代码块或同步方法中的同步监视器。否则会出现异常

上述的方法定义在Object类中

obj.wait(long timeout)让进入Object监视器的线程到waitSet等待timeout时间

obj.wait(long timeout, int nacos)让进入Object监视器的线程到waitSet等待timeout + 1时间

wait notify的正确使用姿势

Thread.sleep(long timeout)obj.wait(long timeout)的区别

  1. Thread.sleep(long timeout)是Thread的静态方法,obj.wait(long timeout)是object的方法。
  2. Thread.sleep(long timeout)不需要和synchronized配合使用,但是 obj.wait(long timeout)需要。
  3. Thread.sleep(long timeout)不会释放锁,obj.wait(long timeout)会释放锁。
  4. 状态都是 TIMED_WAITING

保护性暂停

TODO

锁的应用

应用在单例模式

懒汉式的线程安全问题

  1. 效率较差
1
2
3
4
5
6
7
8
9
10
11
12
13
class Bank{
private Bank(){}

private static Bank instance = null;

public static synchronized Bank getInstance() {

if(instance == null){
instance = new Bank();
}
return instance;
}
}
  1. 效率高一些
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Bank1{
private Bank1(){}

private static Bank1 instance = null;

public static Bank1 getInstance() {
if(instance == null){
synchronized (Bank.class){
if(instance == null){
instance = new Bank1();
}
}
}
return instance;
}
}

死锁

  1. 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方先放弃,这就形成了线程的死锁
  2. 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞当中,无法继续

解决方法:

  • 专门的算法、原则
  • 尽量 减少同步资源的定义
  • 避免嵌套同步
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public class DeadLock {
public static void main(String[] args) {
StringBuffer s1 = new StringBuffer();
StringBuffer s2 = new StringBuffer();

new Thread(){
@Override
public void run() {
synchronized (s1) {

try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
s1.append("a");
s2.append("1");
synchronized (s2) {
s1.append("b");
s2.append("2");
}
}
}
}.start();



new Thread(new Runnable() {
@Override
public void run() {
synchronized (s2) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
s1.append("c");
s2.append("3");
synchronized (s1) {
s1.append("d");
s2.append("4");
}
}
}
}).start();
System.out.println(s1);
System.out.println(s2);
}
}

方法三:lock锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Windows implements Runnable{
private int ticket = 100;
private ReentrantLock lock = new ReentrantLock();

@Override
public void run() {
while (true){
try {
lock.lock();
if(ticket > 0){
Thread.sleep(100);
System.out.println(ticket);
ticket--;
}else{
break;
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
}


public class LockTest {
public static void main(String[] args) {
Windows windows = new Windows();
new Thread(windows).start();
new Thread(windows).start();
}
}

synchronized和lock锁的异同:

相同点:都能解决线程安全问题

不同点:lock锁手动的锁定和手动的解锁,synchronized自动释放

JDK5.0新增的线程创建接口

新增方式二线程池

背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大

思路:提前创建好多个线程,放入线程池中,使用时使用时直接获取,使用结束后放回。可以避免重复创建和销毁,实现重复利用,类似于生活中的公共交通。

主要特点是:线程复用;控制最大并发数;管理线程

优点:

  • 提高响应的速度
  • 降低了资源的消耗
  • 便于线程的管理

线程池相关的API的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class NumThread implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(i);
}
}
}


public class ThreadPool {
public static void main(String[] args) {
//提供线程池
ExecutorService service = Executors.newFixedThreadPool(10);
service.execute(new NumThread());//使用Runnable接口
service.shutdown();//关闭连接池
// service.submit();

}
}