一文帮你深度剖析多线程的相关知识(基础篇下)

引言:

大家好我是青花瓷,在上一篇文章中我们共同学习了多线程的一些相关知识,今天我们继续学习多线程的有关知识,如果觉得博主写的不错,给你带来了帮助,麻烦动动你们可爱的小手给博主点上一个关注哟‍‍
如果对多线程的Thread类的用法以及线程中断,休眠,等待相关知识还理解的还不够深入,那么可以去看看博主的上一篇文章哟

一文帮你深度剖析多线程相关知识(基础篇上):

博客地址:https://blog.csdn.net/Biteht/article/details/123739436?spm=1001.2014.3001.5501

一文帮你深度剖析多线程的相关知识(基础篇下)_第1张图片

文章目录

  • Java线程中的6种状态
  • 线程状态转化图
  • 什么是线程安全问题
  • 为什么会有线程安全问题
  • 线程不安全的原因
  • 如何解决线程安全问题
  • synchronized关键字
  • 什么叫可重入
  • 什么是死锁
  • 死锁的其他场景
  • 死锁的四个必要条件
  • volatile关键字
  • wait 和 notify

Java线程中的6种状态

Java中的线程的状态分为6种:

1.初始(NEW):新建了一个线程对象,但是还没有调用 start() 方法

实现Runnable接口和继承Thread可以得到一个线程类,new一个实例出来,线程就进入了初始状态。

2.运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。

线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。

就绪状态(RUNNABLE之READY)
1.就绪状态只是说你资格运行,调度程序没有挑选到你,你就永远是就绪状态。
2.调用线程的start()方法,此线程进入就绪状态。
3.当前线程sleep()方法结束,其他线程join()结束,等待用户输入完毕,某个线程拿到对象锁,这些线程也将进入就绪状态。
4.当前线程时间片用完了,调用当前线程的yield()方法,当前线程进入就绪状态。
5.锁池里的线程拿到对象锁后,进入就绪状态。

运行中状态(RUNNABLE之RUNNING)
线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态。这也是线程进入运行状态的唯一的一种方式。

3.阻塞(BLOCKED):表示线程阻塞于锁

阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态。

4.等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。

处于这种状态的线程不会被分配CPU执行时间,它们要等待被显式地唤醒,否则会处于无限期等待的状态。

5.超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。

处于这种状态的线程不会被分配CPU执行时间,不过无须无限期等待被其他线程显示地唤醒,在达到一定时间后它们会自动唤醒。

6.终止(TERMINATED):表示该线程已经执行完毕

1.当线程的run()方法完成时,或者主线程的main()方法完成时,我们就认为它终止了。这个线程对象也许是活的,但是它已经不是一个单独执行的线程。线程一旦终止了,就不能复生。
2.在一个终止的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。
三、等待队列

线程状态转化图

一文帮你深度剖析多线程的相关知识(基础篇下)_第2张图片

什么是线程安全问题

就是 多线程环境中,且存在数据共享,一个线程访问的共享数据被其他线程修改了,那么就发生了线程安全问题,整个访问过程中,无一共享的数据被其他线程修改了就是线程安全的
通俗的来讲:程序中如果使用成员变量,且对成员变量进行数据修改,就存在数据共享问题,也就是线程安全问题

为什么会有线程安全问题

当多个线程同时共享一个 全局变量,或者静态变量,进行写的操作时,可能会发生数据的冲突,也就是线程安全问题,但是读的操作不会引发线程安全问题

1.线程安全

多个线程在执行同一段代码的时候采用加锁机制,使得每次的执行结果和单线程执行结果一样,不存在执行程序出意外结果

2.线程不安全

线程不安全是指 不提供加锁机制保护,有可能出现多个线程先后更改数据造成所得到的的数据是脏数据

线程不安全的原因

1.线程的抢占式执行(线程不安全的万e之源)

具有随机性,由操作系统内核实现

2.多个线程对同一个变量进行修改操作

如果多个线程对不同的变量进行修改,没事!如果多个线程针对同一个变量读也没事!

3.线程的非原子操作

线程主要有三个操作:
1.从内存中读取到寄存器中
2.通过指令对数据进行操作并写入到寄存器中
3.从寄存器中写入内存
当线程操作过程中的三个操作被打包成一个整体,禁止线程互相穿插执行,就可以保证 线程安全 。

一文帮你深度剖析多线程的相关知识(基础篇下)_第3张图片
4.内存可见性

当两个线程同时操作一个内存,例如一个读一个写,但是当写操作的线程进行的修改的时候,读线程可能读到修改前的数据,也可能读到修改后的数据,这是不确定的。
内存可见性的本质是编译器优化导致了线程1修改的数据没有及时的写入到内存中,线程2就读取不到

举个具体例子:
一文帮你深度剖析多线程的相关知识(基础篇下)_第4张图片
针对同一个变量,一个线程(t1)进行读操作(循环进行很多次),一个线程进行修改操作(合适的时候执行一次)

那么,t1 这个线程在循环读这个变量,按照之前的介绍,读取内存操作,相比于读取寄存器,是一个非常低效的操作,因此在 t1 中频繁的读取到这里的 内存的值,就会非常低效,而且如果 t2 线程迟迟不修改,t1 线程读到的值又始终是一样的值,因此, t1 就有个大胆的想法!!!.就不会再从寄存器读取数据了,而是直接从寄存器里读,不在执行(load),一旦 t1 做出这种大胆的假设,此时万一 t2 修改了 count的值,t1 就不能感知到了

5.指令重排序

和 线程不安全、编译器优化 直接相关。
为了提高程序的执行效率,调整了执行的顺序(调整的目的是为了提高效率,不改变逻辑)
编译器也会自动对顺序进行调整。单线程下调整不会出现问题,多线程下调整会出现大问题。

如何解决线程安全问题

解决方法:1.synchronized关键字 2.lock锁

synchronized关键字

synchronized的基本使用:

1.把synchronized加到普通方法上
2.把synchronized加到代码快上

1.synchronized加到普通方法上
在这里插入图片描述
如果直接修饰普通方法,也就是把锁对象指定为 this(当前类的对象) 了
注意:代码中只需要将类被调用的方法前面加上修饰的关键字:synchronized即可
使用条件:如果操作共享数据的代码完整声明在一个方法中,可以使用此方法,将方法声明为同步的

关于同步方法的总结:
1.同步方法仍然涉及到同步监视器,只是不需要我们显示的声明
2.非静态的同步方法,其同步监视器是:this (当前类的对象)

2.synchronized加到代码块上
一文帮你深度剖析多线程的相关知识(基础篇下)_第5张图片
这里的 this 指的是 锁对象,如果要针对某个代码块加锁,就需要手动指定,锁对象是啥(针对锁对象进行加锁)

同步监视器:俗称: 锁,可以为任意一个类的对象,要求多个线程必须要用同意把锁

什么叫可重入

直观来讲,同一个线程针对同一个锁,连续加锁两次,如果出现死锁,就是不可重入,如果不会死锁,就是可重入的
可重入锁的意义:降低了程序猿的负担(使用成本,提高了开发效率),但是也带来了代价,程序中需要更高的开销(降低了运行效率)

什么是死锁

分析一下,连续锁两次会咋样:
一文帮你深度剖析多线程的相关知识(基础篇下)_第6张图片
这里我们可以看出,外层加了一次锁,里层又对同一个对象再加一次锁

外层锁:进入方法,则开始加锁,这次能够加锁成功,当前锁是没有人占用的
里层锁:进入代码块,开始加锁,这次加锁不能成功(按照我们之前的观点来分析),因为锁被外层占用着,得等到外层锁释放了之后,里面锁才能加锁成功,外层锁要执行完整个方法,才能释放~但是想要执行完整个方法,就得让里层加锁成功继续往下走

这就是死锁!!!

死锁的其他场景

一个线程一把锁,两个线程两把锁,N个线程M把锁

这里给大家举个教科书上的经典案例(N个线程M把锁)

一文帮你深度剖析多线程的相关知识(基础篇下)_第7张图片

死锁的四个必要条件

1.互斥使用:一个锁被一个线程占用了之后,其他线程占用不了(锁的本质,保证原子性)
2.不可抢占:一个锁被一个线程占用了以后,其他的线程不能把这个锁给抢走
3.请求和保持:当一个线程占据了多把锁之后,除非显示的释放锁,否则这些锁始终都是被该线程持有的
4.环路等待:等待关系,成环了~~ A等B,B等C,C又等A

如何避免环路等待?

只要约定好,针对多把锁加锁的时候,有固定的的顺序即可!

volatile关键字

volatile关键字的作用:禁止编译器优化,保证内存可见性。被volatile关键字修饰的变量,如果值发生了变更,其他线程立马可见,避免出现脏读的现象

注意:volatile 只是保证可见性,不保证原子性,只处理一个线程读一个线程写的情况,而 synchronized 都能处理

wait 和 notify

wait 和 notify 都是 Object 对象的方法,调用 wait 方法的线程,就会先入阻塞,阻塞到有其他线程通过 notify来 通知

wait 内部会做三件事:
1.先释放锁
2.等待其他线程的通知
3.收到通知之后,重写获得锁,并继续往下执行

因此想要使用 wait / notify 就得搭配 synchronized
一文帮你深度剖析多线程的相关知识(基础篇下)_第8张图片
注意:wait 哪个对象就需要对那个对象进行加锁

假设有两个线程 t1 和 t2 :
一文帮你深度剖析多线程的相关知识(基础篇下)_第9张图片
a先执行一通知就执行e,e执行完一通知,就执行b,依次类推~

代码实例:


public class TestDemo18 {
    private static Object locker = new Object();//锁对象
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()-> {
           //进行wait
            synchronized (locker) {
                System.out.println("wait 之前");
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("wait 之后");
            }
        });
        t1.start();

        //为了让现象更明显 在 t1 和 t2 之间加个sleep操作
        //等待3秒之后线程2跑起来了
        Thread.sleep(3000);

        Thread t2 = new Thread(()->{
            //进行 notify
            synchronized (locker) {
                System.out.println("notify 之前");
                locker.notify();
                System.out.println("notify 之后");
            }
        });
        t2.start();
    }
}

输出结果:
一文帮你深度剖析多线程的相关知识(基础篇下)_第10张图片

wait 和 notify 都是针对同一个对象来操作的
例如现在有一个对象 0 有10个线程,都调用了 0.wait,此时10个线程都是阻塞状态,如果调用了0.notify,就会把10个其中的一个唤醒(唤醒哪个是不确定的)
针对 notifyAll,就会把所有的10个其中的一个给唤醒,wait 唤醒之后,就会重新尝试获取到锁(这个过程就会发生竞争) 相对来说,更常用的还是 notif

你可能感兴趣的:(JavaEE,多线程,Java,JavaEE)