1、线程与进程
进程: 操作系统会以进程为单位,分配系统资源(CPU时间片、内存等资源),进程是资源分配的最小单位。
线程: 有时候被称为轻量级进程,是操作系统调度(CPU调度)执行的最小单位,即程序执行的最小单位。
如Tomcat是一个进程,其中维护着一些线程,可以处理用户请求。Tomcat这个进程所获得的时间片,其中的线程也可以获得,即线程可以共享进程获得的资源。
2、线程上下文切换
线程的上下文切换巧妙的利用了时间片轮转的方式,CPU给每个任务都服务一定的时间,然后将当前任务的状态保存下来,在加载下一任务的状态后,继续服务下一个任务。线程状态的保存和再加载,就是线程的上下文切换。
时间片轮询保证CPU的利用率。
上下文: 是在某一时间CPU寄存器和程序计数器中的内容。程序计数器会记录线程当前执行的状态,以便之后恢复现场的时候使用。这里可以联想JVM中的程序计数器。
上下文切换的活动:
- 挂起一个线程,将这个进程在CPU中的状态存储于内存中的某处;
- 在内存中检索下一个进程的山下文并将其CPU的寄存器恢复;
- 跳转到程序计数器所指定的位置。
其实我们可以发现,线程上下文的切换和JVM中程序计数器记录当前线程执行的字节码行号的模式是一模一样的。
这就是线程上下文切换所带来的性能损耗。当然如果要更详细的解释线程上下文的切换所带来的损耗,还可以从线程的内核态和用户态来讲。只有内核态的线程才能获取CPU时间片,而Java创建出来的线程,在调用native方法start0()之后会调用JVM底层的C++中的线程创建方法,然后再有C++调用底层操作系统的内核库pthread库中的create方法,从而创建出内核线程,然后会将Java中的用户态线程和内核态线程进行关联。
而用户态到内核态之间的切换,是很耗费性能的!所以我们在实际使用中,才会有线程池和线程复用。以为线程的创建是很耗费资源和性能的。
注意:
1、JVM不具有线程调度的能力,最终还是要使用操作系统中的内核库来创建线程!
2、Java线程是内核级线程,是重量级线程!
3、JVM线程调度
(1)这里穿插一个小问题,在Java中,线程创建的方式有几种?
很多人都会不约而同的回答三种,new Thread、runnable和callable。
其实,在Java中,线程创建的方式,本质上只有一种,那就是new Thread()。至于其他的runnable、callable和线程池方式,都是线程运行的方式。(2)不知道大家有没有这样的疑问,Java中的Thread只是一个类,而我们new Thread不应该是new出来一个对象吗,为什么可以创建出一个真实的线程呢?
JVM线程调度: 依赖JVM内部实现,只要是Native thread scheduling.
思考:线程执行为什么不能直接调用run()方法,而要调用start()方法?
1、Java中调用start方法,然后会调用native方法start0。而Thread中存在一个静态调用,会将Java中的静态方法和JVM中的native方法进行映射关联。比如start0方法会调用JVM中的JVM_StartThread方法。
public
class Thread implements Runnable {
/* Make sure registerNatives is the first thing <clinit> does. */
private static native void registerNatives();
static {
registerNatives();
...
Ps:JVM底层的映射关系如下:
所以当调用start0方法的时候,会调用JVM底层的JVM_StartThread方法。
2、在调用JVM中的StartThread方法之后,会创建真正的Java Thread。即调用C++方法:
native_thread = new JavaThread(&thread entry, sz);
之后会调用OS::Create thread方法去真正的创建线程。最终会调用库函数pthread方法来创建线程,即调用操作系统的方法来创建Java线程对应的内核线程,真正创建一个线程。而创建真正的线程是比较耗费时间和资源的。
3、创建之后会将Java中的线程和JVM中的线程进行绑定。
4、绑定之后开始执行所创建的内核线程,执行thread entry()方法。这时候会执行JVM底层的Thread::start(native thread) 方法,然后再调用os::start 方法,最终回去调用操作系统中的os::pd start thread方法去启动线程,设置线程的状态为RUNNABLE,之后回去唤醒线程。
5、唤醒线程之后就回去调用run方法。
我们进入正题,下面是JVM中的线程调度。
JVM线程调度: 依赖JVM内部实现,主要是Native thread scheduling, 是依赖操作系统的。Java也不能完全是跨平台独立的,对线程调度处理非常敏感的业务开发必须关注底层操作系统的线程调度差异。
用户线程: 指不需要内核支持而在用户程序中实现的线程,其不依赖于操作系统核心,应用进程利用线程库提供创建、同步、调度和管理线程的函数来控制用户线程。不需要用户态/核心态切换,速度快,操作系统内核不知道多线程的存在,因此一个线程阻塞将使得整个进程(包括它的所有线程)阻塞。由于这里的处理器时间片分配是以进程为基本单位,所以每个线程执行的时间相对减少。
内核线程: 由操作系统内核创建和撤销。内核维护进程及线程的上下文信息以及线程切换。一个内核线程由于I/O操作而阻塞,不会影响其它线程的运行。Windows NT和2000/XP支持内核线程。
4、Java线程与内核线程关系
- 内核中的线程才能获取CPU时间片的执行权。
- 正在运行在CPU上的是内核线程。
- Java本身的线程是操作不了时间片的。
- 通过映射关系建立了线程之间的绑定。
5、Java线程的生命周期
在Thread这个类中,其实是有定义一个枚举类来描述Java的线程周期的。
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 <tt>Object.wait()</tt>
* on an object is waiting for another thread to call
* <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
* that object. A thread that has called <tt>Thread.join()</tt>
* 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;
}
注意,线程的阻塞状态实际上只针对Synchronized锁有效。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. 翻译过来就是:处于阻塞状态的线程正在等待监视器锁进入同步块/方法,或者在调用后重新进入同步块/方法 当我们调用join()、park()、wait()方法,这时候的线程其实是等待状态,只不过我们平时习惯叫这种状态为阻塞。但事实上它和线程中的阻塞庄状态是有一定的区别的。
在操作系统的线程状态中的Ready和Running两种状态对应Java线程中的Runnable状态。
Ready是指线程没有获取到时间片,而Running值线程获取到了时间片。
yield()方法指的是释放当前线程拥有的时间片,使得线程从Running 状态切换到Ready装填。实际使用中不建议使用yield()方法,因为有的线程优先级比较高,在释放时间片的瞬间它又会马上获取到时间片。
6、线程安全问题
在了解了线程相关的基础之后,我们应该考虑到线程的安全问题了。
多线程编程中,有可能出现多个线程同时访问一个共享、可变资源的情况;这种资源可能是共享变量、文件等。由于线程执行的过程是不可控的,容易产生线程安全问题。
共享: 资源可以由多个线程同时访问;
可变: 资源可以在其生命周期内被修改。
如果解决线程安全问题?
所有的并发模式在解决线程安全问题时,采用的方案都是序列化访问临界资源。加锁的本质是让线程串行化的访问资源!
当多个线程同时访问共享资源时,获取到CPU时间片的锁开始操作资源,未获取到的进入到阻塞队列中排队。当新来的线程插队去获取资源,这就叫非公平锁。如果新来的资源进入队列等待,叫做公平锁。
7、Java锁体系
8、Synchronized和Lock
8.1、Synchronized加锁方法
8.2、Synchronized原理
互斥性: Synchronized修饰的代码块、实例方法、静态方法,多线程并发访问时,真能有一个线程获取到锁,其他线程都处于阻塞状态。
可见性: 某线程A对于进入同步代码块之前或在Synchronized中对于共享变量的操作,对于后续的只有同一个监视器锁的其他线程可见。
重量级锁底层原理:
- 同步方法和同步代码块底层都是通过monitor来实现同步的。每个对象都与一个monitor相关联。
- 同步方法是通过方法中的access_flags中设置 ACC_SYNCHRONIZED 标志来实现的; 同步代码块是通过 monitorenter和monitorexit来实现的。 两个指令的执行是JVM通过调用操作系统的互斥语句mutext来实现的。被阻塞的线程会被挂起,等待重新调度,会导致“用户态和内核态”两个态之间的来回切换,对性能有很大的影响。
下面我们在具体的代码中来观察Synchronized。
我们都知道,在Java中的Object类中有一个方法wait(),当一个对象调用object.wait()方法之后,就会释放锁。也就是说如果在一个synchronized代码块中当调用了加锁对象的wait()方法之后,当前加锁对象拥有的锁就会被释放,其他的线程就会进入到同步块中来。
而Thread类中的sleep方法不会释放锁资源。
注意,wait()和sleep()方法都会释放时间片,区别就是wait()会释放锁资源,而sleep()不会。
在操作系统层面有一个monitor机制,Java也采用了这种机制,并在Object中进行了实现。可以说每一个Java对象都是一个monitor的实现。即每一个Java对象都对应一个monitor。
package com.jihu.test.thread;
public class Test1 {
public static int sum = 0;
public static Object o = "";
public static void main(String[] args) {
add();
}
// 1、方法上 ACC_SYNCHROPNIZED
public synchronized static void add() {
sum++;
}
}
下面我们来看字节码文件:注意先build之后,我们在使用javap - v Test1.class命令来查看。
public static synchronized void add();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=0, args_size=0
0: getstatic #3 // Field sum:I
3: iconst_1
4: iadd
5: putstatic #3 // Field sum:I
8: return
LineNumberTable:
line 14: 0
line 15: 8
我们可以看到,如果我们在一个方法上面加了Synchronized关键字,那么在字节码文件中,方法的flags标签中会多出一个 ACC_SYNCHRONIZED.
如果我们把Synchronized加到对象上,即使用同步代码块的方式呢?
public static void add() {
Class var0 = Test1.class;
synchronized(Test1.class) {
++sum;
}
}
我们来查看字节码文件:
public static void add();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=0
0: ldc #3 // class com/jihu/test/thread/Test1
2: dup
3: astore_0
4: monitorenter //
5: getstatic #4 // Field sum:I
8: iconst_1 // 执行++操作
9: iadd
10: putstatic #4 // Field sum:I
13: aload_0
14: monitorexit
15: goto 23
18: astore_1
19: aload_0
20: monitorexit // 为什么这里还有一个?
21: aload_1
22: athrow
23: return
可以发现,当使用同步代码块的时候,方法的flags中并没有多出 ACC_SYNCHRONIZED. 但是在同步代码块的前后分别多出了 一个monitorenter和两个monitorexit.
这里大家可能会有疑问,为什么会在同步块后又两个monitorexit呢?
正常逻辑下,在进入同步块之前会生成一个monitorenter,结束之后会生成一个monitorexit。我们看字节码文件中的第13行,有一个“goto 23”,即跳转到23行,也就是return。而22行有一个athrow指令,也就是 Synchronized天然的帮助我们考虑了异常异常情况,即在执行同步代码块的时候如果发生了异常,锁会自动释放。Synchronized天生的就帮助我们控制好了加锁和解锁的步骤,并且考虑到了异常情况。Synchronized就是依靠这种monitor机制来实现的。
而monitor机制是依靠操作系统底层的 metux 来实现的,同样是pthread库提供的,pthread_mutex_lock/unlock来实现加锁和解锁的操作。这样的话,就会存在用户态到内核态之间的不断切换。即Synchronized每次的加锁和解锁都需要切换到内核态来进行。这里就会存在严重的性能问题。
我们可以这样理解,Java中的线程其实并不具备加锁、解锁的能力,最终都需要依靠绑定的内核态线程去调用操作系统中的pthread库函数来实现真正意义上线程的加锁和解锁。
.
下面分享一个小的知识点,我们一般都理解Synchronized只能在同一个方法中加锁,即它不可以跨两个方法,比如在A方法中加锁,然后我在B方法中解锁,通常的情况下是不能实现的。
但其实我们可以调用Synchronized的实现方法来做到这一点。
// Unsafe类只能使用反射来创建对象...
public class Test1 {
public static int sum = 0;
public static Object o = "";
public static Unsafe unsafe;
static {
// 第一种方式:通过构造器获取Unsafe实例
Class<Unsafe> unsafeClass = Unsafe.class;
Constructor<Unsafe> declaredConstructor = null;
try {
declaredConstructor = unsafeClass.getDeclaredConstructor();
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
// 默认的构造方法是private的
declaredConstructor.setAccessible(true);
try {
unsafe = declaredConstructor.newInstance();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
for (int i = 0; i < 1000; i ++) {
add();
add2();
System.out.println(sum);
}
}
public static void add() {
unsafe.monitorEnter(o);
sum++;
}
public static void add2() {
sum++;
unsafe.monitorExit(o);
}
}
Unsafe类功能很强大,可以直接调用JVM的一些接口,如果使用不当,会出现问题,说以后不推荐我们使用。
9、Monitor机制
Java.lang.Object类定义了wait()、notify()、notifyAll()方法,这些方法的具体实现,依赖一个叫ObjectMonitor模式的实现,这是JVM内部基于C++实现的一套机制。
monitor的机制中,monitor Object充当着维护mutex以及wait/singnal API来管理线程的阻塞和唤醒的角色。任何一个Java对象都可以作为monitor机制的monitor object。
新进入的线程和entryList中的线程都会进程锁,所以Synchronized是一个非公平锁。
- owner:持有ObjectMonitor对象的线程;
- waitSet:存放于wait状态的线程队列;
- entryList:存放处于block状态的线程。
获取锁过程的的JDK源码如下:
在monitor_enter过程中,首先会通过CAS操作尝试吧monitor的_owner字段设置为当前线程。
如果_owner字段为NULL的话,就会将NULL替换为当前线程。
如果_owner字段中线程为当前线程,说明当前线程已经持有锁,此次为重入,_recursions自增。从这里也可以看出,Synchronized是一把可重入锁。
Synchronized是一把重量级锁,加锁、解锁的时候线程必须有用户态切换到内核态,调用操作系统层面的库函数才能实现,这个过程是非常耗时和耗费性能的。这时候如果JVM想优化Synchronized,自然是首先会想到能不能从用户态层面去控制?
那么,我们有没有一种办法,可以不需要切换到内核态,在用户态层面就可以设置呢?
对于线程来说,如何识别有没有加锁呢?只需要一个标识就可以了。如果某一个线程想要竞争资源,这样就不需要去切换到内核态去判断了,只需要根据用户态中的这个标识就可以知道。
而Synchronized锁的是对象,这样我们自然会尝试考虑能不能在对象上有一个标识,来表示是否加锁。这时候我们自然的就会想到对象的组成部分,这时候就需要对对象的内存结构有一些了解。
new Object()这个对象在内存中占多少个字节?在64位操作系统中占16个字节。64位操作系统中,对内存的操作必须是8的整数倍。markwork占8个字节,类型指针占8个字节,开启指针压缩有占4个字节,实例数据中没有数据则为0,对其填充指的是需要将对象占用的内存填充到8的倍数大小。如现在的new Object(),内存大小为8+4=12, 需要填充4个字节成为8的倍数,即8+4+4=16 .
注意,数组对象中还多了一个数组长度部分,占4个字节。
普通对象的布局有markword、类型指针、实例数据和对其填充构成。而数组对象中多了一个数据长度。
其中,对象头包括markword和类型指针,类型指针指的是指向当前对象的一个指针。
- 类型指针部分,也叫元数据指针,主要是存储元数据的地址,对于对象的类型信息,指向方法区的类信息部分,对于对象的成员变量部分,基本类型就指向方法区的运行时常量池,String类型指向在jdk1.7之后从方法区移到堆区的字符串常量池,其他的对象类型,则指向堆区的对象存储地址。占八字节内存,jvm有默认开启指针压缩,因为发现类型指针部分用不了64位那么多,所以被指针压缩后,成为了四字节,指针压缩的实现原理是存储的时候后4位抹0,使用的时候后四位加0。
我们运行实际的代码来看看一个对象的具体大小。
这里的ClassLayout类需要使用jol 工具,可以看这篇文章下载使用java 查看class markword,JOL工具,ClassLayout,openjdk
然后我们加上Synchronized之后再看看对象头:
我么可以发现对象头中的第一行中的VALUE发生了改变,有01 00 00 00 变成了18 f4 1e 03 .
所以这也就验证了,Synchronized可以操作对象头,以达到优化的目的。这样就可以在用户态中判断对象有没有加锁。而不需要转化到内核态。。
注意,这里对象头的标志会变化,当调用hashcode()方法的时候,又会改变。
10、对象内存结构和锁膨胀过程
Java对象内存结构中的markword中可以存储Epoch、Thread ID、对象分代年龄、偏向状态以及锁标志状态等。
- age: 垃圾回收中有一个分代年龄的概念,为什么一个对象的轻GC只能有15次呢?因为age的存储占4个字节,最小为0 0 0 0,最大为1
1 1 1,即0~15。 - biased_lock: 偏向锁,占1个字节;
- lock: 锁,占2个字节.
对象头中总共有3个字节可以标志锁状态。所以Synchronized中存在偏向锁、轻量级锁和重量级锁的优化。
- 偏向锁:存储线程ID,偏向锁标志位为1,锁标志位为01;
- 轻量级锁:指向线程栈中锁记录的指针;每一个线程在执行的时候都有一个线程栈,会开辟一块内存空间,空间中存储local record。对象头中的轻量级锁会指向栈空间的local record。而local record中也有一个owner,这个owner指向对象头markword。
- 如果竞争依然激烈,需要使用重量级锁,指向重量级锁Monitor的指针,依赖Mutex操作系统的互斥。
注意:
偏向锁是无法自己撤销的,只能等待另一个线程过来。没有竞争就是偏向锁,当有另一个线程过来的时候,会尝试自旋获取锁。如果不能获取锁的话,偏向锁会被撤销,升级为轻量级锁。如果还是不能获取锁,这时候会升级为重量级锁。
JDK1.6之后Synchronized默认是优化过的。
无锁->偏向锁->轻量级锁逻辑:
- 刚开始,线程1访问对象的markWord,首先会检查是否是偏向锁;
- 如果存在偏向锁,会检查ThreadId是否是线程1,如果不是,可能为NULL,就会通过CAS操作修改markWord;
- 此时对象头中的ThreadId会指向线程1,然后执行同步代码块;
- 然后线程2启动也去访问同步代码块,检查ThreadId是否是线程2;如果不是的话,会尝试CAS修改ThreadId,但是此时会修改失败。因为期待的值应该是NULL但实际上值是Thread
1; - 此时CAS修改失就会撤销偏向锁;注意,偏向锁不会主动释放,当有其他线程发生竞争的时候才会释放。撤销时候必须在安全点进行,会Stop
The World.; - 之后会检查线程1是否退出了同步块,如果执行完成了,会解锁,将偏向锁ID设置为NULL,偏向锁状态设置为0,这样其他线程就可以进入同步代码块,产生新的偏向锁。如果线程1没有退出同步块,会升级为轻量级锁。
- 在一个安全点停止用有锁的线程;
- 遍历线程池,如果存在锁记录的话,需要修改锁记录和Markword,使其变为无锁状态;
- 环境当前线程,将当前锁升级为轻量级锁。
- 线程在自己的栈帧中创建锁记录LockRecord;
- 将所对象中的对象头中的MarkWord复制到刚刚创建的锁记录中;
- 将锁记录中的Owner指针指向锁对象的MarkWord;
- 将锁对象的对象头的MarkWord替换为锁记录的指针。
轻量级锁->重量级锁逻辑:
- 一开始就有两个线程竞争同步块;
- 这两个线程都会在自己的栈上分配内存空间,拷贝Mark Word到Lock Record中;
- 然后这两个线程都会CAS去修改对象的Mark Word,这时候只有一个线程会修改成功;
- 修改成功的线程1会获取到锁,会将对象头中的锁记录指针指向线程栈中的Lock Record,升级为轻量级锁并去执行同步代码块;
- 而修改失败的线程2会进行自适应自旋(一开始是自旋,可以使用JVM参数 -XX:PreBlockSpin 去修改默认的自旋次数,默认为10次;后来优化为了自适应自旋,会根据上一次自旋成功的次数去判断这一次自旋是否会成功,如果上次成功,会增加自旋次数,上次失败,会减少自旋次数;),
- 如果线程2自旋到一定次数后依然没有成功,轻量级锁则会膨胀为重量级锁,然后线程会发生阻塞!
- 而竞争到锁的线程1会CAS修改Mark Word,如果修改成功,则释放锁;因为这里已经升级为了重量级锁,所以Mark Word中的数据会被替换,所以一定会修改失败! 修改失败,则释放锁,唤醒阻塞的线程,开始新一轮的竞争。
这里的优化点在于,没有竞争到锁的线程并没有直接进入阻塞状态,而是进行自旋! 没有进入阻塞状态为什么就算优化呢?可能我们会有这样的疑问和迷惑……
我们来分析,如果线程进入到阻塞状态,那么必然涉及到用户态到内核态的转化,以及需要保护现场那和恢复现场,这一系列的操作正是线程上下文的切换所带来的性能损耗。这里使用自旋来占着CPU时间片的方式,来避免阻塞。但是长期自旋占据时间片这样也不好,所以会有自适应自旋的优化。
偏向锁存在性能问题!
轻量级锁适合同步块中执行速度快的!
重量级锁的撤销,会在GC的时候查看当前锁对象,除了垃圾线程还有没有其他线程,如果没有,则直接降级。我们可以思考,大量请求导致一个锁在升级成重量级锁之后,如果之后只有一个线程进来,不可能此时还是重量级锁吧?所以,必然有一个降级的过程。
因为BasicLocking的实现优先于重量级锁的使用,JVM会尝试在SWT的停顿中对处于“空闲(idle)”状态的重量级锁进行降级(deflate)。这个降级过程是如何实现的呢?我们知道在STW时,所有的Java线程都会暂停在“安全点(SafePoint)”,此时VMThread通过对所有Monitor的遍历,或者通过对所有依赖于MonitorInUseLists值的当前正在“使用”中的Monitor子序列进行遍历,从而得到哪些未被使用的“Monitor”作为降级对象。
可以降级的Monitor对象:
重量级锁的降级发生于STW阶段,降级对象就是那些仅仅能被VMThread访问而没有其他JavaThread访问Monitor对象。