跳到正文
返回

学毛线(Thread)的 Java

发表于 更新于
浏览量: --

封面来源:碧蓝航线 铁血、音符 & 誓言 活动 CG

本文视频参考地址:【狂神说Java】多线程详解

1. 线程简介

1.1 多任务

什么叫多任务?

其实现实生活中就有很多这样的例子,比如:边吃饭边玩手机的低头族、又开车又打电话还挂点滴的疯狂司机,以及边玩手机边撇条的「厕所困难户」。当然这些行为都是不好的!

现实中这样的例子有很多,看起来是多个任务都在做,实质上我们的大脑在同一时间依旧只做了一件事情。

1.2 普通方法调用和多线程

普通方法调用和多线程

1.3 程序、进程、线程

说起进程(Process),就不得不说说 程序 。程序是指指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念。

进程 则是执行程序的一次执行过程,它是一个动态的概念,是系统资源分配的单位。

通常在一个进程中可以包含若干个 线程 (Thread),当然一个进程中至少有一个线程,不然没有存在的意义。线程是 CPU 调度和执行的单位。

注意: 许多多线程都是模拟出来的,真正意义上的多线程是指有多个 CPU,即多核,比如服务器。如果是模拟出来的多线程,即在一个 CPU 的情况下,在同一个时间点,CPU 只能执行一个代码(时间片轮转调度了解一下?),因为切换得很快,所以就有了同时执行的错觉。

1.4 核心概念

线程是独立的执行路径。

在程序运行时,即使没有自己创建线程,后台也会有多个线程,比如主线程、GC 线程等。

main() 称之为主线程,为系统的入口,用于执行整个程序。

在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统密切相关的,先后顺序是不可以人为干预的。

对同一份资源进行操作时,会存在资源抢夺的问题,因此需要加入并发控制。

线程会带来额外的开销,如 CPU 调度时间、并发控制开销等。

每个线程在自己的工作内存交互,内存控制不当会造成数据不一致。

2. 线程的创建

2.1 继承 Thread 类

编写步骤

1、自定义线程类继承 Thread 类

2、重写 run() 方法,编写线程执行体

3、创建线程对象,调用 start() 方法启动线程

但需要注意的是:线程开启不一定立即执行,是由 CPU 调度执行的

代码实现

package com.yang.demo01;

/**
 * @author 默烦 2020/10/5
 */
/**
 * 创建线程方式一
 * 1. 继承 Thread 类,
 * 2. 重写 run() 方法,
 * 3. 调用 start() 开启线程
 */
public class TestThread1 extends Thread{

    @Override
    public void run() {
        // run 方法线程体
        for (int i = 0; i < 20; i++) {
            System.out.println("我在吃饭---" + i);
        }
    }

    public static void main(String[] args) {
        // main 线程,主线程
        // 创建线程对象
        TestThread1 testThread1 = new TestThread1();
        // 调用 start() 方法开启线程
        testThread1.start();
        for (int i = 0; i < 20; i++) {
            System.out.println("我在玩游戏---" + i);
        }
    }
}

运行上述代码后,我们可以发现每次的运行结果不同(如果发现相同,请把循环次数调大,比如 1000)。

练习 - 批量下载网络图片

/**
 * @author 默烦 2020/10/5
 */
// 练习 Thread ,实现多线程同步下载图片
public class TestThread2 extends Thread{
    public String url; // 网络图片地址
    public String name; // 保存的文件名

    public TestThread2(String url, String name) {
        this.url = url;
        this.name = name;
    }

    @Override
    public void run() {
        WebDownloader webDownloader = new WebDownloader();
        webDownloader.downloader(url, name);
        System.out.println("下载的文件名为: " + name);
    }

    public static void main(String[] args) {
        TestThread2 t1 = new TestThread2("http://i0.hdslb.com/bfs/archive/a8cb2b3c23aec9d944784bf87ab55242a8700b7e.jpg", "1.jpg");
        TestThread2 t2 = new TestThread2("http://i1.hdslb.com/bfs/archive/856f9b8c817bc98b53e2ce642d88b5b520b0ed3b.jpg", "2.jpg");
        TestThread2 t3 = new TestThread2("http://i2.hdslb.com/bfs/archive/d9be812df14c44f1b554b055f3d5bd184bd62a3e.jpg", "3.jpg");

        t1.start();
        t2.start();
        t3.start();
    }
}


// 下载器
class WebDownloader {
    // 下载方法
    public void downloader(String url, String name) {
        try {
            FileUtils.copyURLToFile(new URL(url), new File(name));
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("IO 异常,下载器出现问题!");
        }
    }
}

上述下载的图片是 B 站的封面图片,感觉以我现在的知识储备,似乎可以做一个根据网址或 bv \ av 号获取 B 站视频封面的小工具?(挖个坑)

2.2 实现 Runnable 接口

编写步骤

1、自定义线程类实现 Runnable 接口

2、实现 run() 方法,编写线程执行体

3、创建线程对象,调用 start() 方法启动线程

因为 Java 单继承的局限性,推荐使用 Runnable 接口来创建线程。

代码实现

package com.yang.demo01;

/**
 * @author 默烦 2020/10/6
 */
// 创建线程方式二,实现 Runnable 接口
public class TestThread3 implements Runnable{

    @Override
    public void run() {
        // run 方法线程体
        for (int i = 0; i < 20; i++) {
            System.out.println("我在吃饭---" + i);
        }
    }

    public static void main(String[] args) {
        // 创建 Runnable 接口的实现类对象
        TestThread3 testThread3 = new TestThread3();
        // 创建线程对象,通过线程对象来开启线程,代理
//        Thread thread = new Thread(testThread3);
//        thread.start();
        new Thread(testThread3).start();


        for (int i = 0; i < 20; i++) {
            System.out.println("我在玩游戏---" + i);
        }
    }
}

小结

继承 Thread 类:

实现 Runnable 接口:

2.3 初识并发

上面讲述了两种创建线程的方法,假设现在有多个线程来使用同一个对象,会出现什么情况呢?

我们来模拟一下最经典的「买票问题」:

package com.yang.demo01;

/**
 * @author 默烦 2020/10/6
 */
// 多个线程同时操作同一个对象
// 模拟买票
public class TestThread4 implements Runnable {

    private int ticketNums = 10; // 票数


    @Override
    public void run() {
        while (true) {
            if (ticketNums <= 0) break;

            // 模拟延时
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"--->拿到了第"+(ticketNums--)+"张票");
        }
    }

    public static void main(String[] args) {
        TestThread4 ticket = new TestThread4();

        // 输出时出现问题:
        // 多个线程操作同一个资源的情况下,线程不安全,数据紊乱
        new Thread(ticket, "默烦").start();
        new Thread(ticket, "Mofan").start();
        new Thread(ticket, "黄牛党").start();
    }
}

运行上述代码后,在控制台可能看到如下情况:

这种情况就是在多个线程操作同一个对象(资源)的情况下,导致线程不安全,进而数据紊乱。

2.4 模拟龟兔赛跑

package com.yang.demo01;

/**
 * @author 默烦 2020/10/6
 */
// 模拟龟兔赛跑
public class Race implements Runnable {

    private static String winner; // 胜利者

    @Override
    public void run() {
        for (int i = 0; i <= 100; i++) {
            // 模拟兔子睡觉
            if (Thread.currentThread().getName().equals("兔子") && (i % 10 == 0)) {
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            // 判断比赛是否结束
            boolean flag = gameOver(i);
            // 如果比赛结束
            if (flag){
                break;
            }
            System.out.println(Thread.currentThread().getName() + "--->跑了" + i + "步");
        }
    }

    // 判断是否完成比赛
    private boolean gameOver(int steps) {
        // 判断是否有胜利者
        if (winner != null) {
            return true;
        }
        if (steps >= 100) {
            winner = Thread.currentThread().getName();
            System.out.println("winner is " + winner);
            return true;
        }
        return false;
    }

    public static void main(String[] args) {
        Race race = new Race();
        new Thread(race, "兔子").start();
        new Thread(race, "乌龟").start();
    }
}

2.5 实现 Callable 接口

PS:当前只做了解即可。

编写步骤

1、实现 Callable 接口,需要返回值类型

2、重写 call() 方法,需要抛出异常

3、创建目标对象

4、创建执行服务

ExecutorService service = Executors.newFixedThreadPool(1);

5、提交执行

Future<Boolean> result1 = service.submit(t1);

6、获取结果

boolean r1 = result1.get();

7、关闭服务

service.shutdownNow();

演示:利用 Callable 改造图片下载案例

代码实现

package com.yang.demo02;

import org.apache.commons.io.FileUtils;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.concurrent.*;

/**
 * @author 默烦 2020/10/6
 */
// 线程创建方式三:实现 Callable 接口

/**
 * Callable 的好处
 * 1. 可以定义返回值
 * 2. 可以抛出异常
 */
public class TestCallable implements Callable<Boolean> {
    public String url; // 网络图片地址
    public String name; // 保存的文件名

    public TestCallable(String url, String name) {
        this.url = url;
        this.name = name;
    }


    @Override
    public Boolean call() throws Exception {
        WebDownloader webDownloader = new WebDownloader();
        webDownloader.downloader(url, name);
        System.out.println("下载的文件名为: " + name);
        return true;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        TestCallable t1 = new TestCallable("http://i0.hdslb.com/bfs/archive/a8cb2b3c23aec9d944784bf87ab55242a8700b7e.jpg", "1.jpg");
        TestCallable t2 = new TestCallable("http://i1.hdslb.com/bfs/archive/856f9b8c817bc98b53e2ce642d88b5b520b0ed3b.jpg", "2.jpg");
        TestCallable t3 = new TestCallable("http://i2.hdslb.com/bfs/archive/d9be812df14c44f1b554b055f3d5bd184bd62a3e.jpg", "3.jpg");

        // 创建执行服务
        ExecutorService service = Executors.newFixedThreadPool(3);
        // 提交执行
        Future<Boolean> result1 = service.submit(t1);
        Future<Boolean> result2 = service.submit(t2);
        Future<Boolean> result3 = service.submit(t3);
        // 获取结果
        boolean r1 = result1.get();
        boolean r2 = result2.get();
        boolean r3 = result3.get();

        System.out.println(r1);
        System.out.println(r2);
        System.out.println(r3);
 
        // 关闭服务
        service.shutdownNow();
    }
}

// 下载器
class WebDownloader {
    // 下载方法
    public void downloader(String url, String name) {
        try {
            FileUtils.copyURLToFile(new URL(url), new File(name));
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("IO 异常,下载器出现问题!");
        }
    }
}

3. Lambda 表达式

3.1 基本含义

λ 是希腊字母表中排序第十一位的字母,英文名称为 Lambda

Lambda 表达式可以避免匿名内部类定义过多,其实质属于函数式编程的概念:

(params) -> expression [表达式]
 (params) -> statement [语句]
  (params) -> {statement}

比如:

new Thread(() -> System.out.println("多线程学习......")).start();

为什么要使用 Lambda 表达式

也许你会说,看了 Lambda 表达式后,不但不觉得简洁,反而觉得更乱了,而且还看不懂了。

那是因为我们没有使用习惯,用得多了,看习惯了,自然就好了。

3.2 函数式接口

理解 Functional Interface(函数式接口)是学习 Java 8 Lambda 表达式的关键所在。

函数式接口的定义:

任何接口,如果只包含唯一一个抽象方法,那么它就是函数式接口,比如:

public interface Runnable {
    public abstract void run();
}

对于函数式接口,我们可以通过 Lambda 表达式来创建该接口的对象。

3.3 Lambda 的演化

Lambda 表达式针对的是函数式接口,函数式接口也是一种接口,只不过比较特殊。

因此,函数式接口也可以有实现类。如果想要使用一个函数式接口,就需要新开一个类,这个类作为函数式接口的 实现类 ,再对这个实现类实例化,然后使用实例化得到的对象调用方法。

但是这样需要新开一个类,为了不新开一个类可以使用当前类中编写一个 静态嵌套类 ,这个静态嵌套类也需要实现接口,后续的使用就和最开始的一样。

在当前类编写静态嵌套类会「污染」当前类,这个时候可以在方法内部编写 局部内部类

还可以对局部内部类进行简化,将其书写成 匿名内部类

对于函数式接口来说,这个接口只有唯一的一个抽象方法,因此后续使用这个接口时,接口的名称、抽象方法名都不是那么重要,可以将它们省略。

在 JDK 8 中引入了新特性 —— Lambda 表达式 ,使用 Lambda 表达式后可以让函数式接口的使用变得简洁,也避免了匿名内部类定义过多。

演化代码

package com.yang.lambda;

/**
 * @author 默烦 2020/10/6
 */
// lambda 表达式推导
public class TestLambda1 {

    // 3. 静态嵌套类
    static class Like2 implements ILike {

        @Override
        public void lambda() {
            System.out.println("I LIKE Lambda2!");
        }
    }

    public static void main(String[] args) {
        ILike like = new Like();
        like.lambda();

        like = new Like2();
        like.lambda();

        // 4. 局部内部类
        class Like3 implements ILike {
            @Override
            public void lambda() {
                System.out.println("I LIKE Lambda3!");
            }
        }

        like = new Like3();
        like.lambda();

        // 5. 匿名内部类
        // 没有类的名称,必须借助接口或父类
        like = new ILike() {
            @Override
            public void lambda() {
                System.out.println("I LIKE Lambda4!");
            }
        };
        like.lambda();


        // 6. 用 Lambda 进行简化
        like = () -> {
            System.out.println("I LIKE Lambda5!");
        };
        like.lambda();

    }
}

// 1. 定义一个函数式接口
interface ILike {
    void lambda();
}

// 2. 实现类
class Like implements ILike {
    @Override
    public void lambda() {
        System.out.println("I LIKE Lambda!");
    }
}

Lambda 表达式的简化

Lambda 表达式在一定条件下还可以进一步简化,让代码变得精简。

比如:

package com.yang.lambda;

/**
 * @author 默烦 2020/10/6
 */
public class TestLambda2 {

    public static void main(String[] args) {

        ILove love1 = (int a) ->{
                System.out.println("I Love You --> " + a);
        };

        // 简化1:参数类型简化
        ILove love2 = (a) -> {
            System.out.println("I Love You --> " + a);
        };

        // 简化2:简化括号
        ILove love3= a -> {
            System.out.println("I Love You --> " + a);
        };

        // 简化3:简化大括号
        ILove love4 = a -> System.out.println("I Love You --> " + a);
        /**
         * Lambda 表达式内执行语句只有一行时,可以去掉大括号
         * 前提是接口为函数式接口
         * 如果接口的抽象方法有多个参数,可以去掉参数的类型,但必须加上括号
         */
        love4.love(2);
    }
}

interface ILove {
    void love(int a);
}

如果函数式接口中的抽象方法有返回值,只需要在 {} 包裹的函数体内 return 即可。

4. 静态代理

为什么多线程讲的好好的,突然讲到 Lambda 表达式,然后又讲到静态代理呢?

那肯定是因为多线程里面涉及了这两个知识点嘛~ 😜

创建线程的第二种方式,使用实现 Runnable 接口的方式就和动态代理一样。

我们可以看看下述以婚庆公司为例子的静态代理:

package com.yang;

/**
 * @author 默烦 2020/10/6
 */
public class StaticProxy {
    
    // 创建线程的方式与案例的对比
    public static void main(String[] args) {

        // Lambda 表达式写法
//        new Thread(()->System.out.println("线程1")).start();
//        new WeddingCompany(()-> System.out.println("哥要结婚了")).HappyMarry();

        // 常见写法
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程2");
            }
        });
        thread.start();
        
        WeddingCompany weddingCompany = new WeddingCompany(new You());
        weddingCompany.HappyMarry();
    }
}

interface  Marry {
    void HappyMarry();
}

// 真实角色 You 去结婚
class You implements Marry {

    @Override
    public void HappyMarry() {
        System.out.println("你要去结婚啦!");
    }
}

// 代理角色 帮助你结婚
class WeddingCompany implements Marry {

    // 代理角色代理谁? 被代理的目标
    private Marry target;

    public WeddingCompany(Marry target) {
        this.target = target;
    }

    @Override
    public void HappyMarry() {
        before();
        this.target.HappyMarry(); // 真实对象调用
        after();
    }

    private void after() {
        System.out.println("结婚之后,收尾款");
    }

    private void before() {
        System.out.println("结婚之前,布置现场");
    }
}

根据 main() 方法中的对比可以很容易地看出:使用实现 Runnable 接口创建线程的方式就是一种静态代理!

总结

1、真实角色和代理对象需要实现同一个接口

2、代理对象要代理真实对象

这样做的好处:可以让代理对象做很多真实对象做不了的事,真实对象可以专注于自己的事

5. 线程状态

5.1 状态概述

相关概述

线程状态图解

Thread t = new Thread();,这行代码可以创建一个线程,线程对象一旦创建就进入到新生状态。

当调用 start() 方法时,线程立即进入就绪状态,但这并不意味着线程立即被调度执行。

当线程被调度执行后,线程立即进入运行状态,线程才真正执行线程体里的代码块。

当调用 sleep()wait() 或同步锁定时,线程进入阻塞状态,进入阻塞状态后,代码就不会往下执行。阻塞事件解除后,重新进入就绪状态,等待 CPU 调度执行。

线程中断或结束,一旦进入死亡状态,就不能再次启动。

线程方法

方法说明
setPriority(int newPriority)更改线程的优先级
static void sleep(long millis)在指定的毫秒数内让当前正在执行的线程休眠
void join()等待该线程终止
static void yield()暂停当前正在执行的线程对象,并执行其他线程
void interrupt()中断线程,不建议使用
boolean isAlive()测试线程是否处于活动状态

5.2 线程停止

JDK 官方已不推荐使用 stop()destory() 来停止一个线程。

建议使用一个标志位进行终止变量,当 flag == false 时,终止线程运行。

测试代码

package com.state;

/**
 * @author 默烦 2020/10/6
 */

/**
 * 测试 stop
 */
public class TestStop implements Runnable {

    // 1. 设置一个标志位
    private boolean flag = true;

    @Override
    public void run() {
        int i = 0;
        while (flag) {
            System.out.println("run......Thread " + i++);
        }
    }

    // 2. 设置一个公开的方法停止线程,变换标志位
    public void stop() {
        this.flag = false;
    }

    public static void main(String[] args) {
        TestStop testStop = new TestStop();
        new Thread(testStop).start();

        for (int i = 0; i < 1000; i++) {
            System.out.println("main " + i);
            if (i == 900) {
                // 调用 stop() 切换标志位,让线程停止
                testStop.stop();
                System.out.println("线程停止了!");
            }
        }
    }
}

运行后可能会看不到结果,可以多运行几次试试,毕竟我们不能更改 CPU 的调度。

5.3 线程休眠

sleep(休眠时间)指定当前线程阻塞的毫秒数;

sleep 存在异常 InterruptedException

sleep 时间达到后,线程就进入就绪状态;

sleep 可以模拟网络延时,倒计时等;

每一个对象都有一个锁,而 sleep 并不会释放锁。

测试代码

package com.state;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * @author 默烦 2020/10/8
 */
// 模拟时间
public class TestSleep2 {

    public static void main(String[] args) {
        Date startTime = new Date(System.currentTimeMillis());
        while (true) {
            try {
                Thread.sleep(1000);
                System.out.println(new SimpleDateFormat("HH:mm:ss").format(startTime));
                startTime = new Date(System.currentTimeMillis()); // 更新当前时间
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    // 模拟倒计时方法
    public static void tenDown() throws InterruptedException {
        int num = 10;

        while (true) {
            Thread.sleep(1000);
            System.out.println(num--);
            if (num <= 0) {
                break;
            }
        }
    }
}

5.4 线程礼让

礼让(yield)线程,让当前正在执行的线程暂停,但不阻塞。

让线程从运行状态转为就绪状态。

让 CPU 重新调度,但是礼让不一定成功,「需要看 CPU 的心情」。

测试代码

package com.state;

/**
 * @author 默烦 2020/10/8
 */
// 测试礼让线程
// 礼让不一定成功
public class TestYield {
    public static void main(String[] args) {
        MyYield myYield = new MyYield();
        new Thread(myYield, "a").start();
        new Thread(myYield, "b").start();
    }
}

class MyYield implements Runnable {

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " 线程开始执行......");
        Thread.yield(); // 礼让
        System.out.println(Thread.currentThread().getName() + " 线程停止执行......");
    }
}

5.5 线程强制执行

Join 合并线程,等当前线程执行完后,再执行其他线程,其他线程阻塞,可以想象成插队。

与休眠方法 sleep() 一样,join() 方法存在异常 InterruptedException

测试代码

package com.state;

/**
 * @author 默烦 2020/10/8
 */
public class TestJoin implements Runnable{

    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            System.out.println("线程 VIP 来了!" + i);
        }
    }

    public static void main(String[] args) throws InterruptedException{
        // 启动我们自己的线程
        TestJoin testJoin = new TestJoin();
        Thread thread = new Thread(testJoin);

        thread.start();

        // 主线程
        for (int i = 0; i < 500; i++) {
            if (i == 200) {
                thread.join(); // 爷来了,靠边!
            }
            System.out.println("main" + i);
        }
    }
}

5.6 线程状态观测

Thread.State

线程状态。线程可以处于以下状态之一:

NEW:尚未启动的线程处于此状态

RUNNABLE:在 Java 虚拟机中执行的线程处于此状态

BLOCKED:被阻塞等待监视器锁定的线程处于此状态

WAITING:正在等待另一个线程执行特定动作的线程处于此状态

TIMED_WAITING:正在等待另一个线程执行动作达到指定等待时间的线程处于此状态

TERMINATED:已退出的线程处于此状态

一个线程可以在给定时间点处于一个状态,这些状态是不反应任何操作系统线程状态的虚拟机状态。

测试代码

package com.state;

/**
 * @author 默烦 2020/10/8
 */
// 观察测试线程状态
public class TestState {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(()->{
            for (int i = 0; i < 5; i++) {
                try {
                    Thread.sleep(1000);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            System.out.println("///////");
        });

        // 观察状态
        Thread.State state = thread.getState();
        System.out.println(state); // NEW

        // 观察启动后
        thread.start();
        state = thread.getState();
        System.out.println(state);

        // 只要线程不终止,就一直输出状态
        while (state != Thread.State.TERMINATED) {
            Thread.sleep(100);
            state = thread.getState();
            System.out.println(state); // 输出线程状态
        }
    }
}

5.7 线程优先级

Java 提供一个线程调度器来监控程序启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程执行。

线程的优先级用数字表示,范围从 0 ~ 10:

使用以下方式可以改变或获取优先级:getPriority()setPriority(int xxx)

但并不是优先级高的线程就一定会被调用。

优先级低只是意味着获得调度的概率低,并不是优先级低就不会被调用了,这所有都会看 CPU 的调度,甚至可能会出现性能倒置(优先级低的线程先被调用,优先级高的线程反而在等待)。

在设置优先级时,优先级的设定建议在 start() 方法调用前。

测试代码

package com.state;

/**
 * @author 默烦 2020/10/8
 */
public class TestPriority{
    public static void main(String[] args) {
        // 主线程默认优先级
        System.out.println(Thread.currentThread().getName()
                + "-->"
                + Thread.currentThread().getPriority());

        MyPriority myPriority = new MyPriority();
        Thread t1 = new Thread(myPriority);
        Thread t2 = new Thread(myPriority);
        Thread t3 = new Thread(myPriority);
        Thread t4 = new Thread(myPriority);
        Thread t5 = new Thread(myPriority);
        Thread t6 = new Thread(myPriority);

        // 设置优先级再启动
        t1.start();

        t2.setPriority(1);
        t2.start();

        t3.setPriority(4);
        t3.start();

        t4.setPriority(Thread.MAX_PRIORITY); // 10
        t4.start();

        t5.setPriority(8);
        t5.start();

        t6.setPriority(7);
        t6.start();
    }
}

class MyPriority implements Runnable {

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()
                + "-->"
                + Thread.currentThread().getPriority());
    }
}

5.8 守护线程

守护(daemon)线程。线程分为 用户线程守护线程

虚拟机必须确保用户线程执行完毕,虚拟机不用等待守护线程执行完毕,比如:后台记录操作日志、监控内存、垃圾回收等待…

测试代码

package com.state;

/**
 * @author 默烦 2020/10/8
 */
public class TestDaemon {

    public static void main(String[] args) {
        God god = new God();
        You you = new You();

        Thread thread = new Thread(god);
        thread.setDaemon(true); // 默认 false 表示用户线程
        thread.start();

        new Thread(you).start(); // 用户线程启动
    }

}


// God
class God implements Runnable{
    @Override
    public void run() {
        while (true) {
            System.out.println("上帝保佑着你!\uD83D\uDE04");
        }
    }
}


// You

class You implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 36500; i++) {
            System.out.println("一生都开心得活着");
        }
        System.out.println("====== GoodBye World ======");
    }
}

6. 线程同步

6.1 线程同步机制

线程同步

需要在多个线程操作同一个资源时进行线程同步。

并发:同一个对象多个线程 同时操作。比如:抢火车票、两个银行同时取钱…

现实生活中,我们经常会遇到「同一个资源,多个人想去使用」的问题。比如,食堂排队时,每个人都想要打饭,最简单的解决办法就是排队,一个一个来!

处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象。这时候我们就需要线程同步,线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个 对象的等待池 形成队列,等待前面线程使用完毕,下一个线程再使用。

队列和锁

要解决线程同步的安全性,就需要用到队列和锁。

每个对象都有一个锁,这些对象操作同一个资源时,会将锁交给这个资源,当前对象操作完那个资源后,资源就将锁还给对象,操作完资源的对象「撤退」,下一个对象再继续(类似于上厕所)。

线程同步问题

由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突问题,为了保证数据在方法中被访问时的正确性,在访问时加入 锁机制 synchronized,当一个线程获得对象的排它锁,就会独占资源,而其他线程必须等待,使用后释放锁即可。但会存在以下问题:

6.2 线程不安全案例

2.3 初始并发 中,编写了一个买票的经典案例,运行那个案例后,在控制台可能会看到某人购买到第 0 张或第 -1 张票的情况,这种情况就是线程不安全的一种案例。

我们将买票的逻辑抽出来,重新编写一个 buy() 方法,然后在重写的 run() 方法中调用 buy() 方法:

package com.yang.syn;

/**
 * @author 默烦 2020/10/8
 */
// 不安全的买票
    // 运行结果不对,存在负数
public class UnsafeBuyTicket {
    public static void main(String[] args) {
        BuyTicket station = new BuyTicket();

        new Thread(station, "mofan").start();
        new Thread(station, "默烦").start();
        new Thread(station, "You").start();
    }
}

class BuyTicket implements Runnable{
    private int ticketNums = 10; // 票数
    boolean flag = true; // 外部停止标志

    @Override
    public void run() {
        while (flag) {
            try {
                buy();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private void buy() throws InterruptedException {
        if (ticketNums <= 0) {
            flag = false;
            return;
        }
        // 模拟延时
        Thread.sleep(100);
        // 买票
        System.out.println(Thread.currentThread().getName() + "拿到" + ticketNums--);
    }
}

除此之外,我们还可以编写一个银行取钱的案例,在这个案例中,线程也是不安全的:

package com.yang.syn;

/**
 * @author 默烦 2020/10/8
 */
// 不安全的取钱
public class UnsafeBank {
    public static void main(String[] args) {
        // 账户
        Account account = new Account(100, "结婚基金");

        Drawing you = new Drawing(account, 50, "你");
        Drawing girlFriend = new Drawing(account, 100, "girlFriend");

        you.start();
        girlFriend.start();

    }
}

class Account {
    int money; // 余额
    String name; // 卡名

    public Account(int money, String name) {
        this.money = money;
        this.name = name;
    }
}

// 银行:模拟取款
class Drawing extends Thread {
    Account account;
    int drawingMoney; // 取了多少钱
    int nowMoney; // 身上的钱

    public Drawing(Account account, int drawingMoney, String name) {
        super(name);
        this.account = account;
        this.drawingMoney = drawingMoney;
    }

    @Override
    public void run() {
        // 判断是否有钱
        if (account.money - drawingMoney < 0) {
            System.out.println(Thread.currentThread().getName() + " 钱不够,取不了");
            return;
        }
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 卡内余额
        account.money = account.money - drawingMoney;
        // 手中的钱
        nowMoney = nowMoney + drawingMoney;
        System.out.println(account.name + " 余额为:" + account.money);
        // this.getName() 等价于 Thread.currentThread().getName()
        System.out.println(this.getName() + " 手里的钱:" + nowMoney);
    }
}

运行上述代码后,控制台输出的「余额为」可能会是一个负数,这就是线程不安全的表现。

在 JDK 源码动态数组 ArrayList 类中的顶部注释有这样一句话:

Note that this implementation is not synchronized.

简单来说,JDK 提供的 ArrayList 没有实现同步,ArrayList 是线程不安全的。我们可以通过一些代码来进行验证:

package com.yang.syn;

import java.util.ArrayList;

/**
 * @author 默烦 2020/10/8
 */
// 线程不安全的集合
public class UnsafeList {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        for (int i = 0; i < 10000; i++) {
            new Thread(() ->{
               list.add(Thread.currentThread().getName());
            }).start();
        }
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(list.size());
    }
}

运行后,可以在控制台看到数组的长度没有达到 10000,这是由于在某一时刻多个线程向同一个位置插入了多个数据,最终导致数组长度没有达到 10000。因此,JDK 提供的 ArrayList 是线程不安全的。

6.3 同步方法与同步块

我们可以通过 private 关键字来保证数据对象只能被方法访问,所以我们需要针对方法提出一套机制,这套机制就是 synchronized 关键字,它包括两种用法:synchronized 方法和 synchronized 块。

同步方法

同步方法 :

public synchronized void method(int args) {} 

synchronized 方法控制对「对象」 的访问,每个对象对应一把锁,每个 synchronized 方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞。

方法一旦执行,就独占该锁,直到该方法返回才会释放锁,然后后面被阻塞的线程才能获得这个锁,并继续执行。但这有一个很大的缺陷:将一个大的方法申明为 synchronized 会影响效率。

同步方法弊端

一个方法中可能存在只读的代码,也可能存在修改数据的代码,方法里面需要修改数据的代码才需要锁,只读的代码是不用锁的。如果使用同步方法,就是将整个方法锁了,锁得太多,就会浪费资源。

比如,在 6.2 线程不安全案例 中给 buy() 方法添加一个 synchronized 后,让这个方法变成一个同步方法。再次运行时,就不会出现线程不安全的情况。

但是在 6.2 线程不安全案例 中银行取钱的案例中,给重写的 run() 方法加上 synchronized 后,并不能使线程变成安全的。为什么会这样呢?

介绍一下 synchronized 关键字的应用方式。👇

synchronized 关键字

参考链接:【Java并发编程之深入理解】Synchronized的使用

Java 中每一个对象都可以作为锁,这是 synchronized 实现同步的基础:

  1. 普通同步方法(实例方法),锁是当前实例对象,进入同步代码前要获得当前实例的锁;
  2. 静态同步方法,锁是当前类的 class 对象,进入同步代码前要获得当前类对象的锁;
  3. 同步方法块,锁是括号里面的对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

synchronized 关键字锁住的是:同步操作的对象,也就是共享资源。

在银行取钱的案例中出现的是第一种情况,创建了两个不同的对象,因此最终的运行结果仍然没有改变,依旧是线程不安全的。要想规避这种情况,就要使用同步块来解决。

同步块

synchronized (Obj) { }

Obj 称之为 同步监视器Obj 可以是任何对象,但是推荐使用共享资源作为同步监视器。同步方法中无需指定同步监视器 , 因为同步方法的同步监视器就是 this , 就是这个对象本身 , 或者是 class。(反射中进行了讲解)

同步监视器的执行过程:

  1. 第一个线程访问,锁定同步监视器,执行其中代码;
  2. 第二个线程访问,发现同步监视器被锁定,无法访问;
  3. 第一个线程访问完毕,解锁同步监视器;
  4. 第二个线程访问,发现同步监视器没有锁,然后锁定并访问

因此,针对银行取钱的案例中,使用同步块后如下(只针对重写的 run() 方法进行修改):

@Override
public void run() {
    // 锁的对象是变化的量,是需要增删改的对象
    synchronized (account) {
        // 判断是否有钱
        if (account.money - drawingMoney < 0) {
            System.out.println(Thread.currentThread().getName() + " 钱不够,取不了");
            return;
        }
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 卡内余额
        account.money = account.money - drawingMoney;
        // 手中的钱
        nowMoney = nowMoney + drawingMoney;
        System.out.println(account.name + " 余额为:" + account.money);
        // this.getName() 等价于 Thread.currentThread().getName()
        System.out.println(this.getName() + " 手里的钱:" + nowMoney);

    }
}

在银行取钱的案例中,账户中的钱会被多个人取出,因此需要将共享资源(账户中的钱)锁住,就可以保证线程安全了。

针对 JDK 默认提供的非线程安全动态数组 ArrayList,使用同步块就可以解决非线程安全:

for (int i = 0; i < 10000; i++) {
    new Thread(() ->{
        // 使用同步块解决
        synchronized (list) {
            list.add(Thread.currentThread().getName());
        }
    }).start();
}

6.4 CopyOnWriteArrayList

在 JDK 5.0 中引入了一个新的包,名为 java.util.concurrent (简称 JUC),在这个包中提供了在并发编程中很常用到的工具类。

前文说到,JDK 提供了 ArrayList 是非线程安全的,在并发环境下使用会出现数据异常,为此,JUC 中提供了一个相似的类,名为 CopyOnWriteArrayList,它也是一个动态数组,但它是线程安全的。

我们可以测试一下:

package com.yang.syn;

import java.util.concurrent.CopyOnWriteArrayList;

/**
 * @author 默烦 2020/10/8
 */
public class TestJUC {
    public static void main(String[] args) {
        CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
        for (int i = 0; i < 10000; i++) {
            new Thread(()->{
               list.add(Thread.currentThread().getName());
            }).start();
        }
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(list.size());
    }
}

控制台最后输出的结果一直是 10000,并没有出现数据异常的情况。

关于 JUC 的其他内容先挖个坑,以后慢慢填~ 😏

6.5 死锁

多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源,得到那个资源后才能运行,而导致两个或者多个线程都在等待对方释放资源,最后都停止执行的情形。

某一个同步块同时拥有 「 两个以上对象的锁 」 时,就可能会发生 「 死锁 」 的问题。

死锁案例

package com.yang.syn;

import javax.lang.model.element.VariableElement;

/**
 * @author 默烦 2020/10/8
 */
// 死锁:多个线程互相抱着对方需要的资源,然后僵持
public class DeadLock {
    public static void main(String[] args) {
        MakeUp g1 = new MakeUp(0, "灰姑娘");
        MakeUp g2 = new MakeUp(1, "白雪公主");

        g1.start();
        g2.start();
    }
}

// 口红
class Lipstick {

}


// 镜子
class Mirror {

}

// 化妆
class MakeUp extends Thread {

    // 用 static 保证资源只有一份
    static Lipstick lipstick = new Lipstick();
    static Mirror mirror = new Mirror();

    int choice; // 选择
    String girlName; // 使用化妆品的人

    public MakeUp(int choice, String girlName) {
        this.choice = choice;
        this.girlName = girlName;
    }

    @Override
    public void run() {
        // 化妆
        try {
            makeUp();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    // 化妆,互相持有对方的锁,就是需要拿到对方的资源
    private void makeUp() throws InterruptedException {
        if (choice == 0) {
            synchronized (lipstick) { // 获得口红的锁
                System.out.println(this.girlName + " 获得口红的锁");
                Thread.sleep(1000);
                synchronized (mirror) { // 一秒中后获得镜子的锁
                    System.out.println(this.girlName + " 获得镜子的锁");
                }
            }
        } else {
            synchronized (mirror) { // 获得镜子的锁
                System.out.println(this.girlName + " 获得镜子的锁");
                Thread.sleep(2000);
                synchronized (lipstick) { // 两秒中后获得口红的锁
                    System.out.println(this.girlName + " 获得口红的锁");
                }
            }
        }
    }
}

运行上述代码后,我们可以发现控制台卡死了,一直处于:

白雪公主 获得镜子的锁
灰姑娘 获得口红的锁

除非手动结束,否则会一直处于这种状态,当然输出先后可能有所交换。

这是因为白雪公主想要口红的锁,但是灰姑娘又抱着口红的锁;灰姑娘想要镜子的锁,但是白雪公主又抱着镜子的锁,两者互相僵持,谁都不放手,最终死锁。

想要解决也很简单,只需要:

// 化妆,互相持有对方的锁,就是需要拿到对方的资源
private void makeUp() throws InterruptedException {
    if (choice == 0) {
        synchronized (lipstick) { // 获得口红的锁
            System.out.println(this.girlName + " 获得口红的锁");
            Thread.sleep(1000);
        }
        synchronized (mirror) { // 一秒中后获得镜子的锁
            System.out.println(this.girlName + " 获得镜子的锁");
        }
    } else {
        synchronized (mirror) { // 获得镜子的锁
            System.out.println(this.girlName + " 获得镜子的锁");
            Thread.sleep(2000);
        }
        synchronized (lipstick) { // 两秒中后获得口红的锁
            System.out.println(this.girlName + " 获得口红的锁");
        }
    }
}

死锁避免方式

产生死锁的四个必要条件:

  1. 互斥条件:一个资源每次只能被一个进程使用;
  2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放;
  3. 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺;
  4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

上面列出了死锁的四个必要条件,我们只要想办法破其中的任意一个或多个条件就可以避免死锁发生。

6.6 Lock

从 JDK 5.0 开始,Java 提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用 Lock 对象充当。
java.util.concurrent.locks.Lock 接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对 Lock 对象加锁,线程开始访问共享资源之前应先获得 Lock 对象。

ReentrantLock (可重入锁)类实现了 Lock,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是 ReentrantLock,可以显式加锁、释放锁。

测试代码

package com.yang.advanced;


import java.util.concurrent.locks.ReentrantLock;

/**
 * @author 默烦 2020/10/9
 */
public class TestLock {
    public static void main(String[] args) {
        TestLock2 testLock2 = new TestLock2();
        new Thread(testLock2).start();
        new Thread(testLock2).start();
        new Thread(testLock2).start();
    }

}

class TestLock2 implements Runnable {
    int ticketNums = 10;

    // 定义 lock 锁
    private final ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            try {
                lock.lock(); // 开锁
                if (ticketNums > 0) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(ticketNums--);
                } else {
                    break;
                }
            } finally {
                // 解锁
                lock.unlock();
            }

        }
    }
}

运行上述代码后,不会出现数据异常情况,证明我们自定义显式加锁是没有问题的!😎

synchronized 与 Lock 的对比

Lock 是显式锁(手动开启和关闭锁,别忘记关闭锁),而 synchronized 是隐式锁,出了作用域自动释放。

Lock 只有代码块锁,synchronized 有代码块锁和方法锁。

使用 Lock 锁,JVM 将花费较少的时间来调度线程,性能更好,并且具有更好的扩展性(提供更多的子类)。

优先使用顺序:Lock > 同步代码块(已经进入了方法体,分配了相应资源)> 同步方法(在方法体之外)

7. 线程协作

7.1 生产者消费者问题

问题描述

应用场景:生产者和消费者问题。

1、假设仓库中只能存放一件产品,生产者将生产出来的产品放入仓库,消费者将仓库中产品取走消费。

2、如果仓库中没有产品,则生产者将产品放入仓库,否则停止生产并等待,直到仓库中的产品被消费者取走为止。

3、如果仓库中放有产品,则消费者可以将产品取走消费,否则停止消费并等待,直到仓库中再次放入产品为止。

生产者与消费者

问题分析

这是一个线程同步问题,生产者和消费者共享同一个资源,但这又不仅仅是一个线程同步问题,因为生产者和消费者之间相互依赖,互为条件。

对于生产者,在没有生产产品之前,要通知消费者等待,而生产了产品之后,又需要马上通知消费者消费。对于消费者,在消费之后,要通知生产者已经结束消费,需要生产新的产品以供消费。

在生产者消费者问题中,仅使用 synchronized 是不够的。synchronized 可阻止并发更新同一个共享资源,这实现了同步,但 synchronized 不能用来实现不同线程之间的消息传递 (通信)。

线程通信方法

Java 内提供了几个方法解决线程之间的通信问题:

方法名作用
wait()线程一直等待,直到其他线程通知,与 sleep() 不同,它会释放锁
wait(long timeout)指定等待的毫秒数
notify()唤醒一个处于等待状态的线程
notifyAll()唤醒同一个对象上所有调用 wait()方法的线程,优先级别高的线程优先调度

注意: 这些方法均是 Object 类的方法,都 只能在同步方法或者同步代码块中使用,否则会抛出异常 IllegalMonitorStateException

至于为什么是 Objext 类的方法,其实很简单。因为每个对象都有一个锁,因此定义在所有类的「祖宗类」 Object 类中。

这三个方法的调用者必须是同步代码块或同步方法中的同步监视器。比如,同步代码块中的就是 synchronized(this) {} 中的 this,这个 this 就是同步监视器,它可以换成任何一个唯一的对象。也就是说,如果使用 this,这三个方法的调用者必须都是 this(在同步代码块中 this 可以省略)。

wait 和 notify 的理解

wait():一旦执行此方法,当前线程就会进入阻塞状态,并释放同步监视器(锁)。

notify():一旦执行此方法,就会唤醒被 wait 的一个线程。如果有多个线程被 wait,会唤醒优先级高的线程。

notifyAll():一旦执行此方法,就会唤醒所有被 wait 的线程。

假设需要使用两个线程打印 1 ~ 100,线程 A 和线程 B 交替打印。那么则有以下思路:

在同步块中编写打印方法,一进入同步块就先使用 notify() 方法,然后进行打印,打印完后使用 wait() 方法。

理解一下:由于打印的方法在同步块中,一次只能有一个线程进入,假设线程 A 先进入,然后执行 notify() 方法,只不过现在没有被 wait 的方法,就继续执行到打印,执行完打印后,执行 wait() 方法,使当前线程进入阻塞状态,并释放锁。这个时候线程 B 拿到锁,进入同步块中执行,先执行 notify() 方法,唤醒一个线程,由于只有 A 和 B 线程,所以会唤醒 A 线程,但由于锁还在线程 B 手里,因此线程 B 继续执行,线程 A 先不执行。线程 B 执行打印,执行完后,执行 wait() 方法,使线程 B 进入阻塞状态并释放锁以便线程 A 可以继续执行。线程 A 刚被线程 B 唤醒,线程 A 执行 notify() 方法唤醒线程 B,线程 A 打印,执行 wait() 方法,进入阻塞状态。按照这个方式轮流打印,直到打印完。

解决方法

1、管程法

2、信号灯法

3、使用线程池

7.2 管程法

方法描述

简单来说,就是引入一缓冲区。

生产者:负责生产数据的模块 (可能是方法 , 对象 , 线程 , 进程);

消费者:负责处理数据的模块 (可能是方法 , 对象 , 线程 , 进程);

缓冲区:消费者不能直接使用生产者的数据 , 他们之间有个「缓冲区」

生产者需要将生产好的数据放入缓冲区,而消费者从缓冲区拿出数据,不能直接使用生产者的数据。

管程法

测试代码

package com.yang.advanced;

/**
 * @author 默烦 2020/10/9
 */
// 管程法
public class TestPC {
    public static void main(String[] args) {
        SynContainer container = new SynContainer();
        new Producer(container).start();
        new Consumer(container).start();
    }
}

// 生产者
class Producer extends Thread {
    SynContainer container;

    public Producer(SynContainer container) {
        this.container = container;
    }

    // 生产方法
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            container.push(new Chicken(i));
            System.out.println("生产了" + i + "只鸡");
        }
    }
}

// 消费者
class Consumer extends Thread {
    SynContainer container;

    public Consumer(SynContainer container) {
        this.container = container;
    }

    // 消费

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("消费了-->" + container.pop().id + "只鸡");
        }
    }
}

// 产品
class Chicken {
    int id; // 产品编号

    public Chicken(int id) {
        this.id = id;
    }
}

// 缓冲区
class SynContainer {
    // 需要一个容器的大小
    Chicken[] chickens = new Chicken[10];
    // 容器计数器
    int count = 0;

    // 生产者放入产品
    public synchronized void push(Chicken chicken) {
        // 如果容器满了,就需要等待消费者消费
        if (count == chickens.length) {
            // 通知消费者消费,生产等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        // 如果没有满,就丢入产品
        chickens[count] = chicken;
        count++;
        // 通知消费者消费
        this.notifyAll();
    }

    // 消费者消费产品
    public synchronized Chicken pop() {
        // 判断能否消费
        if (count == 0) {
            // 等待生产者生产,消费者等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        // 如果可以消费
        count--;
        Chicken chicken = chickens[count];

        // 吃完了,通知生产者生产
        this.notifyAll();
        return chicken;
    }

}

7.3 信号灯法

所谓信号灯法,就是设置一个标志位。不多 BB,直接整代码:

package com.yang.advanced;

/**
 * @author 默烦 2020/10/9
 */
// 信号灯法
public class TestPC2 {
    public static void main(String[] args) {
        TV tv = new TV();
        new Player(tv).start();
        new Audience(tv).start();
    }
}

// 生产者 --> 演员
class Player extends Thread {
    TV tv;

    public Player(TV tv) {
        this.tv = tv;
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            if (i % 2 == 0) {
                this.tv.play("Hop");
            } else {
                this.tv.play("抖音,记录美好生活~");
            }
        }
    }
}

// 消费者 --> 观众
class Audience extends Thread {
    TV tv;

    public Audience(TV tv) {
        this.tv = tv;
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            tv.watch();
        }
    }
}

// 产品 --> 节目
class TV {
    // 演员表演时,观众等待 T
    // 观众观看时,演员等待 F
    String voice; // 表演的节目
    boolean flag = true;

    // 表演
    public synchronized void play(String voice) {
        if (!flag) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("演员表演了:" + voice);
        // 通知观众观看
        this.notifyAll(); // 通知唤醒
        this.voice = voice;
        this.flag = !this.flag;
    }

    // 观看
    public synchronized void watch() {
        System.out.println("观众观看了:" + voice);
        if (flag) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("观众观看了:" + voice);
        // 通知演员表演
        this.notifyAll();
        this.flag = !this.flag;
    }
}

7.4 线程池

基本概念

背景:当经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。

思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。这样可以避免频繁创建销毁,从而实现重复利用。这类似生活中的公共交通工具。

好处:

corePoolSize:核心池的大小

maximumPoolSize:最大线程数

keepAliveTime:线程没有任务时最多保持多长时间后会终止

使用方法

JDK 5.0 起提供了线程池相关 API:ExecutorServiceExecutors

ExecutorService:真正的线程池接口,常见子类 ThreadPoolExecutor

void execute(Runnable command):执行任务 / 命令,没有返回值,一般用来执行 Runnable

<T> Future<T> submit(Callable<T> task):执行任务,有返回值,一般又来执行 Callable

void shutdown():关闭连接池

Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池

测试代码

package com.yang.advanced;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @author 默烦 2020/10/9
 */
public class TestPool {
    public static void main(String[] args) {
        // 创建服务,创建线程池
        ExecutorService service = Executors.newFixedThreadPool(10);

        // 执行
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());

        // 关闭连接
        service.shutdown();
    }
}

class MyThread implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}

Java 多线程基础完

如果这篇文章对你有帮助,可以通过
支付宝
支付宝
微信
微信
请我喝杯 Coffee ☕


上一篇
Java 泛型
下一篇
【算法】回溯