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

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

在这里插入图片描述


每博一文案

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


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;

    @Override
    public 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;

    @Override
    public 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();

    @Override
    public 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;

    @Override
    public 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;

    @Override
    public 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;

    @Override
    public 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();   // 售票窗口1
        Thread t2 = new MyThread7();   // 售票窗口2
        Thread 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 静态的和类一起加载,仅仅只会加载一次,所有对象共享。

    @Override
    public 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();   // 售票窗口1
        Thread t2 = new MyThread7();   // 售票窗口2
        Thread 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 静态的和类一起加载,仅仅只会加载一次,所有对象共享。

    @Override
    public 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()方法,其票也已经出售完了。所以就出现了一个线程将所有票都出售完了。

@Override
    public 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();   // 售票窗口1
        Thread t2 = new MyThread7();   // 售票窗口2
        Thread 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 静态的和类一起加载,仅仅只会加载一次,所有对象共享。

    @Override
    public 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();

    @Override
    public 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;
    }

    @Override
    public 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;
    }

    @Override
    public 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;
    }

    @Override
    public 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;
    }

    @Override
    public 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;
    }

    @Override
    public 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. 总结:

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

5. 最后:

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


在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值