
进程是代码在数据集合上的一次运行活动, 是系统进行资源分配和调度的基本单位, 线程则是进程的一个执行路径, 一个进程中至少有一个线程, 进程中的多个线程共享进程的资源。 *** 作系统在分配资源时是把资源分配给进程的, 但是CPU 资源比较特殊, 它是被分配到线程的, 因为真正要占用CPU 运行的是线程, 所以也说线程是CPU 分配的基本单位。在Java 中, 当我们启动main 函数时其实就启动了一个JVM进程, 而main 函数所在的线程就是这个进程中的一个线程, 也称主线程
一个进程中有多个线程, 多个线程共享进程的堆和方法区资源, 但是每个线程有自己的程序计数器和栈区域。
2.为何要将程序计数器设置为线程私有的?程序计数器是一块内存区域, 用来记录线程当前要执行的指令地址 。线程是占用CPU 执行的 基本单位, 而CPU 一般是使用时间片轮转方式让线程轮询占用的, 所以当前线程CPU 时间片 用完后, 要让出CPU, 等下次轮到自己的时候再执行 。 那么如何知道之前程序执行到哪里了呢? 其实程序计数器就是为了记录该线程让出CPU 时的执行地址的, 待再次分配到时间片时 线程就可以从自己私有的计数器指定地址继续执行 。 另外需要注意的是, 如果执行的是native 方法, 那么pc 计数器记录的是undefined 地址, 只有执行的是Java 代码时pc 计数器记录的 才是下一条指令的地址。
3.局部变量,对象实例,jvm加载的类,常量及静态变量都存储在内存的什么部位?是线程私有的吗?- 每个线程都有自己的栈资源,用于存储该线程的局部变量, 这些局部变量是该线程私有的, 其他线程是访问不了的, 除此之外栈还用来存放线程的调用栈帧。
- 堆是一个进程中最大的一块内存, 堆是被进程中的所有线程共享的,是进程创建时分配的, 堆里面主要存放使用new *** 作创建的对象实例。
- 方法区则用来存放NM 加载的类 、 常量及静态变量等信息, 也是线程共享的。
使用继承方式的好处是, 在run()方法内获取当前线程直接使用this 就可以了, 无须使用Thread.currentThread()方法;不好的地方是Java 不支持多继承, 如果继承了Thread 类, 那么就不能再继承其他类。另外任务与代码没有分离, 当多个线程执行一样的任务时需要多份任务代码, Runable 则没有这个限制。
public class DemoTest extends Thread {
// private int tickets = 20;
private volatile int tickets = 20;
@Override
public void run() {
synchronized (this) {
while (tickets > 0) {
System.out.println(Thread.currentThread().getName() + "卖出一张票" + tickets);
tickets--;
}
}
}
public static void main(String[] args) {
//实际上一共卖出了80张票,每个线程都有自己的私有的非共享数据。都认为自己有20张票
DemoTest test4 = new DemoTest();
DemoTest test5 = new DemoTest();
DemoTest test6 = new DemoTest();
DemoTest test7 = new DemoTest();
test4.setName("一号窗口:");
test5.setName("二号窗口:");
test6.setName("三号窗口:");
test7.setName("四号窗口:");
test4.start();
test5.start();
test6.start();
test7.start();
}
}
5.IllegalMonitorStateException出现的原因?
如果调用wait() 方法的线程没有事先获取该对象的监视器锁, 则调用wait ()方法时调用线程 会抛出IllegalMonitorState Exce ption 异常。
Object.notify(), Object.notifyAll(), Object.wait(), Object.wait(long), Object.wait(long, int)
public class ExceptionTest {
public static void main(String[] args) {
Object object = new Object();
try {
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
6.一个线程如何才能获取到对象的监视器锁呢?
- 执行synchronized 同步代码块时, 使用该共享变量作为参数。
synchronized (MonitorTest.class) {
//do something
}
- 调用该共享变量的方法, 并且该方法使用了synchronized 修饰。
synchronized void add(int a) {
//do something
}
7.什么是虚假唤醒?如何避免虚假唤醒?
在一个线程没有被其他线程调用notify() 、 notifyAll() 方法进行通知, 或者被中断, 或者等待超时, 这 个线程仍然可以从挂起状态变为可以运行状态 (也就是被唤醒), 这就是所谓的虚假唤醒。
虽然虚假唤醒在应用实践中很少发生, 但要防患于未然, 做法就是不停地去测试该线程被唤醒的条件是否 满足, 不满足则继续等待, 也就是说在一个while循环中调用wait() 方法进行防范 。 退出循环的条件是 满足了唤醒该线程的条件。
synchronized (obj) {
//do something
while (条件不满足){
obj.wait();
}
}
While在这里是防止虚假唤醒的关键,试想下, 一旦发生虚假唤醒, 线程会根据while添加再次进行判断, 一旦条件不满足, 会立即再次将线程挂起
8.调用共享对象的notify()方法后, 会唤醒一个在该共享变量上调用wait()的线程, 说说两个线程对锁的获取释放过程?一个线程调用共享对象的notify()方法后, 会唤醒一个在该共享变量上调用wait()方法后被挂起的线程。此外, 被唤醒的线程不能马上从wait方法返回并继续执行, 它必须在获取了共享对象的监视器锁后才可以返回(调用wait方法后, 会释放当前共享对象的锁, 如果不释放会 造成死锁) , 也就是唤醒它的线程释放了共享变量上的监视器锁后, 被唤醒的线程 也不一定会立即获取到共享对象的监视器锁, 这是因为该线程还需要和其他线程一起竞争该锁, 只有该线程竞争到了共享变量的监视器锁后才可以继续执行。
类似wait 系列方法, 只有当前线程获取到了共享变量的监视器锁后, 才可以调用共享变量的notify()方法, 否则会抛出Illega!MonitorStateException 异常。
9.说说线程中的join方法和yeild方法?在项目实践中经常会遇到一个场景, 就是需要等待某几件事情完成后才能继续往下执行, 比如多个线程加载资源, 需要等待多个线程全部加载完毕再汇总处理 。Thread类中有一个join 方法就可以做这个事情,join方法是Thread 类直接提供的 。join 是无参且返回值为void 的方法。
Thread 类中有一个静态的yield 方法, 当一个线程调用yield 方法时, 实际就是在暗示线程调度器当前 线程请求让出自己的CPU 使用 。 我们知道 *** 作系统是为每个线程分配一个时间片来占有CPU 的,正常 情况下当一个线程把分配给自己的时间片使用完后, 线程调度器才会进行下一轮的线程调度, 而当一 个线程调用了Thread 类的静态方法yield 时,是在告诉线程调度器自己占有的时间片中还没有使用完的部 分自己不想使用了, 这暗示线程调度器现在就可以进行下一轮的线程调度。
当一个线程调用y ield 方法时,当前线程会让出CPU 使用权, 然后处于就绪状态, 线程调度器会从线 程就绪队列里面获取一个线程优先级最高的线程, 当然也有可能会调度到刚刚让出CPU 的那个线程来 获取CPU 执行权。
10.说说sleep方法和yeild方法的区别?sleep方法 与yeild方法的区别在于,当线程调用sleep 方法时调用线程会被阻塞挂起指定的时间, 在这期间线程调度器不会去调度该线程。而调用yie ld 方法时, 线程只是让出自己剩余的时间片, 并没有被阻塞挂起,而是处于就绪状态, 线程调度器下一次调度时就有可能调度到当前线程执行。
11.说说interrupt(),interrupted(),isInterrupted()的区别?-
void interrupt()方法: 中断线程, 例如, 当线程A 运行时, 线程B 可以调用钱程A的interrupt()方法来设置线程A 的中断标志为true 并立即返回 。 设置标志仅仅是设置标志 , 线程A 实际并没有被中断, 它会继续往下执行 。 如果线程A 因为调用了wait 系列函数 、join 方法或者sleep 方法而被阻塞挂起, 这时候若线程B 调用线程A 的interrupt()方法, 线程A 会在调用这些方法的地方抛出InterruptedException 异常而返回。
-
boolean isinterrupted()方法: 检测当 前线程是否被中断, 如果是返回true, 否 则返回false 。
-
boolean interrupted()方法: 检测当前线程是否被中断, 如果是返回true, 否则返回false 。 与is lnterrupted 不同的是,该方法如果发现当前线程被中断, 则会清除中断标志, 并且该方法是static 方法,
可以通过Thread 类直接调用 。 另外从下面的代码可以知道, 在interrupted() 内部是获取当前调用线程的中断标志而不是调用interrupted()方法的实例对象的中断标志。
从代码中可以看出, interrupted方法调用的currentThread()的navtive 方法, 而isInterrupted方法调用的实例对象的native方法 。 Native方法传参: true代表清除中断标志, false代表不清除 中断标志
public void interrupt() {
if (this != Thread.currentThread())
checkAccess();
synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
interrupt0(); // Just to set the interrupt flag
b.interrupt(this);
return;
}
}
interrupt0();
}
/**
* Tests whether the current thread has been interrupted. The
* interrupted status of the thread is cleared by this method. In
* other words, if this method were to be called twice in succession, the
* second call would return false (unless the current thread were
* interrupted again, after the first call had cleared its interrupted
* status and before the second call had examined it).
*
* A thread interruption ignored because a thread was not alive
* at the time of the interrupt will be reflected by this method
* returning false.
*
* @return true if the current thread has been interrupted;
* false otherwise.
* @see #isInterrupted()
* @revised 6.0
*/
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
/**
* Tests whether this thread has been interrupted. The interrupted
* status of the thread is unaffected by this method.
*
* A thread interruption ignored because a thread was not alive
* at the time of the interrupt will be reflected by this method
* returning false.
*
* @return true if this thread has been interrupted;
* false otherwise.
* @see #interrupted()
* @revised 6.0
*/
public boolean isInterrupted() {
return isInterrupted(false);
}
/**
* Tests if some Thread has been interrupted. The interrupted state
* is reset or not based on the value of ClearInterrupted that is
* passed.
*/
private native boolean isInterrupted(boolean ClearInterrupted);
12.什么是死锁?
死锁是指两个或两个以上的线程在执行过程中, 因争夺资源而造成的互相等待的现象, 在无外力作用的情况下, 这些线程会一直相互等待而无法继续运行下去
如上图例子: 线程A 己经持有了资源2, 它同时还想申请资源1, 线程B 已经持有了资源1, 它同时还想申请资源2,所以线程1 和线程2 就因为相互等待对方已经持有的资源,而进入了死锁状态。
13.为什么会出现死锁?写出死锁的代码?死锁的产生必须具备以下四个条件。
- 互斥条件: 指线程对己经获取到的资源进行排它性使用, 即该资源同时只由一个线程 占用 。 如果此时还有其他线程请求获取该资源, 则请求者只能等待, 直至占有资源的 线程释放该资源。
- 请求并持有条件: 指一个线程己经持有了至少一个资源, 但又提出了新的资源请求, 而新资源己被其他线程占有, 所以当前线程会被阻塞, 但阻塞的同时并不释放自己己 经获取的资源。
- 不可剥夺条件: 指线程获取到的资源在自己使用完之前不能被其他线程抢占, 只有在 自己使用完毕后才由自己释放该资源。
- 环路等待条件: 指在发生死锁时, 必然存在一个线程→资源的环形链, 即线程集合 {T0 , T1, T2, …, Tn} 中的T0 正在等待一个T1 占用的资源, T1 正在等待T2 占用的资源, ……Tn 正在等待己被T0 占用的资源。
public class DeadLockDemo {
private static String A = "A";
private static String B = "B";
public static void main(String[] args) {
new DeadLockDemo().deadLock();
}
private void deadLock() {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
//线程1获取A的锁
synchronized (A) {
try {
Thread.currentThread().sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (B) {
System.out.println("1");
}
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
//线程2获取B的锁
synchronized (B) {
//A对象已经被线程1持有
synchronized (A) {
System.out.println("2");
}
}
}
});
t1.start();
t2.start();
}
}
14.如何避免死锁呢?
要想避免死锁, 只需要破坏掉至少一个构造死锁的必要条件即可, 目前只有请求并持有和环路
等待条件是可以被破坏的。
- 避免一个线程同时获取多个锁。
- 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。
- 尝试使用定时锁,使用locktryLock(timeou t)来替代使用内部锁机制。
- 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。
如上题代码让在线程B 中获取资源的顺序和在线程A 中获取资源的顺序保持一致, 其 实资源分配有序性就是指, 假如线程A 和线程B 都需要资源1, 2, 3, … , n 时, 对资源 进行排序, 线程A 和线程B 只有在获取了资源n-1 时才能去获取资源n 。
public class DeadLockRelessDemo {
private static String A = "A";
private static String B = "B";
public static void main(String[] args) {
new DeadLockRelessDemo().deadLock();
}
private void deadLock() {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
//线程1获取A的锁
synchronized (A) {
try {
Thread.currentThread().sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (B) {
System.out.println("1");
}
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
//线程2也获取A的锁
synchronized (A) {
synchronized (B) {
System.out.println("2");
}
}
}
});
t1.start();
t2.start();
}
}
15.简单说说你对ThreadLocal的了解?
ThreadLocal 是JDK 包提供的, 它提供了线程本地变量, 也就是如果你创建了一个ThreadLocal 变量, 那么访问这个变量的每个线程都会有这个变量的一个本地副本 。 当多个线程 *** 作这个变 量时, 实际 *** 作的是自己本地内存里面的变量, 从而避免了线程安全问题。
创建一个ThreadLocal 变量后, 每个线程都会复制一个变量到自己的本地内存, 如下图所示。
16.描述下ThreadLocal的原理?Thread 类中有一个threadLocals 和一个inheritableThreadLocals, 它们都是ThreadLocalMap 类型的变量, 而ThreadLocalMap 是一个定制化的Hashmap 。在默认情况下, 每个线程中的这两个变量都为null, 只有当前线程第一次调用ThreadLocal 的set 或者get 方法时才会创建它们 。 其实每个线程的本地变量不是存放在ThreadLocal 实例里面, 而是存放在调用线程的threadLocals 变量里面 。也就是说, ThreadLocal 类型的本地变量存放在具体的线程内存空间中。ThreadLocal 就是一个工具壳, 它通过set 方法把value 值放入调用线程的threadLocals 里面并存放起来, 当调用线程调用它的get方法时, 再从当前线程的threadLocals 变量里面将其拿出来使用 。 如果调用线程一直不终止, 那么这个本地变量会一直存放在调用线程的threadLocals 变量里面, 所以当不需要使用本地变量时可以通过调用ThreadLocal 变量的remove 方法, 从当前线程的threadLocals 里面删除该本地变量。另外, Thread 里面的threadLocals 为何被设计为map 结构? 很明显是因为每个线程可以关联多个ThreadLocal 变量。
在Thread类中有以下变量
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
/*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
17.ThreadLocal中set方法的源码剖析?
public class ThreadLocalTest {
public static void main(String[] args) {
ThreadLocal<String> t1 = new ThreadLocal<>();
t1.set("1");
t1.set("2");
System.out.println(t1.get());
}
}
Connected to the target VM, address: '127.0.0.1:51508', transport: 'socket'
2
Disconnected from the target VM, address: '127.0.0.1:51508', transport: 'socket'
Process finished with exit code 0
疑问: 我set了两个值啊, 为什么获取出来的只有 thanks1. 这就引出了另外一个问题, ThreadLocal是个Map, 它key是啥? 目前来看thanks是被 thanks1覆盖掉了 。 看下set方法的源代码:
先获取当前线程 t, 然后以 t 为 key获取当前 threadlocalmap 。 如果 Map存在则设置, 注意设置的key为this, this代表当前对象, key不变, 所以value会被覆盖 。 如果map不存在则进行createMap。
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
18.ThreadLocal支持继承吗?
public class ThreadLocalTest2 {
public static void main(String[] args) {
ThreadLocal<String> t1 = new ThreadLocal<>();
t1.set("1");
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("我是子线程,t1:" + t1.get());
}
}).start();
System.out.println("我是主线程,t1:" + t1.get());
}
}
//我是主线程,t1:1
//我是子线程,t1:null
也就是说, 同一个ThreadLocal 变量在父 线程中被设置值后, 在子线程中是获取不 到的。根据之前题目的线程私有的介绍, 这应该是正常现象, 因为在子线程 thread 里面调用get 方法时当前线程为thread 线程, 而这里调用set方法设置线程变量的是main 线程, 两者是不同的线程, 自然子线程访问时返回null 。
19.有没有办法让子线程访问到主线程的ThreadLocal变量?为了解决上题提出的问题, InheritableThreadLocal 来了 。 InheritableThreadLocal继承自ThreadLocal, 其提供了一个特性, 就是让子线程可以访问在父线程中设置的本地 变量 。 下面看一下InheritableThreadLocal 的代码。
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
protected T childValue(T parentValue) {
return parentValue;
}
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}
这是InheritableThreadLocal 的全部代码, 他继承了ThreadLocal, 并复写了三个方法 。 一个是getMap, 上一题有看过ThreadLocal源码, 一个是ceateMap, 上上一题有看过ThreadLocal源码。
20.具体说说InheritableThreadLocal 是如何让子线程可以访问在父线程 中设置的本地变量的?public class InheritableThreadLocal<T> extends ThreadLocal<T> {
protected T childValue(T parentValue) {
return parentValue;
}
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}
从代码上看, getMap和createMap没什么稀奇的, 无非是创建和获取 。 这不是原理所在。
除了getMap和createMap, 只能来看看childValue这个方法了 。 我们看到代码逻辑是 return parentValue;有这么 点意思了。
为了说清楚childValue 这个方法, 我们得先看Thread类的init 方法: 从代码可以看出, Thread 初始化的时候进 行了判断, 如果父类的 inheritableThreadLocals不为空, 则进行 createInheriteMap方法创建, 继续点进去看
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];
for (int j = 0; j < len; j++) {
Entry e = parentTable[j];
if (e != null) {
@SuppressWarnings("unchecked")
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
if (key != null) {
Object value = key.childValue(e.value);//重点此处调用了 childValue, 返回 parent的value,在该函数内部把父线程 inhritblThreadLocal 的值复制到新的 ThreadLocalMap 对象
Entry c = new Entry(key, value);
int h = key.threadLocalHashCode & (len - 1);
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++;
}
}
}
}
总结: InheritableThreadLocal 类通过重写代码 。 getMap和 createMap 让本地变量保存到了具体线程的inheritableThreadLocals 变量里面, 那么线程在通过InheritableThreadLocal 类实例的set 或者get 方法设置变量时, 就会创建当前线程的inheritableThreadLocals 变量 。 当父线程创建子线程时, 构造函数会把父线程中inheritableThreadLocals 变量里面的本地变量复制一份保存到子线程的inheritableThreadLocals 变量里面。
public class InheritableThreadLocalTest {
public static void main(String[] args) {
InheritableThreadLocal<String> t1 = new InheritableThreadLocal<>();
t1.set("1");
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("我是子线程,t1:" + t1.get());
}
}).start();
System.out.println("我是主线程,t1:" + t1.get());
}
}
//我是主线程,t1:1
//我是子线程,t1:1
21.说说InheritableThreadLocal 的使用场景?
情况还是蛮多的, 比如子线程需要使用存放在threadlocal 变量中的用户登录信息, 再比如一些中间件需要把统一的id追踪的整个调用链路记录下来 。 其实子线程使用父线程中的threadlocal 方法有多种方式, 比如创建线程时传入父线程中的变量, 并将其复制到子线程中, 或者在父线程中构造一个map 作为参数传递给子线程, 但是这些都改变了我们的使用习惯, 所以在这些情况下InheritableThreadLocal 就显得比较有用。
22.程序上下文切换检测和解决办法?监测:
使用Lmbench3可以测量上下文切换的时长;
使用vmstat可以测量上下文切换的次数
排查步骤:
1.查询Java程序的进程ID,使用 jstack 命令 dump出线程文件
sudo -u admin/opt/ifeve/java/bin/jstack31177>/home/t engfei.fangtf/dump17
2.统计线程状态
[tengfei.fangtf@ifeve~]$grep java.lang.ThreadState dump17|awk '{print }
| sort | uniq -c
39 RUNNABLE
21 TIMED_WAITING(onobjectmonitor)
6 TIMED_WAITING(parking)
51 TIMED_WAITING(sleeping)
305 WAITING(onobiectmonitor)
3 WAITING(parking)
3.查看WAITING状态的线程在干什么?
"http-0.0.0.0-7001-97" daemon prio=10 tid=0x000000004f6a8000 nid=0x555e in
Objectwait()[0x0000000052423000]
java.lang.Thread.State: WAITING(on object monitor) at java.lang.Objectwait(Native Method)
-waiting on <0x00000007969b2280>(a orapachetomcat.utinetAprEndpoint$Worker at java.lang.Objectwait(Objectjava:485)
at org.apache.tomcat.util.netAprEndpoint$Workerawait(AprEndpointjava:1464
-locked <0x00000007969b2280>(a org.apache.tomcatutilnetAprEndpoint$Worker) at org.apache.tomcat.util.net.AprEndpoint$Worker.run(AprEndpointjava:1489) at java.lang.Thread.run(Threadjava:662)
4.减少JBOSS工作线程数
23.发现程序CPU飙升100%,内存和I/O利用正常,是什么原因?如何排查?
原因: 死锁
排查: dump线程数据
24.Synchonized关键字的三种使用方式?java中的每一个对象都可以作为锁。具体表现为以下3种形式。
- ·对于普通同步方法, 锁是当前实例对象。
- ·对于静态同步方法, 锁是当前类的Class对象。
- ·对于同步方法块, 锁是Synchonized括号里配置的对象。
当一个线程试图访问同步代码块时, 它首先必须得到锁, 退出或抛出异常时必须释放锁
25.Synchonized在JVM里的实现原理JVM基于进入和退出Monitor对象来实现方法同步和代码块同步, 但两者的实现细节不一样。
代码块同步是使用monitorenter 和monitorexit指令实现的, 而方法同步是使用另外一种方式实现的。
monitorenter指令是在编译后插入到同步代码块的开始位置, 而monitorexit是插入到方法结束处和异常处, JVM要保证每个monitorenter必须有对应的monitorexit与之配对。
任何对象都有 一个monitor与之关联, 当且一个monitor被持有后, 它将处于锁定状态 。线程执行到 monitorenter 指令时, 将会尝试获取对象所对应的monitor的所有权, 即尝试获得对象的锁。
修饰方法
public class SynchonizedTest1 {
private static int a = 0;
public synchronized void add(){
a++;
}
}
可以看到在 add 方法的flags 里面多了一个ACC_SYNCHRONIZED标志, 这标志用来告诉JVM 这是一个同步方法
┌─[qinyingjie@qinyingjiedeMacBook-Pro] - [~/Documents/idea-workspace/ant/ant-juc/target/classes/com/xiaofei/antjuc/方腾飞] - [Thu Apr 14, 18:51]
└─[$] <git:(master*)> javap -v SynchonizedTest1.class
Classfile /Users/qinyingjie/Documents/idea-workspace/ant/ant-juc/target/classes/com/xiaofei/antjuc/方腾飞/SynchonizedTest1.class
Last modified 2022-4-14; size 486 bytes
MD5 checksum 1a0bdb0e66832a2980bb5b8c0a58eff7
Compiled from "SynchonizedTest1.java"
public class com.xiaofei.antjuc.方腾飞.SynchonizedTest1
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#18 // java/lang/Object."":()V
#2 = Fieldref #3.#19 // com/xiaofei/antjuc/方腾飞/SynchonizedTest1.a:I
#3 = Class #20 // com/xiaofei/antjuc/方腾飞/SynchonizedTest1
#4 = Class #21 // java/lang/Object
#5 = Utf8 a
#6 = Utf8 I
#7 = Utf8
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcom/xiaofei/antjuc/方腾飞/SynchonizedTest1;
#14 = Utf8 add
#15 = Utf8
#16 = Utf8 SourceFile
#17 = Utf8 SynchonizedTest1.java
#18 = NameAndType #7:#8 // "":()V
#19 = NameAndType #5:#6 // a:I
#20 = Utf8 com/xiaofei/antjuc/方腾飞/SynchonizedTest1
#21 = Utf8 java/lang/Object
{
public com.xiaofei.antjuc.方腾飞.SynchonizedTest1();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/xiaofei/antjuc/方腾飞/SynchonizedTest1;
public synchronized void add();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field a:I
3: iconst_1
4: iadd
5: putstatic #2 // Field a:I
8: return
LineNumberTable:
line 6: 0
line 7: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this Lcom/xiaofei/antjuc/方腾飞/SynchonizedTest1;
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: iconst_0
1: putstatic #2 // Field a:I
4: return
LineNumberTable:
line 4: 0
}
SourceFile: "SynchonizedTest1.java"
修饰类
有两个 monitorexit呢?
第一个: 正常退出
第二个: 异常退出
public class SynchonizedTest2 {
private static int a = 0;
public void add() {
synchronized (SynchonizedTest2.class) {
a++;
}
}
}
方腾飞|master⚡ ⇒ jjavap -v SynchonizedTest2
警告: 二进制文件SynchonizedTest2包含com.xiaofei.antjuc.方腾飞.SynchonizedTest2
Classfile /Users/qinyingjie/Documents/idea-workspace/ant/ant-juc/target/classes/com/xiaofei/antjuc/方腾飞/SynchonizedTest2.class
Last modified 2022-4-14; size 599 bytes
MD5 checksum e4ad4e62082f26cefee3bb1715e94295
Compiled from "SynchonizedTest2.java"
public class com.xiaofei.antjuc.方腾飞.SynchonizedTest2
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#22 // java/lang/Object."":()V
#2 = Class #23 // com/xiaofei/antjuc/方腾飞/SynchonizedTest2
#3 = Fieldref #2.#24 // com/xiaofei/antjuc/方腾飞/SynchonizedTest2.a:I
#4 = Class #25 // java/lang/Object
#5 = Utf8 a
#6 = Utf8 I
#7 = Utf8
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcom/xiaofei/antjuc/方腾飞/SynchonizedTest2;
#14 = Utf8 add
#15 = Utf8 StackMapTable
#16 = Class #23 // com/xiaofei/antjuc/方腾飞/SynchonizedTest2
#17 = Class #25 // java/lang/Object
#18 = Class #26 // java/lang/Throwable
#19 = Utf8
#20 = Utf8 SourceFile
#21 = Utf8 SynchonizedTest2.java
#22 = NameAndType #7:#8 // "":()V
#23 = Utf8 com/xiaofei/antjuc/方腾飞/SynchonizedTest2
#24 = NameAndType #5:#6 // a:I
#25 = Utf8 java/lang/Object
#26 = Utf8 java/lang/Throwable
{
public com.xiaofei.antjuc.方腾飞.SynchonizedTest2();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/xiaofei/antjuc/方腾飞/SynchonizedTest2;
public void add();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: ldc #2 // class com/xiaofei/antjuc/方腾飞/SynchonizedTest2
2: dup
3: astore_1
4: monitorenter
5: getstatic #3 // Field a:I
8: iconst_1
9: iadd
10: putstatic #3 // Field a:I
13: aload_1
14: monitorexit
15: goto 23
18: astore_2
19: aload_1
20: monitorexit
21: aload_2
22: athrow
23: return
Exception table:
from to target type
5 15 18 any
18 21 18 any
LineNumberTable:
line 7: 0
line 8: 5
line 9: 13
line 10: 23
LocalVariableTable:
Start Length Slot Name Signature
0 24 0 this Lcom/xiaofei/antjuc/方腾飞/SynchonizedTest2;
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 18
locals = [ class com/xiaofei/antjuc/方腾飞/SynchonizedTest2, class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: iconst_0
1: putstatic #3 // Field a:I
4: return
LineNumberTable:
line 4: 0
}
SourceFile: "SynchonizedTest2.java"
26.Synchonized锁信息在对象中的存储位置?
存储在对象的对象头 (Mark Word)中。
无锁状态下32位JVM 的Mark Word的默认存储结构如下:
有锁状态的Mark Word的信息变化如下, 并从下图中能够看到锁的信息的确是放到Mark Word中的, 并且不同的锁类型, Mark Word中的信息会有变化。
27.描述下锁分类和锁升级?Java SE 1.6为了减少获得锁和释放锁带来的性能消耗, 引入了“偏向锁”和“轻量级锁”, 在 Java SE 1.6中, 锁一共有4种状态, 级别从低到高依次是: 无锁状态 、偏向锁状态 、 轻量级锁状 态和重量级锁状态。
这几个状态会随着竞争情况逐渐升级 。锁可以升级但不能降级, 意味着偏 向锁升级成轻量级锁后不能 降级成偏向锁 。 这种锁升级却不能降级的策略, 目的是为了提高 获得锁和释放锁的效率
28.偏向锁的原理?当一个线程访问同步块并 获取锁时, 会在对象头和栈帧中的锁记录里存储锁偏向的线程ID, 以后该线程在进入和退出 同步块时不需要进行CAS *** 作来加锁和解锁, 只需简单地测试一下 对象头的Mark Word里是否 存储着指向当前线程的偏向锁。
如果测试成功, 表示线程已经获得了锁。
如果测试失败, 则需要再测试一下Mark Word中偏向锁的标识是否设置成1 (表示当前是偏向锁):
- 如果没有设置, 则 使用CAS竞争锁;
- 如果设置了, 则尝试使用CAS将对象头的偏向锁指向当前线程。
偏向锁使用了一种等到竞争出现才释放锁的机制, 所以当其他线程尝试竞争偏向锁时, 持有偏向锁的线程才会释放锁。
偏向锁的撤销, 需要等待全局安全点(在这个时间点上没有正 在执行的字节码)。
它会首先暂停拥有偏向锁的线程, 然后检查持有偏向锁的线程是否活着, 如果线程不 处于活动状态, 则将对象头设置成无锁状态;
如果线程仍然活着, 拥有偏向锁的栈会被执行, 遍历偏向对象的锁记录, 栈中的锁记 录和对象头的Mark Word要么重新偏向于其他线程, 要么恢复到无锁或者标记对象不适 合作为偏向锁, 最后唤醒暂停的线程。
30.JVM偏向锁可以撤销吗?偏向锁在Java 6和Java 7里是默认启用的, 但是它在应用程序启动几秒钟之后才激活, 如 有必要可以使用JVM参数来关闭延迟: -XX:BiasedLockingStartupDelay=0。
如果你确定应用程 序里所有的锁通常情况下处于竞争状态, 可以通过JVM参数关闭偏 向锁: -XX:- UseBiasedLocking=false, 那么程序默认会进入轻量级锁状态。
31.轻量级锁加锁和解锁的过程?轻量级锁加锁
- 线程在执行同步块之前, JVM会先在当前线程的栈桢中创建用于存储锁记录的空间, 并将对象头中的Mark Word复制到锁记录空间中中, 官方称为Displaced Mark Word;
- 然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。
- 如果成功, 当前线程获得锁, 如果失败, 表示其他线程竞争锁, 当前线程便尝试使 用自旋来获取锁。
轻量级解锁
- 会使用原子的CAS *** 作将Displaced Mark Word替换回到对象头, 如果成 功, 则表示没 有竞争发生 。 如果失败, 表示当前锁存在竞争, 锁就会膨胀成重量级锁
偏向锁到轻量级锁:
线程1作为持有者, 线程2作为竞争者出现了,线程2由于cas替换偏向锁中的线程id失败, 发起了撤销偏向锁的动作 。此时线程1还存活,暂停了线程1的线程, 此时线程1的栈中的锁记录会被执行遍历, 将对象头中的锁的是否是偏向锁位置改成0, 并将锁标志位从01改成00, 升级为轻量级锁。
轻量级锁到重量级锁:
线程1为锁的持有者, 线程2为竞争者 。线程2尝试CAS *** 作将轻量级锁的指针指向自己栈中的锁记录失败后。发起了升级锁的动作。线程2会将Mark Word中的锁指针升级为重量级锁指针。自己处于阻塞状态, 因为此时线程1还没释放锁。当线程1执行完同步体后,尝试CAS *** 作将Displaced Mark Word替换回到对象头时, 此时肯定会失败,因为mark word中已经不是原来的轻量级指针了, 而是线程2的重量级指针 。 那么此时线程1很无奈, 只能释放锁, 并唤醒其他线程进行锁竞争。此时线程2被唤醒了, 获取了重量级锁。
33.比较三种锁的优缺点及使用场景?其实偏向锁, 本就为一个线程的同步访问的场景 。在出现线程竞争非常小的环境下, 适合偏向锁。轻量级锁自旋获取线程, 如果同步块执行很快, 能减少线程自旋时间, 采用轻量级锁很适合。重量级锁就不用多说了, synchronized就是经典的重量级锁。
34.为什么要引入轻量级锁?解答这个问题, 先要自问一句, 不引入轻量级锁, 直接用重量级锁有什么坏处。
我们知道重量级锁, 如果线程竞争锁失败, 会直接进入阻塞 (Blocked)状态, 阻塞线程需要 CPU 从用户态 转到内核态, 代价较大, 假设一个线程刚刚阻塞不久这个锁就被释放了, 这个线程被唤醒后, 还需要从内核态 切换到用户态, 一来一 回就两次状态切换, 那这个代价就有点得不偿失了, 因此这个时候就干脆不阻塞
这个线程, 让它自旋的等待锁释放。
如果自旋的等待锁的释放, 正好是我们的轻量级锁的特性, 那么为什么引入轻量级锁就明白了。
35.什么是CPU用户状态和内核状态?CPU 的两种工作状态: 内核态(管态)和用户态(目态)。
内核态
1. 系统中既有 *** 作系统的程序, 也有普通用户程序 。 为了安全性和稳定性, *** 作系统的程序不能随便访问, 这就是内核态 。 即需要执行 *** 作系统的程序就必须转换到内核态才能执行!
2. 内核态可以使用计算机所有的硬件资源!
用户态
不能直接使用系统资源, 也不能改变 CPU 的工作状态, 并且只能访问这个用户程序自己的存储空间! 当一个进程在执行用户自己的代码时处于用户运行态(用户态), 此时特权级最低, 为 3 级, 是普通的用户 进程运行的特权级, 大部分用户直接面对的程序都是运行在用户态 。 Ring3 状态不能访问 Ring0 的地址空间 , 包括代码和数据; 当一个进程因为系统调用陷入内核代码中执行时处于内核运行态(内核态) , 此时特权级 最高, 为 0 级 。执行的内核代码会使用当前进程的内核栈, 每个进程都有自己的内核栈 。 用户运行一个程序, 该程序创建的进程开始时运行自己的代码, 处于用户态 。如果要执行文件 *** 作、 网络数 据发送等 *** 作必须通过write、send 等系统调用, 这些系统调用会调用内核的代码 。进程会切换到 Ring0, 然 后进入内核地址空间去执行内核代码来完成相应的 *** 作 。 内核态的进程执行完后又会切换到 Ring3, 回到用户 态 。 这样, 用户态的程序就不能随意 *** 作内核地址空间, 具有一定的安全保护作用 。 这说的保护模式是指通 过内存页表 *** 作等机制, 保证进程间的地址空间不会互相冲突, 一个进程的 *** 作不会修改另一个进程地址空 间中的数据。
36.用户态和内核态切换的触发条件?当在系统中执行一个程序时, 大部分时间是运行在用户态下的, 在其需要 *** 作系统帮助完成一些用户态自己没有特权 和能力完成的 *** 作时就会切换到内核态。
用户态切换到内核态的 3 种方式
(1)系统调用
这是用户态进程主动要求切换到内核态的一种方式 。 用户态进程通过系统调用申请使用 *** 作系统提供的服务程序完成 工作 。例如 fork()就是执行了一个创建新进程的系统调用 。 系统调用的机制是使用了 *** 作系统为用户特别开放的一 个中断来实现, 如 Linux 的 int 80h 中断。
(2)异常
当 cpu 在执行运行在用户态下的程序时, 发生了一些没有预知的异常, 这时会触发由当前运行进程切换到处理此异常 的内核相关进程中, 也就是切换到了内核态, 如缺页异常。
(3)外围设备的中断
当外围设备完成用户请求的 *** 作后, 会向 CPU 发出相应的中断信号, 这时 CPU 会暂停执行下一条即将要执行的指令而 转到与中断信号对应的处理程序去执行, 如果前面执行的指令时用户态下的程序, 那么转换的过程自然就会是 由用户 态到内核态的切换 。 如硬盘读写 *** 作完成, 系统会切换到硬盘读写的中断处理程序中执行后边的 *** 作等。
这三种方式是系统在运行时由用户态切换到内核态的最主要方式, 其中系统调用可以认为是用户进程主动发起的, 异 常和外围设备中断则是被动的 。从触发方式上看, 切换方式都不一样, 但从最终实际完成由用户态到内核态的切换 *** 作来看, 步骤有事一样的, 都相当于执行了一个中断响应的过程 。 系统调用实际上最终是中断机制实现的, 而异常和 中断的处理机制基本一致。
37.由用户态切换到内核态的步骤?[1] 从当前进程的描述符中提取其内核栈的 ss0 及 esp0 信息。
[2] 使用 ss0 和 esp0 指向的内核栈将当前进程的 cs,eip,eflags,ss,esp 信息保存起来, 这个
过程也完成了由用户栈到内核栈的切换过程, 同时保存了被暂停执行的程序的下一
条指令。
[3] 将先前由中断向量检索得到的中断处理程序的 cs,eip 信息装入相应的寄存器, 开始
执行中断处理程序, 这时就转到了内核态的程序执行了。
38.什么是适应性自旋?和普通自旋的区别?JDK 1.5的自旋锁(spinlock): 是指当一个线程在获取锁的时候, 如果锁已经被其它线程获取 , 那么该线程将循环等待, 然后不断的判断锁是否能够被成功获取, 直到获取到锁才会退出循环。 自旋次数可以设定, 通过-XX:PreBlockSpin=10 自行设置自旋次数, 此处举例说明设置为10次。
在JDK 1.6 引入了适应性自旋锁, XX:PreBlockSpin参数也就没有用了 。 适应性自旋锁意味着自旋的 时间不在是固定的了, 而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定, 基本认为一个线程上下文切换的时间是最佳的一个时间,同时 JVM 还针对当前 CPU 的负荷情况做 了较多的优化, 如下三点优化非常突出。
-
如果平均负载小于 CPUs 则一直自旋
-
如果有超过 (CPUs/2) 个线程正在自旋, 则后来线程直接阻塞(升级为重量级锁)
-
如果正在自旋的线程发现 Owner 发生了变化则延迟自旋时间(自旋计数)或进入阻塞(升级 为重量级锁)
JDK 1.5使用- XX:+UseSpinning 手动开启。
JDK1.6 及后续版本默认开启轻量级锁。
40.什么是原子 *** 作?说说i++ *** 作原子 (atomic) 本意是“不能被进一步分割的最小粒子”, 而原子 *** 作(atomic operation) 意 为“不可被中断的一个或一系列 *** 作”。
i++是系列 *** 作, *** 作中包括如下三个:
- 读 *** 作: 读i的当前值;
- 改 *** 作: 在i的当前值上做+1 *** 作;
- 写: 将修改后的值写回内存。
在Java中可以通过锁和循环CAS的方式来实现原子 *** 作
CAS: JVM中的CAS *** 作正是利用了处理器提供的CMPXCHG指令实现的 。从Java 1.5开始, JDK的 并发包里提供了一些类来支持原子 *** 作, 如AtomicBoolean (用原子 方式更新的boolean值) 、 AtomicInteger (用原子方式更新的int值) 和AtomicLong (用原子方式更 新的long值) , 其中就 是依靠CAS *** 作来完成的。
锁: 如synchronized以及Lock锁, 线程获取对象锁之后, 会完成系列 *** 作后释放锁, 运行期间, 其他线程会处于阻塞状态, 因此是原子性的 *** 作 。 (后续会对两者进行更加深入的剖析)
锁机制保证了只有获得锁的线程才能够 *** 作锁定的内存区域 。JVM内部实现了很多种锁 机制, 有 偏向锁 、 轻量级锁和互斥锁 。 有意思的是除了偏向锁, JVM实现锁的方式都用了循环CAS, 即当 一个线程想进入同步块的时候使用循环CAS的方式来获取锁, 当它退出同步块的时 候使用循环CAS释放锁
42.CAS *** 作的原理?CAS: Compare and Swap, 即比较再交换。
jdk5 增加了并发包 java.util.concurrent.*, 其下面的类使用 CAS 算法实现了区别于 synchronized 悲 观锁的一种乐观锁 。JDK 5 之前 Java 语言是靠 synchronized 关键字保证同步的, 这是一种独占锁, 也是是悲观锁。
所谓的 CAS, 其实是个简称, 全称是 Compare And Swap, 对比之后交换数据 。 上面的方法, 有几 个重要的参数:
(1) this, Unsafe 对象本身, 需要通过这个类来获取 value 的内存偏移地址。
(2) valueOffset, value 变量的内存偏移地址。
(3) expect, 期望更新的值。
(4) update, 要更新的最新值。
其中步骤 (1) 和步骤 (2) 是根据内存偏移地址获取当前的值value, expect值是未修改之前的value 值 。 如果修改时通过内存偏移地址获取到的value与except的value值一样, 则进行更新, 否则不更新, 再次从新获取value值, 循环下去, 直到成功为止 。 (通过源码阅读)
这里看到有一个 LOCK_IF_MP, 作用是如果是多处理器, 在指令前加上 LOCK 前缀, 因为在单 处理器中, 是不会存在缓存不一致的问题的, 所有线程都在一个 CPU 上跑, 使用同一个缓存区, 也就不存在本地内存与主内存不一致的问题, 不会造成可见性问题 。 然而在多核处理器中, 共 享内存需要从写缓存中刷新到主内存中去, 并遵循缓存一致性协议通知其他处理器更新缓存 。 Lock 在这里的作用:
• 在 cmpxchg 执行期间, 锁住内存地址 [edx], 其他处理器不能访问该内存, 保证原子性 。 即 使是在 32 位机器上修改 64 位的内存也可以保证原子性。
• 将本处理器上写缓存全部强制写回主存中去, 保证每个线程的本地内存与主存一致。
• 禁止 cmpxchg 与前后任何指令重排序, 防止指令重排序。
43.CAS存在的问题,ABA问题,如何解决的?- ABA问题
- 循环时间长开销大
- 只能保证一个共享变量的原子 *** 作
ABA问题 。 因为CAS需要在 *** 作值的时候, 检查值有没有发生变化, 如果没有发生变化 则更新, 但 是如果一个值原来是A, 变成了B, 又变成了A, 那么使用CAS进行检查时会发现它 的值没有发生变 化, 但是实际上却变化了 。ABA问题的解决思路就是使用版本号 。在变量前面 追加上版本号, 每次 变量更新的时候把版本号加1, 那么A→ B→A就会变成1A→2B→3A。
从 Java 1.5开始, JDK的Atomic包里提供了一个类AtomicStampedReference来解决ABA问题 。 这个 类的compareAndSet方法的作用是首先检查当前引用是否等于预期引用, 并且检查当前标志是 否等 于预期标志, 如果全部相等, 则以原子方式将该引用和该标志的值设置为给定的更新值。
44.CAS循环时间太长,会有什么问题?自旋CAS如果长时间不成功, 会给CPU带来非常大的执行开销。
使用CAS自旋, 需要考虑业务场景是否是多任务快速处理的场景, 如果单个任务处理够快且任务量 大, 使用CAS会带来很好地效果 。轻量级锁的设计原理底层就是使用了CAS的 *** 作原理。
一些处理器支持pause指令有两个作用: 第 一, 它可以延迟流水线执行指令 (de-pipeline) , 使 CPU不会消耗过多的执行资源, 延迟的时间 取决于具体实现的版本, 在一些处理器上延迟时间是 零 。 延迟流水线执行命令会减少CPU流水线上的任务, 在重拍流水线或者清空流水线时, 能够减小 开支。
第二, 它可以避免在退出循环的时候因 (伪共享) 内存顺序冲突 (Memory Order Violation) 而引 起CPU流水线被清空 (CPU Pipeline Flush) , 从而 提高CPU的执行效率。
45.什么是伪共享内存顺序冲突?如何避免?由于存放到CPU缓存行的是内存块而不是单个变量, 所以可能会把多个变量存放到 同一个缓存行 中, 当多个线程同时修改 这个缓存行里面的多个变量时, 由于同时只能有一个线程 *** 作缓存行 , 此时有两个线程同时修改同一个缓存行下的两个不同的变量, 这就是伪共享, 也称内存顺序冲突。
当出现伪共享时, CPU必须清空流水线, 会造成CPU比较大的开销。
如何避免:
JDK1.8 之前一般都是通过字节填充的方式来避免该问题, 也就是创建一个变量时使
用填充字段填充该变量所在的缓存行, 这样就避免了将多个变量存放在同一个缓存行中, 例如如下代码:
假如缓存行为64字节, 那么我们在 FilledLong 类里填充 了 6个long 类型的变
一个long 类型变量占用8字节, 加上自己的value 变量占用的8个字节, 总共 56 字节 。 另外, 这里FilledLong 是一个类对象, 而类对象的字节码的对象头占用8字节, 所以一个 FilledLong对象实际会占用64字节的内存, 这正好可以放入同一个缓存行。
JDK 提供了 sun.misc Contended 注解, 用来解决伪共享问题 。将上面代码修改为如下。
特别注意
在默认情况下, @Contended 注解只用于 Java 核心类, 比如rt包下的类。
如果用户类路径下的类需要使用这个注解, 需要添加JVM 参数: - XX:-RestrictContended 。 填充的宽度默认为 128, 要自定义填充宽度则可以通过参数 -XX:ContendedPaddingWidth 参数 进行设置。
46.对于同步方法,处理器如何实现原子 *** 作?处理器提供总线锁定和缓存锁定两个机制来保证复杂 内存 *** 作的原子性。
总线锁定:
如果多个处理器同时对非同步共享变量进行读改写 *** 作 (i++就是经典的读改写 *** 作) , 那么共 享变量就会被多个处理器同时进行 *** 作, 这样读改写 *** 作就不是原子的, *** 作完之后共享变量的 值会和期望的不一致 。原因可能是多个处理器同时从各自的缓存中读取变量i, 分别进行加1 *** 作, 然后分别写入系统内存中。
对于同步方法 *** 作i++时, 部分处理器使用总线锁就是来解决这个问题的 。所谓总线锁就是使用 处理器提供的一个 LOCK#信号 (参见93题的Lock汇编指令) , 当一个处理器在总线上输出此信 号时, 其他处理器的请求将被阻塞住, 那么该 处理器可以独占共享内存, 只不过总线锁定开销很 大。
缓存锁定:
所谓“缓存锁定”是指内存区域如果被缓存在处理器的缓存 行中, 并且在Lock *** 作期间 被锁定, 那么当它执行锁 *** 作回写到内存时, 处理器不在总线上声 言LOCK#信号, 而是修改内 部的内存地址, 并允许它的缓存一致性机制来保证 *** 作的原子 性, 因为缓存一致性机制会阻止同 时修改由两个以上处理器缓存的内存区域数据, 当其他处理器回写已被锁定的缓存行的数据时 , 会使缓存行无效.
47.缓存锁定性能优于总线锁定, 为什么不淘汰总线锁定?有两种情况下处理器不会使用缓存锁定。
第一种情况是: 当 *** 作的数据不能被缓存在处理器内部 (比如外部磁盘数据) , 或 *** 作的数据跨 多个缓存行 (cache line) 时, 则处理器会调用总线锁定。
第二种情况是: 有些处理器不支持缓存锁定 。对于Intel 486和Pentium处理器, 就算锁定的 内存 区域在处理器的缓存行中也会调用总线锁定。
48.java内存模型?Java线程之间的通信由Java内存模型 (本文简称为JMM)控制, JMM决定一个线程对共享 变量的写入何时对另一个线程可见。
49.说说重排序的分类?在执行程序时, 为了提高性能, 编译器 (jvm里的) 和处理器 ( *** 作系统级别的) 常常会对指令做重排序 。 重排 序分3种类 型。
1) 编译器优化的重排序 。 编译器在不改变单线程程序语义的前提下, 可以重新安排语句的执行顺序。
2) 指令级并行的重排序 。现代处理器采用了指令级并行技术 (Instruction-Level Parallelism, ILP) 来将多条指令 重叠执行 。 如果不存在数据依赖性, 处理器可以改变语句对应机器指令的执行顺序。
3) 内存系统的重排序 。 由于处理器使用缓存和读/写缓冲区, 这使得加载和存储 *** 作看上去可能是在乱序执行。
上述的1属于编译器重排序, 2和3属于处理器重排序 。 这些重排序可能会导致多线程程序 出现内存可见性问题 。 对于编译器, JMM的编译器重排序规则会禁止特定类型的编译器重排序 (不是所有的编译器重排序都要禁止) 。
对于处理器重排序, JMM的处理器重排序规则会要求Java编译器在生成指令序列时, 插入特定类型的内存屏障 (Memory Barriers, Intel称之为 Memory Fence) 指令, 通过内存屏障指令来禁止特定类型的处理器重排序。
在单线程程序中, 对存在数据依赖的 *** 作重排序, 不会改变执行结果 (这也是as-if-serial 语义允许对存在控制 依赖的 *** 作做重排序的原因) ; 但在多线程程序中, 对存在控制依赖的 *** 作重排序, 可能会改变程序的执行结 果, 因此必须要通过一定的同步手段加以控制。
50.内存屏障的种类以及说明?StoreLoad Barriers是一个“全能型”的屏障, 它同时具有其他3个屏障的效果 。现代的 多处 理器大多支持该屏障 (其他类型的屏障不一定被所有处理器支持) 。执行该屏 障开销会很昂 贵, 因为当前处理器通常要把写缓冲区中的数据全部刷新到内存中 (Buffer Fully Flush) 。
51.volatile 内存语义的实现原理Volatile内存语义请参见 43 题 。此题主要讲解volatile内存语义的实现原理。
为了实现volatile内存语义, JMM 会分别禁止如下两种类型的重排序类型:
从图中可看出:
-
volatile写写禁止重排序
-
volatile读写, 读读禁止重排序; volatile读和普通写禁止重排序
-
volatile写读, volatile写写禁止重排序。
为了实现volatile的内存语义, 编译器在生成字节码时, 会在指令序列中插入内存屏障来 禁止特定类型的处 理器重排序 。对于编译器来说, 发现一个最优布置来最小化插入屏障的总 数几乎不可能 。 为此, JMM采取 保守策略 。 下面是基于保守策略的JMM内存屏障插入策略。
• ·在每个volatile写 *** 作的前面插入一个StoreStore屏障。
• ·在每个volatile写 *** 作的后面插入一个StoreLoad屏障。
• ·在每个volatile读 *** 作的后面插入一个LoadLoad屏障。
• ·在每个volatile读 *** 作的后面插入一个LoadStore屏障。
距离说明:
52.说说双重检查锁以及其优点?如上就是双重检查锁的正确代码 。 看起来是不是有点像单例设计模式? 但是普通的单例模式的写法貌 似只有一个if(instance==null)的判断, 这里的if判断有两个, 这就是所谓的双重检查 。对于锁就是代码 中的synchronized代码块 。 两个判断+一个synchronized=双重检查锁。
看看双重检查所的优势: 如上面代码所示, 如果第一次检查instance不为null, 那么就不需要执行下面 的加锁和初始 化 *** 作 。 因此, 可以大幅降低synchronized带来的性能开销。
53.双重检查锁的变量为什么使用volatile变量看着图中的注释, 假设线程A执行getInstance方法
执行到第4步: instance 为null, 则进入if判断;
执行到第5步: 获取synchronized锁, 成功, 进入同步代码块;
执行到第6步: 继续判断instance, 为null则进入if判断;
执行到第7步: instance = new Instance() 。 看似是一句代码, 其实是三句代码。
执行到第7.1步: 为对象分配内存空间。
执行到7.3步: 对于7.2 步和7.3步, 两者没有依赖关系, 设置instance指向刚分配的内存地址。
就在线程A执行完7.3步之后, 还没执行7.2初始化对象的时候, 线程B来了。
看看线程B:
执行第4步: instance 不为null, 不进入if判断; (为什么此时instance不为null? 因为==的判断, 判断的是内 存地址, 线程A虽然没有初始化对象, 但是已经设置了instance指向内存地址了, 所以误认为此时instance不 为null)
执行第10步: return instance; (由于还没有执行初始化对象, 此时instance对应的实例对象为null) 。
出问题了。
所以加入volatile, 禁止 7.2 和 7.3的重排序, 问题解决了。
54.多线程的状态及转换过程? 55.如下守护线程是否会执行finally模块中的代码?Daemon线程是一种支持型线程, 因为它主要被用作程序中后台调度以及支持性工作 。 这 意味着, 当一个Java虚拟机中不存在非Daemon线程的时候, Java虚拟机将会退出 。 可以通过调 用Thread.setDaemon(true)将线程设置为Daemon线程。
56.简单描述下Lock和synchronized的区别?Java SE 5之后, 并发包中新增 了Lock接口 (以及相关实现类) 用来实现锁功能, 它提供了与synchronized 关键字类似的同步功 能, 只是在使用时需要显式地获取和释放锁 。 虽然它缺少了 (通过synchronized块或 者方法所提 供的) 隐式获取释放锁的便捷性, 但是却拥有了锁获取与释放的可 *** 作性 、 可中断的获取锁 以 及超时获取锁等多种synchronized关键字所不具备的同步特性。
57.什么是AQS?队列同步器AbstractQueuedSynchronizer (以下简称同步器) , 是用来构建锁或者其他同步组件的基础框架, 它使用了一个int成员变量表示同步状态, 通过内置的FIFO队列来完成资源获取线程的排队工作。
同步器的主要使用方式是继承, 子类通过继承同步器并实现它的抽象方法来管理同步状 态, 在抽象方法的 实现过程中免不了要对同步状态进行更改, 这时就需要使用同步器提供的3 个方法 (getState() 、 setState(int newState)和compareAndSetState(int expect,int update)) 来进行 *** 作, 因为它们能够保证状态 的改变是安全的。
同步器自身没有实现任何同步接口, 它仅仅是定义了若干同步状态获取和释放的方法来 供自定义同步组 件使用, 同步器既可以支持独占式地获取同步状态, 也可以支持共享式地获 取同步状态, 这样就可以方 便实现不同类型的同步组件 (ReentrantLock 、 ReentrantReadWriteLock和CountDownLatch等) 。
同步器是实现锁 (也可以是任意同步组件) 的关键, 在锁的实现中聚合同步器, 利用同步 器实现锁的 语义 。 可以这样理解二者之间的关系: 锁是面向使用者的, 它定义了使用者与锁交 互的接口 (比如可 以允许两个线程并行访问) , 隐藏了实现细节; 同步器面向的是锁的实现者, 它简化了锁的实现方式, 屏蔽了同步状态管理 、线程的排队 、 等待与唤醒等底层 *** 作 。锁和同 步器很好地隔离了使用者和实现 者所需关注的领域。
58.AQS 是基于什么设计模式实现的?同步器的设计是基于模板方法模式的, 也就是说, 使用者需要继承同步器并重写指定的 方法, 随后将同 步器组合在自定义同步组件的实现中, 并调用同步器提供的模板方法, 而这些 模板方法将会调用使用者 重写的方法。
可重写的方法如下图所示:
实现自定义同步组件时, 将会调用同步器提供的模板方法
59.AQS 底层同步队列的原理?同步器依赖内部的同步队列 (一个FIFO双向队列) 来完成同步状态的管理, 当前线程获取 同步状态失败时, 同步 器会将当前线程以及等待状态等信息构造成为一个节点 (Node) 并将其 加入同步队列, 同时会阻塞当前线程 , 当同步状态释放时, 会把首节点中的线程唤醒, 使其再 次尝试获取同步状态。
同步队列中的节点 ( Node) 用来保存获 取同步状态失败的线程 引用 、 等待状态以及前 驱和 后继节点, 节点 的属性类型与名称以及 描述如右图所示:
同步器拥有首节点 (head) 和尾节点 (tail) , 没有成功获取同步状态的线程将会成为节点加入该队列的尾部
同步器包含了两个节点类型的引用, 一个指向头节点, 而另一个指向尾节点 。 试想一下, 当一个线程成功地 获取了同步状态 (或者锁) , 其他线程将无法获取到同步状态, 转 而被构造成为节点并加入到同步队列中 , 而这个加入队列的过程必须要保证线程安全, 因此同步器提供了一个基于CAS的设置尾节点的方法: compareAndSetTail(Node expect,Node update), 它需要传递当前线程“认为”的尾节点和当前节点, 只有设 置成功后, 当前节点才正式 与之前的尾节点建立关联
同步队列遵循FIFO, 首节点是获取同步状态成功的节点, 首节点的线程在释放同步状态 时, 将会唤醒后继节点, 而后继节点将会在获取同步状态成功时将自己设置为首节点。
设置首节点是通过获取同步状态成功的线程来完成的, 由于只有一个线程能够成功获取到同步状态, 因此设置 头节点的方法并不需要使用CAS来保证 (enq方法中compareAndSetHead怎么回事儿?) , 它只需要将首节 点设置成为原首节点的后继节点并断开原首节点的next引用即可。
60.AQS 独占式同步状态获取与释放AQS的 acquire 方法
通过调用同步器的acquire(int arg)方法可以获取同步状态, 该方法对中断不敏感, 也就是 由于线程获 取同步状态失败后进入同步队列中, 后续对线程进行中断 *** 作时, 线程不会从同 步队列中移出
上述代码主要完成了同步状态获取 、 节点构造 、 加入同步队列以及在同步队列中自旋等 待的相关工 作, 其主要逻辑是: 首先调用自定义同步器实现的tryAcquire(int arg)方法, 该方法 保证线程安全的 获取同步状态, 如果同步状态获取失败, 则构造同步节点 (独占式 Node.EXCLUSIVE, 同一时刻只能 有一个线程成功获取同步状态) 并通过addWaiter(Node node) 方法将该节点加入到同步队列的尾部, 最后调用acquireQueued(Node node,int arg)方法, 使得该节点以“死循环”的方式获取同步状态 。如果 获取不到则阻塞节点中的线程, 而被阻塞线程的 唤醒主要依靠前驱节点的出队或阻塞线程被中断来 实现。
上述代码通过使用compareAndSetTail(Node expect,Node update)方法来确保节点能够被线 程安全添加。
在enq(final Node node)方法中, 同步器通过“死循环”来保 证节点的正确添加, 在“死循 环”中只有通过CAS将节点设 置成为尾节点之后, 当前线程才能从该方法返回, 否则 , 当前线 程不断地尝试设置 。 可以看出, enq(final Node node)方法将并发添加节点的请求通过CAS变 得“串行化”了。
如何前驱节点是头节点, 则尝试获取 同步锁 。 而只有前驱节点是头节点才
能够尝试获取同步状态, 这是为什么? 头节点是成功获取到同步状态的节点 , 而头节点的线程释放了同步状态之后 , 将会 唤醒其后继节点, 后继节点的 线程被唤醒后需要检查自己的前驱节点 是否是头节点。
如果前驱节点不是头节点, 则获取同步锁失败, 那么线程 继续在同步队列中等待
独占式同步状态获取流程, 也就是acquire(int arg)方法调用流程如下所示:
当前线程获取同步状态并执行了相应逻辑之后, 就需要释放同步状态, 使得后续节点能 够继 续获取同步状态 。 通过调用同步器的release(int arg)方法可以释放同步状态, 该方法在释 放了 同步状态之后, 会唤醒其后继节点 (进而使后继节点重新尝试获取同步状态) 。该方法代 码 如下所示:
该方法执行时, 会唤醒头节点的后继节点线程, unparkSuccessor(Node node)方法使用 LockSupport (在后面的有关LockSupport题目来专门说明) 来唤醒处于等待状态的线程。
61.共享式同步状态获取与释放?共享式获取与独占式获取最主要的区别在于同一时刻能否有多个线程同时获取到同步状 态 。 以文件的读写为例, 如果一个程序在对文件进行读 *** 作, 那么这一时刻对于该文件的写 *** 作均被阻塞, 而读 *** 作能够同时进行。在acquireShared(int arg)方法中, 同步器调用tryAcquireShared(int arg)方法尝试获取同步状 态,tryAcquireShared(int arg)方法返回值为int类型, 当返回值大于等于0时, 表示能够获取到同 步状态。因此, 在共享式获取的自旋过程中, 成功获取到同步状态并退出自旋的条件就是 tryAcquireShared(intarg)方法返回值大于等于0。在doAcquireShared(int arg)方法的自 旋过程中, 如果当前节点的前驱为头节点时, 尝试获取同步状态,如果返回值大于等于0, 表示 该次获取同步状态成功并从自旋过程中退出。
该方法在释放同步状态之后, 将会唤醒后续处于等待状态的节点 。对于能够支持多个线 程同时访问的并发组件, 它和独占式主要区别在于tryReleaseShared(int arg) 方法必须确保同步状态 (或者资源数) 线程安全释放, 一般是通过循环和CAS来保证的, 因为释放同步状态的 *** 作可能会同时来自多个线程。
62.AQS独占式超时获取锁和可中断获取锁?在Java 5之前, 当一 个线程获取不到锁而被阻塞在synchronized之外时, 对该线程进行中断 *** 作, 此时 该线程的中 断标志位会被修改, 但线程依旧会阻塞在synchronized上, 等待着获取锁 。在Java 5中, 同 步器 提供了acquireInterruptibly(int arg)方法, 这个方法在等待获取同步状态时, 如果当前线程被中 断, 会立刻返回, 并抛出InterruptedException。
超时获取同步状态过程可以被视作响应中断获取同步状态过程的“增强版”, doAcquireNanos(int
arg,long nanosTimeout)方法在支持响应中断的基础上, 增加了超时获取的 特性 。针对超时获取, 主要 需要计算出需要睡眠的时间间隔nanosTimeout, 为了防止过早通知, nanosTimeout计算公式为: nanosTimeout-=now-lastTime, 其中now为当前唤醒时间, lastTime为上 次唤醒时间, 如果 nanosTimeout大于0则表示超时时间未到, 需要继续睡眠nanosTimeout纳秒, 反之, 表示已经超时。
如果nanosTimeout小于等于 spinForTimeoutThreshold (1000纳秒) 时 , 将不会使该线程进行 超时等待, 而是进入 快速的自旋过程 。原因在于, 非常短的超时 等待无法做到十分精确, 如果 这时再进行 超时等待, 相反会让nanosTimeout的超时 从整体上表现得反而不精确 。 因此, 在超 时非常短的场景下, 同步器会进入无条件的 快速自旋。
63.什么是可重入锁? 可重入锁的实现原理是什么?首先明确下synchronized和lock接口均为可重入锁。
重入锁, 顾名思义, 就是支持重进入的锁, 它表示该锁能够支持一个线程对 资源的重复加锁。
1) 线程再次获取锁 。锁需要去识别获取锁的线程是否为当前占据锁的线程, 如果是, 则再 次成功获取。
2) 锁的最终释放 。线程重复n次获取了锁, 随后在第n次释放该锁后, 其他线程能够获取到 该锁 。锁的最终释放要求锁对于获取进行计数自增, 计数表示当前锁被重复获取的次数, 而锁被释放时, 计数自减, 当计数等于0时表示锁已经成功释放。
64.ReentrantLock 的 非公平锁的获取与释放?成功获取锁的线程再次获取锁, 只是增加了同步状态值, 这也就要求ReentrantLock在释放 同 步状态时减少同步状态值
65.ReentrantLock公平锁的获取和释放?公平性与否是针对获取锁而言的, 如果一个锁是公平的, 那么锁的获取顺序就应该符合 请求的绝对 时间顺序, 也就是FIFO。
该方法与nonfairTryAcquire(int acquires)比较, 唯一不同的置为判断条件多了 hasQueuedPredecessors()方法,
即加入了同步队列中当前节点是否有前驱节点的判断, 如该 方法返回true, 则表示有线程比当前线程更早地请求获取锁, 因此需要等待前驱线程获取并释 放锁之后才能继续获取锁
释放锁和非公平锁一样
66.为什么非公平锁会造成线程饥饿?首先说下公平锁 。假设目前AQS的同步队列中有 A B C 三个线程 。线程 A 排在最前边 。 当线程 A 获取锁的同时, 线程 D 也要获取锁 。此时D线程先会通过 tryAcquire 方法判断是否自己是同步队列的头结点, 如果不是, 则乖 乖的去同步队列中等待 。线程A 处于无竞争状态下获取锁 。 因此说公平锁完全按照线程先后进行FIFO的获取锁。
1) ReentrantLock:lock()。
2) FairSync:lock()。
3) AbstractQueuedSynchronizer:acquire(int arg)。
4) ReentrantLock:tryAcquire(int acquires)
再说非公平锁 。 假设目前AQS的同步队列中有 A B C 三个线程 。线程 A 排在最前边 。 当线程 A 获取锁的同时,
线程 D 也要获取锁 。此时D 线程不管三七二十一直接进行争夺, 虽说D是后来的, 但是作为非公平锁, 直接回进行 cas的竞争 。 如果竞争成功, 线程A 继续作为头结点, 等待D线程释放锁, 参与下一轮竞争 。 如果竞争失败, 线程D 也需要乖乖的到同步队列中排队。
从这段话我们可以看到, 如果线程A一直竞争不到锁, 那么就会一直留在同步队列中等待, 造成线程饥饿, 没事儿 可干。
使用非公平锁时, 加锁方法lock()调用轨迹
如下。
1) ReentrantLock:lock()。
2) NonfairSync:lock()。
3)AbstractQueuedSynchronizer:compareAndSetState(int expect,int update)。
67.什么是读写锁?读写锁, 读读不排他, 读写排他, 写写排他 。 。Java并发包提供读写锁的实现是 ReentrantReadWriteLock, 它提供的特性如下所示:
ReentrantReadWriteLock作为 ReadWriteLock的子类 。 ReadWriteLock仅定义了获取读锁和写锁的两个方法, 即readLock()方法和writeLock()方法
而其实现—— ReentrantReadWriteLock, 除了接口方法之外, 还提供了一些便于外界监控其 内部工 作状态的方法, 这些方法以及描述如下所示。
右图示例中, Cache组合一个非线程安全的HashMap作为缓存的实现, 同时使用读写锁的 读锁和写锁来保证Cache是线程安全的 。在读 *** 作get(String key)方法中, 需要获取读锁,这使 得并发访问该方法时不会被阻塞。写 *** 作put(String key,Object value)方法和clear()方法, 在更新 HashMap必须提前获取写锁, 当获取写锁后,其他线程对于读锁和写锁的获取均被阻塞, 而 只有写锁被释放之后, 其他读写 *** 作才能继续 。 Cache使用读写锁提升读 *** 作的并发性, 也保 证每次 *** 作对所有的读写 *** 作的可见性, 同时简化了编程方式。
68.ReentrantReadWriteLock实现原理?读写锁同样依赖自定义同步器来实现同步功能, 而读写状态就是其同步器的同步状态state 。 回想 ReentrantLock中自定义同步器的实现, 同步状态表示锁被一个线程重复获取的次数, 而读 写锁的 自定义同步器需要在同步状态 (一个整型变量) 上维护多个读线程和一个写线程的状 态, 使得该 状态的设计成为读写锁实现的关键。
如果在一个整型变量上维护多种状态, 就一定需要“按位切割使用”这个变量, 读写锁将 变量切分 成了两个部分, 高16位表示读, 低16位表示写, 划分方式如下图所示:
69.ReentrantReadWriteLock写锁的获取与释放?写锁是一个支持重进入 的排它锁。如果当前线 程已经获取了写锁,则 增加写状态。如果当 前 线程在获取写锁时,读 锁已经被获取(读状态 不为0)或者当前线程 不是已经获取写锁的线 程(意思是非重入状 态), 则当前线程进入 等待状态:
从代码上看,如果 c!= 0, w = null。说明 r!=null。则return false,当前线程进入同步队列等待。否则 进行加锁,更改state的值,如果c==0,这说明读写锁都没有。则进行判断是否写线程需要block或者进行更新 同步状态失败。否则设置当前线程未owner线程,获取锁成功。 写锁的释放与ReentrantLock的释放过程基本类似,每次释放均减少写状态,当写状态为0 时表示写锁已被释放
70.ReentrantReadWriteLock读锁的获取与释放?读锁是一个支持重进入的共享锁,它能够被多个线程同时获取,在没有其他写线程访问 (或者写状态为0) 时,读锁总会被成功地获取,而所做的也只是(线程安全的)增加读状态。如 果当前线程已经获取了读锁, 则增加读状态。如果当前线程在获取读锁时,写锁已被其他线程 获取,则进入等待状态。
在tryAcquireShared(int unused) 方法中,如果其他线程已经获 取了写锁,则当前线程获取读锁失败,进入等待状态。 读锁的每次释放(线程安全的, 可能有多个读线程同时释放读 锁)均减少读状态,减少的 值 是(1<<16)。
71.ReentrantReadWriteLock的锁降级问题?锁降级指的是写锁降级成为读锁 。如果当前线程拥有写锁, 然后将其释放, 最后再获取读 锁, 这 种分段完成的过程不能称之为锁降级 。锁降级是指把持住 (当前拥有的) 写锁, 再获取到 读锁 , 随后释放 (先前拥有的) 写锁的过程。
思索: 为什么先获取读锁才敢释放写锁?
RentrantReadWriteLock不支持锁升级 。也就是不支持读锁升级为写锁, 思索下为什么?
72.LockSupport工具是什么?当需要阻塞或唤醒一个线程的时候, 都会使用LockSupport工具类来完成相应 工作 。 LockSupport定义了一组的公共静态方法, 这些方法提供了最基本的线程阻塞和唤醒功 能, 而 LockSupport也成为构建同步组件的基础工具 。 (对比使用synchronized方法的阻塞唤醒功能 。)
LockSupport定义了一组以park开头的方法用来阻塞当前线程, 以及unpark(Thread thread) 方法 来唤醒一个被阻塞的线程。
73.对比parkNanos(long nanos)方法和parkNanos(Object blocker,long nanos)方法在使用场景上有什么不同?LockSupport增加了park(Object blocker) 、 parkNanos(Object blocker,long nanos) 和 parkUntil(Object blocker,long deadline)3个方法, 用于实现阻塞当前线程的功能, 其中参数 blocker是用来 标识当前线程在等待的对象 (以下称为阻塞对象) , 该对象主要用于问题排查和 系统监控。
从右图的线程dump结果可以看出, 代码片段的内容都是阻塞当前线程10秒, 但从线程 dump结果可以看出, 有阻塞对象的parkNanos方法能够传递给开发人员更多的现场信息 。 这是 由于在Java 5之前, 当线程阻塞 (使用synchronized关键字) 在一个对象上时, 通过线程dump能够查看到该线程的阻塞对象, 方便问题定位,而Java 5推出的Lock等并发工具时却遗漏了这一 点, 致使在线程dump时无法提供阻塞对象的信息 。 因此, 在Java 6中,LockSupport新增了上述3 个含有阻塞对象的park方法, 用以替代原有的park方法。
74.Lock锁的Condition接口是做什么的?任意一个Java对象, 都拥有一组监视器方法 (定义在java.lang.Object上) , 主要包括wait() 、 wait(long timeout)、 notify()以及notifyAll()方法, 这些方法与synchronized同步关键字配合, 可以 实现等待/通知模式 。 Condition接 口也提供了类似Object的监视器方法, 与Lock配合可以实现等 待/通知模式。
通过对比Object的监视器方法和Condition接口,可以更详细地了解Condition的特性
Condition定义了等待/通知两种类型的方法, 当前线程调用这些方法时, 需要提前获取到 Condition对象关联的锁 。 Condition对象是由Lock对象 (调用Lock对象的newCondition()方法) 创 建出来的, 换句话说, Condition是依赖Lock对象的。
当调用await()方法后, 当前线程会 释 放锁并在此等待, 而其他线程调用 Condition对象的signal()方法, 通知当 前线程后, 当前线程 才从await()方法 返回, 并且在返回前已经获取了锁。一个线程进入等待状态----释放锁— -线程被唤醒----线程获取锁----等待 状态结束。
75.Condition的实现原理?ConditionObject是同步器AbstractQueuedSynchronizer的内部类, 因为Condition的 *** 作需要 获取相关联的锁, 所以作为同步器的内部类也较为合理 。 每个Condition对象都包含着一个队 列 (以下称为等待队列) , 该队 列是Condition对象实现等待/通知功能的关键。
如果一个线程调用了Condition.await()方法, 那么该线程将会 释放锁 、构造成节点加入等待队列并进入等待 状态。
一个Condition包含一个等待队列, Condition拥有首节点 (firstWaiter) 和尾节点 (lastWaiter) 。 当前线程调 用Condition.await()方法, 将会以当前线程构造节点, 并将节点从尾部 加入等待队列
Condition拥有首尾节点的引用, 而新增节点只需要将原有的尾节点nextWaiter 指向它, 并且更新尾节点即可 。 上述节点引用更新的过程并没有使用CAS保证, 原因在于调用 await()方法的线程必定是获取了锁的线程, 也就是说该过程是由锁来保证线程安全的。
在Object的监视器模型上, 一个对象拥有一个同步队列和等待队列,而并发包中的 Lock (更确切地说是同步器) 拥有一个同步队列和多个等待队列
如果从队列 (同步队列和等待队列) 的角度看await()方法, 当调用await()方法时, 相当于同 步队列的首节点 (获取了锁的节点) 移动到Condition的等待队列中。
调用Condition的signal()方法, 将会唤醒在等待队列中等待时间最长的节点 (首节点) , 在 唤醒节点之前, 会将节点移到同步队列中 。 调用该方法的前置条件是当前线程必须获取了锁, 可以看到signal()方法进行了isHeldExclusively()检查, 也就是当前线程必须是获取了锁的线程 。 接着获取等待队列的首节点, 将其移动到同步队列并使用LockSupport唤醒节点中的线程。
Condition的signalAll()方法, 相当于对等待队列中的每个节点均执行一次signal()方法, 效 果就是将等 待队列中所有节点全部移动到同步队列中, 并唤醒每个节点的线程。
76.阻塞队列 (BlockingQueue) 的实现原理?阻塞队列使用Lock+多个Condition实现的FIFO的队列 。 多线程环境下安全的, 如果队列满了, 放入元素的 线程会被阻塞, 如果队列空了, 取元素的线程会被阻塞 。 具体原理一起看源代码。
JDK 7提供了7个阻塞队列, 如下。
·ArrayBlockingQueue: 一个由数组结构组成的有界阻塞队列。
· LinkedBlockingQueue: 一个由链表结构组成的有界阻塞队列。
· PriorityBlockingQueue: 一个支持优先级排序的无界阻塞队列。
· DelayQueue: 一个使用优先级队列实现的无界阻塞队列。
·SynchronousQueue: 一个不存储元素的阻塞队列。
· LinkedTransferQueue: 一个由链表结构组成的无界阻塞队列。
· LinkedBlockingDeque: 一个由链表结构组成的双向阻塞队列。
ArrayBlockingQueue是一个用数组实现的有界阻塞队列 。此队列按照先进先出 (FIFO) 的原则对元素进行排序。 默认情况使用的是非公平锁。
LinkedBlockingQueue是一个用链表实现的有界阻塞队列 。此队列的默认和最大长度为 Integer.MAX_VALUE 。此队列按照 先进先出的原则对元素进行排序。
PriorityBlockingQueue是一个支持优先级的无界阻塞队列 。默认情况下元素采取自然顺序 升序排列 。也可以自定义类实 现compareTo()方法来指定元素排序规则, 或者初始化 PriorityBlockingQueue时, 指定构造参数Comparator来对元素进行 排序。
DelayQueue是一个支持延时获取元素的无界阻塞队列 。 队列使用PriorityQueue来实现 。 队 列中的元素必须实现Delayed 接口, 在创建元素时可以指定多久才能从队列中获取当前元素 。 只有在延迟期满时才能从队列中提取元素。
SynchronousQueue是一个不存储元素的阻塞队列 。 每一个put *** 作必须等待一个take *** 作, 否则不能继续添加元素 。 它 支持公平访问队列 。默认情况下线程采用非公平性策略访问队列 。 队列本身并不存储任何元素, 非常适合传递性场景 。 SynchronousQueue的吞吐量高于 LinkedBlockingQueue和ArrayBlockingQueue。
LinkedTransferQueue是一个由链表结构组成的无界阻塞TransferQueue队列 。相对于其他阻 塞队列 , LinkedTransferQueue多了tryTransfer和transfer方法 。 如果当前有消费者正在等待接收元素 (消费者使用take()方法或带时 间限制的poll()方法 时) , transfer方法可以把生产者传入的元素立刻transfer (传输) 给消费者 。 如果没有消费者在等 待 接收元素, transfer方法会将元素存放在队列的tail节点, 并等到该元素被消费者消费了才返 回 。tryTransfer方法是用来试 探生产者传入的元素是否能直接传给消费者 。 如果没有消费者等 待接收元素, 则返回false 。 和transfer方法的区别是 tryTransfer方法无论消费者是否接收, 方法 立即返回, 而transfer方法是必须等到消费者消费了才返回。
LinkedBlockingDeque是一个由链表结构组成的双向阻塞队列 。所谓双向队列指的是可以 从队列的两端插入和 移出元素 。双向队列因为多了一个 *** 作队列的入口, 在多线程同时入队 时, 也就减少了一半的竞争 。相比其 他的阻塞队列, LinkedBlockingDeque多了addFirst 、 addLast 、 offerFirst 、 offerLast 、 peekFirst和peekLast等方 法, 以First单词结尾的方法, 表示插入 、 获取 (peek) 或移除双端队列的第一个元素 。 以Last单词结尾的方 法, 表示插入 、 获取或移除双 端队列的最后一个元素。
在初始化LinkedBlockingDeque时可以设置容量防止其过度膨胀 。 另外, 双向阻塞队列可以 运用在“工作窃取” 模式中。
77.DelayQueue的使用场景?在很多场景我们需要用到延时任务, 比如给客户异步转账 *** 作超时后发通知告知用户, 还有客户下单后多长时间内没支付则取消订单等等, 这些都可以使用延时任务来实现。
a) 关闭空闲连接 。服务器中, 有很多客户端的连接, 空闲一段时间之后需 要关闭之。
b) 缓存 。缓存中的对象, 超过了空闲时间, 需要从缓存中移出。
c) 任务超时处理 。在网络协议滑动窗口请求应答式交互时, 处理超时未响 应的请求。
后续我们的面试题涉及线程池 ScheduledThreadPoolExecutor 时, 还会看到DelayQueue的使用。
78.为什么SynchronousQueue的吞吐量高于 LinkedBlockingQueue 和ArrayBlockingQueue?SynchronousQueue无锁竞争, 需要依据实际情况注意生产者线程和消费者线程的配比.
79.线程安全的非阻塞队列 ConcurrentLinkedQueue?Tail节点并不总是尾节点, 所以每次入队都必须 先通过tail节点来找到尾节点 。尾节点可能 是 tail节点, 也可能是tail节点的next节点。
让tail节点永远作为队列的尾节点, 这样实现代码量非常少, 而且逻辑清晰和易懂 。但是, 这么做有个缺点, 每次 都需要使用循环CAS更新tail节点 。 如果能减少CAS更新tail节点的次 数, 就能提高入队的效率, 所以并不 是每次节 点入队后都将tail节点更新成尾节点, 但是距离越长带来的负面效果就是每次入队时定位尾节点的时间就越长, 因为 循环体需要多循环一次来定位出尾节点, 但是这样仍然能提高入队的效率, 因为从本质上来 看它通过增加对volatile 变量的读 *** 作来减少对volatile变量的写 *** 作, 而对volatile变量的写 *** 作开销要远远大于读 *** 作, 所以入队效率会有 所提升
首先获取头节点的元素,然后判断头节点元素是否为空, 如果为空, 表示另外一个线程已 经进行了一次出队 *** 作将该节点的元素取走, 如果不为空, 则使用CAS的方式将头节点的引 用设置成null, 如果CAS成功, 则直接返回头节点的元素, 如果不成功,表示另外一个线程已经进行了一次出队 *** 作更新了head节点, 导致元素发生了变化, 需要重新获取头节点。
80.什么是Fork/Join框架, 原理?Fork/Join框架是Java 7提供的一个用于并行执行任务的框架, 是一个把大任务分割成若干 个小任务, 最终汇 总每个小任务结果后得到大任务结果的框架。
使用算法: 工作窃取算法。
工作窃取 (work-stealing) 算法是指某个线程从其他队列里窃取任务来执行 。 那么, 为什么 需要使用工作窃 取算法呢? 假如我们需要做一个比较大的任务, 可以把这个任务分割为若干 互不依赖的子任务, 为了减少线 程间的竞争, 把这些子任务分别放到不同的队列里, 并为每个 队列创建一个单独的线程来执行队列里的任务, 线程和队列一一对应 。 比如A线程负责处理A 队列里的任务 。但是, 有的线程会先把自己队列里的任务干完 , 而其他线程对应的队列里还有 任务等待处理 。 干完活的线程与其等着, 不如去帮其他线程干活, 于是它就去 其他线程的队列 里窃取一个任务来执行 。 而在这时它们会访问同一个队列, 所以为了减少窃取任务线程和被 窃取任务线程之间的竞争, 通常会使用双端队列, 被窃取任务线程永远从双端队列的头部拿 任务执行, 而窃 取任务的线程永远从双端队列的尾部拿任务执行。
ForkJoinPool由ForkJoinTask数组和ForkJoinWorkerThread数组组成, ForkJoinTask数组负责 将存放程序提交给 ForkJoinPool的任务, 而ForkJoinWorkerThread数组负责执行这些任务。
当我们调用ForkJoinTask的fork方法时, 程序会调用ForkJoinWorkerThread的pushTask方法 异步地执行这个任 务, 然后立即返回结果 。代码如下。
pushTask方法把当前任务存放在ForkJoinTask数组队列里 。 然后再调用ForkJoinPool的 signalWork()方法唤醒 或创建一个工作线程来执行任务 。代码如下。
Join方法的主要作用是阻塞当前线程并等待获取结果 。 让我们一起看看ForkJoinTask的join 方法的实现, 代码如下。
首先, 它调用了doJoin()方法, 通过doJoin()方法 得到当前任务的状态来判断返回什么结 果, 任务 状态有4种: 已完成 (NORMAL) 、被取消 ( CANCELLED) 、信号 (SIGNAL) 和出现异常 ( EXCEPTIONAL) 。 · 如果任务状态是已完成 , 则直接返回任务结果 。 · 如果任务状态是被取消, 则直接抛出CancellationException 。 · 如果任务状 态是抛出异常, 则直接抛出对应的异常。
再来分析一下doJoin()方法的实现代码
在doJoin()方法里, 首先通过查看任务的状态, 看任 务是否已经执行完成, 如果执行完成, 则直接返回 任务状态; 如果没有执行完, 则从任务数组里取出 任务并执行 。 如果任务顺利执行 完成, 则设置任务 状态为NORMAL, 如果出现异常, 则记录异常, 并 将任务状态设置为 EXCEPTIONAL。
81.说说java中的原子 *** 作类?Java从JDK 1.5开始提供了java.util.concurrent.atomic包 (以下简称Atomic包) , 这个包中 的原子 *** 作类提供了一种用法 简单 、 性能高效 、线程安全地更新一个变量的方式 。 因为变量的类型有很多种, 所以在Atomic包里一共提供了13个类, 属于4种类型的原子更 新方式, 分别是原子更新基本类型 、 原子更新数组 、 原子更新引用和原子更新属性 (字段) 。 Atomic包里的类基本都是使用Unsafe实现的包装类。
atomic 提供了 3 个类用于原子更新基本类型: 分别是 AtomicInteger 原子更新整形, AtomicLong 原子更新长整 形, AtomicBoolean 原子更新 bool 值。
atomic 里提供了三个类用于原子更新数组里面的元素, 分别是: AtomicIntegerArray: 原子更新整形数组里的元 素; AtomicLongArray: 原子更新长整形数组里的元素; AtomicReferenceArray: 原子更新引用数组里的元素。
原子更新基本类型的 AtomicInteger 只能更新一个变量, 如果要原子更新多个变量, 就需要使用原子更新引用类 型提供的类了 。原子引用类型 atomic 包主要提供了以下几个类: AtomicReference: 原子更新引用类型 ; AtomicReferenceFieldUpdater: 原子更新引用类型里的字段; AtomicMarkableReference: 原子更新带有标记位的引用 类型 。 可以原子更新一个布尔类型的标记位和引用类型 。构造方法是 AtomicMarkableReference (V initialRef , booleaninitialMark) 。
如果需要原子更新某个对象的某个字段, 就需要使用原子更新属性的相关类, atomic 中提供了一下几个类用于原 子更新属性: AtomicIntegerFieldUpdater: 原子更新整形属性的更新器; AtomicLongFieldUpdater: 原子更新长整形的 更新器; AtomicStampedReference: 原子更新带有版本号的引用类型 。该类将整数值与引用关联起来, 可用于原子的更 新数据和数据的版本号, 可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。
82.说说对CountDownLatch的理解?在JDK 1.5之后的并发包中提供的CountDownLatch也可以实现join的功能, 并且比join的功 能更多 。也是基 于AQS实现的 (看源码) 。
CountDownLatch的构造函数接收一个int类型的参数作为计数器, 如果你想等待N个点完 成, 这里就传入 N 。 当我们调用CountDownLatch的countDown方法时, N就会减1, CountDownLatch的await方法 会阻塞 当前线程, 直到N变成零 。 由于countDown方法可以用在任何地方, 所以这里说的N个 点, 可以是N个线 程, 也可以是1个线程里的N个执行步骤 。 用在多个线程时, 只需要把这个 CountDownLatch的引用传递到 线程里即可。
83.同步屏障CyclicBarrier理解?CyclicBarrier的字面意思是可循环使用 (Cyclic) 的屏障 (Barrier) 。 它要做的事情是, 让一 组线程到达一个屏 障 (也可以叫同步点) 时被阻塞, 直到最后一个线程到达屏障时, 屏障才会 开门, 所有被屏障拦截的线程才 会继续运行。
CyclicBarrier默认的构造方法是CyclicBarrier (int parties) , 其参数表示屏障拦截的线程数 量, 每个线程调用 await方法告诉CyclicBarrier我已经到达了屏障, 然后当前线程被阻塞 。 CyclicBarrier还提供一个更高级的构造函 数CyclicBarrier (int parties, Runnable barrier- Action) , 用于在线程到达屏障时, 优先执行barrierAction, 方 便处理更复杂的业务场景
84. CyclicBarrier和CountDownLatch的区别?CountDownLatch的计数器只能使用一次, 而CyclicBarrier的计数器可以使用reset()方法重 置 。所以 CyclicBarrier能处理更为复杂的业务场景 。例如, 如果计算发生错误, 可以重置计数 器, 并让线程重新 执行一次。
CyclicBarrier还提供其他有用的方法, 比如getNumberWaiting方法可以获得Cyclic-Barrier 阻塞的线程数 量 。 isBroken()方法用来了解阻塞的线程是否被中断。
85.说说控制并发线程数的Semaphore?Semaphore (信号量) 是用来控制同时访问特定资源的线程数量, 它通过协调各个线程, 以 保证合 理的使用公共资源。
Semaphore可以用于做流量控制, 特别是公用资源有限的应用场景, 比如数据库连接 。假 如有一个 需求, 要读取几万个文件的数据, 因为都是IO密集型任务, 我们可以启动几十个线程 并发地读取 , 但是如果读到内存后, 还需要存储到数据库中, 而数据库的连接数只有10个, 这 时我们必须控制只 有10个线程同时获取数据库连接保存数据, 否则会报错无法获取数据库连 接 。 这个时候, 就可以使 用Semaphore来做流量控制。
底层AQS实现, 支持公平锁和非公平锁 。默认为非公平锁。
86.说说线程间交换数据的Exchanger?Exchanger (交换者) 是一个用于线程间协作的工具类 。 Exchanger用于进行线程间的数据交 换。 它提供一个同步点, 在这个同步点, 两个线程可以交换彼此的数据 。 这两个线程通过 exchange 方法交换数据, 如果第一个线程先执行exchange()方法, 它会一直等待第二个线程也 执行 exchange方法, 当两个线程都到达同步点时, 这两个线程就可以交换数据, 将本线程生产 出来 的数据传递给对方。
Exchanger可以用于遗传算法, 遗传算法里需要选出两个人作为交配对象, 这时候会交换 两人的 数据, 并使用交叉规则得出2个交配结果。
Exchanger也可以用于校对工作, 比如我们需 要将纸制银行流水通过人工的方式录入成电子银行 流水, 为了避免错误, 采用AB岗两人进行 录入, 录入到Excel之后, 系统需要加载这两个Excel, 并对两个Excel数据进行校对, 看看是否 录入一致 。 如果两个线程有一个没有执行exchange()方 法, 则会一直等待, 如果担心有特殊情况发 生, 避免一直等待, 可以使用exchange (V x , longtimeout, TimeUnit unit) 设置最大等待时长。
87.线程池的实现原理?当提交一个新任务到线程池时, 线程池的处理流程如下。
1) 线程池判断核心线程池里的线程是否都在执行任务 。 如果不是,则创建一个新的工作 线程来执行任务 。 如果核心线程池里的线程都在执行任务, 则进入下个流程。
2) 线程池判断工作队列是否已经满 。 如果工作队列没有满, 则将新提交的任务存储在这 个工作队列里。如果工作队列满了, 则进入下个流程。
3)线程池判断线程池的线程是否都处于工作状态 。 如果没有, 则创建一个新的工作线程 来执行任务。如果已经满了, 则交给饱和策略来处理这个任务。
线程池创建线程时, 会将线程封装成工作线程Worker , Worker在执行完任务 后, 还会循环获取工作队列里的任 务来执行 。 我们可以从Worker类的run()方法里看到这点。
ThreadPoolExecutor执行execute方法分下面4种情况。
1) 如果当前运行的线程少于corePoolSize, 则创建新线程 来执行任务 (注意, 执行这一步骤 需要获取全局锁) 。
2) 如果运行的线程等于或多于corePoolSize, 则将任务 加入BlockingQueue。
3) 如果无法将任务加入BlockingQueue (队列已满) , 则 创建新的线程来处理任务 (注意, 执行这一步骤需要获取 全局锁) 。
4) 如果创建新线程将使当前运行的线程超出 maximumPoolSize, 任务将被拒绝, 并调用 RejectedExecutionHandler.rejectedExecution()方法。
ThreadPoolExecutor采取上述步骤的总体设计思路, 是为 了在执行execute()方法时, 尽可能地避免获取全局锁 (那 将会是一个严重的可伸缩瓶颈) 。在ThreadPoolExecutor 完成预热之后 (当前运行的线程数大于等于 corePoolSize) , 几乎所有的execute()方法调用都是执行 步骤2, 而步骤2不需要获取全局锁。
88.创建线程池的重要参数?1) corePoolSize (线程池的基本大小) : 当提交一个任务到线程池时, 线程池会创建一个线 程来执行任务, 即使其 他空闲的基本线程能够执行新任务也会创建线程, 等到需要执行的任 务数大于线程池基本大小时就不再创建 。 如果 调用了线程池的prestartAllCoreThreads()方法, 线程池会提前创建并启动所有基本线程。
2) runnableTaskQueue (任务队列) : 用于保存等待执行的任务的阻塞队列 。 可以选择以下几 个阻塞队列。
·ArrayBlockingQueue: 是一个基于数组结构的有界阻塞队列, 此队列按FIFO (先进先出) 原 则对元素进行 排序。
· LinkedBlockingQueue: 一个基于链表结构的阻塞队列, 此队列按FIFO排序元素, 吞吐量通 常要高于 ArrayBlockingQueue 。 静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
·SynchronousQueue: 一个不存储元素的阻塞队列 。 每个插入 *** 作必须等到另一个线程调用 移除 *** 作, 否
则插入 *** 作一直处于阻塞状态, 吞吐量通常要高于Linked-BlockingQueue, 静态工 厂方法
Executors.newCachedThreadPool使用了这个队列。
· PriorityBlockingQueue: 一个具有优先级的无限阻塞队列。
3) maximumPoolSize (线程池最大数量) : 线程池允许创建的最大线程数 。 如果队列满了, 并 且已创建的线程数 小于最大线程数, 则线程池会再创建新的线程执行任务 。值得注意的是, 如 果使用了无界的任务队列这个参数就没 什么效果。
4) RejectedExecutionHandler (饱和策略) : 当队列和线程池都满了, 说明线程池处于饱和状 态, 那么必须 采取一种策略处理提交的新任务 。 这个策略默认情况下是AbortPolicy, 表示无法 处理新任务时抛出异常 。在 JDK 1.5中Java线程池框架提供了以下4种策略。
·AbortPolicy: 直接抛出异常。
· CallerRunsPolicy: 只用调用者所在线程来运行任务。
· DiscardOldestPolicy: 丢弃队列里最近的一个任务, 并执行当前任务。
· DiscardPolicy: 不处理, 丢弃掉。
5) keepAliveTime (线程活动保持时间) : 线程池的工作线程空闲后, 保持存活的时间 。所以, 如果任务很 多, 并且每个任务执行的时间比较短, 可以调大时间, 提高线程的利用率。
6) TimeUnit (线程活动保持时间的单位) : 可选的单位有天 (DAYS) 、 小时 (HOURS) 、分钟 (MINUTES) 、 毫秒 (MILLISECONDS) 、微秒 (MICROSECONDS, 千分之一毫秒) 和纳秒 (NANOSECONDS, 千分之一微秒) 。
89.线程池的execute方法和submit方法的区别?可以使用两个方法向线程池提交任务, 分别为execute()和submit()方法。
execute()方法用于提交不需要返回值的任务, 所以无法判断任务是否被线程池执行成 功 。 通过以下代码可知execute()方法输入的任务是一个Runnable类的实例。
submit()方法用于提交需要返回值的任务 。线程池会返回一个future类型的对象, 通 过这个 future对象可以判断任务是否执行成功, 并且可以通过future的get()方法来获 取返回值, get()方 法会阻塞当前线程直到任务完成, 而使用get (long timeout , TimeUnit unit) 方法则会阻塞当前线 程一段时间后立即返回, 这时候有可能任务没 有执行完。
90.线程池shutDown和shutDownNow方法的区别?线程池可以通过调用线程池的shutdown或shutdownNow方法来关闭线程池。
它们的原理是遍历线 程池中的工作线程, 然后逐个调用线程的interrupt方法来中断线程, 所以 无法响应中断的任务 可能永远无法终止。
但是它们存在一定的区别, shutdownNow首先将线程池的状态设置成 STOP, 然后尝试停止所 有的正在执行或暂停任务的线程, 并返回等待执行任务的列表, 而 shutdown只是将线程池的 状态设置成SHUTDOWN状态, 然后中断所有没有正在执行任务的线 程。
只要调用了这两个关闭方法中的任意一个, isShutdown方法就会返回true 。 当所有的任务 都已 关闭后, 才表示线程池关闭成功, 这时调用isTerminaed方法会返回true 。 至于应该调用哪 一 种方法来关闭线程池, 应该由提交到线程池的任务特性决定, 通常调用shutdown方法来关闭 线程池, 如果任务不一定要执行完, 则可以调用shutdownNow方法。
91.volatile 是如何保证可见性的?如: volatile instance = new instance(); 示例代码中, instance 被volatile修饰。
上边的new *** 作, 转化成汇编代码如下:
有volatile变量修饰的共享变量进行写 *** 作的时候会多出第二行Lock汇编代码, Lock前缀的指令在多核 处理器下会引发了两件事情:
1)将当前处理器缓存行的数据写回到系统内存 。(volatile写的内存语义)
2)这个写回内存的 *** 作会使在其他CPU里缓存了该内存地址的数据无效 。(volatile写的内存语义)
如果对声明了volatile的 变量进行写 *** 作, JVM就会向处理器发送一条Lock前缀的指令, 将这个变量所在 缓存行的数据写回到系统内存。
在多处理器下, 为了保证各个处理器的缓存是一致的, 就会实现缓存一致性协议, 每个处理器通过嗅探在 总线上传播的数据来检查自己缓存的值是不是过期了, 当 处理器发现自己缓存行对应的内存地址被修改, 就会将当前处理器的缓存行设置成无效状态, 当处理器对这个数据进行修改 *** 作的时候, 会重新从系统内 存中把数据读到处理器缓存里 (volatile读的内存语义)。
92.可以重排序的情况下,如何实现线程安全的单例模式?安全的单例模式
1.使用volatile禁止重排序
2.可以重排序,但重排序对其他线程不可见
初始化一个类,包括执行这个类的静态初始化和初始化在这个类中声明的静态字段。根据Java语言规范,在首次发生下列任意一种情况时,一个类或接口类型T将被立即初始化。
1)T是一个类,而且一个T类型的实例被创建。
2)T是一个类,且T中声明的一个静态方法被调用。
3)T中声明的一个静态字段被赋值。
4)T中声明的一个静态字段被使用,而且这个字段不是一个常量字段。
5)T是一个顶级类(Top Level Class,见Java语言规范的7.6),而且一个断言语句嵌套在T内部被执行。
public class InstanceFactory {
private static class InstanceHolder {
public static Instance instance=new Instance();
public static Instance getinstance(){
return InstanceHolderinstance; //这里将导致InstanceHolder类被初始化
}
93.Aqs会旋转几次获取锁?
我理解的是2次,第一次的时候,未获取到会生成队列节点,第二次是是否为头结点,第二次可以是多次,可能出现非公平锁的饥饿状态,获取锁的过程实际上是获取statu.
94.JUC包概述?底层依赖—>中层工具—>具体实现
95.详细说下单例模式的实现?首先看一下类的初始化条件:
T是一个类,而且一个T类型的实例被创建;
T是一个类,且T中声明的一个静态方法被调用;
T中声明的一个静态字段被赋值;
T中声明的一个静态字段被使用,而且这个字段不是一个常量字段;
T是一个顶级类(top level class,见java语言规范的§7.6),而且一个断言语句嵌套在T内部被执行。
静态内部类实现单例, 在调用getInstance()方法时,使用到了内部类的静态成员变量,所以会执行内部类的初始化,实现了静态域的延迟加载(或者延迟初始化)。
双检查锁实现了(实例域 的延迟初始化,也就是对象的非静态域。)
最好的方式 是使用枚举实现单例模式,因为可以避免反射供给和解决序列化之后不相等的问题,
解决反射攻击是因为 jvm在判断类型为ENUM的时候回直接抛出异常,枚举的反序列化并不是通过反射实现的.
一般情况下,不建议使用懒汉方式(有两种一个是最基本的,另一个是线程安全的),建议使用饿汉方式。 只有在要明确实现 lazy loading 效果时,才会使用静态内部类单例方式。 如果涉及到反序列化创建对象时,可以尝试使用枚举方式实现的单例。 如果有其他特殊的需求,可以考虑使用双检锁方式实现的单例。
双检查锁的使用时避免成员变量共享时出现的线程安全问题,在dubbo源码中我们可以看到很多场景运用的都是双检查锁,但是都是在方法内部,对于局部变量使用双检查锁,而Java内存模型中,我们知道Java栈,本地方法栈,程序计数器都是线程私有的,我们执行的方法,都是栈桢入栈 *** 作,也就是进入到虚拟机栈(Java栈)所以都是线程私有的不存在线程安全问题,所以使用是没有问题的。dubbo中使用的目的不是延迟初始化,使用的目的就是我们使用了缓存的前提下,只要缓存中有的时候我们尽量去缓存中去取,只在缓存中没有的时候再去创建或者数据库中获取数据,使用双检查锁可以避免缓存的多次创建和频繁的创建或者链接数据库(在redis并发的场景中我们可以使用。)
96.如何减少上下文切换减少上下文切换的方法有无锁并发编程、CAS算法、使用最少线程和使用协程。
- 无锁并发编程。多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用锁,如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据。
- CAS算法。Java的Atomic包使用CAS算法来更新数据,而不需要加锁。
- 使用最少线程。避免创建不需要的线程,比如任务很少,但是创建了很多多线程来处理,这样会造成大量线程都处于等待状态。
- 协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。
synchronized用的锁是存在Java对象头里的。如果对象是数组类型,则虚拟机用3个字宽(W/ord)存储对象头,如果对象是非数组类型,则用2字宽存储对象头。在32位虚拟机中,1字宽等于4字节,即32bit,如表2-2所示。
Java对象头里的MarkWord里默认存储对象的HashCode、分代年龄和锁标记位。32位JVN M的MarkWord的默认存储结构如表2-3所示。
欢迎分享,转载请注明来源:内存溢出
微信扫一扫
支付宝扫一扫
评论列表(0条)