900字范文,内容丰富有趣,生活中的好帮手!
900字范文 > Java多线程:多线程同步安全问题的 “三“ 种处理方式 ||多线程 ”死锁“ 的避免

Java多线程:多线程同步安全问题的 “三“ 种处理方式 ||多线程 ”死锁“ 的避免

时间:2019-06-06 15:34:31

相关推荐

Java多线程:多线程同步安全问题的 “三“ 种处理方式 ||多线程 ”死锁“ 的避免

Java多线程:多线程同步安全问题的 “三“ 种处理方式 ||多线程 ”死锁“ 的避免 || 单例模式”懒汉式“的线程同步安全问题

每博一文案

常言道:“不经一事,不懂一人”。一个人值不值得交往,在关键时候才能看得清。看过这样的一个故事:晚清历史上,红顶商人胡雪岩家喻户晓。有一名商人在生意中惨败,需要大笔资金周转。为了救急,他主动上门,开出低价想让胡雪岩收购自己的产业。胡雪岩给出正常的市场价,来收购对方的产业。手下们不解地问胡雪岩,为啥送上门的肥肉都不吃。胡雪岩说:“你肯为别人打伞,别人才原意为你打伞。”那个商人的产业可能是几辈子人积攒下来的,我要是以他开出来的价格来买,当然很占便宜,但人家可能就一辈子也翻不了身。这不是单纯的投资,而是救了一家人,既交了朋友,又对得起良心。谁都有雨天没伞的时候,能帮人遮点雨就遮点吧。落叶才知秋,落难才知友。做人真正的成功,不是看你认识哪些人,而是看你落魄时,还有哪些人原意认识你。身处低谷之时,才知道谁假,经历重重的苦难,才真正看透人心。相信时间,相信它最终告诉你,谁是虚伪的脸,谁是真心的伴。余生,把心情留给懂你的人,把感情留给爱你的人,别交,交不透的人,别府不值得付的心。—————— 一禅心灵庙语

文章目录

Java多线程:多线程同步安全问题的 “三“ 种处理方式 ||多线程 ”死锁“ 的避免 || 单例模式”懒汉式“的线程同步安全问题每博一文案1. 多线程同步安全的”三“ 种处理方式1.1 多线程同步的安全问题1.2 synchronized 关键字的介绍1.3 解决多线程同步安全问题方式一: synchronized () { } 代码块1.3.1 java 三大变量在作为 同步监视器 ”锁“ 的线程安全问题的讨论 1.4 解决多线程同步安全问题方式二:synchronized( ) 方法1.4.1 synchronized ( ) 非静态方法的 ”锁“1.4.2 synchronized () 静态方法的 ”锁“1.4.3 synchronized() 代码块的方式 与 synchronized()方法的解决线程安全问题的异同 1.5 解决多线程同步安全问题的方式三:lock. 的使用1.5.1 synchronized 与 Lock 的对比 1.6 如何避免多线程安全问题:1.7 开发中如何处理线程安全问题及其注意事项 2. 多线程的 ”死锁“ 现象2.1 "死锁" 介绍2.2 释放锁的操作2.3 不会释放锁的操作2.4 如何避免 ”死锁“ 的出现 3. 单例模式 ”懒汉式“ 的线程安全问题4. 关于 ”锁“ 的面试题:4.1 题目一4.2 题目二4.3 题目三 4. 总结:5. 最后:

1. 多线程同步安全的”三“ 种处理方式

1.1 多线程同步的安全问题

所谓的多线程安全问题:

多个线程执行的不确定性引起执行结果的不稳定。多个线程对进程中的共享数据,操作对数据的修改不同步,造成数据的损坏。

举例如下:

模拟火车站卖票,一共有 100 张火车票,让三个人工售票窗口(三个线程)一起售票。

package blogs.blog4;/*** 模拟火车站卖票,一共有 100 张火车票,让三个人工售票窗口(三个线程)一起售票*/public class ThreadTest6 {public static void main(String[] args) {// 创建窗口对象Window window = new Window();Thread t1 = new Thread(window); // 售票窗口一Thread t2 = new Thread(window); // 售票窗口二Thread t3 = new Thread(window); // 售票窗口三t1.setName("售票窗口一:");t2.setName("售票窗口二:");t3.setName("售票窗口三:");t1.start();t2.start();t3.start();}}/*** 火车窗口*/class Window implements Runnable {private int ticket = 100;@Overridepublic void run() {while (true) {// 有票,便出售if (this.ticket > 0) {System.out.println(Thread.currentThread().getName() + "所售票号: " + this.ticket);this.ticket--; // 售票成功,减减} else {break; // 没票了,停止出售。}}}}

我们可以附加上一个sleep() 线程睡眠(进入阻塞状态)的情况,提高出现线程安全问题的概率。如下:

package blogs.blog4;/*** 模拟火车站卖票,一共有 100 张火车票,让三个人工售票窗口(三个线程)一起售票*/public class ThreadTest6 {public static void main(String[] args) {// 创建窗口对象Window window = new Window();Thread t1 = new Thread(window); // 售票窗口一Thread t2 = new Thread(window); // 售票窗口二Thread t3 = new Thread(window); // 售票窗口三t1.setName("售票窗口一:");t2.setName("售票窗口二:");t3.setName("售票窗口三:");t1.start();t2.start();t3.start();}}/*** 火车窗口*/class Window implements Runnable {private int ticket = 100;@Overridepublic void run() {while (true) {// 有票,便出售if (this.ticket > 0) {System.out.println(Thread.currentThread().getName() + "所售票号: " + this.ticket);try {Thread.sleep(10); // 当前线程睡眠: 0.001s ,提高出现线程安全问题的概率。} catch (InterruptedException e) {e.printStackTrace();}this.ticket--; // 售票成功,减减} else {break; // 没票了,停止出售。}}}}

上述代码出现线程安全问题的原因分析:

三个售票窗口,三个线程(售票窗口1,2,3线程)。售票窗口1线程进入到打印票号 (ticket = 100) 时,并没有将ticket票号--语句执行给执行完,(该线程睡眠了 sleep(0.001s)方法)出于网络原因停止了一小下,售票窗口2线程就进入到了打印 ticket 票号,这时侯的票号,因为上一个售票窗口1线程并没有将ticket票号--语句执行就睡眠了 sleep(),所以这时候的 ticket 票号还是和售票窗口1线程的票号是一样的 100 的,这时候售票窗口2线程也睡眠了,也没有执行到(ticket票号--语句),售票窗口3线程进来了,这时候的 ticket 票号可能还是 100,因为可能这时候的售票窗口1线程并没有醒来(也就还没有执行,ticket票号--语句)。这样的结果就是:售票窗口1,售票窗口2,售票窗口3 都出售了 同一张 100 的票号的火车票,导致的结果就是 有三个人买到了 同一张一模一样的火车票,如果始发站都一样的话:那可怕的就是:三个人座同一张座位,发生争执。代码图示解析: 实例图解

1.2 synchronized 关键字的介绍

多线程出现安全问题的原因:

当多个线程在操作同一个进程共享的数据的时候,一个线程对共享数据的执行仅仅只执行了一部分,还没有执行完,另一个线程参与进来执行。去操作所有线程共享的数据,导致共享数据的错误。

就相当于生活当中:你上厕所,你上到一半还没有上完,另外一个人,就来占用你这个茅坑上厕所。

解决办法

对于多线程操作共享数据时,只能有一个线程操作,其他线程不可以操作共享数据的内容,只有当一个线程对共享数据操作完了,其他线程才可以操作共享数据。就相等于对于共享数据附加上一把锁,只有拿到了这把锁的钥匙的线程才可以操作共享数据的内容,而锁只有一把,只有当某个线程操作完了,将手中的锁钥匙释放了,其他线程才可以拿到该锁钥匙,操作共享数据。就是拿到锁钥匙的线程睡眠了,阻塞了,其他线程也必须等到该线程将手中的锁钥匙释放了,其他线程才可以拿到锁钥匙,操作共享数据。

就相当于生活当中:你上厕所,就把厕所门给锁了,其他想上厕所的人进不来,就算你在厕所中睡着了,没有打开厕所门的锁,其他人也是进不去厕所的,只有当你将厕所门的锁打开了,其他人才能进去上厕所。

同理我们Java当中使用synchronized关键字附加上锁

synchronized几种写法

修饰代码块修饰普通方法修饰静态方法

1.3 解决多线程同步安全问题方式一: synchronized () { } 代码块

synchronized 修饰代码块的使用方式

synchronized (同步监视器也称"锁") {// 这里放可以加锁的逻辑:其实就是操作共享数据的内容:修改共享数据方面的内容}

同步监视器: 所谓的同步监视器,也称为 “锁”。任何一个对象都可以充当一个锁,成为锁对象,也称为同步锁 。比如 Object ,String ,自定义对象都可以充当锁,但是要实现达到解决对应的线程安全问题,就需要根据实际情况,设置锁的对象了。但是 同步监视器“锁”不可以为 null 不然报NullPointerExceptionnull 指针异常的。

synchronized()后面的小括号中的这个 “锁”, 设置锁对象是关键,这个必须是多线程共享的 对象,才能到达多线程排队拿锁钥匙,解决多线程安全问题的效果。这样的效果的锁,被称为同步锁

比如:synchronized()放什么对象,那要看你想让哪些线程同步,假设 t1,t2,t3,t4,t5 有5个线程,你只想让 t1,t2,t3线程访问共享数据时的线程安全问题,排队获取同步锁进行。t4,t5 不解决,不需要排队,怎么办设置 ”锁“: 你设置的同步锁对象,就需要是 t1,t2,t3线程共享的对象了,而这个对象对于 t4,t5 来说是不共享的。这样就达到了,t1,t2,t3线程排队获取同步锁,执行操作共享数据,而t4,t5 不用排队获取同步锁,可以多线程并发操作共享数据。

需要注意一点就是:这个同步锁的对象一定要选好了,这个”锁“一定是你需要排队获取”锁“后执行操作共享数据的线程对象所共享的,多加注意定义的”锁“对象的作用域

在java中,每一个对象有且仅有一个同步锁。这也意味着,同步锁是依赖于对象而存在。当我们调用某对象的synchronized方法时,就获取了该对象的同步锁

例如,synchronized(obj)就获取了“obj这个对象”的同步锁。不同线程对同步锁的访问是互斥的。

也就是说,某时间点,对象的同步锁只能被一个线程获取到!通过同步锁,我们就能在多线程中,实现对“对象/方法”的互斥访问。

例如,现在有两个线程A和线程B,它们都会访问“对象obj的同步锁”。假设,在某一时刻,线程A获取到“obj的同步锁”并在执行一些操作;而此时,线程B也企图获取“obj的同步锁” —— 线程B会获取失败,它必须等待,直到线程A释放了“该对象的同步锁”之后线程B才能获取到“obj的同步锁”从而才可以运行。

举例:解决上述买火车票的多线程安全问题:设置不同的 同步监视器 ”锁“,达到的效果也是不一样的。有的可以解决多线程安全问题,有的不能,一起来看看吧。

设置 同步监视器 ”锁“的对象为Object object = new Object();的成员变量。

package blogs.blog4;/*** 模拟火车站卖票,一共有 100 张火车票,让三个人工售票窗口(三个线程)一起售票*/public class ThreadTest6 {public static void main(String[] args) {// 创建窗口对象Window window = new Window();Thread t1 = new Thread(window); // 售票窗口一Thread t2 = new Thread(window); // 售票窗口二Thread t3 = new Thread(window); // 售票窗口三t1.setName("售票窗口一:");t2.setName("售票窗口二:");t3.setName("售票窗口三:");t1.start();t2.start();t3.start();}}/*** 火车窗口*/class Window implements Runnable {private int ticket = 100;Object object = new Object();@Overridepublic void run() {while (true) {synchronized (object) {// object 是三个线程共享的// 有票,便出售if (this.ticket > 0) {System.out.println(Thread.currentThread().getName() + "所售票号: " + this.ticket);try {Thread.sleep(10); // 当前线程睡眠: 0.001s ,提高出现线程安全问题的概率。} catch (InterruptedException e) {e.printStackTrace();}this.ticket--; // 售票成功,减减} else {break; // 没票了,停止出售。}}}}}

从结果上看是可以解决多线程安全问题的:因为我们这里使用的是 implements Runnable 接口的方式创建的线程对象,其中所传的对象都是 window 地址,其中的 object 的对象是 三个 售票线程共享的对象的一把 ”锁“,售票1,2,3线程需要排队获取到 锁,才能操作共享数据的内容,如下图示:

将同步监视器 ”锁“ 设置为:Object object = new Object();中的 run()方法当中,作为局部变量,这样导致的结果就是:售票1,2,3线程共用的不是同一把 ”锁“了,因为局部变量,是存在于栈当中的(出了run()方法的作用域就销毁了,再次进入run()方法就会重写建立新的一个局部变量),栈每个线程各自都一份,线程之间不共享。售票1,2,3线程各个都有一把自己独有的锁,不共享,不需要等待别人手中的锁了,自己就有,不需要排队执行了,多线程安全问题就出现了。

package blogs.blog4;/*** 模拟火车站卖票,一共有 100 张火车票,让三个人工售票窗口(三个线程)一起售票*/public class ThreadTest6 {public static void main(String[] args) {// 创建窗口对象Window window = new Window();Thread t1 = new Thread(window); // 售票窗口一Thread t2 = new Thread(window); // 售票窗口二Thread t3 = new Thread(window); // 售票窗口三t1.setName("售票窗口一:");t2.setName("售票窗口二:");t3.setName("售票窗口三:");t1.start();t2.start();t3.start();}}/*** 火车窗口*/class Window implements Runnable {private int ticket = 100;@Overridepublic void run() {Object object = new Object(); // 局部变量,售票1,2,3线程不共享,各个都有,不需要等待别人手中的锁了while (true) {synchronized (object) {// object局部变量,所有线程都可以进入了。不需要等待对方的锁了。线程安全问题。// 有票,便出售if (this.ticket > 0) {System.out.println(Thread.currentThread().getName() + "所售票号: " + this.ticket);try {Thread.sleep(10); // 当前线程睡眠: 0.001s ,提高出现线程安全问题的概率。} catch (InterruptedException e) {e.printStackTrace();}this.ticket--; // 售票成功,减减} else {break; // 没票了,停止出售。}}}}}

将同步监视器 ”锁“ 设置为 ”this“ 当前对象的引用。 因为我们这里使用的是 implements Runnable 接口的方式创建的线程对象,其中所传的对象都是 window 地址,其中的 object 的对象是 三个 售票线程共享的对象的一把 ”锁“,售票1,2,3线程需要排队获取到 锁,才能操作共享数据的内容,如下图示:和 我们将 锁设置为 Object object = new Object(); 成员变量是一样的。

package blogs.blog4;/*** 模拟火车站卖票,一共有 100 张火车票,让三个人工售票窗口(三个线程)一起售票*/public class ThreadTest6 {public static void main(String[] args) {// 创建窗口对象Window window = new Window();Thread t1 = new Thread(window); // 售票窗口一Thread t2 = new Thread(window); // 售票窗口二Thread t3 = new Thread(window); // 售票窗口三t1.setName("售票窗口一:");t2.setName("售票窗口二:");t3.setName("售票窗口三:");t1.start();t2.start();t3.start();}}/*** 火车窗口*/class Window implements Runnable {private int ticket = 100;@Overridepublic void run() {while (true) {synchronized (this) {// this 当前对象:是三个线程共享了。一// 有票,便出售if (this.ticket > 0) {System.out.println(Thread.currentThread().getName() + "所售票号: " + this.ticket);try {Thread.sleep(10); // 当前线程睡眠: 0.001s ,提高出现线程安全问题的概率。} catch (InterruptedException e) {e.printStackTrace();}this.ticket--; // 售票成功,减减} else {break; // 没票了,停止出售。}}}}}

将同步监视器”锁“ 设置为 ”类对象“, 类名.class(这里的类对象为:Window.class) / 字符串(这里我们设置为”abc“)。都是可以解决多线程安全问题的,因为:类对象 是存放在方法区当中的,而且类仅仅只会加载一次到内存当中,所有对象,线程共用,而字符串在 JVM 中的字符串池中存在的,同样也是仅仅只会生成一个唯一的字符串对象,所有对象共用,线程共用。

package blogs.blog4;/*** 模拟火车站卖票,一共有 100 张火车票,让三个人工售票窗口(三个线程)一起售票*/public class ThreadTest6 {public static void main(String[] args) {// 创建窗口对象Window window = new Window();Thread t1 = new Thread(window); // 售票窗口一Thread t2 = new Thread(window); // 售票窗口二Thread t3 = new Thread(window); // 售票窗口三t1.setName("售票窗口一:");t2.setName("售票窗口二:");t3.setName("售票窗口三:");t1.start();t2.start();t3.start();}}/*** 火车窗口*/class Window implements Runnable {private int ticket = 100;@Overridepublic void run() {while (true) {synchronized ("abc") {// 字符串池的存在:所有对象/线程共享// 有票,便出售if (this.ticket > 0) {System.out.println(Thread.currentThread().getName() + "所售票号: " + this.ticket);try {Thread.sleep(10); // 当前线程睡眠: 0.001s ,提高出现线程安全问题的概率。} catch (InterruptedException e) {e.printStackTrace();}this.ticket--; // 售票成功,减减} else {break; // 没票了,停止出售。}}}}}

同步监视器"锁" 不可以为 null ,编译器会报错,就算骗过了编译器,在运行的时候也是会报错的:NullPointerExceptionnull 异常。

补充:

1.在实现Runnable接口创建多线程的方式中,我们可以考虑使用this充当同步监视器"锁",因为我们使用的都是同一个Runnable对象创建的 Thread对象,

2.如果是extends Thread的方式创建多线程,我们可以考虑使用 “类.class" 的方式充当同步监视器"锁”,因为类仅仅只会加载一次,但是这种继承方式慎用 this充当"锁"同名监视器

1.3.1 java 三大变量在作为 同步监视器 ”锁“ 的线程安全问题的讨论

Java当中有 三大变量

局部变量:存放在栈中成员变量:存放在堆中静态变量:存放在方法区中

对于着三种变量充当同步监视器 ”锁“ 存在的线程安全问题。

一个进程一个堆和一个方法区,一个进程包含多个线程,一个线程一个栈。

所以对于同一个进程中的堆和方法区中的数据,对于所有的线程来说都是共享的。

以上三大变量中:局部变量是永远不存在线程安全问题。因为局部变量存在栈中(一个线程一个栈),是每个线程各自独立拥有的,不使用特殊方式的话,是无法共享的。

实例变量在堆中,堆只有一个,静态变量在方法区中,方法区只有一个,一个进程一个堆一个方法区,所有多线程共享的,所有有可能存在线程的安全问题。

1.4 解决多线程同步安全问题方式二:synchronized( ) 方法

同样的synchronized可以修饰代码块,也是可以修饰方法的。

修饰方法用两种用法:1. 修饰非静态方法,2. 修饰静态方法。这两者之间是又差异的。

1.4.1 synchronized ( ) 非静态方法的 ”锁“

synchronized还可以放在方法声明中,表示整个方法同步方法,这里修饰非静态方法

private synchronized void sell() {// 这里放可以加锁的逻辑:其实就是操作共享数据的内容:修改共享数据方面的内容}

使用 extends Thread 的方式创建多线程,同样是:模拟火车站卖票,一共有 100 张火车票,让三个人工售票窗口(三个线程)一起售票。

这里我们使用 synchronized 修饰非静态方法的方式处理就不行了。

synchronized 修饰非静态方法时,默认的同步监视器 ”锁“是this当前对象的引用,不可以修改的。

package blogs.blog4;/*** synchronized 修饰方法*/public class ThreadTest7 {public static void main(String[] args) {Thread t1 = new MyThread7(); // 售票窗口1Thread t2 = new MyThread7(); // 售票窗口2Thread t3 = new MyThread7(); // 售票窗口3// 设置线程名t1.setName("售票窗口: ");t2.setName("售票窗口2: ");t3.setName("售票窗口3: ");// 创建线程t1.start();t2.start();t3.start();}}/*** 售票*/class MyThread7 extends Thread {// 设置为 static 的成员变量,不然会出现每个售票窗口都有 100 张火车票的情况了private static int ticket = 100; // static 静态的和类一起加载,仅仅只会加载一次,所有对象共享。@Overridepublic void run() {this.sell();}private synchronized void sell() {// synchronized 修饰方法: 同步方法。while (true) {// 有票,便出售if (ticket > 0) {System.out.println(Thread.currentThread().getName() + "所售票号: " + ticket);try {Thread.sleep(10); // 当前线程睡眠: 0.001s ,提高出现线程安全问题的概率。} catch (InterruptedException e) {e.printStackTrace();}ticket--; // 售票成功,减减} else {break; // 没票了,停止出售。}}}}

为什么这里使用 synchronized 修饰非静态方法无法解决 线程安全问题 ???

是因为 synchronized 修饰非静态方法 的同步监视器 ”锁“ 是this这是默认写死了的,是无法修改的。这里我们使用的是 extends Thread 的方式创建的多线程,

其中的 run() 方法是在(栈区中)不是共享的对象,所以 this 也就不是共享的对象了,也就不是三个 售票1,2,3线程共享的”锁“了,自然无法实现排队获取 ”锁“,也就无法处理线程安全问题了。

解决: 将 synchronized 修饰的方法改为 static 静态方法。

1.4.2 synchronized () 静态方法的 ”锁“

synchronized还可以放在方法声明中,表示整个方法同步方法,这里修饰静态方法

private synchronized static void sell() {// 这里放可以加锁的逻辑:其实就是操作共享数据的内容:修改共享数据方面的内容}

synchronized 修饰静态方法时,默认的同步监视器 ”锁“是类名.class也就是类对象,类是存储在 方法区当中的,仅仅只能加载一次到内存当中,所有对象,线程共用,无法修改。

这里使用 synchronized 修饰静态方法 就可以简单的解决 上述 extends Thread 创建多线程的火车售票问题了。

如下:

package blogs.blog4;/*** synchronized 修饰方法*/public class ThreadTest7 {public static void main(String[] args) {Thread t1 = new MyThread7(); // 售票窗口1Thread t2 = new MyThread7(); // 售票窗口2Thread t3 = new MyThread7(); // 售票窗口3// 设置线程名t1.setName("售票窗口1: ");t2.setName("售票窗口2: ");t3.setName("售票窗口3: ");// 创建线程t1.start();t2.start();t3.start();}}/*** 售票*/class MyThread7 extends Thread {// 设置为 static 的成员变量,不然会出现每个售票窗口都有 100 张火车票的情况了private static int ticket = 100; // static 静态的和类一起加载,仅仅只会加载一次,所有对象共享。@Overridepublic void run() {this.sell();}private synchronized static void sell() {// synchronized 修饰方法: 同步方法。while (true) {// 有票,便出售if (ticket > 0) {System.out.println(Thread.currentThread().getName() + "所售票号: " + ticket);try {Thread.sleep(10); // 当前线程睡眠: 0.001s ,提高出现线程安全问题的概率。} catch (InterruptedException e) {e.printStackTrace();}ticket--; // 售票成功,减减} else {break; // 没票了,停止出售。}}}}

从结果上我们可以看到,这里执行所有的票,都是被 售票3线程给出售了,其他售票线程根本就没有机会,是因为:

这里我们是 run() 方法调用被 synchronized 修饰的静态方法,使用的是 类.class 这个类对象锁,所有对象共用,导致了,只要是

一个售票线程拿到类锁(所有线程共享共用),进入了 sell()方法,其他线程必须等待其释放类锁才有机会进入到 sell() 方法中,但是其中的 sell()方法中有一个while(true) 循环,当该线程执行完 sell()方法,其票也已经出售完了。所以就出现了一个线程将所有票都出售完了。

@Overridepublic void run() {this.sell();}private synchronized static void sell() {// synchronized 修饰方法: 同步方法。while (true) {// 有票,便出售if (ticket > 0) {System.out.println(Thread.currentThread().getName() + "所售票号: " + ticket);try {Thread.sleep(10); // 当前线程睡眠: 0.001s ,提高出现线程安全问题的概率。} catch (InterruptedException e) {e.printStackTrace();}ticket--; // 售票成功,减减} else {break; // 没票了,停止出售。}}}

我们可以修改一下,将 while()循环方法 run() 方法中,不要放到 sell()方法就可以了,如下

package blogs.blog4;/*** synchronized 修饰方法*/public class ThreadTest7 {public static void main(String[] args) {Thread t1 = new MyThread7(); // 售票窗口1Thread t2 = new MyThread7(); // 售票窗口2Thread t3 = new MyThread7(); // 售票窗口3// 设置线程名t1.setName("售票窗口1: ");t2.setName("售票窗口2: ");t3.setName("售票窗口3: ");// 创建线程t1.start();t2.start();t3.start();}}/*** 售票*/class MyThread7 extends Thread {// 设置为 static 的成员变量,不然会出现每个售票窗口都有 100 张火车票的情况了private static int ticket = 100; // static 静态的和类一起加载,仅仅只会加载一次,所有对象共享。@Overridepublic void run() {while (true) {this.sell();}}private synchronized static void sell() {// synchronized 修饰方法: 同步方法。// 有票,便出售if (ticket > 0) {System.out.println(Thread.currentThread().getName() + "所售票号: " + ticket);try {Thread.sleep(10); // 当前线程睡眠: 0.001s ,提高出现线程安全问题的概率。} catch (InterruptedException e) {e.printStackTrace();}ticket--; // 售票成功,减减} else {return ;}}}

1.4.3 synchronized() 代码块的方式 与 synchronized()方法的解决线程安全问题的异同

synchronized 无论是修饰 代码块,还是方法都有 同步监视器 ”锁“的机制存在。不同的是 synchronized 修饰代码块,可以灵活的设定同步监视器 ”锁“ 的对象,而 synchronized 修饰方法却不可以了,synchronized 修饰非静态方法,默认同步监视器”锁“ 是this,修饰静态方法 static 默认的同步监视器”锁“ 是类.class类对象,类锁这些都是固定的无法修改,比较死板。synchronized 修饰方法处理多线程比较方便,简单,直接在方法中加 synchronized 就可以了。synchronized 修饰代码块的效率 比 synchronized 修饰方法的效率更快,因为:synchronized 出现在方法上,表示整个方法体都需要同步,可能会无故扩大同步的范围,导致程序的执行效率降低(多线程转为单线程处理同步安全问题的逻辑事务更多了)。一般建议优先使用 synchronized 修饰代码块的方式,处理多线程安全问题。

1.5 解决多线程同步安全问题的方式三:lock. 的使用

JDK 5.0开始,Java提供了更强大的线程同步机制——> 通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当

java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的 工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。Lock是一个接口,我们是无法 new 的我们需要找到其实现类就是ReentrantLock

其中 Lock 接口对应的抽象方法如下

ReentrantLock类实现了 Lock ,它拥有与 synchronized 相同的并发性和 内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock可以显式加锁、释放锁。同样的 ReentrantLock重写了 Lock 中的重写方法。

ReentrantLock的重写的lock()显式的启动/获取锁,unlock()显式释放手中的 ”锁“的方法

使用 Lock 接口中 lock() 获取锁 / unlock () 释放锁解决多线程安全问题的步骤

首先创建ReentrantLock的实例对象,用于调用其中重写 Lock 接口中的抽象方法 lock() 获取锁,unlock() 释放锁,这里定义为成员变量

private ReentrantLock reentrantLock = new ReentrantLock();

在适合的位置,通过lock()显式的获取/启动 ”锁“

reentrantLock.lock(); // 2.调用lock()显式启动锁

最后在合适的位置调用unlock()显示释放当前线程的 ”锁“。一般是定义在 finally{} 中防止该线程因为一些异常原因,没有释放手中的锁,让其他线程拿到锁,无法访问。注意点:一般是将 lock() 调用在 try{} 中 ,unlock() 调用在finally{} 中,确保线程手中的锁一定会被释放 ,让其他线程可以获取到 ”锁“,进行共享数据的操作

完整实现如下:同样:模拟火车站卖票,一共有 100 张火车票,让三个人工售票窗口(三个线程)一起售票。

package blogs.blog4;import java.util.concurrent.locks.ReentrantLock;/*** 解决多线程同步机制的方式三: Lock*/public class ThreadTest8 {public static void main(String[] args) {// 创建窗口对象Window8 window = new Window8();Thread t1 = new Thread(window); // 售票窗口一Thread t2 = new Thread(window); // 售票窗口二Thread t3 = new Thread(window); // 售票窗口三t1.setName("售票窗口一:");t2.setName("售票窗口二:");t3.setName("售票窗口三:");t1.start();t2.start();t3.start();}}/*** 火车窗口*/class Window8 implements Runnable {private int ticket = 100;// 1.创建ReentrantLock 实例对象调用其中的 lock()启动锁,unlock() 手动解锁private ReentrantLock reentrantLock = new ReentrantLock();@Overridepublic void run() {while (true) {try {reentrantLock.lock(); // 2.调用lock()显式启动锁// 有票,便出售if (this.ticket > 0) {System.out.println(Thread.currentThread().getName() + "所售票号: " + this.ticket);try {Thread.sleep(10); // 当前线程睡眠: 0.001s ,提高出现线程安全问题的概率。} catch (InterruptedException e) {e.printStackTrace();}this.ticket--; // 售票成功,减减} else {break; // 没票了,停止出售。}} finally {reentrantLock.unlock(); // 3.释放锁,注意使用 finally 无论是否出现异常都一定会被执行,一定会释放锁}}}}

1.5.1 synchronized 与 Lock 的对比

相同:这两者都可以解决线程安全问题。

不同:

synchronized 机制在执行完相应的同步代码以后,自动的释放同步监视器(锁),以及是隐式设置锁的。Lock 是手动通过调用 lock() 方法显式获取锁的,以及调用 unlock() 手动释放 锁的lock 比 synchronized(无论是修饰代码块,还是方法)都更加的灵活。Lock 只有代码锁,synchronized 有代码块锁和方法锁使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)

1.6 如何避免多线程安全问题:

局部变量是永远不存在线程安全问题。因为局部变量存在栈中(一个线程一个栈),是每个线程各自独立拥有的,不使用特殊方式的话,是无法共享的。所以可以尽可能使用局部变量。对于成员变量,静态变量,尽可能不要被多线程操作了。如果必须是成员变量,那么可以考虑创建多个对象,这样成员变量的内存就不是共享的(锁就不是唯一的一把了)(一个线程对应一个对象,100个线程对应100个对象,对象不共享,就没有数据安全问题了)集合上的线程安全需要明确: 对于 String 字符串:如果使用局部变量的话:建议使用StringBuilder因为局部变量不存在线程安全问题,选择StringBuilder 更合适,StringBuffer效率比较低,因为进行了 synchronized 的处理.ArrayList是非线程安全的Vector是线程安全的HashMapHashSet是非线程安全的Hashtable是线程安全的

1.7 开发中如何处理线程安全问题及其注意事项

是一上来就选择线程同步吗?synchronized,不是,synchronized 会让程序的执行效率降低,用户体验不好,系统的用户的吞吐量降低,用户体验差,在不得以的情况下,再选择线程同步机制。

明确哪些代码是多线程运行的代码

明确多个线程是否有共享数据

明确多线程运行代码中是否有多条语句操作共享数据

对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其 他线程不可以参与执行。即所有操作共享数据的这些语句都要放在同步范围中

同步锁的使用注意:范围:范围太小:没锁住所有有安全问题的代码,范围太大:没发挥多线程的功能,过多的没有线程安全问题的代码,从多线程处理变成了单线程处理,效率降低了。

注意同步监视器 ”锁“的对象,是否需要所有线程同一把锁,以及对象锁,类锁的使用。

三种处理线程安全问题的,合理顺序:Lock ——> 同步代码块(已经进入了方法体,分配了相应资源)——> 同步方法(在方法体之外)

同步线程:解决了多线程的安全问题,但是同样也会降低一些效率。合理运用

2. 多线程的 ”死锁“ 现象

2.1 “死锁” 介绍

四锁:不同的线程分别占用对方需要的同步资源 “锁”不放弃,都在等待对方放弃自己需要的同步资源”锁“,就形成了线程的死锁。

出现了死锁之后,不会出现提示,只是所有线程都处于阻塞状态,无法继续,这种最难调试了。不过可以通过 JDK 自带的 jconsole 工具检测死锁

举例:编写一个死锁程序:如下,两个线程(线程一,线程二),两个锁(o1,o2)

package blogs.blog4;/*** 死锁现象*/public class ThreadTest9 {public static void main(String[] args) {Object o1 = new Object(); // 锁一Object o2 = new Object(); // 锁二Thread t1 = new MyLock1(o1,o2); // 线程一Thread t2 = new MyLock2(o1,o2); // 线程二// 设置线程名t1.setName("线程一:");t2.setName("线程二:");// 创建新线程,启动run()t1.start();t2.start();}}class MyLock1 extends Thread {private Object o1 = null;private Object o2 = null;public MyLock1() {super();}public MyLock1(Object o1, Object o2) {super(); // 调用父类的构造器this.o1 = o1;this.o2 = o2;}@Overridepublic void run() {// 锁一synchronized (o1) {System.out.println(Thread.currentThread().getName() + "begin");try {Thread.sleep(1000); // 当前线程睡眠 1s,模拟网络延迟了 1s} catch (InterruptedException e) {e.printStackTrace();}// 锁二synchronized (o2) {System.out.println(Thread.currentThread().getName() + "end");}}}}class MyLock2 extends Thread {private Object o1 = null;private Object o2 = null;public MyLock2() {super();}public MyLock2(Object o1, Object o2) {super(); // 调用父类的构造器this.o1 = o1;this.o2 = o2;}@Overridepublic void run() {// 锁一synchronized (o2) {System.out.println(Thread.currentThread().getName() + "begin");try {Thread.sleep(1000); // 当前线程睡眠 1s,模拟网络延迟了 1s} catch (InterruptedException e) {e.printStackTrace();}// 锁二synchronized (o1) {System.out.println(Thread.currentThread().getName() + "end");}}}}

使用JDK 中的 Jconsole 检测死锁的存在具体使用大家可以移步至 : 🔜🔜🔜 Java多线程:创建多线程的“四种“ 方式

分析上述代码形成 ”死锁“的原因:

线程一拿到 ”o1“ 锁,进入到语句块中,当线程一还想要再拿到 “02"锁的时候,这时候出现了网络延迟了1s(sleep(1000)模拟的),这时候的线程二趁机拿到了 ”o2” 锁,当线程二还想要再拿到 “o1” 锁时,已经不行了,因为“o1”锁被线程一拿到了,线程二只能等到线程一释放 “o1” 锁,才有可能拿到了,可是这个时候的线程一从网络延迟中恢复过来,想要去拿 “o2” 锁,拿不到了,因为已经被线程二给拿到了,现在就出现了这样一个死循环问题:

线程一想要的 ”o2“ 锁,在线程二手上抓住不放。而线程二想要的 ”o1“ 锁 ,在线程一手上抓住不放。线程一只有拿到了 ”o1" 和 “o2" 两把锁才会释放锁,而线程二也是只有拿到了 ”o1“ 和 ”o2“ 这两把锁才会释放锁,现在两个线程各个占用各个需要的同步资源:”锁“,互补相让。形成了”死锁“

如下图示:

这里我们注意到一点没有就是形成 ”死锁“ 的关键资源:同步资源”锁“被他方抢到了。

所以我们这里需要认识到:什么时候会释放锁,什么时候不会释放锁。知道了这些我们才可以避免写出死锁

2.2 释放锁的操作

当前线程的同步方法,同步代码块执行结束。当强线程的同步方法,同步代码块中遇到break,return终止了该代码块,该方法的执行。当强线程的同步方法,同步代码块中出现了未处理的 ErrorException异常,导致异常的结束。当强线程的同步方法,同步代码块中执行了线程对象的wait()方法,当前线程暂停,并释放当前线程所占用的

2.3 不会释放锁的操作

线程执行同步代码块 或 同步方法时,程序调用了Thread.sleep( ),Thread.yield( )方法暂停了当前线程的执行。线程执行同步代码块,其他线程调用了该线程的 **suspend( ) ** 方法,将该线程挂起,该线程不会释放锁(同步监视器)。应尽量避免使用suspend( )resume( )来控制线程。

2.4 如何避免 ”死锁“ 的出现

尽量避免编写出嵌套的 synchronized同步锁。尽量减少同步资源 ”锁“对象的定义。使用专门的算法,原则,规避。

3. 单例模式 ”懒汉式“ 的线程安全问题

存在多线程安全的单例模式的 ”懒汉式“的编写: 导致其中的instance经历了多次赋值,第一次是无效的,被第二次的线程给覆盖了,第三次线程覆盖了,只有最后一次线程的赋值才是有效的

如下:

package blogs.blog4;public class BankTest {}class Bank {private static Bank instance = null;// 构造器私有化private Bank() {}public static Bank getInstance() {if(instance == null) {instance = new Bank();}return instance;}}

解决方式一: 使用 synchronized 修饰 方法附加上同步锁,synchronized 修饰静态方法默认的是类.class类锁。

package blogs.blog4;public class BankTest {}class Bank {private static Bank instance = null;// 构造器私有化private Bank() {}public synchronized static Bank getInstance() {// 静态方法:默认是类.class 类锁if(instance == null) {instance = new Bank();}return instance;}}

解决方式二:使用 synchronized 修饰代码块,设置为 类.class 的锁。

package blogs.blog4;public class BankTest {}class Bank {private static Bank instance = null;// 构造器私有化private Bank() {}public static Bank getInstance() {synchronized (Bank.class) {// 设置类锁if (instance == null) {instance = new Bank();}}return instance;}}

方式二的优化: 方式二的第一种方式,效率低,因为存在这样一种情况:多线程等到锁。

如下所示:当多个线程需要等待获取 ”锁“ 进入if (instance == null) 时,其中只有第一个获取”锁“的线程,才执行了instance = new Bank();赋值的操作,因为等第一个线程赋值以后,instance != null ,无法赋值了,那前面的多个线程进入 synchronized {} 锁块以后,什么也没有干,那等了半天的 ”锁“进入。

就相当于是:排队核酸作检测,明明前面已经没有检测试剂了,却不早说,而是等,排到你的时候,才告诉你没有检测试剂了(让你白白浪费了时间),而不是已经没有试剂了,就告诉大家不要排队了,没有检测试剂。

如下优化:就是提前告诉其他线程,已经赋值好了,不要在排队了。就是当 if (instance == null) 的时候才进行 synchronized 同步锁机制

package blogs.blog4;public class BankTest {}class Bank {private static Bank instance = null;// 构造器私有化private Bank() {}public static Bank getInstance() {if (instance == null) {synchronized (Bank.class) {if (instance == null) {// 防止多线程安全问题,进一步再判断。instance = new Bank();}}}return instance;}}

4. 关于 ”锁“ 的面试题:

观察如下代码,思考其执行结果

4.1 题目一

该代码中: doSome()方法执行的时候需要等待doOther() 方法的锁释放结束吗???

package blogs.blog4;public class ThreadTest10 {public static void main(String[] args) {MyClass m1 = new MyClass();Thread t1 = new MyThread(m1);Thread t2 = new MyThread(m1); // 多态性t1.setName("t1");t2.setName("t2");t1.start();try {Thread.sleep(1000); // 这个睡眠的作用: 为了保证t1线程先执行} catch (InterruptedException e) {e.printStackTrace();}t2.start();}}class MyThread extends Thread{private MyClass mc = null;public MyThread(MyClass mc) {super();this.mc = mc;}@Overridepublic void run() {if("t1".equals(super.getName())) {mc.doSome();}if("t2".equals(super.getName())) {mc.doOther();}}}class MyClass {public static void doSome() {// 这里的同步监视器是: this 锁System.out.println("doSome begin");try {Thread.currentThread().sleep(1000*10);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("doSome over");}public static synchronized void doOther() {System.out.println("doOther begin");System.out.println("doOther over");}}

答:不用,因为虽然 doOther()是静态方法又被 synchronized 修饰了,默认是 类.class 类锁,所有对象,线程共用,doSome 也是静态方法所有对象,线程共用,但是 doSome 并没有附加 synchronized 同步锁,是不需要等待 doOther()方法的类锁释放的。

4.2 题目二

如果 doSome 加上 synchronized 后, doOther 方法执行的时候需要等待doSome方法的锁释放结束吗???

package blogs.blog4;public class ThreadTest10 {public static void main(String[] args) {MyClass m1 = new MyClass();Thread t1 = new MyThread(m1);Thread t2 = new MyThread(m1); // 多态性t1.setName("t1");t2.setName("t2");t1.start();try {Thread.sleep(1000); // 这个睡眠的作用: 为了保证t1线程先执行} catch (InterruptedException e) {e.printStackTrace();}t2.start();}}class MyThread extends Thread {private MyClass mc = null;public MyThread(MyClass mc) {super();this.mc = mc;}@Overridepublic void run() {if ("t1".equals(super.getName())) {mc.doSome();}if ("t2".equals(super.getName())) {mc.doOther();}}}class MyClass {public static synchronized void doSome() {// 这里的同步监视器是: this 锁System.out.println("doSome begin");try {Thread.currentThread().sleep(1000 * 10);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("doSome over");}public static synchronized void doOther() {System.out.println("doOther begin");System.out.println("doOther over");}}

答:需要因为 无论是 doSome() 还是 doOther() 方法都是静态方法,并且都被 synchronized 修饰了,默认是 类.class 类锁,类锁是仅仅只有一个所有对象,线程共用,所以 t2线程中的 doOther()方法需要等待 t1线程执行完 doSome()方法释放类锁,才可以执行。

4.3 题目三

该代码中: doSome()方法执行的时候需要等待doOther() 方法的锁释放结束吗???

package blogs.blog4;public class ThreadTest10 {public static void main(String[] args) {MyClass m1 = new MyClass();MyClass m2 = new MyClass();Thread t1 = new MyThread(m1);Thread t2 = new MyThread(m2); // 多态性t1.setName("t1");t2.setName("t2");t1.start();try {Thread.sleep(1000); // 这个睡眠的作用: 为了保证t1线程先执行} catch (InterruptedException e) {e.printStackTrace();}t2.start();}}class MyThread extends Thread {private MyClass mc = null;public MyThread(MyClass mc) {super();this.mc = mc;}@Overridepublic void run() {if ("t1".equals(super.getName())) {mc.doSome();}if ("t2".equals(super.getName())) {mc.doOther();}}}class MyClass {public static synchronized void doSome() {// 这里的同步监视器是: this 锁System.out.println("doSome begin");try {Thread.currentThread().sleep(1000 * 10);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("doSome over");}public static synchronized void doOther() {System.out.println("doOther begin");System.out.println("doOther over");}}

答:需要,因为这两个对象虽然是不同的,但是 synchronized static 修饰静态方法,默认是 类.class 类锁,类锁是仅仅只有一个所有对象,线程共用,所以 t2线程中的 doOther()方法需要等待 t1线程执行完 doSome()方法释放类锁,才可以执行。

4. 总结:

synchronized() 修饰代码块任何对象都可以设置为同步监视器”锁“,Object,“abc”,类.class,this 但是 同步监视器“锁”不可以为 null 不然报NullPointerExceptionnull 指针异常的。synchronized () 修饰非静态方法,默认是 this 对象锁,修饰静态方法,默认是 类.class 类锁,仅仅只会加载一次,所有对象,线程共用。无法修改java 三大变量在作为 同步监视器 ”锁“ 的线程安全问题的讨论避免写出 ”死锁“。会释放锁 ,不会释放锁的操作有哪些。单例模式中的 ”懒汉式“ 的优化 线程安全问题,提前告知法。

5. 最后:

限于自身水平,其中存在的错误,希望大家给予指教,韩信点兵——多多益善,谢谢大家,后会有期,江湖再见 !!!

Java多线程:多线程同步安全问题的 “三“ 种处理方式 ||多线程 ”死锁“ 的避免 || 单例模式”懒汉式“的线程同步安全问题

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。