jvm专题 - 2/3多线程-实现

宁可枝头抱香死,何曾吹落北风中。这篇文章主要讲述jvm专题 - 2/3多线程-实现相关的知识,希望能为你提供帮助。

本章内容比较简单,主要是对Thread的实现方式做下初步介绍,稍带着描述下相关方法的原理和使用场景。此节的内容也是开发同学能不能写出高质量线程程序的一个基础,同样也非常重要。
一、基础知识还是按之前文档描述的习惯,用一张图来概念下本章的内容,如下图所示红框内描述所示:(理论基础请看上一章内容,线程协同主要是讲锁相关的知识的后续会做为第三节补充上):

二、Thread基本方法线程的基本方法基本就是Object和Thread这两个API提供的,它们控制着线程的生命周期和状态的流转,如下图所示:

2.1、Object基本方法
2.1.1、线程等待(wait)调用该方法的线程进入 WAITING 状态,只有等待另外线程的通知或被中断才会返回,需要注意的是调用 wait()方法后,会释放对象的锁。因此,wait 方法一般用在同步方法或同步代码块中。

2.1.2、线程让步(yield)yield 会使当前线程让出 CPU 执行时间片,与其他线程一起重新竞争 CPU 时间片。一般情况下,优先级高的线程有更大的可能性成功竞争得到 CPU 时间片,但这又不是绝对的,有的操作系统对线程优先级并不敏感。
2.1.3、等待其他线程终止(join )【jvm专题 - 2/3多线程-实现】join() 方法,等待其他线程终止,在当前线程中调用一个线程的 join() 方法,则当前线程转为阻塞状态,回到另一个线程结束,当前线程再由阻塞状态变为就绪状态,等待 cpu 的宠幸。
在主线程生成并启动了子线程,需要用到子线程返回的结果,也就是需要主线程需要在子线程结束后再结束,这时候就要用到 join() 方法。
System.out.println(Thread.currentThread().getName() + "线程运行开始!");
Thread6 thread1 = new Thread6();
thread1.setName("线程 B");
thread1.join();
System.out.println("这时 thread1 执行完毕之后才能执行主线程");

2.1.4、线程唤醒(notify)Object 类中的 notify() 方法,唤醒在此对象监视器上等待的单个线程,如果所有线程都在此对象上等待,则会选择唤醒其中一个线程,选择是任意的,并在对实现做出决定时发生,线程通过调用其中一个 wait() 方法,在对象的监视器上等待,直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程,被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争。类似的方法还有 notifyAll() ,唤醒再次监视器上等待的所有线程。
2.2、Thread基本方法
2.2.1、线程睡眠(sleep)sleep 导致当前线程休眠,与 wait 方法不同的是 sleep 不会释放当前占有的锁,sleep(long)会导致线程进入 TIMED-WATING 状态,而 wait()方法会导致当前线程进入 WATING 状态。
2.2.2、 线程中断(interrupt)中断一个线程,其本意是给这个线程一个通知信号,会影响这个线程内部的一个中断标识位。这个线程本身并不会因此而改变状态(如阻塞,终止等)。
  • 调用 interrupt()方法并不会中断一个正在运行的线程。也就是说处于 Running 状态的线程并不会因为被中断而被终止,仅仅改变了内部维护的中断标识位而已。
  • 若调用 sleep()而使线程处于 TIMED-WATING 状态,这时调用 interrupt()方法,会抛出InterruptedException,从而使线程提前结束 TIMED-WATING 状态。
  • 许多声明抛出 InterruptedException 的方法(如 Thread.sleep(long mills 方法)),抛出异常前,都会清除中断标识位,所以抛出异常后,调用 isInterrupted()方法将会返回 false。
  • 中断状态是线程固有的一个标识位,可以通过此标识位安全的终止线程。比如,你想终止一个线程 thread 的时候,可以调用 thread.interrupt()方法,在线程的 run 方法内部可以根据 thread.isInterrupted()的值来优雅的终止线程。
2.2.3、其他方法
  • isAlive(): 判断一个线程是否存活。
  • activeCount(): 程序中活跃的线程数。
  • enumerate(): 枚举程序中的线程。
  • currentThread(): 得到当前线程。
  • isDaemon(): 一个线程是否为守护线程。
  • setDaemon(): 设置一个线程为守护线程。(用户线程和守护线程的区别在于,是否等待主线程依赖于主线程结束而结束)
  • setName(): 为线程设置一个名称。
  • setPriority(): 设置一个线程的优先级。
  • getPriority()::获得一个线程的优先级。
2.3、sleep与wait区别
对于 sleep()方法,我们首先要知道该方法是属于 Thread 类中的。而 wait()方法,则是属于 Object 类中的。sleep()方法导致了程序暂停执行指定的时间,让出 cpu 该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。 在调用 sleep()方法的过程中,线程不会释放对象锁。而当调用 wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用 notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。
三、单线程实现3.1、继承Thread类
Thread 类本质上是实现了 Runnable 接口的一个实例,代表一个线程的实例。启动线程的唯一方法就是通过 Thread 类的 start()实例方法。
start()方法是一个 native 方法,它将启动一个新线程,并执行 run()方法。
public class MyThread extends Thread
public void run()


System.out.println("MyThread.run()");
MyThread myThread1 = new MyThread();
myThread1.start();

3.2、实现Runnable接口
如果自己的类已经 extends 另一个类,就无法直接 extends Thread,此时,可以实现一个 Runnable 接口。
public class MyThreadRunnable implements Runnable
public void run()


//启动 MyThread,需要首先实例化一个 Thread,并传入自己的 MyThread 实例:
MyThread myThread = new MyThread();
Thread thread = new Thread(myThread);
thread.start();
//事实上,当传入一个 Runnable target 参数给 Thread 后,Thread 的 run()方法就会调用
target.run()

3.3、start与run区别
  1. start()方法来启动线程,真正实现了多线程运行。这时无需等待 run 方法体代码执行完毕,可以直接继续执行下面的代码。
  2. 通过调用 Thread 类的 start()方法来启动一个线程, 这时此线程是处于就绪状态, 并没有运行;
  3. 方法 run()称为线程体,它包含了要执行的这个线程的内容,线程就进入了运行状态,开始运行 run 函数当中的代码。 Run 方法运行结束, 此线程终止。然后 CPU 再调度其它线程。
3.4、“每任务每线程”的程序缺点
  1. 线程生命周期的开销,如果请求是频繁且轻量级的,就会消耗大量的计算资源。
  2. 资源消耗量,主要是针对内存。如果可运行的线程数多于可用的处理器数,线程将会空闲。大量空闲线程占用更多的内存,给GC带来压力,而且会存在大量线程在竞争CPU资源,还会产生其他的性能开销。如果有足够多的线程保持所有CPU忙碌,那么再创建更多的线程是百害无一利的。
  3. 稳定性,应该限制可创建线程的数目,这个数目受OS、JVM启动参数、Thread的构造函数中请求的栈大小等因素影响。如果打破了这些规则可能会收到OutOfMemoryError错误。(在32位的机器上,主要的限制因素是线程栈的地址空间,每个线程都维护着两个执行栈,一个用于java代码,另一个用于原生代码,典型的JVM默认会产生一个组合的栈,大小在0.5M大小左右,可以通过-Xss JVM参数或者通过Thread的构造函数修改这个值。如果为每个线程分配了大小232字节的栈,那么你的线程数量将被限制在几千到几万间不等)。
这种实现方式只能当作练习,不能应用到正式的环境中去。

四、线程池实现4.1、基本原理
Java 中的线程池是通过 Executor 框架实现的,该框架中用到了 Executor,Executors,ExecutorService,ThreadPoolExecutor ,Callable 和 Future、FutureTask 这几个类。

4.1.1、线程池的原理线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量超出数量的线程排队等候,等其它线程执行完毕,再从队列中取出任务来执行。他的主要特点为:线程复用; 控制最大并发数; 管理线程。
每一个 Thread 的类都有一个 start 方法。 当调用 start 启动线程时 Java 虚拟机会调用该类的 run方法。 那么该类的 run() 方法中就是调用了 Runnable 对象的 run() 方法。 我们可以继承重写Thread 类,在其 start 方法中添加不断循环调用传递过来的 Runnable 对象。 这就是线程池的实现原理。循环方法中不断获取 Runnable 是用 Queue 实现的,在获取下一个 Runnable 之前可以是阻塞的。
4.2.2、线程池的组成
  1. 线程池管理器:用于创建并管理线程池
  2. 工作线程:线程池中的线程
  3. 任务接口:每个任务必须实现的接口,用于工作线程调度其运行
  4. 任务队列:用于存放待处理的任务,提供一种缓冲机制
4.3.3、线程池的执行策略将任务的提交与任务的执行进行解耦,价值在于让你可以简单地为一个类给定的任务制定执行策略,并且保证后续的修改不至于太困难,执行策略指明了:
  1. 任务在什么线程中执行;
  2. 任务以什么顺序执行(FIFO、LIFO、优先级);
  3. 可以有多少个任务并发执行;
  4. 可以有多少个任务进入等待执行队列;
  5. 如果系统过载,需要放弃哪个任务并且如何通知Application知道这一切;
  6. 在一个任务的执行前与后,应该插入什么处理操作。
执行策略是资源管理工具,最佳策略取决于可用的计算资源和你对服务质量的需求,将任务的提交与执行进行分离,有助于在部署阶段选择一个与当前硬件最匹配的执行策略。所以以后的程序中尽量少用或不用new Thread(runnable).start()这种方式而改用Executor委托执行。
4.4.4、线程池工作过程
  1. 线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
  2. 当调用 execute() 方法添加一个任务时,线程池会做如下判断:
  1. 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
  2.   如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;
  3. 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
  4. 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常 RejectExecutionException。
  1. 当一个线程完成任务时,它会从队列中取下一个任务来执行。
  2. 当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。
4.2、Executor
线程是使任务异步执行的机制,作为Executor框架的一部分,它是基于生产—消费模式设计,java.util.concurrent提供了一个灵活的线程池实现。Executor很简单但可用于异步任务执行,支持不同类型的任务执行策略,还为任务提交和任务执行之间的解耦提供了标准的方法,还提供了对生命周期的支持以及钩子函数等。在生产—消费模式中,提交任务是执行者是生产者,执行任务的线程是消费者。Executor接口的实现相当于一个模板或抽象模板实现,是把Runnable委托给Executor来执行。所以可以定义自己的Executor实现类。
Executor实现通常只是为执行任务而创建线程,JVM会在所有线程全部终止后才通出,因此无法正确关闭Executor,进而会阻塞JVM的结束。
因为Executor是异步地执行任务,所以有很多不确定因素,为了解决执行服务的生命周期问题,提供了ExecutorService接口,它扩展了Executor接口同时添加了一些用于生命周期(运行、关闭、终止)管理的方法。
public interface ExecutorService extends Executor
//不在接收新的任务后平缓关闭
void shutdown();
//强制停止正在运行和队列中等待的任务,并返回列表以便序列化等操作,下次启动时恢复
List< Runnable> shutdownNow();
boolean isShutdown();
boolean isTerminated();
//转换ExecutorService的状态,同时调用shutdown()方法
boolean awaitTermination(long timeout, TimeUnit unit)throws InterruptedException;

一旦所有的任务全部完成后ExecutorService会转入终止状态,可以调用awaitTermination等待ExecutorService到达终止状态,也可以轮询isTerminated判断ExecutorService是否已经终止。通常shutdown会紧随awaitTermination之后,这样可以产生同步关闭ExecutorService的效果。
4.2.1、基本线程池实现(Runnable)
ExecutorService threadPool = Executors.newFixedThreadPool(10);
while (true)
threadPool.execute(new Runnable()); // 提交多个线程任务,并执行

@Override
public void run ()
System.out.println(Thread.currentThread().getName() + " is running ..");
try
Thread.sleep(3000);
catch (InterruptedException e)
e.printStackTrace();


4.2.2、带返回值的线程(Callable, Future)Callable主要用于计算时间长的任务,比如下载、复杂运算、数据库操作。它会在主进入点call--等待返回值,并为可能抛出的异常预告做好了准备,Executors包含一些工具方法,可以把其他类型的任务封装成一个Callable。Callable< void> ===Runnable。在Executor框架中,如果任务执行要花费很长时间,任务可以手动取消,但对于已经开始的任务,只有中断后才可以取消,取消一个已完成的任务不会有任何影响。
Future可以描述任务的生命周期,并提供了相关的方法来获得任务的结果、取消任务以及检验任务是否完成。Future和ExecutorService这些线程相关的对象的生命周期是单向的,无法回退。Future的get方法是个阻塞方法, 注意get处理异常的能力,它会把异常重新包装后再抛出。
ExecutorService的所有submit()方法都返回一个Future。可以将一个Runnable或Callable提交给executor,然后得到一个Future。也可以显示的为Runnable或Callable创建一个FutureTask。FutureTask实现了Runnable接口。所以可以直接交给ExecutorService来执行,也可以直接调用run方法。
private final ExecutorService executor = Executors.newCachedThreadPool();

void renderPage(CharSequence source)
final List< ImageInfo> imageInfos = scanForImageInfo(source);
Callable< List< ImageData> > task =
new Callable< List< ImageData> > ()
public List< ImageData> call()
List< ImageData> result = new
ArrayList< ImageData> ();
for (ImageInfo imageInfo : imageInfos)
result.add(imageInfo.downloadImage());
return result;

;

Future< List< ImageData> > future = executor.submit(task);
//FutureTask future = new FutureTask(task);
//executor.submit(future); //这两行代码和上面一行代码是等价的。

try
List< ImageData> imageData = https://www.songbingjia.com/android/future.get();
for (ImageData data : imageData)

    推荐阅读