Java多线程超全面详解(确定不点进来看看?)

什么是进程?什么是线程?什么是多线程?

想要了解什么是多线程,就得知道什么是线程。
想要了解什么是线程,就得知道什么是进程。
想要了解什么是进程,就得知道什么是一个程序。
那么我们先从程序慢慢说起。

程序

程序表现为系统中的一个可执行文件(该可执行文件为静态)。

说明:双击打开某个文件,不意味着这个文件就是程序

进程

程序(静态的可执行文件)运行起来之后,就是一个进程(运行态的程序)。
或者
对于操作系统来说,一个任务就是一个进程。

补充:
进程是作为系统分配资源的最小单位(CPU,内存等等)
一个进程,系统是分配有唯一的一个id标识(pid)

关于操作系统:需要了解的与进程相关的操作系统知识

1.并发与并行

并发:在系统调度一个CPU,采取时间片轮转调度的方式,在一个时间范围内,执行多个进程的代码。

对于人的肉眼来说,这个时间是不可感知的,就认为是同时在执行。
同一个时间点,只有一个进程在一个CPU执行,但是在一个时间范围,是一个CPU执行多个进程(也就是假同时)
从我们的角度看,是同时执行;
从CPU的角度看,是一个时间点,执行一个进程。

并行:多个CPU在同一个时间点,同时执行多个进程的代码(真同时)。

2.进程运行涉及的:

(1)系统加载进程的程序代码到内存
(2)系统调度CPU执行程序代码指令

3.内核态和用户态

执行的部分就涉及权限问题:
(1)系统操作权限最高,如果一个程序执行某个指令,没有权限,就需要调用系统接口来执行,此时,用户进程就进入“内核态”或者“核心态”。
(2)用户进程来说,权限是最低的,称为“用户态”。

以上两种状态就涉及到互相转换的问题:进程进行权限高的指令(如IO操作),用户态---->内核态,执行完毕,返回;内核态---->用户态。
用户态转变为内核态是比较消耗资源的。(需要注意和考虑的问题点:多线程代码的使用或设计原则,和tcp协议设计原则类似,保证数据安全的情况下,尽可能的提高效率)

4.上下文

简单来说,上下文就是一个环境,进程在时间片轮转切换时,由于每个进程运行环境不同,就涉及到转换前后的上下文环境的切换。

这个名词不是进程和线程独有,而是在环境不同,要进行切换的时候,都需要的词汇。

CPU调度执行进程1,切换到进程2,又切换到进程1(进程1切换到进程2,环境变化就有上文和下文)。
在环境切换出进程1时,保存进程1上文环境,切换到进程2,再切换回进程1时,需要恢复为进程1的上文环境。
认识上下文环境的作用:

  • 切换出去时,保存上下文环境
  • 切换进来时,恢复上下文环境
5.进程状态

在这里插入图片描述

  • 就绪:进程处于可运行的状态,只是CPU时间片还没有轮转到该进程,则该进程处于就绪状态。
  • 运行:进程处于可运行的状态,且CPU时间片轮转到该进程,该进程正在执行代码,则该进程处于运行状态。
  • 阻塞:进程不具备运行条件,正在等待某个时间的完成。

线程(Thread)

1.线程与进程的关系

进程包含多个线程
每个进程至少有一个线程存在,即主线程。

例如:
我是一个进程,要完成写代码、测试APP性能、宣传产品三件事情,那我就依次执行完成这三件事;
如果采取多线程的方式,那就是我雇佣三个人,分别完成以上三件事情。

2.线程和进程的区别(部分):

(1)进程是系统分配资源(CPU,内存)的最小单位,线程是系统调度CPU执行的最小单位。
(2)数据共享:同一个进程内的线程之间可以共享资源,代价比较小;不同进程之间,要共享数据,需要进行进程通信。


多线程

1.多线程作用

提高效率(尽可能充分利用系统资源:CPU)。

需要注意的事项:创建进程是比较耗费时间和资源的,所以要综合考虑同时执行指令(任务量)+创建线程数量+系统可用资源(内存,CPU)。

2.多线程使用场景

一个时间点,要同时做很多件事情(同时执行多行代码)。

例如:
一个QQ打开以后,可以:
(1)收发消息
(2)上传下载文件
(3)查看图片/pdf文件/视频
(以上由多线程完成,否则代码在阻塞等待时无法做其他事情)
又例如:
本地播放视频:
(1)图片:很多图片每秒多少张切换出来,展示出来,就是视频(多少帧)
(2)声音
(3)弹幕,字母(文本内容)
(多线程的方式就是把以上内容,按照时间范围,规定好某个时间点要展示的图片、播放的声音、展示的文本)


3.创建线程的方法
  1. 继承Thread类:
    new一个Thread的子类,Thread就是java中的线程。
Public class MyThread extends Thread{
  	@Override
  	public void run(){
  	    System.out.println("线程运行的代码");
  	}
}
MyThread t = new MyThread();
t.start(); //线程开始运行

Thread的常见构造方法:
Thread()创建线程对象
Thread(Runnable target)使用Runnable对象创建线程对象
Thread(Runnable target,String name) 使用Runnable对象创建线程对象,并命名

  1. 实现Runnable接口
    new一个Runnable接口的子类,传到Thread中执行。

Runnable是java中任务的概念,new一个Runnable就是定义一个任务的描述,再传入Thread线程中执行。
该方法的好处是可以规避类的单继承的限制,但是需要通过Thread.currentThread()来获取当前线程的引用。

Public class MyRunnable implements Runnable{
  	@Override
  	public void run(){
  	    System.out.println("Thread.currentThread().getName() + 线程运行的代码");
  	}
}
Thread t = new Thread(new MyRunnable());
t.start(); //线程开始运行
  1. 通过Callable创建线程
4.启动线程

start:线程启动的方法(启动以后,才能表现并发,并行的特性)。

申请系统调度线程,让CPU执行(创建态—>就绪态),如果线程获取CPU时间片,就开始执行任务(Tread类中run方法就是任务的定义,或者是Runnable对象中的run方法)

start和run的区别:
start会真正的启动一个线程,以及调用start方法所在的线程,是并发并行执行;
run方法只是一个线程的任务定义,如果直接调用run方法,属于普通方法调用。

5.中断线程
  • 使用标志位
    可以实现某种程度上的,在满足条件(中断线程的条件:如转账发现诈骗)的情况下,中断一个线程。

存在的问题:如果线程中处于阻塞状态(需要满足一定条件如sleep休眠一定时间,才能恢复),就无法快速的中断线程。

  • 基于Thread类本身的api来实现中断
    Thread类中,保存有一个中断标志位,初始值=false(没有被中断)
    ①某个线程的引用.interrupt()
    ②线程可以获取中断标志位,通过循环判断,来决定是否需要中断

中断某个线程:
(1)线程中断标志位=true
(2)线程要不要中断,线程定义任务的代码自行决定
(3)如果线程处于阻塞状态(调用多线程api方法,显示抛出InterruptrdException的方法)。如果提前让线程从阻塞态转变为就绪态,在系统调度执行后,以抛异常的方式继续执行。(此时线程的中断标志位会重置为false)

6.线程等待

某些情况下,我们需要等待一个线程完成它的工作后,才能进行自己的下一步工作。

  • 线程引用对象.join()
  • 线程引用对象.join(long)
public class Thread {
    public static void main(String[] args) throws InterruptedException {
        Runnable target = () -> {
            for (int i = 0; i < 10; i++) {
                try {
                    System.out.println(Thread.currentThread().getName()
                                      + ": 我还在工作!");
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + ": 我结束了!");
       };
       Thread thread1 = new Thread(target, "李四");
       Thread thread2 = new Thread(target, "王五");
       System.out.println("先让李四开始工作");
       thread1.start();
       thread1.join();
       System.out.println("李四工作结束了,让王五开始工作");
       thread2.start();
       thread2.join();
       System.out.println("王五工作结束了");
    }
}

当前线程等待,直到满足以下条件:
(1)无参的方法,就是线程执行完毕
(2)有参的方法,是线程执行完毕和时间到达任意一个满足,满足以后,当前线程继续往下执行

7.线程安全

如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则收这个程序是线程安全的。

线程不安全的原因:
(1)从代码层面看:多个线程对同一个变量的操作(读,写),有一个写操作,就有线程安全问题。
(2)从原理方面看:
①原子性:多行代码执行,执行时,是一组不可再分的最小执行单位
多个线程同时并发并行的执行代码指令,可能在一个线程操作一个共享变量时,是有前后依赖关系,指令之间有其他线程的操作,就会导致线程不安全。
②可见性:
在这里插入图片描述
主存:线程都使用的共享区域,对其中变量/对象的操作
工作内存:线程之间互相不可见,CPU执行线程中的代码指令,是从主存复制到CPU高速缓存
③有序性:
代码重排序:java代码顺序是固定的,但是jvm执行字节码或CPU执行机器码,都可能重排序指令顺序,目的是提高运行效率;但是在多线程中,指令重排序了,代码就会是错误的。

8.如何解决线程不安全的问题

一组代码,如果存在多线程对共享变量的操作,都需要考虑线程安全问题。

一般把多线程操作的共享变量,称为临界资源
一组代码,称为临界区

解决思路:
把临界区加锁,多个线程执行临界区代码,最终表现就是一个线程申请加锁,执行代码,释放锁,其他线程申请失败,需要等待(不一定是阻塞态,也可以是运行态)。
一个线程一个线程依次的执行临界区代码。

线程执行临界区代码---->申请加锁(临界区代码)---->执行临界区代码(申请到锁)---->释放锁---->通知等待的线程再次去申请锁---->自己等待---->下一轮的申请加锁
线程执行临界区代码---->申请加锁(临界区代码)---->等待(没有申请到锁)---->下一轮的申请加锁

  • synchronized关键字
    同步的关键字:一次执行某段代码
    作用:对对象头加锁的方式,保证线程安全,多线程申请同一把锁,会产生同步互斥的作用(多个线程依次执行临界区代码)。
    使用:
    ①在静态方法上:加锁整个方法,锁对象为当前的类对象。
    ②在实例方法上:加锁整个方法,锁对象为this。
    ③同步代码块:
    synchronized(锁对象){
    注意:如果不是使用同一个对象加锁,意味着能同时执行(并发并行)。
  • volatile关键字
    是修饰一个变量的关键字。
    作用:
    (1)保证变量的可见性(分解为字节码指令后,有变量的指令,变量有可见性)。
    (2)建立一个内存屏障,禁止指令重排序。
    使用场景:
    (1)多线程对共享变量的操作,如果代码行本身保证了原子性,就可以不加锁,只使用volatile保证可见性,该共享变量的操作也是线程安全的。
    (读是原子性,写(修改,赋值)操作;值不依赖共享变量,比如是一个常量,就是原子性)
    (2)多线程代码的设计目标:线程安全的情况下,尽可能的提高效率。
9.线程通信

线程并发并行的执行,但是在一定的条件下,可能A线程需要等待,B线程在其他条件,又可能需要让A线程恢复。
方法如下:
在这里插入图片描述
使用的前提条件:必须是在synchronize关键字作用的代码块内。
满足一定的条件,加锁的对象.wait()----让当前线程等待(提前释放对象锁);
加锁对象.notify()----同一个锁对象,调用wait()进入等待状态的进程,随机唤醒一个,再次竞争对象锁;
加锁对象.notifAll()----同一个锁对象,调用wait()进入等待状态的进程,全部唤醒,让这些线程再次竞争对象锁。

10.线程池

作用:不用每次执行任务时都创建线程(会真实创建系统级别的线程,比较耗时,销毁也是),而是可以使用线程池中的线程来复用。
优点:减少每次启动,销毁线程的消耗。

11.停止线程池

可以通过调用线程池的ShutDown或ShutDownNow方法来关闭线程池。
原理:遍历线程池中的工作线程,然后逐个调用线程的interrupt()方法来中断线程(只是修改中断标志位,至于要不要中断,提交的任务代码自行决定),所以无法响应中断的任务可能永远无法终止。

ShutDown():中断队列中的任务,工作线程正在执行的任务,还是会执行完毕才能关闭线程池;
ShutDownNow():中断队列中的任务,及工作线程正在执行的任务。

线程的优点

  1. 创建一个新线程的代价要比创建一个新进程小得多
  2. 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
  3. 线程占用的资源要比进程少很多
  4. 能充分利用多处理器的可并行数量
  5. 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
  6. 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
  7. I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作

进程与线程的区别

  1. 进程是系统进行资源分配的最小单位,线程是CPU调度的最小单位
  2. 进程有自己的内存地址空间,线程只独享指令执行的必要资源,如寄存器和栈
  3. 由于同一进程的各线程间共享内存和文件资源,可以不通过内核进行直接通信(进程数据传输需要通信)
  4. 线程的创建,切换及终止效率更高

wait和sleep的对比

  1. wait 之前需要请求锁,而wait执行时会先释放锁,等被唤醒时再重新请求锁。这个锁是 wait 对像上的 monitor lock
  2. sleep 是无视锁的存在的,即之前请求的锁不会释放,没有锁也不会请求
  3. wait 是 Object 的方法
  4. sleep 是 Thread 的静态方法
  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值