秒懂Java多线程

版权申明】非商业目的可自由转载
博文地址:https://www.jianshu.com/p/25675e583943
出自: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.jpg

而实际情况是两个没有加锁或者同步的线程来并发做这件事情的话,很有可能执行顺序如下图所示:


2.jpg

很明显,第二种情况得到了错误的结果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),他们的关系可以看下面的一张图。
[图片上传失败...(image-ccc912-1530440208607)]

具体解释如下:

  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

总结

未完待续

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342