或许是执念太重,又或许是性格缺陷,我对java中一些知识的坚持,已经到了让人无法接受的地步。有些人甚至因此在背后骂我神经病、傻瓜。但我依旧我行我素,即使中间懈怠了很长时间,重新开始时我依旧会以这些知识为起点。不过,如此这般循环往复,我对这些知识的理解却依旧模模糊糊。这着实是天底下最大的笑话!譬如本篇要梳理的多线程。为了掌握它,我不仅买了很多相关的书籍,而且还买了超级多的视频课程,但最终也就那样——面试时吞吞吐吐,语焉不详;与人讨论时仍然毫无想法,好似一张刚做出来的白纸一样。每每这时,我都极力深刻反省,但情绪化的想法总会让我认为造成这种结果的原因是那些人不愿意给予机会于我。直到最近读了了凡先生的《了凡四训》,我才有所顿悟:原来所有的结果都是自己造就的,并非别人造成的。
在本章中,我将结合以前的课程、资料以及网络上的一些资源,对java多线程进行一次自认为相对全面的梳理,以明晰这些知识的前后逻辑,加深本人对这些知识的理解。本节将从以下几个方面展开:
- 多线程的基本知识(多线程的基本概念、java中多线程的基本操作)
- 与多线程相关的一些知识的梳理
- java内存模型
- java多线程与内存模型之间的关系
1 多线程的基本知识
看到这个标题,我虽然很想多说一些,但实在不知道从何说起。这不仅仅是因为自己的表达能力有欠缺,更是因为自己的知识是碎片化的,是不完整的。不过,既然已经决定出来出丑了,所有的不足都已经不重要了,耐心做好本小节的梳理才是对自己最好的报答。
1.1 常见概念
首先让我们来看几组在多线程开发中经常遇到的概念:a) 同步与异步(这组概念通常用来形容一次方法调用。所谓同步是指方法调用一旦开始,调用者就必须等被调用方法执行结束后,才能继续后面的操作;所谓异步是指方法调用一旦开始,就会立即返回,调用者几乎不用等待,就可以继续后面的操作,而被调用的方法会在另一个线程中真实的执行,整个执行过程不会阻碍调用者的工作。【注意这里出现了一个名词——线程】)。下面这幅图展示了同步调用与异步调用之间的关系:
从图中可以看出两个时间线(图中的两个向右的箭头),其中最上面的时间线上方法调用是同步进行的,也就是说后一个逻辑需要在前一个逻辑完成后才能进行下一步逻辑。而异步显然不会这样,从图中可以可看出方法调用了另一个时间线,但是该调用后立马返回,另一时间线上的处理并不会影响本时间线上的处理。b) 并发和并行(这两个概念非常容易混淆,虽然它们都可以表示两个或者多个任务一起执行,但侧重点有所不同。并发侧重于多个任务交替执行,而多个任务之间有可能还是串行的,而并行是真正意义上的“同时执行”。大家都用过多核CPU,它里面的执行就可能是并行执行,然而在单核CPU中,即使有多进程或者多线程任务,那么真实环境中这些任务也不可能是并行的,因为一个CPU一次只能执行一条指令,因此在这种情况下多进程之间是并发执行的)。具体可以参见下面这幅图:
这幅图中,左侧是一个并行流程,右侧是一个并发流程。c) 临界区(临界区通常用于表示一组公共资源或共享数据,它可以被多个线程使用。不过每次,只能有一个线程使用它,一旦临界区资源被占用,其他线程要想使用这个临界区资源就必须等待)。d) 阻塞和非阻塞(阻塞和非阻塞通常用来形容多线程之间的相互影响。比如一个线程占用了临界区资源,那么其他所有需要这个资源的线程就必须在这个临界区中等待。等待会导致线程挂起,这种情况就是阻塞。如果一旦出现某个占用资源的线程不愿意释放资源,那么其他所有阻塞在这个临界区上的线程就不能工作【注意这里出现了另外一个名词——线程阻塞】)。e) 死锁、饥饿和活锁(这三个名词都属于多线程的活跃性问题。如果出现这几种情况,那么相关线程就可能不再活跃了,也就是说它们很难再继续执行下去。所谓死锁就是参与活动的几个线程之间相互持有彼此需要的锁,比如A线程持有A锁,但此时需要另外一个B锁,而B线程持有B锁,却同时需要另外一个A锁,那么此时A、B线程之间就出现了死锁。所谓饥饿是指某一个或多个线程因为某种原因无法得到所需的资源,导致一直无法执行。比如可能线程优先级低,导致高优先级线程一直抢占它们所需的资源,进而导致它们无法工作。所谓活锁则是一种因为彼此过于礼貌而出现的线程无法执行的情况,比如线程A需要锁C,而线程B也需要锁C,此时线程A发现有人需要就让出锁C的使用权,希望对方先执行,而线程C也发现了这个问题,也主动让出了锁C的使用权)。
1.2 Java中线程的基本概念
接着让我们来看一下java中与线程有关的基本知识:在计算机中进程是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。由此可以直到进程可以容纳若干个线程。其实线程就是一个轻量级的进程,是程序执行的最小单位。使用多线程而非用多进程去进行并发程序的设计,是因为线程间的切换和调度的成本远远小于进程。下面这张图展示了线程的生命周期:
其实这些状态都在Thread类中的State枚举类中有定义,这个枚举类的源代码如下所示,不过还是建议浏览jdk源码:
public enum State {/*** Thread state for a thread which has not yet started.*/NEW,/*** Thread state for a runnable thread. A thread in the runnable* state is executing in the Java virtual machine but it may* be waiting for other resources from the operating system* such as processor.*/RUNNABLE,/*** Thread state for a thread blocked waiting for a monitor lock.* A thread in the blocked state is waiting for a monitor lock* to enter a synchronized block/method or* reenter a synchronized block/method after calling* {@link Object#wait() Object.wait}.*/BLOCKED,/*** Thread state for a waiting thread.* A thread is in the waiting state due to calling one of the* following methods:* <ul>* <li>{@link Object#wait() Object.wait} with no timeout</li>* <li>{@link #join() Thread.join} with no timeout</li>* <li>{@link LockSupport#park() LockSupport.park}</li>* </ul>** <p>A thread in the waiting state is waiting for another thread to* perform a particular action.** For example, a thread that has called {@code Object.wait()}* on an object is waiting for another thread to call* {@code Object.notify()} or {@code Object.notifyAll()} on* that object. A thread that has called {@code Thread.join()}* is waiting for a specified thread to terminate.*/WAITING,/*** Thread state for a waiting thread with a specified waiting time.* A thread is in the timed waiting state due to calling one of* the following methods with a specified positive waiting time:* <ul>* <li>{@link #sleep Thread.sleep}</li>* <li>{@link Object#wait(long) Object.wait} with timeout</li>* <li>{@link #join(long) Thread.join} with timeout</li>* <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>* <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>* </ul>*/TIMED_WAITING,/*** Thread state for a terminated thread.* The thread has completed execution.*/TERMINATED;
}
到这里,我想起了《重新认识AbstractQueuedSynchronizer》这篇文章。在梳理这篇文章时我想到了很多问题:java中的多线程是什么?java中线程的状态有哪些?java中线程的通信方式有几种?这些都是面试时,面试官问过的问题。现在来看第二个问题:java中线程的状态有6种,它们分别为:新建(NEW)、运行时(RUNNABLE)、阻塞(BLOCKED)、有限等待(TIMED_WAITING)、等待(WAITING)、终结(TERMINATED)。其中NEW表示线程刚刚创建。当调用线程的start()方法后,线程就开始执行了(注意这个说法只是为了让我们有个形象的了解),此时线程处于RUNNABLE状态(这里要清楚线程处在这个状态就表示线程所需的一切资源都已经准备好了)。如果线程在执行过程中,遇到了synchronized同步块,就会进入BLOCKED阻塞状态,这时线程处于暂停状态,直到获得请求的锁。WAITING和TIMED_WAITING都表示等待状态,区别在于前者会进入一个无时间限制的等待,后者会进入一个有时限的等待(处于等待状态的线程究竟在等什么呢?一般处于这种状态的线程是在等待一些特殊的事件。比如通过wait()方法等待的线程在等待notify()方法,而通过join()方法等待的线程则会等待目标线程的终止。注意:一旦等到了期望的事件,线程就会再次执行,进入RUNNABLE状态)。当线程执行完毕,则会进入TERMINATED状态。【注意从NEW状态出发后,线程不能再回到NEW状态,同理,处于TERMINATED状态的线程也不能再回到RUNNABLE状态】
1.3 Java中线程的基本操作
通过前面的梳理,我们知道了Java线程的几种状态,期间也有一些新的操作映入眼帘,比如join()方法,那Java中线程的基本操作到底有那些呢?记得这个问题在之前的文章中做过梳理(譬如:《ThreadLocal“你”真的了解吗?(一)》),但这些仅仅是简单的介绍,并不完善,这里我将再做一次梳理: