【JAVA】多线程 JUC 知识总结

138 阅读7分钟

多线程 JUC

1.什么是多线程?

什么是进程

进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。

image-20231222150832809

可以简单理解成,打开一个软件,就是在执行一个进程

什么是线程

线程是操作系统能够进行运算调度的最小单位,被包含在进程之中,是进程中的实际运作单位。

image-20231222151216079

如图所示,打开某安全软件,里面有如上几个功能。

可以简单理解成:应用软件中相互独立,可以同时运行的功能。

如果进程中线程较多,就形成了多线程。

有了多线程之后,我们就可以让程序同时做多件事情。

那么多线程如何执行呢?

简单理解:

image-20231222151623269

如上图所示,单线程程序每执行一条就会稍微摸摸鱼~这无疑会降低了 CPU 的效率。

那么把等待的时间应用起来,就是多线程的特点

多线程的应用场景

例如,原神,启动!打开一个大型游戏,桌面会显示一个进度条,加载大量的资源文件,此时如果单单做这件事情,就未免太慢太枯燥了。所以在加载资源文件的过程中,还可以去启动另一个线程去做别的事情,例如检查一下版本号、播放背景音乐等等。

2.并发、并行

  • 并发:在同一时刻,有多个指令在单个 CPU 上交替进行。

    边打游戏、边拿瓶可乐、边看看微信。

    CPU 就是在这三条线程中交替进行~

  • 并行:在同一时刻,有多个指令在多个 CPU 上同时进行。

CPU: 2 核 4 线程。代表CPU 能同时跑 4 条线程。如果只需要执行 4 条线程,很显然可以并行执行。但是如果所需的量大于 4 条线程,就需要并发配合并行啦。

image-20231222152535963

并发与并行同时发生

3.多线程的实现方式

当 JVM虚拟机启动之后,会自动的启动多条线程,其中有一条线程就叫 main 线程

他的作用就是去调用 main 方法,并执行里面的代码

  1. 继承 Thread 类的方式进行实现

    image-20231222153144039

    如果你想拥有一条线程,就创建一个 Thread 的对象就行。

    image-20231222154503701

    image-20231222154516330

    运行结果

  2. 实现 Runnable 接口的方式进行实现

    实现 Runnable 接口的类,该类实现 run 方法。

    image-20231222155615677

  3. 利用 Callable 接口和 Future 接口方式实现

    特点:可以获得多线程运行的结果

    image-20231222160527596

4.多线程中的常用成员方法

image-20231222160917267

线程的优先级

Java 执行的是抢占式调度的线程程切换方法,完全随机。优先级越高,获取线程的概率越高!是概率,并非优先级越高就会一定抢到。

当线程没有设置优先级时,默认是 5。 优先级的级别为 1-10

  • setPriority(int):设置线程的优先级
  • final int getPriority():获取线程的优先级

守护线程

image-20231222162236021

应用场景: 当两个人在聊天,可以把聊天当作一个线程、传输当作一个线程。当把聊天页面关闭了,传输文件停掉,就可以用到守护线程。

image-20231222162401778

5.线程的生命周期

image-20231222163600327

6.线程的安全问题

多个线程操作同一个数据,会出现问题

线程执行时,有随机性

同步代码块

image-20231222170745823

image-20231222171324135

两个小细节

  • image-20231222172106641

    如果把同步代码快放在 while 循环语句外面,会造成线程一直在某个线程执行,直到 while 循环结束。

  • synchronized 后面的锁对象,一定要是唯一的!不然锁相当于没有~

可以用MyThread.class 作为锁对象,铁定唯一~

同步方法

image-20231223104846399

选中里面的方法,command + L + M,登登登登:

image-20231223104926987

这就抽取成了一个同步方法。

Lock 锁

image-20231223105748275

Lock 锁的使用很简单,Lock lock = new ReentrantLock(); 注意不能直接 new Lock,因为这是一个接口,必须 new 其实现类。

使用 Lock 锁的实例以及注意事项:

image-20231223112832572

如图所示,乍一看这些个代码没什么问题!

但是程序运行起来你会发现:

image-20231223112421198

有问题:程序一直没有停止,虽然已经输出完毕了。 那么是为什么呢?

仔细分析一下代码发现:当 ticket == 10 时,程序直接 break 了跳出了 while 循环。那么很显然,就没有执行到 lock .unlock()这句解锁代码,所以线程一直拿着锁没有放。

那么如何解决呢?有个不够优雅的办法就是在 break 语句之前加上一句lock .unlock(),这样做确实是可以达到目的,但是 unlock 一般用于结尾使得代码结构好看。

有一个办法就是一定能执行到 unlock 方法,就是用 try、 catch 、 finally 中的 finally 语句。

image-20231223113126531

死锁

死锁是一个错误,记住一句话,写代码的时候,不要让两个锁嵌套起来

生产者消费者

image-20231223114849877

让两条线程轮流执行~原理:

image-20231223115005826

理想的情况下,生产者线程先拿到了 CPU 控制权,生产了资源,再供给消费者

  • 消费者等待:消费者先拿到控制权,只能先等待 --wait。 生产者再拿到控制权,生产好了执行 唤醒---notify

  • 生产者等待: 生产者先拿到控制权,下一次还是生产者拿到控制权,只能先等待 --wait 。

    image-20231223115637310

    常见方法

image-20231223115722407

代码实现

  • 桌子

    image-20231223122043327

  • 生产者

    image-20231223122136413

  • 消费者

    image-20231223122016089

    多线程的 6 种状态

image-20231223122421889

线程池

为什么要有线程池呢?

image-20231224184952116

image-20231224185301483

线程池的核心原理

线程池的代码实现

  1. 创建线程池

  2. 提交任务

  3. 所有的任务执行完毕,关闭线程池

    线程池一般是不会销毁的,因为服务器一般不会关闭~

创建线程池对象的方法:

package com.threadpool;
​
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
​
public class mythreadpool {
    public static void main(String[] args) {
        /*
         * 1.public static ExecutorService newCachedThreadPool() //创建没有上限的线程池
         * 2.public static ExecutorService newCachedThreadPool(int nThreads)//有上限的线程池
         * */
​
        //1.获取线程池对象
        ExecutorService pool1 = Executors.newCachedThreadPool();
​
        //2.提交任务
        pool1.submit(new MyRunnable());
        pool1.submit(new MyRunnable());
        pool1.submit(new MyRunnable());
        pool1.submit(new MyRunnable());
​
        //3.销毁线程池
        pool1.shutdown();
    }
}
​
package com.threadpool;
​
public class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " ---");
    }
}
​

上面两段代码为线程池的基本操作,该实现相当于在线程池中创造了四个线程,没有达到复用的效果。

public class mythreadpool {
    public static void main(String[] args) throws InterruptedException {
        /*
         * 1.public static ExecutorService newCachedThreadPool() //创建没有上限的线程池
         * 2.public static ExecutorService newCachedThreadPool(int nThreads)//有上限的线程池
         * */
​
        //1.获取线程池对象
        ExecutorService pool1 = Executors.newCachedThreadPool();
​
        //2.提交任务
        pool1.submit(new MyRunnable());
        Thread.sleep(1000);
        pool1.submit(new MyRunnable());
        Thread.sleep(1000);
        pool1.submit(new MyRunnable());
        Thread.sleep(1000);
        pool1.submit(new MyRunnable());
        Thread.sleep(1000);
​
        //3.销毁线程池
        pool1.shutdown();
    }
}

在每段提交任务之后,设置一个休眠,保证前一个任务已经执行完,所以下一次执行就会实现复用。

有上限的线程池

package com.threadpool;
​
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
​
public class mythreadpool {
    public static void main(String[] args) throws InterruptedException {
        /*
         * 1.public static ExecutorService newCachedThreadPool() //创建没有上限的线程池
         * 2.public static ExecutorService newFixedThreadPool(int nThreads)//有上限的线程池
         * */
​
        //1.获取线程池对象
        ExecutorService pool1 = Executors.newFixedThreadPool(3);
​
        //2.提交任务
        pool1.submit(new MyRunnable());
        pool1.submit(new MyRunnable());
        pool1.submit(new MyRunnable());
        pool1.submit(new MyRunnable());
        pool1.submit(new MyRunnable());
​
        //3.销毁线程池
        pool1.shutdown();
    }
}
​

执行结果:

image-20231224191428044

可见,最多只创建了三个线程

自定义线程池

ThreadPoolExecutor pool = new ThreadPoolExecutor();

实际上就是调用这个类来实现自己定义线程池,需要输入 7 个不同的参数!

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)

image-20231224194659379

示例

比喻:

image.png

线程池多大合适呢

以四核八线程的 CPU 来解释:

事实上可以简单理解成 CPU 有四个核心,通过超线程技术,每个核心虚拟一分为二,可以同时处理八条线程。

最大并行数为 8.

//获取执行线程数量
int count = Runtime.getRuntime().availableProcessors();
        System.out.println(count);

image-20231224200646500

可以用 thread dump 工具测试