JavaEE|多线程(五)

关于线程安全问题的总结

1.是啥

一段代码,在多线程中,并发执行后,产生bug

2.原因

  1. 操作系统对于线程的调度是随机的,抢占式执行[根本]
  2. 多个线程同时修改一个变量
  3. 修改操作不是原子的
  4. 内存可见性 -> 编译器优化
  5. 指令重排序

3.解决方案

(1)加锁

synchronized(锁对象){

需要加锁的代码

}

(2)volatile

编译器优化,出bug

使用这个关键字修饰的变量,就属于"易失""易变",必须每次重新读取内存中的数据

4.死锁

一旦代码触发死锁,此时线程就卡住了

原因

  1. 互斥

  2. 不可剥脱/不可抢占

  3. 请求和保持

  4. 循环等待

解决死锁

  1. 避免循环嵌套 = > 打破3
  2. 约定加锁顺序 = >打破4

JMM

JMM是java内存模型

缓存

寄存器虽然快但空间小存放不了的多少东西,于是在cpu上另外建设了一些存储空间,称为缓存

workmemory

这是Java官方文档的术语

每个线程,有一个自己的"工作内存"(work memory),同时这些线程共享同一个"主内存"(main

memory)

工作内存可以理解为存储空间,是寄存器与缓存的综合

当一个线程循环进行上述读取变量操作的时候,就会把主内存中的数据,拷贝到该线程的工作内存

后续另一个线程修改,也是先修改自己的工作内存,拷贝到主内存里。

由于第一个线程仍然在读自己的工作内存,因此感知不到主内存的变化。

这与前面讲到的把读内存的操作优化成都寄存器的操作类似

wait/notify

当多个线程竞争一把锁的时候,获取到锁的线程如果释放了,其他线程也不一定能拿到锁,因为线

程调度是随机的,充满不确定性

其他线程都属于锁上阻塞状态,当前这个释放锁的线程是就绪状态,这个线程有很大概率再次拿到

这把锁

wait和notify都是Object的方法,Java中的任意对象都提供了wait和notify

Java标准库中每个阻塞的方法都会抛出这个异常,意味着随时可能会被Interrupt唤醒

wait的使用

java 复制代码
 public static void main(String[] args) throws InterruptedException {
        Object ob=new Object();
        System.out.println("wait之前");
        ob.wait();
        System.out.println("wait之后");
    }
java 复制代码
 synchronized (ob){
            ob.wait();
        }

非法的锁状态,对于ob.wait()这个方法来说,第一件事就是先释放object对象对应的锁,能够释放

锁的前提是object对相应处于加锁状态才能释放

java 复制代码
 synchronized (ob){
            ob.wait();
        }

代码进入wait,就会先释放锁,并且阻塞等待,如果其他线程做完了必要工作,调用notify唤醒这

个wait线程,wait就会阻塞等待,重新获取锁,继续执行并返回。

注意

这里要求synchronized的锁对象必须和wait的对象是同一个

notify的使用

wait 操作必须要搭配锁来进行,wait 会先释放锁。notify 操作原则上说不涉及到加锁解锁操作,但在Java 中,也强制要求 notify 搭配 synchronized

java 复制代码
synchronized (lock){
                lock.notify();
            }
java 复制代码
 public static void main(String[] args) {
        Object lock=new Object();
        Thread t1=new Thread(()->{
            try {
                Thread.sleep(1000);
                System.out.println("wait之前");
                synchronized (lock){
                    lock.wait();
                }
                System.out.println("wait之后");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });
        Thread t2=new Thread(()->{
            Scanner sc=new Scanner(System.in);
            System.out.println("输入任意内容,唤醒t1");
            sc.next();
            synchronized (lock){
                lock.notify();
            }
        });
        t1.start();
        t2.start();
    }

sc.next就是一个带有阻塞的操作,等待用户在控制台输入

locker.notify()这里同样需要先拿到锁,再进行notify

注意

这四处必须是相同对象

wait和notify是针对同一个对象才能生效,这两个相同对象是线程沟通的桥梁

如果不是两个相同的对象,则没有任何相互的影响和作用

同时要务必确保先wait再notify才有作用,如果先notify再wait,此时wait无法被唤醒

一个notify唤醒一个wait,当出现多个wait,notify只会随机唤醒一个

java 复制代码
  public static void main(String[] args) throws InterruptedException {
        Object lock=new Object();
        Thread t1=new Thread(()->{
            try {
                System.out.println("t1的wait之前");
                synchronized (lock){
                    lock.wait();
                }
                System.out.println("t1的wait之后");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        },"t1");
        Thread t2=new Thread(()->{
            try {
                System.out.println("t2的wait之前");
                synchronized (lock){
                    lock.wait();
                }
                System.out.println("t2的wait之后");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        },"t2");
        Thread t3=new Thread(()->{
            Scanner sc=new Scanner(System.in);
            System.out.println("输入");
            sc.next();
            synchronized (lock){
                lock.notify();
            }
        });
    t1.start();
    t2.start();
    t3.start();
    }

可以通过多增加notify()来唤醒另外一个线程

notifyAll

notifyAll一次唤醒所有的wait线程

wait与join的区别

join也是等,但是等另外一个线程彻底执行完才继续走,join有等待时间

wait也是等,等到另外一个线程执行notify才继续走(不需要执行完另一个线程),wait也引入了超时间等待

wait可以使用notify提前唤醒

sleep也可以使用Interrupt提前唤醒,Interrupt看起来是唤醒sleep,其实本身的作用是通知线程终

wait和sleep最主要的区别在于针对锁的操作

  1. wait必须要搭配锁,才能用wait,sleep不需要
  2. 如果都是在synchronized内部使用,wait会释放锁,sleep不会释放锁
java 复制代码
synchronized (locker){
            Thread.sleep(1000);
        }

sleep是抱着锁睡,其他线程是没法获取这个锁的

多线程案例

针对不同的场景有不同的设计模式。

单例模式

单例模式是一种设计模式,单例模式强制要求一个类不能创建多个对象,在代码中,如果创建了多

个实例,直接编译失败

单例模式的实现有以下两种方式

饿汉模式和懒汉模式

饿汉模式

类加载的同时, 创建实例.

java 复制代码
class Singleton{
    private static Singleton singleton=new Singleton();
    public static Singleton getSingleton(){
        return singleton;
    }
    private Singleton(){

    }
}
public class Demo05 {
    public static void main(String[] args) {
        Singleton singleton1=Singleton.getSingleton();
        Singleton singleton2=Singleton.getSingleton();
        System.out.println(singleton1==singleton2);
    }
}

静态成员的初始化是在类加载时的阶段触发的

类加载往往就是在程序一启动就会触发

对于不同情况下,需要传入参数的话,通过创建不同的私有构造方法来实现不同参数传入

懒汉模式

懒汉模式与饿汉模式创建实例的时间相反,懒汉模式创建实例尽量晚

java 复制代码
class SingletonLazy{
private static SingletonLazy singletonLazy=null;
public static SingletonLazy getSingletonLazy(){
        if (singletonLazy==null){
        singletonLazy=new SingletonLazy();
        }
        return singletonLazy;
        }
private SingletonLazy(){

        }
        }
public class Demo06 {
    public static void main(String[] args) {

    }
}

懒汉模式下创建实例的时机是在第一次使用的时候,而不是在程序启动的时候

懒汉模式-多线程版

上面的懒汉模式的实现是线程不安全的

线程安全问题发生在首次创建实例时. 如果在多个线程中同时调用 getSingletonLazy方法, 就可能导

致创建出多个实例

加锁可以改善这里的线程安全问题

java 复制代码
 public static SingletonLazy getSingletonLazy() {
        synchronized (lock) {
            if (singletonLazy == null) {
             singletonLazy = new SingletonLazy();
         }
            return singletonLazy;
        }
    }

加入锁之后执行的线程就会在加锁的位置阻塞,阻塞到前一个线程解锁,当后一个线程进入的条件

的时候,前一个线程已经修改完毕,singletonLazy不再为null,就不会进行后续的new操作

这个写法相当于锁对象换成了类对象SingletonLazy.class和之前的locker相比没区别

加锁后有引入新的问题

当把实例创建好了之后,后续再调用 getSingletonLazy 都是直接执行 return,如果只是进行 if 判

定 + return是纯粹的读操作,而对于读操作,不涉及到线程安全问题.但是,每次调用上述的方法,

都会触发一次加锁操作,虽然不涉及线程安全问题了。多线程情况下,这里的加锁就会相互阻塞~

影响程序的执行效率.

java 复制代码
public static SingletonLazy getSingletonLazy() {
        if (singletonLazy == null) {
            synchronized (lock) {
                if (singletonLazy == null) {
                    singletonLazy = new SingletonLazy();
                }
               
            }
        }
        return singletonLazy;
    }

多加的锁是涉及实例创建的,如果实例已经创建就不涉及线程安全问题;如果还没创建就涉及线程

的安全问题

这个看起来应该没啥问题了吧,但是很抱歉,还是有问题的

我们先引入一个概念

指令重排序

指令重排序是在逻辑不变的前提下,修改代码执行的顺序

懒汉模式-多线程版(改进)

多线中,两次判定之间可能存在其他线程就把if中的singletonLazy变量修改了,也就导致这里的两

次if的结论可能不同

t1线程读取singletonLazy的时候,t2进行修改,指令重排序会在这里出现问题

对于这个new操作,执行顺序是

  1. 申请内存空间
  2. 在空间上构造对象
  3. 内存空间的首地址,赋值给引用变量

正常来说是按1,2,3的顺序执行下去,但在指令重排序的情况下,可能成为1,3,2的顺序

(单线程环境下,1,2,3还是1,3,2其实无所谓)

多线程的背景下,执行第一个if操作时,另外线程调走可能导致拿到一个"未初始化"的对象

使用volatile

这里我们可以通过增加volatile来解决这个问题

volatile的功能有两方面

1.确保每次读取操作都是都内存--解决内存可见性问题

2.关于该变量的读取和修改操作不会触发重排序--解决指令重排序问题

相关推荐
疋瓞2 小时前
pringBoot + 若依框架开发与部署流程
java
forEverPlume2 小时前
c++如何通过解析二进制包头信息解决Socket传输过程中的粘包问题【详解】
jvm·数据库·python
豆豆2 小时前
高校网站用什么CMS?站群管理+国产化适配方案
java·大数据·cms·建站系统·信创国产化·高校网站·站群cms
玉小格2 小时前
对py作业的一个复盘
开发语言·python
Rust研习社2 小时前
使用 Tonic 构建高性能异步 gRPC 服务
开发语言·网络·后端·http·rust
captain3762 小时前
JDBC(Java Data Base Connectivity)
java·开发语言
longxibo2 小时前
【flowable 7.2.0 二开之三:基于 Flowable 7.2 的审批流系统解压即用】
java·tensorflow·jar
南境十里·墨染春水2 小时前
C++笔记 STL——vector
开发语言·c++·笔记
zhangchaoxies2 小时前
c++怎么在Linux下获取文件被最后一次访问的精确纳秒时间【进阶】
jvm·数据库·python