Day11、线程概述-线程并发并行

Day11、线程概述-线程并发并行

线程

什么是线程?

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

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

public static void main(String[] args) {
    //代码
for (int i = 0 ; i < 10 ; i++) {
System.out.println(i);
	}
    //代码
}

程序中如果只有一条执行路径, 那么这个程序就是单线程的程序。

多线程是什么?

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

image-20220516110830717

关于多线程要学的东西

image-20220516111039029

多线程的创建

Thread 类

  • Java 是java.lang.Thread 类来代表线程的。

  • 按照面向对象的思想, Thread 类应该提供了实现多线程的方式。

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

  • 定义一个子类MyThread 继承线程类Java.lang.Thread, 重写run() 方法

  • 创建MyThread 类的对象

  • 调用线程对象的start() 方法启动线程( 启动后还是执行run 方法的)

方式一优缺点:

  • 优点: 编码简单

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

小结:

1 、为什么不直接调用了run 方法, 而是调用start 启动线程。

  • 直接调用run 方法会当成普通方法执行, 此时相当于还是单线程执行。
  • 只有调用start方法才是启动一个新的线程执行。

2 、把主线程任务放在子线程之前了。

  • 这样主线程一直是先跑完的, 相当于是一个单线程的效果了。

总结:

image-20220516161144567

public class ThreadDemo01 {
    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);
        }
    }
}

方式二优缺点:

  • 优点: 线程任务类只是实现接口, 可以继续继承类和实现接口, 扩展性强。

  • 缺点: 编程多一层对象包装, 如果线程有执行结果是不可以直接返回的。

总结:

image-20220516162117846

public class ThreadDemo02 {
    public static void main(String[] args) {
        //3.n创建一个任务对象
        Runnable r = new MyRunnable();
        //4.把任务对象交给Thread处理
        //new Thread(r).start();
        Thread t = new Thread(r);
        //5.启动线程
        t.start();
        for (int i = 0; i < 5; i++) {
            System.out.println("主线程执行输出:" + i);
        }
    }
}

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

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

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

  2. 交给Thread 处理。

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

public class ThreadDemo02Other {
    public static void main(String[] args) {
        //3.创建一个任务对象
        Runnable target = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 5; i++) {
                    System.out.println("子线程1执行输出:" + i);
                }
            }
        };
        //4.把任务对象交给Thread处理
        //new Thread(r).start();
        Thread t = new Thread(target);

        //5.启动线程
        t.start();

        new Thread(new Runnable() { //匿名写法
            @Override
            public void run() {
                for (int i = 0; i < 5; i++) {
                    System.out.println("子线程2执行输出:" + i);
                }
            }
        }).start();

        new Thread(() -> {
                for (int i = 0; i < 5; i++) {
                    System.out.println("子线程3执行输出:" + i);
                }
        }).start();

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

小结:

1、前2 种线程创建方式都存在一个问题:

  • 他们重写的run 方法均不能直接返回结果。
  • 不适合需要返回线程执行结果的业务场景。

2 、怎么解决这个问题昵?

  • JDK 5·0 提供了Callable 和FutureTask 来实现。
  • 这种方式的优点是: 可以得到线程执行的结果。

多线程的实现方案三: 利用callable. FutureTask 接囗实现。

  • 得到任务对象

  1. 定义类实现Cable 接口, 重写call()方法, 封装要做的事情。
  2. 用FutureTask 把Callable 对象封装成线程任务对象。
  • 把线程任务对象交给Thread 处理。

  • 调用Thread 的start 方法启动线程, 行任务

  • 线程执行完毕后、通过FutureTask 的get 方法去获取任务执行的结果。

FutureTask的API

方法名称 说明
public FutureTask<>(CaIIabIe call) 把Callable 对象封装成FutureTask 对象。
public V get() throws Exception 获取线程执行c 方法返回的结果

方式三优缺点:

  • 优点: 线程任务类只是实现接口, 可以继续继承类和实现接口, 扩展性强。

  • 可以在线程执行完毕后去获取线程执行的结果。

  • 缺点: 编码复杂一点。

public class ThreadDemo03 {
    public static void main(String[] args) {
        //3.创建callable任务对象
        Callable<String> call1 = new MyCallable(100);
        //4.把Callable任务对象 交给FutureTas对象
        //FutureTask对象的作用1:是Runnable的对象(实现了Runnable接口) 可以交给Thread了
        //FutureTask对象的作用2:可以在线程执行通过之后调用其get方法得到线程执行完成的结果
        FutureTask<String> f1 = new FutureTask<>(call1);
        //5.交给线程处理
        Thread t1 = new Thread(f1);
        //6.启动线程
        t1.start();
        Callable<String> call2 = new MyCallable(50);
        FutureTask<String> f2 = new FutureTask<>(call2);
        Thread t2 = new Thread(f2);
        t2.start();
        try {
            String s1 = f1.get();
            System.out.println("第一个线程结果:" + s1);
        } catch (Exception e) {
            e.printStackTrace();
        }
        try {
            String s2 = f2.get();
            System.out.println("第一个线程结果:" + s2);
        } 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 = 0; i <= n; i++) {
            sum += i;
        }
        return "子线程执行的结果是:" + sum;
    }
}

总结:3 种方式对比

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

Thread的常用方法

Thread 常用API 说明

  • Thread 常用方法: 获取线程名称getName() 、设置名称setName() 、获取当前线程对象currentThread()。

  • 至于Thread 类提供的诸如: yield 、join 、interrupt 、不推荐的方法st0p 、守护线程、线程优先级等线程的控制方法, 在开发中很少使用, 这些方法会在高级篇以及后续需要用到的时候再为大家讲解。

多线程执行时,如何区分线程

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

Thread 类获得当前线程的对象

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

注意:

  1. 此方法是Thread 类的静态方法, 可以直接使用Thread 类调用。

  2. 这个方法是在哪个线程执行中调用的, 就会得到哪个线程对象。

Thread的构造器

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

Thread 类的线程休眠方法

方法名称 说明
public static void sleep(long time) 让当前线程休目民指定的时间后再继续执行, 单位为毫秒。

总结:

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 对象交给线程对象,并指定线程名称

线程安全

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

  1. 存在多线程并发

  2. 同时访问共享资源

  3. 存在修改共享资源

案例:取钱业务

image-20220516191151697

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

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

线程同步:为了解决安全问题

1 、取钱案例出现问题的原因?

  • 多个线程同时执行, 发现账户都是够钱的。

2 、如何才能保证线程安全昵?

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

线程同步的核心思想

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

同步代码块

. 作用: 把出现线程安全问题的核心代码给上锁。

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

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

锁对象要求

  • 理论上: 锁对象只要对于当前同时执行的线程来说是同一个对象即可。

  • 锁对象不能用任意唯一的对象,会影响其他无关线程的执行

  • 建议使用共享资源作为对象锁

  • 对于实例方法建议使用this作为锁对象

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

同步方法

作用: 把出现线程安全问题的核心方法给上锁。

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

格式

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

同步方法底层原理

  • 同步方法其实底层也是有隐式锁对象的, 只是锁的范围是整个方法代码。

  • 如果方法是实例方法: 同步方法默认用this 作为的锁对象。但是代码要高度面向对象!

  • 如果方法是静态方法: 同步方法默认用类名· class 作为的锁对象。

同步代码块好还是同步方法好?

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

总结:

image-20220516201834201

Lock 锁

  • 为了更清晰的表达如何加锁和释放锁, JDK5以后提供了一个新的锁对象Lock , 更加灵活、方便。

  • Lock 实现提供比使用synchronized 方法和语句可以获得更广泛的锁定操作。

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

方法名称 说明
public ReentrantLock() 获得Lock锁的实现类对象

Lock的API

方法名称 说明
void lock() 获得锁
void unlock() 释放锁

线程通信

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

  • 所谓线程通信就是线程间相互发送数据, 线程通信通常通过共享一个数据的方式实现。

  • 线程间会根据共享数据的情况决定自己该怎么做, 以及通知其他线程怎么做。

线程通信常见模型

  • 生产者与消费者模型: 生产者线程负责生产数据, 消费者线程负责消费数据。

  • 要求: 生产者线程生产完数据后, 唤醒消费者, 然后等待自己; 消费者消费完该数据后, 唤醒生产者, 然后等待自己。

image-20220517095751495

Object 类的等待和唤醒方法:

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

注意:

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

线程池🗡️

什么是线程池?

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

不使用线程池的问题

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

线程池的工作原理:

image-20220517105524123

谁代表线程池?

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

如何得到线程池对象

方式一: 使用ExecutorService 的实现类ThreadPooIExecutor 自创建一个线程池对象

ExecutorService→ThreadPoolExecutor.

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

image-20220517105709704

面试题:常问

image-20220517110209233

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 submit(Callable task) 执行任务, 返回未来任务对获取线程结果, 一般韋来执行callable 任务
Void shutdown() 等任务执行完毕后关闭线程池
List shutdownNow() 立刻关闭, 停止正在执行的任务, 返回队列中未执行的任务

新任务拒绝策略

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

ExcutorService的常用方法

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

总结:

线程池如何处理Callable任务,并得到任务执行完后返回的结果

  1. 使用ExcutorSerive的方法:
  2. Future submit(Callable command)

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

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

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

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

Executors使用可能存在的陷阱

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

方法名称 存在问题
public statlc ExecutorService newFixedThreadPool ( int nThreads ) 允许请求的任务队列长度是Interger.MAX_VALUE, 可能出现OOM错误(java.lang.OutOfMemoryError )
public statlc ExecutorService newSingleThreadExecutor()
public statlc ExecutorService newCachedThreadPool() 创建的线程数量最大上限是lInterger.MAX_VALUE,线程数可能会随着任务1 : 1 增长也可能出现OOM错误(java.lang.OutOfMemoryError)
public statlc ScheduledExecutorService newScheduledThreadPool(int corePoolSize)

Excutors使用可能存在的陷阱

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

image-20220521204537775

定时器

定时器

  • 定时器是一种控制任务延时调用或者周期调用的技术。

  • 作用: 闹钟、定时邮件发送。

定时器的实现方式

  • 方式一:Timer

  • 方式二:ScheduledExecutorService

Timer 定时器

构造器 说明
public Timer() 创建Timer 定时器对象
方法 说明
public void schedule (TimerTask task, long delay,long period ) 开启一个定时器, 按照计划处理TimerTask 任务

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

  1. Timer 是单线程, 处理多个任务按照顺序执行, 存在延时与设置定时器的时间有出入。

  2. 可能因为其中的某个任务的异常使Timer 线程死掉, 从而影响后续任务执行。

ScheduledExecutorService 定时器

  • ScheduledExecutorService 是jdk1.5 中引入了并发包, 目的是为了弥补Timer 的缺陷, ScheduledExecutorService 内部为线程池。

Executors 的方法 说明
public static ScheduledExecutorService newScheduledThreadPool ( int corePoolSize ) 得到线程池对象
ScheduledExecutorService 的方法 说明
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay,long periods,TimeUnit unit) 周期调度方法

ScheduIedExecutorService 的优点

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

补充知识:并发并行

并发与并行

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

并发的理解:

  • CPU 同时处理线程的数量有限。

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

并行的理解:

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

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

线程的状态

  • 线程的状态: 也就是线程从生到死的过程, 以及中间经历的各种状态及状态转换。

  • 理解线程的状态有利于提升并发编程的理解能力。

Java线程的状态

  • Java 总共定义了6 种状态

  • 6 种状态都定义在Thread 类的内部枚举类中。

public class Thread{
    ...
	public enum State {
		NEW,
        RUNNABLE,
        BLOCKED,
        WAITING,
        TIMED_WAITING,
        TEMINATED;
    }
}

image-20220521213712764

线程的6 种状态总结

线程状态 描述
NEW( 新建) 线程刚被创建,但是并未启动。
RunnabIe( 可运行) 线程已经调用了start() 等待CPU 调度
Blocked ( 锁阻塞) 线程在执行的时候未竞争到锁对象, 则该线程进入BIocked 状态;
Waiting( 无限等待) 一个线程进入Waiting 状态, 另一个线程调用notify 或者notifyAII 方法才能够唤醒
Timed Waiting( 计时等待) 同waiting 状态, 有几个方法有超时参数, 调用他们将进入Timed Waiting 状态。带有超时参数的常用方法有Thread.sIeep 、Object.wait。
Teminated(被终止) 因为run 方法正常退出而死亡/ 或者因为没有捕获的异常终止了run 方法而死亡。
posted on 2022-06-01 08:55  Cafune-Ding  阅读(64)  评论(0)    收藏  举报