目录
回顾
因为线程安全我们引入了锁,但加锁就不会有线程安全了吗?显然线程安全问题不只这些,加锁解决的是操作不是原子的造成的问题。
造成线程安全的原因
| 1 | 操作不是原子的。 |
| 2 | 多线程同时对一个变量进行修改 |
| 3 | 内容不可见 |
| 4 | 指令重排序 |
| 5 | 调度是随机的(抢占式执行) 根本原因 |
synchronized 带来的问题
sychronized 的两个特性 1.互斥 2.可重入,互斥当拿到锁的线程在操作一个变量的时候排斥其他线程操作这个线程,使其阻塞等待,避免了多线程同时操作同一个变量:可重入同一个线程针对同一个锁对象,可以多次加锁不会触发死锁。那什么是死锁?
死锁(deadlock)
通俗来讲‘死锁’就是锁解不开了。
通过一个代码观察一下,
死锁1
Thread t = new Thread(()-> {
synchronized(locker){
synchronized(locker){
for(int i = 0; i < 50000; i++){
count++;
}
}
}
});

这种死锁还有另一种形式
public class demo_deadlock3 {
private int count = 0;
void func1(){
synchronized(this){
func2();
}
}
void func2(){
func3();
}
void func3(){
func4();
}
void func4(){
synchronized(this){
count++;
}
}
}
这样的两种写法在Java中并不会死锁,因为synchronized 是可重入的, JVM 中在加锁时锁对象会自动记录下自己的拥有者是哪个线程,会记录下线程的 id,会有一个计数器,加一把锁计数加一反之解锁减一。
Object 内存区域的隐藏区域保存对象头

加锁前先判定线程对象头有没有锁,有锁计数器累加,多重锁时解锁从最外层开始解锁,计数器减一,等计数器为0时释放锁。
synchronized(locker){ // 1
synchronized(locker){ // 2
synchronized(locker){ // 3
}
}
} // 最外层 从这开始解锁
死锁2
两个线程分别有一把锁,不解开已有的锁就去尝试拿对方的锁。
这样的结果:线程1 要 线程 2的锁 线程 2 不给
线程2 要 线程 1的锁 线程 1 也不给 都拿不到锁一直竞争一直阻塞
形式如下:
public class demoDeadLock {
private static Object locker1 = new Object();
private static Object locker2 = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(()->{
synchronized(locker1){
System.out.println("t1 获得 locker1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 死锁
synchronized(locker2){
System.out.println("t1 获得 locker2");
}
}
});
Thread t2 = new Thread(()->{
synchronized(locker2){
System.out.println("t2 获得 locker2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//死锁
synchronized(locker1){
System.out.println("t2 获得 locker1");
}
}
});
t1.start();
t2.start();
}
}
结果

t1 正常拿到了 locker1 ,t2 正常拿到了locker2 ,但都没有拿到对方的锁且程序进行不下去了。死锁产生了。
死锁3
循环等待 N个线程 M把锁
哲学家就餐问题
哲学家:只做两件事吃饭,思考,吃饭,思考........循环往复
现在有一个房间有 4 个哲学家和一张桌子,桌子上有 5 根筷子且每根筷子都在哲学家左手边(每两个哲学家之间放有一根筷子)

正常情况下5个哲学家轮流就餐或个别同时就餐。
特殊情况:
1.一个哲学家思考完了来吃饭,但左手边的筷子被拿了他可能拿起右手边的筷子,去竞争左手边正在用自己左手边筷子吃饭的哲学家手里的筷子,一直抢抢不到吃不上饭只能等他吃完再拿回筷子。
2.五个哲学家同时都要吃饭,每个人都拿起了左手边的筷子。都吃不上饭(1等2,2等3,3等4,4等5)循环等待
在多线程环境下,这样的情况发生的可能低但不是不会出现,我们要避免这样的问题。怎么避免?
从哲学家就餐问题中我们可以发现要促成这样的死锁需要不少条件
1.互斥条件:筷子一次只能被一个哲学家使用
2.请求与保持条件:哲学家拿着一根筷子等待另一根
3.不可抢占条件:筷子不能被强行夺走
4.循环等待:形成了环形等待链
所以
死锁的必要条件
1.锁是互斥的 sychronized 是互斥的
2.锁是不可抢占的 线程抢锁抢不到只能阻塞
3.请求和保持 拥有一把锁的线程不解锁去抢另一把锁
4.循环等待 t1等t2 ,t2等t3......
解决死锁(仅针对synchronized)
synchronized 是互斥的,不可抢夺(没有超时放弃的方法)所以仅从请求和保持和循环等待下手.
解决请求和保持问题和破坏循环等待条件
规定线程使用锁的顺序
让线程t1 先用锁1,再用t2 ; 让线程t2 先用 锁2 再用 锁1 这时线程1尝试加上锁2前就已经把锁1解锁了且进入阻塞状态等待线程2解锁锁2;线程2尝试加上锁1前就已经把锁2解锁了且进入阻塞状态等待线程1解锁锁1。虽然要等待但都能拿到锁。
public class demoDeadLock {
private static Object locker1 = new Object();
private static Object locker2 = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized(locker1) {
System.out.println("t1 获得 locker1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 死锁
// synchronized(locker2) {
// System.out.println("t1 获得 locker2");
// }
}
// 打破死锁
synchronized(locker2) {
System.out.println("t1 获得 locker2");
}
});
Thread t2 = new Thread(() -> {
synchronized(locker2) {
System.out.println("t2 获得 locker2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 死锁
// synchronized(locker1) {
// System.out.println("t2 获得 locker1");
// }
}
// 打破死锁
synchronized(locker1) {
System.out.println("t2 获得 locker1");
}
});
t1.start();
t2.start();
}
}
固定资源使用顺序
让线程t1先用锁资源执行完再把锁资源放给线程t2用。
public class demo {
private static Object locker1 = new Object();
private static Object locker2 = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (locker1) {
System.out.println("t1 get locker1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (locker2) {
System.out.println("t1 get locker2");
}
}
});
Thread t2 = new Thread(() -> {
synchronized (locker1) {
System.out.println("t2 get locker1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (locker2) {
System.out.println("t2 get locker2");
}
}
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

上述两种方法同样可以用来解决循环等待带来的死锁问题,针对锁编号,针对加多个锁的时候,约定按照一定顺序来加锁,避免出现循环等待的问题,但锁加多了会影响效率应该避免过多加锁。
内存可见性引起的线程安全问题
因为编译器优化产生的问题,来看一段代码
package demo;
import java.util.Scanner;
public class demo1 {
private static int flog = 0;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while(flog == 0){
// 不做任何操作
}
System.out.println("t1 end");
});
Thread t2 = new Thread(() -> {
Scanner scanner = new Scanner(System.in);
System.out.println(("please update flog's value"));
flog = scanner.nextInt();
System.out.println("t2 end");
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
预期输出:通过线程2修改flog使其值不为零t2 end,停止线程1的循环并打印 “t1 end"
实际输出

why:
出现这样的原因就是因为编译器优化误判引起的内存不可见问题,t1中一直在循环比较flog == 0 这里反复从内存中读 flog 的值和反复比较操作,每次读到的值都一样,编译器为了节省开销不从内存中读取flog 的值转而从 寄存器中读就会出现 t2改了flog 的值存到了内存中,但t1 一直读不到,不知道flog 的值发生了变化。编译起误判的现象在多线程环境下容易出现。
how:
怎么避免这样的问题
1.在while 循环中sleep一下触发线程调度此时线程可能会将寄存器中的数据刷新到内存,同时重新从内存读取最新值,这样编译器就不会误判一定程度上解决了内存可见性问题,但像 ++ 这样的操作,读内存占得比重大的操作还是会被优化,其他操作可能可以避免编译器优化但并不能完全解决内存可见性问题。
2.提醒编译器这个变量是易变的让它不要优化,添加 volatile 关键字。
package demo;
import java.util.Scanner;
public class demo1 {
private static volatile int flog = 0;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while(flog == 0){
// 不做任何操作
}
System.out.println("t1 end");
});
Thread t2 = new Thread(() -> {
Scanner scanner = new Scanner(System.in);
System.out.println(("please update flog's value"));
flog = scanner.nextInt();
System.out.println("t2 end");
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

指令重排序
为了提高效率,编译器会优化你的代码,在保证逻辑的情况下,调整代码的顺序。
eg:
就像吃东西一个滚烫的汤,一盘番茄炒蛋(默认下饭吃),一盘红烧肉(默认下饭吃)。
吃饭为了吃饱,进食顺序可以是:
1.喝汤(边吹凉边喝)
2.吃番茄炒蛋
3.吃红烧肉从滚烫吹到能入口到喝完汤,两个菜都凉了
可以调整一下进食顺序
1.吃红烧肉
2.吃点番茄炒蛋
3.喝汤
虽然调整了顺序但吃饱的结果没变,后者吃上了热菜还更省时间(妈妈再也不用担心我的学习了)
设计模式
一些大佬针对软件设计中的常见问题的给出的可复用的,经过验证的解决方案。我们可以通过学习这些设计模式培养设计素养。
单例模式(面试重点)
简单来看就是一个类中只希望创建一个实例(对象)。通过私有化构造方法防止外部多创建实例
饿汉模式
‘饿汉’ 比较迫切所以第一时间创建实例(在类加载的时候就创建)
// 单例 期望这个类只有一个实例
// 饿汉模式 创建实例实际十分紧迫十分靠前的
class Singleton{
// 把唯一的实例创建出来 此时的 instance 是static 成员 所以在类加载的时候就创建出来了
private static Singleton instance = new Singleton();
// 君子协定
public static Singleton getInstance(){
return instance;
}
private Singleton(){
// 私有构造方法,防止外部实例化
}
}
public class demo_SingleModel {
public static void main(String[] args) {
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
System.out.println(s1 == s2);
}
}
class Student {
private String name;
private int age;
}
懒汉模式
‘懒汉’ 比较懒非必要不创建,要用时再创建。
// 单例模式
// 懒汉模式 延迟加载 只有在调用getInstance 方法的时候才会创建实例
class SingletonLazy {
private static SingletonLazy instance = null;
private SingletonLazy() {
}
public static SingletonLazy getInstance() {
if (instance == null) {
instance = new SingletonLazy();
}
return instance;
}
}
public class demo_SingletonLazy {
public static void main(String[] args) {
SingletonLazy s1 = SingletonLazy.getInstance();
SingletonLazy s2 = SingletonLazy.getInstance();
System.out.println(s1 == s2);
}
}
考虑线程安全
这样写两个模式现成安全吗
饿汉模式在一开始就创建了实例,后续getInstance() 方法只涉及读操作(return)读操作天然是线程安全的。
懒汉模式中getinstance()方法中就不只是读操作了,涉及带判定条件的赋值语句

线程安全的懒汉
// 单例模式
// 懒汉模式 延迟加载 只有在调用getInstance 方法的时候才会创建实例
// 线程安全
class SingletonLazySafe {
private static volatile SingletonLazySafe instance = null;
// 构造方法私有化
private SingletonLazySafe() {
}
public static SingletonLazySafe getInstance() {
if(instance == null){
synchronized(SingletonLazySafe.class){
if(instance == null){
instance = new SingletonLazySafe();
}
}
}
return instance;
}
}
public class demo_singletonLazy_safe {
public static void main(String[] args) {
SingletonLazySafe s1 = SingletonLazySafe.getInstance();
SingletonLazySafe s2 = SingletonLazySafe.getInstance();
System.out.println(s1 == s2);
}
}
加锁(双重判断)
if(instance == null){
instance = new SingletonLazySafe();
}只有这里会触发现成安全问题。
第一重判定:判定是否要加锁,不当加锁就会造成不必要的阻塞影响效率。只有instance为空时,才需要加锁,不为空不涉及修改操作仅为return,是线程安全的不需要加锁。
第二重判定:判定是否要创建实例,instance 为空时才创建对象
class SingletonLazySafe {
private static SingletonLazySafe instance = null;
// 构造方法私有化
private SingletonLazySafe() {
}
public static SingletonLazySafe getInstance() {
if(instance == null){
synchronized(SingletonLazySafe.class){
if(instance == null){
instance = new SingletonLazySafe();
}
}
}
return instance;
}
}
public class demo_singletonLazy_safe {
public static void main(String[] args) {
SingletonLazySafe s1 = SingletonLazySafe.getInstance();
SingletonLazySafe s2 = SingletonLazySafe.getInstance();
System.out.println(s1 == s2);
}
}
指令重排序的问题
instance = new SingletonLazySafe(); 这一段可以拆分成三个指令。
1. 申请内存空间
2. 初始化
3. 把内存地址存到引用中
编译器会为了提升效率调换 3 ,2 的顺序,单线程没有问题
在多线程环境下:

这里同样可以用 volatile 提醒编译器不要优化。
private static SingletonLazySafe instance = null;
在答录机上留下我的心跳,你一定偷偷在微笑 ----------- 是是非非 DT
✨💗👍

被折叠的 条评论
为什么被折叠?



