探秘java并发系列一(线程)

290 阅读9分钟

前言: 虽然说各大博客上关于java线程的文章已经烂大街了,但是笔者还是想按照自己的理解写下这下这个系列的博客,一方面巩固自己的基础知识,还有一方面如果有理解不到位的地方还请各位大佬在评论区多多指教

线程的概念

线程是什么?这是一个看起来简单的问题,其实牵扯到一大串的计算机基础知识。
在聊线程之前,我们要先了解进程的概念
用官方的话来说 “进程是资源分配的最小单位,线程是CPU调度的最小单位”
这个回答有点抽象,不了解的同学可能看了会觉得云里雾里的
举个贴近生活的例子

进程可以看成公司里独立的团队,团队的资源是进程创建的时候就分配好的,团队之间一般情况下也是管各自的工作的,线程可以看做团队里的小伙伴

1.一个进程可以包含多个线程(一个团队里有很多小伙伴)
2.进程要比线程消耗更多的计算机资源(团队管理需要更大的成本,计算机资源指的是计算机的cpu,内存,磁盘io等)
3.进程之间的通讯更困难(团队之间的沟通更花费时间)
ps(进程之间的通讯方式有管道,消息队列,共享内存,信号量,socket等)
4.线程之间数据共享相对简单(团队内部沟通更高效)
ps(java线程之间通讯方式有 锁同步机制,共享内存,Object里的wait/notify方法等)
5.进程之间相互独立,线程可能会影响线程(例如一个线程的oom会直接导致虚拟机进程崩溃)

随着cpu的制程越来越接近瓶颈(隐约记得好像硅结晶的话3nm就是极限了)摩尔定律以另外一种形式继续延续着它的预言,cpu的核心数越来越多。 那么掌握多线程,充分利用上cpu资源已经是程序员的一个必备技能了。
尤其是在大体量的任务计算上面,使用多线程能够提高数倍甚至数十倍的接口执行效率(笔者在实际开发过程中也感受到了多线程的魅力)

java中线程的创建

那么java中怎么开启一个线程呢? 其实万变不离其宗,如果我们要创建一个线程,那么必然要创建一个Thread对象,然后调用start方法 我们追踪start源码可以看到最后调用了一个native方法start0()

native关键字是与其他语言协同开发时用的,告诉虚拟机这个方法的实现是用其他语言写的 这里不展开讨论,我们就知道最后还是会调用run方法就好了

当然我们要知道start()方法和run()方法的区别,一个是会告诉操作系统需要创建一个新的线程执行run方法,一个就是通过当前线程直接执行run方法(千万不要搞错!!)

具体代码的话,按照java标准的写法有三种形式,就用伪代码稍微贴一下,当然还有匿名内部类的写法,lambada的写法,这些都是语法糖,有兴趣的同学就自己搜一下。

1.继承Thread类,重写run方法,创建ThreadStart实例,调用start()方法

class ThreadStart extends Thread{
    @Override
    public void run() {
        System.out.println("线程启动方式一(继承Thread类,重写run方法): 线程启动了");
    }
}

2.实现一个runnable接口,重写run方法,创建Thread实例并以重写过run方法的runnable对象作为构造函数入参,调用start()方法

class ThreadRunnableStart implements Runnable{
    @Override
    public void run() {
        System.out.println("线程启动方式二(实现Runnable,重写run方法): 线程启动了");
    }
}

3.实现callable接口,重写call方法,创建FutureTask实例并以重写过call方法的callable对象作为构造函数入参,创建Thread实例并以fetureTask对象作为构造函数入参,调用start()方法启动线程。 调用FutureTask的get()方法可以获取返回值。

ps(此处call方法不是由线程的start0方法调用,而是被FutureTask的run方法调用)

class ThreadTaskStart implements Callable<String> {

    public String call() throws Exception {
        System.out.println("线程启动方式三(实现Callable,重写call方法): 线程启动了");
        return "我是call方法返回值";
    }
}

线程生命周期

ps(图裂了,下次补上)

通过线程周期状态图可知,线程的生命周期可分为以下五个阶段

  • NEW(新建) ps:创建出线程对象时默认状态
  • RUNNABLE(就绪) ps:调用start()方法后进入就绪阶段,此时线程还需要听从cpu的调度
  • RUNNING(运行) ps:当线程分配到cpu时间片的时候,进入运行状态
  • BLOCKED(阻塞) ps: 当调用suspend/sleep/wait方法时,进入阻塞状态
  • TERMINATED(结束) ps: 当运行完成,或者调用stop方法,进行进入结束状态

FetureTask源码探秘

Future是一个接口,FutureTask是Future的一个实现类,并实现了Runnable,因此FutureTask可以传递到线程对象Thread中新建一个线程执行。

FutureTask是为了弥补Thread的不足而设计的,它可以让我们准确地知道线程什么时候执行完成并能够获得到线程执行完成后返回的结果。

这里分享一个笔者的经验: 在读源码的过程中,一定要带着问题去读,或者说一定要有一个主线

在读FetureTask源码过程中,我们就可以带着这么一个问题(fetureTask是如何实现返回线程执行的结果的)

我们可以先大致看一遍类中定义的核心成员变量

    private volatile int state; //当前任务的执行状态
    private static final int NEW          = 0;//未开始
    private static final int COMPLETING   = 1;//完成中
    private static final int NORMAL       = 2;//已正常完成
    private static final int EXCEPTIONAL  = 3;//异常
    private static final int CANCELLED    = 4;//取消
    private static final int INTERRUPTING = 5;//中断中
    private static final int INTERRUPTED  = 6;//已中断
    
    private Callable<V> callable;//我们自己创建的callable对象,创建fetureTask对象时用构造方法注入,会在run方法里面调用callable的call()方法
    
    private Object outcome;//call方法返回值或者异常
    
    private volatile Thread runner;//当前执行任务的线程信息
    
    private volatile WaitNode waiters;//等待获取执行结果的线程信息
    

FetureTask是实现了Runnable接口的,所以在线程启动的时候会调用run()方法,所以run方法是fetureTask的核心方法。

如果需要重复执行任务可以调用runAndReset()方法 每次执行完之后会把state重置为NEW状态

public void run() {
        //方法开始的时候会进行task状态的判断,并且使用cas尝试给runnerd对象赋值当前线程对象的引用
        if (state != NEW ||
            !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                         null, Thread.currentThread()))
            return; 
        try {
            Callable<V> c = callable;
            if (c != null && state == NEW) {
                V result;
                boolean ran;
                try {
                    //在这里调用callable的call方法,并获得callable的返回值
                    result = c.call();
                    //执行状态
                    ran = true;
                } catch (Throwable ex) {
                    result = null;
                    ran = false;
                    //state: NEW->COMPLETING->EXCEPTIONAL
                    setException(ex);
                }
                if (ran)
                    //执行成功就给outcome赋值,并且唤醒所有在waiters中等待的线程
                    //state: NEW->COMPLETING->NORMAL
                    set(result);
            }
        } finally {
            //执行完成之后将runner重置。
            //直到set状态前,runner一直都是非空的,为了防止并发调用run()方法
            runner = null;
            //重置后判断状态
            int s = state;
            // 有别的线程要中断当前线程,把CPU让出去,自旋等一下
            if (s >= INTERRUPTING)
                handlePossibleCancellationInterrupt(s);
        }
    }

现在futureTask的任务执行完了,我们要怎么获得call方法的返回值呢?

在看源码之前我们可以先推导一下,futureTask中有执行状态的成员变量state,有存储返回值的成员变量outCome。那只要判断一下state,如果处于执行完成状态,那么直接返回outCome就好了,如果还未完成,则需要让当前线程进入阻塞,等完成之后再唤醒该线程。

public V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException {
        if (unit == null)
            throw new NullPointerException();
        int s = state;
        //如果task尚未执行完成,则进入等待方法 awaitDone
        if (s <= COMPLETING &&
            (s = awaitDone(true, unit.toNanos(timeout))) <= COMPLETING)
            //获取超时直接抛出TimeOutException
            throw new TimeoutException();
        return report(s);
    }
    
    //timed 是否定时等待,nanos 等待时间
   private int awaitDone(boolean timed, long nanos)
        throws InterruptedException {
        final long deadline = timed ? System.nanoTime() + nanos : 0L;
        WaitNode q = null;
        boolean queued = false;
        for (;;) { //此处死循环主要是为了进行各种判断
            //如果线程已经中断,那么直接抛出异常
            if (Thread.interrupted()) {
                removeWaiter(q);
                throw new InterruptedException();
            }

            int s = state;
            //如果任务已经完成,直接返回
            if (s > COMPLETING) {
                if (q != null)
                    q.thread = null;
                return s;
            }
            //如果任务正在完成中,先让出cpu
            else if (s == COMPLETING) // cannot time out yet
                Thread.yield();
            //初始化等待节点
            else if (q == null)
                q = new WaitNode();
            //节点cas入队
            else if (!queued)
                queued = UNSAFE.compareAndSwapObject(this, waitersOffset,
                                                     q.next = waiters, q);
            //判断是否超时
            else if (timed) {
                nanos = deadline - System.nanoTime();
                if (nanos <= 0L) {
                    removeWaiter(q);
                    return state;
                }
                //调用park方法让线程阻塞(带唤醒时间)
                LockSupport.parkNanos(this, nanos);
            }
            else
                //调用park方法让线程阻塞
                LockSupport.park(this);
        }
    }

image.png

这个是futureTask中定义的线程等待队列,就是一个单向链表,存储着等待线程的引用信息。 因为get方法可能被并发调用,所以在线程包装类WaitNode入队的时候是使用cas来保证线程安全的

CAS(Compare and swap)比较和替换

说到CAS,就不得不提java中的乐观锁和悲观锁

悲观锁就是每次都假定数据会被修改,所以悲观锁在持有数据的时候总会把资源 或者 数据 锁住,这样其他线程想要请求这个资源的时候就会阻塞,直到等到悲观锁把资源释放为止。比如java中的synchronized就是一个悲观锁,这里先不展开说了。

乐观锁就是每次假定数据没有被修改,但是在写入操作的时候会进行验证来保证线程安全性(如果没有这一步...也不叫锁了)

cas就是属于乐观锁的一种,cas有三个操作数----内存对象(V)、预期原值(A)、新值(B)。CAS原理就是对v对象进行赋值时,先判断原来的值是否为A,如果为A,就把新值B赋值到V对象上面,如果原来的值不是A(代表V的值放生了变化),就不赋新值。

当然cas也有一些缺点,比如ABA问题,这个可以通过额外设置一个版本号来解决,在juc下有个AtomicMarkableReference类的实现方案,想了解的同学可以自行百度~

总结

这篇博客主要介绍了

1.线程的概念

2.Java中线程的启动方式

3.线程生命周期

4.FeatureTask的源码解读

5.cas简单介绍

大家有什么补充或者建议可以在评论区留言 下一期笔者准备写写锁的老大哥(synchronized)以及后起之秀(Reentrantlock)