按照规划,从本篇开始咱们开启『并发』系列内容的总结,从本篇的线程开始,到线程池,到几种并发集合源码的分析,咱们一点点来,但愿你也有耐心,由于并发这块知识是你职业生涯始终绕不过的坎,任何一个项目都或多或少的要涉及一些并发的处理。java
这一系列文章只能算是对并发这块基本理论知识的一个总结与介绍,想要成为并发高手,必然是须要经过大规模并发访问的线上场景应用,或许之后我有了相关经验了,再给大家作一点分享吧。git
进程和线程算是操做系统内两个很基本、很重要的概念了,进程是操做系统中进行保护和资源分配的基本单位,操做系统分配资源以进程为基本单位。而线程是进程的组成部分,它表明了一条顺序的执行流。github
系统中的进程线程模型是这样的:算法
进程从操做系统得到基本的内存空间,全部的线程共享着进程的内存地址空间。固然,每一个线程也会拥有本身私有的内存地址范围,其余线程不能访问。缓存
因为全部的线程共享进程的内存地址空间,因此线程间的通讯就容易的多,经过共享进程级全局变量便可实现。bash
同时,在没有引入多线程概念以前,所谓的『并发』是发生在进程之间的,每一次的进程上下文切换都将致使系统调度算法的运行,以及各类 CPU 上下文的信息保存,很是耗时。而线程级并发没有系统调度这一步骤,进程分配到 CPU 使用时间,并给其内部的各个线程使用。微信
在分时系统中,进程中的每一个线程都拥有一个时间片,时间片结束时保存 CPU 及寄存器中的线程上下文并交出 CPU,完成一次线程间切换。固然,当进程的 CPU 时间使用结束时,全部的线程必然被阻塞。markdown
JAVA API 中用 Thread 这个类抽象化描述线程,线程有几种状态:多线程
其中 RUNNABLE 表示的是线程可执行,但不表明线程必定在获取 CPU 执行中,可能因为时间片使用结束而等待系统的从新调度。BLOCKED、WAITING 都是因为线程执行过程当中缺乏某些条件而暂时阻塞,一旦它们等待的条件知足时,它们将回到 RUNNABLE 状态从新竞争 CPU。并发
此外,Thread 类中还有一些属性用于描述一个线程对象:
其中,tid 是一个自增的字段,每建立一个新线程,这个 id 都会自增一。优先级取值范围,从一到十,数值越大,优先级越高,默认值为五。
Runnable 是一个接口,它抽象化了一个线程的执行流,定义以下:
public interface Runnable {
public abstract void run();
}
复制代码
经过重写 run 方法,你也就指明了你的线程在获得 CPU 以后执行指令的起点。咱们通常会在构造 Thread 实例的时候传入这个参数。
建立一个线程基本上有两种方式,一是经过传入 Runnable 实现类,二是直接重写 Thread 类的 run 方法。咱们详细看看:
一、自定义 Runnable 实现
public class MyThread implements Runnable{ @Override public void run(){ System.out.println("hello world"); } } 复制代码
public static void main(String[] args) { Thread thread = new Thread(new MyThread()); thread.start(); System.out.println("i am main Thread"); } 复制代码
运行结果:
i am main Thread
hello world
复制代码
其实 Thread 这个类也是继承 Runnable 接口的,而且提供了默认的 run 方法实现:
@Override public void run() { if (target != null) { target.run(); } } 复制代码
target 咱们说过了,是一个 Runnable 类型的字段,Thread 构造函数会初始化这个 target 字段。因此当线程启动时,调用的 run 方法就会是咱们本身实现的实现类的 run 方法。
因此,天然会有第二种建立方式。
二、继承 Thread 类
既然线程启动时会去调用 run 方法,那么咱们只要重写 Thread 类的 run 方法也是能够定义出咱们的线程类的。
public class MyThreadT extends Thread{ @Override public void run(){ System.out.println("hello world"); } } 复制代码
Thread thread = new MyThreadT();
thread.start();
复制代码
效果是同样的。
关于线程的操做,Thread 类中也给咱们提供了一些方法,有些方法仍是比较经常使用的。
一、sleep
public static native void sleep(long millis)
复制代码
这是一个本地方法,用于阻塞当前线程指定毫秒时长。
二、start
public synchronized void start()
复制代码
这个方法可能不少人会疑惑,为何我经过重写 Runnable 的 run 方法指定了线程的工做,但倒是经过 start 方法来启动线程的?
那是由于,启动一个线程不只仅是给定一个指令开始入口便可,操做系统还须要在进程的共享内存空间中划分一部分做为线程的私有资源,建立程序计数器,栈等资源,最终才会去调用 run 方法。
三、interrupt
public void interrupt()
复制代码
这个方法用于中断当前线程,固然线程的不一样状态应对中断的方式也是不一样的,这一点咱们后面再说。
四、join
public final synchronized void join(long millis)
复制代码
这个方法通常在其余线程中进行调用,指明当前线程须要阻塞在当前位置,等待目标线程全部指令所有执行完毕。例如:
Thread thread = new MyThreadT(); thread.start(); thread.join(); System.out.println("i am the main thread"); 复制代码
正常状况下,主函数的打印语句会在 MyThreadT 线程 run 方法执行前执行,而 join 语句则指明 main 线程必须阻塞直到 MyThreadT 执行结束。
多线程的优势咱们不说了,如今来看看多线程,也就是并发下会有哪些内存问题。
一、竞态条件
这是一类问题,当多个线程同时访问并修改同一个对象,该对象最终的值每每不如预期。例如:
咱们建立了 100 个线程,每一个线程启动时随机 sleep 一会,而后为 count 加一,按照通常的顺序执行流,count 的值会是 100。
可是我告诉你,不管你运行多少遍,结果都不尽相同,等于 100 的几率很是低。这就是并发,缘由也很简单,count++ 这个操做它不是一条指令能够作的。
它分为三个步骤,读取 count 的值,自增一,写回变量 count 中。多线程之间互相不知道彼此,都在执行这三个步骤,因此某个线程当前读到的数据值可能早已不是最新的了,结果天然不尽如指望。
但,这就是并发。
二、内存可见性
内存可见性是指,某些状况下,线程对于一些资源变量的修改并不会立马刷新到内存中,而是暂时存放在缓存,寄存器中。
这致使的最直接的问题就是,对共享变量的修改,另外一个线程看不到。
这段代码很简单,主线程和咱们的 ThreadTwo 共享一个全局变量 flag,后者一直监听这个变量值的变化状况,而咱们在主线程中修改了这个变量的值,因为内存可见性问题,主线程中的修改并不会立马映射到内存,暂时存在缓存或寄存器中,这就致使 ThreadTwo 没法知晓 flag 值的变化而一直在作循环。
总结一下,进程做为系统分配资源的基本单元,而线程是进程的一部分,共享着进程中的资源,而且线程仍是系统调度的最小执行流。在实时系统中,每一个线程得到时间片调用 CPU,多线程并发式使用 CPU,每一次上下文切换都对应着「运行现场」的保存与恢复,这也是一个相对耗时的操做。
ps:前段时间确实有点忙,拖更好多天,这里再给你们说声抱歉了,感谢大家尚未走,如今正式恢复,开启并发系列总结~
文章中的全部代码、图片、文件都云存储在个人 GitHub 上:
欢迎关注微信公众号:OneJavaCoder,全部文章都将同步在公众号上。