开线程的方式

首先何为线程(进程/线程)?
进程操作系统动态执行基本单元,系统为每个进程分配内存,包括一般情况下,包括文本区域(text region/指令)、数据区域(data region/变量)和堆栈(stack region/对象)。

我们的程序虽然可以做到多进程,但是,多进程需要切换上下文,什么是上下文?
当程序执行到多进程的指令,那么会把当前的运行环境-堆栈等,复制一份,到另一块内存区域,而又因为cpu是轮寻机制,关于CPU运行原理,百度上有篇文章写的很好,我这边引用一下。
频繁切换上下文(大概一次20微秒),属于没必要的消耗。另外就是进程间通信需要通过管道机制,比较复杂。

所以通常需要性能的场景通常合理使用线程来提高效率,线程的定义是,一个线程有且至少有一个线程,线程利用进程的资源,且本身不拥有系统资源,所以对线程的调度的开销就会小很多(相对进程)。

因为这篇文章我定义到Java的分类下面,所以还是要通过Java来描述
其实我认为要真的好好深入学习线程进程,cpu调度这块,还是要通过C来学
日后有时间,我会用C语言来模拟实现一遍

既然了解了什么是线程,看下Java怎么实现多线程:
Thread,Runnable,Future
至于网上有些说4种的,其实就是用ExecutorService来管理了一下。
那么从头聊一聊。

Thread 其实是Runnable的实现类,类声明如下

public class Thread implements Runnable

看下最核心的一个方法
首先判定现成的状态,0状态表示该线程是新创建的,一切不是新建状态的线程,都视为非法
第二部添加到线程组,线程组默认初始长度为4,如果满了就阔为2倍。
之后可以看到,调用了一个本地方法start0,如果成功,则更改started标签量
最后一个判定,启动失败,从线程组中移除当前线程。

public synchronized void start() {
    /**
     * This method is not invoked for the main method thread or "system"
     * group threads created/set up by the VM. Any new functionality added
     * to this method in the future may have to also be added to the VM.
     *
     * A zero status value corresponds to state "NEW".
     */
    if (threadStatus != 0)
        throw new IllegalThreadStateException();

    /* Notify the group that this thread is about to be started
     * so that it can be added to the group's list of threads
     * and the group's unstarted count can be decremented. */
    group.add(this);

    boolean started = false;
    try {
        start0();
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
            /* do nothing. If start0 threw a Throwable then
              it will be passed up the call stack */
        }
    }
}

Thread中出现的Runnable,作为一个接口,只有一个方法,就是run

@FunctionalInterface
public interface Runnable {
    /**
     * When an object implementing interface <code>Runnable</code> is used
     * to create a thread, starting the thread causes the object's
     * <code>run</code> method to be called in that separately executing
     * thread.
     * <p>
     * The general contract of the method <code>run</code> is that it may
     * take any action whatsoever.
     *
     * @see     java.lang.Thread#run()
     */
    public abstract void run();
}

之后来看Future,
Future其实提供了和Runnable能力并列的接口,简单解释一下为什么这么说,Runnable接口提供了run,也就是可以放在线程中执行的能力,Future其实是赋予了线程执行后可以返回的能力,run的声明是void,所以没有返回值。

两者结合,简单易懂的一个类RunnableFuture的接口就出来了。
那么相当于Thread的实现类,FutureTask就出现了,它就是集大成者。

这么说可能有点跳跃,先看下下面的实现,一看,诶,怎么没有Future?
Future本身是一个接口,跟Runnable是相同的级别,但区别通俗来讲在于他没有run的能力,这个能力来自于Runnable。
追溯一下FutureTask,发现它继承了RunnableFuture,诶,这个单词起的就有意思了,包含了Runnable,和Future。
点进去看下

public interface RunnableFuture<V> extends Runnable, Future<V> {
    /**
     * Sets this Future to the result of its computation
     * unless it has been cancelled.
     */
    void run();
}

Future就在这,关于Future里面有什么,大家可以点进去看看,里面最关键的就是get() throws InterruptedException, ExecutionException;这个方法,就这这个方法,让我们通过调用api,拿到线程里面的值。

如果想使用这个东西,开启线程,这个时候不能用new Thread(future)这种方式了,因为Thread没有这种能力,只实现了一个Runnable接口,
这个时候,一个新的类出现了,源码如下

@FunctionalInterface
public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}

看下Callable的声明,是有返回值的,并且可以抛出异常的。
这个返回值就很关键了,通过这个返回值,你可以把任何你想通过线程拿到的结果拿回来。
而拿结果的方法就是FutureTask的get()方法,之前我们看源码时又看到,这个get方法来自于Future接口的V get()方法

简单看下如何使用一个有返回值的多线程操作

public class CallableTest {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Callable<Integer> callable = new Task();
        FutureTask task = new FutureTask(callable);

        Thread oneThread = new Thread(task);
        oneThread.start();

        System.out.println(">>>  工作结果 " + task.get().toString());
    }

}

class Task implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        System.out.println(">>>  线程开始工作");
        Thread.sleep(1000);
        System.out.println(">>>  结束工作开始返回");
        return 10;
    }

}

可以看到FutureTask依然调用的是Thread,走的是本地方法start0
Runbale就没什么好说的了,实现一个接口,放到Thread里面去执行,基本没什么东西,能力与Thread差不多,区别是实现Runnable接口的类必须依托于Thread类才能启动,

//使用这个构造方法
public Thread(Runnable target) {
    this(null, target, "Thread-" + nextThreadNum(), 0);
}

然后用Thread的start方法,需要注意的是,千万不要调run方法, 要用start。

最后看下ExecutorService这个类,ExecutorService级别很高,他的爸爸直接就是Executor。
他的儿子,是AbstractExecutorService,这里实现了submit,doInvokeAny等方法。
而我们调用Executors.newFixedThreadPool(poolSize);返回的是ThreadPoolExecutor


注:一般不建议使用Executors.newFixedThreadPool(poolSize);,什么东西全是默认,建议如下方式:

ThreadFactory factory = new ThreadFactoryBuilder().setNameFormat("test-%d").build();
ExecutorService service = new ThreadPoolExecutor(5, 200, 0L, TimeUnit.MILLISECONDS,
         new LinkedBlockingDeque<>(1024), factory, new ThreadPoolExecutor.AbortPolicy());

具体参数含义就自行百度吧,很多人讲的。

ThreadPoolExecutor这个类,又是AbstractExecutorService的儿子,所以这个关系就很明显了
ThreadPoolExecutor -> AbstractExecutorService -> ExecutorService -> Executor

看到一堆submit方法,然而没什么用,真正关键的方法,是execute方法,在ThreadPoolExecutor中实现。

这个类有点意思,一上来就给我一个下马威
private final AtomicInteger ctl = new AtlmicInteger(ctlOf(RUNNING, 0));

这个ctl到底是什么?
查了很久,在cnblogs里找到一个大神的描述 -- Okevin

这个变量使用来干嘛的呢?它的作用有点类似我们在《7.ReadWriteLock接口及其实现ReentrantReadWriteLock》中提到的读写锁有读、写两个同步状态,而AQS则只提供了state一个int型变量,此时将state高16位表示为读状态,低16位表示为写状态。这里的clt同样也是,它表示了两个概念:

workerCount:当前有效的线程数
runState:当前线程池的五种状态,Running、Shutdown、Stop、Tidying、Terminate。
  int型变量一共有32位,线程池的五种状态runState至少需要3位来表示,故workCount只能有29位,所以代码中规定线程池的有效线程数最多为2^29-1。

看到这先来聊一下线程提交任务的规则,--《java并发编程艺术》

  1. 首先会判断核心线程池里是否有线程可执行,有空闲线程则创建一个线程来执行任务。
  2. 当核心线程池里已经没有线程可执行的时候,此时将任务丢到任务队列中去。
  3. 如果任务队列(有界)也已经满了的话,但运行的线程数小于最大线程池的数量的时候,此时将会新建一个线程用于执行任务,但如果运行的线程数已经达到最大线程池的数量的时候,此时将无法创建线程执行任务。
      所以实际上对于线程池不仅是单纯地将任务丢到线程池,线程池中有线程就执行任务,没线程就等待。

最后附上大神对execute的注解

/**
  * corePoolSize:核心线程池的线程数量
  * 
  * maximumPoolSize:最大的线程池线程数量
  * 
  * keepAliveTime:线程活动保持时间,线程池的工作线程空闲后,保持存活的时间。
  * 
  * unit:线程活动保持时间的单位。
  * 
  * workQueue:指定任务队列所使用的阻塞队列
*/
//ThreadPoolExecutor#execute
public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
   //由它可以获取到当前有效的线程数和线程池的状态
   /*1.获取当前正在运行线程数是否小于核心线程池,是则新创建一个线程执行任务,否则将任务放到任务队列中*/
   int c = ctl.get();
    if (workerCountOf(c) < corePoolSize){
        if (addWorker(command, tre))     //在addWorker中创建工作线程执行任务
            return ;
        c = ctl.get();
    }
    /*2.当前核心线程池中全部线程都在运行workerCountOf(c) >= corePoolSize,所以此时将线程放到任务队列中*/
    //线程池是否处于运行状态,且是否任务插入任务队列成功
    if (isRunning(c) && workQueue.offer(command))    {
        int recheck = ctl.get();
     if (!isRunning(recheck) && remove(command))//线程池是否处于运行状态,如果不是则使刚刚的任务出队
       reject(command);//抛出RejectedExceptionException异常
     else if (workerCountOf(recheck) == 0)
       addWorker(null, false);
  }
    /*3.插入队列不成功,且当前线程数数量小于最大线程池数量,此时则创建新线程执行任务,创建失败抛出异常*/
  else if (!addWorker(command, false)){
    reject(command);    //抛出RejectedExceptionException异常
  }
}
//ThreadPoolExecutor#addWorker
private boolean addWorker(Runnable firstTask, boolean core) {
/*首先会再次检查线程池是否处于运行状态,核心线程池中是否还有空闲线程,都满足条件过后则会调用compareAndIncrementWorkerCount先将正在运行的线程数+1,数量自增成功则跳出循环,自增失败则继续从头继续循环*/
  ...
  if (compareAndIncrementWorkerCount(c))
    break retry;
  ...
  /*正在运行的线程数自增成功后则将线程封装成工作线程Worker*/
  boolean workerStarted = false;
  boolean workerAdded = false;
  Worker w = null;
  try {
    final ReentrantLock mainLock = this.mainLock;//全局锁
    w = new Woker(firstTask);//将线程封装为Worker工作线程
    final Thread t = w.thread;
    if (t != null) {
      //获取全局锁
      mainLock.lock();
      /*当持有了全局锁的时候,还需要再次检查线程池的运行状态等*/
      try {
        int c = clt.get();
        int rs = runStateOf(c);        //线程池运行状态
        if (rs < SHUTDOWN || (rs == SHUTDOWN && firstTask == null)){          //线程池处于运行状态,或者线程池关闭且任务线程为空
          if (t.isAlive())    //线程处于活跃状态,即线程已经开始执行或者还未死亡,正确的应线程在这里应该是还未开始执行的
            throw new IllegalThreadStateException();
          //private final HashSet<Worker> wokers = new HashSet<Worker>();
          //包含线程池中所有的工作线程,只有在获取了全局的时候才能访问它。将新构造的工作线程加入到工作线程集合中
          workers.add(w);
          int s = worker.size();    //工作线程数量
          if (s > largestPoolSize)
            largestPoolSize = s;
          workerAdded = true;    //新构造的工作线程加入成功
        }
      } finally {
        mainLock.unlock();
      }
      if (workerAdded) {
        //在被构造为Worker工作线程,且被加入到工作线程集合中后,执行线程任务
        //注意这里的start实际上执行Worker中run方法,所以接下来分析Worker的run方法
        t.start();
        workerStarted = true;
      }
    }
  } finally {
    if (!workerStarted)   //未能成功创建执行工作线程
      //在启动工作线程失败后,将工作线程从集合中移除
      addWorkerFailed(w);    
  }
  return workerStarted;
}
//ThreadPoolExecutor$Worker,它继承了AQS,同时实现了Runnable,所以它具备了这两者的所有特性
private final class Worker extends AbstractQueuedSynchronizer implements Runnable {
  final Thread thread;
  Runnable firstTask;
  public Worker(Runnable firstTask) {
    //设置AQS的同步状态为-1,禁止中断,直到调用runWorker
    setState(-1);    
    this.firstTask = firstTask;
    //通过线程工厂来创建一个线程,将自身作为Runnable传递传递
    this.thread = getThreadFactory().newThread(this);    
  }
  public void run() {
    runWorker(this);    //运行工作线程
  }
}

Okevin博客

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

推荐阅读更多精彩内容