多线程的各种状态与线程安全问题及wait和notify详解

本文详细介绍了Java线程的基本状态、线程安全问题的成因,重点剖析了count++引发的线程安全问题,以及如何通过加锁(synchronized)和volatile关键字解决。此外,文章还讲解了wait和notify方法的区别,以及它们在多线程协作中的作用。
摘要由CSDN通过智能技术生成


前言


一、线程的各种状态

1.NEW:安排了工作,还未开始行动
Thread 对象创建好了.但是还没有能用start方法

public class Tread {
    public static void main(String[] args) {
        Thread t=new Thread(()->{
            while (true){
                //System.out.println("dddd");
            }
        });
        System.out.println(t.getState());
        t.start();
    }
}

在这里插入图片描述
2.TERMINATED:工作完成了.

public class Tread {
    public static void main(String[] args) throws InterruptedException {
        Thread t=new Thread(()->{
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        System.out.println(t.getState());//t还没运行
        t.start();
        t.join();
        System.out.println(t.getState());//t一定结束了

    }
}

在这里插入图片描述
3.RUNNABLE:可工作的.又可以分成正在工作中和即将开始工作.
可理解成两种情况:1)线程正在cpu上运行;2)线程在这里排队,随时都可以去cpu上执行.

public class Tread {
    public static void main(String[] args) throws InterruptedException {
        Thread t=new Thread(()->{
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
       // System.out.println(t.getState());
        t.start();
        //t.join();
        System.out.println(t.getState());

    }
}


在这里插入图片描述
4.TIMED_WAITING:这几个都表示排队等着其他事情
因为sleep产生阻塞了-

public class Tread {
    public static void main(String[] args) throws InterruptedException {
        Thread t=new Thread(()->{
            try {
                Thread.sleep(90000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
       // System.out.println(t.getState());

        t.start();
        //t.join();
        Thread.sleep(1000);
        System.out.println(t.getState());

    }
}

在这里插入图片描述
5. BLOCKED:这几个都表示排队等着其他事情
因为锁产生阻塞了
6. WAITING:这几个都表示排队等着其他事情
因为wait()产生阻塞了
7.各种状态之间的关系:
在这里插入图片描述

二、线程安全问题

1.啥叫bug?

只要实际运行效果和预期效果(需求中的效果)不一致,就可以称为是一个bug !

2.线程安全问题

在多线程下,发现由于多线程执行,导致的bug,统称为“线程安全问题”。
如果某个代码,在单线程下执行没有问题,多个线程下执行也没问题,则称为“线程安全",反之也可以称为“线程不安全"。

1.count++引起的线性安全问题(实例)

两个线程,针对同一个变量,进行循环自增.各自自增5w次,预期最终应该是10w:

class Counter{
    public int count=0;
    public void increase(){
        count++;
    }
}
public class Tread07 {
    public static void main(String[] args) throws InterruptedException {
        Counter counter=new Counter();//涉及变量捕获,此处Counter变量对象未变,符合变量捕获规则。
        Thread t1=new Thread(()->{
            for (int i = 0; i <50000 ; i++) {
                counter.increase();
            }
        });
        Thread t2=new Thread(()->{
            for (int i = 0; i <50000 ; i++) {
                counter.increase();
            }
        });
        t1.start();
        t2.start();

        t1.join();
        t2.join();
        System.out.println(counter.count);
    }
}

在这里插入图片描述
实际上并不是!!
结果!=100000,多线程计算出现问题,为啥?
因为count++:
在这里插入图片描述
虽然一个cpu核心上,击存器就这么—组.
但是两个线程,可以视为是各自有各自的一组奇存器.本质上是“分时复用"的.
在这里插入图片描述
线程的执行是随机的,抢占式执行的过程.
如:此处这里的结果就会出现问题,自增两次,结果为1
在这里插入图片描述
虽然是自增两次。但是由于两个线程并发执行,就可能在一定的执行顺序下,导致运算的中间结果就被覆盖了。
在这5w 次的过程中有多少次会出要覆盖结果的?不确定!而且得到的这个错误值,—定是小于10w。

2.线程安全问题的原因

1.多个线程之间的调度顺序是“随机的”,
操作系统使用“抢占式"执行的策略来调度线程。(罪魁祸首,万恶之源)(无法使线程在固定顺序下执行,无力改变)
2.多个线程同时修改同一个变量,容易产生线程安全问题.
3.进行的修改,不是"原子的”。
如果修改操作,能按照原子的方式来完成此时也不会有线程安全问题
count++就不是原子的。
4.内存可见性,引起的线程安全问题.
5.指令重排序,引起的线程安全问题.

3.加锁synchronized

在count++引起线程安全问题的实例中主要是由1,2,3导致的,其中1和2无法更改,因此解决3.
利用加锁=>相当于是把一组操作,给打包成一个“原子”的操作
代码中的锁,就是让多个线程同一时刻只有一个线程能使用这个变量
Java引入了一个synchronized关键字加锁。
直接给方法进行加锁,进入方法就会加锁,出了方法就会解锁。
在这里插入图片描述
在这里插入图片描述

当t1加锁了之后,t2进行阻塞等待,这个阻塞直持续到t1把锁释放之后,t2才能够加锁成功!!
此处t2的阻度等待,就把t2的针对count++操作推迟到了后面完成,直到完成了count ++,t2才能够真正进行count++,把“穿插执行”变成了串行执行了。
在这里插入图片描述
synchronized每次加锁,也是针对某个特定的对象加锁。synchronized修饰方法,此时就相当于是针对this 加锁.
在这里插入图片描述
此处this针对counter对象加锁。
在这里插入图片描述
所以此处是两个线程针对同一个对象进行加锁:
就会出视锁竞争争/冲突(一个线程能加锁成功.另一个线程阻塞等待)
如果是两个线程针对的不同对象加锁?
不会产生锁竞争.也就不存在锁冲突等一系列的操作了。
例如—个线程加锁了,一个线程没加锁此时会存在线程安全问题,不存在锁竞争。

注意:synchronized给静态方法加锁:
在这里插入图片描述

3.内存可见性,引起的问题

1.编译器优化问题

t1始终在进行 while循环,t2则是要让用户通过控制台输入一个整数,作为isQuit的值。
当用户输入的仍然是0的时候, t1线程继续执行.
如果用户输入的非0, t1线程就应该循环结束.

public class Tread08 {
    public static int isQuit=0;

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

            }
            System.out.println("t1执行结束");
        });
        Thread t2=new Thread(()->{
            Scanner scanner=new Scanner(System.in);
            System.out.println("修改t1的值:");
            isQuit=scanner.nextInt();
        });
        t1.start();
        t2.start();
    }
}

在这里插入图片描述

此题中虽然修改了t1的值但是t1线程并没有结束,这也是不符合预期的,也是bug !!
编译器优化:
程序猿负责写代码的.当写好一个代码之后,人家开发java编译器,开发jvm的大佬,可能会认为你这个代码写的不够好
当你的代码实际执行的时候,编译器/jvm就可能把你的代码给改了,保持原有逻辑不变的情况下,提高代码的执行效率.
在这里插入图片描述
本质上是两个指令:1.load(读内存)(读内存操作,速度非常慢)

2.jcmp(比较,并跳转)(寄存器操作,速度极快)

此时,编译器JVM就发现,这个逻辑中,代码要反复的,快速的读取同一个内存的值。并且,这个内存的值,每次读出来还是一样的.
此时,编译器就做出一个大胆的决策,直接把 load 操作优化掉了.只是第一次执行load . 后续都不再执行load,直接拿寄存器中的数据进行比较了。
但是,程序猿在另一个线程中,修改了isQuit的值!!所以出现了bug。
在这里插入图片描述
注意:编译器什么时候,对什么进行优化是一个玄学。

2.volatile 关键字

把volatile用来修饰一个变量之后,编译器就明白,这个变量是"易变"的,就不能按照上述方式,把读操作优化到读寄存器中.(编译器就会禁止上述优化)
于是就能保证t1在循环过程中,始终都能读取内存中的数据
在这里插入图片描述

public class Tread08 {
    public volatile static int isQuit=0;

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

            }
            System.out.println("t1执行结束");
        });
        Thread t2=new Thread(()->{
            Scanner scanner=new Scanner(System.in);
            System.out.println("修改t1的值:");
            isQuit=scanner.nextInt();
        });
        t1.start();
        t2.start();
    }
}

在这里插入图片描述

volatile 本质上是保证变量的内存可见性.(禁止该变量的读操作被优化到读寄存器中)

三、wait和notify

wait(等待)和notify(通知),都是Object提供的方法.
随便找个对象,都可以使用wait和notify~~

1.wait

wait在执行的时候,会做三件事:
1.解锁. object.wait,就会尝试针对object 对象解锁.
⒉.阻塞等待.
3.当被其他线程唤醒之后,就会尝试重新加锁,加锁成功, wait执行完毕,继续往下执行其他逻辑.

public class Tread09 {//当wait引起线程阻塞之后,可以使用interrupt方法,把线程给唤醒.打断当前线程的阻塞状态的.

    public static void main(String[] args) throws InterruptedException {
        Object object=new Object();
        object.wait();
    }
}

报错:
在这里插入图片描述
wait要解锁,前提就是先能加上锁
核心思路:先加锁,在synchronized里头再进行wait!!

public class Tread09 {
    public static void main(String[] args) throws InterruptedException {
        Object object=new Object();
        synchronized (object) {
            object.wait();
        }
        System.out.println("wait结束");
    }
}

在这里插入图片描述
结果:一直阻塞等待。一直会阻塞到其他进程进行notify。

2.wait和notify

public class Tread09 {
    public static Object locker=new Object();
    public static void main(String[] args){
        Thread t1=new Thread(()->{
            while (true){
                synchronized (locker){
                    System.out.println("t1 wait开始");
                    try {
                        locker.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("t1 wait结束");
                }
            }
        });
        Thread t2=new Thread(()->{
            while (true){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (locker){
                    System.out.println("t2 notify开始");
                    locker.notify();
                    System.out.println("t2 notify结束");
                }
            }
        });
        t1.start();
        t2.start();
    }
}

在这里插入图片描述
在这里插入图片描述

几个注意事项:
1.要想让notify能够顺利唤醒wait就需要确保wait和notify 都是使用同一个对象调用的
.2. wait和notify 都需要放到 synchronized之内的.
虽然 notify不涉及"解锁操作"但是java 也强制要求notify要放到synchronized中
(系统的原生api中,没有这个要求)
3.如果进行notify的时候,另一个线程并没有处于wait状态.此时,notify相当于“空打一炮".不会有任何副作用~~

可以实现:如果就想唤醒某个指定的线程,就可以让不同的线程,使用不同的对象来进行wait 。想唤醒谁,就可以使用对应的对象来notify

notifyAll()

线程可能有多个
比如,可以有N个线程进行wait,一个线程负责notify
notify操作只会唤醒一个线程.具体是唤醒了哪个线程?是随机的!!
notifyAll 唤醒全部处于waiting中的线程

3.wait和sleep之间的区别

sleep是有一个明确的时间的. 到达时间,自然就会被唤醒.也能提前唤醒,使用interrupt就可以
wait 默认是一个死等,一直等到有其他线程notify.(顺理成章的唤醒.唤醒之后该线程还需要继续工作后续还会进入到wait 状态.)
wait也能够被interrupt提前唤醒.(很少用)


总结

  • 21
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值