写点什么

还在死磕 Java 多线程漏洞?看完阿里 p8 大牛分享的笔记,分分钟解决

发布于: 2021 年 05 月 17 日
还在死磕Java多线程漏洞?看完阿里p8大牛分享的笔记,分分钟解决

今日分享开始啦,请大家多多指教~

今天给大家分享一下解决线程不安全方案,正文开始啦~

线程不安全的原因

①CPU 抢占 执行(万恶之源)

无法解决

②代码非原子性

在关键代码处,让使用的 CPU 排队执行(加锁)

③(内存)不可见

可使用 volatile 关键字

④编译器/代码优化(指令重排序)

可使用 volatile 关键字

⑤多个线程同时修改了同一个变量

不通用,修改难度大

volatile 关键字

volatile 关键字 轻量级解决线程不安全的方案代码示例如下:


该代码执行结果为:

我们发现,就是在定义全局变量 flag 时,添加了 volatile 关键字,通过解决内存不可见的方法,解决了线程不安全的问题。

volatile 作用:

①禁止指令重排序

②解决线程可见性的问题,实现原理:当操作完变量之后,强制删除掉线程工作内存中的此变量。

注意:

volatile 关键字,无法解决多线程非原子性问题。


代码执行结果:

可见,volatile 关键字,无法解决多线程非原子性问题,进而无法解决线程非安全。

锁操作

Java 中解决线程安全操作(锁的操作)

1.使用 synchronized 关键字来加锁和释放锁【JVM 层面的解决方案,自动帮我们进行加锁和释放锁的操作】

2.Lock 手动锁【Java 层面的解决方案,需要程序员自己去加锁和释放锁】

公平锁与非公平锁

公平锁可以按顺序进行执行,而非公平锁执行的效率更高。在 Java 中所有锁默认的策略都是非公平锁。

synchronized 的锁机制是非公平锁。

Lock 默认的锁策略也是非公平锁,但是 Lock 也可以声明为公平锁。

锁操作的关键步骤

1.尝试获取(如果成功拿到锁,加锁,进行排队等待)

2.释放锁

synchronized 的使用

synchronized 的底层是使用操作系统的 mutex lock 实现的。

1.当线程释放锁时,JMM 会把该线程对应的工作内存中的共享变量刷新到主内存中

2.当线程获取锁时,JMM 会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须从主内存中读取共享变量

synchronized 用的锁是存在 Java 对象头里的。

synchronized 同步快对同一条线程来说是可重入的,不会出现自己把自己锁死的问题;

同步块在已进入的线程执行完之前,会阻塞后面其他线程的进入。

程序的关键操作加锁,示例代码如下:


注意事项:在进行加锁操作的时候,同一组业务必须为共同的锁对象。该代码的执行结果如下:

我们发现,此程序就是线程安全的。

synchronized 实现原理:1.操作:互斥锁 mutex

2.JVM:帮我们实现的监视器锁的加锁和释放锁的操作

3.Java:a) 锁对象 mutexb) 锁存放的地方:变量的对象头

synchronized 在 JDK 6 之前,使用重量级锁实现的,性能非常低,所以用到的并不多。JDK 6 对 synchronized 做了优化(锁升级 )

synchronized 的使用场景:1.使用 synchronized 来修饰代码块 (加锁对象可以自定义)上述 ThreadDemo31 就是 synchronized 来修饰代码块的使用场景

2.使用 synchronized 来修饰静态方法,示例如下:


该代码的执行结果:

我们发现,使用 synchronized 来修饰静态方法,也能使该程序线程安全。

3.使用 synchronized 可以用来修饰普通方法(加锁对象是当前类的实例),示例如下:


该代码的执行结果:

我们发现,使用 synchronized 来修饰普通方法,也能使该程序线程安全。

Lock 的使用

Lock 的使用,示例代码如下:



注意事项:一定要把 lock( ) 放在 try 外面,原因如下:

1.如果将 1ock()方法放在 try 里面,那么当 try 里面的代码出现异常之后,那么就会执行 finally 里面的释放锁的代码,但这个时候加锁还没成功,就去释放锁。

2.如果将 lock( ) 方法放在 try 里面,那么当执行 finally 里面释放锁的代码的时候就会报错(线程状态异常),释放锁的异常会覆盖掉业务代码的异常报错,从而增加了排除错误成本。

演示:将 lock( ) 方法放入 try 里面,示例代码如下:

该代码的执行结果如下:

我们发现,该程序执行时会报错,且异常类型为锁操作异常,并非业务异常信息。如果将 lock()方法放在 try 的外面,示例代码如下:

该代码的执行结果如下:

我们发现:异常类型为我们预期的业务异常类型。

Lock 声明公平锁示例代码如下:


该代码执行结果为:

Lock 的使用场景:只能用来修饰代码块。

volatile 和 synchronized 有什么区别?

  1. volatile 可以解决内存可见性问题和禁止指令重排序,但 volatile 不能解决原子性问题;

  2. synchronized 可以解决任何关于线程安全的问题(关键代码排队执行,始终只有一个线程会执行加锁操作;原子性问题…)

synchronized 和 Lock 有什么区别?

  1. synchronized 既可以修饰代码块,又可以修饰静态方法和普通方法;而 Lock 只能修饰代码块。

  2. synchronized 只有非公平锁的锁策略,而 Lock(ReentrantLock) 默认也是非公平锁策略,也可以通过构造函数声明成公平锁。

  3. 使用 Lock(ReentrantLock) 更加灵活(比如 tryLock )。

  4. synchronized 是自动加锁释放锁的,而 Lock(ReentrantLock) 需要程序员来加锁和手动释放锁的。

死锁问题

基本概念

线程和锁的关系(一对多):一个线程可以拥有多把锁;但是一个锁只能被一个线程拥有。

定义:在多线程编程中(两个或两个以上的线程),因为资源抢占,造成线程无限等待的问题。

死锁问题,示例代码:


该代码的执行结果如下:

我们发现,线程进入的无限等待状态从而无法使程序执行完毕。

排查死锁工具

1.使用 Java 监控和管理控制台(jconsole)可以检测出,该线程出现了死锁问题。

2.使用 jvisualvm 工具可以检测出,该线程出现了死锁问题。


3.使用 jmc 工具可以检测出,该线程出现了死锁问题。

死锁程序的三个关键点

1.获得锁 A 操作 2.线程休眠 3.获得锁 B 操作

死锁操作的四个条件(同时满足)

  1. 互斥条件:一个资源只能被一个线程持有,当被一个线程持有之后就不能被其他线程持有。

  2. 请求拥有条件:一个线程持有了一个资源之后,又试图请求另一个资源。

  3. 不可剥夺条件:一个资源被一个线程拥有之后,如果这个线程不释放此资源,其他线程不能尝试获得此资源。

  4. 环路等待条件:多个线程在获取资源时,形成了环形链。

解决死锁问题

上面造成死锁的四个条件中,互斥条件与不可剥夺条件无法修改,只能从请求拥有条件和环路等待条件入手。从以下条件入手,修改任意一个条件即可:1.请求拥有条件 2.环路等待条件其中,最容易实现的方法就是修改环路等待条件。

我们可以修改 控制请求锁的有序性,如图:

通过修改 控制请求锁的有序性 即让线程 1 和线程 2 都先请求锁 A,再让线程 1 和线程 2 再去请求锁 B。我们修改 ThreadDemo36 的部分代码,即可解决死锁问题,代码如下:


该代码的执行结果如下:

我们发现,死锁问题得到解决。

线程等待

基本概念

之前在学习线程休眠 Thread.sleep() 的时候,这个方法有一个弊端:必须有明确的结束时间,在休眠期间无法唤醒。为了解决这个问题,Java 提供了 wait(休眠)/ notify(唤醒)/ notifyall(唤醒全部) 机制

线程通讯机制:一个线程的动作可以让另一个线程感知到就叫做线程通讯。

示例代码如下:


该代码的执行结果如下:

wait 为什么要加锁:

wait 在使用的时候,必须要释放锁,在释放锁之前,必须要有一把锁,所以要加锁。

wait 为什么要释放锁:

wait 默认是不传任何值的,当不传递任何值的时候,表示永久等待,这样就会造成一把锁被一个线程一直持有,为了这个问题的发生,所以在使用 wait 时,一定要释放锁。

wait / notify /notifyAll 使用注意事项

  1. 使用以上方法时,必须要加锁。

  2. 加锁对象和 wait / notify /notifyAll 的对象必须保持一致。

  3. 一组 wait 和 notify /notify 必须是同一个对象。

  4. notifyAll 只能唤醒当前对象的所有等待线程。

wait 传参示例代码如下:


该代码的执行结果如下:

我们发现该线程等待 3 秒会自动唤醒。

notifyAll 的使用,示例代码如下:


该代码的执行结果如下:

Thread.sleep(0) 和 Object.wait(0) 的区别:

1.sleep 是 Thread 的静态方法;而 lock 是 Object 的方法。

2.sleep(0) 立即触发一次 CPU 资源的抢占;而 lock(0) 会让线程永久等待下去。

wait 和 sleep 的异同

相同点:

  1. 两者都可以使当前的线程休眠。

  2. 两者都要处理一个 Interrupt 的异常。

不同点:

  1. wait 来自于 Object 中的一个方法;而 sleep 来自于 Thread 中的一个静态方法。

  2. 传递参数不同:wait 可以没有参数;而 sleep 必须有一个大于等于 0 的参数。

  3. wait 使用时,必须加锁;而 sleep 使用时,不用加锁。

  4. wait 使用时,会释放锁;而 sleep 使用时,不会释放锁。

  5. 不传参的情况下 wait 会进入 WAITING 状态;而 sleep 会进入 TIMED_WAITING 状态。

为什么 wait 释放锁;而 sleep 不释放锁?

答:wait 默认等待无限期。

为什么 wait 要放在 Object 中而不是 Thread 中?

答:wait 操作必须要加锁和释放锁,而锁属于对象级别,而非线程级别(线程和锁是一对多的关系,也就是一个线程可以有多把锁),为了灵活起见(一个线程会有多把锁),就把 wait 放在了 Object 中。

LockSupport 的使用

LockSupport.park();同样会使线程进入 WAITING 状态,示例代码如下:

代码执行时,使用 jconsole 工具可以观察线程状态,如下:

使用 LockSupport.unpark(线程名);唤醒线程,示例代码如下:

该代码的执行结果如下:

使用 LockSupport.unpark(线程名);唤醒线程,可以指定唤醒线程顺序,示例代码如下:


该代码的执行结果如下:

使用 LockSupport.park(参数);传参,示例代码如下:

该代码的执行结果如下:

我们发现,使用 LockSupport.park(参数);传参,线程是可以自动唤醒的。

wait 和 LockSupport 异同

相同点:

1.两者都可以使线程休眠。

2.两者都可以无参或者传递参数,并且两者的线程状态也是一致的。

不同点:

1.wait 必须要配合 synchronized 一起使用(必须加锁),而且 wait LockSupport 不许加锁。

2.wait 只能唤醒全部或随机的一个线程,而 LockSupport 可以按顺序唤醒指定线程。

今日份分享已结束,请大家多多包涵和指点!

用户头像

还未添加个人签名 2021.04.20 加入

Java工具与相关资料获取等WX: pfx950924(备注来源)

评论

发布
暂无评论
还在死磕Java多线程漏洞?看完阿里p8大牛分享的笔记,分分钟解决