最近在准备面试,把知识点复习一遍,整理出的笔记记录下,里面会穿插代码和面试例题。
内容不是原创,是总结和收集,并在理解的基础上进行一些完善,如果侵权了请联系作者,若有错误也请各位指正。因为收集的时候忘记把来源记录下来了,所以就不po出处了,请见谅(这是个坏习惯,一定改)。
本章为上篇,将分为上下两篇。上篇涉及Synchronized的实现原理,以及锁升级的介绍。
JUC指的是java.util 下几个包的简称,涉及多线程开发的相关操作。
我们看一下Thread.State的源码中的枚举,线程的生命周期内有以下6种状态,并将其注释翻译出来:
public enum State {
/**
* Thread state for a thread which has not yet started.
*/
NEW, //新生状态。 使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。是线程还未调用start()方法之前的状态。
/**
* 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, //运行状态。 运行的线程状态。线程正在Java虚拟机中执行(Running),但它可能正在等待来自操作系统(如处理器)的其他资源(Ready)。包含了Ready和Running两种状态。
/**
* 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, //阻塞状态。 是正在等待锁监视器的状态。等待进入synchronized块/方法;再次进入synchronized块/方法后等待响应;调用了Object.wait()方法。
/**
* Thread state for a waiting thread.
* A thread is in the waiting state due to calling one of the
* following methods:
*
* - {@link Object#wait() Object.wait} with no timeout
* - {@link #join() Thread.join} with no timeout
* - {@link LockSupport#park() LockSupport.park}
*
*
* A thread in the waiting state is waiting for another thread to
* perform a particular action.
*
* For example, a thread that has called Object.wait()
* on an object is waiting for another thread to call
* Object.notify() or Object.notifyAll() on
* that object. A thread that has called Thread.join()
* is waiting for a specified thread to terminate.
*/
WAITING, //等待状态。 线程处于无限期等待状态中。由于调用以下方法之一,线程处于等待状态:Object.wait(),没有超时时间,无限期;Thread.join(),没有超时时间,无限期;LockSupport#park()。
/**
* 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:
*
* - {@link #sleep Thread.sleep}
* - {@link Object#wait(long) Object.wait} with timeout
* - {@link #join(long) Thread.join} with timeout
* - {@link LockSupport#parkNanos LockSupport.parkNanos}
* - {@link LockSupport#parkUntil LockSupport.parkUntil}
*
*/
TIMED_WAITING, //超时等待。 线程在特定的时长内等待。同样是以下方法之一导致:Thread.sleep(long)/Object.wait(long)\Thread.join(long)\LockSupport.parkNanos()\LockSupport.parkUntil()。
/**
* Thread state for a terminated thread.
* The thread has completed execution.
*/
TERMINATED;//终止状态。 线程已经完成操作。
}
使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。是线程还未调用start()方法之前的状态。
调用start()方法之后线程处于可运行状态的线程了。Runnable表示运行的线程状态,它可能正在Java虚拟机中执行,它也可能正在等待来自操作系统(如处理器)的其他资源。即包含了就绪(Ready)和运行(Running)两种状态。
是正在等待锁监视器的状态。等待进入synchronized块/方法;再次进入synchronized块/方法后等待响应;调用了Object.wait()方法。
线程处于无限期等待状态中。由于调用以下方法之一,线程处于等待状态:Object.wait(),没有超时时间,无限期;Thread.join(),没有超时时间,无限期;LockSupport#park()。
wait()与wait(0)同义,意思是无限期等待;sleep(0)的意思是不等待,等待时间为0。
线程在特定的时长内等待。同样是以下方法之一导致:Thread.sleep(long)、Object.wait(long)、Thread.join(long)、LockSupport.parkNanos()、LockSupport.parkUntil()。
线程已经完成操作。已经死亡的线程不能使用start()重新唤醒,死亡等于结束了。线程会以如下3种方式结束,结束后就处于死亡状态:
wait()需要先获取对象的monitor锁,才能调用,否则报出异常java.lang.IllegalMonitorStateException,这也是wait需要在同步代码块中使用的原因。常用写法:
synchronized(obj){
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
线程安全是多线程编程时的计算机程序代码中的一个概念。在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。
简单来说:如果你的代码在多线程下执行和在单线程下执行永远都能获得一样的结果,那么你的代码就是线程安全的。
线程安全在三个方面体现:
像String、Integer、Long这些,都是final类型的类,任何一个线程都改变不了它们的值,要改变除非新创建一个,因此这些不可变对象不需要任何同步手段就可以直接在多线程环境下使用
不管运行时环境如何,调用者都不需要额外的同步措施。要做到这一点通常需要付出许多额外的代价,Java中标注自己是线程安全的类,实际上绝大多数都不是线程安全的,不过绝对线程安全的类,Java中也有,比方说CopyOnWriteArrayList、CopyOnWriteArraySet
相对线程安全也就是我们通常意义上所说的线程安全,像Vector这种,add、remove方法都是原子操作,不会被打断,但也仅限于此,如果有个线程在遍历某个Vector、有个线程同时在add这个Vector,99%的情况下都会出现ConcurrentModificationException,也就是fail-fast(快速失败)机制。
ArrayList、LinkedList、HashMap等都是线程非安全的类,在多线程下操作往往因为并发导致输出容量少于实际容量,或者因为扩容的重复导致越界异常的现象。
线程Thread 类是一个单独的资源类,内含属性和方法,可直接使用。Thread 类是函数式接口,使用时将资源类放入线程即可。其构造方法中实现了Runnable接口,可以将Runnable当做资源类放入。
public class Test {
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
public void run() {
System.out.println("多线程任务执行!");
}
});
t.start();
//Java8新特性 Lambda表达式
new Thread(()->{System.out.println("多线程任务执行!");},"t").start();
}
}
多线程中的并发:多线程操作同一个资源类。
一般是以下两种情况导致线程不安全:一是资源类中的数据共享(临界资源),二是多线程同时访问并改变数据。
传统的线程安全方式是用synchronized关键字,synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:
public class AccountingSync implements Runnable{
//共享资源(临界资源)
static int i=0;
/**
* synchronized 修饰实例方法
*/
public synchronized void increase(){
i++;
}
@Override
public void run() {
for(int j=0;j<1000000;j++){
increase();
}
}
public static void main(String[] args) throws InterruptedException {
AccountingSync instance=new AccountingSync();
Thread t1=new Thread(instance);
Thread t2=new Thread(instance);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
//输出为:2000000,如果没有synchronized关键字,很可能输出小于2000000
注意:
synchronized
方法。static synchronized
方法时,线程B也可以同时访问实例对象的static synchronized
方法,因为前者获取的是实例对象的监视器锁,而后者获取的是类对象的监视器锁,两者不存在互斥关系。要了解Synchronized的实现原理,要先介绍下对象头。
首先,我们要知道Java对象在内存中的布局:
已知对象是存放在堆内存中的,对象大致可以分为三个部分,分别是对象头、实例变量和填充字节。
Synchronized不论是修饰方法还是代码块,都是通过持有修饰对象的锁来实现同步,Synchronized锁对象存在锁对象的对象头的MarkWord中。
在32位的虚拟机中MarkWord的结构:
在64位的虚拟机中MarkWord的结构:
图中的偏向锁和轻量级锁都是在java6以后对锁机制进行优化时引进的,下文的锁升级部分会具体讲解,Synchronized关键字对应的是重量级锁,接下来对重量级锁在Hotspot JVM中的实现锁讲解。
Synchronized重量级锁对应的锁标志位是10,存储了指向重量级监视器锁的指针,在Hotspot中,对象的监视器(monitor)锁对象由ObjectMonitor对象实现(C++),其跟同步相关的数据结构如下:
ObjectMonitor() {
_count = 0; //用来记录该对象被线程获取锁的次数
_waiters = 0;
_recursions = 0; //锁的重入次数
_owner = NULL; //指向持有ObjectMonitor对象的线程
_WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet
_WaitSetLock = 0 ;
_EntryList = NULL ; //处于等待锁block状态的线程,会被加入到该列表
}
结合线程状态来说明一下Synchronized的实现原理。
在JVM规范里可以看到,不管是方法同步还是代码块同步都是基于进入和退出monitor对象来实现,然而二者在具体实现上又存在很大的区别。
Synchronized代码块同步在需要同步的代码块开始的位置插入monitorentry指令,在同步结束的位置或者异常出现的位置插入monitorexit指令;JVM要保证monitorentry和monitorexit都是成对出现的,任何对象都有一个monitor与之对应,当这个对象的monitor被持有以后,它将处于锁定状态。
Synchronized方法同步不再是通过插入monitorentry和monitorexit指令实现,而是由方法调用指令来读取运行时常量池中的ACC_SYNCHRONIZED标志隐式实现的。
如果方法表结构(method_info Structure)中的ACC_SYNCHRONIZED标志被设置,那么线程在执行方法前会先去获取对象的monitor对象,如果获取成功则执行方法代码,执行完毕后释放monitor对象,如果monitor对象已经被其它线程获取,那么当前线程被阻塞。
简单来说就是有Synchronized关键字修饰的方法,在其方法表结构中设置了ACC_SYNCHRONIZED标志,用来标识需要调用指令获取监视器(monitor)锁。方法执行完毕之后就释放监视器(monitor)锁。
Java SE 1.6为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”,在Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。
HotSpot的作者大量的研究发现,大多数时候是不存在锁竞争的,常常是一个线程多次获得同一个锁,因此如果每次都要竞争锁会增大很多没有必要付出的代价,为了降低获取锁的代价,才引入的偏向锁。
当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS(CompareAndSet 比较并交换)操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。
偏向锁的取消:
偏向锁是默认开启的,而且开始时间一般是比应用程序启动慢几秒,如果不想有这个延迟,那么可以使用-XX:BiasedLockingStartUpDelay=0;
如果不想要偏向锁,那么可以通过-XX:-UseBiasedLocking = false来设置。
偏向锁的释放采用了一种只有竞争才会释放锁的机制,线程是不会主动去释放偏向锁,就算线程执行完毕也不会释放锁,需要等待其他线程来竞争。
偏向锁的撤销需要等待全局安全点(这个时间点是上没有正在执行的代码)。其步骤如下:
轻量级锁考虑的是竞争锁对象的线程不多,而且线程持有锁的时间也不长的情景。因为阻塞线程需要CPU从用户态转到内核态,代价较大,如果刚刚阻塞不久这个锁就被释放了,那这个代价就有点得不偿失了,因此这个时候就干脆不阻塞这个线程,让它自旋这等待锁释放。
重量级锁通过对象内部的监视器(monitor)实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。重量级锁就是Synchronized锁。实现原理是mutex(互斥),任一时刻,只能有一个线程访问该对象。
注意:为了避免无用的自旋,轻量级锁一旦膨胀为重量级锁就不会再降级为轻量级锁了;偏向锁升级为轻量级锁也不能再降级为偏向锁。一句话就是锁可以升级不可以降级,但是偏向锁状态可以被重置为无锁状态。
当有线程A在执行时,B线程来竞争锁,CAS竞争锁失败,证明当前存在多线程竞争情况,将偏向锁升级为轻量锁。
当线程B竞争后已经升级为轻量锁的情况下,B线程不停的自旋等待锁释放,自旋次数超过定义次数,或者此时又有已升级为轻量锁的线程C同时来竞争,则升级为重量级锁(自旋超限、或轻量级锁中竞争线程较多),重量级锁把除了拥有锁的线程都阻塞,防止CPU空转。
JDK 1.6 引入了更加聪明的自旋锁,即自适应自旋锁。所谓自适应就意味着自旋的次数不再是固定的,它由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。
线程如果自旋成功了,那么下次自旋的次数会更多,因为虚拟机认为既然上次成功了,那么此次自旋也很有可能会再次成功,那么它就会允许自旋等待持续的次数更多。反之,如果对于某个锁,很少有自旋能够成功的,那么在以后要或者这个锁的时候自旋的次数会减少甚至省略掉自旋过程,以免浪费处理器资源。
有了自适应自旋锁,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测会越来越准确,虚拟机会变得越来越聪明。
锁粗化就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁,避免频繁的加锁解锁操作。
按理来说,同步块的作用范围应该尽可能小,仅在共享数据的实际作用域中才进行同步,这样做的目的是为了使需要同步的操作数量尽可能缩小,缩短阻塞时间,如果存在锁竞争,那么等待锁的线程也能尽快拿到锁。
但是加锁解锁也需要消耗资源,如果存在一系列的连续加锁解锁操作,可能会导致不必要的性能损耗。因此有了锁粗化。
Java虚拟机在JIT编译时通过对运行上下文的扫描,经过逃逸分析,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间。
为了提高热点代码的执行效率,在运行时会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,这就是即时编译。
Lock 是JDK1.5以后才出现的具体的类。使用lock是调用对应的API。是API层面的锁。
Lock接口处于java.util.concurrent.locks包中,属于JUC范围内的一个接口。使用lock锁操作多线程,比synchronized 拥有更广泛的锁定操作,并且可以监控锁的状态,操作更加灵活。
它的实现类有ReentrantLock,ReentrantReadWriteLock.ReadLock,ReentrantReadWriteLock.WriteLock
。我们常用的是可重入锁ReentrantLock
。
介绍可重入锁之前,需要知道几个概念:
ReentrantLock
默认的类型是非公平锁。非公平锁性能高于公平锁性能。首先,在恢复一个被挂起的线程与该线程真正运行之间存在着严重的延迟。而且,非公平锁能更充分的利用cpu的时间片,尽量的减少cpu空闲的状态时间。
使用Lock锁操作:
//使用Lock锁操作: 1、获取锁对象Lock lock = new Lock();
//2、上锁 lock.lock(); 3、try\catch代码块中执行真正的操作
//4、finally 中释放锁 lock.unlock();
public class MyFairLock {
/**
* true 表示 ReentrantLock 的公平锁
* false 表示非公平锁,默认为false
*/
private ReentrantLock lock = new ReentrantLock(true);
lock.lock();
public void lockPrint(){
try {
System.out.println(Thread.currentThread().getName() +"获得了锁");
}finally {
lock.unlock();
}
}
public static void main(String[] args) {
MyFairLock fairLock = new MyFairLock();
Thread[] threadArray = new Thread[10];
for (int i=0; i<10; i++) {
threadArray[i] = new Thread(() -> {
System.out.println(Thread.currentThread().getName()+"已启动");
fairLock.lockPrint();
},i).start();
}
}
}
任意一个Java对象,都拥有一组监视器方法(定义在Object类中),Condition类是线程中的监视器类,Condition接口也提供了类似的Object的监视器方法,与Lock配合可以线程的精准唤醒/等待。它也是java.util.concurrent.locks包下的。
一个Condition类就对应了一个监视器。使用Lock类与Condition类可以实现指定唤醒,而Synchronized使用notify/notifyAll只能随机唤醒。
下面使用Condition类实现一个按顺序操作的线程:
//使用Condition类控制线程唤醒的顺序
public class ConditionTest {
public static void main(String[] args){
ThreadTest threadTest = new ThreadTest();
new Thread(()->{
for(int i = 0; i <5; i++){
threadTest.printA();
}
},"A").start();;
new Thread(()->{
for(int i = 0; i <5; i++){
threadTest.printB();
}
},"B").start();;
new Thread(()->{
for(int i = 0; i <5; i++){
threadTest.printC();
}
},"C").start();;
}
}
class ThreadTest{
private ReentrantLock lock = new ReentrantLock();
private Condition condA = lock.newCondition();
private Condition condB = lock.newCondition();
private Condition condC = lock.newCondition();
private int count = 0;//用来标记唤醒哪个线程 0:A 1:B 2:C
public void printA(){
lock.lock();
try {
while(count != 0){
condA.await();
}
System.out.println(Thread.currentThread().getName() + "-printA");
//将标记置为1 唤醒B
count = 1;
condB.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printB(){
lock.lock();
try {
while(count != 1){
condB.await();
}
System.out.println(Thread.currentThread().getName() + "-printB");
//将标记置为2 唤醒C
count = 2;
condC.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printC(){
lock.lock();
try {
while(count != 2){
condC.await();
}
System.out.println(Thread.currentThread().getName() + "-printC");
//将标记置为0 唤醒A
count = 0;
condA.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
//输出: A B C A B C ...
lock()方法是平常使用得最多的一个方法,就是用来获取锁。Lock锁不会自动释放锁,需要使用unlock()方法释放锁。一般lock()与unlock()都是成对出现的,出现的次数也必需相同,否则就会进入死锁状态。
tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true;如果获取失败(即锁已被其他线程获取),则返回false,也就是说,这个方法无论如何都会立即返回(在拿不到锁时不会一直在那等待)。
tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false,同时可以响应中断。如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。
lockInterruptibly()方法比较特殊,当通过这个方法去获取锁时,如果线程 正在等待获取锁,则这个线程能够 响应中断,即中断线程的等待状态。例如,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。
返回绑定到此 Lock 实例的新 Condition 实例。
查询此锁是否由任意线程保持。用于判断是否已上锁。