Java多线程详解

塈百日而求新,念三番未发,其三

线程简介

多任务:一个人同时干多件事情

多线程:通过道路划分,提高车辆运行效率

程序:是一个静态的概念,指令和数据的有序集合

进程/Process:程序的一次执行过程,系统必定会分配的,是指操作系统中运行的程序,比如QQ、游戏等

线程/Thread:

  • 比如游戏中的声音、图像、弹幕
  • 多线程需要多核——多个CPU,因为同一时间一个CPU只能执行一行代码
  • 线程之间相互独立
  • 线程的运行由调度器安排,不能人为干预
  • 对同一份资源操作时存在资源抢夺的问题,需要加入并发控制
  • 线程会带来额外的开销:CPU调度时间,并发控制开销
  • 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致

线程实现(重点)

线程创建

  • Thread class(重点):继承Thread类
  • Runnable接口(重点):实现Runnable接口
  • Callable接口:实现Callable接口
Thread(重点)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C5ZGcUvN-1661861385952)(C:\Users\86156\AppData\Roaming\Typora\typora-user-images\image-20220606202634090.png)]

如上图所示:有run()就先执行;有start()就同步执行

范例:

package com.qf.Demo01;
//1.    继承Thread类
public class TestThread1 extends Thread{
    //2.    重写run方法
    @Override
    public void run() {
        for (int i = 0; i < 2000; i++) {
            System.out.println("我在看代码--------"+i);
        }
    }

    //主线程
    public static void main(String[] args) {
        //3.    调用start方法开启线程
        TestThread1 testThread1 = new TestThread1();
        testThread1.start();
        for (int i = 0; i < 2000; i++) {
            System.out.println("我在学习多线程-------"+i);
        }
    }
}

结果如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9Vbfs6rl-1661861385964)(C:\Users\86156\AppData\Roaming\Typora\typora-user-images\image-20220606202815091.png)]

也就能看出

  • start()是同步进行
  • 线程开启后不一定立即执行,由CPU调度执行
实现多线程同步下载图片

先下载commons-io[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uhWRiSQZ-1661861385965)(C:\Users\86156\AppData\Roaming\Typora\typora-user-images\image-20220606204711275.png)]

创建Package,取名lib,复制过来该文件

然后再右键选择Add as Library,就可以进行使用了,如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YlmJoB5g-1661861385965)(C:\Users\86156\AppData\Roaming\Typora\typora-user-images\image-20220606204848700.png)]

先理顺一下思路:先写下载器WebDownloader→实现线程类/继承Thread,并且用构造器放入url和name→重写run()→通过构造器创建t1~t3三个线程,然后启动

代码范例:

package com.qf.Demo01;

import org.apache.commons.io.FileUtils;

import java.io.File;
import java.io.IOException;
import java.net.URL;

public class TestThread2 extends Thread {
    private String url; //网络图片地址
    private String name;    //保存的文件名

    //写构造器
    public TestThread2(String url, String name) {
        this.url = url;
        this.name = name;
    }

    //重写之后的run()变为下载图片线程的执行体
    @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("https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg-blog.csdnimg.cn%2F20210220081933289.png%3Fx-oss-process%3Dimage%2Fwatermark%2Ctype_ZmFuZ3poZW5naGVpdGk%2Cshadow_10%2Ctext_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQyNDMyNjcz%2Csize_16%2Ccolor_FFFFFF%2Ct_70&refer=http%3A%2F%2Fimg-blog.csdnimg.cn&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1657113199&t=4014c8175dc3d6c90c68f61aba35cb5e",
                "1.jpg");
        TestThread2 t2 = new TestThread2("https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg-blog.csdnimg.cn%2F20210220081933289.png%3Fx-oss-process%3Dimage%2Fwatermark%2Ctype_ZmFuZ3poZW5naGVpdGk%2Cshadow_10%2Ctext_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQyNDMyNjcz%2Csize_16%2Ccolor_FFFFFF%2Ct_70&refer=http%3A%2F%2Fimg-blog.csdnimg.cn&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1657113199&t=4014c8175dc3d6c90c68f61aba35cb5e",
                "2.jpg");
        TestThread2 t3 = new TestThread2("https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg-blog.csdnimg.cn%2F20210220081933289.png%3Fx-oss-process%3Dimage%2Fwatermark%2Ctype_ZmFuZ3poZW5naGVpdGk%2Cshadow_10%2Ctext_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQyNDMyNjcz%2Csize_16%2Ccolor_FFFFFF%2Ct_70&refer=http%3A%2F%2Fimg-blog.csdnimg.cn&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1657113199&t=4014c8175dc3d6c90c68f61aba35cb5e",
                "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异常,downloader方法出现问题");
            }
        }

}
结果:
下载了文件名为:1.jpg
下载了文件名为:3.jpg
下载了文件名为:2.jpg

图片下载顺序结果和操作顺序不同也说明线程同时执行。

查看下载的图片:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xAh3DPSV-1661861385966)(C:\Users\86156\AppData\Roaming\Typora\typora-user-images\image-20220606213015678.png)]

Runnable(重点)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9wytKXby-1661861385966)(C:\Users\86156\AppData\Roaming\Typora\typora-user-images\image-20220607125916736.png)]

范例:

package com.qf.Demo01;

public class TestThread3 implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 2000; 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 < 2000; i++) {
            System.out.println("我在学习多线程-------"+i);
        }
    }
}

结果如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-O6CV14in-1661861385966)(C:\Users\86156\AppData\Roaming\Typora\typora-user-images\image-20220607131050355.png)]

同样也能看出两个线程是同时进行的

对比继承Thread和Runnable接口

在这里插入图片描述

  • 两者的重写run()是相同的
  • 继承Thread是直接调用start();Runnable接口则需要将该类的实现对象放入线程对象当中,作为其参数,然后再调用start()。

同样的,如果将继承Thread改成实现Runnable接口只需要更改两处地方:

  • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cxL9tk2n-1661861385967)(C:\Users\86156\AppData\Roaming\Typora\typora-user-images\image-20220607131846957.png)]

  • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yuVdKFiP-1661861385967)(C:\Users\86156\AppData\Roaming\Typora\typora-user-images\image-20220607131917264.png)]

小结:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3sLDWAXT-1661861385967)(C:\Users\86156\Desktop\image-20220607132025759.png)]
多个线程同时操作同一个对象

范例1:

package com.qf.Demo01;
//买火车票
public class TestThread4 implements Runnable{
    //票数
    private int ticketnumbers = 10;

    //重写run()
    @Override
    public void run() {
        while (true){
            if(ticketnumbers<=0){
                break;
            }
            //利用Thread.sleep()模拟抢票的延时情况,然后捕获异常
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"-->拿到了第"+ticketnumbers--+"张票");    //该方法可以获得当前正在执行的线程的名字
        }
    }

    public static void main(String[] args) {
        //因为实现了Runnable接口,所以只需要创建一个类对象,然后让多个线程一起start即可
        TestThread4 ticket = new TestThread4();
        new Thread(ticket,"小明").start(); //重写run()中的getName(),这里可以赋予name具体值;如果不赋予,最终结果就是"线程0~2“
        new Thread(ticket,"小红").start();
        new Thread(ticket,"小绿").start();
    }
}
结果:
小红-->拿到了第9张票
小绿-->拿到了第10张票
小明-->拿到了第8张票
小明-->拿到了第6张票
小红-->拿到了第5张票
小绿-->拿到了第7张票
小红-->拿到了第3张票
小明-->拿到了第2张票
小绿-->拿到了第4张票
小明-->拿到了第1张票
小绿-->拿到了第1张票
小红-->拿到了第0张票

结果的倒数第二、第三行出现了小明和小绿抢了同一张票,所以多个线程操作同一个资源的情况下,线程不安全,数据紊乱

范例2:

先理清一下思路:距离为100步的for循环→判断比赛是否结束gameOver(),并且打印出胜利者→比赛开始start()→模拟兔子睡觉→乌龟获胜

package com.qf.Demo01;
//龟兔赛跑
public class Race implements Runnable{
    //胜利者 只有一个,可以使用静态常量static
    private static String winner;
    @Override
    public void run() {
        for (int i = 0; i <= 100; i++) {
            //模拟兔子休息    让"兔子"没跑11步睡一会
            if(Thread.currentThread().getName().equals("兔子") && i%10==0){
                try {
                    Thread.sleep(10);
                } 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();
    }
}

结果如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BdfWRqJc-1661861385968)(C:\Users\86156\AppData\Roaming\Typora\typora-user-images\image-20220607140428347.png)]

Callable接口(了解)

特点

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

  • 重写call方法,需要抛出异常

  • 创建执行服务:

    • ExecutorService ser = Executors.newFixedThreadPool();
      
  • 提交执行

    • Future<Boolean> r1 = ser.submit(t1);
      
  • 获取结果

    • boolean rs1 = r1.get();
      
  • 关闭服务

    • ser.shutdownNow();
      

用Callable方法实现下载图片,和Runnable接口进行比较:

在这里插入图片描述

总结:

Callab的好处:

  • 可以定义返回值
  • 可以抛出异常
静态代理模式

范例:

package com.qf;

public class StaticProxy {
    public static void main(String[] args) {
        //别忘了写main函数  0.0
        //如果不使用静态代理模式,就要像下面两行代码这样运行
        //You you = new You();	//你要结婚
        //you.HappyMarry();
        WeddingCompany weddingCompany = new WeddingCompany(new You());
        weddingCompany.HappyMarry();
        //以上两行代码=下面这一行代码
        //new WeddingCompany(new You()).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();
        this.target.HappyMarry();	//真实对象结婚
        after();
    }

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

    private void before() {
        System.out.println("结婚之前,布置现场");
    }
}
结果:
结婚之前,布置现场
百年好合
结婚之后,收尾款

总结:

  • 真实对象和代理对象都要实现同一个接口
  • 代理对象要代理真实角色/要传递一个参数

好处:

  • 代理对象可以做很多真实对象做不了的事情
  • 真实对象可以专注做自己的事情(上班族要结婚,就将这项任务交给婚庆公司,不耽误自己正常工作)
Lambda表达式

为什么要用Lambda表达式:

  • 避免匿名内部类定义过多
  • 简洁化代码
  • 精简化代码

Lambda表达式重要性:实现多线程方法中有一个非常重要的接口——Runnable接口,它里面就是一个interface+run(),也就是说如果Runnable接口里的实现类简单,可以用Lambda表达式简化

先了解函数式接口:只包含唯一一个抽象方法,如下例所示:

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

对于函数式接口,就可以通过lambda表达式来创建该接口的对象

范例1:

package com.qf.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 ilike = new Like();   //注意这里ILike是抽象的,无法实例化,注意写法
        ilike.lambda();

        //ilike 是定义的一个接口,不需要再次定义
        ilike = new Like2();
        ilike.lambda();

        //4.    使用局部内部类进一步优化
        class Like3 implements ILike{

            @Override
            public void lambda() {
                System.out.println("i like lambda3");
            }
        }
        ilike = new Like3();
        ilike.lambda();

        //5.    使用匿名内部类继续优化     匿名也就是没有名字,必须借助接口或者父类来实现		然后把核心代码放入匿名内部类当中即可
        //所以这里new ILike()
        ilike = new ILike() {
            @Override
            public void lambda() {
                System.out.println("i like lambda4");
            }
        };
        ilike.lambda();

        //6.    用lambda简化   因为类和方法都是一样的,所以直接省略
        ilike = ()-> {
            System.out.println("i like lambda5");
        };
        ilike.lambda();
    }
}

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

//2.    实现类
class Like implements ILike{

    @Override
    public void lambda() {
        System.out.println("i like lambda");
    }
}
结果:
i like lambda
i like lambda2
i like lambda3
i like lambda4
i like lambda5

范例2:

package com.qf.lambda;
//Ctrl+Shift+/ 可以将选定的部分全部进行多行注释
public class TestLambda2 {

    public static void main(String[] args) {

        //不用Lambda一般都是这样:首先先实现Ilove接口  (new 实现类)   →   然后调用love实现类中重写的love方法
        //使用lambda
        //Ilove love = (int a) -> { System.out.println("i love you ->"+a); };
        //Lambda表达式简化
        //为了方便,先将Ilove love 这部分省略掉
        Ilove love =null;
        //1.    参数类型
        //love = (a) -> { System.out.println("i love you ->"+a); };
        //2.    简化括号
        //love = a -> { System.out.println("i love you ->"+a); };
        //3.    去掉花括号
        love = a -> System.out.println("i love you ->"+a);
        love.love(522);
    }
}

//接口
interface Ilove{
    void love(int a);
}
结果:
i love you ->522

总结:

  • Lambda表达式只能将一行表达式简化为一行代码,如果有多行,就用代码块包裹({})

  • 前提:接口是函数式接口

  • 多个参数:①要去掉函数类型就一起去掉;②保留括号

    • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4dI7IH1S-1661861385969)(C:\Users\86156\AppData\Roaming\Typora\typora-user-images\image-20220608100333572.png)]

线程状态

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cIdu7L1v-1661861385969)(C:\Users\86156\AppData\Roaming\Typora\typora-user-images\image-20220608101145337.png)]

线程停止

范例:

package com.qf.state;

//①    建议线程正常停止-->利用次数,不建议死循环
//②    建议使用标志位
//③    不要使用stop或者destroy等过时或者JDK不建议使用的方法

public class TestStop implements Runnable{
    //1.    设置一个标志位
    private boolean flag = true;
    //重写run(),同时也是线程体
    @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);   //main 是主线程
            //调用自己写的stop()转换标志位,不管主线程还是辅线程,只要i==900,就停止辅线程
            if(i==900){
                testStop.stop();
                System.out.println("线程该停止了");
            }
        }
    }
}

结果如下图所示:当然也可以看出主辅两个线程是"同时"运行的

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-09LiCxsY-1661861385969)(C:\Users\86156\AppData\Roaming\Typora\typora-user-images\image-20220608102822352.png)]

线程休眠

  • sleep(时间) 指当前线程阻塞的毫秒数(1/1000秒)
  • 存在异常,需要抛出
  • sleep时间结束后,线程进入就绪状态
  • sleep可以模拟网络延时、倒计时等,放大问题的发生性,让其更加真实
  • 每个对象都有一把锁,sleep不会释放锁

范例1:

package com.qf.state;
//模拟倒计时
public class TestSleep2 {
    public static void tenDown() throws InterruptedException {
        int num = 10;
        while (true){
            Thread.sleep(1000);
            System.out.println(num--);
            if(num<=0) break;
        }
    }

    public static void main(String[] args) {
        try {
            tenDown();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
结果:
    10
9
8
7
6
5
4
3
2
1

范例2:

package com.qf.state;

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

//模拟倒计时
public class TestSleep2 {
    public static void tenDown() throws InterruptedException {
        int num = 10;
        while (true){
            Thread.sleep(1000);
            System.out.println(num--);
            if(num<=0) break;
        }
    }

    public static void main(String[] args) {
        //打印当前系统时间
        Date startTime = new Date(System.currentTimeMillis());
        while (true){
            try {
                Thread.sleep(1000);
                //每休眠1s,打印一次时间
                System.out.println(new SimpleDateFormat("HH:mm:ss").format(startTime));
                //更新时间
                startTime = new Date(System.currentTimeMillis());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

结果如下图所示: 没过1s打印一次系统时间

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-j1ld8ExH-1661861385970)(C:\Users\86156\AppData\Roaming\Typora\typora-user-images\image-20220608104756487.png)]

线程礼让

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5ys8ZxG6-1661861385970)(C:\Users\86156\AppData\Roaming\Typora\typora-user-images\image-20220609125540046.png)]

如上图所示:线程A和线程B都是就绪状态,当A进入CPU开始执行时如果进行线程礼让,A和B就会回到同一起跑线/就绪态,等待CPU重新调度。但是此时礼让是否成功/CPU是否调度B是不一定的,看心情。

范例:

package com.qf.state;

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()+"线程停止执行");
    }
}
结果:
a线程开始执行
b线程开始执行
a线程停止执行
b线程停止执行

线程强制执行

理解为插队,Join将制定线程执行完成之后再执行其他线程,其他线程暂时堵塞。

范例:

package com.qf.state;

public class TestJoin implements Runnable{

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

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

        //主线程
        for (int i = 0; i < 1000; i++) {
            if(i==200){
                thread.join();  //插队
            }
            System.out.println("main"+i);
        }
    }
}

结果如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FGvUfHAh-1661861385970)(C:\Users\86156\AppData\Roaming\Typora\typora-user-images\image-20220609131120513.png)]

线程状态观测

  • NEW
  • RUNNABLE
  • BLOCKED
  • WAITING
  • TIMED _WAITING
  • TERMINATED

范例:

package com.qf.state;

public class TestState {
    public static void main(String[] args) throws InterruptedException {
        //以Lambda表达式形式创建线程
        Thread thread = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("//");
        });

        //观察状态
        Thread.State state = thread.getState();     //先写 thread.getState() 然后使用Alt+Enter来快捷生成变量对象
        System.out.println(state);  //NEW

        //观察启动后
        thread.start();
        state = thread.getState();  //不用每次都创建一个新的state,可以直接使用上一次创建好的,节省空间
        System.out.println(state);  //RUN

        while (state != Thread.State.TERMINATED){   //只要线程不终止,就一直输出状态
            Thread.sleep(100);
            state = thread.getState();  //更新while()里的线程状态
            System.out.println(state);
        }
        thread.start();     //线程不能启动两次
    }
}
结果:
NEW
RUNNABLE
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
//
TERMINATED
Exception in thread "main" java.lang.IllegalThreadStateException
	at java.lang.Thread.start(Thread.java:710)
	at com.qf.state.TestState.main(TestState.java:31)

线程优先级

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

Thread.MIN_PRIORITY = 1;
Thread.MAX_PRIORITY = 10;
Thread.NORM_PRIORITY = 5;

​ 优先级高了,权重、分配的资源就高,就更可能先执行。

package com.qf.state;

public class TestPriority {
    public static void main(String[] args) {
        //打印主线程的默认优先级
        System.out.println(Thread.currentThread().getName()+"-->"+Thread.currentThread().getPriority());
        MyPriority myPriority = new MyPriority();
        //创建多个对象并且把线程体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(-1); //不合法
        t5.start();

        t6.setPriority(11); //不合法
        t6.start();
    }
}

class MyPriority implements Runnable{

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"-->"+Thread.currentThread().getPriority());
    }
}
结果:
main-->5
Thread-2-->4
Thread-3-->10
Thread-0-->5
Thread-1-->1
Exception in thread "main" java.lang.IllegalArgumentException
	at java.lang.Thread.setPriority(Thread.java:1094)
	at com.qf.state.TestPriority.main(TestPriority.java:27)

守护线程

  • 线程分为用户线程(main) 和 守护线程(gc/垃圾回收)
  • 虚拟机必须确保用户线程执行完毕
  • 虚拟机不用等待守护线程执行完毕

范例:

package com.qf.state;

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); //.setDaemon()默认是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("一生都开心的活着");
        }
        System.out.println("去天堂");
    }
}

结果如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FQ86A6bI-1661861385971)(C:\Users\86156\AppData\Roaming\Typora\typora-user-images\image-20220609135739190.png)]

注意:理论上在用户线程结束/去天堂之后,就不会继续执行守护线程,但是虚拟机关闭需要一定的时间。

线程同步(重点)

​ 多个线程操作同一个资源/并发,可能出现票数为负数的情况,这时候就需要进行线程同步。线程同步就是一个等待机制,要进行操作的线程要进入这个对象的等待池形成队列,依次执行。

​ 线程同步的形成条件/解决安全性的条件:队列 + 锁

​ 锁机制:synchronized,保证线程安全。当一个线程获得对象的排它锁,独占资源,其他线程必须等待,使用后释放锁即可。

​ 锁机制也会引起一下问题:

  • 一个线程持有锁会导致其他相关线程挂起,虽然安全,但是加锁、释放锁的操作会降低性能
  • 优先级高的线程等待低优先级的线程释放锁,导致导致优先级导致/性能问题

不安全范例

范例1:

package com.qf.syn;
//不安全买票
public class UnsafeBuyTicket {
    public static void main(String[] args) {
        BuyTicket station = new BuyTicket();
        new Thread(station,"1").start();
        new Thread(station,"2").start();
        new Thread(station,"3").start();
    }
}

class BuyTicket implements Runnable{
    //票数 private为了安全
    private int ticketnumbers = 10;
    boolean flag = true;    //外部停止方式
    @Override
    public void run() {
        //买票
        while (flag){
            buy();
        }
    }
    private void buy(){
        //判断是否有票
        if(ticketnumbers<=0){
            flag = false;
            return;
        }
        //有票
        System.out.println(Thread.currentThread().getName()+"拿到"+ticketnumbers--);
        //模拟延时
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
结果:
1拿到9
3拿到8
2拿到10
2拿到7
1拿到7
3拿到6
2拿到5
1拿到5
3拿到4
1拿到3
2拿到2
3拿到1

拿到相同的两张票(还可能出现票数为-1的情况)→线程不安全

范例2:

package com.qf.syn;
//不安全的取钱
public class UnsafeBank {
    public static void main(String[] args) {
        //账户
        Account account = new Account(100,"零花钱");   //如果在Account类中,不添加constructor,在这里就会报错
        Drawing you = new Drawing(account,50,"你");
        Drawing yourgirlfriend = new Drawing(account,100,"yourgirlfriend");
        you.start();
        yourgirlfriend.start();
    }
}
//账户
class Account{
    //因为需要在银行类中使用这两个变量,不写private更方便调用
    int money;  //余额
    String name;    //卡名

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

//银行:模拟取款   因为涉及到多个线程操作同一个对象所以extends Thread
class Drawing extends Thread{
    Account account;    //账户
    int drawingMoney;   //要取多少钱
    int nowMoney;   //现在还有多少钱
  

    //因为继承了Thread类,无法通过Alt+Insert来快捷生成构造方法,所以需要手动填写
    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; //钱不够就退出了
        }
        //sleep放大问题的发生性
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //卡内余额 = 余额 - 你取的钱
        account.money -= drawingMoney;
        //你手里的钱
        nowMoney += drawingMoney;
        //打印账户余额
        System.out.println(account.name+"余额为:"+account.money);
        //打印手里的钱    这里Thread.currentThread().getName()=this.getName()
        System.out.println(this.getName()+"手里的钱"+nowMoney);

    }
}
结果:
零花钱余额为:0
yourgirlfriend手里的钱100
零花钱余额为:-50
你手里的钱50

通过结果的余额为负就能看出线程的不安全

这里有个比较有趣的点:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wzpLC6Cy-1661861385971)(C:\Users\86156\AppData\Roaming\Typora\typora-user-images\image-20220614035537743.png)][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HjkjVeRQ-1661861385971)(C:\Users\86156\AppData\Roaming\Typora\typora-user-images\image-20220614035644914.png)]

该行代码放在Drawing方法体里的第一行时是真确的,但是放在第三行就会出现错误。提示上说“对于super()的申明必须作为结构体的第一个声明”,按照提示上来即可。

范例3:

package com.qf.syn;

import java.util.ArrayList;
import java.util.List;

//线程不安全的集合
public class UnsafeList {
    public static void main(String[] args) {
        //先实例化一个ArrayList型的List
        List<String> list = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            new Thread(() ->{
                list.add(Thread.currentThread().getName());
            }).start();
        }
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(list.size());
    }
}
结果:
997   

结果不是1000→不安全。原因也很简单,因为将名字加入list中时,多个线程可能添加到同一个位置/操作对象的位置相同,也就覆盖掉了,总数也就少了。

同步方法及同步块

​ 在封装中通过使用private关键字和get/set来使对象安全。相应的,也就有synchronized关键字(分为方法和块,两者的运行原理相同)。

​ 每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象的所才能执行,否则线程就会阻塞,方法执行时独占该锁,知道方法返回才释放锁,后面被阻塞的线程才能获得锁来继续执行。

​ 缺点:如果一个大的方法申明synchronized就会影响效率。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Wfhqc6Gl-1661861385972)(C:\Users\86156\AppData\Roaming\Typora\typora-user-images\image-20220614041800520.png)]

代码中只读部分是每个人都可以读取的,同步只需要同步修改部分的代码,否则就会浪费资源。也就是synchronized块。

修改火车站买票(只有一个操作对象):

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FgMH5evq-1661861385972)(C:\Users\86156\AppData\Roaming\Typora\typora-user-images\image-20220614042728721.png)]

修改两人银行取钱(有两个操作对象):

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-i6F7PQ5h-1661861385972)(C:\Users\86156\AppData\Roaming\Typora\typora-user-images\image-20220614043331082.png)]

当锁执行方法——run()之后发现还是会出错,这是因为由于默认锁的是自己这个类本身(this),也就是Drawing,而增删改查的对象是account账户。需要使用同步块来锁定变化的量(同步块可以锁定任何对象):

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zPB9l6Es-1661861385972)(C:\Users\86156\AppData\Roaming\Typora\typora-user-images\image-20220614043815358.png)]

将原来的代码移到synchronized (account){}中即可

修改线程不安全的范例:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kfQLczg9-1661861385973)(C:\Users\86156\AppData\Roaming\Typora\typora-user-images\image-20220614044455632.png)]

CopyOnWriteArrayList
package com.qf.syn;

import java.util.concurrent.CopyOnWriteArrayList;

//测试JUC安全类型的集合
public class TestJUC {
    public static void main(String[] args) {
        CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<String>(); //这个类是安全类,理解成已经加号加synchronized锁的类
        for (int i = 0; i < 1000; i++) {
            new Thread(()->{
                list.add(Thread.currentThread().getName());
            }).start();
        }
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(list.size());
    }
}
结果:
1000
死锁

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

范例:

package com.qf.syn.thread;

public class DeadLock {
    public static void main(String[] args) {
        Makeup g1 = new Makeup(0,"小红");
        Makeup g2 = new Makeup(10,"小绿");
        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;    //使用化妆品的人

    //构造器
    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) {  //1s后想获得镜子
                    System.out.println(this.girlname + "获得镜子的锁");
                }
            }
        } else {
            synchronized (mirror) {   //获得镜子的锁
                System.out.println(this.girlname + "获得镜子的锁");
                Thread.sleep(1000);

                synchronized (lipstick) {  //1s后想获得口红
                    System.out.println(this.girlname + "获得口红的锁");
                }

            }
        }
    }
}
结果:
小绿获得镜子的锁
小红获得口红的锁

出现了死锁,只需要分别将一个锁移出代码块即可:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D6Ka6kJL-1661861385973)(C:\Users\86156\AppData\Roaming\Typora\typora-user-images\image-20220614065140938.png)]

Lock

ReentrantLock(可重入锁)类实现了Lock,比较常用,需要实现接口,可以显示加锁、释放锁

范例:

package com.qf.syn.thread.gaoji;

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;


    @Override
    public void run() {
        while (true){
            if(ticketNums>0){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(ticketNums--);
            }else {
                break;
            }
        }
    }
}
结果:
10
8
9
7
6
5
4
3
2
1
-1
0

出现-1,是不安全的。

使用Lock进行调整:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eFnYyBqK-1661861385973)(C:\Users\86156\AppData\Roaming\Typora\typora-user-images\image-20220614070829021.png)]

synchronized与Lock对比

  • Lock是显式锁(手动开启和关闭锁),synchronized是隐式锁,除了作用域自动释放
  • Lock只有代码块锁
  • Lock锁:JVM将花费较少的时间来调度进程,性能更好,并且有更多的子类(ReentrantLock)
生产者消费者问题

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LcTLzgCh-1661861385974)(C:\Users\86156\AppData\Roaming\Typora\typora-user-images\image-20220614071949861.png)]

使用wait()和notify()实现通信

管程法

范例:

package com.qf.syn.thread.gaoji;

//生产者消费者模型,利用缓冲区解决
public class TestPC {
    public static void main(String[] args) {
        SynContainer container = new SynContainer();
        new Productor(container).start();
        new Consumer(container).start();
    }
}

//生产者
class Productor extends Thread{ //将Productor存在词典里就不会有下波浪线了
    //生产者和消费者都需要一个容器
    SynContainer container;
    //构造方法
    public Productor(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;
    }
}

结果如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kGWZBDod-1661861385974)(C:\Users\86156\AppData\Roaming\Typora\typora-user-images\image-20220614223028995.png)]

必须生产之后才能消费,符合规律。

信号灯法
package com.qf.syn.thread.gaoji;
//测试生产者消费者问题2:信号灯法,标志位解决
public class TestPC2 {
    public static void main(String[] args) {
        TV tv = new TV();
        new Player(tv).start();
        new Watcher(tv).start();
    }
}

//生产者 -->演员
class Player extends Thread{
    //都需要"节目"
    TV tv;
    //构造方法
    public Player(TV tv){
        this.tv = tv;
    }

    @Override
    public void run() {
        for (int i = 0; i < 5; 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() {
        for (int i = 0; i < 5; 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(){
        if (flag){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("观看了:"+voice);
        //通知演员表演
        this.notifyAll();
        this.flag = !this.flag;
    }

}
结果:
演员表演了快乐大本营播放中
观看了:快乐大本营播放中
演员表演了抖音:记录美好生活
观看了:抖音:记录美好生活
演员表演了快乐大本营播放中
观看了:快乐大本营播放中
演员表演了抖音:记录美好生活
观看了:抖音:记录美好生活
演员表演了快乐大本营播放中
观看了:快乐大本营播放中
线程池

​ 由于经常创建和销毁重复性线程对性能影响很大,所以可以提前创建好多个线程,放入线程池中,要用就拿,用完放回去,理解成共享单车。

​ 好处:

  • 提高响应速度(减少了创建新县城的时间)
  • 降低资源消耗(重复利用线程池中的线程,不需要每次都创建)
  • 便于线程管理
    • 核心池的大小/能存放多少个线程
    • 最大线程数/能同时跑多少个线程
    • 线程没任务时最多保持多长时间终止
package com.qf.syn.thread.gaoji;

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

//测试线程池
public class TestPool {
    public static void main(String[] args) {
        //1.    创建服务 创建线程池
        //newFixedThreadPool():参数为线程池大小
        ExecutorService service = Executors.newFixedThreadPool(10);
        //2.    执行  把线程丢进去,execute可以执行Runnable接口的实现类
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());

        //3.    关闭链接
        service.shutdownNow();
    }
}

class MyThread implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}
结果:
pool-1-thread-1
pool-1-thread-3
pool-1-thread-2
pool-1-thread-4
总结

创建线程方法:

  • 1.继承Thread类
  • 2.实现Runnable接口
  • 3.实现Callable接口

线程通信问题

高级主题

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值