操作系统原理
进程是一个独立的运行环境,而线程是在进程中执行的一个任务。它们的区别在于是否单独占用内存地址空间以及其它系统资源。进程独享所属进程的内存地址和资源,数据共享简单,但同步复杂。进程的创建与销毁不仅需要保存寄存器和栈信息,还需要资源和分配(CPU)以及页调度(内存),开销较大。线程的创建与销毁只需要保存寄存器和栈信息,开销较小。进程是资源分配的基本单位,线程是调度的基本单位。协程(Coroutines)是一种比线程更加轻量级的存在,正如一个进程可以拥有多个线程一样,一个线程可以拥有多个协程。协程不是被操作系统内核所管理的,而是完全由程序所控制,也就是在用户态执行。这样带来的好处是性能大幅度的提升,因为不会像线程切换那样消耗资源。
- 用户态和内核态
内核态:CPU可以访问内存所有数据, 包括外围设备, 例如硬盘, 网卡. CPU也可以将自己从一个程序切换到另一个程序。
用户态:只能受限的访问内存, 且不允许访问外围设备. 占用CPU的能力被剥夺, CPU资源可以被其他程序获取。
用户级线程和内核级线程可以是一对一模型、多对一模型,也可以是混合模式。Java中的线程是内核控制的线程,又称又称轻型进程LWP(light weight process)。首先资源和分配并不能由Java控制,但Java提供了大量的大量的线程管理机制。其用户态与内核态的对应关系并不确定。由用户态切换到内核态需要进行系统调用。
- 上下文切换
上下文切换是指 CPU 从一个进程(或线程)切换到另一个进程(或线程)。上下文是指某一时间点 CPU 寄存器和程序计数器的内容。
上下文切换通常是计算密集型的,意味着此操作会消耗大量的 CPU 时间,故线程也不是越多越好。
进程和线程的切换者都是操作系统,而协程的切换是用户。线程切换过程需要由“用户态到内核态到用户态”,协程的切换过程只有用户态,即没有陷入内核态,因此切换效率高。
-
系统调用
-
用户线程和守护线程
在linux或者unix操作系统中,守护进程(Daemon)是一种运行在后台的特殊进程,它独立于控制终端并且周期性的执行某种任务或等待处理某些发生的事件。
守护线程是依赖于用户线程,用户线程退出了,守护线程也就会退出,典型的守护线程如垃圾回收线程。用户线程是独立存在的,不会因为其他用户线程退出而退出。默认情况下启动的线程是用户线程,通过setDaemon(true)将线程设置成守护线程,这个函数务必在线程启动前进行调用,否则会报java.lang.IllegalThreadStateException异常,启动的线程无法变成守护线程,而是用户线程。
当 JVM 中不存在任何一个正在运行的非守护线程时,则 JVM 进程即会退出。守护线程拥有自动结束自己生命周期的特性,而非守护线程不具备这个特点。
- 临界资源和临界区
临界资源:共享资源,可以被多个进程或线程访问。当一个时刻只能被一个进程或者线程访问。否则将会对资源的值造成影响。属于临界资源的硬件有打印机、磁带机等;软件资源有消息队列、变量、数组、缓冲区等。
临界区:访问临界资源的代码块。
- 死锁
多个线程相互持有对方所等待的资源而造成的阻塞。
死锁有四个必要条件:互斥条件、请求和保持条件、不可剥夺条件、环路等待条件。除了互持条件不可破坏,破除其它三个条件中任意一个都可以解决死锁。
产生死锁的原因:资源竞争或者进程(线程)推进顺序错误。
- 互斥 同步 通信
进程(线程)竞争资源时要实施互斥,互斥是一种特殊的同步,而进程(线程)同步(推进顺序)是一种进程通信。
- 线程安全
多线程的环境下,多次运行,其结果是不变的,或者说其结果是可预知的。
-
共享变量的安全访问
-
当多个线程访问某个方法时,不管你通过怎样的调用方式或者说这些线程如何交替的执行,我们在主程序中不需要去做任何的同步,这个类的结果行为都是我们设想的正确行为。
JVM
线程数量 = (机器本身可用内存 - JVM分配的堆内存) / Xss的值
JVM命令
JVM参数
JVM体系
Java内存划分
对于每一个线程来说,栈都是私有的,而堆是共有的。也就是说在栈中的变量(局部变量、方法定义参数、异常处理器参数)不会在线程之间共享,也就不会有内存可见性的问题,也不受内存模型的影响。而在堆中的变量是共享的,称为共享变量。内存可见性是针对共享变量的。
JMM
Java 虚拟机规范定义了 Java 内存模型来屏蔽掉各种硬件和操作系统的内存差异,达到跨平台的内存访问效果。 为了获得更好的执行性能,Java 内存模型没有限制执行引擎使用处理器的特定缓存器或缓存来和主内存(可以和 RAM类比,但是是虚拟机内存的一部分)交互,工作内存(可类比高速缓存,也是虚拟机内存的一部分)为线程私有。 工作内存和主内存的划分和 Java 堆,栈,方法区的划分不同,两者基本没有关系,如果勉强对应,则主内存可理 解为堆中实例数据部分,工作内存则对应栈中部分区域[1]。
并发模型
并发模型用户解决线程如何通信以及线程如何同步。目前有两种并发模型:消息传递并发模型、共享内存并发模型。
Java采用的是共享内存的并发模型。即在在线程通信时,线程之间共享程序的公共状态,通过读写公共状态来进行隐式的通信。在线程同步时,必须显示指定某段代码需要在线程之间互斥执行。同步是显示的。
内存可见性与JMM
内存可见性是指一个线程对主内存(堆)的修改能够被其它线程知道。
为什么堆是共享的,在堆中会有内存不可见问题?
线程之间的共享变量存在主内存中,每个线程都有一个私有的本地内存,存储了该线程以读、写共享变量的副本。本地内存是Java内存模型的一个抽象概念,并不真实存在。它涵盖了缓存、写缓冲区、寄存器等。
JMM,即Java内存模型,是对一组规则和规范的描述。它是抽象概念。隶属于JVM。
JMM对同步的规则如下:
-
线程在解锁前,必须把共享的变量立刻刷新回主存!
-
线程在加锁前,必须读取主存中最新的值到工作内存(线程有自己的工作内存)中!
-
加锁和解锁是同一把锁!
JMM三大特性如下:
-
可见性:指的是线程之间的可见性,当一个线程修改了共享变量时,另一个线程可以读取到这个修改后的值。volatile、synchronized、final都可以解决可见性问题
-
原子性(操作不可中断)。Java中提供了两个高级指令 monitorenter和 monitorexit,也就是对应的synchronized同步锁来保证原子性。
-
有序性(happens-before)。synchronized和volatile可以保证多线程之间操作的有序性,volatile会禁止指令重排序
线程对共享变量的所有操作都必须在自己的本地内存中进行,不能直接从主内存中读取。JMM通过控制主内存与每个线程的本地内存之间的交互,来提供内存可见性保证。
重排序与happens-before
计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排。指令重排就是减少中断的一种技术,使得指令更好的进行流水线作业。
指令重排序分为以下三种:
-
编译器优化重排
-
指令并行重排
-
内存系统重排
指令重排可以保证串行语义一致,但是没有义务保证多线程间的语义也一致。所以在多线程下,由于编译器优化重排的存在,多个线程使用的变量能否保证一致性是不确定的,结果无法预测。
为了平衡重排带来的影响和JMM这一强大模型,JMM提供了happens-before规则。
JMM使用happens-before的概念来定制两个操作之间的执行顺序。这两个操作可以在一个线程以内,也可以是不同的线程之间。因此,JMM可以通过happens-before关系向程序员提供跨线程的内存可见性保证。
happens-before关系的定义如下: 1. 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。 2. 两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么JMM也允许这样的重排序。
as-if-serial语义保证单线程内重排序后的执行结果和程序代码本身应有的结果是一致的,happens-before关系保证正确同步的多线程程序的执行结果不被重排序改变。
Java多线程
注意Java提供的多线程机制与方法的应用场景!!!
volatile
volatitle是一种轻量级锁。它保证了JMM中所描述的可见性和顺序性。但它不并保证原子性。
保证可见性
public class VolatileTest {
public static void main(String[] args) throws InterruptedException {
System.out.println("可见性测试");
MyData myData = new MyData(); // 资源类
//AA线程
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t come in");
try {
TimeUnit.SECONDS.sleep(3); // 三秒种后把值改为60
myData.addTo60();
} catch (Exception e) {
}
}, "AAA").start();
// main线程
while (myData.number == 0) {
// main线程持有共享数据的拷贝,一直为0
// AA线程队变量的修改 main线程不可见
// System.out.println(myData.number); // 不要用System.out.println调试
}
System.out.println(
Thread.currentThread().getName() + "\t mission is over. main get number value: " + myData.number);
}
}
【慎用System.out.println】
public void println(int x) {
synchronized (this) {
print(x);
newLine();
}
}
查看字节码,当被volatile修饰的关键字,会多一个ACC_VOLATILE的字段访问标致。当volatile变量修饰的共享变量进行写操作的时候,JVM就会向处理器发送一条Lock前缀的指令,然后将这个变量所在缓存行的数据写回到系统内存,为了保证各个处理器的缓存是一致,这时候就会实现缓存一致性协议。
【为什么ACC_VOLATILE和ACC_FINAL不能修饰同一字段】
保证顺序性
// 多线程下flag=true可能先执行,还没走到a=1就被挂起。其它线程进入method02的判断,修改a的值=5,而不是6。
public void method01() {
a = 1;
flag = true;
}
public void method02() {
if (flag) {
a += 5;
System.out.println("*****retValue: " + a);
}
}
编译器采用JMM内存屏障插入策略来限制volatile变量的重排序,保证在任何处理器平台,任何程序中都能得到正确的volatile内存语义。
不保证原子性
for (int i = 0; i < 20; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
test.addPlusPlus();
}
},String.valueOf(i)).start();
}
public void addPlusPlus(){
a++; // a = a + 1
}
i++在多线程下存在写覆盖。假设有3个线程,分别执行number++,都先从主内存中拿到最开始的值,number=0,然后三个线程分别进行操作。假设线程0执行完毕,number=1,也立刻通知到了其它线程,但是此时线程1、2已经拿到了number=0,所以结果就是写覆盖,线程1、2将number变成1。
i++;
getfield // i的值进栈
iconst_1 // 1进栈
iadd // 栈顶两元素相加
putfield // 结果再给i
解决volatile不保证原子性
-
synchronized
-
原子类
volatile场景
通常是,存在一个或者多个共享变量,会有线程对他们写操作,也会有其它线程对他们读操作。这样的变量都应该使用volatile修饰。如果不符合一下两条规则,仍然需要通过加锁来保证原子性[1]。
-
运算结果并不依赖于变量的当前值
-
变量不需要其它的状态变量共同参与不变约束
线程安全的单例模式
/**
* 双检锁/双重校验锁(DCL,即 double-checked locking)
*
*/
public class SingletonPattermDemo05 {
private volatile static SingletonPattermDemo05 instance; // !!volatile修饰 因为 new 实际上分为好几步
private SingletonPattermDemo05() {
}
public static SingletonPattermDemo05 getInstance() {
if (instance == null) { // 先判断一次 看是否需要加锁
synchronized (SingletonPattermDemo05.class) {
if (instance == null) { // 再判断一次 看实例是否需要创建
/**
* 对象的实例化,并不是一步完成的,而是分为多步:
*
* 1、申请内存空间,并且默认初始化(赋0值,半初始化)
*
* 2、调用构造方法,进行初始化
*
* 3、引用指向分配的地址空间
*
* 如果不用volatile 2、3可能重排序
*/
instance = new SingletonPattermDemo05();
}
}
}
return instance;
}
public static void main(String[] args) {
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + ":" + SingletonPattermDemo05.getInstance());
}, String.valueOf(i)).start();
}
}
}
volatile修饰数组
volatile修饰对象和数组能保证其内部元素的可见性?[8]
答案暂不确定。不过数组有可能是大对象,如果在主存与本地内存之间拷贝更新值应该比较消耗资源。
后续看一下ConcurrentHashMap中是如何操作的
/**
* The array of bins. Lazily initialized upon first insertion.
* Size is always a power of two. Accessed directly by iterators.
*/
transient volatile Node<K,V>[] table;
/**
* The next table to use; non-null only while resizing.
*/
private transient volatile Node<K,V>[] nextTable;
violatile修饰对象的引用
被volatile关键字修饰的对象作为类变量或实例变量时,其对象中携带的类变量和实例变量也相当于被volatile关键字修饰了。
violatile实现原理
violatile实现原理可参考博文
CAS
CAS理论
Compare And Swap,比较并替换,是一种乐观锁。在CAS机制中有三个值:内存地址A,旧的期望值E,拟修改的新值U。更新一个变量的时候,只有当变量的预期值E和内存地址A当中的实际值相同时,才会将内存地址V对应的值修改为U。
AtomicInteger atomicInteger = new AtomicInteger(10);
System.out.println(atomicInteger.compareAndSet(10, 2020) +"\t current data : "+ atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(10, 2021)+"\t current data : "+ atomicInteger.get());
//true current data : 2020
//false current data : 2020
CAS原理
CAS操作基本是由Unsafe工具类的compareAndSwapXXX来实现的。
CAS优缺点
当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。主要有以下三个问题:
-
ABA问题:一个值原来是A,变成了B,又变回了A。这个时候使用CAS是检查不出变化的,但实际上却被更新了两次。解决方案:在变量前面追加上版本号或者时间戳(AtomicStampedReference类)。
-
CPU开销大:CAS多与自旋结合。如果自旋CAS长时间不成功,会占用大量的CPU资源。解决方案:让JVM支持处理器提供的pause指令。pause指令能让自旋失败时cpu睡眠一小段时间再继续自旋,从而使得读操作的频率低很多,为解决内存顺序冲突而导致的CPU流水线重排的代价也会小很多。
-
只能保证一个共享变量的原子操作。解决方案:1、synchronized;2、原子引用,把多个变量放到一个对象里面进行CAS操作。
CAS应用场景
JDK1.6之后的synchronized通过CAS进行了优化。CAS适用于线程冲突较少的情况下使用,而synchronized适合线程冲突较多的情况下使用
原子引用
AtomicReference和AtomicInteger非常类似,不同之处就在于AtomicInteger是对整数的封装,底层采用的是compareAndSwapInt实现CAS,比较的是数值是否相等,而AtomicReference则对应普通的对象引用,底层使用的是compareAndSwapObject实现CAS,比较的是两个对象的地址是否相等。也就是它可以保证你在修改对象引用时的线程安全性。AtomicReference可用于对多个变量进行CAS操作。
User user1 = new User("Jack",25);
User user2 = new User("Lucy",21);
AtomicReference<User> atomicReference = new AtomicReference<>();
atomicReference.set(user1);
System.out.println(atomicReference.compareAndSet(user1,user2)); // true
System.out.println(atomicReference.compareAndSet(user1,user2)); //false
AQS
AQS是AbstractQueuedSynchronizer抽象类的简称,即抽象队列同步器。AQS内置了自旋锁实现(CAS)和同步队列(LockSupport,构建同步组件的基础工具,帮AQS完成相应线程的阻塞或者唤醒的工作。双端队列,头尾都可以进行添加删除操作),封装了入队和出队的操作,提供了独占、共享、中断等方法。
AQS内部使用了一个volatile的变量state来作为资源的标识。该属性的值即表示了锁的状态,state为0表示锁没有被占用,state大于0表示当前已经有线程持有该锁,这里之所以说大于0而不说等于1是因为可能存在可重入的情况。同时定义了几个获取和修改state的final方法。
// The synchronization state.
private volatile int state;
// 这些方法都是原子操作
getState()
setState()
compareAndSetState()
同步器自身没有实现任何同步接口,它仅仅是定义了若干同步状态获取和释放的方法来供自定义同步组件使用,同步器既可以支持独占式(ReentrantLock)地获取同步状态(资源),也可以支持共享式地(Semaphore,ReentrantReadWriteLock,SynchronousQueue)获取同步状态,这样就可以方便实现不同类型的同步组件(ReentrantLock,Semaphore,ReentrantReadWriteLock,SynchronousQueue,FutureTask,ReadWriteLock)[2]。
【同步器与锁的关系】 同步器是实现锁(也可以是任意同步组件)的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义。可以这样理解二者之间的关系:锁是面向使用者的,它定义了使用者与锁交互的接口(比如可以允许两个线程并行访问),隐藏了实现细节;同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。锁和同步器很好地隔离了使用者和实现者所需关注的领域[2]。
AQS队列结点的定义:
/**
* AbstractQueuedSynchronizer类的静态内部类
* 资源共享模式的定义(队列中的Node)
* 通过Node可以实现两个队列 一个借助prev和next线程同步队列(双向队列),
* 一个是借助nextWaiter实现的等待队列(单向队列 等待条件)
*/
static final class Node {
/** 共享模式 */
static final Node SHARED = new Node();
/** 独享模式 */
static final Node EXCLUSIVE = null;
/** timeout or interrupt 之后的该线程结点状态 */
static final int CANCELLED = 1;
/** 表示后继结点需要被唤醒 */
static final int SIGNAL = -1;
/** 表示该结点(对应的线程)在等待某一条件 */
static final int CONDITION = -2;
/**
* waitStatus value to indicate the next acquireShared should
* unconditionally propagate
* 表示有资源可用,新head结点需要继续唤醒后继结点(共享模式下,多线程并发释放资源,而head唤醒其后继结点后,需要把多出来的资源留给后面的结点;设置新的head结点时,会继续唤醒其后继结点)
*/
static final int PROPAGATE = -3;
volatile int waitStatus;
volatile Node prev;
volatile Node next;
/**
* 该节点对应的线程. Initialized on
* construction and nulled out after use.
*/
volatile Thread thread;
/**
* Link to next node waiting on condition, or the special
* value SHARED. Because condition queues are accessed only
* when holding in exclusive mode, we just need a simple
* linked queue to hold nodes while they are waiting on
* conditions. They are then transferred to the queue to
* re-acquire. And because conditions can only be exclusive,
* we save a field by using special value to indicate shared
* mode.
*/
Node nextWaiter;
/**
* 判断共享模式的方法
* Returns true if node is waiting in shared mode.
*/
final boolean isShared() {
return nextWaiter == SHARED;
}
/**
* Returns previous node, or throws NullPointerException if null.
* Use when predecessor cannot be null. The null check could
* be elided, but is present to help the VM.
*/
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() { // Used to establish initial head or SHARED marker
}
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
AQS采用模板方法模式定义了一些子类必须重写的方法(主要是获取资源和释放资源的方法)以及实现了一些不可修改的模板模板方法(对队列的操作逻辑)。通过继承并根据需求重写那些必须重写的方法就可以方便实现上面提到的不同类型的同步组件。这样子类只需要关心资源怎么处理,而无需关心队列怎么操作。
比如获取资源的一个模板方法acquire(int arg)。子类就去重写tryAcquire(arg)方法(独占方式获取资源)就行。
// 子类可以重写的方法
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
// 模板方法
public final void acquire(int arg) {
// 如果获取资源失败,即tryAcquire(arg)返回flase,!!tryAcquire(arg) 为true,&&后面的方法会执行 将当前线程加入到等待队列尾部中,然后再等待队列头部拿一个正在等待的线程,之后再中断当前线程
// 如果获取资源成功,即tryAcquire(arg)返回true,!!tryAcquire(arg) 为false,&&后面的方法不会执行
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
private Node addWaiter(Node mode) {
// 生成该线程对应的Node节点
Node node = new Node(Thread.currentThread(), mode);
// 将Node插入队列中
Node pred = tail;
if (pred != null) {
node.prev = pred;
// 使用CAS尝试,如果成功就返回
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 如果等待队列为空或者上述CAS失败,再自旋CAS插入
enq(node);
return node;
}
// 自旋CAS插入等待队列
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
同步队列与等待队列
Thread类
创建线程相关的四个参数
-
ThreadGroup g(线程组):有利于对线程进行统一管理。
-
Runnable target (Runnable 对象)
-
String name (线程的名字)
-
long stackSize (为线程分配的栈的大小,若为0则表示忽略这个参数):0 表示使用系统默认。
重要方法
run()
run()方法是一个普通的对象方法,因此,不需要线程调用start()后才可以调用的。可以线程对象可以随时随地调用run方法。此时多个线程调run()方法的执行顺序是固定的。
start()
处于NEW状态的线程对象调用start(),它内部使用了native方法来启动线程,它将导致一个新的线程被创建出来,线程状态变为RUNNABLE,当线程获得处理机之后,执行run()方法。
sleep()
使得当前线程让出CPU。但是, 当前线程仍然持有它所获得的监视器锁, 这与同时让出CPU资源和监视器锁资源的Object的wait方法是不一样的。
yield()
它对于CPU只是一个建议, 告诉CPU, 当前线程愿意让出CPU给其他线程使用, 至于CPU采不采纳, 取决于不同厂商的行为。所以调用yield方法不会使线程退出RUNNANLE状态,顶多会使线程从running 变成 ready, 但是sleep方法是有可能将线程状态转换成TIMED_WAITING的。
join()
在当前线程中调用其它线程的join()方法,会使当前线程阻塞,知道其它线成执行完。
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()-> {
System.out.println("线程一");
});
Thread t2 = new Thread(()-> {
System.out.println("线程二");
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("主线程");
}
/**
* 线程一
* 线程二
* 主线程
* 一二行的位置可能不一样,当第三行一定在前两行后执行
*/
interrupted()
// ClearInterrupted参数来决定要不要重置中断标志位
private native boolean isInterrupted(boolean ClearInterrupted);
public boolean isInterrupted() {
// ClearInterrupted参数为false, 说明它仅仅返回线程实例的中断状态,但是不会对现有的中断状态做任何改变
return isInterrupted(false);
}
// 在返回线程中断状态的同时,重置了中断标识位为false
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
以上方法不会引起InterruptedException。在线程处于“waiting, sleeping”甚至是正在运行的过程中,如果被中断了,才会抛出InterruptedException。
所谓“中断一个线程”,其实并不是让一个线程停止运行,仅仅是将线程的中断标志设为true, 或者在某些特定情况下抛出一个InterruptedException,它并不会直接将一个线程停掉,在被中断的线程的角度看来,仅仅是自己的中断标志位被设为true了,或者自己所执行的代码中抛出了一个InterruptedException异常,仅此而已。
线程状态
阅读Thread源码,在Thread类中, 线程状态是通过threadStatus属性以及State枚举类实现的。Thread.State源码中定义了线程的六种状态。
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
- NEW:新创建了一个线程对象。处于NEW状态的线程此时尚未启动。这里的尚未启动指的是还没调用Thread实例的start()方法。
【注】同一个线程不能反复调用其start方法。也不能在一个线程执行完毕(此时处于TERMINATED状态),再次调用这个线程的start()方法。在调用一次start()之后,threadStatus的值会改变(threadStatus !=0),此时再次调用start()方法会抛出IllegalThreadStateException异常。即只能在NEW状态下调用start()方法。
/* Java thread status for tools,
* initialized to indicate thread 'not yet started'
*/
private volatile int threadStatus = 0;
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}
/**
* Returns the state of this thread.
* This method is designed for use in monitoring of the system state,
* not for synchronization control.
*
* @return this thread's state.
* @since 1.5
*/
public State getState() {
// get current thread state
return sun.misc.VM.toThreadState(threadStatus);
}
public static State toThreadState(int var0) {
if ((var0 & 4) != 0) { // threadStatus==4 RUNNABLE
return State.RUNNABLE;
} else if ((var0 & 1024) != 0) { // threadStatus==1024 BLOCKED
return State.BLOCKED;
} else if ((var0 & 16) != 0) { // threadStatus==16 WAITING
return State.WAITING;
} else if ((var0 & 32) != 0) { //threadStatus==32 TIMED_WAITING
return State.TIMED_WAITING;
} else if ((var0 & 2) != 0) { //threadStatus==2 TERMINATED
return State.TERMINATED;
} else {
// 如果threadStatus==0 0&1==0 所以此方法返回 State.NEW
// 如果threadStatus!=0(可能等于2、4、16、32、1024) 0&1==0 所以此方法返回 State.RUNNABLE 此索引值为1
return (var0 & 1) == 0 ? State.NEW : State.RUNNABLE; // threadStatus==0 NEW
}
}
-
RUNNABLE:线程被创建后,其他线程(比如main线程)调用了该对象的的start()方法,该状态的线程处于可运行。Java线程的RUNNABLE状态其实是包括了传统操作系统线程的就绪和运行两个状态的。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.
-
BLOCKED:Thread state for a thread blocked waiting for a monitor lock. 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 Object.wait!.
【注】 Thread.sleep(long millis)与Object.wait()的区别:
sleep()不会释放线程当前已经获取的锁(这里更加准确的描述是monitor),待指定时间过去后,线程继续执行;wait()会释放已经获取的锁(monitor),待其他线程调用对象的notify()后,会重新去获取锁,然后才能继续执行。调用wait()和notify()的前提都是当前对象要拥有锁,所以wait()和notify()必须放在synchronized中(详见Object类注释)。
condition类(Lock接口的实现类产生),提供await()/signal()方法,完成跟对象的wait()/notify()方法一样的功能(synchronized中)。
【注】为什么多线程中的判断都是循环?线程被唤起后能够继续检查条件是否满足,避免虚假唤起
- WAITING:A thread is in the waiting state due to calling one of the following methods: Object.wait with no timeout, Thread.join with no timeout, LockSupport.park. A thread in the waiting state is waiting for another thread to perform a particular action。就是其请求方法所对应的方法。
【注意】BLOCKED和WAITING的区别
个人理解:处于WAITING状态下的线程没有机会获得锁(等待队列),处于BLOCKED状态下的线程是有机会获得锁的(同步队列)。如果没有线程处于BLOCKED状态,那么处于WAITING状态下的线程可以直接转换成RUNNABLE状态。
“阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态,但是阻塞在 java.concurrent包中Lock接口的线程状态却是等待状态,因为java.concurrent包中Lock接口对于阻塞的实现均使用了LockSupport类中的相关方法”——《Java并发编程的艺术》
在《码出高效中》将WAITING状态、BLOCKED状态、TIMED_WAITING合并为BLOCKED状态:可能是同步阻塞(锁被占用),可能是主动阻塞(Thread的join方法 sleep方法),也可能是等待阻塞(Object的wait方法)。其次这本书中分开定义了就绪和运行两个状态。这种更加类似于操作系统中的定义。实际上在Thread的注释中也明确写了“These states are virtual machine states which do not reflect any operating system thread states.”
-
TIMED_WAITING:TIMED_WAITING设定了一个等待时间,如果超过了等待时间,处于TIMED_WAITING状态的线程会转变为BLOCKED状态去争夺锁。
-
TERMINATED
实现多线程的四种方法
在Java中创建线程有且只有一种方式:创建一个Thread类实例,并调用它的start方法。而实现多线程一般有四种方法
-
继承Thread类,重写run方法
-
实现Runnable接口
-
实现Callable接口
使用Callable+FutureTask获取执行结果
- 线程池
【继承Thread类与实现Runnable接口的区别】 由于Java“单继承,多实现”的特性,Runnable接口使用起来比Thread更灵活。Runnable接口出现更符合面向对象,将线程单独进行对象的封装。Runnable接口出现,降低了线程对象和线程任务的耦合性。如果使用线程时不需要使用Thread类的诸多方法,显然使用Runnable接口更为轻量。
【Callable,Runnable的区别及用法】
https://blog.csdn.net/qq_27258799/article/details/51451143 https://blog.csdn.net/zhangzhaokun/article/details/6615454
线程的基本方法
Thread类中的方法
终止线程的四种方法
-
正常退出
-
标志位
-
使用中断
@Override
public void run() {
try {
// 1. isInterrupted() 用于终止一个正在运行的线程。
while (!isInterrupted()) {
// 执行任务...
}
} catch (InterruptedException ie) {
// 2. InterruptedException异常用于终止一个处于阻塞状态的线程
}
}
Java没有提供一种安全直接的方法来停止某个线程,但是提供了中断机制。对于被中断的线程,中断只是一个建议,至于收到这个建议后线程要采取什么措施,完全由线程自己决定。中断机制的核心在于中断状态和InterruptedException异常。
Thread.interrupt()
Java线程的通信方式
锁
互斥机制。结合等待通知机制可以实现同步。
Java多线程的锁都是基于对象的,Java中的每一个对象都可以作为一个锁。
乐观锁:CAS 悲观锁:synchronized
可重入锁:某个线程已经获得某个锁,可以再次获取锁而不会出现死锁。synchronized、ReentrantLock 不可重入锁:
公平锁:获取锁采用先来先服务。ReentrantLock
非公平锁:锁空闲时,即使是后来的线程也可以去竞争锁资源。ReentrantLock
读锁(共享锁)/写锁(独享锁)
读写锁/互斥锁
自旋锁:指当一个线程尝试获取某个锁时,如果该锁已被其他线程占用,就一直循环检测锁(很消耗CPU资源)是否被释放,而不是进入线程挂起或睡眠状态。非自旋锁在获取不到锁的时候会进入阻塞状态,从而进入内核态,当获取到锁的时候需要从内核态恢复,需要线程上下文切换。自旋锁适用于锁保护的临界区很小的情况,临界区很小的话,锁占用的时间就很短。CAS失败后通常会采用自旋。
无锁/偏向锁/轻量级锁/重量级锁
分段锁
这些锁仅仅表明一种性质。
synchornized
synchornized保证了JMM的三个特性,即原子性,可见性,顺序性。它保证同一时刻只能由一个线程执行被synchornized包括的方法块或者代码块。但要注意在其方法块或者代码块中的指令可能被重排序。
// 关键字在代码块上,锁为括号里面的对象
public void blockLock01() {
Object o = new Object();
synchronized (o) {
// code
}
}
// 关键字在代码块上,锁为括号里面的对象
public void blockLock02() {
synchronized (this.getClass()) {
// code
}
}
// 关键字在实例方法上,锁为当前实例
public synchronized void instanceLock() {
// code
}
// 关键字在静态方法上,锁为当前Class对象
public static synchronized void classLock() {
// code
}
从字节码分析要分为代码块和方法。方法元信息中使用ACC_SYNCHRONIZED标识该方法是一个同步方法。同步方法的常量池中会有一个ACC_SYNCHRONIZED标志。当调用一个设置了ACC_SYNCHRONIZED标志的方法,执行线程需要先获monitor锁,然后开始执行方法,方法执行之后再释放monitor锁。同步代码块中也是使用monitorenter和monitorexit两个字节码指令获取和释放monitor。
//javac -encoding utf-8 XXX.java
//javap -c XXX.class
public void blockLock01();
Code:
0: new #2 // class java/lang/Object
3: dup
4: invokespecial #1 // Method java/lang/Object."<init>":()V
7: astore_1
8: aload_1
9: dup
10: astore_2
11: monitorenter
12: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
15: ldc #4 // String 同步代码块
17: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
20: aload_2
21: monitorexit
22: goto 30
25: astore_3
26: aload_2
27: monitorexit
28: aload_3
29: athrow
30: return
Exception table:
from to target type
12 22 25 any
25 28 25 any
【注】monitorenter之后会有两个monitorexit。一个用于正常退出,一个用户异常退出。
JDK1.6之前synchornized是一种重量级锁。依靠操作系统底层的互斥量实现。
JDK1.6之后为synchornized提供了三种锁实现:偏向锁、轻量级锁、重量级锁。
- 每一个线程在准备获取共享资源时首先检查MarkWord里面是不是放的自己的ThreadId(或者MarkWord中ThreadId为空),如果是,表示当前线程是处于 “偏向锁” 。下一次该线程取锁时,会判断锁对象头中的ThreadId是否和当前线程的ThreadId一致。如果一致,该线程不再重复获得锁。
- 如果不一致,锁升级(轻量级锁),这时候,用CAS来执行切换,新的线程根据MarkWord里面现有的ThreadId,通知之前线程暂停,之前线程将Markword的内容置为空。
- 两个线程都把锁对象的HashCode复制到自己新建的用于存储锁的记录空间,接着开始通过CAS操作, 把锁对象的MarKword的内容修改为自己新建的记录空间的地址的方式竞争MarkWord。
- 第三步中成功执行CAS的获得资源,失败的则进入自旋。
- 自旋的线程在自旋过程中,成功获得资源(即之前获的资源的线程执行完成并释放了共享资源),则整个状态依然处于 轻量级锁的状态,如果自旋失败,进入重量级锁的状态,这个时候,自旋的线程进行阻塞,等待之前线程执行完成并唤醒自己。
应用场景
Synchronized关键字会让没有得到锁资源的线程进入BLOCKED状态,而后在争夺到锁资源后恢复为RUNNABLE状态,这个过程中涉及到操作系统用户模式和内核模式的转换,代价比较高。
Lock
在Lock接口出现之前,Java程序是靠synchronized关键字实现锁功能的。
【注】Lock与synchronized的异同
Lock和synchronized都是Java中实现的锁机制。
synchronized是Java的关键字,是在JVM层面实现。而Lock是在API层面实现的锁。
使用synchronized不需要手动去释放锁,它也不能被中断,除非抛出异常或者正常退出。而Lock需要手动释放,可以被中断。
Lock既可以是公平锁,也可以是非公平锁。默认是非公平锁。
Lock可以绑定多个condition,有选择性的唤起线程。
见LockCondition.java
ReentrentLock
ReentrentLock类实现了Lock接口,并利用了一个继承子自AQS内部类Sync的实例实现了ReentrentLock类的各个方法。
ReadWriteLock
StampedLock
LockSupport
LockSupport类是Java 6 引入的一个工具类,提供了基本的线程同步原语。LockSupport实际上是调用了Unsafe类里的函数,归结到Unsafe里,只有两个函数:
-
park(boolean isAbsolute, long time):阻塞当前线程。
-
unpark(Thread jthread):使给定的线程停止阻塞。
锁优化技术
-
自旋
-
锁消除
-
锁粗化
-
轻量级锁
等待/通知机制
信号量
Semaphore
CountDownLatch
CyclicBarrier
以上三个都是工具类 使用要看应用场景
管道
阻塞队列
基于BlockingQueue和ReentrantLock可以实现具体的条件等待。BlockingQueue一般用于生产者-消费者模式,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。BlockingQueue就是存放元素的容器。阻塞队列的原理是利用了Lock锁的多条件(Condition)阻塞控制。
public interface BlockingQueue<E> extends Queue<E> {
//将给定元素设置到队列中,如果设置成功返回true, 否则返回false。如果是往限定了长度的队列中设置值,推荐使用offer()方法。
boolean add(E e);
//将给定的元素设置到队列中,如果设置成功返回true, 否则返回false. e的值不能为空,否则抛出空指针异常。
boolean offer(E e);
//将元素设置到队列中,如果队列中没有多余的空间,该方法会一直阻塞,直到队列中有多余的空间。
void put(E e) throws InterruptedException;
//将给定元素在给定的时间内设置到队列中,如果设置成功返回true, 否则返回false.
boolean offer(E e, long timeout, TimeUnit unit)
throws InterruptedException;
//从队列中获取值,如果队列中没有值,线程会一直阻塞,直到队列中有值,并且该方法取得了该值。
E take() throws InterruptedException;
//在给定的时间里,从队列中获取值,时间到了直接调用普通的poll方法,为null则直接返回null。
E poll(long timeout, TimeUnit unit)
throws InterruptedException;
//获取队列中剩余的空间。
int remainingCapacity();
//从队列中移除指定的值。
boolean remove(Object o);
//判断队列中是否拥有该值。
public boolean contains(Object o);
//将队列中值,全部移除,并发设置到给定的集合中。
int drainTo(Collection<? super E> c);
//指定最多数量限制将队列中值,全部移除,并发设置到给定的集合中。
int drainTo(Collection<? super E> c, int maxElements);
}
ArrayBlockingQueue
由数组结构组成的有界阻塞队列。内部结构是数组,故具有数组的特性。可以初始化队列大小, 且一旦初始化不能改变。构造方法中的fair表示控制对象的内部锁是否采用公平锁,默认是非公平锁。
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == items.length)
notFull.await();
enqueue(e);
} finally {
lock.unlock();
}
}
private E dequeue() {
// assert lock.getHoldCount() == 1;
// assert items[takeIndex] != null;
final Object[] items = this.items;
@SuppressWarnings("unchecked")
E x = (E) items[takeIndex];
items[takeIndex] = null;
if (++takeIndex == items.length)
takeIndex = 0;
count--;
if (itrs != null)
itrs.elementDequeued();
notFull.signal();
return x;
}
put的流程:
-
所有执行put操作的线程竞争lock锁,拿到了lock锁的线程进入下一步,没有拿到lock锁的线程自旋竞争锁。
-
判断阻塞队列是否满了,如果满了,则调用await方法阻塞这个线程,并标记为notFull(生产者)线程,同时释放lock锁,等待被消费者线程唤醒。
-
如果没有满,则调用enqueue方法将元素put进阻塞队列。注意这一步的线程还有一种情况是第二步中阻塞的线程被唤醒且又拿到了lock锁的线程。
-
唤醒一个标记为notEmpty(消费者)的线程。
LinkedBlockingQueue
由链表结构组成的有界阻塞队列。内部结构是链表,具有链表的特性。默认队列的大小是Integer.MAX_VALUE,也可以指定大小。此队列按照先进先出的原则对元素进行排序。
DelayQueue
DelayQueue是一个没有大小限制的队列,因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞。
PriorityBlockingQueue
基于优先级的无界阻塞队列(优先级的判断通过构造函数传入的Compator对象来决定),内部控制线程同步的锁采用的是公平锁。
SynchronousQueue
这个队列比较特殊,没有任何内部容量,甚至连一个队列的容量都没有。并且每个 put 必须等待一个 take,反之亦然。
生产者消费者模式的实现
传统的等待通知机制方式实现
class ShareData { // 资源类
private int number = 0;
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
// 多线程判断用while
public void increase() throws Exception {
lock.lock();
try {
// 判断 多线程判断用while
while (number != 0) {
// 有资源 等待
condition.await();
}
// 干活
number++;
System.out.println(Thread.currentThread().getName() + "\t" + number);
// 通知
condition.signalAll();
} finally {
lock.unlock();
}
}
public void decrease() throws Exception {
lock.lock();
try {
// 判断 多线程判断用while
while (number == 0) {
// 有资源 等待
condition.await();
}
// 干活
number--;
System.out.println(Thread.currentThread().getName() + "\t" + number);
// 通知
condition.signalAll();
} finally {
lock.unlock();
}
}
}
/**
*
* 一个初始为零的变量 两个线程对其交替操作 一个加1一个减1
*
* 线程 操作 资源类 判断 干活 通知
*/
public class ProducerConsumer_TraditionDemo {
public static void main(String[] args) {
ShareData shareData = new ShareData();
new Thread(()->{
for (int i = 0; i < 5; i++) {
try {
shareData.increase();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
},"AA").start();
new Thread(()->{
for (int i = 0; i < 5; i++) {
try {
shareData.decrease();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
},"BB").start();
}
}
利用同步队列实现
线程池
线程池的好处
-
创建/销毁线程需要消耗系统资源,线程池可以复用已创建的线程。
-
控制并发的数量。并发数量过多,可能会导致资源消耗过多,从而造成服务器崩溃。(主要原因)
-
可以对线程做统一管理。
构造函数的参数
ThreadPoolExecutor类提供了四个构造器,最多可以有七个参数。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler){……}
-
corePoolSize:该线程池中核心线程数最大值。
-
maximumPoolSize:该线程池中线程总数的最大值,该值等于核心线程数加上非核心线程数。
-
keepAliveTime和unit:设置处于闲置状态的非核心线程的生存时间。
-
workQueue:在线程池中活跃线程数达到corePoolSize时,线程池将会将后续的task提交到workQueue中。
【在线程池中活跃线程数达到corePoolSize时,线程池将会将后续的task提交到workQueue中,而不是创建非核心线程去执行task】
线程池创建线程需要获取mainlock这个全局锁,影响并发效率,阻塞队列可以很好的缓冲。
-
threadFactory:创建线程的工厂 ,用于批量创建线程,统一在创建线程时设置一些参数,如是否守护线程、线程的优先级等。如果不指定,会新建一个默认的线程工厂。
-
handler:默认拒绝处理策略是丢弃任务并抛出RejectedExecutionException异常。
拒绝服务策略
-
ThreadPoolExecutor.AbortPolicy:默认拒绝处理策略,丢弃任务并抛出RejectedExecutionException异常。
-
ThreadPoolExecutor.DiscardPolicy:丢弃新来的任务,但是不抛出异常。
-
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列头部(最旧的)的任务,然后重新尝试执行程序(如果再次失败,重复此过程)。
-
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务。
线程池处理流程
// JDK 1.8
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
// 1.当前线程数小于corePoolSize,则调用addWorker创建核心线程(需要获取mainlock这个全局锁)执行任务
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
// 2.如果不小于corePoolSize,则将任务添加到workQueue队列。
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
// 2.1 如果isRunning返回false(状态检查),则remove这个任务,然后执行拒绝策略。
if (! isRunning(recheck) && remove(command))
reject(command);
// 2.2 线程池处于running状态,但是没有线程,则创建线程
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
// 3.如果放入workQueue失败,则创建(需要获取mainlock这个全局锁)非核心线程执行任务,
// 如果这时创建非核心线程失败(当前线程总数不小于maximumPoolSize时),就会执行拒绝策略。
else if (!addWorker(command, false))
reject(command);
}
线程池的状态
-
RUNNING
-
SHUTDOWN
-
STOP
-
TERMINATED
线程复用原理
-
当Thread的run方法执行完一个任务之后,会循环地从阻塞队列中取任务来执行,这样执行完一个任务之后就不会立即销毁了;
-
当工作线程数小于核心线程数,那些空闲的核心线程再去队列取任务的时候,如果队列中的Runnable数量为0,就会阻塞当前线程,这样线程就不会回收了。
Executors
newCachedThreadPool
只会创建非核心线程,适合执行很多短任务。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
newFixedThreadPool
只会创建核心线程。
newSingleThreadExecutor
所有任务按照先来先执行的顺序执行。
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
newScheduledThreadPool
创建一个定长线程池,支持定时及周期性任务执行。
创建线程池
对于CPU密集型任务,最大线程数是CPU线程数+1。对于IO密集型任务,尽量多配点,可以是CPU线程数*2,或者CPU线程数/(1-阻塞系数)。 13-创建线程池.png
submit与execute
ThreadLocal
线程安全的集合
CopyOnWriteArrayList
适用于读多写少的情况。写的时候加锁赋值合并,读的时候不加锁。CopyOnWriteArrayList并不是完全意义上的线程安全,如果涉及到remove操作,一定要谨慎处理。
CopyOnWriteSet
用CopyOnWriteArrayList实现。
ConcurrentHashMap
参考与引用
[1] 深入理解Java虚拟机
[2] Java并发编程的艺术
[3] 码出高效 Java开发手册
[4]阿里巴巴Java开发手册终极版v1.3.0
[5] 深入浅出Java多线程
[6] Java大厂面试题第二季 周阳