JavaSE-18【线程】

第一章 线程

1.1 进程与线程

  • 进程:
    • 是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,
    • 一个应用程序可以同时运行多个进程
    • 进程也是程序的一次执行过程,是系统运行程序的基本单位
    • 系统运行一个程序,都是一个进程从创建,运行到死亡的过曾
  • 线程
    • 线程是进程中的一个执行单元,负责当前进程中程序的执行
    • 一个进程中至少有一个线程,
    • 一个进程中也可以拥有多个线程,这个应用程序被称为多线程程序
  • 查看电脑进程:windows搜索任务管理器—打开任务管理器

线程调度

  • 分时调度

    所有的线程轮流占有CPU的使用权,平均分配每个线程占用的CPU的时间

  • 抢占式调度

    优先让优先级高的线程使用CPU,如果线程的优先级相同,那么随机选择一个去执行–线程的随机性

    Java使用的就是抢占式调度

1.2 并发与并行

  • 并发:是指两个或两个以上的事件在同一时间段内发生—同一时间段内发生
    • 单个CPU交替执行任务----速度慢
  • 并行:是指两个或两个以上的事件在同一时刻发生—同时发生
    • 多个CPU同时执行任务----速度快

在这里插入图片描述

1.3 Thread类

Thread类是的包类路径是:java.lang.Thread。其中有需要我们掌握的常用API如下:

构造方法:

  • 1、public Thread(); 分配一个新的线程对象
  • 2、public Thread(String name);分配一个指定名称的新的线程对象
  • 3、public Threaf(Runable target);分配一个带有指定目标的新的线程对象
  • 4、public Threaf(Runable target , String name);分配一个带有指定目标的新的指定名称的线程

常用方法:

  • 1、public String getName();获取当前线程的名称
  • 2、public void start();此线程开始执行,java虚拟机调用此线程的run方法
  • 3、public void run();此线程要执行的任务在此方法中定义
  • 4、public static void sleep(long millis);使当前正在执行的线程以指定的毫秒数暂停执行
  • 5、public static Thread currentThread();返回当前正在执行线程的引用

获取线程名称的两种方式案例:

public class YourThread extends Thread{
    /*
        获取线程的名称:
            1、使用Thread类中的方法getName();,返回当前正在执行线程的名称
            2、使用线程的currentThread();得到当前正在执行的线程
                再通过线程的getName();获取线程的名称
     */
    //重写Thread中的run方法,设置线程任务
    @Override
    public void run() {
        //方式1:
        String name = getName();
        System.out.println("方式1新线程的名称是:"+name);
        //方式2
        //1、获取当前正在执行的线程的引用
        Thread thread = Thread.currentThread();
        //2、获取当前正在执行线程的名称
        String threadName = thread.getName();
        System.out.println("方式2新线程的名称是:"+threadName);
    }
}
public class Demo02_ThreadName {
    public static void main(String[] args) {
        //1、创建Thread类的子类对象
        YourThread thread = new YourThread();
        //2、调用start()方法,开启新线程,执行run()方法
        thread.start();
        //3、获取主线程的名称
        System.out.println("主线程的名称是:"+Thread.currentThread().getName());//main
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Md7HRnM9-1692323226157)(photo/JavaSE17_线程.assest/1671716162117.png)]

  • sleep案例–模拟计时器

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ryIW0yC0-1692323226158)(photo/JavaSE17_线程.assest/1671716718345.png)]

public class Demo03_SleepMethod {
    /*
        线程的sleep()方法
            使当前正在执行的线程以指定的毫秒数暂停,毫秒结束之后,线程继续执行
            public static void sleep(long millis){}
     */
    public static void main(String[] args) {
        //模拟计时器
        for (int i = 1; i < 60; i++) {
            System.out.println(i);
            //使用Thread类的sleep方法让线程睡眠1秒钟
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

1.4 单线程程序–main(主)线程

  • 主线程:执行主方法main()的线程
  • 单线程程序:java程序中只有一个线程,执行从main()方法开始,从上到下一次执行
  • 单线程程序执行步骤:
    • 1、JVM执行main()方法,main方法进入栈内存
    • 2、JVM会找操作系统开辟一条main()方法通向CPU的执行路径
    • 3、CPU通过此路径来执行main方法,此路径被称为main(主)线程
    • 4、当主线程执行过程中出现异常,那么在异常之后的代码将不会被执行
public class Person {
    private String name;

    //定义成员方法
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(name + "----: " + i);
        }
    }

    public Person() {
    }

    public Person(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
public class Demo06_MainThread {
    /*
        主线程:执行主方法main()的线程
        单线程程序:
            java程序中只有一个线程,执行从main()方法开始,从上到下一次执行
        单线程程序执行步骤:
            1、JVM执行main()方法,main方法进入栈内存
            2、JVM会找操作系统开辟一条main()方法通向CPU的执行路径
            3、CPU通过此路径来执行main方法,此路径被称为main(主)线程
            4、当主线程执行过程中出现异常,那么在异常之后的代码将不会被执行
     */
    public static void main(String[] args) {
        //创建Person对象
        Person p1 = new Person("Tom");
        Person p2 = new Person("Anny");
        //调用方法--按照顺序执行---即单线程程序
        p1.run();
        //手动设定数学异常
        System.out.println(1/0);
        p2.run();
    }
}

单线程程序在无异常的顺序执行的情况

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sPMyftSu-1692323226158)(photo/JavaSE17_线程.assest/1672456494305.png)]

单线程程序在有异常的顺序执行的情况

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5RrVnsDE-1692323226158)(photo/JavaSE17_线程.assest/1672457290335.png)]

1.5 多线程原理

案例讲述多线程原理:

自定义线程类:
public class MyThread extends Thread {
    /*
        1、创建Thread线程的一个子类
        2、在其子类中重写run()方法,设置线程任务
     */
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("run线程"+i);
        }
    }
}
线程测试类:
public class Demo01_Thread {
    public static void main(String[] args) {
        //1、创建MyThread类的子类对象
        MyThread mt = new MyThread();
        //2、调用线程中的start()方法开启线程,执行线程中的run()方法
        mt.start();
        //3、主线程执行主方法中的代码
        for (int i = 0; i < 10; i++) {
            System.out.println("main主线程" + i);
        }
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-b95DZNG8-1692323226159)(photo/JavaSE17_线程.assest/1671710274322.png)]

执行原理图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jcAqbaQC-1692323226159)(photo/JavaSE17_线程.assest/1671712262560.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Kpk2RqfC-1692323226159)(photo/JavaSE17_线程.assest/1671712978155.png)]

1.6 多线程程序–方式一继承Thread

**创建线程类:**Java使用java.lang.Thread类代表线程,所有的线程对象都必须是Thread的子类或者子类的实例

每个线程的作用是完成一定的任务,实际上就是执行一段程序。Java使用线程执行体来代表这段程序,

Java通过集成Thread类来创建并启动多线程的步骤如下:

  • 1、定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程的执行体。
  • 2、创建Thread子类的实例,即创建了线程对象
  • 3、调用线程对象的start()方法,来启动该线程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3Ty2mKuw-1692323226160)(photo/JavaSE17_线程.assest/1672458974802.png)]

public class ThreadA extends Thread{
    /*
        1、创建Thread的子类
        2.重写Thread的run()方法,设置线程任务
     */
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("run线程执行"+i);
        }
    }
}
public class Demo07_MoreThread1 {
    /*
        创建多线程程序的方式1:继承Thread类,重写run()方法
        实现步骤:
            1、创建Thread类的子类
            2、在子类中重写run()方法,设置线程任务,即开启线程做什么
            3、创建子类对象
            4、子类对象调用start()方法,开启新的线程,执行run()方法
        start()方法的解释:
            1、void start()方法,是使该线程开始执行,Java虚拟机调用线程的run()方法
            2、结果是两个线程并发的运行:即当前线程(main线程)和另一个线程(新创建的线程,执行其run()方法)
            3、多次启动一个线程是非法的,特别是当线程已经结束执行后,不能再重新启动
        注:Java程序是抢占式调度,哪个线程的优先级高,就先执行哪个,同级别,则随机选择一个执行
     */
    public static void main(String[] args) {
        //3、创建Thread子类对象
        ThreadA ta = new ThreadA();
        //4、子类对象调用start()方法,开启新的线程,执行run()方法
        ta.start();

        //主线程执行
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() +" :"+ i);
        }
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1DfKSzTW-1692323226160)(photo/JavaSE17_线程.assest/1672459030717.png)]
在这里插入图片描述

1.7 多线程程序–方式二实现Runnalbe

创建线程的方式有两种,

  • 1、继承Thread类,重写run();方法,
  • 2、实现Runable接口,实现run();方法

方式一的继承方式已经在上面使用过,接下来使用第二种方式创建线程,采用了java.lang.Runable

实现Runable接口,重写run方法

  • 步骤:
    • 1、定义Runable接口的实现类,并重写该接口的run方法,该run方法的方法体同样是该线程的执行体
    • 2、创建Runable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread才是真正的线程对象
    • 3、调用线程对象的Thread方法来启动线程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VzBpwEzo-1692323226162)(photo/JavaSE17_线程.assest/1671717795991.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-me1waRkS-1692323226162)(photo/JavaSE17_线程.assest/1671717815935.png)]

  • 案例:
public class MyRunable implements Runnable{
    //1、创建接口实现类,并重写run方法
    @Override
    public void run() {
        //在run方法中设置线程任务
        for (int i = 1; i < 10; i++) {
            //获取当前正在执行线程的名称
            System.out.println(Thread.currentThread().getName()+i);
        }
    }
}
public class Demo04_Runable {
    /*
    实现Runable接口,创建线程对象
        public interface Runnable
            Runnable接口应由任何类实现,其实例将由线程执行。
            该类必须定义一个无参数的方法,称为run 。
        java.lang.Thread的构造方法,可以传递Runable实现类的对象作为参数
            public Thread(Runnable target)
            分配一个新的 Thread对象。
            public Thread(Runnable target, String name)
            分配一个新的 Thread对象。
         实现步骤:
            1、创建一个Runable接口的实现类
            2、在实现类中重写Runable接口总的run方法,设置线程任务
            3、创建一个Runable接口的实现类对象
            4、创建Thread类的对象,构造方法中传递Runable接口的实现类对象
            5、调用Thread类中的start()方法,开启新的线程,执行run方法
     */
    public static void main(String[] args) {
        //3、创建一个Runable接口的实现类对象
        MyRunable runable = new MyRunable();
        //4、创建Thread类的对象,构造方法中传递Runable接口的实现类对象
        Thread thread = new Thread(runable);
        //5、调用Thread类中的start()方法,开启新的线程,执行run方法
        thread.start();

        for (int i = 1; i < 10; i++) {
            System.out.println(Thread.currentThread().getName()+i);
        }
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z1i9MQAW-1692323226162)(photo/JavaSE17_线程.assest/1671717857326.png)]

1.8 Thread和Runable的区别

继承Thread类,创建线程对象,不适合资源共享,因为java是单继承的

实现Runable接口,重写run方法,适合资源共享,因为接口可以多实现

实现Runable接口比继承Thread类的优势:

  • 1、适合多个相同的程序代码的线程去共享同一个资源
  • 2、可以便面java中单继承的局限性
  • 3、增加程序的健壮性,增加解耦操作,代码可以被多个线程共享,代码和线程独立
  • 4、线程池只能放入事先Runable或者Callable类的线程,不能直接放入继承Thread类的线程

在java中,每次程序运行至少要启动两个线程,一个是main线程,另一个是垃圾回收线程,因为每次使用java执行一个类的时候,实际上就会启动一个JVM,每一个JVM其实就是在操作系统中启动了一个进程。

1.9 匿名内部类方式实现线程的创建

使用线程的匿名内部类方式,可以方便的实现每个线程执行不同的任务操作

使用匿名内部类实现Runable接口,重写Runable接口中的额run方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bn8zBUx3-1692323226163)(photo/JavaSE17_线程.assest/1671779951244.png)]

public class Deomo05_InnerClassThread {
    /*
        匿名内部类实现线程的创建
            1、子类继承父类,重写父类的方法,创建子类对象,一步完成
            2、实现类实现接口,重写接口中的方法,创建实现类对象,一步完成
        匿名内部内的最终产物是:子类/实现类对象,并且这个类没有名字
        语法格式:
            new 父类/接口(){
                重写父类/接口中的方法
            }
     */
    public static void main(String[] args) {
        //方式1:父类Thread
        new Thread() {
            //重写run方法
            @Override
            public void run() {
                for (int i = 0; i < 5; i++) {
                    System.out.println("新线程1启动执行:"+Thread.currentThread().getName()+"---"+i);
                }
            }
        }.start();

        //方式2:接口Runnable
        Runnable runnable =  new Runnable(){
            //重写run方法
            @Override
            public void run() {
                for (int i = 0; i < 5; i++) {
                    System.out.println("新线程2启动执行:"+Thread.currentThread().getName()+"---"+i);
                }
            }
        };
        //启动线程
        new Thread(runnable).start();

        //方式2:接口Runnable简化写法
        new Thread(
                new Runnable(){
                    //重写run方法
                    @Override
                    public void run() {
                        for (int i = 0; i < 5; i++) {
                            System.out.println("新线程2启动执行:"+Thread.currentThread().getName()+"---"+i);
                        }
                    }
                }
        ).start();
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Xtj5AZOO-1692323226163)(photo/JavaSE17_线程.assest/1671780028498.png)]

第二章 线程安全

2.1 线程安全

如果有多个线程同时运行,而这些线程可能会同时运行这些代码,程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值和预期的也是一样的,就是线程安全的。

我们通过一个案例,演示线程的安全问题

模拟电影院售票过程,假设本次播放电影的放映室有100个座位,也就是本场次电影能卖100张票

  • 模拟电影售票窗口,多个窗口同时卖票,总票数是100张,

  • 售票窗口:使用线程对象来模拟

  • 售票数:使用Runnable接口的实现类来模拟

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dO9NaWVj-1692323226164)(photo/JavaSE17_线程.assest/1672468759123.png)]

售票案例之线程不安全:

package saleticket;

/*
    实现售票案例
 */
public class RunnableImpl implements Runnable {
    //定义一个多线程共享的票数
    private int ticket = 100;

    //设置线程任务:卖票
    @Override
    public void run() {
        //设置死循环,一直卖票
        while (true) {
            //判断票号是否存在
            if (ticket > 0) {
                try {
                    //提高安全问题出现的概率,让程序睡眠
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //当前正在执行的线程的名称
                String name = Thread.currentThread().getName();
                //票存在,进行售卖
                System.out.println(name + "正在卖第" + ticket + "张票");
                //售出之后,总票数-1
                ticket--;
            }
        }
    }
}
package saleticket;

/**
 * 模拟卖票案例:创建3个线程,同时开启对共享的票进行出售
 */
public class Demo01_Ticket {
    public static void main(String[] args) {
        //创建接口实现类对象
        RunnableImpl run = new RunnableImpl();
        //构造方法创建线程传递实现类对象
        Thread t1 = new Thread(run);
        Thread t2 = new Thread(run);
        Thread t3 = new Thread(run);
        t1.start();
        t2.start();
        t3.start();
    }
}

运行结果,出现了线程安全问题,:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qZISWjga-1692323226164)(photo/JavaSE17_线程.assest/1672474914438.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7UcaSFCT-1692323226164)(photo/JavaSE17_线程.assest/1672474952263.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3DaJBwWA-1692323226165)(photo/JavaSE17_线程.assest/1672475046598.png)]

售票线程安全问题出现的原理分析图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ghkgeA5Q-1692323226165)(photo/JavaSE17_线程.assest/1672475426809.png)]

售票线程安全问题的解决方案:

让1个线程在访问共享数据的时候,无论是否失去了CPU的执行权,都让其他的线程进行等待,直到当前线程卖完票,代码执行完毕,其他线程再进行卖票,保证始终只有一个线程在共享数据中取票。

2.2 线程同步

但我们使用多个线程访问同一个资源的时候,且多个线程多共享资源有写的操作,就容易出现线程安全问题,

需要解决上述多线程访问同一资源的安全性问题,也就是解决重复票与不存在票的问题。

在java中提供了同步机制 synchronized 来解决

根据案例简述:

  • 1、窗口1的线程进入操作的时候,窗口2和窗口3线程只能在外等候,窗口1结束,窗口2和窗口3才有机会执行
  • 2、也就是在某个线程在修改共享资源的时候,其他线程不能修改该资源,等待执行的线程修改完毕之后,才能去抢夺CPU的执行权,完成对资源的操作,保证了数据的同步性,解决了线程不安全的问题。
  • 3、为了保证每个线程都能正常执行原子操作,java引入了线程同步机制
    • 同步代码块
    • 同步方法
    • 锁机制

2.3 同步代码块

  • 同步代码块

    • synchronized 关键字可以用于方法的某个区域中,表示只对这个区域的资源实行互斥访问
    • 格式:
    synchronized(同步数){
        需要同步操作的代码
    }
    
  • 同步锁

    • 对象的同步锁只是一个概念,可以想象为对象标记了一个锁
    • 1、锁对象可以是任意类型
    • 2、多个线程对象要使用同一把锁
    • 3、在任何时候,最多允许一个线程拥有同步锁,谁拿到锁,就进入代码块,其他的线程只能在外等待
    • 4、锁对象的作用:把同步代码块锁住,只让一个线程在同步代码块中中性
  • 使用同步代码块解决售票线程安全问题:

package synchronizedticket;

/*
    解决售票案例卖出重复的票和不存在的票的线程安全问题
    解决方案1:
        使用同步代码块
    格式:
        synchronized(锁对象){
            可能出现线程安全问题的代码;
        }
 */
public class RunnableImpl implements Runnable {
    //定义一个多线程共享的票数
    private int ticket = 100;

    //创建一个锁对象
    Object object = new Object();

    //设置线程任务:卖票
    @Override
    public void run() {
        //设置死循环,一直卖票
        while (true) {
            //创建同步代码块
            synchronized (object) {
                //判断票号是否存在
                if (ticket > 0) {
                    try {
                        //提高安全问题出现的概率,让程序睡眠
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //当前正在执行的线程的名称
                    String name = Thread.currentThread().getName();
                    //票存在,进行售卖
                    System.out.println(name + "正在卖第" + ticket + "张票");
                    //售出之后,总票数-1
                    ticket--;
                }
            }
        }
    }
}

运行结果:不会出现相同票和不存在票的现象

同步的原理分析:

  • 1、使用了一个锁对象,这个锁对象就叫做同步锁,也叫对象锁,也叫作对象监视器
  • 2、3个线程一起抢夺CPU的执行权,谁抢到了,谁执行main方法进行售票操作
  • 3、当t1抢到了CPU的执行权,执行run方法,遇到synchronized代码块。这时t1会检查synchronized代码里是否有锁对象,发现有,就会获取到锁对象,进入到同步代码块中执行,
  • 4、t2抢到了CPU的执行权,执行run方法,遇到synchronized代码块,这时t2会检查synchronized代码块是否有锁对象,发现没有,t2就进入到阻塞状态,会一直等待t1线程归还锁对象,一直到t1线程执行完同步代码块中的代码,才会将锁对象归还给同步代码块,t2才能获取到锁对象,进入到同步代码块中执行
  • 5、总结:同步中的线程在没有执行完毕代码前不会释放锁对象,同步代码块外的线程没有锁对象不能去同步,同步保证了只能有一个线程在同步执行中共享数据,保证了安全性,但是频繁的判断锁,获取锁,释放锁,程序的效率会降低。

2.4 同步方法

  • 同步方法:使用synchronized关键字修饰的方法,就叫做同步方法,即保证A线程在执行该方法的时候,其他线程只能在方法外等待。

  • 语法格式:

    public synchronized 返回值类型 方法名(参数列表){
        可能产生线程安全问题的代码;
    }
    
  • 同步方法的原理

    • 1、同步方法会将方法内部的代码锁住
    • 2、只让一个线程执行,其余线程等待
    • 3、同步方法的锁对象
      • 对于普通方法(非static)方法而言,其同步锁就是this,即接口的实现类对象
      • 对于static方法,同步锁就是当前方法的调用类的字节码对象(类名.class)
  • 同步锁的问题

    • 1、对于普通方法(非static)方法而言,其同步锁就是this
    • 2、对于static方法,同步锁就是当前方法的调用类的字节码对象(类名.class)
    • 3、注意:同步方法中的锁对象this,其实也就是同步代码块中的锁对象Object,即this
  • 案例:

package synchronizedmethod;

/*
    实现售票案例,同步方法解决线程安全问题

    使用同步方法解决线程安全问题的步骤:
       1、抽取出访问了共享数据的代码,放在一个方法中
       2、在方法上条件synchronized修饰符
 */
public class RunnableImpl implements Runnable {
    //定义一个多线程共享的票数
    private int ticket = 100;

    //设置线程任务:卖票
    @Override
    public void run() {
        //验证锁对象
        System.out.println("锁对象是" + this);
        //设置死循环,一直卖票
        while (true) {
            //调用方法
            saleTicket();
        }
    }

    /*
        定义一个同步方法,将访问共享数据的代码放在其中
        原理:
            1、同步方法会将方法内部的代码锁住
            2、只让一个线程执行
            3、同步方法的锁对象
                对于普通方法(非static)方法而言,其同步锁就是this,即接口的实现类对象
                对于static方法,同步锁就是当前方法的调用类的字节码对象(类名.class)
     */
    //同步方法实现解决线程安全问题
    public synchronized void saleTicket() {
        //判断票号是否存在
        if (ticket > 0) {
            try {
                //提高安全问题出现的概率,让程序睡眠
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //当前正在执行的线程的名称
            String name = Thread.currentThread().getName();
            //票存在,进行售卖
            System.out.println(name + "正在卖第" + ticket + "张票");
            //售出之后,总票数-1
            ticket--;
        }
    }
    //同步代码块实现解决线程安全问题:
    /*public void saleTicket() {
        synchronized (this) {//this可以是代替单独创建的Object对象,
            //判断票号是否存在
            if (ticket > 0) {
                try {
                    //提高安全问题出现的概率,让程序睡眠
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //当前正在执行的线程的名称
                String name = Thread.currentThread().getName();
                //票存在,进行售卖
                System.out.println(name + "正在卖第" + ticket + "张票");
                //售出之后,总票数-1
                ticket--;
            }
        }
    }*/
}
package synchronizedmethod;

/**
 * 模拟卖票案例:
 *      创建3个线程,同时开启对共享的票进行出售
 *      使用同步方法解决线程安全问题
 */
public class Demo01_Ticket {
    public static void main(String[] args) {
        //创建接口实现类对象
        RunnableImpl run = new RunnableImpl();
        System.out.println("this就是锁对象"+run);
        //构造方法创建线程传递实现类对象
        Thread t1 = new Thread(run);
        Thread t2 = new Thread(run);
        Thread t3 = new Thread(run);
        t1.start();
        t2.start();
        t3.start();
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SzAfZmeP-1692323226166)(photo/JavaSE17_线程.assest/1672730244581.png)]

2.5 静态同步方法

  • 使用静态同步方法实现解决线程安全问题
  • 对于static方法,同步锁就是当前方法的调用类的字节码对象(类名.class)
  • 注:
    • this可以是接口的字节码对象或者是接口实现类的字节码对象
    • 静态同步方法无法看出this是谁,可以变成静态方法中同步同步代码块来看到this
package staticsynchronizedmethod;

/*
    实现售票案例,同步静态方法解决线程安全问题

    使用静态同步方法解决线程安全问题的步骤:
       1、抽取出访问了共享数据的代码,放在一个方法中
       2、在方法上static和synchronized修饰符
       3、此时的锁对象是:当前方法的调用类的字节码对象(类名.class)
 */
public class RunnableImpl implements Runnable {
    //定义一个多线程共享的票数
    private static int ticket = 100;

    //设置线程任务:卖票
    @Override
    public void run() {
        //设置死循环,一直卖票
        while (true) {
            //调用方法
            saleTicketStatic();
        }
    }

    /*
        定义一个同步方法,将访问共享数据的代码放在其中
        原理:
            1、同步方法会将方法内部的代码锁住
            2、只让一个线程执行
            3、同步方法的锁对象
                对于普通方法(非static)方法而言,其同步锁就是this,即接口的实现类对象
                对于static方法,同步锁就是当前方法的调用类的字节码对象(类名.class)
     */
    //静态同步方法实现解决线程安全问题
   /* public static synchronized void saleTicketStatic() {
        //判断票号是否存在
        if (ticket > 0) {
            try {
                //提高安全问题出现的概率,让程序睡眠
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //当前正在执行的线程的名称
            String name = Thread.currentThread().getName();
            //票存在,进行售卖
            System.out.println(name + "正在卖第" + ticket + "张票");
            //售出之后,总票数-1
            ticket--;
        }
    }*/

    //上述无法输出this,使用同步代码块实现
    public static void saleTicketStatic() {
        synchronized (RunnableImpl.class) {
            //static修饰方法,此时的this不是对象本身,是方法调用者的类的字节码对象、或者父类接口的字节码对象
            //判断票号是否存在
            if (ticket > 0) {
                try {
                    //提高安全问题出现的概率,让程序睡眠
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //当前正在执行的线程的名称
                String name = Thread.currentThread().getName();
                //票存在,进行售卖
                System.out.println(name + "正在卖第" + ticket + "张票");
                //售出之后,总票数-1
                ticket--;
            }
        }
    }
}
package staticsynchronizedmethod;

/**
 * 模拟卖票案例:
 *      创建3个线程,同时开启对共享的票进行出售
 *      使用同步静态方法解决线程安全问题
 */
public class Demo01_Ticket {
    public static void main(String[] args) {
        //创建接口实现类对象
        RunnableImpl run = new RunnableImpl();
        //构造方法创建线程传递实现类对象
        Thread t1 = new Thread(run);
        Thread t2 = new Thread(run);
        Thread t3 = new Thread(run);
        t1.start();
        t2.start();
        t3.start();
    }
}

2.6 Lock锁

java.util.concurrent.locks。Lock机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,

同步代码块和同步方法所具有的功能,Lock锁都有,并且其功能更强大,更能体现面向对象

Lock锁也称同步锁,加锁和释放锁的方法如下:

  • public void lock(); 加同步锁
  • public void unlock(); 释放同步锁

Lock接口的实现类:

import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLock implements Lock, java.io.Serializable {
	类成员;
}

使用步骤:

  • 1、在类的成员位置创建一个ReentrantLock对象
  • 2、在可能出现安全问题的代码前调用Lock接口中的方法,lock添加锁
  • 3、在可能出现安全问题的代码后调用unLock接口中的方法,lock释放锁

注意:

  • 一般情况unlock方法会放在finally代码块中,无论程序是否异常,都会释放锁

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AfVqAGaa-1692323226167)(photo/JavaSE17_线程.assest/1672990438326.png)]

Lock锁解决售票案例的线程安全问题:

package lock;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/*
    售票案例出现了线程安全问题,出现课不存在的票和重复的票

    解决线程安全问题的第三种方式,使用Lock锁
    Lock接口中的方法
        1、添加锁:public void lock();
        2、释放锁:public void unlock();
    Lock接口的实现类:
        java.util.concurrent.locks.ReentrantLock implements Lock接口
    使用步骤:
        1、在成员位置创建一个ReentrantLock对象
        2、在可能出现安全问题的代码前调用Lock接口中的方法,lock添加锁
        3、在可能出现安全问题的代码后调用unLock接口中的方法,lock释放锁
 */
public class RunnableImpl implements Runnable {
    //定义一个多线程共享的票数
    private int ticket = 100;

    //1、在成员位置创建Lock接口实现类对象
    Lock lock = new ReentrantLock();

    //设置线程任务:卖票
    @Override
    public void run() {
        //设置死循环,一直卖票
        while (true) {
            //2、在可能出现安全问题的代码前调用Lock接口中的方法,lock添加锁
            lock.lock();
            //判断票号是否存在
            if (ticket > 0) {
                try {
                    //提高安全问题出现的概率,让程序睡眠
                    Thread.sleep(10);
                    //当前正在执行的线程的名称
                    String name = Thread.currentThread().getName();
                    //票存在,进行售卖
                    System.out.println(name + "正在卖第" + ticket + "张票");
                    //售出之后,总票数-1
                    ticket--;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    //3、在可能出现安全问题的代码后调用unLock接口中的方法,lock释放锁
                    lock.unlock();//无论程序是否异常,都会释放锁
                }
            }
        }
    }
}
package lock;

/**
 * 模拟卖票案例:
 *  创建3个线程,同时开启对共享的票进行出售
 *  线程不安全问题出现。
 */
public class Demo01_Ticket {
    public static void main(String[] args) {
        //创建接口实现类对象
        RunnableImpl run = new RunnableImpl();
        //构造方法创建线程传递实现类对象
        Thread t1 = new Thread(run);
        Thread t2 = new Thread(run);
        Thread t3 = new Thread(run);
        t1.start();
        t2.start();
        t3.start();
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gcudFgFU-1692323226167)(photo/JavaSE17_线程.assest/1672990537506.png)]

第三章 等待唤醒机制

3.1 线程间通信

概念:多个线程在处理同一个资源,但是线程的任务不同。

如:线程A是司机开公交车,线程B是乘客乘坐公交车,公交车可以理解成同一个资源,线程A和线程B的动作,一个是生产,一个是消费,那么线 程A和线程B之间就存在线程通信问题。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4Mumd44u-1692323226167)(photo/JavaSE17_线程.assest/1672988756984.png)]

为什么需要处理线程通信?

多个线程并发执行的时候,在默认情况下CPU是需要随机切换线程的,当需要多个线程来共同完成一个任务,并且希望它们有规律的执行,那么多线程之间就需要协调通信,以此来完成多线程共同操作一份数据。

如何保证线程之间通信有效利用资源?

多个线程在处理同一个资源,并且任务不同时,需要线程通信来解决线程之间对同一个变量的使用或操作,就是多个线程在操作同一份数据时,避免对同一共享变量的争夺,也就是需要通过一定的手段使得各个线程能够有效的利用资源,这种手段叫做----等待线程唤醒机制。

3.2 等待唤醒机制

什么是等待线程唤醒机制?

这是多个线程之间的协作机制,说到线程想到的就是线程之间的竞争,比如线程去争夺锁对象,但这不是线程的全部,不仅有竞争也有协作,线程之间也会有协作机制、

线程等待机制,就是线程在进行了规定操作后,就进入等待机制wait()。等待其它线程执行完自身的代码后,再将其唤醒notify(),在有多个线程进行等待时,如果需要,可以使用notifyAll(),来唤醒所有等待的线程。

wait();和notify()/notifyAll();是线程的一种协作机制

等待唤醒机制就相当于解决线程间通信的问题,使用到的三个方法的功能如下:

  • 1、wait:线程不再活动,不再参与调度,进入wait set中,因此不会消费CPU的资源,也不会去竞争锁,这时的线程状态就是waitting,它还要等的别的线程执行一个操作,也就是通知notify()在这个对象上等待的线程从wait set中释放出来,重新进入到调度队列中。
  • 2、notify:选取所通知对象的wait set中的一个线程释放,
  • 3、notifyAll:释放所通知对象的wait set 上的全部线程

注意事项:

哪怕只通知了一个等待的线程,被通知的线程也不能立即恢复执行,因为当初中断的地方在同步块中,而此刻它已经不再持有锁,所以会再次尝试去获取锁,可能面临其它线程的竞争,成功之后才能在当初调用wait方法之后的地方进行执行。

总结:

  • 1、如果能获取锁,线程就从waitting状态编程runnable状态
  • 2、否则,从wait set 出来,又进入entry set ,线程就从waitting状态变成blocked装填

调用wait和notify方法需要注意的细节:

  • 1、wait方法和notify方法必须由同一个锁对象调用,因为,对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程
  • 2、wait方法与notify方法是属于Object类的方法,因为,锁对象可以是任意对象,而任意对象的所属类都是继承了Object类。
  • 3、wait方法和notify方法必须要在同步代码块或者是同步方法中使用,因为,必须要通过锁对象调用这两个方法。

3.3 生产者与消费者问题

等待唤醒机制其实就是经典的生产者和消费者的问题。

举例实现等待唤醒机制如何实现有效利用资源:

司机生产座位,乘客消费座位,当座位没有时,座位的状态为false,乘客线程等待,司机线程生产座位,产出座位,此刻座位的状态为true,并通知乘客线程,解除乘客线程的等待状态,因为已经有了座位,那么司机线程进入等待状态,紧接着,乘客线程能否进一步执行取决于锁对象的获取状况,如果乘客获得到锁,那么就执行乘车的动作,座位用完,座位状态为false,并通知司机线程解除生产座位的等待状态.

通信:对座位状态进行判断

  • 1、没有座位->乘客线程唤醒司机线程->乘客线程等待->司机线程生产座位->产出座位->修改座位状态为有true
  • 2、有座位–>司机线程唤醒乘客线程–>司机线程等待–>乘客使用座位–>座位用完–>修改座位状态为没有false
  • 3、没有座位->乘客线程唤醒司机线程->乘客线程等待->司机线程生产座位->产出座位->修改座位状态为有true

案例

package waitnotify;

/**
 *  资源座位类:设置座位的属性
 */
public class ZuoWei {
    //设置是否有座位:true有,false没有,初始值是false没有
    boolean flag = false;
}
package waitnotify;

/**
 * 司机类:生产者生产座位
 * 1、是一个线程类,继承Thread
 * 2、对座位的状态进行判断
 * true:有座位,司机线程进入wait等待状态
 * false:没有座位,司机线程生产座位,
 * 3、座位生产完毕,修改座位的状态为true
 * 4、唤醒乘客线程,让乘客使用座位
 * <p>
 * 注意事项:
 * 1、司机线程和乘客线程-->通信互斥
 * 2、必须同时同步,保证两个线程只有一个在执行
 * 3、锁对象必须保证唯一,可以使用座位对象座位锁对象
 * 4、司机类和乘客类就需要把座位对象座位参数传递
 * a、在类的成员位置创建座位类的变量
 * b、使用有参构造为座位变量赋值
 */
public class Driver extends Thread {
    //1、在成员位置定义座位变量
    private ZuoWei zw;

    //2、有参数构造方法,为座位变量赋值
    public Driver(ZuoWei zw) {
        this.zw = zw;
    }

    //3、设置线程任务run()生产作为
    @Override
    public void run() {
        //定义作为数量变量
        int count = 0;
        while (true) {//死循环,让司机一直生产座位
            //4、必须同时同步,保证两个线程只有一个在执行,将座位对象作为锁对象
            synchronized (zw) {
                //5、对作为的状态进行判断
                if (zw.flag == true) {
                    //6、true:有座位,司机线程进入wait等待状态
                    try {
                        zw.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                //被唤醒之后执行的代码,生产座位
                //7、false:没有座位,司机线程被唤醒,生产座位,
                if (count % 2 == 0)
                    count++;
                System.out.println("司机正在生产座位");
                //8、生产座位需要3秒钟
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //9、司机线程生产好作为,修改座位的状态为false
                zw.flag = true;
                //10、唤醒乘客线程,使用座位
                zw.notify();
                System.out.println("司机完成座位生产");
            }
        }
    }
}
package waitnotify;

/**
 * 乘客类:消费者消费座位
 *  1、设置线程run()的任务是使用座位
 *  2、对座位的状态进行判断
 *      false:没有座位,乘客调用wait方法进入等待状态
 *      true:有座位,乘客使用座位,作为被用完,修改作为的状态为没有false
 *  3、乘客唤醒司机线程,生产座位
 */
public class ChengKe extends Thread{
    //1、在成员位置创建座位变量
    private ZuoWei zw;
    //2、生成带参数构造方法,并为此座位变量赋值
    public ChengKe(ZuoWei zw) {
        this.zw = zw;
    }
    //3、设置线程的任务
    @Override
    public void run() {
        //设置死循环,让乘客一直使用座位
        while (true){
            //必须同时同步,保证两个线程只有一个在执行
            synchronized (zw){
                //对座位的状态进行判断
                if (zw.flag==false){
                    //乘客调用wait方法进入等待状态
                    try {
                        zw.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                //被唤醒之后执行的代码,使用座位
                System.out.println("乘客正在使用座位");
                //乘客使用完座位。修改座位的状态为false
                zw.flag = false;
                //乘客唤醒司机线程生产座位
                zw.notify();
                System.out.println("乘客使用完毕座位");
                System.out.println("------------------------------");
            }
        }
    }
}
package waitnotify;

/**
 * 测试类:
 *  1、创建座位对象
 *  2、创建司机线程,开启,生产座位
 *  3、创建乘客线程,开启,使用座位
 */
public class TestZW {
    public static void main(String[] args) {
        //1、创建座位对象
        ZuoWei zw = new ZuoWei();
        //2、创建司机线程,开启,生产座位
        Driver driver = new Driver(zw);
        driver.start();
        //3、创建乘客线程,开启,使用座位
        ChengKe chengKe = new ChengKe(zw);
        chengKe.start();
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EFpMZas6-1692323226168)(photo/JavaSE17_线程.assest/1673236459352.png)]

第四章 线程状态

4.1 线程状态概述

当线程被创建并启动以后,其不是一启动就进入了执行的状态,也不是一直处于执行状态,在线程的声明周期中,

有以下六种状态:

线程状态导致状态发生的条件
New 新建线程刚被创建,但是并未启动,还没有调用run方法
Runnable 可运行线程可以在java虚拟机中运行的状态,可能正在运行自己的代码,也可能没有,取决于操作系统处理器
Blocked 锁阻塞当一个线程试图获取一个对象锁,而该对象锁被其他线程持有,则该线程进入Blocked状态,当该线程持有锁时,该线程状态将变成Runnable状态
Waitting 无限等待一个线程在等待另一个线程执行唤醒动作时,该线程进入Waitting状态,进入这个状态之后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能唤醒
Timed Waitting 计时等待同Wattting状态,有几个方法有超时参数,调用他们将进入Timed Watting状态,这一状态将一直保持到朝时期满或者接收到唤醒通知,带有超时参数的常用方法有Thread.sleep(); Object.wait();
Terminated 退出线程的状态已退出的线程处于这个状态,也就是死亡状态,即因run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lTgXtnu5-1692323226168)(photo/JavaSE17_线程.assest/u=1803265840,2355052716&fm=253&fmt=auto&app=138&f=PNG.png)]

面试题:

  • 1、sleep和wait方法的区别:

    • 1、sleep()是[Thread类中的方法,而wait()则是Object类中的方法。
    • 2、 sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。
    • 3、 使用范围:wait、notify和notifyAll只能在同步控制方法或者同步控制块里面使用,一旦一个对象调用了wait方法,必须要采用notify()和notifyAll()方法唤醒该进程;而sleep可以在任何地方使用。
    • 4、 相同点:sleep必须捕获异常,wait,notify和notifyAll同样需要捕获异常
    • 5、 sleep()方法导致了程序暂停(线程进入睡眠状态),但是他的监控状态依然保持着,当指定的时间到了又会自动恢复到 可运行状态。在调用sleep()方法的过程中,线程不会释放对象锁
    • 6、wait()方法会导致线程放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备获取对象锁进入可运行状态。即wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。
  • 2、yield和join的方法的区别

    • 1、yield()方法:暂停当前正在执行的线程对象
    • 2、yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。
    • 3、yield()只能使同优先级或更高优先级的线程有执行的机会。
    • 4、调用yield方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新获取CPU执行时间,这一点是和sleep方法不一样的。
    • 5、join()方法: 等待该线程终止
    • 6、等待调用join方法的线程结束,再继续执行。如:t.join();//主要用于等待t线程运行结束,若无此句,main则会执行完毕,导致结果不可预测。
    • 7、在很多情况下,主线程创建并启动了线程,如果子线程中要进行大量耗时运算,主线程往往将早于子线程结束之前结束。这时,如果主线程想等待子线程执行完成之后再结束,比如子线程处理一个数据,主线程要取得这个数据中的值,就要用到join()方法了。方法join()的作用是等待线程对象销毁。
  • 3、notify和notifyAll方法的区别

    • 1、 notify()和notifyAll()都是用来用来唤醒调用wait()方法进入等待锁资源队列的线程,区别在于:
    • 2、 notify()唤醒正在等待此对象监视器的单个线程。 如果有多个线程在等待,则选择其中一个随机唤醒(由调度器决定),唤醒的线程享有公平竞争资源的权利
    • 3、 notifyAll()唤醒正在等待此对象监视器的所有线程,唤醒的所有线程公平竞争资源

4.2 线程计时等待 (Timed Waiting)

Timed Waitting的定义:一个正在限时等待另一个线程执行一个唤醒动作的线程处于这一状态

类似于在售票案例中,为了避免线程执行太快,售票不明显等问题,在run方法中条件了sleep语句,这样就强制当前正在执行的线程休眠,即暂停执行,以减慢线程

其实当调用了sleep方法之后,当前执行的线程就进入了休眠状态,其实就是所谓的Timed Waitting计时等待。

**案例:**实现计数器,计数到100,每个数字之间间隔1秒。隔10个数字输出一个字符串

package timedwaitting;

public class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            if (i % 10 == 0) {
                System.out.println("----------" + i);
            }
            System.out.println(i);
            try {
                //调用线程的sleep方法让程序睡眠1秒钟
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
package timedwaitting;

public class TestSleep {
    public static void main(String[] args) {
        //创建线程
        MyThread myThread = new MyThread();
        //开启线程
        myThread.start();
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D1nYP1nE-1692323226168)(photo/JavaSE17_线程.assest/1673250092775.png)]

通过案例可以发现,sleep方法的使用注意事项

  • 1、进入Timed Waitting状态的一种常见的情形就是调用sleep方法,单独的线程也可以调用,不一定非要有协作的关系
  • 2、为了让其他线程有执行机会,可以将Timed Waitting的调用放入到线程run方法内,这样才能保证该线程在执行过程中会睡眠
  • 3、sleep与锁无关,线程睡眠到期自动苏醒,并返回到Runnable可运行状态
  • 4、sleep中指定的时间是线程不会运行的最短时间,因此,sleep方法不能保证该线程睡眠到期后及时苏醒并执行

Timed Watting 线程状态图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AU2NJ3mF-1692323226169)(photo/JavaSE17_线程.assest/1673250832547.png)]

4.3 锁阻塞Blocked

Blocked状态的定义:一个正在阻塞等待一个监视器锁(锁对象)的线程处于这一状态。

在同步机制中,线程A和线程B在使用同一个锁,如果线程A获得到锁,线程A就进入Runnable(可运行)状态。那么线程B就进入了Blocked阻塞状态。

这是由于Runnable的状态进入Blocked状态,除此Timed Waitting状态也会在某种情况下进入阻塞状态。

Blocked阻塞状态图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tZP1XfeL-1692323226169)(photo/JavaSE17_线程.assest/1673251513377.png)]

4.4 线程无线等待(Waiting)

Waittting状态的定义:一个正在无限期等待另一个线程执行一个特别的唤醒动作的线程处于这一状态

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mDZqeVgO-1692323226169)(photo/JavaSE17_线程.assest/1673253148844.png)]

案例:

package waitandnotify;

import javafx.beans.binding.When;

/**
 * 等待线程唤醒:线程通信案例
 * 1、创建一个乘客线程(消费者),告知司机需要座位的数量,调用wait方法,摒弃CPU的执行,进入到Waitting状态
 * 2、创建一个司机线程(生产者),花了3秒生产座位,座位生产完毕后,调用notify方法,唤醒乘客线程使用座位
 * 3、注意:乘客和司机线程必须使用同步代码块包裹起来,保证等待和唤醒只有一个在执行
 * 同步使用的锁对象必须保证唯一
 * 只有锁对象才能调用wait()和notify()方法
 * 4、方法解析
 * void wait();在其他线程调用此对象的notify()或notifyAll()方法之前,导致当前线程等待
 * void notify();唤醒在此对象监视器上等待的单个线程,或继续执行wait()方法之后的代码
 */
public class WaitAndNotify {
    public static void main(String[] args) {
        //创建锁对象,保证唯一,为什么使用Object,因为Object是所有类的超类(多态思维)
        Object object = new Object();
        //创建一个乘客线程--消费者
        new Thread() {
            @Override
            public void run() {
                //设置死循环,让乘客一直等待使用座位
                while (true) {
                    //客和司机线程必须使用同步代码块包裹起来,保证等待和唤醒只有一个在执行
                    synchronized (object) {
                        System.out.println("乘客告知司机需要座位的数量");
                        //调用wait方法,摒弃CPU的执行,进入到Waitting状态
                        try {
                            object.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        //唤醒之后执行的代码
                        System.out.println("座位已经生产完成了,咱们可以使用了");
                        System.out.println("----------------------------");
                    }
                }
            }
        }.start();

        //创建一个司机线程--生产者
        new Thread() {
            @Override
            public void run() {
                //设置死循环,让司机一直生产座位
                while (true) {

                    //花了3秒生产座位
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //客和司机线程必须使用同步代码块包裹起来,保证等待和唤醒只有一个在执行
                    synchronized (object) {
                        System.out.println("司机3秒之后生产完座位,通知乘客使用");
                        //座位生产完毕后,调用notify方法,唤醒乘客线程使用座位
                        object.notify();
                    }
                }
            }
        }.start();
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vV3Bbfnx-1692323226170)(photo/JavaSE17_线程.assest/1673254479388.png)]

进入到Timed Waitting状态的两种方式

方式一:

  • 1、使用Thread.sleep(long time)方法,在毫秒值时间结束之后,线程进入到Runnable/Blocked状态*

方式二:

  • 2、使用wait(long time)方法,wait方法在毫秒值时间结束之后,还没有被notify唤醒,就会自动醒来,线程进入到Runnable/Blocked状态
package waitandnotify;

/**
 * 进入到TimedWaitting状态的两种方式
 * 1、使用Thread.sleep(long time)方法,在毫秒值时间结束之后,
 *      线程进入到Runnable/Blocked状态
 * 2、使用wait(long time)方法,wait方法在毫秒值时间结束之后,
 *      还没有被notify唤醒,就会自动醒来,线程进入到Runnable/Blocked状态
 */
public class TimedWaitting {
    public static void main(String[] args) {
        //创建锁对象,保证唯一.为什么使用Object,因为Object是所有类的超类(多态思维)
        Object object = new Object();
        //创建一个乘客线程--消费者
        new Thread() {
            @Override
            public void run() {
                //设置死循环,让乘客一直等待使用座位
                while (true) {
                    //客和司机线程必须使用同步代码块包裹起来,保证等待和唤醒只有一个在执行
                    synchronized (object) {
                        System.out.println("乘客告知司机需要座位的数量");
                        //调用wait方法,摒弃CPU的执行,进入到Waitting状态
                        try {
                            //设置等待时间后,毫秒时间结束,还未notify,就自动苏醒
                            object.wait(5000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        //唤醒之后执行的代码
                        System.out.println("座位已经生产完成了,咱们可以使用了");
                        System.out.println("----------------------------");
                    }
                }
            }
        }.start();
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bwwUEGEC-1692323226170)(photo/JavaSE17_线程.assest/1673256473040.png)]

唤醒线程的两种方式:

  • 唤醒的方法:
    • 1、void notify();唤醒在此对象监视器上等待的单个线程
    • 2、void notifyAll();唤醒再次对象监视器上等待的所有线程
package waitandnotify;

/**
 * 唤醒的方法:
 *      1、void notify();唤醒在此对象监视器上等待的单个线程
 *      2、void notifyAll();唤醒再次对象监视器上等待的所有线程
 */
public class NotifyAndNotifyAll {
    public static void main(String[] args) {
        //创建锁对象,保证唯一.为什么使用Object,因为Object是所有类的超类(多态思维)
        Object object = new Object();
        //创建一个乘客线程--消费者
        new Thread() {
            @Override
            public void run() {
                //设置死循环,让乘客一直等待使用座位
                while (true) {
                    //客和司机线程必须使用同步代码块包裹起来,保证等待和唤醒只有一个在执行
                    synchronized (object) {
                        System.out.println("乘客1: 告知司机需要座位的数量");
                        //调用wait方法,摒弃CPU的执行,进入到Waitting状态
                        try {
                            object.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        //唤醒之后执行的代码
                        System.out.println("座位已经生产完成了,乘客1:可以使用了");
                        System.out.println("----------------------------");
                    }
                }
            }
        }.start();

        //创建一个乘客线程--消费者
        new Thread() {
            @Override
            public void run() {
                //设置死循环,让乘客一直等待使用座位
                while (true) {
                    //客和司机线程必须使用同步代码块包裹起来,保证等待和唤醒只有一个在执行
                    synchronized (object) {
                        System.out.println("乘客2: 告知司机需要座位的数量");
                        //调用wait方法,摒弃CPU的执行,进入到Waitting状态
                        try {
                            object.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        //唤醒之后执行的代码
                        System.out.println("座位已经生产完成了,乘客2:可以使用了");
                        System.out.println("----------------------------");
                    }
                }
            }
        }.start();

        //创建一个司机线程--生产者
        new Thread() {
            @Override
            public void run() {
                //设置死循环,让司机一直生产座位
                while (true) {
                    //花了3秒生产座位
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //客和司机线程必须使用同步代码块包裹起来,保证等待和唤醒只有一个在执行
                    synchronized (object) {
                        System.out.println("司机3秒之后生产完座位,通知乘客使用");
                        //座位生产完毕后,调用notify方法,唤醒乘客线程使用座位
                        //object.notify();//唤醒等待线程中的额其中一个
                        object.notifyAll();//唤醒所有在等待的线程
                    }
                }
            }
        }.start();
    }
}

notify()的结果:乘客1和乘客2随机被唤醒

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4t0fPAWd-1692323226171)(photo/JavaSE17_线程.assest/1673257264550.png)]

notifyAll()的结果:乘客1和乘客2都被唤醒

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JU1wplEH-1692323226171)(photo/JavaSE17_线程.assest/1673257396500.png)]

第五章 线程池

5.1 线程池思想

使用线程的时候就创建线程,这样实现起来非常简便,但是会有一个问题。

如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程都需要时间,

那么线程可否重复使用,也就是执行完任务之后,不被销毁,而是可以继续执行其他的任务。

在java中可以通过线程池来达到这样的效果

5.2 线程池概念

  • 线程池:相当于一个可以容纳多个线程的容器,其中的线程可以反复的使用,省去了频繁创建线程的操作,无序反复创建线程而消耗更多的资源
  • 线程池的工作原理图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uFGg2WjC-1692323226171)(photo/JavaSE17_线程.assest/7a899e510fb30f248a1c2d01d30cc145ac4b0349.png)]

  • 合理利用线程池带来的好处:
    • 1、降低资源消耗,减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可以执行多个任务
    • 2、提高响应速度,当任务到达时,任务可以不需要在等到线程创建就可以立即执行
    • 3、提高线程的可管理性,可以根据系统的承受能力,调整线程池中工作线程的数目,防止因为消耗过多的内存,而使得服务器崩掉。

5.3 线程池的使用

java中的线程池的顶级接口是java.util.concurrent.Executors,但是严格意义上讲,Executor并不是一个线程池,而只是一个执行线程的工具,真正的线程池的接口是:java.util.comcurrent.ExecutorService.

配置一个线城池是比较复杂的,尤其是对线程池的原理不是很清楚的情况下,很可能配置的线程池不是较好的,因此在java.util.concurrent.Executors线程工厂中提供了一个静态工厂,生成一些常用的线程池,

  • Executors类中创建线程池的方法如下:
    • 1、public static ExecutorService newFixedThreadPool (int nThreads)
    • 返回线程池对象,创建的是有界线程池,也就是池中的线程数是可以指定最大数量
    • 2、public Future< ? > submit(Runnable task):使用线程池对象的方法
    • 获取线程池中的某一个线程对象,并执行Future接口:用于记录线程任务执行完毕后产生的结果。
  • 使用线程池中线程对象的步骤:
    • 1、创建线程池对象
    • 2、创建Runnable接口的子类对象。(task)
    • 3、提交Runnable接口子类对象。(take task)
    • 4、关闭线程池(一般不操作)
  • 案例
package threadpool;

/**
 * 创建接口的实现类对象,重写run方法
 */
public class RunnableImpl implements Runnable{
    @Override
    public void run() {
        //设置线程任务
        System.out.println(Thread.currentThread().getName()+"创建了一个新的线程");
    }
}
package threadpool;

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

/**
 * 1、java.uitl.concurrent.Executors:线程池的工厂类。用来生成线程池
 *      Executors类中的静态方法:
 *          static ExecutorService newFixedThreadPool(int nThreads)
 *          创建一个可重用固定线程数的线程池
 *          参数:创建线程池中包含的线程数量
 *          返回值:返回的是ExecutorService接口的实现类对象,可以使用ExecutorService接口来接收
 * 2、java.util.concurrent.ExecutorService线程池接口中:
 *      1、从线程池中获取线程的方法,并调用start()方法,执行线程任务
 *          submit(Runnable task); 提交一个Runnable任务用于执行
 *      2、关闭/销毁线程池的方法
 *          void shutdown();
 *  3、线程池的使用步骤:
 *      1、使用线程池工厂类Executors中提供的静态方法newFixedThreadPool生产一个指定线程数量的线程池
 *      2、创建一个类,实现Runnable接口,重写run方法,设置线程任务
 *      3、调用ExecutorService接口中的submit方法,传递线程任务(Runnable接口实现类对象),开启线程,执行run方法
 *      4、调用ExecutorService接口中的shutdown方法销毁线程,但是不建议使用
 */
public class ThreadPool {
    public static void main(String[] args) {
        //1、使用线程池工厂类Executors中提供的静态方法newFixedThreadPool生产一个指定线程数量的线程池
        ExecutorService es = Executors.newFixedThreadPool(2);
        //2、创建一个类,实现Runnable接口,重写run方法,设置线程任务
        RunnableImpl runnable = new RunnableImpl();
        //3、调用ExecutorService接口中的submit方法,传递线程任务(Runnable接口实现类对象),开启线程,执行run方法
        //线程池一直开启,使用完了线程,会自动把线程归还给线程池,线程可以继续被调用
        es.submit(runnable);
        es.submit(runnable);
        es.submit(runnable);
        //4、调用ExecutorService接口中的shutdown方法销毁线程,但是不建议使用
        es.shutdown();
        //线程池被关闭,无法调用线程,就会报异常
        //es.submit(runnable);
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ICUg1myy-1692323226172)(photo/JavaSE17_线程.assest/1673323076687.png)]

线程池关闭之后再次调用线程执行任务,就会出现异常

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gO4lrYrM-1692323226172)(photo/JavaSE17_线程.assest/1673323138212.png)]

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值