基础概念
什么是线程和进程
在 Java 中,当我们启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程。
进程:是程序的一次执行过程,系统运行程序的基本单位。
- 系统运行一个程序即是一个进程从创建,运行到消亡的过程。
线程:是比进程更小的执行单位,一个进程在其执行的过程中可以产生多个线程。
- 多个线程共享进程的堆和方法区。
- 每个线程都有自己的程序计数器、虚拟机栈和本地方法栈。
什么是并发和并行
并发:同一时间段,多个任务都在执行 (单位时间内不一定同时执行)
并行:单位时间内,多个任务同时执行。
说说线程的生命周期
线程的生命周期主要分为六个状态:初始状态、运行状态、阻塞状态、等待状态、超时等待状态、终止状态。
初始状态(NEW):线程被构建,但是没有调用start方法。
运行状态(RUNNABLE): 线程在操作系统中处于 就绪或运行两种状态。
阻塞状态(BLOCKED): 线程被锁阻塞了。
等待状态(WAITING): 线程进入等待状态。需要其他线程通知或直接中断。
超时等待状态(TIME_WAITING): 线程指定了超时时间,可以在超时时间结束后自行返回。
终止状态(TERMIATEB): 线程执行结束。
线程不是一直固定在某个状态,而是随着代码的执行在不同状态之间切换。

线程创建之后它将处于 NEW 状态,调用 start() 方法后开始运行,线程这时候处于就绪状态。就绪状态的线程获得了 CPU 时间片(timeslice)后就处于 RUNNABLE 状态。
当线程执行 wait() 方法之后,线程进入 WAITING 状态。进入 WAITING 状态的线程需要依靠其他线程的通知才能够返回到 RUNNABLE 状态,
而 TIME_WAITING 状态的线程在超时后会自行回到 RUNNABLE 状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到 BLOCKED 状态。
线程在执行完 Runnable 的 run() 方法之后将会进入到 TERMINATED 状态。
线程的结束
设置退出标志,是线程正常退出,也就是当run()方法执行完成后线程终止。
使用interrupt()方法中断线程
使用stop()方法强行中断线程(不推荐使用,Thread.stop, Thread.suspend, Thread.resume 和Runtime.runFinalizersOnExit 这些终止线程运行的方法已经被废弃,使用它们是极端不安全的)
并发编程的三大特性
并发编程的三大特性只要是:原子性、可见性、有序性
原子性:即一个操作或者多个操作,要么一起执行完成,中途不可中断,要么都不执行。
可见性:是在多个线程访问一个共享变量是,其中一个线程修改了这个变量的值,其他线程应该立即看到修改的值。
有序性:程序执行的顺序按照代码的先后顺序执行。一般JVM会自动对其进行优化,使其重排序。
java的内存模型 JMM
用于屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果,JMM规范了Java虚拟机与计算机内存是如何协同工作的:规定了一个线程如何和何时可以看到由其他线程修改过后的共享变量的值,以及在必须时如何同步的访问共享变量。
用来定义一个一致的、跨平台的内存模型,是缓存一致性协议,用来定义数据读写的规则。
JMM决定一个线程对共享变量的写入时,能对另一个线程可见。
从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory).
本地内存中存储了该线程可以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。

JAVA 变量的读写
我们在看Volatile关键字的时候先了解一下java变量的读写:
(1)lock:作用于主内存,把变量标识为线程独占状态。
(2)unlock:作用于主内存,解除独占状态。
(3)read:作用主内存,把一个变量的值从主内存传输到线程的工作内存。
(4)load:作用于工作内存,把read操作传过来的变量值放入工作内存的变量副本中。
(5)use:作用工作内存,把工作内存当中的一个变量值传给执行引擎。
(6)assign:作用工作内存,把一个从执行引擎接收到的值赋值给工作内存的变量。
(7)store:作用于工作内存的变量,把工作内存的一个变量的值传送到主内存中。
(8)write:作用于主内存的变量,把store操作传来的变量的值放入主内存的变量中。
重排序
在执行程序时,为了提高性能,编译器和处理器常常会对指令进行重排序。一般重排序可以分为如下三种:
编译器优化的重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
指令级并行的重排序:现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
内存系统的重排序:由于处理器使用缓存和读 / 写缓冲区,这使得加载和存储操作看上去可能是在乱序执行的。
这里还得提一个概念,as-if-serial:不管怎么重排序,单线程下的执行结果不能被改变。
happens-before原则
- 程序次序原则:在一个线程内,按照程序代码顺序,书写在前面的操作优先发生于书写在后面的操作。
- 锁定规则:对于一个锁的解锁操作(unLock),优先发生于后续对这个锁的加锁操作(lock)。
- volatile原则:对一个volatile变量的写操作,优先发生于后续对这个变量的读操作。
- 传递原则:如果A操作先行发生于操作B,B操作先行发生于操作C,即A操作先行发生于操作C。
- 线程启动原则:同一个线程的start()优先发生于此线程的其他方法。
- 线程中断原则:对线程interrupt()方法的调用,优先发生于被中断线程的代码检测到中断事件的发生。
- 线程终结原则:同一个线程所有的操作都优先于线程的终止检测。
- 对象创建原则:一个对象的初始化完成,优先于发生于它的 finalize()的开始。
说说 sleep() 和 wait() 的区别?
- sleep() 和 wait() 都可以暂停线程的执行。
- sleep() 不释放锁,wait() 释放锁。
- sleep() 在 Thread 类中声明的,wait() 在 Object 类中声明。
- sleep() 是静态方法,wait() 是非静态方法(必须由同步锁对象调用)。
- sleep() 方法导致线程进入阻塞状态后,当时间到了或者 interrupt() 会醒来。
- wait() 方法导致线程进入阻塞状态后,需要由 notify() 或 notifyAll() 唤醒,或者使用 wait(long timeout) 超时后线程会自动苏醒。
为什么不能直接调用 run() 方法?
调用 run() 方法,会被当做普通方法去执行,不是多线程工作。
调用 start() 方法,会启动线程并使线程进入了就绪状态,当分配到时间片后就可以运行 run() 方法内容了,这是真正的多线程工作
说说 Runnable 和 Callable 的区别?
Runnable 和 Callable 都是接口,都可以编写多线程程序。不同的是:
Runnable 接口 run 方法无返回值,Callable 接口 call 方法有返回值。
Runnable 接口 run 方法只能直接抛出运行时异常,Callable 接口 call 方法可以捕获异常。
对于 Calleble 来说,Future 和 FutureTask 均可以用来获取任务执行结果,不过 Future 是个接口,FutureTask 是 Future 的具体实现。
FutureTask 表示一个异步运算的任务。
FutureTask 里面可以传入一个 Callable 的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。
只有当运算完成的时候结果才能取回,如果运算尚未完成 get 方法将会阻塞。一个 FutureTask 对象可以对调用了 Callable 和 Runnable 的对象进行包装,由于 FutureTask 也是Runnable 接口的实现类,所以 FutureTask 也可以放入线程池中。
volatile 于 synchronized 区别
volatile 只保证可见性,不保证原子性,禁止重排序保证了有序性。
synchronized 既可以保证原子性,也能保证可见性。synchronized确保了一次只有一个线程执行,即happens-before的有序原则,也确保了有序性。
volatile 只能保证数据的可见性,不能用于同步,因此多个线程访问volatile修饰的变量不会造成zuse。
n 不仅保证了可见性,也保证了原子性。
因为经n修饰后,只有获得锁的线程才能进入临界区,从而保证了临界区内的所有语句都全部执行。
多个线程争抢n变量时,会出现阻塞情况
volatile是轻量级的,因为只能修饰变量。
n是重量级的,可以修饰变量、代码块、方法。