Java多线程并发面试知识点梳理

1. 什么是线程、进程?

进程是程序执行的过程,是系统运行程序的基本单位。(时间片轮询)
线程是轻量级的进程,同类的多个线程共享进程的堆、方法区的资源,拥有独立的虚拟机栈、本地方法栈和程序计数器。例如java中main函数启动,就是启动了一个线程。

2.为什么要用多线程?

目的是为了提高CPU利用率。从计算机底层来说线程是程序执行的最小单位,线程间切换和调度的成本远远小于进程。从互联网发展趋势来讲,多线程是高并发支撑的基础,可以提高系统整体并发能力以及性能。
实际应用场景:文件跑批

3.JAVA线程实现/创建方式

1)继承Thread类,Thread本质上是实现Runnable接口的一个实例。启动线程的唯一方式是通过Thread的start()方法[这是个native方法],它将启动一个新线程,并执行run()方法。
2) 实现Runnable接口,Thread thread = new Thread(你的方法);
3) 实现Callable接口,有返回值任务。重写call方法,Thread thread = new Thread(new FutureTask(你的方法)); thread.start(); new FutureTask(你的方法).get()拿到返回值。
4) 使用线程池。ExecutorService es = Executors.newFixedThreadPool(3);

4.线程的生命周期

线程一共有六种状态。新建(NEW)、就绪、运行(RUNNABLE)、阻塞(BLOCKED)、终止(TERMINATED)、超时(TIME_WAITING、WAITING)。
NEW: 初始状态,使用new关键字创建一个线程后,就处于该状态,此时仅由JVM分配内存,并初始化其成员变量。
RUNNABLE: 运行状态,JAVA线程把操作系统中就绪和运行两种状态统称运行中。 就绪-程序对象调用start()方法,线程处于就绪状态。Java虚拟机会为其创建栈和程序计数器,等待调度。运行-就绪状态的线程获得了CPU,开始执行run()方法的线程执行体。
BLOCKED: 阻塞状态,表示线程因为某种原因放弃了CPU使用权。分为三种情况:
1)等待阻塞(o.wait() -> 等待队列)
2)同步阻塞(lock -> 锁池) : 该同步锁被其他线程占用
3)其他阻塞(sleep/join/IO请求)
TERMINATED:终止状态,正常结束、异常结束、调用stop
Java多线程并发面试知识点梳理_第1张图片

5.线程终止4种方式

1)正常运行结束线程
2)使用退出标志退出线程
设置一个volatile的boolean值来做标志,while(flag) {} 来用
3)interrupt方法退出线程
分两种情况:
a.线程处于阻塞状态:使用了sleep、同步锁的wait、socket中的receive, accept方法,会使线程进入阻塞状态。会抛出InterruptedException异常,通过代码捕获该异常,通过break跳出循环状态。[思考,阻塞线程的共性是释放取决于外部事件,如等不到外部触发就无法终止,因此允许一个线程请求来停止自己做的事,抛异常告诉调用者该方法被中断]
b.线程未处于阻塞状态:使用isInterrupted()判断来退出循环。使用interrupt()方法,中断标志就会设置为true。

@Override
public void run() {
	while(!isInterrupted) {
		try {
			xxxx...
		} catch (InterruptedException) {
			break;
		}
	}
}

4)stop方法终止线程(不安全)

扩展:

java还提供了线程主动复位的方法[把线程中断的标识位清除],Thread.interrupted()。此外还有一种被动复位的场景,在抛出InterruptedException之前,JVM会把线程中断的标识清除。
为什么要复位?
是当前线程对外界中断信号的响应,标识自己已经得到中断信号,但不会立刻中断自己,具体什么时候中断由自己决定,让外界知道在自身中断之前状态仍为false。(不是很理解)
interrupt线程终止原理
interrupt()方法里调用了interrupt0()的一个native方法。
这个方法设置了一个interrupted状态标识为true,若线程处于阻塞状态,会通过ParkEvent的unpark方法来唤醒线程。唤醒后通过is_interrupt方法判断是否被中断,如果被中断,清除interrupted标识,抛出InterruptedException异常,抛出该异常不意味着要终止线程,而是提示有中断操作发生。接下来怎么处理,取决于线程本身。

6.sleep和wait的区别

1.对于sleep(),该方法属于Thread类。而wait()属于Object类。
2.sleep()状态导致程序暂停执行指定时间,但是他的监控状态依然保持。
3.调用sleep()方法,线程不会释放对象锁。
4.调用wait()方法,线程会放弃对象锁,进入WaitSet,需要调用notify()唤醒进入Entry list。

7.JAVA守护线程

1.定义:为用户线程提供公共服务,在没有用户线程时会自动离开。
2.优先级:守护线程的优先级比较低,用于为系统中其他线程和对象提供服务。
3.设置:通过setDaemon(true)来设置守护线程,将一个用户线程设置为守护线程的方式是 在线程对象创建之前,用线程对象的setDaemon(true)方法。
4.守护线程中产生的线程也是守护线程。
5.线程是JVM级别的。
6.例子:垃圾回收线程,当我们程序中不再有任何运行的Thread,程序就不会再产生垃圾,垃圾回收器就无事可做。所以当JVM上仅剩垃圾回收线程,它会自动离开。
7.生命周期:与系统共生死,当JVM中所有线程都是守护线程,JVM就可以退出了;还有一个或以上非守护线程则JVM不会退出。

8.乐观锁与悲观锁

乐观锁:认为读多写少,遇到并发写的可能性低,每次修改认为别人不会修改,因此不会上锁。在更新时会判断一下在此期间别人有没有去更新这个数据,采用在写时先读出当前版本号,比较跟上次版本号,如果一样则更新。失败则循环。例如CAS
悲观锁:每次读写都会上锁,直接block直至拿到锁才能接着操作,如synchronized

9.Synchronized原理

synchronized可以把任意非NULL对象当锁,是独占式、可重入锁。
作用范围:
1.作用于实例方法时,锁住的是对象实例(this)
2.作用于静态方法时,锁住的是Class实例,全局锁
3.作用于同步代码块,指定一个对象当锁
扩展:为什么任何一个对象都可以当锁?
因为Java对象都派生与Object类,Object在JVM内部都有一个C++的oop/oopDesc对应;oop对象有monitor这个变量,线程在获取锁时就是去修改锁对象的monitor的标识。

// 作用于实例方法
public synchronized void method1() {}
// 作用于静态方法
public static synchronized void method2() {}
// 作用于同步代码块
public void method3() {
	synchronized(xxx.class) {
		//...
	}
}

核心组件
1.WaitSet: 调用wait方法被阻塞的线程被放置在这里
2.ContentionList:竞争队列,所有请求锁的线程首先被送入这个队列
3.EntryList:ContentionList中有资格成为候选资源的线程被移动到此
4.OnDeck:任意时刻,最多只有一个线程竞争锁资源,被成为OnDeck
5.Owner:当前获取到锁资源的Owner
6.!Owner:当前释放锁的线程

Java多线程并发面试知识点梳理_第2张图片
1.JVM每次从队列尾部取出一个数据用于锁竞争候选者(OnDeck),但是并发情况下ContentionList会被大量并发线程CAS访问,为了降低对尾部元素的竞争,JVM会将一部分线程移动到EntryList作为竞争线程。
2.Owner线程会在unlock时,将ContentionList中部分线程迁移至EntryList,并指定EntryList中一个为OnDeck(一般是最先进的)
3.Owner不会直接把锁传递给OnDeck线程,而是把锁竞争权力交给OnDeck。OnDeck需重新竞争锁,这样牺牲了公平性,确增加了系统吞吐量,被成为“竞争切换”
4.OnDeck线程获取到资源变成Owner线程,而没有锁资源的仍在EntryList中,如Owner线程被wait方法阻塞,则转移到WaitSet队列,直到某一时刻被notify()或notifyAll()唤醒,再回到EntryList中
5.处于ContentionList/EntryList/WaitSet中的线程都处于阻塞状态,该阻塞由操作系统来完成(Linux内核采用pthread_mutex_lock内核函数来实现)
6.Synchronized是非公平锁。Synchronized在线程进入ContentionList时,线程会先尝试自旋获取锁,获取不到再进入竞争队列。这明显对于已经在EntryList的线程是不公平的。
7.每个对象都有个monitor,加锁就是在竞争monitor对象,代码块加锁是在前后分别加上monitor_enter/exit指令来实现的,方法加锁是通过标记位判断的
8.synchronized是一个重量级操作,需要调用操作系统相关接口,性能是低效的,有可能给线程加锁销耗时间比有用操作消耗时间还高。
9.JDK1.6后,加入自适应自旋锁,锁消除,锁粗化,轻量级锁以及偏向锁。
10.锁从偏向锁升级到轻量级锁,升级至重量级锁。这个过程叫锁膨胀。
锁是如何存储的
对象在heap里布局:对象头(对象标记[markOop],类元信息),实例数据,对齐填充。markOop包括 hashcode、分代年龄、同步锁标记、偏向锁标记、偏向锁持有线程ID、monitor()方法[争抢锁的逻辑实现]
Java多线程并发面试知识点梳理_第3张图片
markword结构:
Java多线程并发面试知识点梳理_第4张图片
锁升级过程
1.偏向锁
当一个线程访问加了同步锁的代码块,会在对象头中存储当前线程ID,后续这个线程进入和退出这段加了同步锁的代码块时,不需要再次加锁和释放锁。而是直接比较对象头里面是否存储了指向当前线程的偏向锁。如果相等表示偏向锁是偏向于当前线程的,就不需要再次尝试获取锁了。
1.1 偏向锁获取逻辑
1)首先获取锁对象的markword,判断是否处于可偏向状态。basic_lock=1且ThreadId为空
2)若是可偏向状态,则通过CAS操作,把当前线程ID写入到MarkWord中。
a) 若cas成功,说明成功获得锁,继续执行代码
b) 若cas失败,说明其他线程获得偏向锁,需要撤销已获得偏向锁线程,并把锁升级为轻量级锁[这个操作要等全局安全点]才能执行。
3)若锁是已偏向状态,需检查markword中存储的ThreadId是否等于当前线程的ThreadId
a) 如果相等,不需要再次获得锁,可直接执行代码
b) 如果不相等,说明当前锁偏向于其他线程,需要撤销偏向锁升级为轻量级锁。
1.2 偏向锁撤销逻辑
直接把锁升级为轻量级锁状态,对原持有偏向锁的线程进行撤销时有两种情况:

  1. 若原获得偏向锁线程执行完了,那么会把对象头设置为无锁状态,争抢中的线程可以通过CAS重新偏向
    2)若元获得偏向锁的线程没有执行完,就把原获得偏向锁的线程升级为轻量级锁后继续执行代码块。
    可以通过UseBiasedLocking 来设置开启或关闭偏向锁
    Java多线程并发面试知识点梳理_第5张图片
    2.轻量级锁
    轻量级锁获得
    锁升级到轻量级锁之后,markword也发生相应变化。升级轻量级锁的过程:
    1)线程在自己栈帧中创建锁记录LockRecord
    2)将锁对象对象头中的markword复制到刚刚创建的锁记录中
    3)将锁记录的Owner指针指向锁对象
    4)将锁对象的对象头中的markword替换为指向锁记录的指针
    Java多线程并发面试知识点梳理_第6张图片
    Java多线程并发面试知识点梳理_第7张图片
    自旋锁:
    轻量级锁加锁过程中用到了自旋锁。自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等待有锁的线程释放锁后立即获取,就避免了用户线程和内核切换的消耗。
    JDK1.6后引入了自适应自旋锁,自旋次数由前一次同一个锁上的自旋时间以及锁拥有者的状态决定,基本上认为是一个线程的上下文切换时间为最佳时间。同时JVM还针对当前CPU负荷情况做了较多优化,如平均负载小于CPUs则一直自旋,如果有超过CPUs/2个线程在自旋,则后来的线程直接阻塞;如果CPU处于节电模式则停止自旋等…
    轻量级锁解锁
    其实就是轻量级锁获得的逆向过程。
    Java多线程并发面试知识点梳理_第8张图片
    3.重量级锁
    字节码 monitorenter(表示获得对象对应的monitor监视器) monitorexit(表示释放monitor监视器)。
    monitor依赖系统的MutexLock(互斥锁)来实现,线程被阻塞后便进入内核调度状态,这个会导致系统在用户态和内核态之间来回切换,严重影响性能
    Java多线程并发面试知识点梳理_第9张图片

10.信号机制

wait
调用该方法对象的线程会进入WAITING状态,只有等待另外线程的通知或被中断才会返回,需要注意调用wait()方法之后会释放对象锁。
sleep
sleep会导致线程休眠,与wait不同的是,sleep不会释放对象锁,sleep(时间)会让线程进入TIME_WAITING状态。sleep()会进入WAITING状态。
yield
yield让当前线程让出时间片,与其他线程一起重新争夺CPU时间片。一般情况下,优先级高的线程有更大可能性成功争夺CPU时间片,但是有的操作系统对优先级不敏感。
interrupt
见前面扩展
join
等待其他线程终止。例如主线程要等子线程结果,就可以用join。
notify/notifyAll
唤醒在此对象监视器上等待的线程

11.volatile

变量可见性
保证该变量对所有线程可见,这里可见性指当一个线程修改了变量的值,那么新的值对于其他线程是可以立即获取的。
禁止重排序
硬件层面可见性由来
解决CPU、内存、设备三者的处理速度差异,提升CPU使用效率,引入高速缓存——将需要运算的数据复制到缓存,让运算快速进行,当运算结束后再从缓存同步到内存中。
Java多线程并发面试知识点梳理_第10张图片
解决了速度问题,引入了更高复杂度,缓存一致性。解决方法:
缓存一致性协议MSI/MESI/MOSI等,最常见的是MESI协议。MESI表示缓存的四种状态,Modify(只缓存在当前CPU中,并且是被修改状态,也就是缓存与主内存中数据不一致),Exclusive(独占状态,数据只缓存在当前CPU,且无修改),Shared(数据可能被多个CPU缓存,并且各个缓存和主内存数据一致),Invalid(缓存已失效)。读请求:MES状态都可被读取,I状态只能从主内存读取。写请求:ME可以被写,S需把其他CPU状态标记为I才能写。
引入StoreBuffers,解决缓存一致性协议带来的阻塞,改为异步刷数据(但是带来乱序的问题)。如果storebuffer有值直接从storebuffer读取,否则从总内存读取。CPU提供内存屏障 Memory Barrier: 读屏障、写屏障、全屏障 解决乱序问题。

12.如何在两个线程间共享数据

Java中多线程通信的方式主要是以共享内存的方式进行的。共享内存需关注:可见性有序性原子性。JMM解决了可见性有序性,锁解决了原子性。
将数据抽象为一个类,并将数据操作作为这个类的Synchronized方法

你可能感兴趣的:(面试,java,面试)