秒懂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

总结

未完待续

猜你喜欢

转载自blog.csdn.net/shusheng0007/article/details/80633873
今日推荐