JavaEE初阶-线程3

文章目录


一、线程安全问题-内存可见性

java 复制代码
import java.util.Scanner;

public class Demo27 {
    private static int count=0;
    //下面这段代码会出现内存的可见性问题
    //将从内存中读取count值的操作称为load 判断操作称为cmp
    //load和cmp的执行速度差了好几个数量级,在线程2开始执行代码提示输入数字时,线程1的while循环已经执行了很多遍
    //java编译器会自动给代码进行优化
    //导致load只是第一次时真正从内存中读取count值,其余都是从cpu的寄存器中读取
    //然而线程2修改count是在内存中进行修改,线程1根本访问不到count的值
    //可以在变量前加上volatile关键字来提醒编译器不要优化
    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和线程t2,线程t1中建立一个循环,线程t2控制循环中的条件,按照预期,应该在t2中改变条件之后t1结束,整个程序也应该结束,但事实并非如此。

输入数字之后线程一仍然没有结束,这是因为count==0这个条件涉及到两个操作load和cmp,先将count的值从内存中载入cpu的寄存器进行比较,在线程二中还未输入数字时,线程一的循环已经执行很多次了,编译器发现这么多次count的值没有变化就会进行优化,只有第一个load会从内存中读count的值,剩余次数都是直接用寄存器中的值,这样既使后来在线程二当中改变了内存中的count的值,因为循环条件的count不从内存中读值因此访问不到修改后的count,线程一就会继续循环,从而程序就结束不了。如果不想让编译器进行优化,可以在count前面加上volatile关键字即可。

另外上述的内存可见性问题加锁,即包含在synchronized内也可以解决,因为加锁是比较重量的,load相对就不算慢了就不会触发优化。在循环内加上sout输出语句也是一个道理,sout相对于load更慢无法触发优化,自然也能解决上述的内存可见性的问题。

在网上查询资料还可以看到以一种JMM(java内存模型)角度来理解该问题的解答:

当t1执行的时候,会从工作内存中读取count而不是从主内存中。

t2修改count会先修改工作内存,再同步到主内存,但由于t1没有重新读主内存,导致t1没有感知到t2的修改。

二、等待通知

2.1 wait()方法

代码示例如下:

java 复制代码
public class Demo28 {

    public static void main(String[] args) throws InterruptedException {
        Object locker = new Object();
        
        System.out.println("等待之前");
        //wait必须在synchronized内使用,会解锁并且让线程进入阻塞等待状态
        synchronized (locker) {
            locker.wait();
        }

        System.out.println("等待之后");

    }
}

wait方法是object类的方法,任何类对象都可以使用,它只能在锁内使用,如果执行wait方法会释放锁并且线程进入阻塞状态waiting。wait也有限时的参数,加上时间,会在限定时间内阻塞之后自动唤醒。
注意:wait和sleep一样可以被interrupt打断并且清空标志位。

2.2 notify()方法

通过notify方法来唤醒wait方法进入阻塞的线程,wait的线程状态从Waiting->Runnable->Blocked。

代码示例如下:

java 复制代码
public class Demo29 {
    public static void main(String[] args) throws InterruptedException {

        Object locker = new Object();
        //1.wait和notify必须都在synchronized内
        //2.一个notify唤醒一个wait() 如果多个线程wait就使用多个notify或者notifyAll 这种唤醒时随机的
        //3.如果想唤醒指定线程也可以必须一对一写好锁对象
        //4.notify需要在wait之后

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


        });


        Thread t2 = new Thread(() -> {
            System.out.println("t2 通知之前");
            Scanner scanner = new Scanner(System.in);

            synchronized (locker) {
//控制阻塞,不输入数字t1仍是阻塞
                scanner.nextInt();
                locker.notify();
            }
            System.out.println("t2 通知之后");
        });


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

    }

}

notify必须也在synchronized之内,并且一个notify只能唤醒对应的一个wait,如果多个线程wait就使用多个notify或者notifyAll,这种唤醒时随机的,唤醒的前提是通过一个对象来调用的wait及notify方法。如果想唤醒指定线程也可以必须一对一写好锁对象。另外notify的使用必须在wait之后,如果在wait之前使用notify不会有任何效果,不过wait就没有办法唤醒了。

使用相应的锁对象进行一对一唤醒代码示例如下:

java 复制代码
public class Demo30 {

    public static void main(String[] args) throws InterruptedException {
        Object locker = new Object();
        Object locker1 = 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 (locker1) {
                System.out.println("t2等待之前");
                try {
                    locker1.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("t2等待之后");
            }


        });


        Thread t3 = new Thread(() -> {
            synchronized (locker) {
                //locker.notifyAll();
                locker.notify();
            }

            synchronized (locker1) {
                locker1.notify();
            }


        });


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

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

    }

}
相关推荐
小李不想输啦40 分钟前
什么是微服务、微服务如何实现Eureka,网关是什么,nacos是什么
java·spring boot·微服务·eureka·架构
张铁铁是个小胖子41 分钟前
微服务学习
java·学习·微服务
ggs_and_ddu42 分钟前
Android--java实现手机亮度控制
android·java·智能手机
AITIME论道2 小时前
论文解读 | EMNLP2024 一种用于大语言模型版本更新的学习率路径切换训练范式
人工智能·深度学习·学习·机器学习·语言模型
敲代码娶不了六花2 小时前
jsp | servlet | spring forEach读取不了对象List
java·spring·servlet·tomcat·list·jsp
Yhame.2 小时前
深入理解 Java 中的 ArrayList 和 List:泛型与动态数组
java·开发语言
是小崔啊4 小时前
开源轮子 - EasyExcel02(深入实践)
java·开源·excel
myNameGL4 小时前
linux安装idea
java·ide·intellij-idea
青春男大4 小时前
java栈--数据结构
java·开发语言·数据结构·学习·eclipse
HaiFan.5 小时前
SpringBoot 事务
java·数据库·spring boot·sql·mysql