[Java笔记22] 多线程

目录

多线程的创建

Thread的构造器

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

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

多线程的实现方案三:利用Callable、FutureTask接口实现

三种方式对比

Thread的常用方法

线程同步

线程安全问题

同步代码块

同步方法

Lock锁(JDK5以后)

线程池

如何得到线程池对象

ThreadPoolExecutor构造器的参数说明

ExecutorService的常用方法

新任务拒绝策略

Executors工具类实现线程池

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


视频教程传送门 -> https://www.bilibili.com/video/BV1Cv411372m?p=164

多线程的创建

方式一:继承Thread类
方式二:实现Runnable接口
方式三:JDK 5.0新增:实现Callable接口

Thread的构造器

public Thread(String name) -> 可以为当前线程指定名称
public Thread(Runnable target) -> 封装Runnable对象成为线程对象
public Thread(Runnable target ,String name ) -> 封装Runnable对象成为线程对象,并指定线程名称

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

Java是通过java.lang.Thread 类来代表线程的,Thread类提供了实现多线程的方式

步骤:

定义一个子类MyThread继承线程类java.lang.Thread,重写run()方法
创建MyThread类的对象
调用线程对象的start()方法启动线程(启动后还是执行run方法的)

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

注意:
1.为什么不直接调用了run方法,而是调用start启动线程?
直接调用run方法会当成普通方法执行,此时相当于还是单线程执行。
只有调用start方法才是启动一个新的线程执行。

2、为什么把主线程任务放在子线程之后
把主线程任务放在子线程之前的话,主线程先跑完,相当于是一个单线程的效果了。

package com.test.d1_create;

/**
   多线程的创建方式一:继承Thread类实现。
 */
public class ThreadDemo1 {
    public static void main(String[] args) {
        // 3、new一个新线程对象
        Thread t = new MyThread();
        // 4、调用start方法启动线程(执行的还是run方法)
        t.start();

        for (int i = 0; i < 5; i++) {
            System.out.println("主线程执行输出:" + i);
        }

    }
}

/**
   1、定义一个线程类继承Thread类
 */
class MyThread extends Thread{
    /**
       2、重写run方法,里面是定义线程以后要干什么
     */
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("子线程执行输出:" + i);
        }
    }
}

输出示例:

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

步骤:

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

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

package com.test.d1_create;

/**
   线程的创建方式二,理解它的优缺点。
 */
public class ThreadDemo2 {
    public static void main(String[] args) {
        // 3、创建一个任务对象
        Runnable target = new MyRunnable();
        // 4、把任务对象交给Thread处理
        Thread t = new Thread(target);
        // Thread t = new Thread(target, "1号");
        // 5、启动线程
        t.start();

        for (int i = 0; i < 5; i++) {
            System.out.println("主线程执行输出:" + i);
        }
    }
}

/**
   1、定义一个线程任务类 实现Runnable接口
 */
class MyRunnable  implements Runnable {
    /**
       2、重写run方法,定义线程的执行任务的
     */
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("子线程执行输出:" + i);
        }
    }
}


多线程的实现方案三:利用Callable、FutureTask接口实现

前2种线程创建方式存的问题:
重写的run方法均不能直接返回结果,不适合需要返回线程执行结果的业务场景

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

FutureTask的API
public FutureTask<>(Callable call) -> 把Callable对象封装成FutureTask对象
public V get() throws Exception -> 获取线程执行call方法返回的结果

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

package com.test.d1_create;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
   线程的创建方式三:实现Callable接口,结合FutureTask完成
 */
public class ThreadDemo3 {
    public static void main(String[] args) {
        // 3、创建Callable任务对象
        Callable<String> call = new MyCallable(10);
        // 4、把Callable任务对象 交给 FutureTask 对象
        //  FutureTask对象的作用1: 是Runnable的对象(实现了Runnable接口),可以交给Thread了
        //  FutureTask对象的作用2: 可以在线程执行完毕之后通过调用其get方法得到线程执行完成的结果
        FutureTask<String> f1 = new FutureTask<>(call);
        // 5、交给线程处理
        Thread t1 = new Thread(f1);
        // 6、启动线程
        t1.start();


        Callable<String> call2 = new MyCallable(20);
        FutureTask<String> f2 = new FutureTask<>(call2);
        Thread t2 = new Thread(f2);
        t2.start();

        try {
            // 如果f1任务没有执行完毕,这里的代码会等待,直到线程1跑完才提取结果。
            String rs1 = f1.get();
            System.out.println("第一个结果:" + rs1);
        } catch (Exception e) {
            e.printStackTrace();
        }

        try {
            // 如果f2任务没有执行完毕,这里的代码会等待,直到线程2跑完才提取结果。
            String rs2 = f2.get();
            System.out.println("第二个结果:" + rs2);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

/**
    1、定义一个任务类 实现Callable接口  应该申明线程任务执行完毕后的结果的数据类型
 */
class MyCallable implements Callable<String>{
    private int n;
    public MyCallable(int n) {
        this.n = n;
    }

    /**
       2、重写call方法(任务方法)
     */
    @Override
    public String call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= n ; i++) {
            sum += i;
        }
        return "子线程执行的结果是:" + sum;
    }
}

输出:

三种方式对比

方式优点缺点
继承Thread类

编程比较简单,可以直接使用Thread类中的方法

扩展性较差,不能再继承其他的类;
不能返回线程执行的结果
实现Runnable接口扩展性强,实现该接口的同时还可以继承其他的类编程相对复杂,不能返回线程执行的结果
实现Callable接口扩展性强,实现该接口的同时还可以继承其他的类;
可以得到线程执行的结果
编程相对复杂


 

Thread的常用方法

String getName​() -> 获取当前线程的名称,默认线程名称是Thread-索引
void setName​(String name) -> 设置线程名称
public static Thread currentThread() -> 返回对当前正在执行的线程对象的引用
public static void sleep(long time) -> 让线程休眠指定的时间,单位为毫秒
public void run() -> 线程任务方法
public void start() -> 线程启动方法

【例】获取线程名称getName()、设置名称setName()、获取当前线程对象currentThread()

MyThread.java

package com.test.d2_api;

public class MyThread extends Thread{
    public MyThread() {
    }

    public MyThread(String name) {
        // 为当前线程对象设置名称,送给父类的有参数构造器初始化名称
        super(name);
    }

    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            System.out.println( Thread.currentThread().getName() + "输出:" + i);
        }
    }
}

ThreadDemo01.java

package com.test.d2_api;

public class ThreadDemo01 {
    // main方法是由主线程负责调度的
    public static void main(String[] args) {
        Thread t1 = new MyThread("1号线程");
        // t1.setName("1号");
        t1.start();
        System.out.println("第一个子线程名称设置的是"+t1.getName());

        Thread t2 = new MyThread("2号线程");
        // t2.setName("2号");
        t2.start();
        System.out.println("第二个子线程名称设置的是"+t2.getName());

        // 哪个线程执行它,它就得到哪个线程对象(当前线程对象)
        // 主线程的名称就叫main
        Thread m = Thread.currentThread();
        System.out.println("主线程默认的名称是"+m.getName());
        m.setName("最牛的线程");
        System.out.println("主线程新的名称是"+m.getName());

        for (int i = 0; i < 3; i++) {
            System.out.println( m.getName() + "输出:" + i);
        }
    }
}

输出示例 =>
第一个子线程名称设置的是1号线程
第二个子线程名称设置的是2号线程
主线程默认的名称是main
主线程新的名称是最牛的线程
最牛的线程输出:0
2号线程输出:0
1号线程输出:0
2号线程输出:1
最牛的线程输出:1
2号线程输出:2
1号线程输出:1
最牛的线程输出:2
1号线程输出:2

【例】sleep()

package com.test.d2_api;
import java.util.Date;
/**
    目标:线程的API
 */
public class ThreadDemo02 {
    // main方法是由主线程负责调度的
    public static void main(String[] args) throws Exception {
        for (int i = 1; i <= 5; i++) {
            System.out.println(new Date() + " 输出:" + i);
            if(i == 3){
                // 让当前线程进入休眠状态
                // 段子:项目经理让我加上这行代码,如果用户愿意交钱,我就注释掉。
                Thread.sleep(3000);
            }
        }
    }
}

线程同步

线程安全问题

多个线程同时操作同一个共享资源的时候可能会出现业务安全问题,称为线程安全问题
=> 例如银行取钱

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

解决方法 =>

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

方式一:同步代码块
方式二:同步方法
方式三:Lock锁

同步代码块

对出现问题的核心代码使用synchronized进行加锁 => 每次只能一个线程占锁进入访问

synchronized(同步锁对象) {
    操作共享资源的代码(核心代码)
}

 

锁对象的规范要求

对于实例方法建议使用this作为锁对象
对于静态方法建议使用字节码(类名.class)对象作为锁对象

public class Account {
    private String cardId;
    private double money; // 余额 关键信息

    public Account() {
    }

    public Account(String cardId, double money) {
        this.cardId = cardId;
        this.money = money;
    }
……
    public double getMoney() {
        return money;
    }

    public void setMoney(double money) {
        this.money = money;
    }


    public void drawMoney(double money) {
        // 1、拿到是谁来取钱
        String name = Thread.currentThread().getName();
        // 同步代码块
        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+"来取钱,余额不足!");
            }
        }
    }
}

同步方法

对出现问题的核心方法使用synchronized修饰 => 每次只能一个线程占锁进入访问

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

同步代码块锁的范围更小,同步方法锁的范围更大

同步方法的同步锁对象的原理    
对于实例方法默认使用this作为锁对象
对于静态方法默认使用类名.class对象作为锁对象
 

public class Account {
    private String cardId;
    private double money; // 余额 关键信息

    public Account() {
    }

    public Account(String cardId, double money) {
        this.cardId = cardId;
        this.money = money;
    }

……

    public synchronized void drawMoney(double money) {
        // 1、拿到是谁来取钱
        String name = Thread.currentThread().getName();
        // 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+"来取钱,余额不足!");
        }
    }
}

Lock锁(JDK5以后)

Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来构建Lock锁对象

public ReentrantLock​() -> 获得Lock锁的实现类对象
void lock() -> 获得锁
void unlock() -> 释放锁

public class Account {
    private String cardId;
    private double money; // 余额 关键信息
    // final修饰后:锁对象是唯一和不可替换的,非常专业
    private final Lock lock = new ReentrantLock();

    public Account() {
    }

    public Account(String cardId, double money) {
        this.cardId = cardId;
        this.money = money;
    }

……
    public void drawMoney(double money) {
        // 1、拿到是谁来取钱
        String name = Thread.currentThread().getName();
        // 2、判断余额是否足够
        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+"来取钱,余额不足!");
            }
        } finally {
            lock.unlock(); // 解锁
        }

    }
}

线程池

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

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

如何得到线程池对象

JDK 5.0起提供了代表线程池的接口:ExecutorService
方式一:使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象
方式二:使用Executors(线程池的工具类)调用方法返回不同特点的线程池对象(使用有风险)

ThreadPoolExecutor构造器的参数说明

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) 

参数一:指定线程池的线程数量(核心线程): corePoolSize
参数二:指定线程池可支持的最大线程数: maximumPoolSize -> 最大数量 >= 核心线程数量
参数三:指定临时线程的最大存活时间: keepAliveTime
参数四:指定存活时间的单位(秒、分、时、天): unit
参数五:指定任务队列: workQueue
参数六:指定用哪个线程工厂创建线程: threadFactory
参数七:指定线程忙,任务满的时候,新任务来了怎么办: handler

临时线程什么时候创建?
新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程。

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

ThreadPoolExecutor创建线程池对象示例 -->
ExecutorService pools = new ThreadPoolExecutor(3, 5, 8, TimeUnit.SECONDS, new ArrayBlockingQueue<>(6), Executors.defaultThreadFactory() , new ThreadPoolExecutor.AbortPolicy());

ExecutorService的常用方法

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

新任务拒绝策略

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

【例】线程池处理Runnable任务

MyRunnable.java

package com.test.d8_threadpool;

public class MyRunnable implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 2; i++) {
            System.out.println(Thread.currentThread().getName() + "输出了:HelloWorld ==> "  + i);
        }
        try {
            System.out.println(Thread.currentThread().getName() + "本任务与线程绑定了,线程进入休眠了zZZ~");
            Thread.sleep(10000);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

ThreadPoolDemo1.java

package com.test.d8_threadpool;

import java.util.concurrent.*;

public class ThreadPoolDemo1 {
    public static void main(String[] args) {
        // 1、创建线程池对象
        /**
         public ThreadPoolExecutor(int corePoolSize,
                                 int maximumPoolSize,
                                 long keepAliveTime,
                                 TimeUnit unit,
                                 BlockingQueue<Runnable> workQueue,
                                 ThreadFactory threadFactory,
                                 RejectedExecutionHandler handler)
         */
        ExecutorService pool = new ThreadPoolExecutor(3, 5 ,
                6, TimeUnit.SECONDS, new ArrayBlockingQueue<>(4) , Executors.defaultThreadFactory(),
               new ThreadPoolExecutor.AbortPolicy() );

        // 2、给任务线程池处理。
        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.execute(target);
        pool.execute(target);
        // 不创建,触发拒绝策略
        //pool.execute(target);

        // 关闭线程池(开发中不建议使用)
        // pool.shutdownNow(); // 立即关闭,即使任务没有完成,会丢失任务的!
        pool.shutdown(); // 会等待全部任务执行完毕之后再关闭(建议使用)
    }
}

39行如果不注释,饱和后会丢弃任务并抛出RejectedExecutionException异常

注释39行后,输出如下,箭头处5个线程忙完了第一波继续处理队列里的任务

 【例】MyCallable.java

package com.test.d8_threadpool;

import java.util.concurrent.Callable;

/**
    1、定义一个任务类 实现Callable接口  应该申明线程任务执行完毕后的结果的数据类型
 */
public class MyCallable implements Callable<String>{
    private int n;
    public MyCallable(int n) {
        this.n = n;
    }

    /**
       2、重写call方法(任务方法)
     */
    @Override
    public String call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= n ; i++) {
            sum += i;
        }
        return Thread.currentThread().getName()
                + "计算 1-" + n+ "的和,结果是:" + sum;
    }
}

ThreadPoolDemo2.java

package com.test.d8_threadpool;

import java.util.concurrent.*;

public class ThreadPoolDemo2 {
    public static void main(String[] args) throws Exception {
        // 1、创建线程池对象
        /**
         public ThreadPoolExecutor(int corePoolSize,
                                 int maximumPoolSize,
                                 long keepAliveTime,
                                 TimeUnit unit,
                                 BlockingQueue<Runnable> workQueue,
                                 ThreadFactory threadFactory,
                                 RejectedExecutionHandler handler)
         */
        ExecutorService pool = new ThreadPoolExecutor(3, 5 ,
                6, TimeUnit.SECONDS, new ArrayBlockingQueue<>(5) , Executors.defaultThreadFactory(),
               new ThreadPoolExecutor.AbortPolicy() );

        // 2、给任务线程池处理。
        Future<String> f1 = pool.submit(new MyCallable(10));
        Future<String> f2 = pool.submit(new MyCallable(20));
        Future<String> f3 = pool.submit(new MyCallable(30));
        Future<String> f4 = pool.submit(new MyCallable(40));
        Future<String> f5 = pool.submit(new MyCallable(50));
        Future<String> f6 = pool.submit(new MyCallable(60));

        System.out.println(f1.get());
        System.out.println(f2.get());
        System.out.println(f3.get());
        System.out.println(f4.get());
        System.out.println(f5.get());
        System.out.println(f6.get());
        pool.shutdown();
    }
}

输出:

pool-1-thread-1计算 1-10的和,结果是:55
pool-1-thread-2计算 1-20的和,结果是:210
pool-1-thread-3计算 1-30的和,结果是:465
pool-1-thread-2计算 1-40的和,结果是:820
pool-1-thread-3计算 1-50的和,结果是:1275
pool-1-thread-2计算 1-60的和,结果是:1830

Executors工具类实现线程池

Executors:线程池的工具类通过调用方法返回不同类型的线程池对象

使用存在风险,堆积大量请求 / 创建大量线程 --> OOM

 

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

并发:CPU分时轮询的执行线程
并行:同一个时刻同时在执行

线程的6种状态

线程状态描述
New(新建)线程刚被创建,但是并未启动

Runnable(可运行)

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

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值