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 多线程同步的安全问题

所谓的多线程安全问题:

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

举例如下:

模拟火车站卖票,一共有 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. 三个售票窗口,三个线程(售票窗口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 的票号的火车票,导致的结果就是 有三个人买到了 同一张一模一样的火车票,如果始发站都一样的话:那可怕的就是:三个人座同一张座位,发生争执。
  2. 代码图示解析:

  1. 实例图解


1.2 synchronized 关键字的介绍

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

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

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

解决办法

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

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

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

synchronized几种写法

  1. 修饰代码块
  2. 修饰普通方法
  3. 修饰静态方法

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 ,编译器会报错,就算骗过了编译器,在运行的时候也是会报错的:NullPointerException null 异常。


补充:

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

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


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

Java当中有 三大变量

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

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

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

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

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

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

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()方法的解决线程安全问题的异同

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

  1. 首先创建 ReentrantLock 的实例对象,用于调用其中重写 Lock 接口中的抽象方法 lock() 获取锁,unlock() 释放锁,这里定义为成员变量
private ReentrantLock reentrantLock = new ReentrantLock();
  1. 在适合的位置,通过 lock() 显式的获取/启动 ”锁“
reentrantLock.lock(); // 2.调用lock()显式启动锁
  1. 最后在合适的位置调用 unlock() 显示释放当前线程的 ”锁“。一般是定义在 finally{} 中防止该线程因为一些异常原因,没有释放手中的锁,让其他线程拿到锁,无法访问。
  2. 注意点:一般是将 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 的对比

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

不同:

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

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

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

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

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

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

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

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

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

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

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

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

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

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多线程:多线程同步安全问题的 “三“ 种处理方式 ||多线程 ”死锁“ 的避免 || 单例模式”懒汉式“的线程同步安全问题相关推荐

  1. java源代码实例倒计时_Java倒计时三种实现方式代码实例

    写完js倒计时,突然想用java实现倒计时,写了三种实现方式 一:设置时长的倒计时: 二:设置时间戳的倒计时: 三:使用java.util.Timer类实现的时间戳倒计时 代码如下: package ...

  2. java jndi tomcat_tomcat下jndi的三种配置方式

    Java命名和目录接口(the Java naming and directory interface,JNDI)是一组在Java应用中访问命名和目录服务的API.命名服务将名称和对象联系起来,使得读 ...

  3. java实现定时任务 schedule_Java定时任务的三种实现方式

    前言 现代的应用程序早已不是以前的那些由简单的增删改查拼凑而成的程序了,高复杂性早已是标配,而任务的定时调度与执行也是对程序的基本要求了. 很多业务需求的实现都离不开定时任务,例如,每月一号,移动将清 ...

  4. java倒计时_Java倒计时三种实现方式代码实例

    写完js倒计时,突然想用java实现倒计时,写了三种实现方式 一:设置时长的倒计时: 二:设置时间戳的倒计时: 三:使用java.util.Timer类实现的时间戳倒计时 代码如下: package ...

  5. Java多线程的三种实现方式(重点看Collable接口实现方式)

    1.通过继承Thread类来实现多线程 在继承Thread类之后,一定要重写类的run方法,在run方法中的就是线程执行体,在run方法中,直接使用this可以获取当前线程,直接调用getName() ...

  6. 【多线程】线程同步问题的三种解决方法

    目录 一.前言 二.同步代码块 三.同步方法 四.Lock方法 五.总结 一.前言 解决线程同步问题有三种方式:同步代码块.同步方法.锁(JDK5新增) 使用synchronized 解决线程同步问题 ...

  7. java线程三种创建方式与线程池的应用

    前言:多线程下程序运行的结果是随机的,以下案例代码的运行结果仅供参考 一 通过继承Thread线程创建的方法与执行的步骤 /* 1 继承Thread 2重写run方法 3创建线程继承类的子类 4 调用 ...

  8. 读取Java文件到byte数组的三种方式及Java文件操作大全(包括文件加密,String加密)

    读取Java文件到byte数组的三种方式 package zs;import java.io.BufferedInputStream; import java.io.ByteArrayOutputSt ...

  9. java如何实现定时任务_Java定时任务的三种实现方式

    前言 现代的应用程序早已不是以前的那些由简单的增删改查拼凑而成的程序了,高复杂性早已是标配,而任务的定时调度与执行也是对程序的基本要求了. 很多业务需求的实现都离不开定时任务,例如,每月一号,移动将清 ...

最新文章

  1. 【问题收录】Ubuntu(14.04)那些我遇到的各种事
  2. [Spring cloud 一步步实现广告系统] 9. 主类和配置文件
  3. 使用OpenCV与百度OCR C++ SDK实现文字识别
  4. 小程序开发总结一:mpvue框架及与小程序原生的混搭开发
  5. Java八大基本数据类型和对应的封装类型
  6. MySQL本人工具使用
  7. Android android:screenOrientation的简介
  8. 如何在服务器上部署pdf文件,详解如何在云服务器上部署Laravel.pdf
  9. php header()的用法
  10. c++ const常量的实现机制(转载)2
  11. 安装you-get和ffmpeg for Mac
  12. Flash动作补间动画
  13. CUDA安装失败,及解决方案
  14. InstallShield教程-打包.NET程序
  15. 全球及中国电子材料市场需求分析与十四五投资潜力预测报告2021年版
  16. C++ 相关职位的要求
  17. 从明源动力到创新工场这一路走来
  18. UNITER: UNiversal Image-TExt Representation Learning
  19. chrome浏览器使用js导出Excel出现网络错误
  20. 【感悟】20岁的人生不应该停止奋斗。----------------努力努力再努力

热门文章

  1. python除法运算总结
  2. 【CodeForces 1100E】二分答案 | 拓扑排序 | E
  3. javaScript中时间的加减
  4. linux php pear 安装,全面解析ubuntu下安装php、pear
  5. 机器学习:Gan(生成对抗网络)
  6. 天津理工大学计算机调剂信息,天津理工大学2020考研调剂信息发布
  7. SCP Linux远程下载命令
  8. 虚拟机red hat linux下载,Red Hat Enterprise Linux 官方正式版镜像下载
  9. 守护农场Guarding the Farm
  10. 表达式求值(C语言实现)