本月累计签到次数:

今天获取 积分

数据

数据

825 浏览

从串行线程封闭到对象池、线程池

智能科技类 箭在弦上 2017-11-23 10:32 发表了文章 来自相关话题

今天讲一个牛逼而实用的概念,串行线程封闭。对象池是串行线程封闭的典型应用场景;线程池糅合了对象池技术,但核心实现不依赖于对象池,很容易产生误会。

本文从串行线程封闭和对象池入手,最后通过源码分析线程池的核心原理,厘清对象池与线程池之间的误会。

线程封闭与串行线程封闭

线程封闭

线程封闭是一种常见的线程安全设计 查看全部
今天讲一个牛逼而实用的概念,串行线程封闭。对象池是串行线程封闭的典型应用场景;线程池糅合了对象池技术,但核心实现不依赖于对象池,很容易产生误会。

本文从串行线程封闭和对象池入手,最后通过源码分析线程池的核心原理,厘清对象池与线程池之间的误会。

线程封闭与串行线程封闭

线程封闭

线程封闭是一种常见的线程安全设计
799 浏览

我与数据打交道(三)

机械自动化类 机械设计 2016-12-05 14:18 发表了文章 来自相关话题

 前言:本期文章是《我与数据打交道》系列的第三篇,也是最后一篇。后面几期文章的重心是神经网络,敬请关注。


小时候,我会经常听见大人们说:小孩怎么这么爱挑食?却很少有听说大人们也爱挑食,大概是因为大人们买的都是他们愿意吃的东西吧。


“己所不欲,勿施于人;己所之欲,慎施于人”。这句话的意思大概是,不要把自己主观上 查看全部
 前言:本期文章是《我与数据打交道》系列的第三篇,也是最后一篇。后面几期文章的重心是神经网络,敬请关注。


小时候,我会经常听见大人们说:小孩怎么这么爱挑食?却很少有听说大人们也爱挑食,大概是因为大人们买的都是他们愿意吃的东西吧。


“己所不欲,勿施于人;己所之欲,慎施于人”。这句话的意思大概是,不要把自己主观上
805 浏览

我与数据打交道(二)

机械自动化类 机械设计 2016-12-05 14:11 发表了文章 来自相关话题

 前言:前几天有读者来信反应要我推荐一些深度学习的教材或资源,我想留到下一期再推荐给大家。今天文章的主要内容还是来聊聊我与数据打交道的感悟。



提到数据这个词,我们脑海中的第一反应会是什么呢?也许有人会想到:“我今年已经多少岁了”,“我的生日是几月几号”,“我朋友圈中有多少个好友了”,“今天我在朋友圈收到了多少个 查看全部
 前言:前几天有读者来信反应要我推荐一些深度学习的教材或资源,我想留到下一期再推荐给大家。今天文章的主要内容还是来聊聊我与数据打交道的感悟。



提到数据这个词,我们脑海中的第一反应会是什么呢?也许有人会想到:“我今年已经多少岁了”,“我的生日是几月几号”,“我朋友圈中有多少个好友了”,“今天我在朋友圈收到了多少个
780 浏览

我与数据打交道(一)

机械自动化类 机械设计 2016-12-05 14:06 发表了文章 来自相关话题

 

前言:公众号文章纯粹谈技术貌似不太好,一些复杂的公式也无法支持,插入图片看起来也并不美观,因此以后的文章主要以文字的形式谈谈技术以及感悟。本篇主要回顾一下我与数据分析有过的峥嵘岁月。那些年,为了赶工手抄版或电子版的实验报告插图、为了拿高分不得不让数据看起来更美观所用过的一些数据分析软件的心得体会。



    查看全部
 

前言:公众号文章纯粹谈技术貌似不太好,一些复杂的公式也无法支持,插入图片看起来也并不美观,因此以后的文章主要以文字的形式谈谈技术以及感悟。本篇主要回顾一下我与数据分析有过的峥嵘岁月。那些年,为了赶工手抄版或电子版的实验报告插图、为了拿高分不得不让数据看起来更美观所用过的一些数据分析软件的心得体会。



   
825 浏览

从串行线程封闭到对象池、线程池

智能科技类 箭在弦上 2017-11-23 10:32 发表了文章 来自相关话题

今天讲一个牛逼而实用的概念,串行线程封闭。对象池是串行线程封闭的典型应用场景;线程池糅合了对象池技术,但核心实现不依赖于对象池,很容易产生误会。

本文从串行线程封闭和对象池入手,最后通过源码分析线程池的核心原理,厘清对象池与线程池之间的误会。

线程封闭与串行线程封闭

线程封闭

线程封闭是一种常见的线程安全设计策略:仅在固定的一个线程内访问对象,不对其他线程共享。

使用线程封闭技术,对象O始终只对一个线程T1可见,“单线程”中自然不存在线程安全的问题。

ThreadLocal是常用的线程安全工具。线程封闭在Servlet及高层的web框架Spring等中应用不少。
https://monkeysayhi.github.io/2016/11/27/源码%7CThreadLocal的实现原理/

串行线程封闭

线程封闭虽然好用,却限制了对象的共享。串行线程封闭改进了这一点:对象O只能由单个线程T1拥有,但可以通过安全的发布对象O来转移O的所有权;在转移所有权后,也只有另一个线程T2能获得这个O的所有权,并且发布O的T1不会再访问O。

所谓“所有权”,指修改对象的权利。

相对于线程封闭,串行线程封闭使得任意时刻,最多仅有一个线程拥有对象的所有权。当然,这不是绝对的,只要线程T1事实不会再修改对象O,那么就相当于仅有T2拥有对象的所有权。串行线层封闭让对象变得可以共享(虽然只能串行的拥有所有权),灵活性得到大大提高;相对的,要共享对象就涉及安全发布的问题,依靠BlockingQueue等同步工具很容易实现这一点。

对象池是串行线程封闭的经典应用场景,如数据库连接池等。

对象池

对象池利用了串行封闭:将对象O“借给”一个请求线程T1,T1使用完再交还给对象池,并保证“未擅自发布该对象”且“以后不再使用”;对象池收回O后,等T2来借的时候再把它借给T2,完成对象所有权的传递。

猴子撸了一个简化版的线程池,用户只需要覆写newObject()方法:

public abstract class AbstractObjectPool<T> {
  protected final int min;
  protected final int max;
  protected final List<T> usings = new LinkedList<>();
  protected final List<T> buffer = new LinkedList<>();
  private volatile boolean inited = false;
  public AbstractObjectPool(int min, int max) {
    this.min = min;
    this.max = max;
    if (this.min < 0 || this.min > this.max) {
      throw new IllegalArgumentException(String.format(
          "need 0 <= min <= max <= Integer.MAX_VALUE, given min: %s, max: %s", this.min, this.max));
    }
  }
  public void init() {
    for (int i = 0; i < min; i++) {
      buffer.add(newObject());
    }
    inited = true;
  }
  protected void checkInited() {
    if (!inited) {
      throw new IllegalStateException("not inited");
    }
  }
  abstract protected T newObject();
  public synchronized T getObject() {
    checkInited();
    if (usings.size() == max) {
      return null;
    }
    if (buffer.size() == 0) {
      T newObj = newObject();
      usings.add(newObj);
      return newObj;
    }
    T oldObj = buffer.remove(0);
    usings.add(oldObj);
    return oldObj;
  }
  public synchronized void freeObject(T obj) {
    checkInited();
    if (!usings.contains(obj)) {
      throw new IllegalArgumentException(String.format("obj not in using queue: %s", obj));
    }
    usings.remove(usings.indexOf(obj));
    buffer.add(obj);
  }
}

AbstractObjectPool具有以下特性:
支持设置最小、最大容量
对象一旦申请就不再释放,避免了GC

虽然很简单,但大可以用于一些时间敏感、资源充裕的场景。如果时间进一步敏感,可将getObject()、freeObject()改写为并发程度更高的版本,但记得保证安全发布安全回收;如果资源不那么充裕,可以适当增加对象回收策略。

可以看到,一个对象池的基本行为包括:
创建对象newObject()
借取对象getObject()
归还对象freeObject()

典型的对象池有各种连接池、常量池等,应用非常多,模型也大同小异,不做解析。令人迷惑的是线程池,很容易让人误以为线程池的核心原理也是对象池,下面来追一遍源码。

线程池

首先摆出结论:线程池糅合了对象池模型,但核心原理是生产者-消费者模型。

继承结构如下:

用户可以将Runnable(或Callables)实例提交给线程池,线程池会异步执行该任务,返回响应的结果(完成/返回值)。

猴子最喜欢的是submit(Callable<T> task)方法。我们从该方法入手,逐步深入函数栈,探究线程池的实现原理。

submit()

submit()方法在ExecutorService接口中定义,AbstractExecutorService实现,ThreadPoolExecutor直接继承。

public abstract class AbstractExecutorService implements ExecutorService {
...
    public <T> Future<T> submit(Callable<T> task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<T> ftask = newTaskFor(task);
        execute(ftask);
        return ftask;
    }
...


AbstractExecutorService#newTaskFor()创建一个RunnableFuture类型的FutureTask。

核心是execute()方法。

execute()

execute()方法在Executor接口中定义,ThreadPoolExecutor实现。

public class ThreadPoolExecutor extends AbstractExecutorService {
...
    public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        else if (!addWorker(command, false))
            reject(command);
    }
...
}

我们暂且忽略线程池的池化策略。关注一个最简单的场景,看能不能先回答一个问题:线程池中的任务如何执行?

核心是addWorker()方法。以8行的参数为例,此时,线程池中的线程数未达到最小线程池大小corePoolSize,通常可以直接在9行返回。

addWorker()

简化如下:

public class ThreadPoolExecutor extends AbstractExecutorService {
...
    private boolean addWorker(Runnable firstTask, boolean core) {
        boolean workerStarted = false;
        boolean workerAdded = false;
        Worker w = null;
        try {
            w = new Worker(firstTask);
            final Thread t = w.thread;
            if (t != null) {
                final ReentrantLock mainLock = this.mainLock;
                mainLock.lock();
                try {
                    int rs = runStateOf(ctl.get());
                    if (rs < SHUTDOWN) {
                        workers.add(w);
                        workerAdded = true;
                    }
                } finally {
                    mainLock.unlock();
                }
                if (workerAdded) {
                    t.start();
                    workerStarted = true;
                }
            }
        } finally {
            if (! workerStarted)
                addWorkerFailed(w);
        }
        return workerStarted;
    }
...
}

我去掉了很多用于管理线程池、维护线程安全的代码。假设线程池未关闭,worker(即w,下同)添加成功,则必然能够将worker添加至workers中。workers是一个HashSet:

private final HashSet<Worker> workers = new HashSet<Worker>();

哪里是对象池?

如果说与对象池有关,那么workers即相当于示例代码中的using,应用了对象池模型;只不过这里的using是一直增长的,直到达到最大线程池大小maximumPoolSize。

但是很明显,线程池并没有将线程发布出去,workers也仅仅完成using“保存线程”的功能。那么,线程池中的任务如何执行呢?跟线程池有没有关系?

哪里又不是?

注意9、17、24行:

9行将我们提交到线程池的firstTask封装入一个worker。
17行将worker加入workers,维护起来
24行则启动了worker中的线程t

核心在与这三行,但线程池并没有直接在addWorker()中启动任务firstTask,代之以启动一个worker。最终任务必然被启动,那么我们继续看Worker如何启动这个任务。

Worker

Worker实现了Runnable接口:

private final class Worker
    extends AbstractQueuedSynchronizer
    implements Runnable
{
...
    Worker(Runnable firstTask) {
        setState(-1); // inhibit interrupts until runWorker
        this.firstTask = firstTask;
        this.thread = getThreadFactory().newThread(this);
    }
    /** Delegates main run loop to outer runWorker  */
    public void run() {
        runWorker(this);
    }
...
}

为什么要将构造Worker时的参数命名为firstTask?因为当且仅当需要建立新的Worker以执行任务task时,才会调用构造函数。因此,任务task对于新Worker而言,是第一个任务firstTask。

Worker的实现非常简单:将自己作为Runable实例,构造时在内部创建并持有一个线程thread。Thread和Runable的使用大家很熟悉了,核心是Worker的run方法,它直接调用了runWorker()方法。

runWorker()

敲黑板!!!

重头戏来了。简化如下:

public class ThreadPoolExecutor extends AbstractExecutorService {
...
    final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        Runnable task = w.firstTask;
        w.firstTask = null;
        w.unlock(); // allow interrupts
        boolean completedAbruptly = true;
        try {
            while (task != null || (task = getTask()) != null) {
                w.lock();
                try {
                    beforeExecute(wt, task);
                    Throwable thrown = null;
                    try {
                        task.run();
                    } catch (RuntimeException x) {
                        thrown = x; throw x;
                    } catch (Error x) {
                        thrown = x; throw x;
                    } catch (Throwable x) {
                        thrown = x; throw new Error(x);
                    } finally {
                        afterExecute(task, thrown);
                    }
                } finally {
                    task = null;
                    w.completedTasks++;
                    w.unlock();
                }
            }
            completedAbruptly = false;
        } finally {
            processWorkerExit(w, completedAbruptly);
        }
    }
...
}

我们在前面将要执行的任务赋值给firstTask,5-6行首先取出任务task,并将firstTask置为null。因为接下来要执行task,firstTask字段就没有用了。

重点是10-31行的while循环。下面分情况讨论。

case1:第一次进入循环,task不为null

case1对应前面作出的诸多假设。

第一次进入循环时,task==firstTask,不为null,使10行布尔短路直接进入循环;从而16行执行的是firstTask的run()方法;异常处理不表;最后,finally代码块中,task会被置为null,导致下一轮循环会进入case2。

case2:非第一次进入循环,task为null

case2是更普遍的情况,也就是线程池的核心。

case1中,task被置为了null,使10行布尔表达式执行第二部分(task = getTask()) != null(getTask()稍后再讲,它返回一个用户已提交的任务)。假设task得到了一个已提交的任务,从而16行执行的是新获得的任务task的run()方法。后同case1,最后task仍然会被置为null,以后循环都将进入case2。

GETTASK()

任务从哪来呢?简化如下:

public class ThreadPoolExecutor extends AbstractExecutorService {
...
    private Runnable getTask() {
        boolean timedOut = false;
        for (;;) {
            try {
                Runnable r = timed ?
                    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                    workQueue.take();
                if (r != null)
                    return r;
                timedOut = true;
            } catch (InterruptedException retry) {
                timedOut = false;
            }
        }
    }
...
}

我们先看最简单的,19-28行。

首先,workQueue是一个线程安全的BlockingQueue,大部分时候使用的实现类是LinkedBlockingQueue:

private final BlockingQueue<Runnable> workQueue;

假设timed为false,则调用阻塞的take()方法,返回的r一定不是null,从而12行退出,将任务交给了某个worker线程。

一个小细节有点意思:前面每个worker线程runWorker()方法时,在循环中加锁粒度在worker级别,直接使用的lock同步;但因为每一个woker都会调用getTask(),考虑到性能因素,源码中getTask()中使用乐观的CAS+SPIN实现无锁同步。

关于乐观锁和CAS,可以参考https://monkeysayhi.github.io/2017/10/22/源码%7C并发一枝花之ConcurrentLinkedQueue【伪】/

workQueue中的元素从哪来呢?这就要回顾execute()方法了。

EXECUTE()

public class ThreadPoolExecutor extends AbstractExecutorService {
...
    public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        else if (!addWorker(command, false))
            reject(command);
    }
...
}

前面以8行的参数为例,此时,线程池中的线程数未达到最小线程池大小corePoolSize,通常可以直接在9行返回。进入8行的条件是“当前worker数小于最小线程池大小corePoolSize”。

如果不满足,会继续执行到12行。isRunning(c)判断线程池是否未关闭,我们关注未关闭的情况;则会继续执行布尔表达式的第二部分workQueue.offer(command),尝试将任务command放入队列workQueue。

workQueue.offer()的行为取决于线程池持有的BlockingQueue实例。

Executors.newFixedThreadPool()、Executors.newSingleThreadExecutor()创建的线程池使用LinkedBlockingQueue,而Executors.newCachedThreadPool()创建的线程池则使用SynchronousQueue。

以LinkedBlockingQueue为例,创建时不配置容量,即创建为无界队列,则LinkedBlockingQueue#offer()永远返回true,从而进入12-18行。

更细节的内容不必关心了,当workQueue.offer()返回true时,已经将任务command放入了队列workQueue。当未来的某个时刻,某worker执行完某一个任务之后,会从workQueue中再取出一个任务继续执行,直到线程池关闭,直到海枯石烂。

CachedThreadPool是一种无界线程池,使用SynchronousQueue能进一步提升性能,简化代码结构。留给读者分析。

CASE2小结

可以看到,实际上,线程池的核心原理与对象池模型无关,而是生产者-消费者模型:

生产者(调用submit()或execute()方法)将任务task放入队列
消费者(worker线程)循环从队列中取出任务处理任务(执行task.run())

钩子方法

回到runWorker()方法,在执行任务的过程中,线程池保留了一些钩子方法,如beforeExecute()、afterExecute()。用户可以在实现自己的线程池时,可以通过覆写钩子方法为线程池添加功能。

但猴子不认为钩子方法是一种好的设计。因为钩子方法大多依赖于源码实现,那么除非了解源码或API声明绝对的严谨正确,否则很难正确使用钩子方法。等发生错误时再去了解实现,可能就太晚了。说到底,还是不要使用类似extends这种表达“扩展”语义的语法来实现继承,详见Java中如何恰当的表达“继承”与“扩展”的语义?。

当然,钩子方法也是极其方便的。权衡看待。

总结

相对于线程封闭,串行线程封闭离用户的距离更近一些,简单灵活,实用性强,很容易掌握。而线程封闭更多沦为单纯的设计策略,单纯使用线程封闭的场景不多。

线程池与串行线程封闭、对象池的关系不大,但经常被混为一谈;没看过源码的很难想到其实现方案,面试时也能立分高下。

线程池的实现很有意思。在追源码之前,猴子一直以为线程池就是把线程存起来,用的时候取出来执行任务;看了源码才知道实现如此之妙,简洁优雅效率高。 查看全部
今天讲一个牛逼而实用的概念,串行线程封闭。对象池是串行线程封闭的典型应用场景;线程池糅合了对象池技术,但核心实现不依赖于对象池,很容易产生误会。

本文从串行线程封闭和对象池入手,最后通过源码分析线程池的核心原理,厘清对象池与线程池之间的误会。

线程封闭与串行线程封闭

线程封闭

线程封闭是一种常见的线程安全设计策略:仅在固定的一个线程内访问对象,不对其他线程共享。

使用线程封闭技术,对象O始终只对一个线程T1可见,“单线程”中自然不存在线程安全的问题。

ThreadLocal是常用的线程安全工具。线程封闭在Servlet及高层的web框架Spring等中应用不少。
https://monkeysayhi.github.io/2016/11/27/源码%7CThreadLocal的实现原理/

串行线程封闭

线程封闭虽然好用,却限制了对象的共享。串行线程封闭改进了这一点:对象O只能由单个线程T1拥有,但可以通过安全的发布对象O来转移O的所有权;在转移所有权后,也只有另一个线程T2能获得这个O的所有权,并且发布O的T1不会再访问O。

所谓“所有权”,指修改对象的权利。

相对于线程封闭,串行线程封闭使得任意时刻,最多仅有一个线程拥有对象的所有权。当然,这不是绝对的,只要线程T1事实不会再修改对象O,那么就相当于仅有T2拥有对象的所有权。串行线层封闭让对象变得可以共享(虽然只能串行的拥有所有权),灵活性得到大大提高;相对的,要共享对象就涉及安全发布的问题,依靠BlockingQueue等同步工具很容易实现这一点。

对象池是串行线程封闭的经典应用场景,如数据库连接池等。

对象池

对象池利用了串行封闭:将对象O“借给”一个请求线程T1,T1使用完再交还给对象池,并保证“未擅自发布该对象”且“以后不再使用”;对象池收回O后,等T2来借的时候再把它借给T2,完成对象所有权的传递。

猴子撸了一个简化版的线程池,用户只需要覆写newObject()方法:

public abstract class AbstractObjectPool<T> {
  protected final int min;
  protected final int max;
  protected final List<T> usings = new LinkedList<>();
  protected final List<T> buffer = new LinkedList<>();
  private volatile boolean inited = false;
  public AbstractObjectPool(int min, int max) {
    this.min = min;
    this.max = max;
    if (this.min < 0 || this.min > this.max) {
      throw new IllegalArgumentException(String.format(
          "need 0 <= min <= max <= Integer.MAX_VALUE, given min: %s, max: %s", this.min, this.max));
    }
  }
  public void init() {
    for (int i = 0; i < min; i++) {
      buffer.add(newObject());
    }
    inited = true;
  }
  protected void checkInited() {
    if (!inited) {
      throw new IllegalStateException("not inited");
    }
  }
  abstract protected T newObject();
  public synchronized T getObject() {
    checkInited();
    if (usings.size() == max) {
      return null;
    }
    if (buffer.size() == 0) {
      T newObj = newObject();
      usings.add(newObj);
      return newObj;
    }
    T oldObj = buffer.remove(0);
    usings.add(oldObj);
    return oldObj;
  }
  public synchronized void freeObject(T obj) {
    checkInited();
    if (!usings.contains(obj)) {
      throw new IllegalArgumentException(String.format("obj not in using queue: %s", obj));
    }
    usings.remove(usings.indexOf(obj));
    buffer.add(obj);
  }
}

AbstractObjectPool具有以下特性:
支持设置最小、最大容量
对象一旦申请就不再释放,避免了GC

虽然很简单,但大可以用于一些时间敏感、资源充裕的场景。如果时间进一步敏感,可将getObject()、freeObject()改写为并发程度更高的版本,但记得保证安全发布安全回收;如果资源不那么充裕,可以适当增加对象回收策略。

可以看到,一个对象池的基本行为包括:
创建对象newObject()
借取对象getObject()
归还对象freeObject()

典型的对象池有各种连接池、常量池等,应用非常多,模型也大同小异,不做解析。令人迷惑的是线程池,很容易让人误以为线程池的核心原理也是对象池,下面来追一遍源码。

线程池

首先摆出结论:线程池糅合了对象池模型,但核心原理是生产者-消费者模型。

继承结构如下:

用户可以将Runnable(或Callables)实例提交给线程池,线程池会异步执行该任务,返回响应的结果(完成/返回值)。

猴子最喜欢的是submit(Callable<T> task)方法。我们从该方法入手,逐步深入函数栈,探究线程池的实现原理。

submit()

submit()方法在ExecutorService接口中定义,AbstractExecutorService实现,ThreadPoolExecutor直接继承。

public abstract class AbstractExecutorService implements ExecutorService {
...
    public <T> Future<T> submit(Callable<T> task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<T> ftask = newTaskFor(task);
        execute(ftask);
        return ftask;
    }
...


AbstractExecutorService#newTaskFor()创建一个RunnableFuture类型的FutureTask。

核心是execute()方法。

execute()

execute()方法在Executor接口中定义,ThreadPoolExecutor实现。

public class ThreadPoolExecutor extends AbstractExecutorService {
...
    public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        else if (!addWorker(command, false))
            reject(command);
    }
...
}

我们暂且忽略线程池的池化策略。关注一个最简单的场景,看能不能先回答一个问题:线程池中的任务如何执行?

核心是addWorker()方法。以8行的参数为例,此时,线程池中的线程数未达到最小线程池大小corePoolSize,通常可以直接在9行返回。

addWorker()

简化如下:

public class ThreadPoolExecutor extends AbstractExecutorService {
...
    private boolean addWorker(Runnable firstTask, boolean core) {
        boolean workerStarted = false;
        boolean workerAdded = false;
        Worker w = null;
        try {
            w = new Worker(firstTask);
            final Thread t = w.thread;
            if (t != null) {
                final ReentrantLock mainLock = this.mainLock;
                mainLock.lock();
                try {
                    int rs = runStateOf(ctl.get());
                    if (rs < SHUTDOWN) {
                        workers.add(w);
                        workerAdded = true;
                    }
                } finally {
                    mainLock.unlock();
                }
                if (workerAdded) {
                    t.start();
                    workerStarted = true;
                }
            }
        } finally {
            if (! workerStarted)
                addWorkerFailed(w);
        }
        return workerStarted;
    }
...
}

我去掉了很多用于管理线程池、维护线程安全的代码。假设线程池未关闭,worker(即w,下同)添加成功,则必然能够将worker添加至workers中。workers是一个HashSet:

private final HashSet<Worker> workers = new HashSet<Worker>();

哪里是对象池?

如果说与对象池有关,那么workers即相当于示例代码中的using,应用了对象池模型;只不过这里的using是一直增长的,直到达到最大线程池大小maximumPoolSize。

但是很明显,线程池并没有将线程发布出去,workers也仅仅完成using“保存线程”的功能。那么,线程池中的任务如何执行呢?跟线程池有没有关系?

哪里又不是?

注意9、17、24行:

9行将我们提交到线程池的firstTask封装入一个worker。
17行将worker加入workers,维护起来
24行则启动了worker中的线程t

核心在与这三行,但线程池并没有直接在addWorker()中启动任务firstTask,代之以启动一个worker。最终任务必然被启动,那么我们继续看Worker如何启动这个任务。

Worker

Worker实现了Runnable接口:

private final class Worker
    extends AbstractQueuedSynchronizer
    implements Runnable
{
...
    Worker(Runnable firstTask) {
        setState(-1); // inhibit interrupts until runWorker
        this.firstTask = firstTask;
        this.thread = getThreadFactory().newThread(this);
    }
    /** Delegates main run loop to outer runWorker  */
    public void run() {
        runWorker(this);
    }
...
}

为什么要将构造Worker时的参数命名为firstTask?因为当且仅当需要建立新的Worker以执行任务task时,才会调用构造函数。因此,任务task对于新Worker而言,是第一个任务firstTask。

Worker的实现非常简单:将自己作为Runable实例,构造时在内部创建并持有一个线程thread。Thread和Runable的使用大家很熟悉了,核心是Worker的run方法,它直接调用了runWorker()方法。

runWorker()

敲黑板!!!

重头戏来了。简化如下:

public class ThreadPoolExecutor extends AbstractExecutorService {
...
    final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        Runnable task = w.firstTask;
        w.firstTask = null;
        w.unlock(); // allow interrupts
        boolean completedAbruptly = true;
        try {
            while (task != null || (task = getTask()) != null) {
                w.lock();
                try {
                    beforeExecute(wt, task);
                    Throwable thrown = null;
                    try {
                        task.run();
                    } catch (RuntimeException x) {
                        thrown = x; throw x;
                    } catch (Error x) {
                        thrown = x; throw x;
                    } catch (Throwable x) {
                        thrown = x; throw new Error(x);
                    } finally {
                        afterExecute(task, thrown);
                    }
                } finally {
                    task = null;
                    w.completedTasks++;
                    w.unlock();
                }
            }
            completedAbruptly = false;
        } finally {
            processWorkerExit(w, completedAbruptly);
        }
    }
...
}

我们在前面将要执行的任务赋值给firstTask,5-6行首先取出任务task,并将firstTask置为null。因为接下来要执行task,firstTask字段就没有用了。

重点是10-31行的while循环。下面分情况讨论。

case1:第一次进入循环,task不为null

case1对应前面作出的诸多假设。

第一次进入循环时,task==firstTask,不为null,使10行布尔短路直接进入循环;从而16行执行的是firstTask的run()方法;异常处理不表;最后,finally代码块中,task会被置为null,导致下一轮循环会进入case2。

case2:非第一次进入循环,task为null

case2是更普遍的情况,也就是线程池的核心。

case1中,task被置为了null,使10行布尔表达式执行第二部分(task = getTask()) != null(getTask()稍后再讲,它返回一个用户已提交的任务)。假设task得到了一个已提交的任务,从而16行执行的是新获得的任务task的run()方法。后同case1,最后task仍然会被置为null,以后循环都将进入case2。

GETTASK()

任务从哪来呢?简化如下:

public class ThreadPoolExecutor extends AbstractExecutorService {
...
    private Runnable getTask() {
        boolean timedOut = false;
        for (;;) {
            try {
                Runnable r = timed ?
                    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                    workQueue.take();
                if (r != null)
                    return r;
                timedOut = true;
            } catch (InterruptedException retry) {
                timedOut = false;
            }
        }
    }
...
}

我们先看最简单的,19-28行。

首先,workQueue是一个线程安全的BlockingQueue,大部分时候使用的实现类是LinkedBlockingQueue:

private final BlockingQueue<Runnable> workQueue;

假设timed为false,则调用阻塞的take()方法,返回的r一定不是null,从而12行退出,将任务交给了某个worker线程。

一个小细节有点意思:前面每个worker线程runWorker()方法时,在循环中加锁粒度在worker级别,直接使用的lock同步;但因为每一个woker都会调用getTask(),考虑到性能因素,源码中getTask()中使用乐观的CAS+SPIN实现无锁同步。

关于乐观锁和CAS,可以参考https://monkeysayhi.github.io/2017/10/22/源码%7C并发一枝花之ConcurrentLinkedQueue【伪】/

workQueue中的元素从哪来呢?这就要回顾execute()方法了。

EXECUTE()

public class ThreadPoolExecutor extends AbstractExecutorService {
...
    public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        else if (!addWorker(command, false))
            reject(command);
    }
...
}

前面以8行的参数为例,此时,线程池中的线程数未达到最小线程池大小corePoolSize,通常可以直接在9行返回。进入8行的条件是“当前worker数小于最小线程池大小corePoolSize”。

如果不满足,会继续执行到12行。isRunning(c)判断线程池是否未关闭,我们关注未关闭的情况;则会继续执行布尔表达式的第二部分workQueue.offer(command),尝试将任务command放入队列workQueue。

workQueue.offer()的行为取决于线程池持有的BlockingQueue实例。

Executors.newFixedThreadPool()、Executors.newSingleThreadExecutor()创建的线程池使用LinkedBlockingQueue,而Executors.newCachedThreadPool()创建的线程池则使用SynchronousQueue。

以LinkedBlockingQueue为例,创建时不配置容量,即创建为无界队列,则LinkedBlockingQueue#offer()永远返回true,从而进入12-18行。

更细节的内容不必关心了,当workQueue.offer()返回true时,已经将任务command放入了队列workQueue。当未来的某个时刻,某worker执行完某一个任务之后,会从workQueue中再取出一个任务继续执行,直到线程池关闭,直到海枯石烂。

CachedThreadPool是一种无界线程池,使用SynchronousQueue能进一步提升性能,简化代码结构。留给读者分析。

CASE2小结

可以看到,实际上,线程池的核心原理与对象池模型无关,而是生产者-消费者模型:

生产者(调用submit()或execute()方法)将任务task放入队列
消费者(worker线程)循环从队列中取出任务处理任务(执行task.run())

钩子方法

回到runWorker()方法,在执行任务的过程中,线程池保留了一些钩子方法,如beforeExecute()、afterExecute()。用户可以在实现自己的线程池时,可以通过覆写钩子方法为线程池添加功能。

但猴子不认为钩子方法是一种好的设计。因为钩子方法大多依赖于源码实现,那么除非了解源码或API声明绝对的严谨正确,否则很难正确使用钩子方法。等发生错误时再去了解实现,可能就太晚了。说到底,还是不要使用类似extends这种表达“扩展”语义的语法来实现继承,详见Java中如何恰当的表达“继承”与“扩展”的语义?。

当然,钩子方法也是极其方便的。权衡看待。

总结

相对于线程封闭,串行线程封闭离用户的距离更近一些,简单灵活,实用性强,很容易掌握。而线程封闭更多沦为单纯的设计策略,单纯使用线程封闭的场景不多。

线程池与串行线程封闭、对象池的关系不大,但经常被混为一谈;没看过源码的很难想到其实现方案,面试时也能立分高下。

线程池的实现很有意思。在追源码之前,猴子一直以为线程池就是把线程存起来,用的时候取出来执行任务;看了源码才知道实现如此之妙,简洁优雅效率高。
799 浏览

我与数据打交道(三)

机械自动化类 机械设计 2016-12-05 14:18 发表了文章 来自相关话题

 前言:本期文章是《我与数据打交道》系列的第三篇,也是最后一篇。后面几期文章的重心是神经网络,敬请关注。


小时候,我会经常听见大人们说:小孩怎么这么爱挑食?却很少有听说大人们也爱挑食,大概是因为大人们买的都是他们愿意吃的东西吧。


“己所不欲,勿施于人;己所之欲,慎施于人”。这句话的意思大概是,不要把自己主观上的想法强推给他人,反过来说,就是“己所之欲,亦施于人”,某些无良网站的某些无良广告大概就是奉行这条准则的吧。


只要有互动的地方,就会有推荐存在的合理性。现在越来越多的网站都在提供智能的推荐服务,例如宣称最关心用户的某新闻类APP就使用了智能推荐对读者推送其关心的头条新闻;某电商平台上浏览了一个物品以后,下方会出现你可能还喜欢的物品栏;在搜索框里输入“微信”两个字时,下方已经出现了一系列推荐词汇,如“微信网页版”,“微信公众号”等等。推荐似乎已经融入了互联网的各个角落,尽管目前来看,推荐的效果仍然是良莠不齐。不过,目光放远一点,搜索引擎的未来也许就是更加智能的推荐系统吧。


那什么是推荐系统呢?推荐是数据的产物,没有数据就没有推荐。推荐系统的工作原理通常分为两大类,内容过滤和协同过滤。下面以电商网站购物为例,谈谈二者的区别。


例如顾客A购买了商品1,如果商品2与商品1相似度非常高,那么推荐系统自然会想到把商品2推荐给顾客A。这就是基于内容过滤的推荐方法,重点关注的是商品本身,而非顾客。


再例如顾客A购买了商品1,顾客B购买了商品2,如果顾客A和顾客B在很多属性上相似度非常高,那么推荐系统自然会想到把商品2推荐给顾客A。这就是基于协同过滤的推荐方法,重点关注的是顾客本身,而非商品。


为了使得推荐系统更加智能化,通常会结合内容过滤和协同过滤一起使用。总之,就是说要想有一个优秀的推荐系统,需要同时考虑内容过滤和协同过滤。前几天有读者希望我推荐一些数据挖掘方面的资源,我考虑了一下,第一我可能不知道你以前偏爱哪种风格的书籍,第二我也不清楚你是否和我有一样的口味,于是我接下来的推荐所导致的资源浏览次数以及读者口碑满意度可能并不太理想了,不过还是尝试一下吧。


《Think Bayes》,这本书对贝叶斯理论讲得很详细,并且随书插入了相关的Python代码,适合一边学习,一边实践。


《Data Science From Scratch First Principles With Python》,这本书写得很入门,包含了数理统计知识、Python高级数据结构的使用、数据可视化,很实用的一本书。

《Machine learning A Probabilistic Perspective》,这本书无需介绍,数据挖掘的经典书籍。

《CS231n Convolutional Neural Network for Visual Recognition》,这是斯坦福大学的一门关于卷积神经网络在视觉识别中的应用,虽然标题强调了CNN,但是课程内容将近一半是讲了深度学习的基础原理与实践技巧,很实用。


以上是我看过的觉得挺有帮助的书籍,至于其他书籍,由于我还没认真看过,所以这里也不敢冒昧列出。


最后分享摘自著名作家余秋雨的书籍《千年一叹》中的金句:

• 干净的痛苦一定会沉淀,沉淀成悠闲,悠闲是痛苦的补偿,痛苦是悠闲的衬垫。


 
来源: 张泽旺 深度学习每日摘要
智造家 查看全部

10.1_.jpg

 前言:本期文章是《我与数据打交道》系列的第三篇,也是最后一篇。后面几期文章的重心是神经网络,敬请关注。


小时候,我会经常听见大人们说:小孩怎么这么爱挑食?却很少有听说大人们也爱挑食,大概是因为大人们买的都是他们愿意吃的东西吧。


“己所不欲,勿施于人;己所之欲,慎施于人”。这句话的意思大概是,不要把自己主观上的想法强推给他人,反过来说,就是“己所之欲,亦施于人”,某些无良网站的某些无良广告大概就是奉行这条准则的吧。


只要有互动的地方,就会有推荐存在的合理性。现在越来越多的网站都在提供智能的推荐服务,例如宣称最关心用户的某新闻类APP就使用了智能推荐对读者推送其关心的头条新闻;某电商平台上浏览了一个物品以后,下方会出现你可能还喜欢的物品栏;在搜索框里输入“微信”两个字时,下方已经出现了一系列推荐词汇,如“微信网页版”,“微信公众号”等等。推荐似乎已经融入了互联网的各个角落,尽管目前来看,推荐的效果仍然是良莠不齐。不过,目光放远一点,搜索引擎的未来也许就是更加智能的推荐系统吧。


那什么是推荐系统呢?推荐是数据的产物,没有数据就没有推荐。推荐系统的工作原理通常分为两大类,内容过滤和协同过滤。下面以电商网站购物为例,谈谈二者的区别。


例如顾客A购买了商品1,如果商品2与商品1相似度非常高,那么推荐系统自然会想到把商品2推荐给顾客A。这就是基于内容过滤的推荐方法,重点关注的是商品本身,而非顾客。


再例如顾客A购买了商品1,顾客B购买了商品2,如果顾客A和顾客B在很多属性上相似度非常高,那么推荐系统自然会想到把商品2推荐给顾客A。这就是基于协同过滤的推荐方法,重点关注的是顾客本身,而非商品。


为了使得推荐系统更加智能化,通常会结合内容过滤和协同过滤一起使用。总之,就是说要想有一个优秀的推荐系统,需要同时考虑内容过滤和协同过滤。前几天有读者希望我推荐一些数据挖掘方面的资源,我考虑了一下,第一我可能不知道你以前偏爱哪种风格的书籍,第二我也不清楚你是否和我有一样的口味,于是我接下来的推荐所导致的资源浏览次数以及读者口碑满意度可能并不太理想了,不过还是尝试一下吧。


《Think Bayes》,这本书对贝叶斯理论讲得很详细,并且随书插入了相关的Python代码,适合一边学习,一边实践。


《Data Science From Scratch First Principles With Python》,这本书写得很入门,包含了数理统计知识、Python高级数据结构的使用、数据可视化,很实用的一本书。

《Machine learning A Probabilistic Perspective》,这本书无需介绍,数据挖掘的经典书籍。

《CS231n Convolutional Neural Network for Visual Recognition》,这是斯坦福大学的一门关于卷积神经网络在视觉识别中的应用,虽然标题强调了CNN,但是课程内容将近一半是讲了深度学习的基础原理与实践技巧,很实用。


以上是我看过的觉得挺有帮助的书籍,至于其他书籍,由于我还没认真看过,所以这里也不敢冒昧列出。


最后分享摘自著名作家余秋雨的书籍《千年一叹》中的金句:

• 干净的痛苦一定会沉淀,沉淀成悠闲,悠闲是痛苦的补偿,痛苦是悠闲的衬垫。


 
来源: 张泽旺 深度学习每日摘要
智造家
805 浏览

我与数据打交道(二)

机械自动化类 机械设计 2016-12-05 14:11 发表了文章 来自相关话题

 前言:前几天有读者来信反应要我推荐一些深度学习的教材或资源,我想留到下一期再推荐给大家。今天文章的主要内容还是来聊聊我与数据打交道的感悟。



提到数据这个词,我们脑海中的第一反应会是什么呢?也许有人会想到:“我今年已经多少岁了”,“我的生日是几月几号”,“我朋友圈中有多少个好友了”,“今天我在朋友圈收到了多少个点赞或评论”,“我的公众号粉丝昨天涨了多少”,“天呐,这个月我竟然消费了多少人民币”,“只剩多少天我就可以熬完期末考试周了”等等。
 
我想每个人都存储着自己独有的海量数据,每个人都有对自己的数据不同的处理方式,也许,每个人都承担着自己王国里的数据科学家的角色。



或许大家都会感到惊叹,我从来不处理数据,怎么可能成为了数据科学家。殊不知,如果我们把万事万物量化一番,我们每个人时时刻刻都在与数据打交道。
 
例如,出租车司机可以根据历史见闻和当下热点来预判什么时间段什么地点有可能拉到更多的乘客,因为出租车司机心中储存着以往拉客的时间地点等数据信息;资深医生可以根据自己以往的诊断经验来辅助判断如何对当下的病人进行对症下药,因为他们可以根据病人的生理特征以及病理反应去大脑中检索自己以往遇到的类似病人的数据;经验丰富的老师可以根据一个学生的平时考试成绩来对他高考成绩进行大概的估计,因为他们可以根据往届无数学生的平时表现以及高考得分来做一个合理的预测。
 
阅历是一种财富,如果把这些抽象的阅历量化成具体的数据,那么数据无疑是当今最重要的财富,至少数据存在机器里要比存在人脑里可靠,因为人可能会犯糊涂,而机器不会。



有人说,数据科学家是21世纪最性感的职业,其实,在我看来,数据科学家从古至今都是很性感的职业,只不过在21世纪变得接地气起来。随着各种数据挖掘的框架、工具包、软件的发布,数据科学的门槛已经变得越来越低;另一方面,随着大数据时代的到来,数据科学的重要性也已经日益彰显。那什么是数据科学呢?我想谈谈我的理解。



我觉得从事数据科学的应该是一类比搞纯编程的人更懂统计学,比搞纯统计的人更懂计算机科学的人。数据科学是一门综合性学科,囊括了计算机科学、数理统计、专业知识三大板块。
 
 
现在大多数数据挖掘教材都只会谈论前两点——计算机科学和数理统计,因为这两点放在任何问题里面都是最基础的通用工具,而对于第三点——专业知识是几千页纸都讲不完的,不同的领域会遇到不同的问题,具体问题具体分析,活学活用吧。



继续数据分析软件的话题。在网上有一句段子叫做“人生苦短,我用Python”,在没有接触Python之前,我也觉得这句段子估计也只是个段子而已。后来,上手了Python以后越来越觉得它极其简化了我的工作,正如Unix系统的一句哲学“Keep it simple,stupid”,Python语言具有上手快并且可读性很好的优点,而且它是一个完完全全面向对象的语言,在很大程度上能够提高项目的操作效率。那为什么要使用Python作为我的数据分析语言呢?
 
我觉得原因主要有三点:
 
第一,Python是一种免费的脚本语言,并且Linux系统对其支持十分友好,而我通常是在Linux环境下工作;
 
第二,Python具有一套完善的科学计算核心库,如Numpy、matplotlib、pandas等,除此之外,还有很强大的机器学习库Theano、Tensorflow也都是基于Python语言,这些完善的框架使得借助Python进行数据分析具有先天的优势;
 
第三,Python的用户群也是十分庞大的,各种基于Python的新框架层出不穷,而且各大互联网公司均有在使用Python进行数据挖掘,因此遇到什么问题一般都可以在网上得到正确的指导。
 
 
 
 
 
 
来源: 张泽旺 深度学习每日摘要
智造家 查看全部

9.1_.jpg

 前言:前几天有读者来信反应要我推荐一些深度学习的教材或资源,我想留到下一期再推荐给大家。今天文章的主要内容还是来聊聊我与数据打交道的感悟。



提到数据这个词,我们脑海中的第一反应会是什么呢?也许有人会想到:“我今年已经多少岁了”,“我的生日是几月几号”,“我朋友圈中有多少个好友了”,“今天我在朋友圈收到了多少个点赞或评论”,“我的公众号粉丝昨天涨了多少”,“天呐,这个月我竟然消费了多少人民币”,“只剩多少天我就可以熬完期末考试周了”等等。
 
我想每个人都存储着自己独有的海量数据,每个人都有对自己的数据不同的处理方式,也许,每个人都承担着自己王国里的数据科学家的角色。



或许大家都会感到惊叹,我从来不处理数据,怎么可能成为了数据科学家。殊不知,如果我们把万事万物量化一番,我们每个人时时刻刻都在与数据打交道。
 
例如,出租车司机可以根据历史见闻和当下热点来预判什么时间段什么地点有可能拉到更多的乘客,因为出租车司机心中储存着以往拉客的时间地点等数据信息;资深医生可以根据自己以往的诊断经验来辅助判断如何对当下的病人进行对症下药,因为他们可以根据病人的生理特征以及病理反应去大脑中检索自己以往遇到的类似病人的数据;经验丰富的老师可以根据一个学生的平时考试成绩来对他高考成绩进行大概的估计,因为他们可以根据往届无数学生的平时表现以及高考得分来做一个合理的预测。
 
阅历是一种财富,如果把这些抽象的阅历量化成具体的数据,那么数据无疑是当今最重要的财富,至少数据存在机器里要比存在人脑里可靠,因为人可能会犯糊涂,而机器不会。



有人说,数据科学家是21世纪最性感的职业,其实,在我看来,数据科学家从古至今都是很性感的职业,只不过在21世纪变得接地气起来。随着各种数据挖掘的框架、工具包、软件的发布,数据科学的门槛已经变得越来越低;另一方面,随着大数据时代的到来,数据科学的重要性也已经日益彰显。那什么是数据科学呢?我想谈谈我的理解。



我觉得从事数据科学的应该是一类比搞纯编程的人更懂统计学,比搞纯统计的人更懂计算机科学的人。数据科学是一门综合性学科,囊括了计算机科学、数理统计、专业知识三大板块。
 
 
现在大多数数据挖掘教材都只会谈论前两点——计算机科学和数理统计,因为这两点放在任何问题里面都是最基础的通用工具,而对于第三点——专业知识是几千页纸都讲不完的,不同的领域会遇到不同的问题,具体问题具体分析,活学活用吧。



继续数据分析软件的话题。在网上有一句段子叫做“人生苦短,我用Python”,在没有接触Python之前,我也觉得这句段子估计也只是个段子而已。后来,上手了Python以后越来越觉得它极其简化了我的工作,正如Unix系统的一句哲学“Keep it simple,stupid”,Python语言具有上手快并且可读性很好的优点,而且它是一个完完全全面向对象的语言,在很大程度上能够提高项目的操作效率。那为什么要使用Python作为我的数据分析语言呢?
 
我觉得原因主要有三点:
 
第一,Python是一种免费的脚本语言,并且Linux系统对其支持十分友好,而我通常是在Linux环境下工作;
 
第二,Python具有一套完善的科学计算核心库,如Numpy、matplotlib、pandas等,除此之外,还有很强大的机器学习库Theano、Tensorflow也都是基于Python语言,这些完善的框架使得借助Python进行数据分析具有先天的优势;
 
第三,Python的用户群也是十分庞大的,各种基于Python的新框架层出不穷,而且各大互联网公司均有在使用Python进行数据挖掘,因此遇到什么问题一般都可以在网上得到正确的指导。
 
 
 
 
 
 
来源: 张泽旺 深度学习每日摘要
智造家
780 浏览

我与数据打交道(一)

机械自动化类 机械设计 2016-12-05 14:06 发表了文章 来自相关话题

 




前言:公众号文章纯粹谈技术貌似不太好,一些复杂的公式也无法支持,插入图片看起来也并不美观,因此以后的文章主要以文字的形式谈谈技术以及感悟。本篇主要回顾一下我与数据分析有过的峥嵘岁月。那些年,为了赶工手抄版或电子版的实验报告插图、为了拿高分不得不让数据看起来更美观所用过的一些数据分析软件的心得体会。



    最近大家都在谈论大数据、信息爆炸等词语,的确,现在正处于信息蓬勃式发展、网络快餐式消费的时代,每天我们每个人为互联网所贡献的比特数至少数亿以上,面对庞大的数据资源,如何充分发掘它们的价值是当下机器学习的热门话题。作为一名普通的在校理工男,太大的数据量我是很难遇到的,我今天只想谈谈那些年,我与小数据打交道的过往点滴。



    回忆起刚刚几年前的时候,我拿到一份粗糙的实验数据,一脸茫然的样子,根本无从下手。还好,那个时候我的第一直觉就是使用Excel来帮助分析数据,当时使用Excel分析数据的好处就是简单粗暴,只要获得了想要的数据,无论是txt格式或者是csv格式,只需要一个按钮即可完美导入所有数据,然后下拉全选,只要熟悉了Excel的一些基本函数操作,对某行某列的数据分析显得如鱼得水了,算术运算、平均数、标准差、最值等等这些都是家常便饭了。
 
 
     当然,如果实验报告中只有光秃秃的几行公式以及统计数据,是不足以吸引老师的眼球的,为了博取高分,数据可视化是数据分析必不可少的亮点。谈起Excel的数据可视化,也就那么几种吧:条形图、柱状图、折线图、甚至三维图,这些图的做法都不难,勾勾就行了。不过,Excel作图有很多限制,一个图的很多属性是难以修改的,因此这种图往实验报告上一放就显得比较平庸甚至会被一些机智的老师嘲笑的。




    这个时候一个数据分析软件就派上用场了,那就是origin科学作图软件,origin与Excel最大的不同点就是它的功能十分庞大,不仅包括了各种统计学工具,而且使用它作出的图形也很美观,而且很多科研论文的图形甚至都是用origin作出来的,足以看出它的地位了。不过,我不太喜欢origin,虽然它功能强大,但是它始终是基于UI界面的,也就是说几乎所有的功能都是通过按钮来实现的,这种操作的优点是很具有人性化,基本上只要你眼睛睁大一点就不会错过某一个强大的功能;但是,从我一个程序员的角度来看,通过按钮操作的不具有可重复操作性,也就是每次作类似图我都要这样点来点去,效率不是很高。
 
 
    人工智能的目的不是消灭人类,而是把人类从纷繁复杂的重复劳动中解放出来,其实编程语言也可以看做一种人工智能,只要第一次写出了分析数据或科学作图的可运行的代码,往后很多重复的工作就可以避免了。



    放弃了学习origin,我便使用了MATLAB作为我的实验报告的主要利器。谈起MATLAB,可能很多人以为它是一个主打仿真的软件,其实不然,MATLAB英文全名是Matrix Laboratory,即矩阵实验室。所谓矩阵实验室,即它是一种线性代数的工具,那自然包括数据处理在内了,并且MATLAB可以作很多类型的图,既美观又富含信息量,一张图如果能做到美观又能传递非常多的信息就已经很成功了,的确,MATLAB可以做到这一点。并且,MATLAB与origin的最大不同之处是它是一种基于命令行的软件,它使用了自带了类似于C语言中一个MATLAB编程语言,任意操作都可以使用代码去表示,无论是数据分析还是科学作图,这样就使得代码的复用性很高,对于类似的操作不用重重复复去点来点去了。而且,MATLAB的用户群非常庞大,所以其社区交流互动非常活跃,遇到什么问题一般只要Google或Baidu一下就可以解决了。



    那么,是不是可以认为MATLAB是最好的数据分析软件呢?
 
 
 
 
 
来源:张泽旺 深度学习每日摘要
智造家 查看全部
 
8.1_.jpg


前言:公众号文章纯粹谈技术貌似不太好,一些复杂的公式也无法支持,插入图片看起来也并不美观,因此以后的文章主要以文字的形式谈谈技术以及感悟。本篇主要回顾一下我与数据分析有过的峥嵘岁月。那些年,为了赶工手抄版或电子版的实验报告插图、为了拿高分不得不让数据看起来更美观所用过的一些数据分析软件的心得体会。



    最近大家都在谈论大数据、信息爆炸等词语,的确,现在正处于信息蓬勃式发展、网络快餐式消费的时代,每天我们每个人为互联网所贡献的比特数至少数亿以上,面对庞大的数据资源,如何充分发掘它们的价值是当下机器学习的热门话题。作为一名普通的在校理工男,太大的数据量我是很难遇到的,我今天只想谈谈那些年,我与小数据打交道的过往点滴。



    回忆起刚刚几年前的时候,我拿到一份粗糙的实验数据,一脸茫然的样子,根本无从下手。还好,那个时候我的第一直觉就是使用Excel来帮助分析数据,当时使用Excel分析数据的好处就是简单粗暴,只要获得了想要的数据,无论是txt格式或者是csv格式,只需要一个按钮即可完美导入所有数据,然后下拉全选,只要熟悉了Excel的一些基本函数操作,对某行某列的数据分析显得如鱼得水了,算术运算、平均数、标准差、最值等等这些都是家常便饭了。
 
 
     当然,如果实验报告中只有光秃秃的几行公式以及统计数据,是不足以吸引老师的眼球的,为了博取高分,数据可视化是数据分析必不可少的亮点。谈起Excel的数据可视化,也就那么几种吧:条形图、柱状图、折线图、甚至三维图,这些图的做法都不难,勾勾就行了。不过,Excel作图有很多限制,一个图的很多属性是难以修改的,因此这种图往实验报告上一放就显得比较平庸甚至会被一些机智的老师嘲笑的。




    这个时候一个数据分析软件就派上用场了,那就是origin科学作图软件,origin与Excel最大的不同点就是它的功能十分庞大,不仅包括了各种统计学工具,而且使用它作出的图形也很美观,而且很多科研论文的图形甚至都是用origin作出来的,足以看出它的地位了。不过,我不太喜欢origin,虽然它功能强大,但是它始终是基于UI界面的,也就是说几乎所有的功能都是通过按钮来实现的,这种操作的优点是很具有人性化,基本上只要你眼睛睁大一点就不会错过某一个强大的功能;但是,从我一个程序员的角度来看,通过按钮操作的不具有可重复操作性,也就是每次作类似图我都要这样点来点去,效率不是很高。
 
 
    人工智能的目的不是消灭人类,而是把人类从纷繁复杂的重复劳动中解放出来,其实编程语言也可以看做一种人工智能,只要第一次写出了分析数据或科学作图的可运行的代码,往后很多重复的工作就可以避免了。



    放弃了学习origin,我便使用了MATLAB作为我的实验报告的主要利器。谈起MATLAB,可能很多人以为它是一个主打仿真的软件,其实不然,MATLAB英文全名是Matrix Laboratory,即矩阵实验室。所谓矩阵实验室,即它是一种线性代数的工具,那自然包括数据处理在内了,并且MATLAB可以作很多类型的图,既美观又富含信息量,一张图如果能做到美观又能传递非常多的信息就已经很成功了,的确,MATLAB可以做到这一点。并且,MATLAB与origin的最大不同之处是它是一种基于命令行的软件,它使用了自带了类似于C语言中一个MATLAB编程语言,任意操作都可以使用代码去表示,无论是数据分析还是科学作图,这样就使得代码的复用性很高,对于类似的操作不用重重复复去点来点去了。而且,MATLAB的用户群非常庞大,所以其社区交流互动非常活跃,遇到什么问题一般只要Google或Baidu一下就可以解决了。



    那么,是不是可以认为MATLAB是最好的数据分析软件呢?
 
 
 
 
 
来源:张泽旺 深度学习每日摘要
智造家