Java多线程详解(通俗易懂)

时间:2022-12-16 12:08:36

一、线程简介

1. 什么是进程?

电脑中会有很多单独运行的程序,每个程序有一个独立的进程,而进程之间是相互独立存在的。例如图中的微信、酷狗音乐、电脑管家等等。
Java多线程详解(通俗易懂)

2. 什么是线程?

进程想要执行任务就需要依赖线程。换句话说,就是进程中的最小执行单位就是线程,并且一个进程中至少有一个线程。

那什么又是多线程呢?

提到多线程这里要说两个概念,就是串行和并行,搞清楚这个,我们才能更好地理解多线程。

  • 串行,其实是相对于单条线程来执行多个任务来说的,我们就拿下载文件来举个例子:当我们下载多个文件时,在串行中它是按照一定的顺序去进行下载的,也就是说,必须等下载完A之后才能开始下载B,它们在时间上是不可能发生重叠的。
    Java多线程详解(通俗易懂)

  • 并行:下载多个文件,开启多条线程,多个文件同时进行下载,这里是严格意义上的,在同一时刻发生的,并行在时间上是重叠的。
    Java多线程详解(通俗易懂)

了解完这两个概念之后,我们再来说什么是多线程,举个例子,比如我们打开联想电脑管家,电脑管家本身是一个程序,也可以说就是一个进程,它里面包括很多功能,电脑加速、安全防护、空间清理等等功能,如果对于单线程来说,无论我们想要电脑加速,还是空间清理,那么必须得一件事一件事的做,做完其中一件事再做下一件事,有一个执行顺序。但如果是多线程的话,我们可以在清理垃圾的同时进行电脑加速,还可以病毒查杀等等其他操作,这个就是在严格意义上的同一时刻发生的,没有先后顺序。

二、线程的创建

Java 提供了三种创建线程的方法:

  • 通过继承 Thread 类本身。(重点)
  • 通过实现 Runnable 接口。(重点)
  • 通过 Callable 和 Future 创建线程。(了解)

1.继承Thread类

  • 自定义线程类继承Thread类
  • 重写run()方法,编写线程执行体
  • 创建线程对象,调用start()方法启动线程
/**
 * @ClassName ThreadDemo
 * @Description TODO 线程创建的第一种方式:继承Thread类
 * @Author ZhangHao
 * @Date 2022/12/10 11:45
 * @Version: 1.0
 */
public class ThreadDemo extends Thread{
    @Override
    public void run() {
        //新线程入口点
        for (int i = 0; i < 100; i++) {
            System.out.println("我在玩手机:"+i);
        }
    }
    //主线程
    public static void main(String[] args) {
        //创建线程对象
        ThreadDemo demo = new ThreadDemo();
        demo.start();//启动线程
        for (int i = 0; i < 1000; i++) {
            System.out.println("我在吃饭:"+i);
        }
        //主线程和多线程并行交替执行
        //总结:线程开启不一定立即执行,由cpu调度执行
    }
}

写一个小小的案例:使用多线程实现网图下载

  • 需要导入一个commons-io-2.11.0-bin.jar (百度搜索下载,版本不限制)
/**
 * @ClassName ImageDownload
 * @Description TODO 网图下载
 * @Author ZhangHao
 * @Date 2022/12/10 12:55
 * @Version: 1.0
 */
public class ImageDownload extends Thread{
    private String url;//网图下载地址
    private String name;//网图名称
    
    public ImageDownload(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) {
        ImageDownload d1 = new ImageDownload("https://cn.bing.com/images/search?view=detailV2&ccid=64mezA1F&id=0567ED050842B109CEFE6D7C2E235E6513915D00&thid=OIP.64mezA1F6eYavcDWrgjHQgHaEK&mediaurl=https%3a%2f%2fimages.hdqwalls.com%2fwallpapers%2fcute-kitten-4k-im.jpg&exph=2160&expw=3840&q=Cat+Wallpaper+4K&simid=608031326407324483&FORM=IRPRST&ck=5E947A96CD5B48E39B116D48F58466AB&selectedIndex=12&ajaxhist=0&ajaxserp=0", "cat1.jpg");
        ImageDownload d2 = new ImageDownload("https://cn.bing.com/images/search?view=detailV2&ccid=qXtg4Nx0&id=A80C30163A6B55D16D61F27E632239424517705F&thid=OIP.qXtg4Nx0BUoeUP53fz_HKgHaFI&mediaurl=https%3a%2f%2fimages8.alphacoders.com%2f856%2f856433.jpg&exph=2658&expw=3840&q=Cat+Wallpaper+4K&simid=608046255722156270&FORM=IRPRST&ck=986D5F99CF8474477F4A1F2DB2850C9D&selectedIndex=25&ajaxhist=0&ajaxserp=0", "cat2.jpg");
        ImageDownload d3 = new ImageDownload("https://cn.bing.com/images/search?view=detailV2&ccid=kvYsfUHA&id=6311D8D1DC87AA4B69783A97020038B03827134D&thid=OIP.kvYsfUHAAQlEVW3Z3_EEWwHaEK&mediaurl=https%3a%2f%2fwallpapershome.com%2fimages%2fpages%2fpic_h%2f19418.jpg&exph=1080&expw=1920&q=Cat+Wallpaper+4K&simid=608016886736366855&FORM=IRPRST&ck=37C2818B80D19766E7A91B5BB7A060D6&selectedIndex=159&ajaxhist=0&ajaxserp=0", "cat3.jpg");
        d1.start();
        d2.start();
        d3.start();
        //每次执行结果有可能不一样,再次证明线程之间是由cpu调度执行
    }
}
//下载器
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异常,downloader方法出现问题!");
        }
    }
}

2. 实现Runnable接口

  • 定义MyRunnable类实现Runnable接口
  • 实现run()方法,编写线程执行体
  • 创建线程对象,调用start()方法启动线程
/**
 * @ClassName RunnableDemo
 * @Description TODO 线程创建的第二种方式:实现Runnable接口 (推荐使用)
 * @Author ZhangHao
 * @Date 2022/12/10 15:07
 * @Version: 1.0
 */
//模拟抢火车票
public class RunnableDemo implements Runnable{
    //票数
    private int ticketNums = 10;

    @Override
    public void run() {
        while (ticketNums > 0){
            try {
                //让线程睡眠一会
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"-->抢到了第"+ticketNums--+"张票");
        }
    }

    public static void main(String[] args) {
        //创建runnable接口的实现类对象
        RunnableDemo demo = new RunnableDemo();
        //创建线程对象,通过线程对象开启线程(使用的代理模式)
        //Thread thread = new Thread(demo,"老王");
        //thread.start();
        //简写:new Thread(demo).start();
        new Thread(demo,"老王").start();
        new Thread(demo,"小张").start();
        new Thread(demo,"黄牛党").start();

        //发现问题:多个线程操作同一个资源时,线程不安全,数据紊乱。(线程并发)
    }
}

再来一个小小的案例:模拟《龟兔赛跑》首先得有一个赛道,兔子天生跑得快,但是兔子跑一段路就偷懒睡觉,乌龟在不停的跑,最终乌龟取得胜利!

/**
 * @ClassName Race
 * @Description TODO 模拟龟兔赛跑
 * @Author ZhangHao
 * @Date 2022/12/11 9:25
 * @Version: 1.0
 */
public class Race implements Runnable {

    private static String winner;//胜利者

    @Override
    public void run() {
        //设置赛道
        for (int i = 1; i <= 100; i++) {
            //让兔子跑得快一点
            if (Thread.currentThread().getName().equals("兔子")) {
                i += 4;
                System.out.println(Thread.currentThread().getName() + "跑了" + i + "步");
                if (i % 4 == 0) {
                    try {
                        //模拟兔子跑一段路就睡觉
                        Thread.sleep(5);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            } else {
                System.out.println(Thread.currentThread().getName() + "跑了" + i + "步");
            }
            //判断游戏是否结束
            boolean flag = gameOver(i);
            if (flag) {
                break;
            }
        }
    }

    //判断游戏是否结束
    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();
    }
}

3. 实现Callable接口

  • 实现Callable接口,需要返回值类型
  • 重写Call方法,需要抛出异常
  • 创建目标对象
  • 创建执行服务:ExecutorService es = Executors.newFixedThreadPool(1);
  • 提交执行:Future r1 = es.submit(d1);
  • 获取结果:Boolean res1 = r1.get();
  • 关闭服务:es.shutdownNow();
/**
 * @ClassName CallableDemo
 * @Description TODO 线程创建的第三种方式:实现Callable接口(了解即可)
 * @Author ZhangHao
 * @Date 2022/12/11 10:24
 * @Version: 1.0
 */
public class CallableDemo implements Callable<Boolean> {
    private String url;//网图下载地址
    private String name;//网图名称

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

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

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CallableDemo d1 = new  CallableDemo("https://cn.bing.com/images/search?view=detailV2&ccid=64mezA1F&id=0567ED050842B109CEFE6D7C2E235E6513915D00&thid=OIP.64mezA1F6eYavcDWrgjHQgHaEK&mediaurl=https%3a%2f%2fimages.hdqwalls.com%2fwallpapers%2fcute-kitten-4k-im.jpg&exph=2160&expw=3840&q=Cat+Wallpaper+4K&simid=608031326407324483&FORM=IRPRST&ck=5E947A96CD5B48E39B116D48F58466AB&selectedIndex=12&ajaxhist=0&ajaxserp=0", "cat1.jpg");
        CallableDemo d2 = new  CallableDemo("https://cn.bing.com/images/search?view=detailV2&ccid=qXtg4Nx0&id=A80C30163A6B55D16D61F27E632239424517705F&thid=OIP.qXtg4Nx0BUoeUP53fz_HKgHaFI&mediaurl=https%3a%2f%2fimages8.alphacoders.com%2f856%2f856433.jpg&exph=2658&expw=3840&q=Cat+Wallpaper+4K&simid=608046255722156270&FORM=IRPRST&ck=986D5F99CF8474477F4A1F2DB2850C9D&selectedIndex=25&ajaxhist=0&ajaxserp=0", "cat2.jpg");
        CallableDemo d3 = new  CallableDemo("https://cn.bing.com/images/search?view=detailV2&ccid=kvYsfUHA&id=6311D8D1DC87AA4B69783A97020038B03827134D&thid=OIP.kvYsfUHAAQlEVW3Z3_EEWwHaEK&mediaurl=https%3a%2f%2fwallpapershome.com%2fimages%2fpages%2fpic_h%2f19418.jpg&exph=1080&expw=1920&q=Cat+Wallpaper+4K&simid=608016886736366855&FORM=IRPRST&ck=37C2818B80D19766E7A91B5BB7A060D6&selectedIndex=159&ajaxhist=0&ajaxserp=0", "cat3.jpg");
        //创建执行任务
        ExecutorService es = Executors.newFixedThreadPool(3);
        //提交执行
        Future<Boolean> r1 = es.submit(d1);
        Future<Boolean> r2 = es.submit(d2);
        Future<Boolean> r3 = es.submit(d3);
        //获取结果
        Boolean res1 = r1.get();
        Boolean res2 = r2.get();
        Boolean res3 = r3.get();
        System.out.println(res1);//打印结果
        System.out.println(res2);
        System.out.println(res3);
        //关闭服务
        es.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异常,downloader方法出现问题!");
        }
    }
}

小结

  • 继承Thread类
    • 子类继承Thread类具备多线程能力
    • 启动线程:子类对象.start()
    • 不建议使用:避免OOP单继承局限性
  • 实现Runnable接口
    • 实现接口Runnable具有多线程能力
    • 启动线程:传入目标对象+Thread对象.start()
    • 推荐使用:避免单继承局限性,灵活方面,方便同一个对象被多个线程使用

静态代理

代理模式在我们生活中很常见,比如我们购物,可以从生产工厂直接进行购物,但是在生活中往往不是这样,一般都是厂家委托给超市进行销售,而我们不直接跟厂家进行关联,这其中就引用了静态代理的思想,厂家相当于真实角色,超市相当于代理角色,我们则是目标角色。代理角色的作用其实就是,帮助真实角色完成一些事情,在真实角色业务的前提下,还可以增加其他的业务。AOP切面编程就是运用到了这一思想。

写一个小小的案例,通过婚庆公司,来实现静态代理。

/**
 * @ClassName StaticProxy
 * @Description TODO 静态代理(模拟婚庆公司实现)
 * @Author ZhangHao
 * @Date 2022/12/11 11:38
 * @Version: 1.0
 */
public class StaticProxy {
    public static void main(String[] args) {
        Marry marry = new WeddingCompany(new You());
        marry.happyMarry();
        //注意:真实对象和代理对象要实现同一个接口
    }
}
//结婚
interface Marry{
    //定义一个结婚的接口
    void happyMarry();
}
//你(真实角色)
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();
        target.happyMarry();
        after();
    }
    private void before(){
        System.out.println("结婚之前:布置婚礼现场");
    }
    private void after(){
        System.out.println("结婚之后:收尾工作");
    }
}

Thread底层就使用的静态代理模式,源码分析

//Thread类实现了Runnable接口
public class Thread implements Runnable{
  //引入了真实对象
  private Runnable target;
  //代理对象中的构造器
  public Thread(Runnable target, String name) {
         init(null, target, name, 0);
  }
}

当我们开启一个线程,其实就是定义了一个真实角色实现了Runnable接口,重写了run方法。

public void TestRunnable{
    public static void main(String[] args){
      MyThread myThread = new MyThread();
      new Thread(myThread,"张三").start();
      //Thread就是代理角色,myThread就是真实角色,start()就是实现方法
    }
}
class MyThread implements Runnable{
   @Override
   public void run() {
     System.out.println("我是子线程,同时是真实角色");
   }
}

动态代理

前面使用到了静态代理,代理类是自己手工实现的,自己创建了java类表示代理类,同时要代理的目标类也是确定的,如果当目标类增多时,代理类也需要成倍的增加,代理类的数量过多,当接口中的方法改变或者修改时,会影响实现类,厂家类,代理都需要修改,于是乎就有了jdk动态代理。

动态代理的好处:

  • 代理类数量减少
  • 修改接口中的方法不影响代理类
  • 实现解耦合,让业务功能和日志、事务和非事务功能分离

实现步骤:

  1. 创建接口,定义目标类要完成功能。
  2. 创建目标类实现接口。
  3. 创建InvocationHandler接口实现类,在invoke()方法中完成代理类的功能。
  4. 使用Proxy类的静态方法,创建代理对象,并且将返回值转换为接口类型。
/**
 * @ClassName DynamicProxy
 * @Description TODO 动态代理
 * @Author ZhangHao
 * @Date 2022/12/11 15:11
 * @Version: 1.0
 */
public class DynamicProxy {
    public static void main(String[] args) {
        //创建目标对象
        Marry target = new You();

        //创建InvocationHandler对象
        MyInvocationHandler handler = new MyInvocationHandler(target);

        //创建代理对象
        Marry proxy = (Marry)handler.getProxy();
        //通过代理执行方法,会调用handle中的invoke()方法
        proxy.happyMarry();
    }
}
//创建结婚接口
interface Marry{
    void happyMarry();
}
//目标类实现结婚接口
class You implements Marry{
    @Override
    public void happyMarry() {
        System.out.println("张三结婚了!");
    }
}
//创建工具类,即方法增强的功能
class ServiceTools{
    public static void before(){
        System.out.println("结婚之前:布置婚礼现场");
    }
    public static void after(){
        System.out.println("结婚之后:清理结婚现场");
    }
}
//创建InvocationHandler的实现类
class MyInvocationHandler implements InvocationHandler{

    //目标对象
    private Object target;

    public MyInvocationHandler(Object target){
        this.target = target;
    }

    //通过代理对象执行方法时,会调用invoke()方法
    /**
     * @Param [proxy:jdk创建的代理类的实例]
     * @Param [method:目标类中被代理方法]
     * @Param [args:目标类中方法的参数]
     * @return java.lang.Object
    **/
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //增强功能
        ServiceTools.before();
        //执行目标类中的方法
        Object obj = null;
        obj = method.invoke(target,args);
        ServiceTools.after();
        return obj;
    }

    //通过Proxy类创建代理对象(自己手写的嗷)
    /**
     * @Param [ClassLoader loader:类加载器,负责向内存中加载对象的,使用反射获取对象的ClassLoader]
     * @Param [Class<?>[] interfaces: 接口, 目标对象实现的接口,也是反射获取的。]
     * @Param [InvocationHandler h: 我们自己写的,代理类要完成的功能。]
     * @return java.lang.Object
    **/
    public Object getProxy(){
        return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(),this);
    }
}

总结

  1. 代理分为静态代理和动态代理
  2. 静态代理需要手动书写代理类,动态代理通过Proxy.newInstance()方法生成
  3. 不管是静态代理还是动态代理,代理与被代理者都要实现两样接口,本质面向接口编程
  4. 代理模式本质上的目的是为了在不改变原有代码的基础上增强现有代码的功能

三、线程的状态

都知道人有生老病死,线程也不例外。

Java中线程的状态分为 6种,可以在Thread类的State枚举类查看 。

Java多线程详解(通俗易懂)

  1. 新建(NEW):使用new关键字创建一个线程的时候,就进入新建状态。

  2. 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态统的称为“运行”。
    2.1 就绪(ready):线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态。
    2.2 运行中(running):就绪状态的线程在获得CPU时间片后变为运行状态。

  3. 阻塞(BLOCKED):阻塞状态是指线程因为某些原因放弃CPU,暂时停止运行。当线程处于阻塞状态时,Java虚拟机不会给线程分配CPU。直到线程重新进入就绪状态,它才有机会转到运行状态。

    • 阻塞情况又分为三种:
      • 等待阻塞:当线程执行wait()方法时,JVM会把该线程放入等待队列(waitting queue)中。
      • 同步阻塞:当线程在获取synchronized同步锁失败(锁被其它线程所占用)时,JVM会把该线程放入锁池(lock pool)中。
      • 其他阻塞:当线程执行sleep()或join()方法,或者发出了 I/O 请求时,JVM会把该线程置为阻塞状态。 当sleep()状态超时、join()等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入就绪状态。
  4. 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。

  5. 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。

  6. 终止(TERMINATED):当线程的run()方法完成时,或者主线程的main()方法完成时,我们就认为它终止了。线程一旦终止了,就不能复生。

四、线程方法

方法 说明
setPriority(int newPriority) 更改线程的优先级
static void sleep(long millis) 在指定的毫秒内让正在执行的线程进入休眠状态
void join() 让其他线程等待当前线程先终止理解成vip插队
static void yield() 暂停正在执行的线程对象,并执行其他的线程理解为礼让
void interrupt() 中断线程,别使用这个方法
boolean isAlive() 测试线程是否处于活动状态

1. 停止线程

  • 不推荐使用JDK提供的stop()、destroy()方法。【过期】
  • 建议让线程自己停下来
  • 建议使用一个标志位进行终止变量
/**
 * @ClassName TestStop
 * @Description TODO 测试停止线程
 * @Author ZhangHao
 * @Date 2022/12/12 20:39
 * @Version: 1.0
 */
public class TestStop implements Runnable {
    //设置一个标志位
    private boolean flag = true;

    @Override
    public void run() {
        int i = 0;
        while (flag) {
            System.out.println("子线程" + i++);
        }
    }
    //设置公开的方法,转换标志位
    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("主线程:" + i);
            if (i == 700) {
                testStop.stop();
                System.out.println("线程停止了!");
            }
        }
        //主线程和子线程并行交替执行,当主线程i=700时,子线程停止执行,主线程继续执行直到执行完成。
    }
}

2. 线程休眠

  • sleep(long millis):以毫秒为单位休眠
  • sleep()到达指定时间后就会进入就绪状态
  • sleep()可以模拟网络延时,计时器等等
  • sleep()存在异常InterruptedException
  • 每个对象都有一把锁,sleep不会释放锁
/**
 * @ClassName TestSleep
 * @Description TODO 线程休眠
 * @Author ZhangHao
 * @Date 2022/12/12 21:31
 * @Version: 1.0
 */
public class TestSleep implements Runnable{
    //票数
    private int ticketNums = 10;

    @Override
    public void run() {
        while (ticketNums > 0){
            try {
                Thread.sleep(10);//模拟网络延时,放大问题的发生性
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"-->抢到了第"+ticketNums--+"张票");
        }
    }

    public static void main(String[] args) {
        RunnableDemo demo = new RunnableDemo();
        new Thread(demo,"老王").start();
        new Thread(demo,"小张").start();
        new Thread(demo,"黄牛党").start();
    }
}

写一个小小的案例:使用sleep()完成倒计时和时间播报的功能

/**
 * @ClassName TestSleep2
 * @Description TODO 倒计时,时间播报
 * @Author ZhangHao
 * @Date 2022/12/12 21:39
 * @Version: 1.0
 */
public class TestSleep2 {
    public static void main(String[] args) {
        //tenDown();
        printNowDate();
    }

    //倒计时
    public static void tenDown(){
        int i = 10;
        while (true) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(i--);
            if (i <= 0) {
                break;
            }
        }
    }

    //时间播报
    public static void printNowDate(){
        //获取当前时间
        Date date = new Date(System.currentTimeMillis());
        while (true){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            String format = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(date);
            //更新当前时间
            date = new Date(System.currentTimeMillis());
            System.out.println(format);
        }
    }
}

3. 线程礼让

  • 让正在执行的线程停止,从运行状态转换为就绪状态,重写竞争时间片。
  • 礼让不一定成功,由CPU重新调度,看CPU心情!
/**
 * @ClassName TestYield
 * @Description TODO 线程礼让
 * @Author ZhangHao
 * @Date 2022/12/13 12:46
 * @Version: 1.0
 */
public class TestYield {
    public static void main(String[] args) {
        MyYield myYield = new MyYield();
        new Thread(myYield,"A").start();
        new Thread(myYield,"B").start();
        
        //通俗的讲,线程礼让其实就是A、B线程处于就绪状态等待被cpu调度执行,
        //当其中有一个线程被cpu调度执行了,则当前这个线程再退回就绪状态重新和另外一个线程竞争
    }
}
class MyYield implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"线程开始!");
        Thread.yield();//礼让
        System.out.println(Thread.currentThread().getName()+"线程停止!");
    }
}

4. 线程插队

  • 非常霸道的一个方法,相当于其他线程正在执行,相当于一个vip线程直接插队执行完,其他线程阻塞,再执行其他的线程。
/**
 * @ClassName TestJoin
 * @Description TODO 线程插队
 * @Author ZhangHao
 * @Date 2022/12/13 13:03
 * @Version: 1.0
 */
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) {
        TestJoin testJoin = new TestJoin();
        Thread thread = new Thread(testJoin);

        for (int i = 0; i < 500; i++) {
            if (i == 200) {
                try {
                    thread.start();
                    thread.join();//插队执行
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("主线程" + i);
        }
    }
}

5. 线程状态

/**
 * @ClassName TestState
 * @Description TODO 线程状态
 * @Author ZhangHao
 * @Date 2022/12/13 13:22
 * @Version: 1.0
 */
public class TestState {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                try {
                    Thread.sleep(200);//线程睡眠
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("线程体执行完毕~");
        });

        Thread.State state = thread.getState();//观察线程状态
        System.out.println(state);//NEW:没有调用start()方法之前都是new

        thread.start();
        state = thread.getState();
        System.out.println(state);//RUNNABLE:进入运行状态

        //只要线程还没有死亡,就打印线程状态
        while (state != Thread.State.TERMINATED){
            try {
                Thread.sleep(100);//100毫秒打印一次状态
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            state = thread.getState();//更新状态
            System.out.println(state);
        }
    }
    /*
        NEW
        RUNNABLE
        TIMED_WAITING
        TIMED_WAITING
        TIMED_WAITING
        TIMED_WAITING
        TIMED_WAITING
        TIMED_WAITING
        TIMED_WAITING
        TIMED_WAITING
        TIMED_WAITING
        线程体执行完毕~
        TERMINATED
     */
}

6. 线程的优先级

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

  • 线程的优先级用数字表示,范围从1~10,不在这个范围内的都会报出异常.

    • Thread.MIN_PRIORITY = 1;
    • Thread.MAX_PRIORITY = 10;
    • Thread.NORM_PRIORITY = 5;
  • 使用以下方式改变和获取优先级

    • setPriority(int xxx)
    • getPriority()
  • 优先级的设定建议在start()之前

/**
 * @ClassName TestPriority
 * @Description TODO 线程的优先级
 * @Author ZhangHao
 * @Date 2022/12/13 13:46
 * @Version: 1.0
 */
public class TestPriority {
    public static void main(String[] args) {
        //主线程默认优先级:5
        System.out.println(Thread.currentThread().getName()+"---->"+Thread.currentThread().getPriority());

        MyPriority myPriority = new MyPriority();
        Thread t1 = new Thread(myPriority);
        t1.start();//默认优先级是5

        Thread t2 = new Thread(myPriority);
        t2.setPriority(3);
        t2.start();

        Thread t3 = new Thread(myPriority);
        t3.setPriority(8);
        t3.start();

        Thread t4 = new Thread(myPriority);
        t4.setPriority(Thread.MAX_PRIORITY);//最大优先级10
        t4.start();

        Thread t5 = new Thread(myPriority);
        //t5.setPriority(-1);//Exception in thread "main" java.lang.IllegalArgumentException
        //t5.start();

        //优先级越大代表被调度的可能性越高,优先级低不代表不会被调度,还是看CPU心情
    }
}
class MyPriority implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"---->"+Thread.currentThread().getPriority());
    }
}

7. 守护线程

  • 线程分为用户线程守护线程
  • 虚拟机必须确保用户线程执行完毕,如main()
  • 虚拟机不用等待守护线程执行完毕,如gc()
  • 如:后台记录操作日志,监控内存,垃圾回收等等
/**
 * @ClassName TestDaemon
 * @Description TODO 守护线程
 * @Author ZhangHao
 * @Date 2022/12/13 14:09
 * @Version: 1.0
 */
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!------");
    }
}

五、线程同步

并发

同一个对象被多个线程同时操作,并发编程又叫多线程编程。生活中的例子很常见,比如过年了学生都需要在手机上抢火车票,几万个人同时去抢那10张票,最终只有10个幸运儿抢到,手速慢的学生是不是就没有抢到呀。

  • 并发针对单核CPU处理器,它是多个线程被一个CPU轮流非常快速切换执行的,逻辑上是同步运行。

并行

同一时刻多个任务(进程or线程)同时执行,真正意义上做到了同时执行,但是这种情况往往只体现在多核CPU,单核CPU是做不到同时执行多个任务的,多核CPU内部集成了多个计算机核心(Core),每个核心相当于一个简单的CPU,多核CPU中的每个核心都可以独立执行一个任务,并且多个核心之间互不干扰,在不同核心上执行的多个任务,称为并行。

  • 并行针对多核CPU处理器,它是在不同核心执行的多个任务完成的,物理上是同步执行。

串行

多个任务按顺序依次执行,就比如小学在学校上厕所,小学的学校一般都是公共的厕所,而且是固定的坑位,大家按照提前排好的次序依次进行上厕所,也就是多个任务之间一个一个按顺序的执行。

线程同步

现实生活中,我们会遇到“同一个资源,多个人都想使用”的问题,比如,食堂排队打饭,每个人都想快速吃到饭,然后几万个学生就一拥而上,全部挤在打饭的窗口,最后饭不仅没吃到,还挨了一顿打,这也就是并发问题引起的,所以我们需要一种解决方案,最天然的解决办法就是,排队一个个来。排队在编程中叫:队列。这种解决办法就叫线程同步。

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

由于同一进程的多个线程共享同一块存储空间,会出现冲突问题,所以为了保证安全性,还加入了机制。synchronized关键字,当一个线程获得对象之后需要上锁,独占着资源,其他线程必须等待,等当前线程使用完释放锁即可。解决了线程安全的问题,同样也带来了一些问题:

  • 一个线程拿到锁之后,其他需要这把锁的线程挂起。

  • 在多线程竞争下,加锁和释放锁会导致频繁上下切换带来调度延迟和性能问题。

  • 如果优先级高的线程等待优先级低的线程释放锁,会导致优先级倒置,引发性能问题。

要想保证安全,就一定会失去性能,要想保证性能,就一定会失去安全。鱼和熊掌不可兼得的道理。

线程同步:队列 + 锁

用三个小小的案例演示并发引起的问题:

/**
 * @ClassName Ticket
 * @Description TODO 模拟买票
 * @Author ZhangHao
 * @Date 2022/12/14 10:40
 * @Version: 1.0
 */
public class Ticket {
    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 ticketNum = 10;//票数
    private boolean flag = true;//设置标志位

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

    //买票
    private void buy() throws InterruptedException {
        if(ticketNum<=0){
            flag = false;
            return;
        }
        Thread.sleep(100);//模拟延时,放大问题的发生性
        System.out.println(Thread.currentThread().getName()+"抢到了第"+ticketNum--+"张票");
    }
}
李四抢到了第10张票
张三抢到了第10张票
王五抢到了第9张票
王五抢到了第8张票
张三抢到了第8张票
李四抢到了第8张票
张三抢到了第7张票
王五抢到了第7张票
李四抢到了第7张票
张三抢到了第6张票
王五抢到了第6张票
李四抢到了第6张票
张三抢到了第4张票
李四抢到了第5张票
王五抢到了第5张票
王五抢到了第3张票
李四抢到了第2张票
张三抢到了第1张票
李四抢到了第0张票
王五抢到了第-1张票

Process finished with exit code 0
/**
 * @ClassName Bank
 * @Description TODO 银行取钱
 * @Author ZhangHao
 * @Date 2022/12/14 11:05
 * @Version: 1.0
 */
public class Bank {
    public static void main(String[] args) {
        Card card = new Card(200);
        new MyThread(card,100,"老婆").start();
        new MyThread(card,150,"我").start();
        //出现将卡的余额取成负数
    }
}

//银行卡
class Card {
    public int money;//余额

    public Card(int money) {
        this.money = money;
    }
}

//取钱
class MyThread extends Thread {

    //卡号
    private Card card;
    //要取的钱
    private int takeMoney;
    //手里的钱
    private int nowMoney;

    public MyThread(Card card,int takeMoney,String name){
        super(name);
        this.card = card;
        this.takeMoney = takeMoney;
    }

    @Override
    public void run() {
        if (card.money - takeMoney < 0) {
            System.out.println(Thread.currentThread().getName() + "--->余额不足");
            return;
        }

        try {
            Thread.sleep(1000);//放大问题的发生性
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //取钱
        card.money = card.money - takeMoney;
        //手里的钱
        nowMoney += takeMoney;

        //this.getName() = Thread.currentThread().getName()
        //因为本类继承了Thread类可以直接使用其方法
        System.out.println(Thread.currentThread().getName() + "取了" + takeMoney + "w,手里还有" + nowMoney + "w,银行卡余额还剩" + card.money);
    }
}
我取了150w,手里还有150w,银行卡余额还剩-50
老婆取了100w,手里还有100w,银行卡余额还剩-50
/**
 * @ClassName UnsafeList
 * @Description TODO 多线程不安全的集合
 * @Author ZhangHao
 * @Date 2022/12/14 11:20
 * @Version: 1.0
 */
public class UnsafeList {
    public static void main(String[] args) throws InterruptedException {
        List<String> strList = new ArrayList<>();
        for (int i = 0; i < 10000; i++) {
            new Thread(()->{
                strList.add(Thread.currentThread().getName());
                System.out.println(strList);
            }).start();
        }
        Thread.sleep(1000);
        System.out.println("集合大小:"+strList.size());
    }
}
//ConcurrentModificationException异常(并发修改异常)
java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
	at java.util.ArrayList$Itr.next(ArrayList.java:859)
	at java.util.AbstractCollection.toString(AbstractCollection.java:461)
	at java.lang.String.valueOf(String.java:2994)
	at java.io.PrintStream.println(PrintStream.java:821)
	at com.hnguigu.demo06.UnsafeList.lambda$main$0(UnsafeList.java:19)
	at java.lang.Thread.run(Thread.java:748)
集合大小:9997

Process finished with exit code 0

1. 同步方法

public synchronized void method(int args) {}

由于我们可以通过private关键字来保证数据对象只被封装的方法访问(get/set),所以我们只需要针对方法提供一套机制,这套机制就是synchronized关键字,包括两种用法synchronized方法和synchronized块。

  • synchronized方法:共享的资源是通过方法来实现的。
  • synchronized块:共享的资源是一个对象。

同步方法中的同步监视器就是this,这个对象的本身。

synchronized关键字是一个修饰符,直接加入在方法返回值前面就可以实现同步。

同步方法的弊端:

  • 方法里面需要修改的内容才需要锁,锁得太多,浪费资源

2. 同步块

同步块:synchronized(obj){}

  • obj称为同步监视器

    • obj可以是任何对象,但是推荐使用共享资源作为同步监视器
  • 同步监视器的执行流程

    • 第一个线程访问:锁定同步监视器,执行代码
    • 第二个线程访问:发现同步监视器被锁,无法访问
    • 第一个线程访问完毕,解锁同步监视器
    • 第二个线程访问:发现同步监视器没有锁,执行代码

使用线程同步解决并发带来的问题

/**
 * @ClassName Ticket
 * @Description TODO 模拟买票
 * @Author ZhangHao
 * @Date 2022/12/14 10:40
 * @Version: 1.0
 */
public class Ticket {
    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 ticketNum = 10;//票数
    private boolean flag = true;//设置标志位

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

    //买票
    //加入了synchronized关键字就是同步方法,锁的对象是this
    private synchronized void buy() throws InterruptedException {
        if(ticketNum<=0){
            flag = false;
            return;
        }
        Thread.sleep(100);//模拟延时,放大问题的发生性
        System.out.println(Thread.currentThread().getName()+"抢到了第"+ticketNum--+"张票");
    }
}
/**
 * @ClassName Bank
 * @Description TODO 银行取钱
 * @Author ZhangHao
 * @Date 2022/12/14 11:05
 * @Version: 1.0
 */
public class Bank {
    public static void main(String[] args) {
        Card card = new Card(200);
        new MyThread(card,100,"老婆").start();
        new MyThread(card,150,"我").start();
    }
}

//银行卡
class Card {
    public int money;//余额

    public Card(int money) {
        this.money = money;
    }
}

//取钱
class MyThread extends Thread {
    //卡号
    private Card card;
    //要取的钱
    private int takeMoney;
    //手里的钱
    private int nowMoney = 0;

    public MyThread(Card card,int takeMoney,String name){
        super(name);
        this.card = card;
        this.takeMoney = takeMoney;
    }

    @Override
    public void run() {
        //如果在这里加上synchronized关键字来修饰这个方法,锁的是this也就是MyThread,而真正操作的对象是Card,所以需要使用同步块实现
        //锁的是需要变化的量,需要增删改的对象
        synchronized (card){
            if (card.money - takeMoney < 0) {
                System.out.println(Thread.currentThread().getName() + "--->余额不足");
                return;
            }
            try {
                Thread.sleep(1000);//放大问题的发生性
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //取钱
            card.money = card.money - takeMoney;
            //手里的钱
            nowMoney += takeMoney;

            //this.getName() = Thread.currentThread().getName()
            //因为本类继承了Thread类可以直接使用其方法
            System.out.println(Thread.currentThread().getName() + "取了" + takeMoney + "w,手里还有" + nowMoney + "w,银行卡余额还剩" + card.money);
        }
    }
}
/**
 * @ClassName UnsafeList
 * @Description TODO 多线程不安全的集合
 * @Author ZhangHao
 * @Date 2022/12/14 11:20
 * @Version: 1.0
 */
public class UnsafeList {
    public static void main(String[] args) throws InterruptedException {
        List<String> strList = new ArrayList<>();
        for (int i = 0; i < 10000; i++) {
            new Thread(()->{
                //锁住需要变化的对象,这里就是list
                synchronized(strList){
                    strList.add(Thread.currentThread().getName());
                    System.out.println(strList);
                }
            }).start();
        }
        Thread.sleep(1000);
        System.out.println("集合大小:"+strList.size());
    }
}

补充:juc(java.util.concurrent)包下的线程安全的集合

/**
 * @ClassName CopyOnWriteArrayList
 * @Description TODO 测试JUC并发编程下线程安全的ArrayList集合
 * @Author ZhangHao
 * @Date 2022/12/14 13:19
 * @Version: 1.0
 */
public class TestCopyOnWriteArrayList {
    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();
        }
        //这里加入sleep()方法是防止在子线程还没完成之前,就打印了集合大小
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("集合大小:"+list.size());
    }
}

3. 死锁

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

/**
 * @ClassName DeadLock
 * @Description TODO 死锁
 * @Author ZhangHao
 * @Date 2022/12/14 16:15
 * @Version: 1.0
 */
public class DeadLock {
    public static void main(String[] args) {
        Makeup makeup1 = new Makeup(0, "灰姑娘");
        Makeup makeup2 = new Makeup(1, "白雪公主");
        
        makeup1.start();
        makeup2.start();
        //最终结果:程序僵持运行着
    }
}
//口红
class Lipstick{
    String name = "迪奥口红";
}
//镜子
class Mirror{
    String name = "魔镜";
}
//化妆
class Makeup extends Thread{

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

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

    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 + "--->获得" + lipstick.name);
                Thread.sleep(1000);
                synchronized (mirror){//一秒钟之后想要镜子的锁
                    System.out.println(this.girlName + "--->获得" + mirror.name);
                }
            }
        }else{
            synchronized (mirror){//获得镜子的锁
                System.out.println(this.girlName + "--->获得" + mirror.name);
                Thread.sleep(2000);
                synchronized (lipstick){//两秒钟之后想要口红的锁
                    System.out.println(this.girlName + "--->获得" + lipstick.name);
                }
            }
        }

    }
}

灰姑娘拿着口红的锁不释放,随后一秒钟后又要魔镜的锁,白雪公主拿着魔镜的锁不释放,两秒钟后又要口红的锁,双方都不释放已经使用完了的锁资源,僵持形成死锁。
解决办法就是用完锁就释放。

/**
 * @ClassName DeadLock
 * @Description TODO 死锁
 * @Author ZhangHao
 * @Date 2022/12/14 16:15
 * @Version: 1.0
 */
public class DeadLock {
    public static void main(String[] args) {
        Makeup makeup1 = new Makeup(0, "灰姑娘");
        Makeup makeup2 = new Makeup(1, "白雪公主");

        makeup1.start();
        makeup2.start();
    }
}
//口红
class Lipstick{
    String name = "迪奥口红";
}
//镜子
class Mirror{
    String name = "魔镜";
}
//化妆
class Makeup extends Thread{

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

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

    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 + "--->获得" + lipstick.name);
                Thread.sleep(1000);
            }
            synchronized (mirror){//一秒钟之后想要镜子的锁
                System.out.println(this.girlName + "--->获得" + mirror.name);
            }
        }else{
            synchronized (mirror){//获得镜子的锁
                System.out.println(this.girlName + "--->获得" + mirror.name);
                Thread.sleep(2000);
            }
            synchronized (lipstick){//两秒钟之后想要口红的锁
                System.out.println(this.girlName + "--->获得" + lipstick.name);
            }
        }
    }
}

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

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

上面就是形成死锁的必要条件,只需要解决其中任意一个或者多个条件就可以避免死锁的发生。

4. Lock锁

从JDK5.0开始,Java提供了更强大的线程同步机制,通过显示定义同步锁对象来实现同步。同步锁使用Lock对象充当。
java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。

/**
 * @ClassName TestLock
 * @Description TODO Lock锁
 * @Author ZhangHao
 * @Date 2022/12/14 16:47
 * @Version: 1.0
 */
public class TestLock {
    public static void main(String[] args) {
        MyLock myLock = new MyLock();

        new Thread(myLock, "张三").start();
        new Thread(myLock, "老王").start();
        new Thread(myLock, "黄牛").start();
    }
}

class MyLock 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(Thread.currentThread().getName() + "--->" + ticketNums--);
                } else {
                    break;
                }
            } finally {
                lock.unlock();//解锁
            }

        }
    }
}

synchronized和lock锁的区别:

  1. Lock是显式锁(手动开启和关闭锁,别忘记关闭锁)synchronized是隐式锁,出了作用域自动释放
  2. Lock只有代码块锁,synchronized有代码块锁和方法锁
  3. 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
    优先使用顺序:
    Lock > 同步代码块(已经进入了方法体,分配了相应资源)> 同步方法(在方法体之外)

六、线程通信

应用场景:生产者和消费者的问题。
假设有一个仓库只能放一件产品,生产者将生产出来的产品放到仓库,消费者从仓库取走商品消费。如果仓库中没有产品,则消费者等待生产者生产商品,有商品则通知消费则取走商品。
这是一个线程同步的问题,生产者和消费者共享同一个资源,并且生产者和消费者之间互相依赖,互成条件。

  • 对于生产者,没有生产产品之前,需要通知消费者等待,而生产了产品之后需要通知消费者取走消费。
  • 对于消费者,在消费完之后,要通知生产者继续生产。
  • 在生产者消费者问题中,仅有synchronized是不够的
    • synchronized可阻止并发更新同一个共享资源,实现了同步。
    • synchronized不能实现不同线程之间消息传递(通信)。

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

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

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

解决办法:

1. 管城法

生产者将生产好的数据放入缓冲区,消费者从缓冲区拿出数据。

/**
 * @ClassName TestPC
 * @Description TODO 生产者和消费者模型
 * @Author ZhangHao
 * @Date 2022/12/14 20:28
 * @Version: 1.0
 */
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;

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

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

class Consumer extends Thread {

    //消费者需要从仓库里面取鸡
    SynContainer container;

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

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

//商品
class Chicken {
    int count;//数量

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

//缓冲区
class SynContainer {
    //需要一个容器装载,假如一个仓库只能装10只鸡
    Chicken[] chickens = new Chicken[10];
    //计数
    int count = 0;

    //生产者放入容器的方法
    public synchronized void push(Chicken chicken) {
        //如果仓库装满了鸡,就通知消费者消费,生产者等待。
        while (count == chickens.length - 1) {
            try {
                this.wait();//生产者等待
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //生产者生产商品丢入仓库
        chickens[count] = chicken;
        count++;
        //通知消费者消费,唤醒消费者。
        this.notifyAll();

    }

    //消费者消费产品的方法
    public synchronized Chicken pop() {
        //仓库里面没有鸡,就通知生产者生产,消费者等待
        while (count == 0) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //消费产品
        count--;
        Chicken chicken = chickens[count];
        //通知生产者生产,唤醒生产者。
        this.notifyAll();
        return chicken;
    }
}

注意:这里如果使用if判断逻辑上是完全没问题的,但是这里会出现一个虚假唤醒,通俗的说如果某个线程处于wait()状态,如果用if判断的话,唤醒后线程会直接从wait方法后执行,不会重新进行if判断,但如果使用while来作为判断语句的话,也会从wait之后的代码运行,但是唤醒后会重新判断循环条件。

2. 信号灯法

通过设置标志位来完成线程之间的通信

/**
 * @ClassName TestTV
 * @Description TODO 信号灯法
 * @Author ZhangHao
 * @Date 2022/12/14 21:33
 * @Version: 1.0
 */
public class TestTV {
    public static void main(String[] args) {
        Tv tv = new Tv();

        new Thread(new Player(tv)).start();
        new Thread(new Watcher(tv)).start();
    }
}

//定义两个线程:生产者和消费者
//表演者
class Player implements Runnable {

    Tv tv;

    Player(Tv tv) {
        this.tv = tv;
    }

    @Override
    public void run() {
        for (int i = 1; i < 20; i++) {
            if (i%2==0){
                tv.play("光头强");
            }else{
                tv.play("喜洋洋");
            }
        }
    }
}

//观众
class Watcher implements Runnable {

    Tv tv;

    Watcher(Tv tv) {
        this.tv = tv;
    }

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

//表演
class Tv {
    //演员表演,观众观看
    String program;//节目
    boolean flag = true;//设置标志位,默认是没有节目观看

    //演员表演
    public synchronized void play(String program) {
        //如果有节目观看,演员就等待观众观看
        if (!flag) {
            try {
                this.wait();//等待观看
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("演员表演了:" + program);
        this.notifyAll();//唤醒观众
        this.program = program;
        this.flag = !this.flag;
    }

    //观众观看
    public synchronized void watch() {
        //如果没有节目观看,就通知演员表演
        if (flag) {
            try {
                this.wait();//等待演出
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("观众观看了:" + program);
        this.notifyAll();//唤醒演员表演
        this.flag = !this.flag;
    }
}

七、线程池

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

思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。

可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。

  • 好处:
    • 提高响应速度(减少了创建新线程的时间)
    • 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
    • 便于线程管理(…)
      • corePoolSize:核心池的大小
      • maximumPoolSize:最大线程数
      • keepAliveTime:线程没有任务时最多保持多长时间后会终止

JDK 5.0起提供了线程池相关API:ExecutorService 和 Executors

  • ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
    • void execute(Runnable command) :执行任务/命令,没有返回值,一般用来执行Runnable
    • Future submit(Callable task):执行任务,有返回值,一般又来执行Callable
    • void shutdown() :关闭连接池

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

/**
 * @ClassName TestPool
 * @Description TODO 线程池
 * @Author ZhangHao
 * @Date 2022/12/14 21:58
 * @Version: 1.0
 */
public class TestPool {
    public static void main(String[] args) {
        //创建线程池,参数是线程池的大小,决定了能装多少个线程
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        
        executorService.execute(new ThreadPool());
        executorService.execute(new ThreadPool());
        executorService.execute(new ThreadPool());
        executorService.execute(new ThreadPool());
        executorService.execute(new ThreadPool());
        
        //关闭连接
        executorService.shutdown();
    }
}
class ThreadPool implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}

完结撒花!

/**
 * @ClassName TestCallable
 * @Description TODO 补充Callable启动线程
 * @Author ZhangHao
 * @Date 2022/12/14 22:28
 * @Version: 1.0
 */
public class TestCallable  {
    public static void main(String[] args){
        MyCallable callable = new MyCallable();
        //Runnable实现类
        FutureTask<Integer> futureTask = new FutureTask<>(new MyCallable());
        new Thread(futureTask).start();

        //获取callable返回值
        try {
            Integer integer = futureTask.get();
            System.out.println(integer);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}
class MyCallable implements Callable<Integer>{
    @Override
    public Integer call() throws Exception {
        System.out.println("完结撒花!");
        return 100;
    }
}

再看!再看!再看就把你吃掉!