秒懂Java多线程

版权申明】非商业目的可自由转载
博文地址:https://blog.csdn.net/ShuSheng0007/article/details/80633873
出自:shusheng007

前言

这个话题一般比较大,如果往深了研究学问可大了,不仅涉及到操作系统知识还会涉及计算机硬件的知识,本文将着眼于应用层面行文。有的同学要说了:“讲那么多干什么,还不是因为自己菜”,我只能说:“被你看穿了,呵呵”。

概述

追求工作效率是人类社会能够迅速向前发展的动力,例如老王公司的软件部门有大把资金大把项目,但是只有一个码农小明,而小明计划一个一个的把项目做完。老王就急了,我这分分钟几百万的生意,你这做到猴年马月呢,于是就又雇了一批码农,将各个项目同时启动。那么我们可以把每一个码农看成一个线程(Thread),这样就形成了多任务并发执行了(其实这个例子已经是并行执行了)。

那么由人设计的计算机操作系统也不例外,它也会想尽一切办法提高任务执行效率的,于是乎多线程应用而生。

进程与线程

面过试的都知道,至于标准答案大家可以网上搜索一下。你只要知道进程面向操作系统,线程面向进程。进程是操作系统实现多任务的手段,多个进程会互相隔离,拥有自己独立的地址空间与资源。而线程存在于进程中,隔离不是很严重,可以共享同一个进程中的内存数据。

多线程的作用

  • 可以充分利用多CPU的硬件资源,提高任务执行效率。
  • 可以执行后台任务,当使用浏览器下载一部小电影的同时,你可以去浏览下性感美女的图片。
  • 提高GUI程序的用户体验,你也不希望在手机上点击了一个下载按钮后,App就卡死在那里了。
  • 等等…

Java中如何使用多线程

Java 对多线程的支持非常完善,Java使用Thread类来表示线程,下面的使用均与此类相关。

继承Thread类创建线程

继承Thread类,重写其run()方法即可。启动此线程时,只需要new MyThread().start();即可。

public class MyThread extends Thread {
    @Override
    public void run() {
        super.run();
        System.out.println("线程名称:"+getName());
    }
}

使用Runnable创建线程

从源码可知Thread存在这样一个构造函数 public Thread(Runnable target) ,因而我们可以使用实现Runnable接口的方式创建线程。

new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("线程名称:"+Thread.currentThread().getName());
    }
}).start();

由于Runnable接口是一个函数接口,所以我们可以使用Lambda表达式来实现,如下所示:

new Thread(() -> System.out.println("线程名称:"+Thread.currentThread().getName())).start();

通过这种方式多个线程可以共享线程执行体,但是线程执行结果无法获得,run()方法没有返回值。

使用Callable和Future创建线程

通过这种方式创建的线程可以有返回值,此处使用Callable 作为线程的执行体,其包含一个拥有返回值的方法call()

@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}

上面提到,Thread的构造方法需要一个Runnable类型的参数,所以不可以直接使用Callable来创建线程。Java提供了一个叫Futurer的接口来表示Callbale接口中call()方法的返回值。还为其提供了一个实现类FutureTask,此类实现了FutureRunnable接口,这样FutureTask类就可以作为参数构建线程了。

talk is cheap ,show me the code.

  private static void startThread()
  {
      //第一步:创建callable实现类
      Callable<String> c=new Callable<String>() {
          @Override
          public String call() throws Exception {
              //经过大量耗时运算得出结论
              return "总有刁民想害朕";
          }
      };
      //第二步:以c作为参数创建FutureTask实例ft
      FutureTask<String>ft=new FutureTask<String>(c);
      //第三步:以ft为参数启动线程
      new Thread(ft).start();
      //第四步:获取执行结果,get()方法是一个阻塞方法。
      try {
          System.out.println("锦衣卫调查谋反结论:"+ft.get());
      } catch (InterruptedException e) {
          e.printStackTrace();
      } catch (ExecutionException e) {
          e.printStackTrace();
      }
  }

输出结果:锦衣卫调查谋反结论:总有刁民想害朕

如何使用在代码注释中已经写的非常清楚了,如果你仍然看不懂,说明你目前水平太差,不适合看这篇文章!

线程的同步

谈到多线程,首先绕不过的话题就是线程同步。因为多线程会对共享资源状态产生竞态条件Race condition),竞态条件是指输出依赖不可控事件发生的顺序或者时间的行为,当这些不可控事件没有按照预期发生时,就会产生bug。对应到编程中就是指多个线程如果没有按照预期的顺序或者时间来操作共享状态时就会产生bug。

假设我们现在使用两个线程Thread1Thread2来并发使一个整数自增,我们期望是两个线程按照如下的顺序执行得到正确值2:
这里写图片描述
而实际情况是两个没有加锁或者同步的线程来并发做这件事情的话,很有可能执行顺序如下图所示:
这里写图片描述

很明显,第二种情况得到了错误的结果1,这种情况之所以发生就是因为整数自增操作不是排他Mutual exclusivity容)的,在发生竞态条件时出了错误。解决上述问题就需要线程的同步技术。

使用synchronized关键字

同步代码块

我们可以使用同步代码块将需要同步的资源操作保护起来,如下代码所示。其中obj称作同步监视器,通常推荐使用可能被并发访问的共享资源充当。

   synchronized (obj)
   {
       ...
   }

同步方法

我们也可以使用同步方法将需要同步的操作置于此方法中,如下代码所示。此实例方法的同步监视器就是调用此方法的实例对象this。如果是静态同步方法,那么同步监视器就是类本身。

    private synchronized void synMethod()
    {
        ...
    }

使用同步锁(Lock)

Java5提供了另一种同步代码的方式,锁(Lock).我们在学习编程的过程中,只要发现一个问题以前已经有一套解决方案,突然在新版本中又提供了另一套解决方案,那么我们立刻可以肯定:在实际开发中第一套解决方案对于解决某些特殊场景下的问题时遇到了困难,才引入第二套解决方案,第二套解决方案大部分情况下不是用来完全替换第一套解决方案的,而是其补充和增强。像Lock就是synchronized 的补充和增强,在日常大部分的开发场景下synchronized 已经足够了,Lock在特殊场景下才会使用。

synchronized其实获取的是每个object都有的隐式监视器锁(implicit monitor lock ),其要求程序获取和释放锁的操作都限定在一个块结构里,就是说其获取锁和释放锁这两个操作不是很灵活,当遇到需要这两个操作不在同一个块结构的场景就无法适应了。这是引入Lock的主要原因,当然Locksynchronized的功能更加丰富,例如使用tryLock()方法尝试获取锁,如果当前锁没有释放,则返回false.例如lockInterruptibly()方法尝试获取锁,但是如果当前锁没有释放,其转入阻塞状态,刚好此时别的线程中断了此线程,则会抛出异常,不再尝试获取锁。

下面是官方举出的一个需要使用lock的场景:

For example, some algorithms for traversing concurrently accessed data structures require the use of * "hand-over-hand" or "chain locking": you acquire the lock of node A, then node B, then release A and acquire
C, then release B and acquire D and so on. Implementations of the {@code Lock} interface enable the use of such techniques by allowing a lock to be acquired and released in different scopes, and allowing multiple locks to be acquired and released in any order.

锁有很多实现类,我们这里主要关注一个ReentrantLock的实现类,使用代码如下

   private final ReentrantLock lock=new ReentrantLock();
   private void m()
   {
       lock.lock();
       try {
           ...

       }catch (Exception e)
       {
           e.printStackTrace();
       }finally {
           lock.unlock();
       }
   }

线程的生命周期

线程的生命周期共有5个状态:新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead),他们的关系可以看下面的一张图。
这里写图片描述

具体解释如下:
1. 新建状态(New): 线程对象被创建后,就进入了新建状态。
2. 就绪状态(Runnable): 也被称为“可执行状态”。线程对象被创建后调用start()方法启动线程后其处于就绪状态,随时可能被CPU调度执行。
3. 运行状态(Running) : 线程获取CPU权限进行执行。需要注意的是,线程只能从就绪状态进入到运行状态。
4. 阻塞状态(Blocked) : 阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行,直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
(1) 等待阻塞 – 通过调用线程的wait()方法,让线程等待某工作的完成。
(2) 同步阻塞 – 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。
(3) 其他阻塞 – 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等 待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
5. 死亡状态(Dead) : 线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
具体可以参考此博文

线程的控制

既然程序中存在多个线程,那么我们就需要对多个线程执行一些控制。

线程等待(Join)

Thread中提供了一个join()方法,例如有两个线程AB,在A的执行过程中调用了Bjoin()方法,那么A线程就会被阻塞,直到B线程执行完毕。

线程睡眠(sleep)

这个大家一定不陌生,Thread.sleep(3*1000)使线程从运行状态进入**阻塞状态**3秒,此期间线程就不会被CPU调度执行。

线程让步(yield)

当我们想让线程调度器立刻做一次新的线程调度时,可以调用当前执行线程的yield()方法,此方法会使调用线程立刻进入就绪状态,线程调度器开始一次新的线程调度。此时线程优先级就起作用了,线程调度器可定是先调度优先级高的线程执行。

例如有AB两个线程,A的线程优先级小于等于B线程,那么当调用A.yield()后,B线程就会被调度执行。如果A线程的优先级大于B线程,那么即使调用A.yield()后,B线程也得不到执行,线程调度器仍然会再次调度线程A来执行。

后台线程

Java中有一类线程叫后台线程(Daemon Thread),也叫守护线程,使用setDaemon(boolean b)设置一个线程是否为后台线程。这类线程有一个特点,就是当所有前台线程都死亡后,后台线程自动死亡。

线程的通信

未完待续

线程池

参考Java8 并发教程之Thread与Executors

总结

未完待续

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
支持下载 m3u8 链接或文件为 mp4 或 ts 格式,并提供丰富的命令行选项。m3u8 downloader 开源的命令行 m3u8/HLS/dash 下载器,支持普通 AES-128-CBC 解密,多线程,自定义请求头等。支持简体中文,繁体中文和英文,完全免费。 开源免费 m3u8 下载工具 m3u8 downloader 中文版开源免费 m3u8 下载工具 m3u8 downloader 中文版 m3u8 downloader 特色: 不支持优酷视频解密 支持AES-128-CBC加密自动解密 支持多线程下载 支持下载限速 支持断点续传 支持Master List 支持直播流录制(BETA) 支持自定义HTTP Headers 支持自动合并 (二进制合并或使用ffmpeg合并) 支持选择下载m3u8中的指定时间段/分片内容 支持下载路径为网络驱动器的情况 支持下载外挂字幕轨道、音频轨道 支持仅合并为音频 自动使用系统代理(可禁止) 提供SimpleG简易的GUI生成常用参数 命令行选项 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 N_m3u8DL-CLI.exe [OPTIONS] --workDir Directory 设定程序工作目录 --saveName Filename 设定存储文件名(不包括后缀) --baseUrl BaseUrl 设定Baseurl --headers headers 设定请求头,格式 key:value 使用|分割不同的key&value --maxThreads Thread 设定程序的最大线程数(默认为32) --minThreads Thread 设定程序的最小线程数(默认为16) --retryCount Count 设定程序的重试次数(默认为15) --timeOut Sec 设定程序网络请求的超时时间(单位为,默认为10) --muxSetJson File 使用外部json文件定义混流选项 --useKeyFile File 使用外部16字节文件定义AES-128解密KEY --useKeyBase64 Base64String 使用Base64字符串定义AES-128解密KEY --useKeyIV HEXString 使用HEX字符串定义AES-128解密IV --downloadRange Range 仅下载视频的一部分分片或长度 --liveRecDur HH:MM:SS 直播录制时,达到此长度自动退出软件 --stopSpeed Number 当速度低于此值时,重试(单位为KB/s) --maxSpeed Number 设置下载速度上限(单位为KB/s) --enableDelAfterDone 开启下载后删除临时文件夹的功能 --enableMuxFastStart 开启混流mp4的FastStart特性 --enableBinaryMerge 开启二进制合并分片 --enableParseOnly 开启仅解析模式(程序只进行到meta.json) --enableAudioOnly 合并时仅封装音频轨道 --disableDateInfo 关闭混流中的日期写入 --noMerge 禁用自动合并 --noProxy 不自动使用系统代理 --disableIntegrityCheck 不检测分片数量是否完整 m3u8 downloader 是一款由 .Net 开发的一个简单易用的 m3u8 下载器,现在很多视频网站的视频都分割成了小片段,其 m3u8 就是来记录这一堆地址的文件,使用本下载器可以快速的下载并合并成一个完整的视频文件。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

ShuSheng007

亲爱的猿猿,难道你又要白嫖?

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

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

打赏作者

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

抵扣说明:

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

余额充值