多线程

多线程

1、多线程简介

1.1、什么是线程?

  • 线程(thread)是一个程序内部的一条执行路径。

  • 我们之前启动程序执行后,main方法的执行其实就是一条单独的执行路径。

    public static void main(String[] args) {
        // 代码…
    		for (int i = 0; i < 10; i++) {
    			System.out.println(i);
    	}
    	// 代码...
    }
    
  • 程序中如果只有一条执行路径,那么这个程序就是单线程的程序。

1.2、多线程是什么?

  • 多线程是指从软硬件上实现多条执行流程的技术。

1.3、多线程用在哪里,有什么好处

  • 再例如:消息通信、淘宝、京东系统都离不开多线程技术。

1.4、多线程内容要求

1.4.1、多线程的创建

  • 如何在程序中实现多线程,有哪些方式,各自有什么优缺点。

1.4.2、Thread类的常用方法

  • 线程的代表是Thread类,Thread提供了哪些线程的操作给我们呢?

1.4.3、线程安全、线程同步

  • 多个线程同时访问一个共享的数据的时候会出现问题,如何去解决?

1.4.4、线程通信、线程池

  • 线程与线程间需要配合完成一些事情。
  • 线程池是一种线程优化方案,可以用一种更好的方式使用多线程。

1.4.5、定时器、线程状态等

  • 如何在程序中实现定时器?线程在执行的过程中会有很多不同的状态,理解这些状态有助于理解线程的执行原理,也有利于面试

2、多线程的创建

2.1、方式一:继承Thread类

2.1.1、Thread类

  • Java是通过java.lang.Thread 类来代表线程的。
  • 按照面向对象的思想,Thread类应该提供了实现多线程的方式。

2.1.2、多线程的实现方案一:继承Thread类

  • 定义一个子类MyThread继承线程类java.lang.Thread,重写run()方法
  • 创建MyThread类的对象
  • 调用线程对象的start()方法启动线程(启动后还是执行run方法的)
/**
 * 1、定义一个子类MyThread继承线程类java.lang.Thread,重写run()方法
 */
public class MyThread extends Thread{
    //自定义了一个线程类继承Thead类
    /**
     *2、重写run方法,里面定义线程以后要干什么
     */
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("子线程已经执行!!!"+i);
        }
    }
}
--------------------------------------------------------
/**
 * 目标;多线程的创建方式一;继承Thread类实现
 */
public class ThreadDemp1 {
    public static void main(String[] args) {
        //3、new 一个新线程对象
        Thread myThread=new MyThread();//多态
        //4、调用start方法启动线程(执行的还是run方法)
        myThread.start();
        for (int i = 0; i < 5; i++) {
            System.out.println("主线程已经执行!!!"+i);
        }
    }
}

2.1.3、方式一优缺点

  • 优点:编码简单
  • 缺点:线程类已经继承Thread,无法继承其他类,不利于扩展。

2.1.4、思考

为什么不直接调用了run方法,而是调用start启动线程。
  • 直接调用run方法会当成普通方法执行,此时相当于还是单线程执行。
  • 只有调用start方法才是启动一个新的线程执行。
把主线程任务放在子线程之前了。
  • 这样主线程一直是先跑完的,相当于是一个单线程的效果了。

2.1.5、小节

方式一是如何实现多线程的?
  • 继承Thread类
  • 重写run方法
  • 创建线程对象
  • 调用start()方法启动。
优缺点是什么?
  • 优点:编码简单
  • 缺点:存在单继承的局限性,线程类继承Thread后,不能继承其他类,不便于扩展

2.2、方式二:实现Runnable接口

2.2.1、多线程的实现方案二:实现Runnable接口

  1. 定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法
  2. 创建MyRunnable任务对象
  3. 把MyRunnable任务对象交给Thread处理。
  4. 调用线程对象的start()方法启动线程

2.2.2、Thread的构造器

构造器 说明
public Thread(String name) 可以为当前线程指定名称
public Thread(Runnable target) 封装Runnable对象成为线程对象
public Thread(Runnable target ,String name ) 封装Runnable对象成为线程对象,并指定线程名称
/**
 * 1、定义一个线程任务类实现Rannable接口
 */
public class Myrannable implements Runnable{
    @Override
    public void run() {
        //2、重写run方法,定义线程任务
        for (int i = 0; i <10 ; i++) {
            System.out.println("子线程已执行"+i);
        }
    }
}
--------------------------------------------------------
/**
 * 目标;学会线程的创建方式二,理解它的优缺点
 */
public class ThreadDemo {
    public static void main(String[] args) {
        //3、创建一个任务对象
        Runnable terger=new Myrannable();//多态
        //4、将任务对象交个线程对象
        Thread thread=new Thread(terger);
        //5、启动线程
        thread.start();
        for (int i = 0; i <10 ; i++) {
            System.out.println("主线程已执行"+i);
        }
    }
}    

2.2.3、方式二优缺点

  • 优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强。
  • 缺点:编程多一层对象包装,如果线程有执行结果是不可以直接返回的。

2.2.4、小节

第二种方式是如何创建线程的?
  • 定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法
  • 创建MyRunnable对象
  • 把MyRunnable任务对象交给Thread线程对象处理。
  • 调用线程对象的start()方法启动线程
第二种方式的优点
  • 优点:线程任务类只是实现了Runnale接口,可以继续继承和实现。
  • 缺点:如果线程有执行结果是不能直接返回的。

2.2.5、补充

多线程的实现方案二:实现Runnable接口(匿名内部类形式)

①可以创建Runnable的匿名内部类对象。

②交给Thread处理。

③调用线程对象的start()启动线程。

注:接口可以创建匿名内部类的对象

public static void main(String[] args) {
    //3、创建一个任务对象
    Runnable terger=new Runnable() {
        @Override
        public void run() {
            //2、重写run方法,定义线程任务
            for (int i = 0; i <10 ; i++) {
                System.out.println("子线程已执行"+i);
            }
        }
    };
    //4、将任务对象交个线程对象
    Thread thread=new Thread(terger);
    //5、启动线程
    thread.start();
    for (int i = 0; i <10 ; i++) {
        System.out.println("主线程已执行"+i);
    }
}
------------------------一级简化--------------------------
public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i <10 ; i++) {
                    System.out.println("子线程已执行"+i);
                }
            }
        }).start();
        for (int i = 0; i <10 ; i++) {
            System.out.println("主线程已执行"+i);
        }
    }//通过不断取代变量已达到简化代码的作用
------------------------二级简化--------------------------
public static void main(String[] args) {
        new Thread(()-> { for (int i = 0; i <10 ; i++) System.out.println("子线程已执行"+i);}).start();
        for (int i = 0; i <10 ; i++) {
            System.out.println("主线程已执行"+i);
        }
    }    

2.3、方式三:JDK 5.0新增:实现Callable接口

2.3.1、思考

前2种线程创建方式都存在一个问题
  • 他们重写的run方法均不能直接返回结果。
  • 不适合需要返回线程执行结果的业务场景。
怎么解决这个问题呢?
  • JDK 5.0提供了Callable和FutureTask来实现。
  • 这种方式的优点是:可以得到线程执行的结果。

2.3.2、FutureTask的API

方法名称 说明
public FutureTask<>(Callable call) 把Callable对象封装成FutureTask对象。
public V get() throws Exception 获取线程执行call方法返回的结果。
import java.util.concurrent.Callable;
//1、定义一个任务类实现Callable接口,应该声明线程任务执行完毕后的结果的数据类型
public class MyCallable implements Callable<String> {
    private int n;
    //2、重写Call方法
    public MyCallable() {
    }
    public MyCallable(int n) {
        this.n = n;
    }
    /**
     * 线程的任务方法(求1到n的和)
     * @return 返回值
     * @throws Exception
     */
    @Override
    public String call() throws Exception {
        int temp=0;
        for (int i = 1; i <=n; i++) {
            temp+=i;
        }
        return "子线程执行结果为"+temp;
    }
}
--------------------------------------------------------
public static void main(String[] args) {
        //3、创建任务对象
        Callable<String>call=new MyCallable(100);//这个不是线程,是任务对象
        //4、因为Thread对象只接收Runnable任务对象,所以要把Callab任务对象交给FutureTask对象
        //FutureTask的作用1:是Runnable对象吗,可以交给Thread了
        //FutureTask的作用2:可以在线程结束之后调用其get方法得到线程的结果
        FutureTask<String> ft=new FutureTask<>(call);
        //5、交给线程处理
        Thread thread=new Thread(ft);
        //6、启动线程
        thread.start();

        FutureTask<String> ft1=new FutureTask<>(new MyCallable(200));
        new Thread(ft1).start();

        try {
            System.out.println("一号"+ft.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

        try {
            System.out.println("二号"+ft1.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

    }    

FutureTask的作用

  • FutureTask的作用1:是Runnable对象吗,可以交给Thread了
  • FutureTask的作用2:可以在线程结束之后调用其get方法得到线程的结果

2.3.3、方式三优缺点

  • 优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强。
  • 可以在线程执行完毕后去获取线程执行的结果。
  • 缺点:编码复杂一点。

2.3.4、三种方法小节

方式 优点 缺点
继承Thread类 编程比较简单,可以直接使用Thread类中的方法 扩展性较差,不能再继承其他的类,不能返回线程执行的结果
实现Runnable接口 扩展性强,实现该接口的同时还可以继承其他的类。 编程相对复杂,不能返回线程执行的结果
实现Callable接口 扩展性强,实现该接口的同时还可以继承其他的类。可以得到线程执行的结果 编程相对复杂

2.3.5、补充

多线程的实现方案三:利用Callable、FutureTask接口实现。
  • 得到任务对象
    • 定义类实现Callable接口,重写call方法,封装要做的事情。
    • 用FutureTask把Callable对象封装成线程任务对象。
  • 把线程任务对象交给Thread处理。
  • 调用Thread的start方法启动线程,执行任务
  • 线程执行完毕后、通过FutureTask的get方法去获取任务执行的结果。

3、Thread的常用方法

3.1、Thread常用API说明

  • Thread常用方法:获取线程名称getName()、设置名称setName()、获取当前线程对象currentThread()。
  • 至于Thread类提供的诸如:yield、join、interrupt、不推荐的方法 stop 、守护线程、线程优先级等线程的控制方法,在开发中很少使用,这些方法会在高级篇以及后续需要用到的时候再为大家讲解。

3.2、思考

当有很多线程在执行的时候,我们怎么去区分这些线程呢?

  • 此时需要使用Thread的常用方法:getName()、setName()、currentThread()等。

3.3、Thread获取和设置线程名称

方法名称 说明
public static Thread currentThread(): 返回对当前正在执行的线程对象的引用

注意

  1. 此方法是Thread类的静态方法,可以直接使用Thread类调用。
  2. 这个方法是在哪个线程执行中调用的,就会得到哪个线程对象。
public class MyThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(getName()+"已输出!!!"+i);
        }
    }
}
--------------------------------------------------------
//main方法是由主线程负责控制调度的
    public static void main(String[] args) {
        Thread thread=new MyThread();
    //        thread.setName("鲁班一号");
        thread.start();

        Thread thread1=new MyThread();
    //        thread1.setName("鲁班二号");
        thread1.start();

        //哪个线程执行它,它就得到哪个线程对象(当前线程对象)
        Thread t=Thread.currentThread();

        for (int i = 0; i < 5; i++) {
            System.out.println(t.getName()+"已输出!!!"+i);
        }
    }    

3.4、Thread的构造器

方法名称 说明
public Thread(String name) 可以为当前线程指定名称
public Thread(Runnable target) 封装Runnable对象成为线程对象
public Thread(Runnable target ,String name ) 封装Runnable对象成为线程对象,并指定线程名称
public class MyThread extends Thread{
    public MyThread() {
    }
    public MyThread(String name) {
        //为当前对象设置名称送给父类的有参构造器初始化名称
        super(name);
    }
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(getName()+"已输出!!!"+i);
        }
    }
}
--------------------------------------------------------
public static void main(String[] args) {
        Thread thread=new MyThread("一号");
        thread.start();

        Thread thread1=new MyThread("二号");
        thread1.start();

        //哪个线程执行它,它就得到哪个线程对象(当前线程对象)
        Thread t=Thread.currentThread();
        t.setName("零号");
        for (int i = 0; i < 5; i++) {
            System.out.println(t.getName()+"已输出!!!"+i);
        }
    }    

3.5、Thread类的线程休眠方法

方法名称 说明
public static void sleep(long time) 让当前线程休眠指定的时间后再继续执行,单位为毫秒。
public static void main(String[] args) throws InterruptedException {
    for (int i = 1; i <= 5; i++) {
        System.out.println("输出"+i);
        if(i==3)
        {
            //让线程进入休眠
            Thread.sleep(3000);
        }
    }
}

3.6、小节

Thread常用方法、构造器

方法名称 说明
String getName() 获取当前线程的名称,默认线程名称是Thread-索引
void setName(String name) 设置线程名称
public static Thread currentThread(): 返回对当前正在执行的线程对象的引用
public static void sleep(long time) 让线程休眠指定的时间,单位为毫秒。
public void run() 线程任务方法
public void start() 线程启动方法
构造器 说明
public Thread(String name) 可以为当前线程指定名称
public Thread(Runnable target) 把Runnable对象交给线程对象
public Thread(Runnable target ,String name ) 把Runnable对象交给线程对象,并指定线程名称

4、线程安全

4.1、线程安全问题是什么、发生的原因

4.1.1、线程安全问题

  • 多个线程同时操作同一个共享资源的时候可能会出现业务安全问题,称为线程安全问题。

4.1.2、取钱模型演示

需求:小明和小红是一对夫妻,他们有一个共同的账户,余额是10万元。
如果小明和小红同时来取钱,而且2人都要取钱10万元,可能出现什么问题呢?

4.1.3、小节

线程安全问题出现的原因?
  • 存在多线程并发
  • 同时访问共享资源
  • 存在修改共享资源

4.2、线程安全问题案例模拟

案例

需求:

小明和小红是一对夫妻,他们有一个共同的账户,余额是10万元,模拟2人同时去取钱10万。
分析:
①:需要提供一个账户类,创建一个账户对象代表2个人的共享账户。
②:需要定义一个线程类,线程类可以处理账户对象。
③:创建2个线程对象,传入同一个账户对象。
④:启动2个线程,去同一个账户对象中取钱10万。

public class Account {
    private String cardId;
    private double money;

    public Account(String cardId, double money) {
        this.cardId = cardId;
        this.money = money;
    }
    @Override
    public String toString() {
        return "Account{" +
                "cardId='" + cardId + '\'' +
                ", money=" + money +
                '}';
    }

    public void drewMoney(double money) {
        if(this.money>=money) {
            System.out.println(Thread.currentThread().getName()+"来取钱成功,吐出"+money);
            this.money -= money;
            System.out.println("当前账户余额为;"+this.money);
        }
        else
            System.out.println("账户余额不足!!");
    }
}
--------------------------------------------------------
//2,创建线程类
public class DrawThread extends Thread{
    private Account acc;

    public DrawThread(Account acc,String name) {
        super(name);
        this.acc = acc;
    }

    @Override
    public void run() {
        //小明 小红 取出钱
        System.out.println("您好"+getName()+"先生/女士,现在正在办理取钱业务,请等待!!!");

        acc.drewMoney(100000);
    }
}
--------------------------------------------------------
public static void main(String[] args) {
        //1、定义线程类创建账户对象
        Account acc=new Account("144514",100000);

        //2、创建两个线程对象代表小明和小红来取钱
        new DrawThread(acc,"小明").start();
        new DrawThread(acc,"小红").start();
    }    

小节

线程安全问题发生的原因是什么?
  • 多个线程同时访问同一个共享资源且存在修改该资源。

5、线程同步

5.1、同步思想概述

5.1.1、为什么要线程同步

  • 为了解决线程安全问题。

5.1.2、思考

取钱案例出现问题的原因?
  • 多个线程同时执行,发现账户都是够钱的。
如何才能保证线程安全呢?
  • 让多个线程实现先后依次访问共享资源,这样就解决了安全问题

5.1.3、线程同步的核心思想

  • 加锁,把共享资源进行上锁,每次只能一个线程进入访问完毕以后解锁,然后其他线程才能进来。

5.1.4、小节

线程同步解决安全问题的思想是什么?
  • 加锁:让多个线程实现先后依次访问共享资源,这样就解决了安全问题。

5.2、方式一:同步代码块

5.2.1、同步代码块简述

  • 作用:把出现线程安全问题的核心代码给上锁。
  • 原理:每次只能一个线程进入,执行完毕后自动解锁,其他线程才可以进来执行。
synchronized(同步锁对象) {
操作共享资源的代码(核心代码)
}

5.2.2、锁对象要求

  • 理论上:锁对象只要对于当前同时执行的线程来说是同一个对象即可。(随便取,只要该对象唯一)

5.2.3、思考

锁对象用任意唯一的对象好不好呢?
  • 不好,会影响其他无关线程的执行。
  • 相当于锁是唯一的,无论谁执行这段代码,都必须排队
锁对象的规范要求
  • 规范上:建议使用共享资源作为锁对象。
  • 对于实例方法建议使用this作为锁对象。
  • 对于静态方法建议使用字节码(类名.class)对象作为锁对象。
// 同步代码块
// 小明 小红
// this == acc 共享账户
synchronized (this) {
    // 2、判断余额是否足够
    if(this.money >= money){
        // 钱够了
        System.out.println(name+"来取钱,吐出:" + money);
        // 更新余额
        this.money -= money;
        System.out.println(name+"取钱后,余额剩余:" + this.money);
    }else{
        // 3、余额不足
        System.out.println(name+"来取钱,余额不足!");
    }
}

5.2.4、小节

同步代码块是如何实现线程安全的?
  • 对出现问题的核心代码使用synchronized进行加锁
  • 每次只能一个线程占锁进入访问
同步代码块的同步锁对象有什么要求?
  • 对于实例方法建议使用this作为锁对象。
  • 对于静态方法建议使用字节码(类名.class)对象作为锁对象。

5.3、方式二:同步方法

5.3.1、同步方法简述

  • 作用:把出现线程安全问题的核心方法给上锁。
  • 原理:每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行。

5.3.2、格式

修饰符 synchronized 返回值类型 方法名称(形参列表) {
操作共享资源的代码
}

5.3.3、同步方法的底层原理

  • 同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码。
  • 如果方法是实例方法:同步方法默认用this作为的锁对象。但是代码要高度面向对象!
  • 如果方法是静态方法:同步方法默认用类名.class作为的锁对象。

5.3.4、思考

是同步代码块好还是同步方法好一点?
  • 同步代码块锁的范围更小,同步方法锁的范围更大。

5.3.5、小节

同步方法是如何保证线程安全的?
  • 对出现问题的核心方法使用synchronized修饰
  • 每次只能一个线程占锁进入访问
同步方法的同步锁对象的原理?
  • 对于实例方法默认使用this作为锁对象。
  • 对于静态方法默认使用类名.class对象作为锁对象。

5.4、方式三:Lock锁

5.4.1、Lock锁简介

  • 为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock,更加灵活、方便。
  • Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作。
  • Lock是接口不能直接实例化,这里采用它的实现类Reentrant Lock来构建Lock锁对象。
方法名称 说明
public Reentrant Lock() 获得Lock锁的实现类对象

5.4.2、Lock的API

方法名称 说明
void lock() 获得锁
void unlock() 释放锁
 						标准格式
//唯一不可替换
    private final Lock lock=new ReentrantLock();
lock.lock();//上锁
        try {
            (可能会出错的代码)
            }
        } catch (Exception e) {
            (捕获异常并处理)
            e.printStackTrace();
        } finally {
            lock.unlock();//关锁
        }
--------------------------------------------------------
//唯一不可替换
private final Lock lock=new ReentrantLock();
lock.lock();//上锁
        try {
            if(this.money >= money){
                // 钱够了
                System.out.println(name+"来取钱,吐出:" + money);
                // 更新余额
                this.money -= money;
                System.out.println(name+"取钱后,余额剩余:" + this.money);
            }else{
                // 3、余额不足
                System.out.println(name+"来取钱,余额不足!");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

6、线程通信

6.1、什么是线程通信、如何实现?

  • 所谓线程通信就是线程间相互发送数据,线程间共享一个资源即可实现线程通信。

6.2、线程通信常见形式

  • 通过共享一个数据的方式实现。
  • 根据共享数据的情况决定自己该怎么做,以及通知其他线程怎么做。

6.3、线程通信实际应用场景

  • 生产者与消费者模型:生产者线程负责生产数据,消费者线程负责消费生产者产生的数据。
  • 要求:生产者线程生产完数据后唤醒消费者,然后等待自己,消费者消费完该数据后唤醒生产者,然后等待自己。

6.4、线程通信案例模拟

  • 模拟客服系统,系统可以不断的接入电话 和 分发给客服。

  • 线程通信的前提:线程通信通常是在多个线程操作同一个共享资源的时候需要进行通信,且要保证线程安全。

6.5、Object类的等待和唤醒方法

方法名称 说明
void wait() 让当前线程等待并释放所占锁,直到另一个线程调用notify()方法或 notifyAll()方法
void notify() 唤醒正在等待的单个线程
void notifyAll() 唤醒正在等待的所有线程

注意

  • 上述方法应该使用当前同步锁对象进行调用。
public class Phone {
    //实现线程间通信,默认手机当前处于等待来电提醒
    private boolean flag=false;
    public void run(){
        //a、负责来电提醒的线程
        new  Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    while (true){
                        //持续不断的接收来电提醒
                       synchronized (Phone.this){
                           if(!flag){
                               //接收到来电请求
                               System.out.println("您有新的来电!");
                               flag=true;//更改信号为true等待其他线程执行
                               //先唤醒别人再等待自己
                               Phone.this.notify();//唤醒其他线程
                               Phone.this.wait();//等待自己
                           }

                       }
                   }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();
        //b、接电话线程,正式接听了
        new  Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    while (true){
                        //持续不断的等待来电
                        synchronized (Phone.this){//Phone.this 锁对象
                            if(flag){
                                //可以接听电话了
                                System.out.println("电话接听中,通话5分结束!");
                                Thread.sleep(2000);
                                flag=false;//更改信号为false等待继续来电
                                //先唤醒别人再等待自己
                                Phone.this.notify();//唤醒其他线程
                                Phone.this.wait();//等待自己
                            }else {//如果一开始就是接电话线程执行,就必须让其他线程先使用,自己等待
                                Phone.this.notify();//唤醒其他线程
                                Phone.this.wait();//等待自己
                            }
                        }

                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();

    }

    public static void main(String[] args) {
        //1、创建一个手机对象
        Phone huawei=new Phone();
        huawei.run();//手机开机
    }
}

6.6、小节

线程通信的三个方法

方法名称 说明
void wait() 当前线程等待,直到另一个线程调用notify() 或 notifyAll()唤醒自己
void notify() 唤醒正在等待对象监视器(锁对象)的单个线程
void notifyAll() 唤醒正在等待对象监视器(锁对象)的所有线程

注意

  • 上述方法应该使用当前同步锁对象进行调用。

6.7、多线程与锁机制练习题

哲学家就餐问题
package com.gewuyou.d7_thread_comunication;
/**
 * 目标;解决哲学家就餐问题
 * 假设有五位哲学家围坐在一张圆形餐桌旁,做以下两件事情之一:吃饭,或者思考。吃东西的时候,
 * 他们就停止思考,思考的时候也停止吃东西。餐桌中间有一大碗意大利面,每两个哲学家之间有一只餐叉。
 * 因为用一只餐叉很难吃到意大利面,所以假设哲学家必须用两只餐叉吃东西。他们只能使用自己左右手边的那两只餐叉。
 * 约束条件;
 * 1、只有拿到筷子时哲学家才会吃饭
 * 2、如果筷子已经被别人拿走,则必须等到别人吃完才能拿到筷子
 * 3.任一哲学家在自己未拿到筷子吃饭前是不会放下手中已经拿到的筷子
 * 分析
 * 关键点在筷子上,所以只要研究筷子如何分配即可,定义一个数组用来存储筷子,将每一位哲学家看做一个线程
 * 我们只需要先定义好某个哲学家的行为,然后创建五个哲学家线程就行了
 */
 class Philosopher extends Thread {
    private String name;//哲学家的名字
    private Fork fork;//筷子

    public Philosopher(String name, Fork fork)//有参构造器
    {
        super(name);//将接收的哲学家名字传递给父类线程
        this.name = name;
        this.fork = fork;
    }

    //  重写run方法,写出某个哲学家的行为
    @Override
    public void run() {
        while (true) {
            thinking();//思考
            fork.takeFork();//判断是否能够吃饭,不能则等待,可以则吃饭,占用筷子
            eating();//吃饭
            fork.putFork();//解除筷子的占用
        }

    }

    private void eating() {
        System.out.println(name + "正在吃饭~~~");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private void thinking() {
        System.out.println(name + "正在思考~~~");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

    static class Fork {              //   0      1      2     3       4
        private boolean[] chopsticks = {false, false, false, false, false};//当数组中筷子被使用时,让该位置的数为true

        /**
         * 只有当某个哲学家左右的筷子未被占用时才能使用筷子
         * 使用筷子的方法
         */
        public synchronized void takeFork() {//通过同步方法来给该方法上锁
            String name = Thread.currentThread().getName();
            int i = Integer.parseInt(name);
            while (chopsticks[i] || chopsticks[((i + 1) % 5)]) {//判断左右两根筷子是否被占用
                //存在筷子被占用
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //筷子处于空闲状态,使用筷子
            chopsticks[i] = true;
            chopsticks[(i + 1) % 5] = true;
        }

        public synchronized void putFork() {
            //吃完饭后应当解除占用
            String name = Thread.currentThread().getName();
            int i = Integer.parseInt(name);
            chopsticks[i] = false;
            chopsticks[(i + 1) % 5] = false;
            //唤醒其他线程
            notifyAll();
        }
        //开始测试
        public static void main(String []args){
            Fork fork=new Fork();
            new Philosopher("0",fork).start();
            new Philosopher("1",fork).start();
            new Philosopher("2",fork).start();
            new Philosopher("3",fork).start();
            new Philosopher("4",fork).start();
        }


    }

}
过桥问题

一条小河上有一座独木桥,规定每次只允许一人过桥。如果把每个过桥这看作一个进程,为保证安全,请用信号量操作实现正确管理。

public class Human extends Thread{

    private String name;
    private Bridge bridge;

    /**
     * 关于人的有参构造器
     * @param name 线程名
     * @param bridge 桥状态
     */
    public Human(String name,Bridge bridge){
        super(name);
        this.name=name;
        this.bridge=bridge;
    }

    /**
     * 重写run方法
     */
    @Override
    public void run() {
        while (true) {
            //等待过桥
            waitfor();
            //判断是否有人过桥
            bridge.putjudge();
            //过桥,更改桥状态
            adopt();
            //过桥完毕恢复桥
            bridge.outjudge();
        }
    }



    //过桥
    private void adopt() {
        System.out.println(Thread.currentThread().getName()+"正在过桥!!!");
        try
        {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    //等待过桥
    private void waitfor() {
        System.out.println(Thread.currentThread().getName()+"等待过桥!!!");
        try
        {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
class Bridge{
    private boolean bridge=false;
    public final synchronized void putjudge(){
        while (bridge){

            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
        bridge=true;
    }
    public final synchronized void outjudge(){
        bridge=false;
        notifyAll();
    }

    public static void main(String[] args) {
        Bridge bridge=new Bridge();
        new Human("李田所",bridge).start();
        new Human("香蕉君",bridge).start();

    }
}
橘子与苹果

桌子上有一只盘子,最多可容纳两个水果,每次只能放入或取出一个水果。爸爸专向盘子中放苹果,妈妈专向盘子中放橘子,两个儿子专等吃盘子中的橘子,两个女儿专等吃盘子中的苹果。请用信号量操作来实现爸爸、妈妈、儿子、女儿之间的同步与互斥关系。

/**
 * 这个是一个盘子类,里面装了放和取操作
 */
public class Plate {
    private int plate[]={0,0};
    //定义一个上锁的方法放入苹果或者橘子

    /**
     * 这个是放入水果
     * @param fruit 水果编号 1代表苹果 2代表橘子
     */
    public final synchronized void put(int fruit){
        String fruitName;//水果名称
        if(fruit==1)
        {
            fruitName="苹果";
        }else
        {
            fruitName="橘子";
        }
        if(plate[0]==0||plate[1]==0)//如果盘子有一个是空的
        {
            if(plate[0]==0)//如果第一个盘子是空的
            {
                plate[0]=fruit;
            }else//或者第二个盘子是空的
            {
                plate[1]=fruit;
            }
            System.out.println(Thread.currentThread().getName()+"放入了一个"+fruitName+"!!!");
            notifyAll();//唤醒线程

        }
        if (plate[0]!=0&&plate[1]!=0)//如果盘子不为空
        {
            try {
                wait();//停止线程
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    //定义一个上锁的方法取出苹果或者橘子

    /**
     * 这个是取出水果
     * @param fruit 水果编号
     */
    public final synchronized void out(int fruit){
        String fruitName;
        if(fruit==1)
        {
            fruitName="苹果";
        }else
        {
            fruitName="橘子";
        }
        if (plate[0]==fruit||plate[1]==fruit)//如果盘子中存在要的水果
        {
            if(plate[0]==fruit)//第一个盘子有
            {
                plate[0]=0;
            }else//第二个盘子有
            {
                plate[1]=0;
            }
            System.out.println(Thread.currentThread().getName()+"拿走了一个"+fruitName+"!!!");
            notifyAll();//唤醒线程
        }
        if (plate[0]!=fruit&&plate[1]!=fruit)//如果盘子没有想要的水果
        {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

7、线程池

7.1、线程池概述

7.1.1、何为线程池

  • 线程池就是一个可以复用线程的技术。

7.1.2、不使用线程池的问题

  • 如果用户每发起一个请求,后台就创建一个新线程来处理,下次新任务来了又要创建新线程,而创建新线程的开销是很大的,这样会严重影响系统的性能。

7.1.3、线程池的工作原理

7.2、线程池实现的API、参数说明

7.2.1、谁代表线程池

  • JDK 5.0起提供了代表线程池的接口:ExecutorService

7.2.2、如何得到线程池对象

  • 方式一:使用Executor Service的实现类Thread Pool Executor自创建一个线程池对象

  • 方式二:使用Executors(线程池的工具类)调用方法返回不同特点的线程池对象

7.2.3、ThreadPoolExecutor构造器的参数说明

public ThreadPoolExecutor(int corePoolSize,
						  int maximumPoolSize,
						  long keepAliveTime,
						  TimeUnit unit,
					BlockingQueue<Runnable> workQueue,
						  ThreadFactory threadFactory,
					RejectedExecutionHandler handler) 
  • 参数一:指定线程池的线程数量(核心线程): corePoolSize(不能小于0)
  • 参数二:指定线程池可支持的最大线程数: maximumPoolSize(最大数量 >= 核心线程数量)
  • 参数三:指定临时线程的最大存活时间: keepAliveTime(不能小于0)
  • 参数四:指定存活时间的单位(秒、分、时、天): unit(时间单位)
  • 参数五:指定任务队列: workQueue(能为null)
  • 参数六:指定用哪个线程工厂创建线程: threadFactory(不能为null)
  • 参数七:指定线程忙,任务满的时候,新任务来了怎么办: handler(不能为null)

7.2.4、线程池常见面试题

临时线程什么时候创建啊?
  • 新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程。
什么时候开始拒绝任务
  • 核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始任务拒绝。

7.2.5、小节

谁代表线程池?
  • ExecutorService接口
ThreadPoolExecutor实现线程池对象的七个参数是什么意思?
  • 使用线程池的实现类ThreadPoolExecutor
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) 

7.3、线程池处理Runnable任务

7.3.1、ThreadPoolExecutor创建线程池对象示例

ExecutorService pools = new ThreadPoolExecutor(3, 5
, 8 , TimeUnit.SECONDS, new ArrayBlockingQueue<>(6),
Executors.defaultThreadFactory() , new ThreadPoolExecutor.AbortPolicy());

7.3.2、ExecutorService的常用方法

方法名称 说明
void execute(Runnable command) 执行任务/命令,没有返回值,一般用来执行 Runnable 任务
Future submit(Callable task) 执行任务,返回未来任务对象获取线程结果,一般拿来执行 Callable 任务
void shutdown() 等任务执行完毕后关闭线程池
List shutdownNow() 立刻关闭,停止正在执行的任务,并返回队列中未执行的任务

7.3.2、新任务拒绝策略

策略 详解
ThreadPoolExecutor.AbortPolicy 丢弃任务并抛出RejectedExecutionException异常。是默认的策略
ThreadPoolExecutor.DiscardPolicy: 丢弃任务,但是不抛出异常 这是不推荐的做法
ThreadPoolExecutor.DiscardOldestPolicy 抛弃队列中等待最久的任务 然后把当前任务加入队列中
ThreadPoolExecutor.CallerRunsPolicy 由主线程负责调用任务的run()方法从而绕过线程池直接执行

7.3.3、小节

线程池如何处理Runnable任务
  • 使用ExecutorService的方法:
  • void execute(Runnable target)

7.4、线程池处理Callable任务

7.4.1、ExecutorService的常用方法

方法名称 说明
void execute(Runnable command) 执行任务/命令,没有返回值,一般用来执行 Runnable 任务
Future submit(Callable task) 执行Callable任务,返回未来任务对象获取线程结果
void shutdown() 等任务执行完毕后关闭线程池
List shutdownNow() 立刻关闭,停止正在执行的任务,并返回队列中未执行的任务

7.4.2、小节

线程池如何处理Callable任务,并得到任务执行完后返回的结果。
  • 使用ExecutorService的方法:
  • Future submit(Callable command)

7.5、Executors工具类实现线程池

7.5.1、Executors得到线程池对象的常用方法

  • Executors:线程池的工具类通过调用方法返回不同类型的线程池对象。
方法名称 说明
public static ExecutorService newCachedThreadPool() 线程数量随着任务增加而增加,如果线程任务执行完毕且空闲了一段时间则会被回收掉。
public static ExecutorService newFixedThreadPool(int nThreads) 创建固定线程数量的线程池,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程替代它。
public static ExecutorService newSingleThreadExecutor () 创建只有一个线程的线程池对象,如果该线程出现异常而结束,那么线程池会补充一个新线程。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) 创建一个线程池,可以实现在给定的延迟后运行任务,或者定期执行任务。

注意:Executors的底层其实也是基于线程池的实现类ThreadPoolExecutor创建线程池对象的。

7.5.2、Executors使用可能存在的陷阱

  • 大型并发系统环境中使用Executors如果不注意可能会出现系统风险。
方法名称 存在问题
public static ExecutorService newFixedThreadPool(int nThreads) 允许请求的任务队列长度是Integer.MAX_VALUE,可能出现
public static ExecutorService newSingleThreadExecutor() OOM错误( java.lang.OutOfMemoryError )
public static ExecutorService newCachedThreadPool() 创建的线程数量最大上限是Integer.MAX_VALUE, 线程数可能会随着任务1:1增长
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) ,也可能出现OOM错误( java.lang.OutOfMemoryError )

7.5.3、小节

Executors工具类底层是基于什么方式实现的线程池对象?
  • 线程池ExecutorService的实现类:ThreadPoolExecutor
Executors是否适合做大型互联网场景的线程池方案?
  • 不合适。
  • 建议使用ThreadPoolExecutor来指定线程池参数,这样可以明确线程池的运行规则,规避资源耗尽的风险。

8、补充知识:定时器

8.1、定时器简述

  • 定时器是一种控制任务延时调用,或者周期调用的技术。
  • 作用:闹钟、定时邮件发送。

8.2、定时器的实现方式

  • 方式一:Timer
  • 方式二: ScheduledExecutorService

8.3、Timer定时器

构造器 说明
public Timer() 创建Timer定时器对象
方法 说明
public void schedule([TimerTask](file:///D:/course/%E5%9F%BA%E7%A1%80%E9%98%B6%E6%AE%B5/API%E6%96%87%E6%A1%A3/docs/api/java.base/java/util/TimerTask.html) task, long delay, long period) 开启一个定时器,按照计划处理TimerTask任务
public static void main(String[] args) {
    //1、创建timer定时器
    Timer timer=new Timer();//定时器本身就是一个线程
    //2、调用方法处理定时任务
    //TimerTask为定时器任务 delay为启动时等待时间的毫秒值 period为任务的执行周期
    timer.schedule(new TimerTask() {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()+"执行一次");
        }
    },3000,2000);
}

8.4、Timer定时器的特点和存在的问题

  1. Timer是单线程,处理多个任务按照顺序执行,存在延时与设置定时器的时间有出入。
  2. 可能因为其中的某个任务的异常使Timer线程死掉,从而影响后续任务执行。

8.5、ScheduledExecutorService定时器

  • ScheduledExecutorService是 jdk1.5中引入了并发包,目的是为了弥补Timer的缺陷, ScheduledExecutorService内部为线程池。
Executors的方法 说明
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) 得到线程池对象
ScheduledExecutorService的方法 说明
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period,TimeUnit unit) 周期调度方法
public static void main(String[] args) {
    //1、创建一个ScheduledExecutorService线程池作定时器
    ScheduledExecutorService pool= Executors.newScheduledThreadPool(3);
    //2、开启定时任务
    pool.scheduleAtFixedRate(new TimerTask() {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()+"输出:AA  "+new Date());
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    },0,2, TimeUnit.SECONDS);
    pool.scheduleAtFixedRate(new TimerTask() {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()+"输出:BB  "+new Date());
            System.out.println(10/0);
        }
    },0,2, TimeUnit.SECONDS);
    pool.scheduleAtFixedRate(new TimerTask() {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()+"输出:CC  "+new Date());
        }
    },0,2, TimeUnit.SECONDS);
}

8.6、ScheduledExecutorService的优点

  • 基于线程池,某个任务的执行情况不会影响其他定时任务的执行。

9、补充知识:并发、并行

9.1、并发与并行简述

  • 正在运行的程序(软件)就是一个独立的进程, 线程是属于进程的,多个线程其实是并发与并行同时进行的。

9.2、并发的理解

  • CPU同时处理线程的数量有限。
  • CPU会轮询为系统的每个线程服务,由于CPU切换的速度很快,给我们的感觉这些线程在同时执行,这就是并发。

9.3、并行的理解

  • 在同一个时刻上,同时有多个线程在被CPU处理并执行。

9.4、小节

简单说说并发和并行的含义
  • 并发:CPU分时轮询的执行线程。
  • 并行:同一个时刻同时在执行。

10、补充知识:线程的生命周期

10.1、线程的状态

  • 线程的状态:也就是线程从生到死的过程,以及中间经历的各种状态及状态转换。
  • 理解线程的状态有利于提升并发编程的理解能力。

10.2、Java线程的状态

  • Java总共定义了6种状态
  • 6种状态都定义在Thread类的内部枚举类中。
public class Thread{
     ...
     public enum State {
     NEW,
     RUNNABLE,
     BLOCKED,
     WAITING,
     TIMED_WAITING,
     TERMINATED;
     }
     ...
}

线程状态 描述
NEW(新建) 线程刚被创建,但是并未启动。
Runnable(可运行) 线程已经调用了start()等待CPU调度
Blocked(锁阻塞) 线程在执行的时候未竞争到锁对象,则该线程进入Blocked状态;。
Waiting(无限等待) 一个线程进入Waiting状态,另一个线程调用notify或者notifyAll方法才能够唤醒
Timed Waiting(计时等待) 同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。带有超时参数的常用方法有Thread.sleep 、Object.wait。
Teminated(被终止) 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。

10.3、小节

线程的六种状态

新建状态( NEW ) (创建线程对象)

就绪状态( RUNNABLE ) (start方法)

阻塞状态( BLOCKED ) (无法获得锁对象)

等待状态( WAITING ) (wait方法)

计时等待( TIMED_WAITING ) (sleep方法)

结束状态( TERMINATED ) (全部代码运行完毕)

posted @ 2023-06-30 15:51  lawranceCha  阅读(40)  评论(0)    收藏  举报