Java多线程

320 阅读50分钟

线程 进程 多线程

线程:独立的执行路径,一个进程可以有多个线程,比如视频中同时听声音,看图像,看弹幕等。线程是CPU调度和执行的基本单位。
多线程:多条执行路径,主线程与子线程并行交替执行(普通方法只有主线程一条路径)
程序:指令和数据的有序集合,本身没有任何运行的含义,是一个静态的概念
进程:在操作系统中运行的程序就是进程,即执行程序的一次执行过程,是一个动态的概念

概述

  • 在程序运行时,即使没有自己创建线程,后台也会有多个线程,比如主线程,GC线程
  • main()称之为主线程,为系统的入口,用于执行整个程序
  • 在一个进程中,如果开辟了多个线程,线程的运行是由调度器(cpu)安排调度的,调度器是与操作系统紧密相- 关的,先后顺序是不能人为干预的
  • 对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制
  • 线程会带来额外的开销,如CPU调度时间,并发控制开销
  • 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致
  • 很多多线程是模拟出来的,真正的多线程是指有多个cpu,即多核,如服务器。如果是模拟出来的多线程,即在一个cpu的情况下,在同一个时间点,cpu只能执行一个代码,因为切换的很快,所以就有同时执行的错觉。

线程创建

继承Thread类(重点)

  1. 自定义线程类继承Thread
  2. 重写其run()方法,编写线程执行体
  3. 创建线程对象,调用start()方法启动线程

语法格式

//线程不一定立即执行,由CPU安排调度
public class TestThread extends Thread{   //定义一个继承Thread类的TestThread类
    @Override
    public void run() {
        ...//run方法线程体,包含线程执行的代码
    }

    public static void main(String[] args) {
        TestThread [线程对象名] = new TestThread(); //创建一个线程对象
        [线程对象名].start();                         //start()开启线程
           ...//可以创建多个线程对象,并一一开启线程
           ...//也可以直接写一段代码,作为main主线程代码执行
    }
//运行结果并发执行
}

示例(图片下载)

package com.kuang.demo01;   //声明了代码所在的包名为com.mine.demo01

import org.apache.commons.io.FileUtils;
// ↑导入org.apache.commons.io包下的FileUtils工具类,这里被用来从URL下载文件并保存到本地
import java.io.File;
// ↑导入Java标准库中的File类。用于文件和目录的创建、查找等操作
import java.io.IOException;
// ↑导入Java标准库中的IOException异常类。表示输入/输出操作中可能发生的异常。这里被用于捕获和处理下载文件时可能出现的IO异常
import java.net.URL;
// ↑导入Java标准库中代表一个统一资源定位符的URL类。表示一个特定的网络地址,如文件路径、Web页面地址等。这里被用于创建一个指向网络图片地址的URL对象

//定义一个类TestThread,该类继承了Thread类,表示这是一个线程
public class TestThread extends Thread{

    private String url;//用于存储网络图片地址
    private String name;//用于存储保存的文件名

//定义一个构造函数,接受两个参数:网络图片地址,保存的文件名
    public TestThread(String url,String name){
        this.url = url;    //将传入的url赋值给类的url变量
        this.name = name;  //将传入的name赋值给类的name变量
    }

    @Override           //标记下面的方法重写了Thread类中的run方法
    public void run(){   //下载图片线程的执行体
//创建了一个新的WebDownloader对象,用于下载图片
        WebDownloader webDownloader = new WebDownloader();
//调用WebDownloader对象的downloader方法,传入url和name参数,下载图片
        webDownloader.downLoader(url,name);
        System.out.println("下载了文件名为:"+name);   //打印已下载的图片文件名
    }

    public static void main(String[] args) {   //主函数开始,程序的入口点
    //创建三个TestThread线程对象,并分别传入不同的url和name参数
        TestThread thread1 = new TestThread("https://img-blog.csdnimg.cn/img_convert/d8885c9a178b2fcaea732190717b516d.png", "1.jpg");
        TestThread thread2 = new TestThread("https://img-blog.csdnimg.cn/img_convert/d8885c9a178b2fcaea732190717b516d.png", "2.jpg");
        TestThread thread3 = new TestThread("https://img-blog.csdnimg.cn/img_convert/d8885c9a178b2fcaea732190717b516d.png", "3.jpg");
        thread1.start();   //启动第一个线程
        thread2.start();   //启动第二个线程
        thread3.start();   //启动第三个线程
        //结果并没有按顺序执行,且每次运行结果不一样,这是因为线程的执行顺序是不确定的
    }
}

class WebDownloader{   
//定义一个下载方法,用于下载指定url的图片到指定文件中
    public void  downLoader(String url,String name){   
        try {
//使用Apache Commons IO库的FileUtils类的copyURLToFile方法来下载图片
            FileUtils.copyURLToFile(new URL(url),new File(name)); 
        } catch (IOException e) {   //如果发生IOException,则打印堆栈跟踪并输出错误消息
            e.printStackTrace();
            System.out.println("IO 异常,Downloader方法出现问题");
        }
    }

}
//可能的输出结果包括:
1.  图片按任意顺序下载完成
2.  如果在下载过程中出现异常(例如网络中断或磁盘空间不足),则相应的异常信息会被打印出来

实现Runnable 接口(重点)

  1. 自定义线程类实现Runnable接口
  2. 实现run()方法,编写线程执行体
  3. 创建线程对象,调用start()方法启动对象

优先使用:与继承Thread类实现多线程相比,使用Runnable接口更加灵活,因为一个类可以实现多个接口,而只能继承一个父类。此外,使用Runnable接口也更符合Java的“组合优于继承”的设计原则。

语法格式

第一种
Thread [线程对象名] = new Thread(new 类名);  //创建一个新的线程对象,传入实现了Runnable接口的类的实例
[线程对象名].start();  // 启动线程,间接调用了run()方法 

第二种
Thread [线程对象名] = new Thread(new Runnable() {  //创建线程对象[线程对象名]
    @Override
    public void run() {   //定义一个实现了Runnable接口的匿名内部类,并重写了run()方法
        //线程执行的代码
    }
});
[线程对象名].start();  // 启动线程

第三种
//仅适用于创建只需要执行一次并且不需要后续操作的线程
[类名] [类对象名] = new [类名]() //创建实现了Runnable接口的类的对象 
new Thread(类对象名).start(); // 使用该对象创建一个新的线程,并开始

第四种
[类名] [类对象名] = new [类名]() 
new Thread([类对象名],"[新线程的名字]").start();

//如果你需要存储线程引用以供后续操作(例如,停止线程,检查线程状态等),那么通常会创建一个实现了
//Runnable接口的类,并传递其实例给Thread构造函数。这样,你可以将Thread对象存储在某个变量中,
//以便以后使用————第一二种

示例一(买车票)

//声明一个实现Runnable接口的类TestThread4,表示这个类可以作为线程来运行
public class TestThread4 implements Runnable{
    private int ticketNums = 10;    //票数

    @Override
    public void run() {
        while (true){
            if(ticketNums<=0) break;   //表示线程会不断尝试购买火车票,直到票数为0

            try { //模拟延时
                Thread.sleep(200);  //sleep是Thread类中的一个静态方法,用于使当前线程休眠
                                    //200是传递给sleep方法的参数,表示休眠的时间,单位是毫秒
            } catch (InterruptedException e) {
                e.printStackTrace();   //printStackTrace()方法用于输出关于异常的完整跟踪信息
            }

            System.out.println(Thread.currentThread().getName()+"拿到了第"+ticketNums--+"张票");   
            //Thread.currentThread()是Java中Thread类的一个静态方法,返回表示当前线程的Thread对象
            //.getName()是Thread类的一个方法,返回表示该线程的名称的字符串
            //ticketNums--是一个后减操作,意味着先返回ticketNums的值,然后再将其减1。
        }
    }
    public static void main(String[] args) {
        TestThread4 ticket = new TestThread4();   //创建了TestThread4类的一个实例
        new Thread(ticket,"小明").start();  
        //创建了一个新的线程,该线程的名字是小明,并使用ticket实例作为运行对象。然后调用start()方法来启动这个线程
        new Thread(ticket,"老师").start();
        new Thread(ticket,"黄牛党").start();
        
    }
}

详解new Thread(ticket,"小明").start();

  1. new Thread(...): 这是Java的Thread类的一个构造方法,用于创建一个新的线程对象
  2. ticket,"小明": 这是传递给Thread构造方法的参数
    • ticket: 这是传递给线程的第一个参数,通常是一个实现了Runnable接口的对象。在这个场景中,ticket是一个TestThread4类的实例,该类实现了Runnable接口,所以它可以作为线程运行的代码主体
    • "小明": 这是传递给线程的第二个参数,通常是一个字符串,用于给线程命名。这里,我们给线程命名为“小明”
  3. .start(): 这是启动新线程的方法。当调用这个方法时,线程会从它的run方法开始执行。注意,你不能直接调用run()方法,因为这会像普通方法一样执行,而不是在新的线程中执行

程序存在问题:
多个线程操作同一个资源,没有使用同步机制来确保对共享资源(票数)的访问是安全的,线程不安全,数据紊乱

示例二(龟兔赛跑)

public class Race implements Runnable{
    private static String winner;    //胜利者

    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {   //循环从1到100,模拟比赛的每一步

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

            //模拟兔子休息
            if(Thread.currentThread().getName().equals("兔子")&& i%10==0){
            //.equals("兔子") 是String类的一个方法,用于比较两个字符串是否相等。这里检查当前线程的名字是否是“兔子”
            //i%10==0 检查i是否是10的倍数
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    //判断是否完成比赛
    private boolean gameOver(int steps){
        if(winner!=null){  //如果已经有胜利者(即winner不为null),则返回true,表示比赛已经结束
            return true;
        }{
            if (steps==100){
            //如果当前步骤数为100,则设置当前线程的名字为胜利者,并打印胜利者的名字,然后返回true
                winner=Thread.currentThread().getName();
                System.out.println("winner is"+winner);
                return true;
            }
        }
        return false;   //上面的条件都不满足,则返回false,表示比赛还没有结束
    }

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

实现Callable 接口(了解)

实现Callable接口,需要返回值类型
重写call方法,需要抛出异常
创建目标对象
创建执行服务:ExecutorService ser = Executors.newFixedThreadPool(1);
提交执行:Future result1 = ser.submit(11);
获取结果:boolean r1 = result1.get()
关闭服务:ser.shutdownNow();
好处

  • 可以定义返回值
  • 可以抛出异常

示例(图片下载)

import java.util.concurrent.*;  // 导入Java的并发工具包,提供了线程池等并发相关的工具

public class TestCallable implements Callable<Boolean> {   //声明一个实现了Callable接口的TestCallable公共类,并指定返回值的类型为Boolean

    private String url;//网络图片地址
    private String name;//报错扥文件名

    //有参构造
    public TestCallable(String url, String name) {
        this.url = url;
        this.name = name;
    }

    //下载图片线程的执行体
    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 c = new TestCallable("https://img-home.csdnimg.cn/images/20201124032511.png", "1.png");
        TestCallable c1 = new TestCallable("https://img-home.csdnimg.cn/images/20201124032511.png", "2.png");
        TestCallable c2 = new TestCallable("https://img-home.csdnimg.cn/images/20201124032511.png", "3.png");
//创建执行服务,使用Executors工具类的newFixedThreadPool方法创建一个固定大小的线程池,大小为3
        ExecutorService ser = Executors.newFixedThreadPool(3);
        //提交执行
        Future<Boolean> r = ser.submit(c);  //将TestCallable对象c提交到线程池执行,返回一个Future对象r,用于获取执行结果
        Future<Boolean> r1 = ser.submit(c1);
        Future<Boolean> r2 = ser.submit(c2);
        //获取结果
        boolean res = r.get();
        //使用Future对象r的get方法获取执行结果,并存储在变量res中。这里会阻塞直到结果可用
        boolean res1 = r1.get();
        boolean res2 = r2.get();
        System.out.println(res);
        System.out.println(res1);
        System.out.println(res2);
        //关闭服务,ExecutorService 接口中的方法,用于尝试停止所有正在执行的活动任务,并返回等待执行的任务列表
        ser.shutdownNow();
    }
}
//class WebDownloader在前面下载图片已经定义了,这里就不用再次写,直接使用就好

静态代理模式

概述
真实对象和代理对象都要实现同一个接口
代理对象要代理真实角色
好处
代理对象可以做很多对象做不了的事情
真实对象专注做自己的事情

代理模式: 代理模式是一种设计模式,其中一个类代表另一个类的功能。代理模式可以用于控制对对象的访问,例如远程代理(代表远程对象)、虚拟代理(代表一个大型对象或耗时操作)等。

当将多线程与代理模式结合时,可以创建一种代理,该代理可以并发地处理多个请求。每个请求可以由一个单独的线程处理,这可以提高并发处理能力。

如何实现Java多线程静态代理:

  1. 定义接口: 首先,定义一个接口,该接口表示你要代理的服务。
public interface MyService {
    void doSomething();
}
  1. 创建被代理类: 实现上面定义的接口,并提供实现。
public class MyServiceImpl implements MyService {
    @Override
    public void doSomething() {
        // 实现业务逻辑
    }
}
  1. 创建代理类: 这个类将实现与被代理类相同的接口,并使用多线程来处理请求。
import java.util.concurrent.*;

public class MyServiceProxy implements MyService {
    private final ExecutorService executor = Executors.newFixedThreadPool(10); // 创建固定大小的线程池
    private final MyService myService;

    public MyServiceProxy(MyService myService) {
        this.myService = myService;
    }

    @Override
    public void doSomething() {
        executor.submit(() -> { // 使用线程池执行任务
            myService.doSomething(); // 调用被代理类的业务逻辑
        });
    }
}
  1. 使用: 现在,你可以创建MyServiceProxy的实例并使用它,它将使用多线程来调用MyServiceImpl

注意:这里使用静态代理是因为代理类和被代理类有相同的接口。如果你使用动态代理(通过Proxy类),那么代码会有所不同。

  1. 注意事项: 使用多线程和代理模式时,需要考虑线程安全和异常处理。例如,确保共享资源在使用前被正确地初始化,以及在出现异常时能够正确地关闭线程池等。

示例

public class StaticProxy {
    public static void main(String[] args) {
        /*You you = new You();
        you.HappyMarry();*/ //原本方式,下面交给代理

        //线程类代理,实际调用了Runnable接口中的run方法
        new Thread(()-> System.out.println("我爱你")).start();
        //创建了一个WeddingCompany对象,并传入一个You对象作为参数。然后调用其HappyMarry方法
        WeddingCompany company = new WeddingCompany(new You());
        company.HappyMarry();
    }
}

interface Marry{
    void HappyMarry();
}

class You implements Marry{ //真实角色
    @Override
    public void HappyMarry() {
        System.out.println("我今天要结婚了!");
    }
}

class WeddingCompany implements Marry{ //代理角色

//定义了一个私有成员变量target,类型为Marry(即真实对象的接口)
    private Marry target;  //target:真实对象。定义一个私有成员变量target,其类型为Marry

    public WeddingCompany(Marry target) {   //接受一个Marry类型的参数并将其赋值给私有成员变量
        this.target = target;
    }

    @Override
    public void HappyMarry() { //实现代理
        before();
        this.target.HappyMarry(); //调用真实对象的方法
        after();
    }

    private void after() {
        System.out.println("善后工作!");
    }
    private void before() {
        System.out.println("婚前布置!");
    }
    
}

总结:程序通过使用代理模式为婚礼添加了前后的额外操作(布置和善后)。原本的“我要结婚了!”消息被包装在代理的“婚前布置!”和“善后工作!”之间。

分析new Thread(()-> System.out.println("我爱你")).start();
这行代码创建了一个新的线程,并在这个线程中执行一个Lambda表达式打印出“我爱你”这个字符串。
具体来说,这行代码做了以下几件事:

  1. new Thread(): 创建一个新的Thread对象,这是Java中创建线程的标准方式
  2. ()-> System.out.println("我爱你"): 这是一个Lambda表达式,用于定义线程的执行任务。在这个例子中,Lambda表达式只做了一件事,就是打印字符串“我爱你”到控制台。
  3. .start(): 调用Thread对象的start方法,这会启动新创建的线程,并开始执行上面定义的Lambda表达式中的任务 代理类中实现代理的接口方法
    before():在真实对象的方法调用之前执行某些操作。这里它打印“婚前布置!”
    this.target.HappyMarry():调用真实对象的HappyMarry方法。这意味着我们在这里将调用真实对象的方法。这是代理的主要功能之一,即在调用真实对象的方法之前或之后添加额外的操作或逻辑。在这个例子中,我们在调用真实对象的方法之前和之后都打印了一些信息
    after():在真实对象的方法调用之后执行某些操作。这里它打印“善后工作!”

线程状态

线程五大状态

1984a898e6924e4ba44b6a3a492a893c.png a8e1138a4078491db6f26235ca51bcbf.png

线程方法

方法名功能说明
start()启动一个新线程,在新的线程运行 run 方法中的代码start 方法只是让线程进入就绪,里面代码不一定立刻运行(CPU的时间片还没分给它)。每个线程对象的start方法只能调用一次,如果调用了多次会出现IllegalThreadStateException
run()新线程启动后会调用的方法如果在构造 Thread 对象时传递了 Runnable 参数,则线程启动后会调用 Runnable 中的 run 方法,否则默认不执行任何操作。但可以创建 Thread 的子类对象,来覆盖默认行为
void join()等待该线程运行结束
join(long n)等待线程运行结束,最多等待 n 毫秒
getId()获取线程长整型的 idid 唯一
getName()获取线程名
setName(String)修改线程名
getPriority()获取线程优先级
setPriority(int newPriority)修改线程优先级java中规定线程优先级是1~10 的整数,较大的优先级能提高该线程被 CPU 调度的机率
getState()获取线程状态Java 中线程状态是用 6 个 enum 表示,分别为:NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED
isInterrupted()判断是否被打断,不会清除 打断标记
boolead isAlive()测试线程是否处于活动状态(未运行完毕)
void interrupt()中断线程,不推荐使用这个方式如果被打断线程正在 sleep,wait,join 会导致被打断的线程抛出 InterruptedException,并清除 打断标记 ;如果打断的正在运行的线程,则会设置 打断标记 ;park 的线程被打断,也会设置 打断标记
interrupted()static,判断当前线程是否被打断会清除 打断标记
Thread.currentThread()static,获取当前正在执行的线程的引用
static void sleep(long n)static,让当前执行的线程休眠n毫秒,休眠时让出 cpu 的时间片给其它线程
static void yield()暂停当前正在执行的线程对象,并执行其它线程主要是为了测试和调试

线程停止

  • 建议线程正常停止—>设置次数限制,不建议死循环
  • 建议使用标志位(flag)来控制程序的执行流程。通过设置一个布尔类型的变量(通常命名为flagshouldContinue),在程序中的某个点检查该变量的值,并根据需要决定是否继续执行后续的代码
  • 不要使用stop和destroy 等过时或者jdk不建议使用的方法

示例

public class Demo15_StopThread implements Runnable {
    
    private boolean flag = true;   //设置一个标志位

    @Override
    public void run() {   //Runnable 接口要求的方法。当线程启动时,这个方法会被调用
        int i = 0;
        while (flag) {   //只要 flag 为 true,它就会持续打印 "run...Thread" 后跟一个递增的整数
            System.out.println("run...Thread" + i++);
        }
    }
    
    //设置一个公开的方法转换标志位,允许外部调用以更改 flag 的值,控制线程停止
    public void stop() {
        this.flag = false;
    }

    public static void main(String[] args) {
        Demo15_StopThread Stop = new Demo15_StopThread();
        new Thread(stop).start();   //使用实例stop作为参数创建了一个新的线程并启动它
        for (int i = 0; i < 1000; i++) {   
            System.out.println("main..." + i);
         //当循环变量 i 达到900时,它会调用 stop.stop() 来将标志位 flag 设置为 false,从而终止线程
            if (i == 900) {
                Stop.stop();   //调用 Stop 对象上的 stop() 方法
                System.out.println("该线程停止了");
            }
        }
    }
}

线程休眠

  • sleep(时间)指定当前线程阻塞的毫秒数
  • sleep 存在异常InterruptedException
  • sleep 时间达到后线程进入就绪状态
  • sleep 可以模拟网络延时,倒计时等
  • 每一个对象都有一个锁,sleep不会释放锁

语法格式

//表示执行到此句,则使当前线程暂停执行 n 毫秒(n是整数)
//Thread 类是 Java 中的一个预定义类,位于 java.lang 包中,因此不需要定义
Thread.sleep(n);

示例

package com.mine.demo05;

import java.text.SimpleDateFormat;   //导入SimpleDateFormat类,用于日期格式化
import java.util.Date;               //导入Date类,用于获取当前系统时间

//模拟倒计时,模拟时钟,模拟网络延时(买火车票例子)
public class TestSleep2 {

    public static void main(String[] args){

        try { //1.模拟倒计时
            tenDown(); //调用tenDown方法,开始10秒倒计时
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //2.打印当前系统时间
        Date startTime = new Date(System.currentTimeMillis()); //获取当前时间,并存储在startTime变量中

        while (true){ //开始一个无限循环,模拟时钟
            try {
                Thread.sleep(1000); //每隔1s
                //使用SimpleDateFormat格式化startTime为"HH:mm:ss"格式,并打印
                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;     //初始化一个变量num为10,表示倒计时从10开始
        while (true){     //开始一个无限循环,进行倒计时
            Thread.sleep(1000);   //当前线程休眠1000毫秒(即1秒)
            System.out.println(num--);
            if(num<=0) break;
        }
    }
}

线程礼让

  • 礼让线程,让当前正在执行的线程暂停,但不阻塞
  • 将线程从运行状态转化为就绪状态
  • 让CPU重新调度,但不一定礼让成功

语法格式

//表示执行到此句,则提示给操作系统当前线程愿意让出CPU使用权,让其他线程有机会运行,不保证一定能切换线程
//Thread 类是 Java 中的一个预定义类,位于 java.lang 包中,因此不需要定义
Thread.yield();

示例

public class ThreadYield {

    public static void main(String[] args) {
        TestYield testYield = new TestYield();


//创建新的Thread对象,testYield 是一个实现了 Runnable 接口的类的对象
//这意味着它可以作为线程的“运行对象”。当线程启动时,run 方法会被调用,"a" 是新线程的名字
        new Thread(testYield,"a").start();
        new Thread(testYield,"b").start();  //同上
    }
}
class TestYield implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"线程开始");  //打印当前线程的名称和"线程开始"

//调用Thread类的yield方法,使当前线程“礼让”,表示它愿意放弃对处理器的时间片,使得其他线程可以运行
//但实际上,是否真的能让出处理器取决于操作系统的调度器
        Thread.yield();//线程礼让
        System.out.println(Thread.currentThread().getName()+"线程结束");
    }
}

//礼让成功的输出
a线程开始
b线程开始
a线程结束
b线程结束

线程插队

  • Join合并线程,待线程执行完成后,在执行其他线程,其他线程堵塞,相当于插队

语法格式

//表示执行到此句,则中止正在的线程,开始执行[线程对象名]的线程直到执行完毕
[线程对象名].join();

示例

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 {//表示该方法可能会抛出InterruptedException异常
        TestJoin testJoin = new TestJoin();
        //使用前面创建的testJoin实例创建一个新的线程,并将其引用赋值给变量thread
        Thread thread = new Thread(testJoin);
        thread.start(); //启动thread线程

        //主线程
        for (int i = 0; i <= 500; i++) {
            //如果当前计数器的值是200,则thread线程先执行完毕再执行主线程
            if(i==200){
                thread.join();//插队
            }
            System.out.println("main"+i);
        }
    }
}
//输出结果
main0
...
main199
线程vip来了0
...
线程vip来了999
main200
...

线程状态观测

通过 Thread 类中的 getState() 方法来获取线程的状态。这个方法返回一个表示线程状态的 Thread.State 枚举值

Thread.State是一个枚举类型,用于表示线程的状态。它是一个内部枚举类,定义了6个枚举常量,分别代表Java线程的6种状态:

  • NEW 尚未启动的线程处于此状态
  • RUNNABLE 在Java虚拟机中执行的线程处于此状态
  • BLOCKED被阻塞等待监视器锁定的线程处于此状态
  • WAITING 正在等待另一个线程执行特定动作的线程处于此状态
  • TIMED_WAITING 正在等待另一个线程执行动作达到指定等待时间的线程处于此状态
  • TERMINATED已退出的线程处于此状态

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

语法格式

//获取[线程对象名]的线程的状态,并将该状态存储在名为[变量名]的Thread.State类型变量中
Thread.State [变量名] = [线程对象名].getState();

//检查或者判断线程状态是否为RUNNABLE
state == Thread.State.RUNNABLE   //而不是 state == RUNNABLE,同理还有state != Thread.State.TERMINATED

//而打印一个Thread.State类型的变量则仅输出6个枚举常量之一
System.out.println(state)   //RUNNABLE

示例

//这个类的目的是观察线程的状态
public class TestState {
    public static void main(String[] args) throws InterruptedException {

//创建一个新的线程,并将一个Lambda表达式作为其运行对象。这个Lambda表达式定义了线程应该执行的代码
        Thread thread = new Thread(()->{
            for (int i = 0; i < 5; i++) {
                try {
                    Thread.sleep(1000);   //每次循环,线程休眠1秒(1000毫秒)
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("///");
        });

        //观察状态
        Thread.State state = thread.getState();   //获取新创建的线程的状态并赋值给state变量
        System.out.println(state); //NEW。此时线程还未启动,所以它的状态是NEW

        thread.start(); //启动 Run()方法
        state = thread.getState();
        System.out.println(state); //RUNNABLE

        //只要线程不终止
        while (state != Thread.State.TERMINATED){
            Thread.sleep(1000);
            state = thread.getState(); //每隔1s,打印一次线程状态
            System.out.println(state);
        }
        //thread.start();死亡之后的线程不能再次启动,报错
    }
}
//输出结果
NEW
RUNNABLE
TIMED_WAITING  //在线程内部的循环中,调用Thread.sleep(1000)方法,使线程进入TIMED_WAITING状态,表示线程正在休眠或等待一段时间
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TERMINATED  //循环5次后,线程任务结束,进入TERMINATED状态

线程优先级

Java提供一个线程调度器来监控程序中已启动处于就绪状态的所有线程,优先级会影响线程的调动顺序,优先级越高的线程被优先调动的概率越大

Java 线程优先级使用 1 ~ 10 的整数表示
特殊的:

  • 最低优先级 1:Thread.MIN_PRIORITY
  • 最高优先级 10:Thread.MAX_PRIORITY
  • 普通优先级 5(默认):Thread.NORM_PRIORITY

优先级若设置范围在 1-10 之外,则抛出 java.lang.IllegalArgumentException 异常 若要为线程设置优先级,则先设置后启动
若在主线程main中创建线程,且没有显式地指定线程名称,则线程名称是"main"

使用 getPriority方法 获取线程优先级的语法格式

//返回运行此代码的线程的优先级
Thread.currentThread().getPriority();  

//返回[线程对象名]的线程的优先级
[线程对象名].getPriority();

//以上代码等同于整数,可以赋给变量或打印

使用 setPriority方法 设置线程优先级的语法格式

//设置运行此代码的线程的优先级为 n
Thread.currentThread().setPriority(n);  

//设置[线程对象名]的线程的优先级为 n
[线程对象名].setPriority(n);

getPriority方法 与 setPriority方法 的示例

public class TestPriority{
    
    public static void main(String[] args) {
        //main 默认优先级 5
        System.out.println(Thread.currentThread().getName()+"-->"+Thread.currentThread().getPriority());
        
        MyPriority myPriority = new MyPriority();

        Thread t1 = new Thread(myPriority);
        Thread t2 = new Thread(myPriority);

        t1.start();   //Thread-0 默认优先级 5
        t2.setPriority(Thread.MAX_PRIORITY); //最大优先级
        t2.start();   //先设置优先级,再启动
    }
}

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

//可能的输出
main-->5  
Thread-0-->5
Thread-1-->10
或
main-->5 
Thread-1-->10 
Thread-0-->5

守护线程

定义:一种特殊的后台线程,其主要作用是为其他线程提供服务。其在后台执行一些不需要用户交互的任务,并且可以在所有非守护线程执行完毕后自动关闭

  • 线程分为用户线程守护线程(daemon)
  • 虚拟机必须确保用户线程执行完毕
  • 虚拟机不用等待守护线程执行完毕
  • 主要用于执行一些辅助任务,如,后台记录操作日志,监控内存,垃圾回收等待…

语法格式

//DaemonTask 是一个实现了 Runnable 接口的类
Thread [线程对象名] = new Thread(new DaemonTask());  
[线程对象名].setDaemon(true);  

//DaemonThread 是一个继承了 Thread 类的类
DaemonThread [线程对象名] = new DaemonThread();  
[线程对象名].setDaemon(true);  

示例

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(); //用户线程启动
    }
}

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

class You implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 36500; i++) {
            System.out.println("你度过了开心的第"+i+"天");
        }
        System.out.println("-==goodbye,world!==-");
    }
}

线程同步

定义:一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前一个线程使用完毕,下一个线程才能使用

形成条件:队列+锁

并发:同一对象被多个线程同时操作(如,抢票)

锁机制

原因:同一进程的多个线程共享同一块存储空间,这带来了访问冲突问题,为了保证数据在方法中被访问时的正确性,在访问时加入锁机制 synchronized
作用:每个对象都有把锁,当一个线程获得对象的排它锁、独占资源时,其他线程必须等待当前线程执行完毕后释放锁,才能接着执行

存在以下问题:

  • 一个线程持有锁会导致其他所有需要此锁的线程挂起
  • 在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题
  • 如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引起性能问题

不安全的线程示例

public class Demo24_UnsafeBuyTicket {
    public static void main(String[] args) {
        BuyTicket buyTicket = new BuyTicket();
        new Thread(buyTicket, "张三").start();
        new Thread(buyTicket, "李四").start();
        new Thread(buyTicket, "王五").start();
    }
}

class BuyTicket implements Runnable {
    private int ticketNums = 10;    //初始化票数
    boolean flag = true;

    @Override
    public void run() {             //计划买票
        while (flag) {
            try {
                buy();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }


    private void buy() {            //进行买票
        if (ticketNums <= 0) {      //判断是否有票
            flag = false;
            return;
        }
        try {                       //延迟
            Thread.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "拿到" + ticketNums--);  //买票
    }
}

分析

程序不安全的原因:存在竞态条件。竞态条件指发生在两个或更多的线程同时访问共享数据,并且至少有一个线程在修改共享数据时。由于线程调度的不确定性,因此可能导致结果不可预测。最常见的竞态条件是“先检测后执行”,即是否执行由检测的结果决定,而检测结果又被多个线程的执行结果影响 时。

在这段代码中,ticketNums是一个共享资源,被多个线程(张三、李四、王五)同时访问和修改。当多个线程几乎同时进入buy()方法时,它们可能会同时检查ticketNums的值,然后都发现票数大于0,于是都继续执行买票操作。这样就会导致票数被多次减少,甚至出现负数,这显然是不合理的。

synchronized 关键字

同步块和同步方法里面有需要修改的内容才需要锁

同步方法

语法格式

[修饰符] synchronized void [方法名](){}

每个对象都有自己的锁,当一个线程进入一个被synchronized修饰的方法时,它首先会尝试获取该方法所属类的 对象的锁,并且在该线程执行完该方法之前,其他线程无法获得该对象的锁;如果该锁已经被其他线程持有,那么当前线程将被阻塞,直到其他线程执行完毕,从而避免了多线程并发访问造成的数据不一致问题。

  • 缺陷:若将一大段的方法申明为synchronized将会影响效率

安全的线程示例

public class SafeBuyTicket {
    public static void main(String[] args) {
        BuyTicket1 buyTicket = new BuyTicket1();
        new Thread(buyTicket, "张三").start();
        new Thread(buyTicket, "李四").start();
        new Thread(buyTicket, "王五").start();
    }
}

class BuyTicket1 implements Runnable {
    private int ticketNums = 10;      //初始化票数
    boolean flag = true;
    
    @Override
    public void run() {               //计划买票
        while (flag) {
            try {
                buy();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    private synchronized void buy() {  //synchronized 同步方法,锁的是调用当前方法的对象
        if (ticketNums <= 0) {         //判断是否有票
            flag = false;
            return;
        }
        try {                          //延迟
            Thread.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "拿到" + ticketNums--);        //买票
    }
}

同步块

synchronized 实现跨对象的同步:首先需要选择一个合适的对象作为锁对象。这个对象通常是 被保护的共享资源 所在的对象,或者是与多个对象相关的对象。然后,在同步块的代码中使用这个对象作为锁对象。最后,需要被安全访问的对象以需要的方式放在同步块中。

  • 共享资源指多个线程同时对同一份资源进行访问(读写操作)的数据或对象
  • 锁对象又叫同步监视器

语法格式

public class [类名] {  
    private Object [锁对象] = new Object();  
    private Object [保护对象1] = new Object();  
    private Object [保护对象2] = new Object();  
      
    public void [方法名]() {  
        synchronized([锁对象]) {  
            // 同步块,访问 [保护对象1] 和 [保护对象2]  
            // ...  
        }  
    }  
}

当一个线程进入 [方法名]() 方法时,它将获取 [锁对象] 的锁,并执行同步块 synchronized(锁对象) { ... } 中的代码,以安全地访问[保护对象1][保护对象2]对象。其他线程将被阻塞,直到当前线程退出同步块并释放锁。

示例

public class MyClass {  
    private Object lock = new Object();  
    private Object obj1 = new Object();  
    private Object obj2 = new Object();  
      
    public void accessObjects() {  
        synchronized(lock) {  
            System.out.println("访问 obj1");  // 访问 obj1   
            System.out.println("访问 obj2");  // 访问 obj2  
        }  
    }  
      
    public static void main(String[] args) {  
        MyClass myClass = new MyClass();  
           
        Thread thread1 = new Thread(() -> {   // 创建两个线程并启动它们 
            myClass.accessObjects();  
        });  
          
        Thread thread2 = new Thread(() -> {  
            myClass.accessObjects();  
        }); 
        thread1.start();  
        thread2.start();  
    }  
}

同步监视器的执行过程:

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

测试Java并发(JUC)库中的安全类型的集合

public class ThreadJuc {
    public static void main(String[] args) {
        CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<String>();
        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());
    }
}

分析

Java并发包java.util.concurrent中的一个类CopyOnWriteArrayList的使用

  1. 泛型 <String>
  • CopyOnWriteArrayList<String>:这里,CopyOnWriteArrayList是一个泛型类,专门用来存储String类型的元素。泛型是Java的一个功能,它允许为类、接口或方法定义一个类型参数,使得代码更加灵活和可重用。
  1. CopyOnWriteArrayList 简介
  • CopyOnWriteArrayList是线程安全的,它是java.util.ArrayList的一个线程安全变种。当列表的内容被修改(例如,通过add, set, remove等操作)时,它会创建列表的一个新副本,而不是在原有的列表上进行修改。这样可以确保在迭代列表时不会抛出ConcurrentModificationException异常。
  • 当你只读取列表而不修改它时,CopyOnWriteArrayList不会进行任何复制操作,因此读取操作是非常高效的。但是,由于每次修改都会导致复制整个列表,所以写操作可能会相对较慢。
  1. 为什么使用 CopyOnWriteArrayList
  • 在多线程环境中,当你有一个线程在遍历列表的同时,另一个线程试图修改列表(例如,添加或删除元素),这可能会导致并发问题。CopyOnWriteArrayList就是为了解决这个问题而设计的。
  • 它特别适用于读操作远多于写操作的场景,因为在这种情况下,读操作的性能不会受到写操作的影响。
  1. 示例用途
  • 假设你有一个Web应用程序,它需要处理大量用户的请求。在处理每个请求时,你可能需要从某个列表中获取一些数据。由于请求处理是并发的,使用CopyOnWriteArrayList可以确保数据的线程安全性。即使有多个线程同时访问和修改这个列表,你的代码也不需要额外的同步开销。

总的来说,CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<String>();这行代码创建了一个线程安全的字符串列表,可以在多线程环境中安全地使用。

死锁

多个线程各自占有一些共享资源,并且互相等待其他线程释放各自占有的资源才能运行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形。某一个同步块同时拥有“两个以上对象的锁”时,就可能会发生“死锁”的问题。

理解

  1. 张三占用着A资源,等着B资源;李四占用着B资源,等着A资源
  2. 张三、李四都只有同时完成AB资源才能完成任务,但是两个人在对方放手之前都不会先放手
  3. 于是,进入了死锁

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

  • 互斥条件: 一个资源每次只能被一个进程使用
  • 请求与保持条件: 一个进程因请求资源而阻塞时,对已获得的资源保持不放
  • 不剥夺条件: 进程已获得的资源,在末使用完之前,不能强行剥夺
  • 循环等待条件: 若干进程之间形成一种头尾相接的循环等待资源关系
    只要破除任意一个就能避免死锁

示例

//解决:一个锁只锁一个对象
public  class DeadLock {
  public static void main(String[] args) {
    Makeup makeup = new Makeup(0, "灰姑娘");
    Makeup makeup1 = new Makeup(1, "白雪公主");
    makeup.start();
    makeup1.start();
  }
}

class Lipstick { }  //口红
class Mirror { }    //镜子

class Makeup extends Thread {
  static Lipstick lipstick = new Lipstick();  //需要的资源只有一份,用static保证只有一份
  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 + "获得口红的锁");
        }
      }
    }
  }
}

分析
关键点在于,两个线程试图以不同的顺序获取相同的锁。这就导致了经典的死锁场景:

  1. “灰姑娘”线程可能首先获得口红的锁。
  2. 同时,“白雪公主”线程可能首先获得镜子的锁。
  3. 当“灰姑娘”线程试图获取镜子的锁时,它必须等待,因为“白雪公主”已经持有这个锁。
  4. 同样,当“白雪公主”线程试图获取口红的锁时,它也必须等待,因为“灰姑娘”已经持有这个锁。

这样,两个线程就会永远地等待对方释放锁,从而导致了死锁。

解决
要避免这种死锁,有几种常见的策略:

  1. 锁顺序:总是以相同的顺序请求锁。(最简单)
  2. 锁超时:设置获取锁的超时时间。如果线程在指定的时间内无法获取锁,就放弃并稍后重试。
  3. 死锁检测:使用专门的算法或工具来检测并恢复死锁。但这种方法通常比较复杂,且可能导致性能问题。
  4. 资源分级:给资源分配不同的优先级,并规定只有持有较低优先级锁的线程才能请求较高优先级的锁。这可以避免循环等待,从而防止死锁。

Lock 锁

通过显示定义同步锁对象(Lock)来实现同步,同步锁使用Lock对象充当

java.util.concurrent.locks.Lock接是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象

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

语法格式

主函数 { 根据[类名A]创建线程,并启动 }

class [类名A] implements Runnable {
    ReentrantLock [锁名] = new ReentrantLock();  
    [锁名].lock();    // 获取锁  
    try {  
           // 执行需要同步的代码块  
    } finally {  
        lock.unlock();   // 释放锁  
    }
    

示例

//测试Lock锁
import java.util.concurrent.locks.ReentrantLock;  

public class TestLock {
  public static void main(String[] args) {
    TestLock2 testLock = new TestLock2();  //创建实现Runnable接口的类的对象,意味着可以被线程执行
    new Thread(testLock).start();
    new Thread(testLock).start();
    new Thread(testLock).start();
  }
}

class TestLock2 implements Runnable {
  int tickerNums = 10;
//定义Lock锁,定义了一个ReentrantLock类型的私有、不可变的变量lock,并立即初始化它。这是一个互斥体,用于确保在任何时刻只有一个线程可以执行某个代码块。
  private final ReentrantLock lock = new ReentrantLock();

  @Override
  public void run() {
    while (true) {      //加锁
      try {
        lock.lock();   //尝试获取锁。如果锁当前被另一个线程持有,则当前线程将被禁用,直到锁被释放
        if (tickerNums <= 0) {
          break;
        }
        try {
          Thread.sleep(1000);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
        System.out.println(tickerNums--);
      } catch (Exception e) {
        e.printStackTrace();
      } finally {  //解锁.无论前面的代码是否成功或抛出异常,`finally`块都会被执行。在这里,它确保锁总是被释放,从而防止死锁
        lock.unlock();
      }
    }
  }
}

区别

  1. 显式与隐式:Lock锁是显式锁,需要手动获取和释放;而synchronized关键字是隐式锁,无需手动获取和释放,可以自动管理。
  2. 适用场景:Lock锁只有代码块锁;而synchronized关键字有代码块锁和方法锁
  3. 属性:Lock锁是Java并发包java.util.concurrent.locks中提供的接口,而synchronized关键字是Java语言的一部分
  4. 功能:Lock锁提供了更灵活的锁控制,例如可以中断等待锁的线程、尝试获取锁等,更适合用在复杂的同步场景。而synchronized关键字则比较简单,一旦一个线程获得了锁,其他线程就必须等待
  5. 性能:在重度竞争情况下,Lock锁可能具有更好的性能。

优先使用顺序: Lock锁 > 同步代码块 > 同步方法

线程协作

线程通信——生产者-消费者模式

应用场景:生产者-消费者模式 (生产者 → 数据缓存区 → 消费者)

  • 生产者与消费者共享一个资源,同时生产者与消费者相互依赖互为条件
  • 如果仓库中没有产品,则生产者将产品放入仓库,否则停止生产并等待,直到仓库中的产品被消费者取走为止
  • 如果仓库中放有产品,则消费者将产品取出消费,否则停止消费并等待,直到仓库中再次放入产品为止

分析
这是一个线程同步问题,生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件
对于生产者,在生产开始之前,要通知消费者等待,而生产了产品之后,又需要马上通知消费者取出
对于消费者,在消费完毕之后,要通知生产者消费已结束,需要生产新的产品以供消费

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

达到通信效果主要方法

方法名作用
wait()先释放锁,并开始等待,直到其他线程通知
wait(n)释放锁并等待 n 毫秒
notify()唤醒一个处于等待状态的线程
notifyAll()唤醒同一个对象上所有调用了wait()方法的线程,优先级高的优先调度

唤醒:将线程从等待状态变为可运行状态
这都是Object类的方法,只能在同步方法或者同步代码块中使用,否则会抛出异常IllegalMonitorStateException

管程法

设置一个缓冲区,用于暂存数据

管程法示例

// 测试:生产者消费者模型-->利用缓冲区解决:管程法

import java.util.LinkedList;
import java.util.Queue;

public class TestPC {
    public static void main(String[] args) {
        SynContainer container = new  SynContainer();  //创建一个SynContainer对象,作为共享缓冲区
        //双线程
        new Producer(container).start();  //创建生产者线程,并启动。生产者将使用提供的container对象
        new Consumer(container).start();  //创建消费者线程,并启动。消费者也将使用提供的container对象
    }
}

//定义生产者线程类。生产者线程将创建产品并将其放入缓冲区
class Producer extends Thread{
    SynContainer container;  //声明一个 SynContainer 类的变量 container

    public Producer(SynContainer container) {  //构造器的声明,创建Producer对象
        this.container = container;  //将传入的参数container 赋值给当前对象的成员变量container
    }

    @Override
    public void run() {  //生产者线程的执行主体。其将创建100个产品,并将其放入缓冲区
        for (int i = 0; i < 100; i++) {
            System.out.println("生产了第" + i + "件产品");
            container.push(new Products(i));  //利用push方法将新创建的产品放入缓冲区
        }
    }
}

//定义消费者线程类。消费者线程将从缓冲区取出产品
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 + "件产品");  //打印从缓冲区中取出的产品id
        }
    }
}

class Products{  //定义产品类,包含一个整数id
    int id;

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

class SynContainer{  //定义同步缓冲区类,生产者和消费者通过这个类来进行交互
//初始化一个空的队列来存储产品:创建一个 LinkedList 对象,并将其引用赋值给一个类型为 Queue<Products> 的变量 queue
    Queue<Products> queue = new LinkedList<Products>();
    int count = 0;  //初始化一个计数器,用于跟踪队列中产品的数量
    int size = 10;  //设置队列的最大容量为10

//定义同步方法,允许一个线程在修改队列时独占访问权。如果队列已满,生产者线程将被阻塞,直到有空间可用
    public synchronized void push(Products product)  //生产者放入产品
    {
        if(count == size)
        //容器满,等待消费者消费
        {                
            try {
                this.wait();  //生产者线程调用wait()方法释放锁并进入等待状态
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //容器未满,需要存入成品,并唤醒消费者
        count++;
        queue.offer(product);
        this.notifyAll();
    }

//定义同步方法,允许一个线程在修改队列时独占访问权。如果队列为空,消费者线程将被阻塞,直到有产品可用
    public synchronized Products pop()
    {
        if (count == 0)
        //容器为空,消费者线程调用wait()方法并进入等待状态,等待生产者生产
        {  
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //容器有产品可用,消费者线程被唤醒以取出物品,并通知等待的生产者线程可以生产了
        count--;
        Products productPoll = queue.poll();  
        this.notifyAll();
        return productPoll;
    }
}

分析

P57
Queue<Products> queue = new LinkedList<Products>(); 创建了一个新的 LinkedList 对象,并将其引用赋值给一个类型为 Queue 的变量 queue。

这里是对代码的逐部分解释:

  1. 泛型 <Products>
    Queue<Products> 表示这是一个队列,该队列中的元素类型是 Products。泛型允许为类、接口或方法指定一个类型参数,这样可以确保添加到队列中的对象是 Products 类型或其子类型。

  2. 变量声明
    Queue<Products> queue:声明了一个名为 queue 的变量,其类型为 Queue。这意味着这个变量可以存储一个队列,其中每个元素都是 Products 类型。

  3. 实例化
    new LinkedList<Products>():这部分代码创建了一个新的 LinkedList 对象,该对象是 Queue 接口的一个实现。LinkedList 是一个双向链表,通常用作队列的实现。

  4. 赋值
    通过使用 = 操作符,新创建的 LinkedList 对象被赋值给 queue 变量。

之后,可以使用 queue 变量来执行队列操作,例如添加元素(使用 add 或 offer 方法)、移除元素(使用 remove 或 poll 方法)等。由于这是一个队列,元素将按照它们被添加的顺序(先进先出,FIFO)被移除。

P75~P77
count++;queue.offer(product); this.notifyAll(); 对这段代码的逐句解释:

  1. count++:将 count 变量的值增加1。 count 变量通常用于跟踪队列中当前有多少个元素
  2. queue.offer(product):将一个 product 对象添加到队列的末尾。这里使用的是 offer 方法,offer方法尝试将元素添加到队列中,如果队列已满,它会立即返回一个特殊的标志值,而不是抛出异常
  3. this.notifyAll():是线程同步的关键部分。它调用 Object 类的 notifyAll 方法,该方法将唤醒所有正在等待该对象的线程。在这种情况下,这意味着如果有任何生产者线程或消费者线程正在等待(因为它们在调用 wait() 方法后被阻塞),它们现在将被唤醒并继续执行

这段代码的作用是:当一个生产者线程成功地向队列中添加了一个产品时,它会通知所有等待的线程(无论是其他生产者还是消费者),以便它们可以继续执行。这确保了生产者和消费者线程之间的正确同步,防止了可能的竞态条件。

P93~P96
count--; Products productPoll = queue.poll(); this.notifyAll(); return productPoll;对这段代码的逐句解释:

  1. count--;:将count的值减少1。count 可能是一个计数器,用于跟踪缓冲区中当前有多少个产品
  2. Products productPoll = queue.poll();:从队列中取出一个产品并赋值给 productPoll 变量。如果队列为空, poll() 方法将返回 null
  3. this.notifyAll();:唤醒在此对象监视器上等待的所有线程(如果有)。这通常是为了通知任何等待的生产者或消费者线程,产品已经被取出或消费,他们现在可以继续执行。poll() 是 Java 并发库中 BlockingQueue 接口的一个方法。它尝试从队列中移除并返回 头部的元素。如果队列为空,它会立即返回 null。这个方法是非阻塞的,也就是说,如果队列为空,它不会等待元素可用,而是立即返回。
  4. return productPoll;:这行代码返回取出的产品。在消费者线程中,这会结束方法调用并返回给调用者

这段代码的作用是:确保在多线程环境中对共享资源(如缓冲区)的访问是安全的。通过使用 synchronized 关键字和方法内的 wait() 和 notifyAll() 方法,可以确保生产者和消费者线程不会相互干扰,从而避免产生竞态条件。

信号灯法

设置一个标记位(类似于容量为1的管程法)

信号灯法示例

// 测试:生产者消费者模型-->通过标志位解决:信号灯法

public class TestPC_01 {
    public static void main(String[] args) {
        TV tv = new TV();  //创建一个 TV 对象,是生产者和消费者的共享资源
//创建线程并启动,将 tv 作为参数传递给 Player\Watcher 构造函数。tv 变量被初始化或存储为线程的一个属性
        new Player(tv).start();  
        new Watcher(tv).start();
    }
}

//生产者-->演员
class Player extends Thread {  //定义一个的类 Player ,该类继承自 Thread 类,表示这是一个线程
    TV tv;          //定义类型为 TV 的成员变量 tv

    public Player(TV tv) {
        this.tv = tv;  //定义一个构造函数,接受一个类型为 TV 的参数,并将其赋值给成员变量
    }

    @Override
    public void run() {  //定义线程的执行逻辑
//循环播放两个节目:当i为偶数时播放"快乐大本营播放中",当i为奇数时播放"抖音:记录美好生活"。循环总共执行20次
        for (int i = 0; i < 20; i++) {
            if (i % 2 == 0) {
                this.tv.play("快乐大本营播放中");
            } else {
                this.tv.play("抖音:记录美好生活");
            }
        }
    }
}

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

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

    @Override
    public void run() {
//这个循环会执行20次。每次循环都会调用 tv.watch() 方法。这意味着这个线程会反复地观看某个节目,至少观看20次
        for (int i = 0; i < 20; i++) {
            tv.watch();  
        }
    }
}

//产品-->节目
class TV {
    //演员表演,观众等待 true
    //观众观看,演员等待 fault
    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();  //唤醒等待的线程
//将传入的 voice 参数值赋给类的成员变量 this.voice。这意味着当前线程更新了 TV 对象中的节目内容
        this.voice = voice;
        this.flag = !this.flag;  //将 flag 的值取反,实现生产者和消费者之间的交替执行
    }
    //表示观众观看节目。该方法内部也使用了标志位来控制线程的执行顺序
    public synchronized void watch() {
        if (flag) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("观看了:" + voice);
        //通知演员表演
        this.notifyAll();
        this.flag = !this.flag;
    }
}

使用线程池

背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大
思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中,可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具
好处:
提高响应速度(减少了创建新线程的时间)
降低资源消耗(重复利用线程池中线程,不需要每次都创建)
便于线程管理
线程池参数
corePoolSize:核心池的大小
maximumPoolSize: 最大线程数
keepAliveTime: 线程没有任务时最多保持多长时间后会终止

JDK 5.0起提供了线程池相关API:ExecutorService 和Executors
ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor

  • void execute(Runnable command):执行任务/命令,没有返回值,一般用来执行Runnable
  • <T>Future<T> submit(Callable<T> task):执行任务,有返回值,一般又来执行 Callable
  • void shutdown():关闭连接池

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

语法格式

import java.util.concurrent.ExecutorService;  
import java.util.concurrent.Executors;  
  
public class [线程池类名] {  
    public static void main(String[] args) {  
        // 使用Executors.newFixedThreadPool()方法创建一个固定大小为 n 的线程池  
        //因为线程池大小是 n ,所以同时最多只能有 n 个任务在执行,其余任务会在队列中等待
        ExecutorService [线程池对象名] = Executors.newFixedThreadPool(n);  

        //每次循环都会创建一个新的 [任务类名] 任务并提交给 [线程池对象名] 
        //因此,如果这个循环执行了 m 次,就会创建 m 个任务
        for () {
            Runnable [任务对象名] = new [任务类名]();  // 括号()中是传递给 [任务类名] 构造函数的参数
            [线程池对象名].execute([任务对象名]);  // 使用execute方法提交 任务[任务对象名] 给线程池
        }  

        [线程池对象名].shutdown(); // 关闭线程池,不再接受新的任务,但还是会等待已提交的任务完成后再关闭
  
        //这个循环会阻塞主线程,当所有提交给线程池的任务都执行完毕isTerminated 方法会返回 true 
        while (![线程池对象名].isTerminated()) {  
        }  
        System.out.println("所有线程执行完毕");  //当所有任务都执行完毕后,打印一条消息到控制台
    }  
}  
  
class [任务类名] implements Runnable {  
    ...                  //其他代码
    @Override
    public void run() {  //实现 Runnable 接口的 run 方法,这是线程执行时的入口点
    ...                  //run方法线程体,包含线程执行的代码 
    }
    
<---------------------------------------有时会用到------------------------------------------>    
    // 模拟耗时操作,使线程睡眠 n 毫秒,以确保所有任务都完成
    private void processCommand() {  
        try {  
            Thread.sleep(n); 
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
    }  
    //作用:
    /**1.在多线程编程中,线程间的同步和协作是很重要的。通过模拟耗时操作,开发者可以更好地理解线程如何等待、
    *    如何被唤醒以及如何进行同步。
    *  2.在某些情况下,可能希望通过模拟耗时操作来控制并发的程度。例如,不希望所有线程都同时完成,
    *    而是希望它们按照一定的顺序一个接一个地完成。
    */ 
}

分析
P8
ExecutorService 是 Java 中用于执行多线程任务的框架之一,可以创建一个线程池,将多个任务提交到线程池中执行。

P8
Executors.newFixedThreadPool() 是Java中 java.util.concurrent.Executors 类的一个静态方法,用于创建一个固定大小的线程池。线程池是用于管理线程的一种机制,它能够控制同时运行的线程数量,并提供了线程的复用,从而避免了为每个任务都创建新线程的开销。

P17
shutdown() 方法是 ExecutorService 接口的一部分,该接口是 java.util.concurrent 包的一部分。当你创建一个 ExecutorService 实例时(例如通过 Executors 工厂类),你可以调用其 shutdown() 方法来关闭执行器服务。这会停止接受新的任务,但会等待已提交的任务完成后再关闭。

P20
isTerminated() 是  ExecutorService  接口中的一个方法。这个方法返回一个布尔值,表示  ExecutorService  是否已经终止。如果 ExecutorService 已经完成执行所有提交的任务并且所有线程都已终止,那么 isTerminated() 将返回 true 。如果 ExecutorService 还在执行任务或仍然有活动的线程,那么 isTerminated() 将返回 false 。

示例

public class ThreadPool {  
  
    public static void main(String[] args) {  
  
        // 这里创建了一个固定大小为10的线程池,并将其引用赋给变量service(注意:通常变量名应使用小写开头,这里service不符合Java的命名规范)。  
        // 所有提交给这个线程池的任务将由这10个线程来处理,这意味着即使你提交了超过10个任务,也只有10个线程会同时运行,  
        // 其他任务会在队列中等待,直到有线程空闲出来。  
        // newFixedThreadPool 参数为:线程池大小  
        ExecutorService service = Executors.newFixedThreadPool(10);  
          
        // 通过调用service的execute方法,向线程池提交了四个任务。每个任务都是一个myThread类的实例
        // execute 方法是 Java 的 ExecutorService 接口中的一个方法,用于将任务添加到线程池的工作队列中,并由一个线程来执行
        // 当任务被提交时,线程池中的一个线程会被用来执行该任务。如果所有线程都在忙,任务会进入队列等待
        service.execute(new myThread());  
        service.execute(new myThread());  
        service.execute(new myThread());  
        service.execute(new myThread());  

        service.shutdown();  // 关闭线程池,不再接受新的任务;当所有已提交的任务都执行完毕,线程池中的所有线程将会终止
    }  
}  
  
class myThread implements Runnable {  // 实现了 Runnable 接口的类 myThread  
  
    @Override  
    public void run() {  // 当线程池中的一个线程开始执行myThread实例时,它会调用这个run方法
        System.out.println(Thread.currentThread().getName());  // 获取并打印当前正在执行的线程的名称  
    }  
}