JavaEE初阶Day 8:多线程(6)

Day8:多线程(6)

回顾

Ⅰ.可重入特性:同一个线程针对同一个对象,多次加锁(嵌套枷锁),解决可重入问题:

  • 需要让锁对象记录哪个线程持有的锁
  • 引入引用计数

Ⅱ.死锁:由于不正确的加锁,导致程序中的某些线程被卡死了(严重bug)

  • 一个线程,一把锁,连续加锁多次,并且这个锁是不可重入锁(synchronized不适用)
  • 两个线程,两把锁,线程1获取到锁A,线程2获取到锁B,然后线程1和线程2分别尝试获取对方的锁
  • N个线程,M把锁:哲学家就餐问题,可以用银行家算法解决

Ⅲ.产生死锁的必要条件(缺一不可)

  • 互斥(锁的基本特性)
  • 不可抢占/不可剥夺(锁的基本特性)
  • 请求和保持
  • 循环等待

避免锁嵌套使用,如果确实需要嵌套使用锁,一定要约定好加锁的顺序

1. 内存可见性问题

1.1 内存可见性问题介绍

package thread;

import java.util.Scanner;

public class Demo23 {

    private static int count = 0;

    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            while (count==0){

            }
            System.out.println("t1执行结束");
        });

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

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

预期效果:t1首先会进入循环,用户输入非0整数,就会使t1线程退出循环,结束线程

但是t1实际上并没有真正出现退出的情况,上述问题产生的原因,就是内存可见性,上述代码,是一个线程写,一个线程读,下面通过站在指令的角度来理解:

while (count==0){

}

上述while循环涉及如下指令:

  • load:从内存读取count数据到cpu寄存器
  • cmp:比较的同时会产生跳转
    • 条件成立,继续顺序执行;
    • 条件不成立,就跳转到另一个地址来执行

于是产生了内存可见性问题:

  • 由于上述代码,循环体是空着的,后续就没有别的指令
  • 当前循环体速度很快,短时间内出现大量的load和cmp反复执行的效果,load执行消耗的时间会比cmp多很多
  • 另外,JVM还发现每次load执行的结果,其实是一样的(在t2修改之前),干脆JVM就把上述load操作优化掉了,只是第一次真正进行load,后续再执行到对应的代码,就不再真正load了,而是直接读取刚才已经load过的寄存器中的值了

上述过程,确实是多线程产生的问题,但是另一方面,也是编译器优化/VM优化产生的问题,正常来说,优化操作,需要保证逻辑是等价的。但是很遗憾,编译器/JVM 在单线程代码中,优化是比较靠谱的,一旦程序引入多线程了编译器/JVM 判断也就不那么准确了

但是,如果上述代码中,循环体内存在IO操作或者阻塞操作(sleep),这就会使循环的执行速度大幅度降低,由于IO操作不能被优化掉(IO操作反复执行的结果是不相同的),而且IO操作的速度慢于load,于是此时就没有优化load的必要的

1.2 内存可见性问题解决

引入volatile关键字,给变量修饰上这个关键字之后,此时编译器就知道这个变量是“反复无常的”,不饿能按照上述策略进行优化了

package thread;

import java.util.Scanner;

public class Demo23 {
    private volatile static int count = 0;

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while (count == 0) {
            
            }
            System.out.println("t1 执行结束");
        });

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

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

private volatile static int count = 0;:告诉编译器,不要触发上述优化,具体在Java中是让javac生成字节码的时候产生了“内存屏障”(javac生成的特定的字节码)相关的指令,但是这个操作和之前synchronized保证的原子性没有任何关系

volatile是专门针对内存可见性的场景来解决问题的

  • 类似的,上述内存可见性也可以使用synchronized在一定程度上解决,引入synchronized是因为加锁操作本身太重量了,相对于load来说,开销更大,编译器自然就不会对load优化了(类似前面讲到的加sleep/IO操作)

  • 优化是javac和java配合完成的工作,统称为编译器优化

  • 在Java中,编译器有且仅有javac,java是jvm(运行环境)

  • IDEA是IDE,是集成开发环境,涵盖代码编译器、依赖管理器、编译器、调试器、工程管理工具等

  • 编译器什么时候优化,什么时候不优化,也是一个“玄学”问题

JMM:Java内存模型

JMM对上述问题的表述:当t1执行的时候,要从工作内存(CPU寄存器+缓存)中读取count的值,而不是从主内存中,后续t2修改count,也是会先修改工作内存,同步拷贝到主内存,但是由于t1没有重新读取主内存,最终导致t1没有感知到t2的修改

2. 线程的等待通知机制

之前提到的join是等待线程结束,此处提到的等待通知,等待代码中会给我们进行显式的通知(不一定要结束),可以更加精细的控制线程之间的执行顺序

系统内部,线程是抢占式执行,随即调度的,程序员也是有手段干预的,通过“等待”的方式,能够让线程一定程度的按照预期的顺序来执行,虽然无法主动让某个线程被调度,但是可以主动让某个线程等待(就给别的线程机会了)

2.1 线程饿死

如果某个线程频繁获取释放锁,由于获取的太快,以至于其他线程得不到CPU资源,这种问题称为线程饿死

系统中的线程调度是无序的,上述情况很可能出现,虽然不会像死锁那样卡死,但是可能会卡住一下,对于程序的效率是有影响的

例子

  • 比如:1号线程要进行某个工作,进行这个工作,需要前提条件,如果前提条件不满足,就得稍后重试
  • 比如:某个线程要从一个队列中读取元素,结果当前队列是空着的,就只能稍后读取了

2.2 等待通知机制

等待通知机制就能够解决上述问题

通过条件,判定当前逻辑是否能够执行,如果不能执行,就主动wait(主动进行阻塞),这样就把执行的机会让给别的线程了,避免该线程进行一些无意义的重试,等到后续条件时机成熟了(需要其他线程进行通知),再让阻塞的线程被唤醒

2.2.1 wait
package thread;

public class Demo24 {
    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        System.out.println("等待之前");
        synchronized (object){
            object.wait();
        }
        System.out.println("等待之后");
    }
}
  • object.wait();是Object类提供的方法,任何一个对象都有这个方法

  • 此处wait也是会被Interrupt打断,wait和sleep一样,能够自动清空标志位

  • wait内部做的事情不仅仅是阻塞等待,还要解锁,准且来说,wait解锁的同时进行等待,相比之下sleep是阻塞等待但是和锁无关,所以线程得先加上锁,那么,wait必须放到synchronized内部使用

2.2.2 notify

通过另一个线程,调用notify来唤醒阻塞的线程

package thread;

import java.util.Scanner;

public class Demo25 {
    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(() ->{
            Scanner scanner = new Scanner(System.in);
            synchronized (locker){
                System.out.println("t2通知之前");
                // 借助 scanner 控制阻塞. 用户输入之前, 都是阻塞状态.
                scanner.next();
                locker.notify();
                System.out.println("t2通知之后");
            }

        });

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

    }
}

用户输入内容之后,此时就会使next接触阻塞,进一步的执行到notify,notify就会唤醒上述wait操作,从而使t1能够回到RUNNABLE状态,并且参与调度

倘若:t2先执行,有可能t2先执行了notify,此时t1还没有wait,那么locker上没有现成的wait,直接notify不会有任何效果(也不会抛出异常),但是后续t1进入wait之后,没有别的线程将其唤醒了

package thread;

public class Demo26 {
    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.notify();
                System.out.println("t3 通知之后");
            }
        });

        t1.start();
        t2.start();
        Thread.sleep(100);
        t3.start();
    }
}

notify只能唤醒多个等待线程中的一个,notify唤醒的线程是随机的

locker.wait(1000);:超过1000ms还没有被notify,就自动唤醒

locker.notifyAll();:唤醒所有的等待线程

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

胖了你都蹲不下来撸猫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值