Java|Java 多线程 万字最详解


文章目录

  • Java多线程
    • 多线程概述
      • 线程与进程
      • 线程调度
      • 同步与异步
      • 并发与并行
      • 线程阻塞
    • Java 多线程的实现
      • 继承 Thread 类
      • 实现 Runnable 接口
      • 实现 `Callable`接口
    • Thread 类讲解
      • 线程状态`currentThread()`
      • 线程休眠`sleep()`
      • 线程中断`interrupt()`
      • 守护线程`setDaemon()`
      • 主线程之前完成`join()`
    • 线程安全问题
      • 线程不安全的演示
      • 解决方法 1:同步代码块
      • 解决方法 2:同步方法
      • 解决方法 3:显示锁 Lock
      • 更多 synchronized 用法
    • 显示锁的问题
      • 公平锁和非公平锁
      • 怎么实现公平锁
    • 多线程经典问题
      • 线程死锁
      • 多线程通信
        • 生产者和消费者
    • 线程的状态
    • 线程池
      • 概念
      • 好处
      • 线程池的分类

Java多线程 多线程概述 线程与进程
进程
  • 是内存中运行的应用程序,每个进程都有一个独立的内存空间
线程
  • 值进程的执行路径,共享一个内存空间,线程之间可以自由切换,并发执行,一个进程最少有一个线程
  • 线程实际上是在进程基础上的进一步划分,一个进程启动之后,里面若干的执行路径又可以划分成若干个线程
线程调度
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eoKunGnP-1620203162745)(/Users/faro_z/Library/Application Support/typora-user-images/image-20210504211610442.png)]
分时调度
  • 所有线程,轮流使用 CPU,平均分配每个线程占用 CPU 的时间
抢占式调度
  • 当 CPU 空闲的时候,CPU 会抛出一个时间片,谁抢到就是谁的。优先让优先级高的线程使用 CPU;如果线程优先级相同,那么,会随机选择一个(线程随机性),Java 使用的就是抢占式调度
  • CPU 使用抢占式调度模式在多个线程之间高速切换,对于 CPU 而言,某时刻,只能执行一个线程,而 CPU 在多个线程切换的速度很快,在我们看来,就像是多个线程在同时执行一样。其实多线程程序并不能提高程序的运行速度(甚至会因为切换的时候的时间消耗,使得总耗时还边长),但是能提高程序的运行效率,让 CPU 使用率更高。
同步与异步
**同步:**排队执行,效率低但是安全
**异步:**同时执行,效率高但是数据不安全
并发与并行
【Java|Java 多线程 万字最详解】并发:指两个或多个事件,在==同一时间段==内发生。(比如说一天内,一小时内)
**并行:**指两个或多个事件,在同一时间发生(同时发生)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UZDhhlwB-1620203162747)(/Users/faro_z/Library/Application Support/typora-user-images/image-20210504212449234.png)]
线程阻塞
一般,比较耗时的操作,可以看成是线程阻塞
比如说,我们要等待用户输入,那么,那一段线程,就是阻塞了
Java 多线程的实现 java 中实现多线程,一共有三种方法
  • 继承 Thread 类
  • 实现 Runnable 接口
  • 实现Callable 接口
继承 Thread 类
public class ThreadDemo { public static void main(String[] args) { new ThreadClass().start(); for (int i = 0; i < 10; i++) { System.out.println("锄禾日当午"+i); } } }class ThreadClass extends Thread {@Override public void run() { for (int i = 0; i < 10; i++) { System.out.println("汗滴禾下土"+i); } } }

Java|Java 多线程 万字最详解
文章图片

Java|Java 多线程 万字最详解
文章图片

每个线程都有自己的栈空间,但是共用一个堆空间
实现 Runnable 接口
public class RunnableDemo { public static void main(String[] args) {//子线程 new Thread(() -> { for (int i = 0; i < 10; i++) { System.out.println("汗滴禾下土"+i); } }, "子线程").start(); //主线程(的一部分,整个 main 都是主线程) for (int i = 0; i < 10; i++) { System.out.println("锄禾日当午"+i); } } }

实现 Callable接口
Callable,是带返回值的线程
上面两个实现方法,可以看成主线程之外的子线程去完成任务,和主线程无关了
但是 Callable,相等于主线程让子线程去完成任务,等到子线程任务完成了,再给主线程返回一个结果。比如说,主线程可以派发 1000 个任务,给 1000 个线程,等着 1000 个任务执行完毕了,可以获得 1000 个结果。
Callable 的使用步骤如下:
  1. 编写实现 Callable 接口,实现 call 方法
class Xxx implements Callable { @Override public call() throws Exception { return T; } }

  1. 创建 FutureTask 对象,并传入编写的 Callable 对象
FutureTask task = new FutureTask<>(callable);

  1. 通过 Thread,启动线程
new Thread(task).start

获取子线程返回结果的方法如下:
  • 使用 FutureTask 的get()方法
**注意:**使用get(),会让主线程等待子线程执行完毕,然后去获得方法
public class CallableDemo { public static void main(String[] args) throws InterruptedException, ExecutionException { MyCallable cl = new MyCallable(); FutureTask task = new FutureTask<>(cl); //执行子线程 new Thread(task).start(); Integer res = task.get(); System.out.println("子线程计算结果为:"+res); for (int i = 0; i < 10; i++) { Thread.sleep(100); System.out.println("main"+i); }} }class MyCallable implements Callable {@Override public Integer call() throws Exception { for (int i = 0; i < 10; i++) { Thread.sleep(1000); System.out.println("子线程正在执行"+i); } return 100; } }

Java|Java 多线程 万字最详解
文章图片

  • 使用使用 FutureTask 的isDone()方法
可以先判断子线程有没有执行完,执行完了,再去获取子线程的返回值,避免主线程等待
public class CallableDemo2 { public static void main(String[] args) throws InterruptedException, ExecutionException { MyCallable2 cl = new MyCallable2(); FutureTask task = new FutureTask<>(cl); new Thread(task).start(); for (int i = 0; i < 10; i++) { Thread.sleep(1000); if (task.isDone()) { Integer res = task.get(); System.out.println("子线程已经执行完了,结果是:"+res); } System.out.println("main"+i); }} }class MyCallable2 implements Callable {@Override public Integer call() throws Exception { for (int i = 0; i < 10; i++) { Thread.sleep(500); // System.out.println("子线程正在执行"+i); } return 100; } }

Java|Java 多线程 万字最详解
文章图片

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-r16OCaP5-1620203162753)(/Users/faro_z/Library/Application Support/typora-user-images/image-20210505154137082.png)]
Thread 类讲解 线程状态currentThread()
获取当前线程
public class ThreadDemo1 { public static void main(String[] args) { System.out.println(Thread.currentThread().getName()); new Thread(() -> { System.out.println(Thread.currentThread().getName()); }, "子线程").start(); } }

Java|Java 多线程 万字最详解
文章图片

线程休眠sleep()
线程休眠
public class ThreadDemo1 { public static void main(String[] args) throws InterruptedException { for (int i = 0; i < 10; i++) { Thread.sleep(1000); System.out.println("hahaha"); } } }

线程中断interrupt()
在早期,有个 stop()方法,但是,使用 stop 强行让一个线程死亡,可能导致其没来得及释放资源,导致资源的浪费。
所以,我们要使用interrupt()来让线程察觉到外部标记,然后自己决定死亡
public class ThreadDemo2 { public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> { for (int i = 0; i < 10; i++) { try { Thread.sleep(1000); System.out.println(Thread.currentThread().getName()+":"+i); //这里的 InterruptedException ,就是检查有没有被中断的 } catch (InterruptedException e) { //e.printStackTrace(); System.out.println("检测到打扰标记,线程死亡!"); /** * 在死亡前,可以在这里进行资源释放 * 类似于交代后事 */ System.out.println("爷要 si 了,资源都释放掉了!"); //如果要线程死亡,我们只要让 run 方法 return 就好了 return; } } }, "子线程"); t1.start(); for (int i = 0; i < 5; i++) { Thread.sleep(1000); System.out.println(Thread.currentThread().getName()+":"+i); }/** * 给线程 t1 添加中断标记 */ t1.interrupt(); } }

Java|Java 多线程 万字最详解
文章图片

Java|Java 多线程 万字最详解
文章图片

守护线程setDaemon()
用户线程:当一个进程不包括任何一个存货的用户线程时,进行结束
**守护线程:**当最后一个用户线程结束后,守护线程自动死亡
==注意:==守护线程的设置,一定要在线程启动之前
public class ThreadDemo3 { public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { for (int i = 0; i < 10; i++) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + ":" + i); } }, "守护线程"); /** * 将 t1 设置为守护线程 * 一定要在 start 之前,进行守护线程的设置 */ t1.setDaemon(true); t1.start(); for (int i = 0; i < 5; i++) { Thread.sleep(1000); System.out.println(Thread.currentThread().getName()+":"+i); } } }

Java|Java 多线程 万字最详解
文章图片

主线程之前完成join()
join()的作用,是保证子线程,在主线程之前完成
如果没有设置超时,那么,主线程在子线程完成之前,不会与之抢夺时间片
public class JoinDemo { public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> { for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName()); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }, "子线程"); t.start(); t.join(); for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName()); } } }

Java|Java 多线程 万字最详解
文章图片

join(int time)还可以设置子线程排在主线程前面的时间:
其他代码不变,这里,我们设置子线程排在主线程前面的时间为
Java|Java 多线程 万字最详解
文章图片

并为主线程每次打印增加等待
Java|Java 多线程 万字最详解
文章图片

可以看到,在两秒后,主线程便开始与子线程抢夺时间片:
Java|Java 多线程 万字最详解
文章图片

线程安全问题 线程不安全的演示
public class ThreadDemo4 { public static void main(String[] args) {MyThread t = new MyThread(); new Thread(t,"A").start(); new Thread(t,"B").start(); new Thread(t,"C").start(); } }class MyThread implements Runnable {private int ticket = 10; @Override public void run() { while (ticket>0) { System.out.println(Thread.currentThread().getName()+"开始卖票"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } ticket--; System.out.println(Thread.currentThread().getName()+"余票:"+ticket); } } }

Java|Java 多线程 万字最详解
文章图片

解决方法 1:同步代码块
public class ThreadDemo5 {public static void main(String[] args) {MyThread2 t = new MyThread2(); new Thread(t,"A").start(); new Thread(t,"B").start(); new Thread(t,"C").start(); }}class MyThread2 implements Runnable {//临界资源区private int ticket = 10; /*** 锁的对象,是要是唯一的,都可以* 有时候为了不使用自定义的锁,甚至可以使用 Class 作为锁* 比如使用 MyThread2.class*/private static final Object lock=new Object(); @Overridepublic void run() {while (true) {synchronized (lock) {if (ticket<=0) break; else {System.out.println(Thread.currentThread().getName()+"开始卖票"); try {Thread.sleep(1000); } catch (InterruptedException e) {e.printStackTrace(); }ticket--; System.out.println(Thread.currentThread().getName()+"余票:"+ticket); }}}}}

Java|Java 多线程 万字最详解
文章图片

解决方法 2:同步方法
即在方法前,加上 synchronized 关键字
同步方法,其实使用的是隐式锁
如果是同步方法,锁的是当前对象
如果是静态方法,锁的是该类的 Class
解决方法 3:显示锁 Lock
使用 JUC 里的 lock
public class ThreadDemo6 { public static void main(String[] args) {MyThread3 t = new MyThread3(); new Thread(t,"A").start(); new Thread(t,"B").start(); new Thread(t,"C").start(); } }class MyThread3 implements Runnable {private int ticket = 10; private Lock lock = new ReentrantLock(); @Override public void run() {while (true) { lock.lock(); if (ticket<=0) break; else { System.out.println(Thread.currentThread().getName()+"开始卖票"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } ticket--; System.out.println(Thread.currentThread().getName()+"余票:"+ticket); } lock.unlock(); } } }

Java|Java 多线程 万字最详解
文章图片

更多 synchronized 用法
之前我写过一个详细的 synchronized 用法的博客,点此跳转
或者看 java/JUC/多线程.md
显示锁的问题 公平锁和非公平锁
**公平锁:**先来,先得到锁
**非公平锁:**大家争抢这个锁
我们一般使用的,都是非公平锁(synchronized,ReentrantLock)
怎么实现公平锁
在构造 ReentrantLock 时,传入一个 true,表示这是一个公平锁
Lock lock = ReentrantLock(true);

多线程经典问题 线程死锁
什么是死锁?
线程 A,拿到 A锁,执行任务,中途又需要 B 锁
线程 B,拿到 B锁,执行任务,中途又需要 A 锁
如果这两个线程是同时执行的,那 A拿不到 B 锁,B拿不到 A 锁,就会相互僵持,出现死锁
多线程通信
比如说我们想要在下载完以后,自动播放电影
那么,当下载线程结束以后,要去通知播放线程
这,就是多线程通信
多线程通信的实现,是:
Object 里的 wait()notify()/notifyAll()方法
生产者和消费者 有了wait()notify()/notifyAll()方法,我们就可以解决生产者和消费者问题
所谓生产者和消费者问题,就是设置一个缓存,当缓存为空,消费者不能消费,生产者必须生产;当缓存已经满了,生产者必须停止,消费者必须消费;剩下的时候,生产者、消费者可以随意。
因为这个缓存是临界区,所以必须加锁
但是,光加锁,不能解决生产者、消费者问题。如果在任何时候,都让生产者和消费者自由争抢时间片,那如果缓存已满,可能生产者还会抢到时间片,这样,就会让缓存溢出。
这个时候,就需要用到线程通信,来解决生产者和消费者问题。
我们定义三个类:
**Cook:**生产者,每次做和上一次不一样的菜
**Waiter:**消费者,每次端出菜
**Food:**食物,缓存长度为 1,即没有菜的时候,cook必须做菜,waiter 不能端菜;有菜的时候,cook 不能做菜,waiter 必须端菜
public class ProducerAndCustomer { public static void main(String[] args) { Food food = new Food(); new Thread(new Cook(food)).start(); new Thread(new Waiter(food)).start(); } }class Waiter implements Runnable {private Food food; public Waiter(Food food) { this.food = food; }@Override public void run() { for (int i = 0; i < 100; i++) { /** * 这里因为要访问临界资源 isEmpty,所以必须加锁 */ synchronized (food) { if (!food.isEmpty) { food.get(); food.isEmpty=true; food.notifyAll(); try { food.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } } }class Cook implements Runnable{private Food food; public Cook(Food food) { this.food = food; }@Override public void run() { for (int i = 0; i < 100; i++) { /** * 这里因为要访问临界资源 isEmpty,所以必须加锁 */ synchronized (food) { if (food.isEmpty) { if (i%2==0) { food.setName("煎饼果子"); food.setTest("咸味"); } else { food.setName("豆腐脑"); food.setTest("超甜味"); }System.out.println("厨师把菜做好了,菜品为:"+food.toString()); //厨师做好了菜,将盘子设置为不空 food.isEmpty=false; //唤醒其他线程 food.notifyAll(); //当前线程休眠 try { food.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } } }class Food { private String name; private String test; public boolean isEmpty=true; public Food(String name, String test) { this.name = name; this.test = test; }public Food() { }public String getName() { return name; }public void setName(String name) { this.name = name; }public String getTest() { return test; }public void setTest(String test) { this.test = test; }public void get() { System.out.println("服务员端走了菜:"+this.toString()); }@Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("名称:").append(this.name).append("\n") .append("味道:").append(this.test).append("\n"); return sb.toString(); } }

结果:
我们可以看到,是严格按照先做菜,再端菜的顺序执行的。
Java|Java 多线程 万字最详解
文章图片

线程的状态 线程一共有种状态
Java|Java 多线程 万字最详解
文章图片

线程池 概念
如果我们要使用大量线程,每个线程执行的时间很短,那每次创建、销毁线程,就会浪费大量时间。所以,我们要先提前创建部分线程,要用的时候,去里面取,不用的时候,再放回去
好处
  • 降低资源消耗
  • 提高响应速度
  • 提高线程的可管理性
虽然好处多多,但是,我们在日常开发的时候,使用自定义线程池的几率少之又少,因为 Java 本来就是按照多线程去设计的,不用我们再手动创建线程池。
线程池的分类
一共有四种线程池
  1. 缓存线程池
/** 缓存线程池. (长度无限制) 执行流程: 1. 判断线程池是否存在空闲线程 2. 存在则使用 3. 不存在,则创建线程 并放入线程池, 然后使用 * * * * * * */ ExecutorService service = Executors.newCachedThreadPool(); //向线程池中 加入 新的任务 service.execute(new Runnable() { @Override public void run() { System.out.println("线程的名称:"+Thread.currentThread().getName()); } }); service.execute(new Runnable() { @Override public void run() { System.out.println("线程的名称:"+Thread.currentThread().getName()); } }); service.execute(new Runnable() { @Override public void run() { System.out.println("线程的名称:"+Thread.currentThread().getName()); } });

  1. 定长线程池
/** * 定长线程池. * (长度是指定的数值) * 执行流程: * 1. 判断线程池是否存在空闲线程 * 2. 存在则使用 * 3. 不存在空闲线程,且线程池未满的情况下,则创建线程 并放入线程池, 然后使用 * 4. 不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程 */ ExecutorService service = Executors.newFixedThreadPool(2); service.execute(new Runnable() { @Override public void run() { System.out.println("线程的名称:"+Thread.currentThread().getName()); } }); service.execute(new Runnable() { @Override public void run() { System.out.println("线程的名称:"+Thread.currentThread().getName()); } });

  1. 单线程线程池
有时候,我们可能需要多个作业排队执行,这个时候,就需要用到这种线程池了
效果与定长线程池 创建时传入数值1 效果一致. /** * 单线程线程池. 执行流程: * 1. 判断线程池 的那个线程 是否空闲 * 2. 空闲则使用 * 4. 不空闲,则等待 池中的单个线程空闲后 使用 */ ExecutorService service = Executors.newSingleThreadExecutor(); service.execute(new Runnable() { @Override public void run() { System.out.println("线程的名称:"+Thread.currentThread().getName()); } }); service.execute(new Runnable() { @Override public void run() { System.out.println("线程的名称:"+Thread.currentThread().getName()); } });

  1. 周期性任务定长线程池
Java|Java 多线程 万字最详解
文章图片

    推荐阅读