Java核心类库(四)——多线程

一、 多线程概述

多线程简单描述:一个程序有多个执行路径

1.1 线程与进程

进程

有独立内存空间的应用程序
【指一个内存中运行的应用程序,每个进程都有一个独立的内存空间(每个内存都有自己的堆、栈,互不共享)】

线程

是进程中的一个执行路径共享一个内存空间

线程之间可以自由切换,并发执行, 一个进程最少有一个线程

线程实际上是在进程基础之上的进一步划分,一个进程启动之后,里面的若干执行路径又可以划分成若干个线程

1.2 线程调度

线程调度,即线程到底是怎样执行的

分时调度

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

使程序更合理地交替进行(交替时间极小,使得看上去感觉许多线程在同时运行)

抢占式调度(Java使用)

通过代码可以调整时间片来控制

优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性)

CPU使用抢占式调度模式在多个线程间进行高速切换,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高

1.3 同步与异步

同步(线程安全)异步(线程不安全)
排队执行同时执行
效率低效率高
数据安全数据不安全

1.4 并发与并行

并发

两个或多个事件在同一个时间段内发生

并行

两个或多个事件在同一时刻发生(同时发生)

二、 创建线程

2.1 继承Thread类

Thread是JAVA提供的用于实现线程的类

有一个继承的方法run,run方法中的代码就是一条新的执行路径,路径的触发方式不是调用run方法,而是通过Thread对象的start来启动任务

Thread类

构造方法

常用构造方法:

构造器描述
Thread()分配新的 Thread对象,默认初始化
Thread​(Runnable target)分配新的 Thread对象,传一个任务
Thread​(Runnable target, String name)分配新的 Thread对象,给一个名称
字段
变量和类型字段描述
static intMAX_PRIORITY最高优先级
static intMIN_PRIORITY最低优先级
static intNORM_PRIORITY默认优先级
常用方法
变量和类型方法描述
longgetId()返回此Thread的标识符
StringgetName()返回此线程的名称
intgetPriority()返回此线程的优先级
voidsetPriority​(int newPriority)更改此线程的优先级
voidstart()导致此线程开始执行; Java虚拟机调用此线程的run方法
static voidsleep​(long millis)指定的毫秒数,使当前正在执行的线程休眠(暂时停止执行)【应用:每隔一秒输入输出等等,比较常用】
static voidsleep​(long millis, int nanos)指定的毫秒数加上纳秒数,使当前正在执行的线程休眠(暂时停止执行)
voidsetDaemon​(boolean on)将此线程标记为daemon线程用户线程
voidstop()停止线程,已过时, 这种方法本质上是不安全的

daemon线程(守护线程):即守护用户线程,掌握不了自己的生命,依附于用户线程,用户线程没了守护线程也就没了

用户线程:所有用户进程都死亡了,程序才结束,自己决定自己的死亡

面试题:
如何将一个线程停止?
用stop(),可能会导致资源未被释放,从而出现资源依旧被占用,因此stop()现在已过时
我们可以通过变量做标记来控制线程,即对线程对象打标记,触发异常,打标记会使程序进入catch块,后续的处理依旧由程序员决定

Thread在程序中的使用

常规用法

新建一个类继承Thread

代码示例:

  • 编写一个线程

class MyThread extends Thread{   //Thread,JAVA提供的用于实现线程的类
    /**
     * 线程线程要执行的任务方法
     * 每个线程都有自己的栈空间,共用一份堆内存
     */
    @Override
    public void run() {
        //这里的代码,就是一条新的执行路径
        //这个执行路径的触发方式,不是调用run方法,而是通过Thread对象的start来启动任务
        for(int i = 0;i < 5;i++){
            System.out.println( i + "这是另一个线程");
        }
    }
  • 再在主函数中编写代码,来实现多线程
public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start();        //时间分配是抢占式分配
        for(int i = 0;i < 5;i++){
            System.out.println( i + "这是main中的线程");
        }
    }

运行结果:

由于Java的时间分配是抢占式时间分配,谁抢占到谁就先执行,因此得到如下输出结果

0这是另一个线程
0这是main中的线程
1这是另一个线程
1这是main中的线程
2这是另一个线程
2这是main中的线程
3这是另一个线程
4这是另一个线程
3这是main中的线程
4这是main中的线程

画图解释此程序中的多线程执行:
在这里插入图片描述

使用匿名内部类

代码示例:

public static void main(String[] args) {
        //new Thread(){}.start();
        
        new Thread(){        //匿名内部类,仅几行代码即可实现一个线程
            @Override
            public void run() {
                for (int i = 0;i < 5;i++){
                    System.out.println(i + "hahaha");
                }
            }
        }.start();
        for (int i = 0;i < 5;i++){
            System.out.println(i + "heiheihei");
        }
    }//end main

运行结果:

0hahaha
0heiheihei
1heiheihei
1hahaha
2heiheihei
3heiheihei
2hahaha
4heiheihei
3hahaha
4hahaha

2.2 实现Runnable接口(用得更多)

用于给线程执行的任务,但是还是要借助Thread
【创建一个任务对象,里面包含了任务→再创建一个线程,为其分配这个任务→start执行】

与前面继承Thread类相比,优势在于:

  1. 通过 创建任务→给线程分配的方式 来实现多线程,更适合多个线程同时执行相同任务的情况

  2. 可以避免单继承所带来的的局限性(java单继承,但是可以多实现)

  3. 任务与线程本身分离,提高了程序的健壮性

  4. !!线程池技术,接收Runnable类型的任务,不接收Thread类型的线程

代码示例:

  • 新建一个MyRunnable类实现Runnable接口
/**
 * 第二种实现多线程技术
 * 实现Runnable接口
 * 用于给线程执行的任务,但是还是要借助Thread
 */
class MyRunnable implements Runnable{

    @Override
    public void run() {
        //线程任务
        for(int i = 0;i < 5;i ++){
            System.out.println( i + "这是另一个线程");
        }
    }
}
  • 再在主函数中编写代码,来实现多线程
    public static void main(String[] args) {
        /**
         * 第二种实现多线程技术
         * 实现Runnable接口(用的更多)
         */
        MyRunnable r = new MyRunnable();//创建一个任务对象,里面包含了任务
        Thread t = new Thread(r);//创建一个线程,为其分配一个任务
        t.start();//执行这个线程
        for(int i = 0;i < 5;i++){
            System.out.println( i + "这是main中的线程");
        }
    }

运行结果:

0这是另一个线程
1这是另一个线程
2这是另一个线程
3这是另一个线程
0这是main中的线程
4这是另一个线程
1这是main中的线程
2这是main中的线程
3这是main中的线程
4这是main中的线程

2.3 实现Callable接口(用的很少)

面试中容易被问到: 这是java中第三种线程的实现方式

Thread和Runnable实现的线程和主线程并发执行(主线程执行,另外的线程也执行)

Callable创建的线程可以与主线程并发执行,也可以实现主线程等待该线程执行完毕并返回结果

FutureTask接口

变量和类型方法描述
Vget()获取线程执行的结果
Vget​(long timeout, TimeUnit unit)传入超时的时间,获取线程执行的结果,如果超时则不获取结果

Callable使用步骤

  1. 编写类实现Callable接口 , 实现call方法
    class XXX implements Callable< T > {
    @Override
    public < T > call() throws Exception {
    return T;
    }
    }
  2. 创建FutureTask对象 , 并传入第一步编写的Callable类对象
    FutureTask< Integer > future = new FutureTask<>(callable);
  3. 通过Thread启动线程
    new Thread(future).start();

代码示例:

使用Callable创建线程:

  1. 编写类实现Callable接口 , 实现call方法
class MyCallable implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        //Thread.sleep(3000);
        for (int i = 0;i < 10;i ++){
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName() + " " + i);
        }
        return 100;
    }
}
  1. 在main中创建FutureTask对象 , 并传入第一步编写的Callable类对象
	Callable<Integer> c = new MyCallable();
	FutureTask<Integer> task = new FutureTask<>(c);
  1. 在main中启动线程
   new Thread(task).start();

线程用法一

与Thread和Runnable一样,Callable创建的线程可以与主线程并发执行

在main中创建主线程:

        for (int i = 0;i < 10;i ++){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " " + i);
        }

输出结果:

两个线程同时交替执行

Thread-0 0
main 0
main 1
Thread-0 1
Thread-0 2
main 2
main 3
Thread-0 3
Thread-0 4
main 4
main 5
Thread-0 5
Thread-0 6
main 6
main 7
Thread-0 7
Thread-0 8
main 8
Thread-0 9
main 9


线程用法二

该线程先执行,主线程等待该线程执行完毕并返回结果

Callalble接口支持返回执行结果,需要调用FutureTask.get()得到,调用此方法会阻塞主进程继续往下执行

Callable获取返回值:在main中主线程的前面添加语句,用 FutureTask的.get()方法获取子线程的返回结果

        Integer x = task.get();
        System.out.println(Thread.currentThread().getName() + "返回值为:" + x);

输出结果:

由输出可以看出,该线程先执行,主线程等待该线程执行完毕后返回结果再执行

Thread-0 0
Thread-0 1
Thread-0 2
Thread-0 3
Thread-0 4
Thread-0 5
Thread-0 6
Thread-0 7
Thread-0 8
Thread-0 9
main返回值为:100
main 0
main 1
main 2
main 3
main 4
main 5
main 6
main 7
main 8
main 9


Callable的其它使用方法

  1. 判断线程任务是否执行完毕:.isDone()

  2. 取消线程:.cancel(参数)
    参数是boolean类型,true表示取消
    返回值类型也是boolean,返回true表示取消线程成功(通常是任务没完成然后取消了),返回false表示取消线程失败(通常情况下任务已经执行完毕)


	Callable<Integer> c = new MyCallable();	//编写类实现Callable接口 , 实现call方法,创建对象
	FutureTask<Integer> task = new FutureTask<>(c);//创建FutureTask对象 , 并传入第一步编写的Callable类对象
	task.isDone();//判断线程任务是否执行完毕
	task.cancel();//取消线程

2.4 Runnable 与 Callable

接口定义

//Runnable接口
public interface Runnable {
	public abstract void run();
}

//Callable接口
public interface Callable<V> {
	V call() throws Exception;
}

异同点

相同点:

  1. 都是接口
  2. 都可以编写多线程程序
  3. 都采用Thread.start()启动线程

不同点:

区别点RunnableCallable
有无返回值没有返回值可以返回执行结果
任务能否抛出异常Runnable的run()不能抛出异常Callable接口的call()允许抛出异常

三、 线程有关操作

常用方法

变量和类型方法描述
longgetId()返回此Thread的标识符
StringgetName()返回此线程的名称
intgetPriority()返回此线程的优先级
voidsetPriority​(int newPriority)更改此线程的优先级
voidstart()导致此线程开始执行; Java虚拟机调用此线程的run方法
static voidsleep​(long millis)指定毫秒数,使当前正在执行的线程休眠(暂时停止执行)【应用:每隔一秒输入输出等等,比较常用】
static voidsleep​(long millis, int nanos)指定毫秒数+纳秒数,使当前正在执行的线程休眠(暂时停止执行)
voidsetDaemon​(boolean on)将此线程标记为daemon线程用户线程
voidstop()停止线程,已过时, 这种方法本质上是不安全的

daemon线程(守护线程):即守护用户线程,掌握不了自己的生命,依附于用户线程,用户线程没了守护线程也就没了
用户线程:所有用户进程都死亡了,程序才结束,自己决定自己的死亡

3.1 设置和获取线程名称

变量和类型方法描述
StringsetName()设置线程的名称
StringgetName()获取线程的名称

代码示例:

  • 新建一个MyRunnable类实现Runnable接口
    static class MyRunnable implements Runnable{
        @Override
        public void run() {
         	//currentThread():获取当前正在执行的对象
            //getName获取线程名称
            System.out.println(Thread.currentThread().getName());
        }
    }
  • 再在主函数中编写代码,来实现多线程以及获取线程名称
 		//currentThread():获取当前正在执行的对象
        System.out.println(Thread.currentThread().getName());//main线程
        Thread t = new Thread(new MyRunnable());
        t.setName("第0个线程");//使用setName设置线程名称
        t.start();
        new Thread(new MyRunnable(),"第1个线程").start();
        new Thread(new MyRunnable(),"第2个线程").start();
        new Thread(new MyRunnable(),"第3个线程").start();
        new Thread(new MyRunnable()).start();//没有给线程setName,则系统会自动命名
        new Thread(new MyRunnable()).start();
        new Thread(new MyRunnable()).start();

运行结果:

main
第0个线程
第1个线程
第3个线程
Thread-12个线程
Thread-3
Thread-2

3.2 线程休眠

变量和类型方法描述
static voidsleep​(long millis)指定毫秒数,使当前正在执行的线程休眠(暂时停止执行)
static voidsleep​(long millis, int nanos)指定毫秒数+纳秒数,使当前正在执行的线程休眠(暂时停止执行)

sleep为Thread的静态方法,因此可以用Thread直接调用:Thread.sleep()

1秒 = 1000毫秒
1毫秒 = 1000微妙 = 1000000纳秒

代码示例:

        for (int i = 0;i < 5;i++){
            System.out.println(i);
            Thread.sleep(1000);			//每次循环暂停1000毫秒后再继续执行
        }

输出结果:

每隔1秒(1000毫秒)输出一个数

0
1
2
3
4

3.3 线程中断

一个线程是一个独立的执行路径,是否应该结束,应该由其自身决定

用stop(),可能会导致资源未被释放,从而出现资源依旧被占用,因此stop()现在已过时

我们可以通过给对象做标记来控制线程,即对线程对象打标记,触发异常,打标记会使程序进入catch块,后续的处理依旧由程序员决定

代码示例:

  • 首先新建类实现Runnable接口,并继承run方法 新建线程
    static class MyRunnable implements Runnable{

        @Override
        public void run() {//父接口没有声明异常的抛出,子不能声明比父更大的异常,因此只能try-catch
            for (int i =0;i < 10;i ++){
                System.out.println(Thread.currentThread().getName() + ":" + i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
  • 编写main
    public static void main(String[] args) {
        Thread t1 = new Thread(new MyRunnable());  //新建线程
        t1.start();

        for (int i =0;i < 5;i ++){//main线程
            System.out.println(Thread.currentThread().getName() + ":" + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {//打标记之后程序进去catch
                e.printStackTrace();
            }
        }
        //main线程打印5次,t1线程打印10次,因此main线程先中断,此时对t1线程打标记
        //给线程t1添加中断标记,但是只是告诉线程它可以死亡,但是未必死亡
        t1.interrupt();


    }
  • 打标记处理:

对线程对象打标记,触发异常,使程序进入catch,后续的处理依旧由程序员决定

① 修改MyRunnable中的try-catch语句

发现中断标记后进入catch,但是程序可以选择不死亡,继续执行

                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
//                    e.printStackTrace();
                    System.out.println("发现了中断标记,但是不死亡");

                }

输出结果:

每个线程都隔1秒打印一个数,由于线程不死亡,因此发现标记之后继续执行

Thread-0:0
main:0
main:1
Thread-0:1
Thread-0:2
main:2
Thread-0:3
main:3
main:4
Thread-0:4
Thread-0:5
发现了中断标记,但是不死亡
Thread-0:6
Thread-0:7
Thread-0:8
Thread-0:9


② 修改MyRunnable中的try-catch语句

发现中断标记后进入catch,程序死亡,中断程序、释放资源

                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
//                    e.printStackTrace();
                    System.out.println("发现了中断标记,线程自杀");
                    return;//表示线程结束,资源释放
                }

输出结果:

每个线程都隔1秒打印一个数,由于发现中断标记后线程自杀死亡,因此发现标记之后结束程序

Thread-0:0
main:0
main:1
Thread-0:1
Thread-0:2
main:2
Thread-0:3
main:3
main:4
Thread-0:4
Thread-0:5
发现了中断标记,线程自杀

3.4 设置守护线程

变量和类型方法描述
voidsetDaemon​(boolean on)将此线程标记为daemon线程用户线程

线程分为守护线程用户线程

用户线程:当一个进程不包含任何存活的用户线程时,进行结束(我们直接创建的线程都是用户线程)

守护线程:用于守护用户线程,当最后一个用户线程结束时,守护线程自动死亡

代码示例:

  • 新建类实现Runnable接口,并继承run方法 新建线程
    static class MyRunnable implements Runnable{
        @Override
        public void run() {//父接口没有声明异常的抛出,子不能声明比父更大的异常,因此只能try-catch
            for (int i =0;i < 10;i ++){
                System.out.println(Thread.currentThread().getName() + ":" + i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
  • 编写main

在线程启动前,用setDaemon()来标记守护线程

    public static void main(String[] args) {
        Thread t1 = new Thread(new MyRunnable());//t1为子线程
        t1.setDaemon(true);//设置t1为守护线程,在t1启动前设置
        t1.start();//启动守护线程
        
        //main主线程,当主线程结束时,守护线程也会结束
        for (int i =0;i < 5;i ++){
            System.out.println(Thread.currentThread().getName() + ":" + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

输出结果:

Thread-0:0
main:0
main:1
Thread-0:1
Thread-0:2
main:2
main:3
Thread-0:3
main:4
Thread-0:4
Thread-0:5

3.5 停止线程

以下文多线程通信问题中的生产者与消费者问题中的程序为例,我们让线程运行,但是我们只是打开了线程,并没有关闭线程,到最后程序运行完只能手动停止线程
在这里插入图片描述
我们也可以选择调用方法去停止线程

        Thread.currentThread().interrupt();
        String threadName = Thread.currentThread().getName();
        System.out.println(threadName + " 当前线程是否已停止:=" + Thread.interrupted());
        System.out.println(threadName + " 当前线程是否已停止:=" + Thread.interrupted());

四、 线程阻塞

通俗地讲,线程阻塞不仅仅指线程休眠,程序停在那读文件、读完之后继续执行,那一段时间也算是阻塞

因此可总结为:
所有消耗时间的操作都属于线程阻塞,如文件读取、接收用户收入、线程休眠等

五、 线程安全

线程不安全问题

一个任务交给三个线程去执行,容易出现线程不安全问题

代码示例:

  • 首先新建类Ticket 实现接口Runnable,新建run卖票任务
    /**
     * 任务创建一个,但是交给三个线程去执行,则会出现线程不安全问题
     */
    static class Ticket implements Runnable{
        //总票数
        private int count = 10;
        @Override
        public void run() {//每次被触发就进卖买票操作
            while(count > 0){
                //卖票
                System.out.println("正在准备卖票");
                //try-catch使得卖票的时间更长
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                count --;
                System.out.println("出票成功!余票:" + count);
            }
        }//end
    }
  • 在main中创建并启动三个线程
    public static void main(String[] args) {
        //线程不安全
        Runnable runnable = new Ticket();
        //启动三个线程
        new Thread(runnable).start();
        new Thread(runnable).start();
        new Thread(runnable).start();
    }

输出结果:

正在准备卖票,请稍等...
正在准备卖票,请稍等...
正在准备卖票,请稍等...
出票成功!余票:8
正在准备卖票,请稍等...
出票成功!余票:9
正在准备卖票,请稍等...
出票成功!余票:7
正在准备卖票,请稍等...
出票成功!余票:5
正在准备卖票,请稍等...
出票成功!余票:6
正在准备卖票,请稍等...
出票成功!余票:4
正在准备卖票,请稍等...
出票成功!余票:3
正在准备卖票,请稍等...
出票成功!余票:2
正在准备卖票,请稍等...
出票成功!余票:1
正在准备卖票,请稍等...
出票成功!余票:0
出票成功!余票:-2
出票成功!余票:-1

通过输出结果观察可知,余票出现了负数,但是代码逻辑上余票count=0时便不再执行了

出现问题原因:假设三段线程为ABC,ABC可能同时进行到while,假设A先进入,此时count = 1,当A进入休眠未进行到count–时,B检测到count = 1,进入while,当B进入休眠未进行到count–时,C检测到count = 1,进入while,此时A运行count–,count = 0,B接着运行count- -,count =-1,C接着运行count- -,count = -2,同时由于线程阻塞以及线程调度,输出的顺序可能不同

这就是多线程完成统一任务时出现的线程不安全问题

实现线程安全(显式锁与隐式锁)

  • 隐式锁:隐式锁使用synchronized修饰符。在使用sync关键字的时候,当sync代码块执行完成之后程序能够自动获取锁和释放锁
  • 显式锁:显式锁使用Lock关键字。在使用Lock的时候,使用者需要手动[获取lock()]和[释放unlock()]锁,如果没有释放锁,就有可能导致出现死锁的现象

添加synchronized关键字同步代码块同步方法属于隐式锁

Lock关键字属于显式锁


1 同步代码块

线程同步,使线程排队执行

实现思路:每个线程在执行时看同一把锁,谁抢到了锁,谁就执行

线程同步实现:synchronized

格式:
synchronized(锁对象){
// 同步代码块
}
`
锁对象: java中任何对象都可以作为锁存在,即任何对象都可以打上锁的标记,作为锁对象


代码示例:

对原有的线程不安全的卖票示例进行修改

在while循环中加锁
同步代码块为:当余票大于0时,进行卖票操作
因此当一个线程正在执行同步代码块时,另外的线程不会执行该代码块,在后面排队等待执行

 /**
     * 任务创建一个,但是交给三个线程去执行,则会出现线程不安全问题
     *
     *  解决线程不安全问题:排队执行
     */
    static class Ticket implements Runnable{
        //总票数
        private int count = 10;
        private Object o = new Object();//创建对象
        @Override
        public void run() {//每次被触发就进卖买票操作
            while(true){
                synchronized (o){//加锁
                    if(count > 0){
                        //卖票
                        System.out.println("正在准备卖票,请稍等...");
                        //try-catch使得卖票的时间更长
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        count --;
                        System.out.println(Thread.currentThread().getName() + "出票成功!余票:" + count);
                    }else{
                        break;
                    }
                }
            }//end while
        }//end run
    }

由于只创建了一个任务,因此Object对象只创建了一个,即创建了一把锁

而后面启动的三个线程由于只有一个任务,因此三个线程在执行的时候看同一把锁,排队执行

        Runnable runnable = new Ticket();//只有一个任务,因此下面的object对象只创建了一个
        //启动三个线程,o是同一个,只有一个任务,因此在执行的时候只看一个o
        new Thread(runnable).start();
        new Thread(runnable).start();
        new Thread(runnable).start();

		//如果上述写法写成如下,则依旧为不安全线程
		//此时创建了三个任务(new Ticket()),分别创建三个object对象(即锁),此时相当于3个人卖票,每个人卖10张 
		//错误写法!!!!注意
		//new Thread(new Ticket()).start();
		//new Thread(new Ticket()).start();
		//new Thread(new Ticket()).start();
		
		

加了锁之后的输出结果:

正在准备卖票,请稍等...
Thread-0出票成功!余票:9
正在准备卖票,请稍等...
Thread-0出票成功!余票:8
正在准备卖票,请稍等...
Thread-0出票成功!余票:7
正在准备卖票,请稍等...
Thread-0出票成功!余票:6
正在准备卖票,请稍等...
Thread-0出票成功!余票:5
正在准备卖票,请稍等...
Thread-0出票成功!余票:4
正在准备卖票,请稍等...
Thread-0出票成功!余票:3
正在准备卖票,请稍等...
Thread-0出票成功!余票:2
正在准备卖票,请稍等...
Thread-0出票成功!余票:1
正在准备卖票,请稍等...
Thread-0出票成功!余票:0


如果将创建锁的对象写在任务的代码块中,如下所示

此时,每个线程启动时都会创建o对象,因此每个线程都有自己锁o,每个线程在执行时都看自己的锁,这时不能排队,要格外注意!!!!

错误写法:

public void run() {//每次被触发就进卖买票操作
            Object o = new Object();//!!!!!!!三个线程启动时都会创建o对象,即每个线程都有自己的锁o,每个人都看自己的不同的锁,此时不能排队
            while(true){
                synchronized (o){//加锁
                    if(count > 0){
                        //卖票
                        System.out.println("正在准备卖票,请稍等...");
                        //try-catch使得卖票的时间更长
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        count --;
                        System.out.println(Thread.currentThread().getName() + "出票成功!余票:" + count);
                    }else{
                        break;
                    }
                }
            }//end while
        }//end run

2 同步方法

与同步代码块相似,不同的是,同步方法以方法为单位进行加锁,给方法添加synchronized修饰符

同步方法的锁为this
同步方法有可能被静态修饰,如果被静态修饰,则同步方法的锁为类.class

代码示例:

	/**
     * 创建一个任务,但是交给三个线程去执行,则会出现线程不安全问题
     * 解决线程不安全问题:排队执行
     */
    static class Ticket implements Runnable{
        //总票数
        private int count = 10;
        @Override
        public void run() {//每次被触发就进卖买票操作
            while(true){
                boolean flag = sale();//sale()为加了锁的方法
                if(!flag){
                    break;
                }
            }//end while
        }//end run

        //添加synchronized修饰符,给方法加锁
        public synchronized boolean sale(){
            //this,同步的方法的锁
            //Ticket.class,如果方法为静态方法,则同步方法的锁为类.class
            if(count > 0){
                //卖票
                System.out.println("正在准备卖票,请稍等...");
                //try-catch使得卖票的时间更长
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                count --;
                System.out.println(Thread.currentThread().getName() + "出票成功!余票:" + count);
                return true;
            }
            return false;
        }
    }

如果同步代码块锁了一段代码,同步方法锁了另一端代码,锁的对象都是this,那么这当一段代码正在执行时,另一段加锁的代码不能执行

如下面的代码所示,在循环前加了一把锁,则当一个线程执行这段代码块时,同步方法sale不能执行

  public void run() {
            synchronized (this){//再加一把锁
                
            }
            while(true){
                boolean flag = sale();
                if(!flag){
                    break;
                }
            }//end while
        }//end run

如果有多个同步的方法,且多个方法都是this这把锁,则其中一个方法执行、其他方法无法执行


3 显式锁Lock

在使用Lock的时候,使用者需要手动[获取lock()]和[释放unlock()]锁,如果没有释放锁,就有可能导致出现死锁的现象

显式锁比隐式锁更好,更能体现锁的概念,体现了面向对象的机制

显式锁Lock的子类:ReentrantLock

代码示例:

  • 创建隐式锁l
	Lock l = new ReentrantLock();
  • 在进行代码块前锁住
	l.lock();
  • 在代码块结束后开锁
	l.unlock();//代码执行完毕,开锁

完整代码如下:

    public static void main(String[] args) {
        //线程不安全
        //解决方案3:显式锁Lock
        //java中任何对象都可以作为锁存在,即任何对象都可以打上锁的标记,作为锁对象
        Runnable runnable = new Ticket();//只有一个任务
        //启动三个线程,但使用的都是runnable对象,因此用的都是同一把锁l
        new Thread(runnable).start();
        new Thread(runnable).start();
        new Thread(runnable).start();
    }

   static class Ticket implements Runnable{
        //总票数
        private int count = 10;
        //创建显式锁l
        private Lock l = new ReentrantLock();
        @Override
        public void run() {//每次被触发就进卖买票操作
            while(true){
                l.lock();//进入if之前,锁住
                if(count > 0){
                    //卖票
                    System.out.println("正在准备卖票,请稍等...");
                    //try-catch使得卖票的时间更长
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    count --;
                    System.out.println(Thread.currentThread().getName() + "出票成功!余票:" + count);
                }else{
                    break;
                }
                l.unlock();//代码执行完毕,开锁
            }//end while
        }//end run
    }

六、 公平锁与非公平锁

大厂面试可能会遇到

  • 公平锁:排队,先来先到,在Lock构造方法传入Boolean值True,则为公平锁
  • 非公平锁:抢,隐式锁Sync属于非公平锁,Lock默认为非公平锁

实现公平锁:

显式锁Lock的构造方法中,参数为True则表示公平锁

Lock l = new ReentrantLock(true);

七、 线程死锁

死锁:多个线程线程互相持有对方所需要的资源,多个线程因竞争资源而造成的一种僵局(互相等待)

死锁举例

拿生活中的场景并结合代码,举一个简单的栗子:

挟持着人质的罪犯警察两人僵持不下(警察抓着罪犯,而罪犯手上有人质)
罪犯对警察说:“你放了我,我放人质!”

然而警察听到后内心想:“我救人质,但是罪犯跑了”

警察对罪犯说:“你放了人质,我放过你!”

然而罪犯听到后内心想:“警察放过我,但是人质跑了”

根据这个场景,来进行代码的实现

  • 罪犯Culprit
	 /**
     * 罪犯
     */
    static class Culprit{
    	//罪犯对警察说
        public synchronized void say(Police p){
            System.out.println("罪犯:你放了我,我放人质!");
            p.fun();
        }
        //听了警察的话,内心回应
        public synchronized void fun(){
            System.out.println("罪犯内心:警察放过我,但是人质跑了");
        }
    }
  • 警察Police
	 /**
     * 警察
     */
    static class Police{
    	//警察对罪犯说
        public synchronized void say(Culprit c){
            System.out.println("警察:你放了人质,我放过你!");
            c.fun();
        }
        //听了罪犯的话,警察回应
        public synchronized void fun(){
            System.out.println("警察内心:我救人质,但是罪犯跑了");
        }
  • 新建线程MyThread,警察对罪犯说
static class MyThread extends Thread{
        private Culprit c;
        private Police p;
        
        //构造方法
        public  MyThread(Culprit c,Police p){
            this.c = c;
            this.p = p;
        }
        @Override
        public void run() {
             /**
             * 警察say方法执行完之后,调用罪犯的fun方法,等待罪犯回应
             */
            p.say(c);//警察说话,让罪犯回应
        }
    }
  • 新建主线程,罪犯对警察说
    public static void main(String[] args) throws InterruptedException {
        Culprit c = new Culprit();//新建一个罪犯对象
        Police p = new Police();//新建一个警察对象
        new MyThread(c,p).start();//新建线程:警察说话,让罪犯回应
        
        /**
         * 罪犯的say方法调用执行完后,调用警察的fun方法,等待警察回应
         */
        c.say(p);//主线程:罪犯说话,让警察回应
    }

此时,有两个线程,而这两个线程中,警察和罪犯都说完了自己的话(执行say),等待对方回应(执行fun),然而等待对方回应前先必须等待对方把话说完(执行say),但是不知道对方有没有先说完(有没有执行完say),因此卡住了,造成了死锁

死锁的结果输出:

罪犯和警察说完之后都在等待对方回应,从而造成了死锁,程序卡在那无法继续进行,只能手动结束程序

罪犯:你放了我,我放人质!
警察:你放了人质,我放过你!

但是,当其中一个线程执行够快,则不会出现死锁情况(概率很低),输出结果如下:

罪犯:你放了我,我放人质!
警察内心:我救人质,但是罪犯跑了
警察:你放了人质,我放过你!
罪犯内心:警察释放我,但是人质跑了

死锁避免

  1. 线程按照一定的顺序加锁
  2. 加锁时限(线程尝试获取锁的时候加上一定的时限超过时限则放弃对该锁的请求,并释放自己占有的锁)
  3. 根源上解决: 在任何有可能产生锁的方法中,不调用另一个有可能产生锁的方法

八、 多线程通信问题

概述

多线程通信问题,也就是生产者与消费者问题

生产者和消费者为两个线程,两个线程在运行过程中交替睡眠,生产者在生产时消费者没有在消费,消费者在消费时生产者没有在生产,确保数据安全

引入

该过程可以类比为一个栗子:

厨师为生产者,服务员为消费者,假设只有一个盘子盛放食品。
厨师在生产食品(厨师线程运行)的过程中,服务员应当等待(服务员线程睡眠),等到食品生产完成(厨师线程结束)后将食品放入盘子中,服务员将盘子端出去(服务员线程运行),此时没有盘子可以放食品,因此厨师休息(厨师线程休眠),一段时间过后服务员将盘子拿回来(服务员线程结束),厨师开始进行生产食品(厨师线程运行),服务员在一旁等待(服务员线程睡眠)…

在此过程中,厨师和服务员两个线程交替睡眠,厨师在做饭时服务员没有端盘子(厨师线程运行时服务员线程睡眠),服务员在端盘子时厨师没有在做饭(服务员线程运行时厨师线程睡眠),确保了数据的安全


根据厨师和服务员这个栗子,我们可以通过代码来一步步实现

  • 定义厨师线程
 /**
     * 厨师,是一个线程
     */
    static class Cook extends Thread{
        private Food f;
        public Cook(Food f){
            this.f = f;
        }

        //运行的线程,生成100道菜
        @Override
        public void run() {
            for (int i = 0 ; i < 100; i ++){
                if(i % 2 == 0){
                    f.setNameAneTaste("小米粥","没味道,不好吃");
                }else{
                    f.setNameAneTaste("老北京鸡肉卷","甜辣味");
                }
            }
        }
    }
  • 定义服务员线程
/**
     * 服务员,是一个线程
     */
    static class Waiter extends Thread{
        private Food f;
        public Waiter(Food f){
            this.f = f;
        }

        @Override
        public void run() {
            for(int i =0 ; i < 100;i ++){
                //等待
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                f.get();
            }
        }//end run

    }//end waiter
  • 新建食物类
 /**
     * 食物,对象
     */
    static class Food{
        private String name;
        private String taste;

        public void setNameAneTaste(String name,String taste){
            this.name = name;

            //加了这段之后,有可能这个地方的时间片更有可能被抢走,从而执行不了this.taste = taste
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.taste = taste;
        }//end set

        public void get(){
            System.out.println("服务员端走的菜的名称是:" + this.name + " 味道:" + this.taste);
        }
    }//end food
  • main方法中去调用两个线程
    public static void main(String[] args) {
        Food f = new Food();
        Cook c = new Cook(f);
        Waiter w = new Waiter(f);
        c.start();//厨师线程
        w.start();//服务生线程     
    }

运行结果:

只截取了一部分,我们可以看到,“小米粥”并没有每次都对应“没味道,不好吃”,“老北京鸡肉卷”也没有每次都对应“甜辣味”,而是一种错乱的对应关系

...
服务员端走的菜的名称是:老北京鸡肉卷 味道:没味道,不好吃
服务员端走的菜的名称是:小米粥 味道:甜辣味
服务员端走的菜的名称是:老北京鸡肉卷 味道:没味道,不好吃
服务员端走的菜的名称是:小米粥 味道:甜辣味
服务员端走的菜的名称是:老北京鸡肉卷 味道:没味道,不好吃
服务员端走的菜的名称是:小米粥 味道:甜辣味
服务员端走的菜的名称是:老北京鸡肉卷 味道:没味道,不好吃
服务员端走的菜的名称是:小米粥 味道:甜辣味
服务员端走的菜的名称是:老北京鸡肉卷 味道:没味道,不好吃
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
...

name和taste对应错乱的原因:

当厨师调用set方法时,刚设置完name,程序进行了休眠,此时服务员可能已经将食品端走了,而此时的taste是上一次运行时保留的taste。
两个线程一起运行时,由于使用抢占式调度模式,没有协调,因此出现了该现象

以上运行结果解释如图:

在这里插入图片描述


加入线程安全

针对上面的线程不安全问题,对厨师set和服务员get这两个线程都使用synchronized关键字,实现线程安全,即:当一个线程正在执行时,另外的线程不会执行,在后面排队等待当前的程序执行完后再执行

代码如下所示,分别给两个方法添加synchronized修饰符,以方法为单位进行加锁,实现线程安全

	/**
     * 食物,对象
     */
    static class Food{
        private String name;
        private String taste;

        public synchronized void setNameAneTaste(String name,String taste){
            this.name = name;
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.taste = taste;
        }//end set

        public synchronized void get(){
            System.out.println("服务员端走的菜的名称是:" + this.name + " 味道:" + this.taste);
        }
        
    }//end food

输出结果:

由输出可见,又出现了新的问题:
虽然加入了线程安全,set和get方法不再像前面一样同时执行并且菜名和味道一一对应,但是set和get方法并没有交替执行(通俗地讲,不是厨师一做完服务员就端走),而是无序地执行(厨师有可能做完之后继续做,做好几道,服务员端好几次…无规律地做和端)

...
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
...

实现生产者与消费者问题

由上面可知,加入线程安全依旧无法实现该问题。因此,要解决该问题,回到前面的引入部分,严格按照生产者与消费者问题中所说地去编写程序

生产者与消费者问题:
生产者和消费者为两个线程,两个线程在运行过程中交替睡眠,生产者在生产时消费者没有在消费,消费者在消费时生产者没有在生产,确保数据安全

厨师在生产食品(厨师线程运行)的过程中,服务员应当等待(服务员线程睡眠),等到食品生产完成(厨师线程结束)后将食品放入盘子中,服务员将盘子端出去(服务员线程运行),此时没有盘子可以放食品,因此厨师休息(厨师线程休眠),一段时间过后服务员将盘子拿回来(服务员线程结束),厨师开始进行生产食品(厨师线程运行),服务员在一旁等待(服务员线程睡眠)…

在此过程中,厨师和服务员两个线程交替睡眠,厨师在做饭时服务员没有端盘子(厨师线程运行时服务员线程睡眠),服务员在端盘子时厨师没有在做饭(服务员线程运行时厨师线程睡眠),确保数据的安全

需要用到的java.lang.Object 中的方法:

变量和类型方法描述
voidnotify()唤醒当前this下的单个线程
voidnotifyAll()唤醒当前this下的所有线程
voidwait()当前线程休眠
voidwait​(long timeoutMillis)当前线程休眠一段时间
voidwait​(long timeoutMillis, int nanos)当前线程休眠一段时间
  • 首先在Food类中加一个标记flag:
    True表示厨师生产,服务员休眠
    False表示服务员端菜,厨师休眠
	private boolean flag = true;
  • 对set方法进行修改
    当且仅当flag为True(True表示厨师生产,服务员休眠)时,才能进行做菜操作
    做菜结束时,将flag置为False(False表示服务员端菜,厨师休眠),这样厨师在生产完之后不会继续生产,避免了厨师两次生产、服务员端走一份的情况
    然后唤醒在当前this下休眠的所有进程,而厨师线程进行休眠
		public synchronized void setNameAneTaste(String name,String taste){
            if(flag){//当标记为true时,表示厨师可以生产,该方法才执行
                this.name = name;
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                this.taste = taste;
                flag = false;//生产完之后,标记置为false,这样厨师在生产完之后不会继续生产,避免了厨师两次生产、服务员端走一份的情况
                this.notifyAll();//唤醒在当前this下休眠的所有进程
                try {
                    this.wait();//此时厨师线程进行休眠
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

        }//end set
  • 对get方法进行修改
    当且仅当flag为False(False表示服务员端菜,厨师休眠)时,才能进行端菜操作
    端菜结束时,将flag置为True(True表示厨师生产,服务员休眠),这样服务员在端完菜之后不会继续端菜,避免了服务员两次端菜、厨师生产一份的情况
    然后唤醒在当前this下休眠的所有进程,而服务员线程进行休眠
        public synchronized void get(){
            if(!flag){//厨师休眠的时候,服务员开始端菜
                System.out.println("服务员端走的菜的名称是:" + this.name + " 味道:" + this.taste);
                flag = true;//端完之后,标记置为true,这样服务员在端完菜之后不会继续端菜,避免了服务员两次端菜、厨师只生产一份的情况
                this.notifyAll();//唤醒在当前this下休眠的所有进程
                try {
                    this.wait();//此时服务员线程进行休眠
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }// end if
        }//end get

作了以上调整之后的程序输出:

我们可以看到,没有出现数据错乱,并且菜的顺序是交替依次进行的

...
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
服务员端走的菜的名称是:小米粥 味道:没味道,不好吃
服务员端走的菜的名称是:老北京鸡肉卷 味道:甜辣味
...

这就是生产者与消费者问题的一个典型栗子


九、 线程的六种状态

java.lang.Object
java.lang.Enum<Thread.State>
java.lang.Thread.State

Enum Thread.State

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

状态描述
NEW尚未启动的线程的线程状态
RUNNABLE可运行线程的线程状态
BLOCKED线程的线程状态被阻塞等待监视器锁定
WAITING等待线程的线程状态
TIMED_WAITING具有指定等待时间的等待线程的线程状态
TERMINATED终止线程的线程状态

NEW

  • 线程刚创建,但是没有启动

RUNNABLE

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

BLOCKED

  • 当一个线程和其他线程排队时

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

WAITING

  • 当一个线程休眠的时候(可以被唤醒)

  • 无限期等待另一个线程执行操作的线程处于此状态

TIMED_WAITING

  • 当一个线程在指定时间内休眠的时候(可以被唤醒)

  • 指定时间内等待另一个线程执行操作的线程处于此状态

TERMINATED

  • 已退出的线程处于此状态

线程在给定时间点只能处于一种状态, 这些状态是虚拟机状态,不反映任何操作系统线程状态

不需要理解如何实现,只需要理解状态种类,理解线程在可运行状态和非运行状态之间的切换情况
在这里插入图片描述


十、 线程池Executors

引入

池:容器的意思

使用一个线程通常要经过创建线程创建任务执行任务关闭线程,在这个过程中,创建任务和执行任务的时间很少

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

线程池就是一个容纳多个线程的容器,池中的线程可以反复使用,省去了频繁创建线程对象的操作,节省了大量的时间和资源

作用

  1. 降低资源消耗

  2. 提高响应速度

  3. 提高线程的可管理性

分类

不论是哪一类,获取线程池的对象都是ExecutorService

1 缓存线程池

长度没有限制

  • 创建缓存线程池:
    .newCachedThreadPool()
//创建缓存线程池
 ExecutorService service = Executors.newCachedThreadPool();
  • 向线程池中加入新的任务,指挥线程池执行新的任务(run):
//向线程池中加入新的任务,指挥线程池执行新的任务(run)
        service.execute(new Runnable() {//execute中传入任务对象即可
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "任务执行");
            }
        });

        //向线程池中加入新的任务,指挥线程池执行新的任务(run)
        service.execute(new Runnable() {//execute中传入任务对象即可
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "任务执行");
            }
        });

        //向线程池中加入新的任务,执行新的任务(run)
        service.execute(new Runnable() {//execute中传入任务对象即可
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "任务执行");
            }
        });

输出结果:

由输出可知,三个线程名称为1,2,3

pool-1-thread-3任务执行
pool-1-thread-1任务执行
pool-1-thread-2任务执行
  • 添加休眠时间,使程序休眠一段时间
	Thread.sleep(1000);//停一秒之后,再去执行线程,此时缓存线程池中已有内容,执行缓存池中的内容
  • 指挥线程池执行任务
    此时缓存池中已有内容,再去执行任务时,执行缓存池中空闲的任务
        //向线程池中加入任务,指挥线程池执行新的任务(run)
        service.execute(new Runnable() {//execute中传入任务对象即可
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "任务执行");
            }
        });

输出结果:

由输出结果可知,线程实现了重复使用,休眠后执行的任务是缓存池中已有的空闲任务3

pool-1-thread-1任务执行
pool-1-thread-3任务执行
pool-1-thread-2任务执行
pool-1-thread-3任务执行

2 定长线程池

相对于缓存线程池,长度有限制,线程池中的当前线程数目不会超过给定的长度

当该值为0的时候,意味着没有任何线程,线程池会终止

代码示例:

  • 创建定长线程池,这里指定线程池大小为2
    .newFixedThreadPool(参数),参数为线程池的长度
//创建定长线程池,指定了线程池的大小为2
		ExecutorService service = Executors.newFixedThreadPool(2);
  • 向线程池中加入新的任务,指挥线程池执行任务
    如下面代码所示,添加3个任务
		//向线程池中加入任务,指挥线程池执行新的任务(run)
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "任务执行");
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "任务执行");
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "任务执行");
            }
        });

输出结果:

由于线程池长度为2,因此最多两个任务,线程池中的当前线程数目不会超过2

pool-1-thread-2任务执行
pool-1-thread-1任务执行
pool-1-thread-1任务执行

3 单线程线程池

与定长线程池中传入参数为1的作用相同,即线程池中只有一个线程

  • 创建单线程线程池
    .newSingleThreadExecutor()
        ExecutorService service = Executors.newSingleThreadExecutor();
  • 向线程池中加入新的任务,指挥线程池执行任务
	service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "任务执行");
            }
        });

        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "任务执行");
            }
        });

        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "任务执行");
            }
        });

        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "任务执行");
            }
        });

输出结果:

由输出可知,线程池中只有一个线程

pool-1-thread-1任务执行
pool-1-thread-1任务执行
pool-1-thread-1任务执行
pool-1-thread-1任务执行

4 周期性任务定长线程池

定长线程池

把一个任务定时在某个时期执行,或者是周期性执行

任务在某个时期执行
  • 创建单线程线程池
    .newScheduledThreadPool(参数),参数为线程池的长度
        //创建 周期性任务定长线程池
        //任务创建出来的结果不一样
        ScheduledExecutorService service = Executors.newScheduledThreadPool(2);
  • 向线程池中加入新的任务,指挥线程池执行任务
    .schedule(参数1,参数2,参数3)
    参数1:定时执行的任务
    参数2:表示时长的数字x(每隔x运行一次任务)
    参数3:时长数字的时间单位,由TimeUnit的常量制定
        /**
         * 定时执行一次
         * 参数1:定时执行的任务
         * 参数2:表示时长的数字x(每隔x运行一次任务)
         * 参数3:时长数字的时间单位,由TimeUnit的常量制定
         */
        service.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "任务执行");
            }
        },5, TimeUnit.SECONDS);//任务在5秒钟后执行

输出结果:

5秒钟后输出

pool-1-thread-1任务执行
周期性执行
  • 创建单线程线程池
    .newScheduledThreadPool(参数),参数为线程池的长度
        //创建 周期性任务定长线程池
        //任务创建出来的结果不一样
        ScheduledExecutorService service = Executors.newScheduledThreadPool(2);
  • 向线程池中加入新的任务,指挥线程池执行任务
    .schedule(参数1,参数2,参数3,参数4)
    参数1:定时执行的任务
    参数2:延迟时长数字n(点击运行程序时n秒后开始运行线程任务)
    参数3:表示时长的数字x(每隔x运行一次任务)
    参数4:时长数字的时间单位,由TimeUnit的常量制定
       /**
         * 周期性执行
         * 参数1:任务
         * 参数2:延迟时长数字n(点击运行程序时n秒后开始运行线程任务)
		 * 参数3:表示时长的数字x(每隔x运行一次任务)
         * 参数4:时长数字的时间单位,由TimeUnit的常量制定
         */
        service.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "任务执行");
            }
        },5,1,TimeUnit.SECONDS);//5秒后执行,每隔1秒执行一次

输出结果:

5秒钟后开始输出,之后每隔1秒输出一次,直到停止程序

pool-1-thread-1任务执行
pool-1-thread-1任务执行
pool-1-thread-1任务执行
pool-1-thread-1任务执行
pool-1-thread-1任务执行

无论是哪种线程池,使用完毕后必须手动关闭线程池,否则会一直在内存中存在


十一、 Lambda表达式

线程举例

首先来写一段冗余的Runnable代码

  • 新建MyRunnable类实现Runnable接口
static class MyRunnable implements Runnable{

        @Override
        public void run() {
//            任务
            System.out.println("任务执行");
        }
}        
  • 在main中执行任务
		//冗余的Runnable代码
        MyRunnable r = new MyRunnable();
        Thread t = new Thread(r);
        t.start();

观察上面的代码,我们可以看到,为了完成某个任务需要写多行代码,代码麻烦,存在冗余

r只用了一次,因此可以用匿名对象(匿名内部类)来实现

        Thread t = new Thread(new Runnable() {//使用匿名内部类来实现
            @Override
            public void run() {
                System.out.println("任务执行");
            }
        });
        t.start();

相比上面的两大段代码,已经简洁了很多

最后来看Lambda表达式的使用

        Thread t = new Thread(() -> {
            System.out.println("任务执行");
        });
        t.start();

明显简洁了很多,具体分析如下图所示
在这里插入图片描述


常见例子

接口必须只有一个方法,才能使用Lambda表达式

  • 定义一个只有一个方法接口
    只包含一个方法,那么在实现接口的时候,一定是为了实现这个方法
    static interface MyMath{
        int sum(int x,int y);
    }
  • 编写print方法
    public static void print(MyMath m,int x,int y){
        int num = m.sum(x,y);
        System.out.println(num);
    }
  • 主函数中去调用这个方法
    使用Lambda表达式中,把匿名内部类的类的部分都删掉,只保留方法参数部分和方法体
 public static void main(String[] args) {
	
		//常规写法
        print(new MyMath() {//实现接口
            @Override
            public int sum(int x, int y) {
                return x+y;
            }
        },100,200);

		//使用Lambda表达式写法
        print((int x, int y) -> {//把匿名内部类的类的部分都删掉,只保留方法参数部分和方法体
                return x+y;
        },100,200);

    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Selcouther

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值