Java线程浅析

    xiaoxiao2021-03-26  8

    本文目录

    1 线程概念

    2 线程任务

    3 线程操作

    4 Executor

    5 CompletionService

     

    1 线程概念

    运行程序,会使用到CPU、内存等资源。程序将通过“进程”来申请并管理所需要的相关资源。每个“进程”会有一个“主线程”,“线程”使用进程申请来的资源,去执行相关的逻辑操作。“主线程”可以创建其它新的“线程”,多个“线程”可以并行(同时)执行各自的逻辑操作。

     

    实际上,对于单核处理器而言,多个线程并不是真正的同时执行,而是轮流获得CPU使用权,执行一部分操作,如图:

     

    当每次CPU轮换的时间间隔无限缩小,趋近于0时,给我们的感觉,像是每个线程在同时进行,如图:

     

    对于多核处理器,当线程数量小于处理器核数的时候,每个线程将由其中一个核单独处理。当线程数量大于处理器核数时,某些线程将共享一个核,相当于上述多线程单核情况。

     

    2 线程任务

    使用线程,首先要确定线程内执行什么任务。有两种方法,来确定线程任务。

     

    2.1 方法一:继承Thread

    class ThreadExample extends Thread { // 重写run方法 public void run() { // 线程执行内容 } }

     

    2.2 方法二:实现Runnable

    class RunnableExample implements Runnable { // 重写run方法 public void run() { // 线程执行内容 } }

     

    2.3 二者区别

    Thread类是对线程的一个抽象,其中包含一些线程的操作方法,例如:启动线程、终止线程等。

    Runnable类是对线程任务的一个抽象,主要是包含相关的处理逻辑。

    将“线程的操作”与“线程的任务”分离,各自关注各自的职责和任务,是一个很好的方案。

    另外,由于Java不允许多继承关系,所以,对于某父类的子类,只能通过实现Runnable接口的方法。

     

    3 线程操作

     

    3.1 创建线程对象(构造方法)

    若通过继承Thread类(方法一)来定义线程执行内容,则直接创建该子类对象即可。

    class ThreadExample extends Thread { // 重写run方法 public void run() { // 线程执行内容 } } class ThreadModel { public static void main(string[] args) { // 创建ThreadExample线程对象 ThreadExample thread = new ThreadExample(); } }

     

    若通过实现Runnable接口(方法二)来定义线程执行内容,则创建标准线程对象并传入Runnable对象。

    class RunnableExample implements Runnable { // 重写run方法 public void run() { // 线程执行内容 } } class ThreadModel { public static void main(string[] args) { // 创建RunnableExample对象 RunnableExample runnable = new RunnableExample(); // 创建标准线程并传入runnable对象(传入线程内容) Thread thread = new Thread(runnable); } }

     

    方法二,通过传入Runnable对象来指定线程内容,因此,可以通过Runnable对象实现数据共享(共享的私有变量)。

    示例代码如下,多个线程可以使用同一个Runnable对象,当两个线程执行完毕时,私有变量shareNum值为2。

    class RunnableExample implements Runnable { // 私有变量 int sharedNum = 1; // 重写run方法 public void run() { sharedNum++; } } class ThreadModel { public static void main(string[] args) { // 创建RunnableExample对象 RunnableExample runnable = new RunnableExample(); // 创建标准线程1并传入runnable对象 Thread thread1 = new Thread(runnable); // 创建标准线程2并传入runnable对象 Thread thread2 = new Thread(runnable); } }

     

    指定与获取线程名称。

    // 指定线程名称 Thread thread = new Thread(String threadName); Thread thread = new Thread(Runnable runObj, String threadName); // 获取线程名称 Thread.currentThread().getName();

     

    3.2 启动线程

    threadModel.start()

    开启一个新的线程。

     

    threadModel.start() 和 threadModel.run() 的区别:

    threadModel.start() 是开启一个新线程去执行 run() 方法,如图:

     

    threadModel.run() 是在当前线程中调用 run() 方法,与调用其它方法一样,如图:

     

    3.3 线程睡眠(暂停)

    Thread.sleep(long millis)

    静态方法。暂停当前线程一段时间(millis-毫秒),继续执行。

    当前线程:调用Thread.sleep方法的线程,即Thread.sleep代码所在的线程。

     

    Thread.sleep(long millis) 与 Object.wait() 的区别:

    Thread.sleep方法是线程操作方法,Object.wait方法是同步锁的方法。

    实际上,二者并不是同一类的内容,不应该进行比较。但是,二者表现出来的效果相似,因此,常常被混淆。

    Thread.sleep方法涉及当前线程,是对线程进行暂停操作。

    Object.wait涉及同步锁和并发问题,是关于锁和执行权的问题。

    具体分析,请浏览《Java内置锁与显式锁浅析》文章。

     

    3.4 终止线程

    threadModel.interrupt()

    终止threadModel线程,即使该线程还没执行完所有内容。

    通过该方法终止线程,会使得被终止的线程抛出InterruptedException异常。

     

    3.5 线程状态

    threadModel.isAlive()

    判断线程是否存活。若线程存活(没有被销毁),则返回true;否则,返回false。

     

    3.6 线程加入(等待其它线程)

    threadOther.join()

    当前线程暂停,等待threadOther线程执行完毕后,继续执行,如图:

     

    3.7 降低优先权

    Thread.yield()

    静态方法。表示临时降低当前线程优先权,使CPU优先分派资源(使用权)给其它线程。

     

    4 Executor

    4.1 基本概念

    线程池用于管理线程,提供一系列管理线程的方法。

    其主要优点在于,通过线程重用,减少线程创建及销毁的次数,从而减少相关操作的消耗。

    Executor框架中的线程重用,不是简单的替换Runnable内容并重新运行线程,而是通过线程及阻塞队列达到重用的效果(即不需要重复创建和销毁线程),是典型的“生产者-消费者”模式。线程池会保持所有线程持续运行,每个线程不断从阻塞队列中提取Runnable任务并执行。若队列中无等待执行的任务,则暂时阻塞线程,待新的任务加入队列后,再次唤醒线程,从而达到线程重用的效果。

     

    4.2 创建Executor

    ExecutorService executor = Executors.newSingleThreadExecutor()

    创建一个单线程池,所有可执行任务将按照指定顺序依次执行,即串行化执行。

     

    ExecutorService executor = Executors.newFixedThreadPool(int threadNum)

    创建一个定长线程池,该线程池拥有固定数量的线程,即可控制线程的最大并发数量。当可执行任务的数量超过线程数量时,多余的部分将在队列中等待。

     

    ExecutorService executor = Executors.newCachedThreadPool()

    创建一个可缓存的线程池,根据需要增加和减少线程数量,无线程数量限制(但是需要考虑硬件配置)。

     

    ExecutorService executor = new ThreadPoolExecutor(...)

    创建一个自定义线程池,可设置核心线程数、最大线程数、闲置线程存活时间、任务队列、任务队列满载处理方法等内容。

     

    4.3 添加执行任务

    executor.execute(Runnable task)

    将task对象加入执行队列中,当有空闲线程时,按照队列顺序,执行task任务。

     

    Future<V> future = executor.submit(Callable<V> task)

    该方法同execute方法,不同之处在于,该方法会返回一个Future对象,通过Future对象,可以获得任务执行结果。

    该方法涉及Callable和Future的使用,不在本文讨论范围。

     

    4.4 停止

    executor.shutdown()

    可理解为关闭添加入口(关闭大门),无法再添加新的执行任务。已经添加到执行队列的任务,不受影响,继续等待被执行。

     

    executor.shutdownNow()

    尝试停止所有正在执行和等待执行的任务,并返回所有等待执行的任务列表(List)。

     

    4.5 等待执行结束

    executor.awaitTermination(long timeout, TimeUnit unit)

    阻塞当前线程(调用该方法的线程),直到线程池中的所有执行任务被完成或发生超时(Timeout)。

     

    5 CompletionService

    5.1 基本概念

    通过Executor管理线程池,大大简化了线程操作,通过Future对象,我们还可以对执行中的线程进行控制。

    但是,通过Future和Executor框架,我们无法知道哪一个线程会最先执行完毕,因此,无法知道应该先对哪一个Future对象结果进行处理。若通过轮询每一个Future对象并执行get操作,虽然可行,但不是一个好的方法。

    CompletionService将Executor和BlockingQueue的功能进行融合,可以将Callable任务交给它进行管理,由CompletionService进行任务的提交。当任务执行完成后,会将结果加入到BlockingQueue队列。因此,我们只需要关注队列中的数据,并通过队列来获取执行结果即可。最先执行完毕的任务会被第一个加入到队列,也将被第一个获取并处理。

     

    5.2 创建CompletionService

     

    CompletionService<T> service = new ExecutorCompletionService<>(Executor executor, BlockingQueue queue)

    指定一个Executor和一个阻塞队列,创建一个CompletionService对象,此后,通过service对象进行任务提交和结果获取操作。

     

    5.3 提交任务

    service.submit(Callable<T> task)

    向service对象提交一个任务,该任务将由executor执行,其结果将加入到queue队列。

     

    5.4 获取结果

    Future<T> future = service.poll()

    获取下一个已完成任务的Future对象,若暂时没有已完成任务,则返回null值。

     

    Future<T> future = service.take()

    获取下一个已完成任务的Future对象,若暂时没有已完成任务,则阻塞等待,直到下一个已完成任务的Future对象被添加至队列。

     

    6 相关文章

    《Java内置锁与显式锁浅析》

     

    转载请注明原文地址: https://ju.6miu.com/read-650329.html

    最新回复(0)