文章目录
1.Jdk1.7到Jdk1.8 java虚拟机发⽣了什么变化?
1.7中存在永久代,1.8中没有永久代,替换它的是元空间,元空间所占的内存不是在虚拟机内部,⽽是本地内存空间
,这么做的原因是,不管是永久代还是元空间,他们都是⽅法区的具体实现,之所以元空间所占的内存改成本地内存,官⽅的说法是为了和JRockit统⼀,不过额外还有⼀些原因,⽐如⽅法区所存储的类信息通常是⽐较难确定的,所以对于⽅法区的⼤⼩是⽐较难指定的,太⼩了容易出现⽅法区溢出,太⼤了⼜会占⽤了太多虚拟机的内存空间,⽽转移到本地内存后则不会影响虚拟机所占⽤的内存。
2.说⼀下HashMap的Put⽅法
先说HashMap的Put⽅法的⼤体流程:
- 根据Key通过哈希算法与与运算得出数组下标
- 如果数组下标位置元素为空,则将key和value封装为Entry对象(JDK1.7中是Entry对象,JDK1.8中是Node对象)并放⼊该位置
- 如果数组下标位置元素不为空,则要分情况讨论
a. 如果是JDK1.7,则先判断是否需要扩容,如果要扩容就进⾏扩容,如果不⽤扩容就⽣成Entry对象,并使⽤头插法添加到当前位置的链表中。(先扩容再添加)
b. 如果是JDK1.8,则会先判断当前位置上的Node的类型,看是红⿊树Node,还是链表Node(分如下三种情况)(先添加再扩容)
- 如果是红⿊树Node,则将key和value封装为⼀个红⿊树节点并添加到红⿊树中去,在这个过程中会判断红⿊树中是否存在当前key,如果存在则更新value
- 如果此位置上的Node对象是链表节点,则将key和value封装为⼀个链表Node并通过尾插法插⼊到链表的最后位置去,因为是尾插法,所以需要遍历链表,在遍历链表的过程中会判断是否存在当前key,如果存在则更新value,当遍历完链表后,将新链表Node插⼊到链表中,插⼊到链表后,会看当前链表的节点个数,如果⼤于等于8且数组长度>64,那么则会将该链表转成红⿊树
- 将key和value封装为Node插⼊到链表或红⿊树中后,再判断是否需要进⾏扩容,如果需要就扩容,如果不需要就结束PUT⽅法
3.ReentrantLock中的公平锁和⾮公平锁的底层实现
⾸先不管是公平锁和⾮公平锁,它们的底层实现都会使⽤AQS来进⾏排队,它们的区别在于:线程在使⽤lock()⽅法加锁时,如果是公平锁,会先检查AQS队列中是否存在线程在排队,如果有线程在排队,则当前线程也进⾏排队,如果是⾮公平锁,则不会去检查是否有线程在排队,⽽是直接竞争锁。
不管是公平锁还是⾮公平锁,⼀旦没竞争到锁,都会进⾏排队,当锁释放时,都是唤醒排在最前⾯的线程,所以是否公平锁只是体现在了线程加锁阶段,⽽没有体现在线程被唤醒阶段
。
另外,ReentrantLock是可重⼊锁,不管是公平锁还是⾮公平锁都是可重⼊的。
(ReentrantLock 默认非公平,且要注意,虽然是可重入,但你 lock了几次,你就要unLock几次=》 state = 0 表示锁可获取了,每重入一次+1,释放锁-1)
4.如何查看线程死锁
- linux中,可使用如下命令
jstack 进程号
- Mysql查询死锁
#1.查询是否锁表
show open tables where In_use >0;
#2.查询进程
show processlist;
#3.查看正在锁的事务
select * from INFORMATION_SCHEMA.INNODB_LOCKS;
#4.查看等待锁的事务
select * from INFORMATION_SCHEMA.INNODB_LOCKS_WAITS;
5.Volatile和Synchronized
- 区别:
Synchronized关键字用来加锁。 Volatile只是保持变量的线程可见性,通常适用于一个线程写,多个线程读的场景。 - Volatile能不能保证线程安全?
不能,volatile只能保证线程的可见性,不能保证原子性。 - DLC 双重检测锁->成员变量为什么要加 volatile修饰?
Volatile防止指令重排。在DCL中,防止高并发情况下,指令重排造成的线程安全问题。
6.JAVA线程锁机制是怎样的?
1、JAVA的锁就是在对象的Markword中记录一个锁状态。无锁,偏向锁,轻量级
锁,重量级锁对应不同的锁状态。
markword问题参考 https://blog.csdn.net/evelynnJava/article/details/124028745
2、JAVA的锁机制就是根据资源竞争的激烈程度不断进行锁升级的过程。
7.Sychronized的偏向锁、轻量级锁、重量级锁
需知:每一个Java对象自打娘胎里出来就带了一把看不见的锁,它叫做内部锁或者Monitor锁。
- 偏向锁: 在锁对象的对象头中记录⼀下当前获取到该锁的线程ID(只是做了个标记),该线程下次如果⼜来获取该锁就可以直接获取到了。(在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,java6后加了该锁,也说明syn是可重入的。偏向锁可以降低无竞争开销,它不是互斥锁,不存在线程竞争的情况,省去了再次同步判断的步骤(获取锁),大大提升了程序运行性能)
- 轻量级锁: 由偏向锁升级⽽来,当⼀个线程获取到锁后,此时这把锁是偏向锁,此时如果有第⼆个线程来竞争锁,偏向锁就会升级为轻量级锁,之所以叫轻量级锁,是为了和重量级锁区分开来,轻量级锁底层是通过⾃旋来实现的,并不会阻塞线程。轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁。
- 重量级锁: 如果轻量锁⾃旋次数过多仍然没有获取到锁(自旋耗cpu),则会升级为重量级锁,重量级锁会导致线程阻塞。(OS处理,串行)
⾃旋锁说明: ⾃旋锁就是线程在获取锁的过程中,不会去阻塞线程,也就⽆所谓唤醒线程,阻塞和唤醒这两个步骤都是需要操作系统去进⾏的,⽐较消耗时间,⾃旋锁是线程通过CAS获取预期的⼀个标记,如果没有获取到,则继续循环获取,如果获取到了则表示获取到了锁,这个过程线程⼀直在运⾏中,相对⽽⾔没有使⽤太多的操作系统资源,⽐较轻量。
流程图:
实际上,锁的细节更多
,如图中有【匿名偏向锁】以及【偏向锁是否已启动】,此处提供一个demo说明。
//TimeUnit.SECONDS.sleep(5);
Object o = new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
synchronized (o){
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
说明: 如上案例,如果注释了sleep五秒,打印出来 00000001 00000000 00000000 00000000 (无锁状态) 和 01101000 11110001 11110101 00000000(轻量级锁) , 并不是说我们的理论不对, 而是 jvm内部会默认延迟 偏向锁启动,大概4秒, 此时结果就会如流程图所示。所以我们测试时需要放开 注释的部分!
延迟偏向原因:jvm启动明显是个多线程环境,而偏向锁机制本身就是考虑到大多场景下,锁只有一个线程获得。 所以这种明确多线程的场景下,还要给每个对象都贴个偏向锁的标志,浪费资源,所以jvm会延迟偏向锁)
放开后,第一个结果打印出 00000101 00000000 00000000 00000000( 低地址变成101->其他bit却都是0,表示匿名偏向锁) ,说明此时,jvm默认预备启动了一个偏向锁,还没有记录到线程id, 但是它随时等着别人来用它 , 第二句打印出 00000101 01000000 10000111 00000010(正常的偏向锁)
简单理解:syn最开始,没有线程进入时,为 无锁状态, 有1个线程,则变为偏向锁;有数个线程,则变为 轻量级锁 ( 该在 syn内, 其实有个 while循环 做自旋 , 除了一个进入的线程, 后面线程在 while中自旋等待, 但自旋到了一定次数后, 如果上一个进入的还没有释放锁, jvm觉得竞争有点激烈,会变为 重量级锁)
优化: 重量锁除锁粗化,锁消除(利用JIT)外,其实也可以用自旋优化,在 Java 6 之后自旋是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,比较智能。 自旋的目的是为了减少线程挂起的次数
,尽量避免直接挂起线程(挂起操作涉及系统调用,存在用户态和内核态切换,这才是重量级锁最大的开销)
8.谈谈你对AQS的理解。AQS如何实现可重入锁?
1、AQS是一个JAVA线程同步的框架。是JDK中很多锁工具的核心实现框架。
2、 在AQS中,维护了一个信号量state和一个个线程组成的双向链表队列(用Node类封装每个线程,每个节点有head和tail指向前后)。其中,这个线程队列,就是用来给线程排队的,而state就像是一个红绿灯,用来控制线程排队或者放行的。 在不同的场景下,有不同的意义。
3、在可重入锁这个场景下,state就用来表示加锁的次数。0标识无锁,每加一次锁,state就加1。释放锁state就减1。
符一张AQS设计的思想图:
9.三个重要的并发工具
1.有A,B,C三个线程,如何保证三个线程同时执行?
- CountDownLatch
await()阻塞线程
countDown()令计数器减1,减到0时线程不再阻塞
场景1 让多个线程等待:模拟并发,让并发线程一起执行
public class CountDownLatchTest {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(1);
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
//准备完毕……运动员都阻塞在这,等待号令
countDownLatch.await();
String parter = "【" + Thread.currentThread().getName() + "】";
System.out.println(parter + "开始执行……");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
Thread.sleep(2000);// 裁判准备发令
countDownLatch.countDown();// 发令枪:执行发令
}
}
场景2 让单个线程等待:多个线程(任务)完成后,进行汇总合并
public class CountDownLatchTest2 {
public static void main(String[] args) throws Exception {
CountDownLatch countDownLatch = new CountDownLatch(5);
for (int i = 0; i < 5; i++) {
final int index = i;
new Thread(() -> {
try {
Thread.sleep(1000 + ThreadLocalRandom.current().nextInt(1000));
System.out.println(Thread.currentThread().getName()+" finish task" + index );
countDownLatch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
// 主线程在阻塞,当计数器==0,就唤醒主线程往下执行。
countDownLatch.await();
System.out.println("主线程:在所有任务运行完成后,进行结果汇总");
}
}
- 功能相比CountDownLatch 更强的栅栏,支持计数器重置等
- CyclicBarrier
场景一 利用CyclicBarrier的计数器能够重置,屏障可以重复使用的特性,可以支持类似“人满发车”的场景
public class CyclicBarrierTest3 {
public static void main(String[] args) {
AtomicInteger counter = new AtomicInteger();
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
5, 5, 1000, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100),
(r) -> new Thread(r, counter.addAndGet(1) + " 号 "),
new ThreadPoolExecutor.AbortPolicy());
CyclicBarrier cyclicBarrier = new CyclicBarrier(5,
() -> System.out.println("裁判:比赛开始~~"));
for (int i = 0; i < 10; i++) {
threadPoolExecutor.submit(new MyThread(cyclicBarrier));
}
}
static class MyThread extends Thread{
private CyclicBarrier cyclicBarrier;
public MyThread(CyclicBarrier cyclicBarrier) {
this.cyclicBarrier = cyclicBarrier;
}
@Override
public void run() {
try {
int sleepMills = ThreadLocalRandom.current().nextInt(1000);
Thread.sleep(sleepMills);
System.out.println(Thread.currentThread().getName() + " 选手已就位, 准备共用时: " + sleepMills + "ms" + cyclicBarrier.getNumberWaiting());
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
}catch(BrokenBarrierException e){
e.printStackTrace();
}
}
}
}
3.如何在并发情况下保证三个线程依次执行?
- Semaphore
acquire() 表示阻塞并获取许可
release() 表示释放许可
场景一 三个线程顺序打印 One Two Three
//也可以用volatile修饰一个变量 int state,每个子线程去判断变量state=n时执行
public class SemaphoneTest1 {
private static Semaphore s1= new Semaphore(1);
private static Semaphore s2= new Semaphore(1);
private static Semaphore s3= new Semaphore(1);
public static void main(String[] args) throws InterruptedException {
//s1 和 s2先调用一次
s1.acquire();
s2.acquire();
//由于构造方法中为1,后面s1 和 s2再调 acquire会阻塞住,直到调用 release()方法
new Thread(()->{
while (true){
try {
s1.acquire();
System.out.println("Two");
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
s2.release();
}
}).start();
new Thread(()->{
while (true) {
try {
s2.acquire();
System.out.println("Three");
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
s3.release();
}
}).start();
new Thread(()->{
while (true) {
try {
s3.acquire();
System.out.println("One");
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
s1.release();
}
}).start();
}
}
结果: One Two Three....顺序打印
场景二 信号量更多的用于限流,例同时只能处理5个请求的限流器
public class SemaphoneTest2 {
/**
* 实现一个同时只能处理5个请求的限流器
*/
private static Semaphore semaphore = new Semaphore(5);
/**
* 定义一个线程池
*/
private static ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 50, 60, TimeUnit.SECONDS, new LinkedBlockingDeque<>(200));
/**
* 模拟执行方法
*/
public static void exec() {
try {
semaphore.acquire(1);
// 模拟真实方法执行
System.out.println("执行exec方法" );
Thread.sleep(2000);
} catch (Exception e) {
e.printStackTrace();
} finally {
semaphore.release(1);
}
}
public static void main(String[] args) throws InterruptedException {
{
for (; ; ) {
Thread.sleep(100);
// 模拟请求以10个/s的速度
executor.execute(() -> exec());
}
}
}
}