当前位置: 首页 > news >正文

线程2---javaEE(校招)

目录

回顾

造成线程安全的原因

synchronized 带来的问题

死锁(deadlock)

死锁1

死锁2

 死锁3

死锁的必要条件

解决死锁(仅针对synchronized)

规定线程使用锁的顺序

固定资源使用顺序

内存可见性引起的线程安全问题

指令重排序

设计模式

单例模式(面试重点)

饿汉模式

懒汉模式

考虑线程安全

线程安全的懒汉 

指令重排序的问题


回顾

因为线程安全我们引入了锁,但加锁就不会有线程安全了吗?显然线程安全问题不只这些,加锁解决的是操作不是原子的造成的问题。

造成线程安全的原因

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){    // 1synchronized(locker){    // 2synchronized(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

✨💗👍

http://www.dtcms.com/a/469267.html

相关文章:

  • [创业之路-687]:华为“1+8+N”战略以及其背后的技术栈、商业逻辑。
  • 基于大语言模型(LLM)的城市时间、空间与情感交织分析:面向智能城市的情感动态预测与空间优化
  • 眼控交互:ErgoLAB新一代人机交互方式
  • 数字货币众筹网站开发如何做高网站的浏览量
  • 网站服务器频繁掉线的主要原因是什么
  • codeigniter换服务器之后,会员登录之后又跳回登录页面的解决方法
  • VS Code 的 SSH 密钥,并将其安全地添加到服务器
  • 香港服务器速度快慢受何影响?
  • 服务器相关:什么是 alios. centos. cuda. cuda tookit. gcc. cudann. pytorch.
  • K8S(五)—— K8s中YAML文件全方位解析:语法、案例、Port详解与快速编写技巧
  • 企业网站注销流程做企业网站需要服务器么
  • k8s存储juicefs简介
  • Ansible模块介绍(接上段)
  • 河南省建设厅网站官网重庆seo务
  • 【开题答辩全过程】以 北京房屋租赁数据分析与可视化为例,包含答辩的问题和答案
  • 什么身一什么网站建设网站开发毕设任务书
  • 【八股消消乐】手撕分布式协议和算法(基础篇)
  • Databend 九月月报:自增列 AUTOINCREMENT 与行级安全
  • Zenlayer 推出分布式推理平台,加速 AI 创新全球落地
  • 01-iptables防火墙安全
  • Docker存储技术全解析:分层与持久化
  • 分布式专题——39 RocketMQ客户端编程模型
  • 物联网二级平台设计与实现:从Home Assistant到JetLinks的设备协同架构实践
  • 分布式文件存储 RPC 服务实现
  • 在哪些软件上建设网站市场营销方案怎么做
  • 《小小梦魇3》今日发售!用UU远程手机躺玩通关
  • Jenkins Pipeline post指令详解
  • 系列文章<一>(从LED显示问题到非LED领域影像画质优化:揭秘跨领域的核心技术):从LED冬奥会、奥运会及春晚等大屏,到手机小屏,快来挖一挖里面都有什么
  • 泊松分布解题步骤
  • Postman API 测试使用指南:从入门到精通