学习日记(多线程)

学习日记(多线程)

线程(thread):是一个程序内部的一条执行路径。启动程序后,main 方法就是一条单独的执行路径。

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

一、多线程的创建

多线程的创建有三种方式:继承 Thread 类、实现 Runnable 接口、实现 Callable 接口并结合 FutureTask 类。

1. 方式一:继承 Thread 类

步骤

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

优点:编码简单。

缺点:存在单继承的局限性,线程类已经继承了 Thread 类,无法继承其他类,不利于扩展。

注意

  • 线程对象只有调用 start 方法启动线程,而不是 run 方法。因为,直接调用 run 方法会当成普通方法执行,此时相当于还是单线程执行。只有调用 start 方法才是启动一个新的线程执行。
  • 应该要把子线程放在主线程之前。否则,如果主线程在前,会将主线程一直跑完,相当于还是一个单线程。

2. 方式二:实现 Runnable 接口

步骤

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

优点:线程任务类只是实现了 Runnable 接口,可以继续继承类和实现接口。

缺点:如果线程有执行结果是不能直接返回的。

Thread 构造器说明
public Thread(Runnable target)封装 Runnable 对象成为线程对象(把任务对象 target 交给 Thread 处理)

另一种写法:将上面步骤中的前两步换为:创建 Runnable 接口的匿名内部类对象。

上面继续简化为

注意:方式一和方式二都存在一个问题:重写的 run 方法不能直接返回结果,不适合需要返回线程执行结果的业务场景。

3. 方式三:实现 Callable 接口并结合 FutureTask 类

步骤

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

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

缺点:编码复杂一点(对比来看)。

构造器或方法说明
构造器public FutureTask(Callable callable)用构造器把 Callable 对象封装成线程任务对象
方法public V get() throws InterruptedException, ExecutionException获取任务执行后的结果

注意:FutureTask 的作用有两个:

  • 是 Runnable 接口的对象(实现了 Runnable 的接口),可以把 Callable 对象封装后交给线程对象;
  • 在线程执行完毕后,通过调用 FutureTask 对象的 get 方法去获取任务执行后的结果。

二、Thread 的常用方法

方法名说明
public final void setName(String name)将此线程的名称修改为指定的名称,也可以在创建线程时通过构造器修改
public final String getName()获取当前线程的名称,默认的线程名称为:Thread-索引
public static native Thread currentThread()获取当前的线程对象,static 方法,返回值类型为 Thread(线程对象)

注意:在实际开发中,一般不为线程改名字。

方法名说明
public static native void sleep(long millis) throws InterruptedException让线程休眠指定的时间,单位为毫秒


三、线程安全

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

线程安全问题出现的原因

  • 存在多线程并发;
  • 同时访问共享资源;
  • 存在修改共享资源。

需求:小明和小红两个人,有一个共同的账户,余额为 10 万元,模拟两人同时去取 10 万元的情况。

分析

  1. 定义一个账户类 Account,创建一个账户对象 acc 代表两个人的共同账户;
  2. 定义一个线程类 DrawThread,线程类可以操作账户对象 acc;
  3. 创建两个线程对象,传入同一个账户对象 acc;
  4. 启动线程,去同一个账户对象中取钱 10 万元。


四、线程同步

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

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

加锁的方式有三种:同步代码块、同步方法、Lock 锁。

1. 方式一:同步代码块

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

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

同步代码块快捷键Ctrl + Alt + t

锁对象要求:理论上锁对象只要对于当前同时执行的线程来说是同一个对象即可。但是锁对象用任意唯一的对象不好,原因是会影响其他无关线程的执行。因此,锁对象在规范上,建议使用共享资源作为锁对象

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

2. 方式二:同步方法

作用:把出现线程安全问题的核心方法使用 synchronized 修饰。

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

同步方法的底层原理

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

注意:同步代码块锁的范围更小,同步方法锁的范围更大。但是实际中,同步方法还是用得更多一点。

3. 方式三:Lock 锁

Lock 锁有比使用 synchronized 方法和语句更广泛的锁定操作

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

在账户类中用实现类构建 Lock 锁对象的方法:private final Lock lock = new ReentrantLock();

方法名说明
void lock()上锁
void unlock()解锁

注意:Lock 锁还有其他强大功能,之后遇到学习。


五、线程通信

概述:线程通信就是线程间相互发送数据,线程通信通常共享一个数据的方式实现。

线程通信常见模型:生产者与消费者模型:生产者线程负责生产数据,消费者线程负责消费数据。

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

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

线程通信的三个常见方法Object 类的等待和唤醒方法:

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

注意

  • 这些方法应该使用当前同步锁对象进行调用,因为只有锁对象才会知道谁在调用我,我要等待谁。
  • 一般先唤醒别的线程,然后再等待自己。
  • 线程通信通常共享一个数据的方式实现,如判断这个数据是否有,然后决定是否唤醒或等待。
  • 锁可以跨方法,如有两个实例方法,他们都用 synchronized 修饰,这样都默认 this 为同步锁对象,当多个线程分别调用这两个方法时,只能有一个线程的一个方法被执行,因为这两个方法共用一个锁。

六、线程池

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

优点:可以复用线程,提高系统的性能。

1. 线程池实现的 API 以及参数说明

线程池的代表ExecutorService 接口。

得到线程池的两种方式

  • 方式一(重点):使用接口 ExecutorService 的实现类 ThreadPoolExecutor 来创建一个线程池对象。
  • 方式二:使用线程池的工具类 Executors 来调用方法返回不同特点的线程池对象。

实现类 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;

问题一:临时线程什么时候创建?

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

问题二:什么时候会开始拒绝任务?

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

2. 线程池处理 Runnable 任务

步骤

  • 创建线程池;
  • 创建任务对象,调用线程池方法,处理任务。

实现类 ThreadPoolExecutor 通过构造器创建线程池对象示例:

ExecutorService pool = new ThreadPoolExecutor(3, 5, 7, TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(5), Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());

线程池 ExecutorService常用方法

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

注意

  • 在执行完全部任务后,线程池不会主动关闭,要想关闭线程池,需要用 shutdown 方法在所有任务执行完后关闭。
  • shutdownNow 表示即使任务没有完成,也立即关闭线程池,这样会丢失任务,因此,在开发中一般不会使用。
  • 构造器的第七个参数表示新任务拒绝策略,ThreadPoolExecutor.AbortPolicy() 是默认的策略,表示丢弃任务并抛出 RejectedExecutionException 异常,此外,还有其他策略。
  • 线程池处理 Runnable 任务和在第一部分的方式二类似,只不过把创建一个线程的步骤换为了线程池。

3. 线程池处理 Callable 任务

步骤

  • 创建线程池;
  • 创建任务对象,调用线程池方法,处理任务。
方法名说明
ExecutorService 接口的方法Future submit(Callable task)执行 Callable 任务,返回未来任务对象
Future 接口的方法V get()通过未来任务对象调用 get 方法,获取线程执行结果

注意:线程池处理 Callable 任务和在第一部分的方式三类似,只不过把创建一个线程的步骤换为了线程池。

  • 第一部分方式三中:由于线程对象不能直接执行 Callable 任务,所以通过 FutureTask 类可以把 Callable 对象封装成任务对象,并调用 get 方法去获取任务执行后的结果;
  • 该部分中:通过线程池的 submit 方法可以直接执行 Callable 任务并返回未来任务对象,再调用 get 方法可以获取任务执行后的结果。

4. Executors 工具类实现线程池

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

Executors 工具类底层其实也是基于 ExecutorService 接口的实现类 ThreadPoolExecutor 来创建线程池对象的。

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

方法名说明存在的问题
public static ExecutorService newCachedThreadPool()线程数量随着任务的增加而增加,如果线程任务执行完毕且空闲了一段时间则会被回收掉创建的线程数量最大上限为 Integer.MAX_VALUE,线程数可能会随着任务 1:1 增长,也可能出现内存溢出错误
public static ExecutorService newFixedThreadPool(int nThreads)创建固定线程数量的线程池,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程替代它允许请求的任务队列长度是 Integer.MAX_VALUE,可能出现内存溢出错误
public static ExecutorService newSingleThreadExecutor()创建只有一个线程的线程池,如果该线程因为执行异常而结束,那么线程池会补充一个新线程替代它同 2
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)创建一个线程池,可以实现在给定的延迟后运行任务,或者定期执行任务(适合做定时器同 1

注意:Executors 工具类不适合在大型并发系统环境中使用,建议使用方式一创建线程池对象,即,使用接口 ExecutorService 的实现类 ThreadPoolExecutor 来创建一个线程池对象,可以明确线程池的运行规则,规避资源耗尽的风险


七、定时器

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

定时器的实现方式有两个:Timer、ScheduledExecutorService(更好)。

1. 方式一:Timer

定时器本身就是一个单线程

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

注意:schedule 方法的第二个参数是启动延时,第三个参数是周期延时

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

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

总结:要想办法解决这两个问题,可以创建多个 Timer 对象,就可以用线程池来处理。

2. 方式二:ScheduledExecutorService

ScheduledExecutorService 弥补了 Timer 的缺陷。

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

类或接口方法名说明
Executors 类public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)得到线程池对象
ScheduledExecutorService 接口public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)通过线程池对象调用,周期完成任务

注意:scheduleAtFixedRate 方法中第一个参数为定时器任务对象、第二个参数是启动延时、第三个参数是周期延时、第四个参数是时间单位。

注意

  • 得到线程池对象时,参数是核心线程的数量,最好设置为大于任务的数量
  • 当任务中有 sleep 延时,则两次任务执行的间隔时间为 sleep 延时时间与定时器周期时间的最大值

八、线程并发、并行

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

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

九、线程的生命周期

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

Java 线程的状态有 6 中,定义在了 Thread 类的内部枚举类中。

线程状态描述
NEW(新建状态)线程刚被创建,但是未启动
RUNNABLE(可运行/就绪状态)线程已经调用了 start 方法等待 CPU 调度
BLOCKED(锁阻塞状态)线程在执行的时候未竞争到锁对象,则该线程进入 BLOCKED 状态
WAITING(无限等待状态)一个线程进入 WAITING 状态,另一个线程调用 notify 或 notifyAll 方法才能唤醒
TIMED_WAITING(计时等待状态)同 WAITING 状态,有几个方法有超时参数,调用线程进入 TIMED_WAITING 状态
TERMINATED(被终止/结束状态)因为 run 方法正常退出而死亡,或者因为没有捕获异常终止了 run 方法而死亡

注意:带有超时参数的常用方法有:Thread.sleep(3000)Object.wait()Object.wait(3000)

sleep 和 wait 的区别

  • sleep 是线程中的方法,而 wait 是 Object 中的方法;
  • sleep 如果在方法内,不会释放锁,休眠后别的线程不能执行该方法,直到休眠时间结束,而 wait 休眠后,无论是否指定休眠时间,都会释放锁,并且加入到等待队列中,别的线程就可以继续竞争锁,执行该方法;
  • sleep 方法不依赖于同步关键字 synchronized,而 wait 需要依赖同步关键字 synchronized
  • sleep 不需要被唤醒,休眠之后就继续运行,而 wait 方法如果不指定时间需要被其他线程唤醒。

注意:

  1. 接口可以直接创建它的匿名内部类对象,然后重写方法。
  2. 对于静态方法建议使用字节码(类名.class)对象作为锁对象。

  1. ReentrantLock 实现了 Lock 接口。

  1. 线程池调用 executesubmit 方法就是在启动线程,而之前启动单个线程,则需要线程对象调用 start 方法。

  2. Timer 定时器本身就是一个单线程,前提是只创建了一个定时器对象,如果创建了多个定时器对象,则它们单独执行任务,互相不影响。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Sun 3285

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

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

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

打赏作者

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

抵扣说明:

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

余额充值