【JavaEE】多线程(3)

首先回顾一下线程不安全的原因:

  1. 线程是随机调度,抢占式执行的
  2. 修改共享数据,多个线程修改同一个变量
  3. 多个线程修改共享数据的操作不是原子性,(count++是3个CPU指令,但是赋值操作就是原子性的)
  4. 内存可见性问题
  5. 指令重排序

前三点已做讲解,接下来对最后两点进行讲解

一、内存可见性问题

1.1 引入概念

先来看下面的代码:

java 复制代码
public class Demo4 {
    public static int count = 0;
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while (count == 0) {
                ; //循环体为空
            }
        });

        Thread t2 = new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入一个整数");
            count = scanner.nextInt();
        });
        
        t1.start();
        t2.start();
    }
}

上述代码就是t1线程来读count,t2来修改count,以原来的逻辑来看:当把count修改为一个非0的值后,t1线程就会结束

输入之后发现,程序没有任何反应,说明t1线程并没有结束,接下来我们仍然站在指令的角度来解释,t1线程中的循环条件count == 0相当于两个指令

  • load:读取内存中的数据到CPU寄存器
  • cmp:比较寄存器中的数据,条件成立就继续执行循环体中的逻辑,不成立就跳转到另外一个地址执行

当前循环体为空,意味着循环速度很快,由于CPU访问寄存器的速度远大于访问内存的速度,所以load执行消耗的时间远多于cmp,也就是执行一次load,会执行很多次load

t2线程中是我们要手动修改count的,要知道load是计算机执行的指令,肯定比人要快很多,所以在t2修改之前会执行很多次的load,JVM发现每次load执行的结果都一样就会把load操作优化掉,后续再执行到对应的代码就不再真正load,而是直接读取load过的寄存器中的值了

上述优化的初衷是为了让程序执行的速度更快,但在多线程这里反而引起了bug

在上述代码中添加一个IO操作或者阻塞操作,循环速度就会大幅降低,也就不会优化掉load,IO操作是不会被优化的:

java 复制代码
public static int count = 0;
Thread t1 = new Thread(() -> {
    while (count == 0) {
        System.out.println("执行IO操作");
    }
});

总结:上述问题本质是编译器/JVM优化引起的,一个线程对共享变量的修改可能不会被其他线程立即看到,导致其他线程读取到的可能是旧值,从而引发线程安全问题。这就是内存可见性问题

那么该如何解决该问题

1.2 volatile 关键字

给变量加上volatile关键字后,编译器就不会触发上述优化

java 复制代码
public class Demo {

    public static volatile int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            while (count == 0) {
            }
        });

        Thread t2 = new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入一个整数");
            count = scanner.nextInt();
        });


        t1.start();
        t2.start();
    }

}

注意:volatile只能保证内存可见性,并不能保证操作的原子性

java 复制代码
public class Demo {
    private static volatile int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count = "+ count);
    }

}

二、线程等待通知机制

2.1 引入概念

先看下面一个ATM取钱的场景

此时小新正在取钱,发现ATM机中钱不够,于是就开锁出来,接下来就该其他人去取钱,但有可能小新觉得自己操作不对就又进去,出来之后发现又不对于是又进去,像这样某个线程频繁获取释放锁,以至于其他线程分配不到CPU资源的问题称为"线程饿死"

系统中线程调度是无序的,线程饿死的情况就有可能出现,但注意:这并不是死锁,死锁是卡死,而线程饿死只会卡住一下下

线程等待通知机制可以调整线程的执行顺序来解决这个问题 ,通过添加判断条件判定当前逻辑是否能够执行,如果不能就wait(主动进行阻塞)就把执行的机会让给别的线程了,避免该线程进行无意义的重试

2.2 wait()方法

wait()做的事情:

  • 使当前执行代码的线程进行等待(把线程放在等待队列中)
  • 释放当前的锁
  • 被唤醒时,重新尝试获取这个锁

可以看到wait()做的事情有释放当前的锁,也就是wait()必须放在synchronized里面,否则会抛出异常:

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

正确的写法如下:

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

当前代码会一直等待下去

wait()结束等待的条件:

  • 其他线程调用该对象的notify()
  • wait等待时间超时(和sleep(1000)效果类似,wait(1000),就是等待1s后如果没有被唤醒就自动唤醒)
  • 其他线程调⽤该等待线程的interrupted⽅法,导致wait抛出InterruptedException异常

2.3 notify()方法

notify 方法用来唤醒等待线程的

  • 该⽅法是⽤来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁
  • 如果有多个线程等待,则有线程调度器随机挑选出⼀个呈wait状态的线程(并没有"先来后到")

看如下示例:

java 复制代码
public class Demo {
    public static void main(String[] args) {
        Object locker = new Object();

        Thread t1 = new Thread(() -> {
            synchronized (locker) {
                System.out.println("t1等待之前");
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("t1等待之后");
            }
        });

        Thread t2 = new Thread(() -> {
           synchronized (locker) {
               System.out.println("t2等待之前");
               locker.notify();
               System.out.println("t2等待之后");
           }
        });

        t1.start();
        t2.start();
    }

}

打印结果:

注意:t2线程的notify()执行完后,并不会释放锁,而是代码走出synchronized后才会真正把锁释放t1线程拿到锁之后继续执行,因此肯定先打印t2等待之后,后打印t1等待之后

2.4 notifyAll() 方法

notifyAll 可以一次唤醒所有的等待线程

java 复制代码
public class Demo {
    public static void main(String[] args) throws InterruptedException {
        Object locker = new Object();

        Thread t1 = new Thread(() -> {
            synchronized (locker) {
                System.out.println("t1等待之前");
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("t1等待之后");
            }
        });

        Thread t2 = new Thread(() -> {
            synchronized (locker) {
                System.out.println("t2等待之前");
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("t2等待之后");
            }
        });

        Thread t3 = new Thread(() -> {
           synchronized (locker) {
               System.out.println("t3等待之前");
               locker.notifyAll();
               System.out.println("t3等待之后");
           }
        });

        t1.start();
        t2.start();
        Thread.sleep(1000);
        t3.start();

    }

}

在t3.start()方法之前放一个sleep是为了防止t3先执行了notify,此时t1或t2还没有wait,此时直接notify没有任何效果,也不会抛异常,放一个sleep是为了保证先wait再notify

接下来看代码的执行效果

所有线程都执行结束,如果改为notify方法,再看代码的执行效果

由于notify会随机唤醒一个等待线程,这里唤醒的t1,此时t3没有被唤醒也就不会尝试获取锁,没有锁就不会继续执行接下来的逻辑,所以t3一直处于等待

如果不想让t3一直等下去,就将t3的wait改为带有时间版本的,这样时间一到就会自动被唤醒

2.5 面试题:wait() 和 sleep()的区别

  1. wait必须搭配 synchronized 来使用,否则会抛出IllegalMonitorStateException 异常,而 sleep可以在任何地方使用
  2. wait是Object类的一个普通方法 ,sleep是Thread类的一个静态方法
  3. 线程可以等 sleep 中的计时结束后主动唤醒 ,但如果是无参版本的 wait,则需要等其他线程调用 notify 或 notifyAll 来被动唤醒
  4. 调用 sleep 方法线程会进入TIMED_WAITING 有时限等待状态,而调用无参数的 wait 方法,线程会进入 WAITING无时限等待状态

相关推荐
漂流瓶66666613 分钟前
Scala的模式匹配变量类型
开发语言·后端·scala
夏天吃哈密瓜18 分钟前
Scala中的正则表达式01
大数据·开发语言·后端·正则表达式·scala
2401_8337880520 分钟前
Scala的模式匹配(2)
java·开发语言
Lbs_gemini060320 分钟前
C++研发笔记14——C语言程序设计初阶学习笔记12
c语言·开发语言·c++·笔记·学习
ac-er888844 分钟前
GD库如何根据颜色生成纯色背景图
开发语言·php
悠悠龙龙2 小时前
框架模块说明 #05 权限管理_03
java·开发语言·spring
开心羊咩咩3 小时前
Idea 2024.3 突然出现点击run 运行没有反应,且没有任何提示。
java·ide·intellij-idea
waterme1onY3 小时前
IDEA中MAVEN的一些设置问题
java·maven·intellij-idea
阿华的代码王国3 小时前
【算法】——前缀和(矩阵区域和详解,文末附)
java·开发语言·算法·前缀和
梦.清..3 小时前
面向对象(二)——类和对象(上)
java