并发艺术(一)一文带你读懂JAVA多线程的来龙去脉

原创技术文章,版权归作者所有,若转载请标明出处
公众号,待定,原公众号长期试灰已被冻结

进程和线程

我们经常说到进程和线程,那么到底两者有什么区别呢?

所谓进程,就是操作系统进行资源分配和调用的最小单位。比如我们做什么事情,什么活动,为了这个事情,我们需要哪些资源,这整个可以理解为一个进程。而线程则是CPU调度的最小单位,它依赖进程而存在,它就相当于我们拿什么资源做什么事情的控制流。

在操作系统中,进程的执行需要加载上下文、执行任务、保存上下文,为了提升效率,操作系统引入了线程,进程内的多个线程可以共享进程资源,而我们通常所说的多线程是指用户线程,需区分于操作系统内核线程

CPU是如何执行的

目前我们使用的如Windows、Linux、MacOs等,都是分时操作系统,就是多任务多用户的。

而对于单核CPU来说,同一时间只能做一件事,为了能达到多用户多任务的目标,则使用了时间片,就是同一时间段,执行一项任务。比如,宿舍里面有四个哥们,但是只有一台电脑,大家都要查资料上网,那么每人只能使用五分钟,时间到了就让下一个人使用,自己去后面继续排队。

而至于说,是有序,还是优先,有不同的调度算法,如RR(时间片轮转)FCFS(先到先服务)SPN(最短作业优先)SRT(最短剩余时间优先)HRRN(高响应比优先)。下面举例RR算法,其他算法后续专栏会深入探讨。


RR调度,时间片轮转法(Round-Robin),主要用于分时操作系统。

前面提到,CPU调度的基本单位是线程,而时间片轮转,则是划分多个时间片,操作系统会保存一个就绪进程的FIFO队列,当CPU处于空闲时,从头部拿出一个就绪的进程,将CPU分派给该进程,此进程则享有CPU的执行权,在时间片内执行该进程的任务。

时间片通常是10~100ms左右,当时间片执行完成,则中断当前进程,会将该进程放入就绪队列尾部,调度器再将CPU分派给队列头部进程。

如果在时间片内执行完成,则分派下个进程。

JAVA线程调度

上面提到,线程调度,其实是操作系统分配CPU使用权的过程,而主要的调度方式有两种,一种是协作式线程调度,一种是抢占式线程调度

协作式线程可以控制线程的执行时间,执行完成后,可以告诉操作系统下一个递交的线程,就是可以从一个线程主动切换到另一个线程,但是如果执行的线程出现异常,一直占用CPU资源,则会导致系统阻塞。

抢占式线程的执行,则依赖我们上述的时间片机制,即使出现异常也不会阻塞系统
复制代码

java中是使用抢占式线程调度,如果想让线程执行的时间长点,可以设置线程等级,但是并不靠谱。而协作式线程在LUA中协同例程中有所体现,java中也有Quasar可以支持,如果习惯于异步编程,并且对性能要求极高,可以考虑一试。

JAVA线程状态

直接上图(纯手工制作)

上图表述了线程的各个状态及状态间切换的操作,主要的五种状态如下,

  1. 新建(New),此时线程已创建,但是未启动
  2. 运行(Runable),此状态包括Running和Ready,线程启动后,是Ready状态,前面讲到的CPU分派,当线程拥有CPU执行权后,则变为Running状态,若运行的线程yield后,则让出执行权,重新变为就绪状态
  3. 等待(Waiting),分无限期等待和有限期等待,wait()、join()、park(),需要被其他线程显示唤醒,如果未被唤醒则会一直等待,直到进程终止
  4. 阻塞(Blocked),严格来讲,阻塞和等待有一些区别,等待会造成当前线程阻塞,如果是同步调用,需要被唤醒,才可继续执行。而阻塞也包含其他阻塞,比如在获取一个排他锁时,如果其他线程已经获取资源锁,则当前线程被阻塞,等他释放锁
  5. 结束(Terminalted),线程已经结束

JAVA中线程使用

JAVA中使用线程主要有三种方式

Thread

# 继承Thread
static class ThreadTest extends Thread{
    
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + ", Thread example.....");
    }
}
复制代码

Runnable

# 实现Runnable接口,实现run()
static class RunnableTest implements Runnable {

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + ", Runnable example.....");
    }
}
复制代码

Callable 如果我们需要返回值,则使用Callable

# 实现Callable接口,实现call(),并返回相应的值
static class CallableTest implements Callable<String> {

    @Override
    public String call() throws Exception {
        System.out.println(Thread.currentThread().getName() + ", Callable example.....");
        return "call result";
    }
}
复制代码

测试

public static void main(String[] args) throws ExecutionException, InterruptedException {
    ThreadTest threadTest = new ThreadTest();
    threadTest.start();

    Thread runnableTest = new Thread(new RunnableTest());
    runnableTest.start();

    FutureTask<String> futureTask = new FutureTask<String>(new CallableTest());
    Thread callableTest = new Thread(futureTask);
    callableTest.start();
    
    String result = futureTask.get();
    System.out.println("callable result :" + result);
}
复制代码

结果

Thread-0, Thread example.....
Thread-1, Runnable example.....
Thread-2, Callable example.....
callable result :call result
复制代码

并发编程的好处

直接上例子,马上双十一了,加入仓库有一堆货物,目前只有一个人搬(甲),暂定有1000000件,如果甲只能一次搬一件,那么总共需要搬运1000000次,如果能加入一个乙与甲一起搬,假定两人搬运时间一样,那么两人各搬500000次,就能搞定。

搬货

static class Carry extends Thread{

    /** 搬运人*/
    private String peopleName;
    /** 搬运次数*/
    private int carryNum;

    public Carry(String peopleName) {
        this.peopleName = peopleName;
    }

    @Override
    public void run() {
        while (!isInterrupted()) {
            synchronized (cargoNum) {
                if (cargoNum.get() > 0) {
                    cargoNum.addAndGet(-1);
                    carryNum++;
                } else {
                    System.out.println("搬运完成,员工:" + peopleName + ",搬运:[" + carryNum + "]次");
                    interrupt();
                }
            }
        }
    }
}
复制代码

甲单独搬

/** 货物个数*/
static AtomicInteger cargoNum = new AtomicInteger(1000000);

public static void main(String[] args) {
    Carry carry1 = new Carry("甲");

    carry1.start();
}

# 结果
搬运完成,员工:甲,搬运:[1000000]次
复制代码

甲乙一起搬

/** 货物个数*/
static AtomicInteger cargoNum = new AtomicInteger(1000000);

public static void main(String[] args) {
    Carry carry1 = new Carry("甲");
    Carry carry2 = new Carry("乙");

    carry1.start();
    carry2.start();
}

# 结果
搬运完成,员工:乙,搬运:[272708]次
搬运完成,员工:甲,搬运:[727292]次
复制代码

上面我们模拟了两个搬货的情况,甲乙两人一起搬运时,能并行工作,效率更高,如果再加丙、丁....则会更快。

现在的CPU一般都支持超线程技术,即一个物理核可当作两个逻辑核,且单CPU基本都是多核,少则几核,多则几十上百,如果我们单独只让一个工作,那么其他的则会处于空闲状态,比如我现在有十个工人可以搬货,但是我只让甲去,那么其他人力成本就相当于浪费掉了。

所以,并发编程最直接的好处就是能合理利用机器资源,提升处理效率。特别是在异步编程,能充分压榨CPU的性能。

例子中用到了synchronizedAtomicInteger,是为了保证货物剩余总量在多人搬运下,数值也是对的,可以尝试将AtomicInteger换成Integer或者去除synchronized,两人最终总计搬运和将与原货物总量不一致。

所以人多了,事也多了,并发编程时,我们需要考虑线程安全,上下文切换等常见问题,这些将在后续章节呈现。

猜你喜欢

转载自juejin.im/post/5db9a2225188253cef5554c5