13.多线程--黑马程序员

多线程

1.多线程概述

1.1 线程(Thread)

线程时一个程序内部的一条执行流程,java的main方法就是由一条默认的主线程执行

1.2 多线程

  • 多线程是指从软硬件上实现的多条执行流程的技术(多条线程由CPU负责调度执行)

  • 许多平台都离不开多线程,因为多线程可以实现并发技术,只有通过多线程,才可能实现多人同时订票等功能,一个人就代表着一个线程

2. 创建多线程

Java是通过java.lang.Thread类的对象来代表线程的

2.1 线程创建方式一:继承Thread类

2.1.1 创建步骤
  1. 定义一个子类继承线程类java.lang.Thread,重写run( )方法,方法体交给另一个线程执行
  2. 创建子类对象
  3. 调用子类对象的start( )方法启动线程(通过start( )方法启动,cpu会自动分配线程执行run( )方法)
//1.子类MyThread继承Thread类
public class MyThread extends Thread{
    //2.重写run方法,run方法的方法体就是新的线程执行体
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("MyThread执行" + i);
        }
    }
}
public class Test1 {
    //main方法默认就是由一个主线程负责执行
    public static void main(String[] args) {
        //3.创建一个子类对象即新的线程对象
        MyThread myThread = new MyThread();
        //4.调用start方法,启动新线程,执行run方法
        myThread.start();

        //5.主线程中,通过for循环执行主线程的执行体
        for (int i = 0; i < 5; i++) {
            System.out.println("main线程执行" + i);
        }
    }
}
result:
MyThread执行0
main线程执行0
MyThread执行1
MyThread执行2
MyThread执行3
MyThread执行4
main线程执行1
main线程执行2
main线程执行3
main线程执行4
由于两个线程并行执行,所以主线程与子线程的输出结果是随机的
2.1.2 优缺点

优点:编码简单

缺点:java是单继承,线程类已经继承了Thread类,就无法继承其他类,不利于功能的扩展

2.1.3 注意事项
  1. 启动线程必须调用start方法,不能调用run方法。直接调用run方法会被当成普通方法执行,cpu不会分配新的线程来执行run方法中的方法体,相当于还是单线程。
public class Test2 {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        //如果调用run方法,那么就不是启动新的线程,而是在主线程中调用run方法
        myThread.run();
        for (int i = 0; i < 5; i++) {
            System.out.println("main线程执行" + i);
        };
    }
}
result:
MyThread执行0
MyThread执行1
MyThread执行2
MyThread执行3
MyThread执行4
main线程执行0
main线程执行1
main线程执行2
main线程执行3
main线程执行4
由结果可以看出变成了单线程,只有主线程负责执行
  1. 不能把主线程任务放在启动子线程之前,否则只有主线程任务执行完才会开启新的线程,没有意义,还是相当于单线程
public class Test2 {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        for (int i = 0; i < 5; i++) {
            System.out.println("main线程执行" + i);
        };
        //主程序的执行体在线程开启之前,所以先执行主程序的执行体,相当于单线程
        myThread.start();
    }
}
main线程执行0
main线程执行1
main线程执行2
main线程执行3
main线程执行4
MyThread执行0
MyThread执行1
MyThread执行2
MyThread执行3
MyThread执行4

2.2 线程创建方式二:实现Runnable接口

2.2.1 创建步骤
  1. 定义一个线程任务类实现Runnable接口,重写run( )方法
  2. 创建任务对象
  3. 将任务对象封装成Thread( )对象
  4. 调用Thread线程对象的start( )方法启动线程
//1.创建一个实现了Runnable接口的类
public class MyRunnable implements Runnable{
    //2.实现类去实现Runnable中的抽象方法run()
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("子线程执行==》" + i);
        }
    }
}
public class Test1 {
    public static void main(String[] args) {
        //3.创建一个任务对象
        Runnable myRunnable = new MyRunnable();
        //4.将任务对象封装成一个线程对象并调用start方法
        new Thread(myRunnable).start();

        for (int i = 0; i < 5; i++) {
            System.out.println("main线程执行==>" + i);
        }
    }
}
result:
子线程执行==》0
main线程执行==>0
子线程执行==》1
main线程执行==>1
子线程执行==》2
main线程执行==>2
main线程执行==>3
main线程执行==>4
子线程执行==》3
子线程执行==》4
2.2.2 优缺点

优点:任务类只是实现接口,可以继续继承其他类、实现其他接口,扩展性强

缺点:需要多创建一个Runnable对象

2.2.3 使用匿名内部类改进代码

public class Test2 {
    public static void main(String[] args) {
        //使用匿名内部类的方式创建线程
        new Thread(new Runnable(){
            @Override
            public void run() {
                for (int i = 0; i < 3; i++) {
                    System.out.println("子线程1执行==>" + i);
                }
            }
        }).start();

        //使用lambda表达式创建线程
        new Thread(() ->{
                for (int i = 0; i < 3; i++) {
                    System.out.println("子线程2执行==>" + i);
                }
            }
        ).start();

        for (int i = 0; i < 3; i++) {
            System.out.println("main线程执行==>" + i);
        }
    }
}
result:
main线程执行==>0
子线程2执行==>0
子线程1执行==>0
子线程1执行==>1
子线程1执行==>2
子线程2执行==>1
main线程执行==>1
子线程2执行==>2
main线程执行==>2

2.3 线程创建方式三:实现Callable接口

2.3.1 实现步骤
  1. 创建任务对象
    1. 定义一个子类实现Callable接口,重写call方法,封装方法体和待返回数据
    2. 将Callable类型的对象封装成FutherTask线程任务对象
  2. 将FutherTask线程任务对象交给Thread对象封装
  3. 调用Thread线程对象的start( )方法启动线程
  4. 线程执行完毕后,通过FutherTask对象的get( )方法获取线程任务执行的结果
//1.创建一个实现Callable接口的实现类
public class MyCallable implements Callable<Double> {
    private int n;

    public MyCallable(int n) {
        this.n = n;
    }
    //2.重写call方法,call方法的方法体就是新的线程执行体
    @Override
    public Double call() throws Exception {
        double sum = 0;
        for (int i = 1; i <= n; i++) {
            sum += i;
        }
        return sum;
    }
}
public class Test {
    public static void main(String[] args) throws Exception {
        //3.创建一个子类对象
        Callable<Double> myCallable = new MyCallable(100);
        //4.创建一个FutureTask对象,将myCallable对象作为构造参数传入
        FutureTask<Double> futureTask = new FutureTask<>(myCallable);
        //5.创建一个Thread对象,并将FutureTask对象作为构造参数传入
        new Thread(futureTask).start();
        //创建第二个线程
        FutureTask<Double> futureTask2 = new FutureTask<>(new MyCallable(200));
        new Thread(futureTask2).start();

        //6.通过FutureTask对象的get方法获取call方法的返回值
        //如果call方法没有执行完毕,get方法会阻塞等待
        Double v = futureTask.get();
        System.out.println(v);
        Double v2 = futureTask2.get();
        System.out.println(v2);
    }
}
result:
5050.0
20100.0
2.3.2 优缺点

优点

  • 线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强;
  • 可以在线程执行完毕后去获取线程执行的结果

缺点:编码比较复杂

3. Thread提供的常用方法

构造器说明
public Thread( String name )可以为当前线程指定名称
public Thread( Runnable target )封装Runnable对象成为线程对象
public Thread( Runnable target, String name )封装Runnable对象成为线程对象,并指定线程名
方法说明
public void run( )线程中要执行的方法
public void start( )启动线程
public String getName( )获取当前线程的名称,线程名称默认是Thread-索引
public void setName( String name )修改线程的名称
public static Thread currentThread( )获取当前执行的线程对象
public static void sleep( long time )让当前执行的线程休眠多少毫秒,1000毫秒=1秒
public final void join( )让调用当前这个方法的线程先执行完再让其他线程执行
public class MyThread extends Thread{
    public MyThread() {
    }
    public MyThread(String name) {
        super(name);
    }

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            //Thread.currentThread()方法返回当前线程对象
            //getName()方法返回当前线程的名字
            System.out.println(Thread.currentThread().getName() + "执行==>" + i);
        }
    }
}
public class Test1 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new MyThread();
        //setName()方法设置线程的名字,必须在start()方法之前调用
        t1.setName("线程1");
        t1.start();
        //join()方法让其他线程休眠,等待t1线程执行完毕之后再继续执行其他线程
        t1.join();
        //getName()方法获取线程的名字
        System.out.println("线程1的名字是:" + t1.getName());
        //创建线程对象的同时设置线程的名字
        Thread t2 = new MyThread("线程2");
        t2.start();
        //让主线程休眠5秒
        Thread.sleep(5000);
        System.out.println("线程2的名字是:" + t2.getName());
        //获取main方法所在的线程对象并修改线程名字
        Thread t3 = Thread.currentThread();
        t3.setName("主线程");
        System.out.println("主线程的名字是:" + t3.getName());
    }
}

4. 线程安全问题

4.1 线程安全问题出现的原因

  • 存在多个线程在同时执行
  • 同时访问一个共享资源
  • 存在修改该共享资源

4.2 模拟线程安全问题

4.2.1 需求分析

小明和小红是一对夫妻,他们有一个共同的账户,余额是10万元,模拟2人同时取钱10万元

  • 需要提供一个账户类,接着创建一个账户对象代表2个人的共享账户
  • 需要定义一个线程类(用于创建两个线程,分别代表小明和小红)
  • 创建2个线程,传入同一个账户对象给2个线程处理
  • 启动2个线程,同时去同一个账户对象中取钱10万
4.2.1 程序模拟
//账户类
public class Account {
    private double money;
    public Account(double money) {
        this.money = money;
    }
    //取钱的方法
    public void drawMoney(double drawMoney){
        String name = Thread.currentThread().getName();
        if (money >= drawMoney){
            System.out.println(name + "取钱成功,吐出钞票:" + drawMoney);
            money -= drawMoney;
            System.out.println(name + "取钱后的余额为:" + money);
        }else {
            System.out.println(name + "取钱失败,余额不足!");
        }
    }
    public double getMoney() {
        return money;
    }
    public void setMoney(double money) {
        this.money = money;
    }
}
//取钱的线程
public class drawThread extends Thread{
    private Account account;
    public drawThread(String name,Account account) {
        super(name);
        this.account = account;
    }

    @Override
    public void run() {
        account.drawMoney(100000);
    }
}
public class Test {
    public static void main(String[] args) {
        //创建一个账户
        Account account = new Account(100000);
        //创建两个线程
        Thread dt1 = new drawThread("小明", account);
        dt1.start();
        Thread dt2 = new drawThread("小红", account);
        dt2.start();
    }
}
/*
    小明取钱成功,吐出钞票:100000.0
    小红取钱成功,吐出钞票:100000.0
    小红取钱后的余额为:-100000.0
    小明取钱后的余额为:0.0
 */

5. 线程同步(解决线程安全问题)

5.1 认识线程同步

解决线程安全问题的方案

5.1.1 线程同步的思想

让多个线程实现先后依次访问共享资源,这样就解决了安全问题

5.1.2 线程同步的常见方案

加锁:每次只允许一个线程加锁,加锁后才能进入访问,访问完毕后自动解锁,然后其他线程才能再加锁进来

  • 同步代码块
  • 同步方法
  • Lock锁

5.2 三种加锁方式

5.2.1 同步代码块

作用:把访问共享资源的核心代码块上锁,以此保证线程安全

在这里插入图片描述

原理:每次只允许一个线程加锁后进入,执行完毕后自动解锁,其他线程才可以进来执行

注意事项:对于当前同时执行的线程来说,同步锁必须是同一把(同一个对象),否则会出bug,因为对于不同的线程需要识别同步锁,同步锁一致才会让一个线程加锁,否则起不到加锁的作用。

锁对象的使用规范:使用共享资源作为锁对象

  1. 对于实例方法建议使用this关键字作为锁对象
//修改4.2.1的程序中Account类中的drawMoney( )方法,其余部分代码不变
//快捷键:ctrl+alt+t
 public void drawMoney(double drawMoney){
        String name = Thread.currentThread().getName();
     	//同步代码块
        synchronized (this) {
            if (money >= drawMoney){
                System.out.println(name + "取钱成功,吐出钞票:" + drawMoney);
                money -= drawMoney;
                System.out.println(name + "取钱后的余额为:" + money);
            }else {
                System.out.println(name + "取钱失败,余额不足!");
            }
        }
    }
/*
        小明取钱成功,吐出钞票:100000.0
        小明取钱后的余额为:0.0
        小红取钱失败,余额不足!
 */
  1. 对于静态方法建议使用字节码(类名.class)对象作为锁对象
//示例
    public static void test(){
        //同步代码块
        synchronized (Account.class) {
        }
    }
5.2.2 同步方法

作用:把访问共享资源的核心方法给上锁,以此保证线程安全

在这里插入图片描述

原理:每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行

底层原理(与同步代码块一致,只是同步方法锁的范围更大)

  • 同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码
  • 如果方法是实例方法:同步方法默认用this作为锁对象
  • 如果方法是静态方法:同步方法默认用类名.clss作为锁对象
//修改4.2.1的程序中Account类中的drawMoney( )方法,其余部分代码不变
//使用synchronized修饰方法,同步方法
    public synchronized void drawMoney(double drawMoney){
        String name = Thread.currentThread().getName();
        //同步代码块
        if (money >= drawMoney){
            System.out.println(name + "取钱成功,吐出钞票:" + drawMoney);
            money -= drawMoney;
            System.out.println(name + "取钱后的余额为:" + money);
        }else {
            System.out.println(name + "取钱失败,余额不足!");
        }
    }
/*
        小明取钱成功,吐出钞票:100000.0
        小明取钱后的余额为:0.0
        小红取钱失败,余额不足!
 */
5.2.3 Lock锁
  • Lock锁是JDK5开始提供的一个新的锁定操作,通过它可以创建出锁对象进行加锁和解锁,更灵活、更方便、更强大。
  • Lock是接口,不能直接实例化,可以采用它的实现类ReentrantLock来构建Lock锁对象。
//修改4.2.1的程序中Account类中的drawMoney( )方法,其余部分代码不变
 
	//创建锁对象
 	private final Lock lock = new ReentrantLock();

    public void drawMoney(double drawMoney){
        String name = Thread.currentThread().getName();
        //使用try\cathc\finally代码块来确保锁一定会被释放,
        //否则一旦代码块中出现异常,锁就不会被释放
        try {
            //加锁
            lock.lock();
            if (money >= drawMoney){
                System.out.println(name + "取钱成功,吐出钞票:" + drawMoney);
                money -= drawMoney;
                System.out.println(name + "取钱后的余额为:" + money);
            }else {
                System.out.println(name + "取钱失败,余额不足!");
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            //释放锁
            lock.unlock();
        }
    }
/*
        小明取钱成功,吐出钞票:100000.0
        小明取钱后的余额为:0.0
        小红取钱失败,余额不足!
 */

6. 线程通信(了解)

线程通信

当多个线程共同操作共享的资源时,线程间通过某种方式互相告知自己的状态,以相互协调,并避免无效的资源争夺(线程通讯的前提是必须线程安全)

线程通信的常见模型(生产者与消费者模型)

  • 生产者线程负责生产数据

  • 消费者线程负责消费生产者生产的数据。

注意:生产者生产完数据应该等待自己,通知消费者消费;消费者消费完数据也应该等待自己,再通知生产者生产!

在这里插入图片描述

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

注意:上述方法应该使用当前同步锁对象进行调用

public class Desk {
    private List<String> list = new ArrayList<>();
    //厨师做包子
    public synchronized void put(){
        try {
            String name = Thread.currentThread().getName();
            if (list.isEmpty()) {
               list.add("name" + "做的包子");
                System.out.println(name + "做了一个包子");
                Thread.sleep(2000);

                //唤醒其他线程,等待自己
                this.notifyAll();
                this.wait();
            }else {
                //有包子,等待
                this.notifyAll();
                this.wait();
            }
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

    //吃货拿包子
    public synchronized void get(){
        try {
            String name = Thread.currentThread().getName();
            if (!list.isEmpty()) {
                list.clear();
                System.out.println(name + "吃了一个包子");
                Thread.sleep(2000);

                //唤醒其他线程,等待自己
                this.notifyAll();
                this.wait();
            }else {
                //没有包子,等待
                this.notifyAll();
                this.wait();
            }
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}
public class Test {
    public static void main(String[] args) {
        Desk desk = new Desk(); //创建桌子对象
        //创建厨师线程
        new Thread(()->{
            while (true) {
                desk.put();
            }
        }, "厨师1").start();
        new Thread(()->{
            while (true) {
                desk.put();
            }
        }, "厨师2").start();
        new Thread(()->{
            while (true) {
                desk.put();
            }
        }, "厨师3").start();
        //创建吃货线程
        new Thread(()->{
            while (true) {
                desk.get();
            }
        }, "吃货1").start();
        new Thread(()->{
            while (true) {
                desk.get();
            }
        }, "吃货2").start();
    }
}

在这里插入图片描述

7. 线程池

7.1 认识线程池

7.1.1 什么是线程池

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

7.1.2 不使用线程池的问题

用户每发起一个请求,后台就需要创建一个新线程来处理,下次新任务来了肯定又要创建新线程处理的,而创建新线程的开销是很大的,并且请求过多时,肯定会产生大量的线程出来,这样会严重影响系统的性能

7.1.3 线程池的工作原理
  1. 如果线程池中有三个工作线程,则每个线程都会处理任务队列中的一个任务

在这里插入图片描述

  1. 当某个线程处理完一个任务后,该线程会继续处理任务队列中的其他任务,依次类推

在这里插入图片描述

在这里插入图片描述

7.2 创建线程池

JDK5.0起提供了代表线程池的接口:ExecutorService

7.1.1 创建线程池对象的方式一(推荐使用)

使用实现了ExecutorService接口的实现类ThreadPoolExecutor创建线程池对象

ThreadPoolExecutor构造器
//格式
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)
//实例
public class Test{
    public static void main(String[] args) {
        // 创建线程池方式一
        /*
           线程池中有三个核心线程
           线程池中有五个最大线程
           线程池中有(5-3=2)个临时线程
           临时线程的存活时间为8秒
           线程池中有4个任务队列
           拒绝策略为:抛出异常并丢弃任务
         */
        new ThreadPoolExecutor(3,5,8, 
                TimeUnit.SECONDS,new ArrayBlockingQueue<>(4), 
                Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
    }
}
  • 参数一:corePoolSize:指定线程池的核心线程的数量。(相当于饭店的固定服务员的数量)
  • 参数二:maximumPoolSize:指定线程池的最大线程数量。(相当于饭店的固定服务员和临时员工的数量)
  • 参数三:keepAliveTime:指定临时线程的存活时间。(相当于临时员工可以干多久)
  • 参数四:unit:指定临时线程存活的时间单位(秒、分、时、天)
  • 参数五:workQueue:指定线程池的任务队列。(相当于待招待的客人的数量)
  • 参数六:threadFactory:指定线程池的线程工厂。(相当于负责招聘服务员的hr)
  • 参数七:handler:指定线程池的任务拒绝策略(线程都在忙,任务队列也满了的时候,新任务来了该怎么处理)(相当于饭店的临时工和固定服务员仍然忙不过来时,应该如果对待新来的客人)
任务拒绝策略说明
ThreadPoolExecutor.AbortPolicy丢弃任务并抛出RejectedExecutionException异常。(默认策略)
ThreadPoolExecutor.DiscardPolicy丢弃任务,但是不抛出异常(不推荐)
ThreadPoolExecutor.DiscardOldestPolicy抛弃队列中等待最久的任务然后把当前任务加入队列中
ThreadPoolExecutor.CallerRunsPolicy由主线程负责调用任务的run( )方法从而绕过线程池直接执行
//创建Runnable任务类
public class MyRunnable implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "线程==>被调用了");
        try {
            //让线程睡眠一会,从而便于模拟并发环境
            Thread.sleep(Integer.MAX_VALUE);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}
public class Test{
    public static void main(String[] args) {
        // 创建线程池
        ExecutorService pool = new ThreadPoolExecutor(3, 5, 8,
                TimeUnit.SECONDS, new ArrayBlockingQueue<>(4),
                Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
        // 创建Runnable任务实例对象
        Runnable target = new MyRunnable();
        //下面的三个任务被三个核心线程处理,不占用任务队列中的空间
        pool.execute(target);
        //该方法会在线程池中自动创建一个新线程自动处理任务,前提是线程池中有空闲线程
        pool.execute(target);
        pool.execute(target);
        //下面的四个任务进入任务队列等待处理
        pool.execute(target);
        pool.execute(target);
        pool.execute(target);
        pool.execute(target);
    }
}
/*
结果:
    pool-1-thread-2线程==>被调用了
    pool-1-thread-1线程==>被调用了
    pool-1-thread-3线程==>被调用了
    因为在任务类中让线程休眠了很久,所以前三个任务还是占有着三个核心线程,导致后面的任务进入任务队列等待处理
 */
注意事项
  1. 临时线程的创建时机

    新任务进入任务队列时发现核心线程都在忙任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程。

public class Test{
    public static void main(String[] args) {
        // 创建线程池
        ExecutorService pool = new ThreadPoolExecutor(3, 5, 8,
                TimeUnit.SECONDS, new ArrayBlockingQueue<>(4),
                Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
        // 创建Runnable任务实例对象
        Runnable target = new MyRunnable();
        //下面的三个任务被三个核心线程处理,不占用任务队列中的空间
        pool.execute(target);
        //该方法会在线程池中自动创建一个新线程自动处理任务,前提是线程池中有空闲线程
        pool.execute(target);
        pool.execute(target);
        //下面的四个任务进入任务队列等待处理
        pool.execute(target);
        pool.execute(target);
        pool.execute(target);
        pool.execute(target);
        //当任务队列满了并且核心线程全部被占用,会创建临时线程处理任务,
        //但是只能创建两个临时线程,因为最大线程数为5
        //下面的两个任务会被临时线程处理
        pool.execute(target);
        pool.execute(target);
    }
}
/*
结果:
        pool-1-thread-2线程==>被调用了
        pool-1-thread-4线程==>被调用了
        pool-1-thread-1线程==>被调用了
        pool-1-thread-5线程==>被调用了
        pool-1-thread-3线程==>被调用了
    此时由于三个核心线程被占用,四个任务也在任务队列中等待处理,所以当再次进入新任务时,会创建两个临时线程4和5进行任务处理
 */
  1. 拒绝新进来的任务的时机

    核心线程和临时线程都在忙并且任务队列也满了,新的任务过来的时候才会开始拒绝任务。

public class Test{
    public static void main(String[] args) {
        // 创建线程池
        ExecutorService pool = new ThreadPoolExecutor(3, 5, 8,
                TimeUnit.SECONDS, new ArrayBlockingQueue<>(4),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());
        // 创建Runnable任务实例对象
        Runnable target = new MyRunnable();
        //下面的三个任务被三个核心线程处理,不占用任务队列中的空间
        pool.execute(target);
        //该方法会在线程池中自动创建一个新线程自动处理任务,前提是线程池中有空闲线程
        pool.execute(target);
        pool.execute(target);
        //下面的四个任务进入任务队列等待处理
        pool.execute(target);
        pool.execute(target);
        pool.execute(target);
        pool.execute(target);
        //当任务队列满了并且核心线程全部被占用,会创建临时线程处理任务,
        //但是只能创建两个临时线程,因为最大线程数为5
        //下面的两个任务会被临时线程处理
        pool.execute(target);
        pool.execute(target);
//        下面的两个任务因为临时线程已经达到最大线程数,所以会根据拒绝策略进行处理
        pool.execute(target);
    }
}
/*
结果见下图
    此时由于三个核心线程被占用,四个任务也在任务队列中等待处理,两个临时线程也被占用,所以会根据拒绝策略进行任务处理
 */

在这里插入图片描述

  1. 核心线程数量的填写原则
    计算密集型的任务:核心线程数量 = CPU的核数 + 1
    I0密集型的任务:核心线程数量 = CPU核数 * 2

    在这里插入图片描述

7.1.2 创建线程池对象的方式二(不推荐使用)

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

Executors是一个线程池工具类,提供了许多静态方法用于返回不同特点的线程池对象

方法名称说明
public static ExecutorService newFixedThreadPool(int nThreads)创建固定线程数量的线程池,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程替代它。
public static ExecutorService newSingleThreadExecutor( )创建只有一个线程的线程池对象,如果该线程出现异常而结束,那么线程池会补充一个新线程。
public static ExecutorService newCachedThreadPool( )线程数量随着任务增加而增加,如果线程任务执行完并且空闲了6s则会被回收掉。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolsize)创建一个线程池,可以实现在给定的延迟后运行任务,或者定期执行任务。

这些方法的底层,都是通过线程池的实现类ThreadPoolExecutor创建的线程池对象

缺点:大型并发系统环境中使用Executors?如果不注意可能会出现系统风险

  1. FixedThreadPool和SingleThreadPool

    允许的请求队列长度为Integer.MAX VALUE,可能会堆积大量的请求,从而导致OOM(内存溢出)。

  2. CachedThreadPool

    允许的创建线程数量为Integer.MAX VALUE,可能会创建大量的线程,从而导致OOM(内存溢出)。

public class Test{
    public static void main(String[] args) {
        // 创建线程池的方式二:使用Executors工具类创建
        // 1.创建一个固定有五个线程的线程池
        ExecutorService pool = Executors.newFixedThreadPool(5);
       /*
       底层代码
        public static ExecutorService newFixedThreadPool(int nThreads) {
            return new ThreadPoolExecutor(nThreads, nThreads,
                    0L, TimeUnit.MILLISECONDS,
                    new LinkedBlockingQueue<Runnable>());
        }
        */
        
        //2.创建只有一个线程的线程池
        ExecutorService pool2 = Executors.newSingleThreadExecutor();
        /*
        底层代码
            public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
        }
         */
        
        //3.创建一个线程数量不固定的线程池
        ExecutorService pool3 = Executors.newCachedThreadPool();
        /*
        底层代码
            public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
        }   
         */
        
        //4.创建一个定时任务的线程池
        ScheduledExecutorService pool4 = Executors.newScheduledThreadPool(5);
        /*
        底层代码
                public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
        }
         */
    }
}

7.3 使用线程池处理任务

ExecutorService的常用方法说明
void execute(Runnable command)执行Runnable任务
Future<T> submit( Callable<T> task)执行Cal1able任务,返回未来任务对象,用于获取线程返回的结果
void shutdown( )等全部任务执行完毕后,再关闭线程池!
List<Runnable> shutdownNow( )立刻关闭线程池,停止正在执行的任务,并返回队列中未执行的任务

注意:一般不会关闭线程池,如果实在需要关闭,则使用void shutdown( )方法关闭

7.3.1 处理Runnable任务
public class Test2 {
    public static void main(String[] args) {
        // 创建线程池
        ThreadPoolExecutor pool = new ThreadPoolExecutor(3, 5, 8,
                TimeUnit.SECONDS, new ArrayBlockingQueue<>(4),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());
        // 创建Runnable任务类
        Runnable myRunnable = new MyRunnable();
        // 向线程池中提交任务,线程池分配线程去执行任务
        pool.execute(myRunnable);
        pool.execute(myRunnable);
        pool.execute(myRunnable);
        pool.execute(myRunnable);
        // 关闭线程池
        pool.shutdown();
    }
}

由下图可见,线程池已经关闭了,程序结束了,否则程序不会结束

在这里插入图片描述

7.3.2 处理Callable任务
//1.创建一个实现Callable接口的实现类
public class MyCallable implements Callable<Double> {
    private int n;

    public MyCallable(int n) {
        this.n = n;
    }
    //2.重写call方法,call方法的方法体就是新的线程执行体
    @Override
    public Double call() throws Exception {
        double sum = 0;
        for (int i = 1; i <= n; i++) {
            sum += i;
        }
        return sum;
    }
}
public class Test3 {
    public static void main(String[] args) throws Exception{
        //创建线程池
        ExecutorService pool = Executors.newFixedThreadPool(3);
        //创建Callable任务对象
        Callable<Double> c1 = new MyCallable(100);
        Callable<Double> c2 = new MyCallable(200);
        Callable<Double> c3 = new MyCallable(300);
        //执行任务并获取Future对象
        Future<Double> s1 = pool.submit(c1);
        Future<Double> s2 = pool.submit(c2);
        Future<Double> s3 = pool.submit(c3);
        //输出结果
        System.out.println(s1.get());
        System.out.println(s2.get());
        System.out.println(s3.get());
    }
}

在这里插入图片描述

8. 并发、并行、线程的生命周期

8.1 并发与并行

8.1.1 进程
  • 正在运行的程序(软件)就是一个独立的进程。
  • 线程是属于进程的,一个进程中可以同时运行很多个线程。
  • 进程中的多个线程其实并发和并行执行的。
8.1.2 并发

进程中的线程是由CPU负责调度执行的,但CPU能同时处理线程的数量有限,为了保证全部线程都能往前执行,CPU会轮询为系统的每个线程服务,由于CPU切换的速度很快,给我们的感觉这些线程在同时执行,这就是并发。

8.1.3 并行

在同一个时刻上,同时有多个线程在被CPU调度执行。

8.2 线程的生命周期

8.2.1 Java线程的状态
  • Java定义了六种状态
  • 定义在Thread类的内部枚举类中
public enum State {
        NEW,
        RUNNABLE,
        BLOCKED,
        WAITING,
        TIMED_WAITING,
        TERMINATED;
    }
线程状态说明
NEW(新建)线程刚被创建,但是并未启动
Runnable(可运行)线程已经调用了start( ),等待CPU调度
Blocked(锁阻塞)线程在执行的时候未竞争到锁对象,则该线程进入Blocked状态
Waiting(无限等待)一个线程进入Waiting状态,另一个线程调用notify或者notifyAll方法才能够唤醒
Timed Waiting(计时等待)同waiting状态,有几个方法(sleep,wait)有超时参数,调用他们将进入Timed Waiting状态。
Teminated(被终止)因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。

在这里插入图片描述

9. 悲观锁与乐观锁

b站黑马程序员对应视频

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值