unsafe类是Java中一个比较特殊的类,它为Java提供了一种不安全的 直接访问和操作内存和线程和对象

正如它的名字一样,unsafe提供了很多不安全的操作,我们应该尽量不直接使用它,除非真的要操作底层

unsafe类需要通过反射才能获取对象,这样做的是让程序员知道这是一个非常底层的类,如果是能直接new的

话,那使用起来就很轻松了

内存操作:

//分配新的本地空间
public native long allocateMemory(long bytes);
//重新调整内存空间的大小
public native long reallocateMemory(long address, long bytes);
//将内存设置为指定值
public native void setMemory(Object o, long offset, long bytes, byte value);
//内存拷贝
public native void copyMemory(Object srcBase, long srcOffset,Object destBase, long destOffset,long bytes);
//清除内存
public native void freeMemory(long address);

对象操作://在对象的指定偏移地址获取一个对象引用publicnativeObjectgetObject(Objecto,longoffset);//在对象指定偏移地址写入一个对象引用publicnativevoidputObject(Objecto,longoffset,Objectx);//在对象的指定偏移地址处读取一个int值,支持volatile load语义publicnativeintgetIntVolatile(Objecto,longoffset);//在对象指定偏移地址处写入一个int,支持volatile store语义publicnativevoidputIntVolatile(Objecto,longoffset,intx);

CAS操作:publicfinalnativebooleancompareAndSwapInt(Objecto,longoffset,intexpected,intx);

线程调度:parkunpark

Class操作://获取静态属性的偏移量publicnativelongstaticFieldOffset(Fieldf);//获取静态属性的对象指针publicnativeObjectstaticFieldBase(Fieldf);//判断类是否需要实例化(用于获取类的静态属性前进行检测)publicnativebooleanshouldBeInitialized(Class<?>c);

AtomicInteger、AtomicLong、AtomicBoolean(三者基本一致):

public final int get() //获取当前的值

public final int getAndSet(int newValue)//获取当前的值,并设置新的值

public final int getAndIncrement()//获取当前的值,并自增

public final int getAndDecrement() //获取当前的值,并自减

public final int getAndAdd(int delta) //获取当前的值,并加上预期的值

boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update)

public final void lazySet(int newValue)//最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。

AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray(三者基本一致):

public final int get(int i) //获取 index=i 位置元素的值

public final int getAndSet(int i, int newValue)//返回 index=i 位置的当前的值,并将其设置为新值:newValue,返回旧值

public final int getAndIncrement(int i)//获取 index=i 位置元素的值,并让该位置的元素自增

public final int getAndDecrement(int i) //获取 index=i 位置元素的值,并让该位置的元素自减

public final int getAndAdd(int i, int delta) //获取 index=i 位置元素的值,并加上预期的值

boolean compareAndSet(int i, int expect, int update) //如果输入的数值等于预期值,则以原子方式将 index=i 位置的元素值设置为输入值(update)

public final void lazySet(int i, int newValue)//最终 将index=i 位置的元素设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。

吊打Java面试官之ConcurrentHashMap(线程安全的哈希表)

jdk1.7

concurrentHashMap里存了一个segement数组,然后一个segement元素类似于一个HashTable。当put元素

时,然后根据Hash值定位到某个segement里,然后对segement加一个ReentrantLock就好了,这也保证了

不同segement之间可以并发操作。

无参构造初始化后

  1. concurrencyLevel(最大并发级别,即决定segement数组的容量)默认为16,一旦初始化后不可扩容

2.初始化segement[i](HashEntry数组)为2,负载因子为0.75,扩容阀值是 2*0.75=1.5,即插入第二个值

后才会触发扩容,扩容一次 size*2

3.插入元素时才会初始segement[cur],其他的都还是null。如果这时候segement[i]扩容到了4,那么其他

segement也会参照segement[cur]进行初始化

4.segementshift 和 segementmask 用来定位元素在哪个segement位置

put流程

1.为输入的key做Hash运算,得到其Hash值

2.通过Hash值,定位到在segement数组的位置

3.获取ReentrantLock

4.再次Hash运算,得到在segement元素内部的位置

5.覆盖or插入HashEntry对象

6.释放ReentrantLock

get(无锁)

为输入的key做Hash运算,得到Hash值

根据Hash值定位到segement对象

再进行一次Hash

定位到segement数组里的具体位置

为什么get()不需要上锁,因为Node节点里的val和next都用到了volatile

jdk1.8

concurrentHashMap采用了Node数组来存元素,并基于CAS+synchronized来保证线程安全

初始化

懒汉式初始化桶元素(new的时候,Node[]还是null,HashMap也是)

负载因子默认为0.75。它这里有个有趣的点,即使让构造时用户手动指定了负载因子,后续扩容还是会

按0.75来进行扩容(HashMap就不是,会按照用户设置好的负载因子进行扩容)

capacity表示你想往里存放的元素个数,但实际它在初始化时会将其变成2的幂,比如你cap=18,它会变

成32,这也是跟HashMap一样

get():无锁,因为Node[]和Node里的next和value都有volatile修饰,保证读取到的都是最新的值

initTable(): CAS保证只有一个线程初始化成功,涉及到volatile 修饰的sizeCtl。如果有线程初始化成功就会把

它设置为-1,下一个线程发现<0时,说明已经有线程进行初始化了。就会进行Thread.yield() 即让出CPU使

用权

put()

1.检查key和value不能为null

2.进入死循环

3.如果是第一次put,则初始化Table,并用cas保证线程安全,然后重新进入死循环

4.如果头节点为null,则通过CAS将当前值设置为头节点,成功就直接break,失败就重新进入死循环

5.如果头节点为ForwardingNode,说明此时当前桶在扩容,则当前线程需要去协助扩容,扩容完成后

重新进入循环

6.到了这里说明要往当前槽位的链表or红黑树里加元素了,此时使用synchronized对头结点进行上锁操作

a.再次判断头节点是否被移动

如果此时是链表,则走链表的更新逻辑

如果是红黑树,就走红黑树的更新逻辑

b.释放锁,判断是否需要树化,然后break

7.执行addCount方法,addCount(1L, binCount); 更新binCount( 当前桶(bucket)中节点个数 )

扩容

何时扩容?当前元素个数为 数组大小*0.75

整个扩容操作分为两个部分

1.构建一个nextTable,容量为原来的2倍,且这个构建是单线程的

2.把原来Table里的元素复制到新的Table里,这个主要是遍历复制的过程。然后遍历是指会从后往前遍历

原Table,然后通过tableAt(i)获取i位置的元素

a.若tableAt(i)等于null,则会将其标记为ForwardingNode,这是并发协作的基础

b.若tableAt(i)为Node节点(fh>=0),即为链表节点,会将其链表一分为2。然后将其放到

nextTable的i和i+n位置上(n为数组长度)

c.若为TreeBin即红黑树节点,则判断是否需要取消树化,将其放到nextTable的i和i+n位置

d.遍历所有元素完成复制,并将nextTable做为新的Table,把sizeCtl赋值为新容量的0.75,至此完

成扩容

ps:sizeCtl是什么?(CTL: Control )

size()

counterCells:每个槽位里的桶数量,会在高并发时被初始化出来,且高并发场景下size的部分逻辑就会

走counterCells

为什么HashMap的key和value可以是null?

containsKey(key):先根据key获取Node,再判断Node是否为null。可以解决key为null的歧义问题

单线程的情况下,不如出现二义性

为什么concurrentHashMap的key和value不可以是null?

在多线程的情况下,会有二义性。

二义性:

get(key)后发现value=null,存在两种情况,一种不存在,一种value为null。但此时你要

containsKey(key)时,有线程把key删了,让你误认为key是不存在,而不是只是value为null

如果想要解决它,其实是可以解决的。两个方法

1.get()方法加锁,保证没有人来删,但费性能

2.value直接不能为null,简单暴力,concurrentHashMap就是采用此方法

当你的value不为null时,key为null,完全是可以通过containsKeys来解决二义性的

那为什么concurrentHashMap还要把key设置为null呢??

ConcurrentHashMap作者 道林认为往Map里塞key为null,显然就是一件奇怪的事。这

也是他HashMap作者的分歧。因为道林他认为key中存在null是存在风险的,而且直接让key不为null可以

在代码层面少去很多判空逻辑

为什么jdk1.8concurrentHashMap要采用synchronized,而不是ReentrantLock

ReentrantLock是对象锁吧,占的空间是要比synchronized大的。

强制:Thread.stop() 已废除了

优雅:Interrupt()

手搓一个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
/**
* 线程根本不存在复用这一说,在它创建,销毁这个生命周期来看,执行完一个任务后肯定是会销毁的
*/

/**
* 我们需要什么?
* 核心线程 coreThread
* 非核心线程 supportThread
* 任务队列 workQueue
* 拒绝策略 rejectHandle
*/
public class MyThreadPool {

public MyThreadPool(BlockingQueue<Runnable> workQueue, int corePoolSize, int maxPoolSize, TimeUnit timeUnit, long timeout, RejectHandle rejectHandle){
this.workQueue = workQueue;
this.corePoolSize=corePoolSize;
this.maxPoolSize=maxPoolSize;
this.timeUnit = timeUnit;
this.timeout = timeout;
this.rejectHandle = rejectHandle;
}
//存放任务的队列
private final BlockingQueue<Runnable> workQueue;
private final int corePoolSize;
private final int maxPoolSize;
private final TimeUnit timeUnit;
private final long timeout;
private final RejectHandle rejectHandle;

//TODO: 当前这个execute一系列判断操作不是原子操作,所以存在线程安全问题,加锁,使用原子变量,volatile
public void execute(Runnable command){
//如果核心线程数还没满,则创建核心线程去执行
if(coreThreadList.size()<corePoolSize){
//创建线程
Thread thread = new CoreThread();
//添加线程
coreThreadList.add(thread);
//开启线程
thread.start();
}
//任务进入任务队列
//如果可以在不违反容量限制的情况下立即将指定的元素插入到此队列中,成功时返回 true,
//如果当前没有可用空间,则返回 false。当使用容量受限的队列时,此方法通常比 add 更可取,
//后者可能仅通过引发异常来无法插入元素。
if(workQueue.offer(command)){
return;
}
//任务队列满了的话,开始创建非核心线程了
//判断线程数是否超过最大线程树
if(coreThreadList.size()+supportThreadList.size()<maxPoolSize){
Thread supportThread = new SupportThread();
supportThreadList.add(supportThread);
supportThread.start();
}
//阻塞队列还是放不下的话,就要执行拒绝策略了
if (!workQueue.offer(command)) {
//执行拒绝策略
rejectHandle.reject(command, this);
}
}
public void executeSafe(Runnable command){
synchronized (command){
//如果核心线程数还没满,则创建核心线程去执行
if(coreThreadList.size()<corePoolSize){
//创建线程
Thread thread = new CoreThread();
//添加线程
coreThreadList.add(thread);
//开启线程
thread.start();
}
//任务进入任务队列
//如果可以在不违反容量限制的情况下立即将指定的元素插入到此队列中,成功时返回 true,
//如果当前没有可用空间,则返回 false。当使用容量受限的队列时,此方法通常比 add 更可取,
//后者可能仅通过引发异常来无法插入元素。
if(workQueue.offer(command)){
return;
}
//任务队列满了的话,开始创建非核心线程了
//判断线程数是否超过最大线程树
if(coreThreadList.size()+supportThreadList.size()<maxPoolSize){
Thread supportThread = new SupportThread();
supportThreadList.add(supportThread);
supportThread.start();
}
//阻塞队列还是放不下的话,就要执行拒绝策略了
if (!workQueue.offer(command)){
//执行拒绝策略
rejectHandle.reject(command,this);
}
}
}
List<Thread> coreThreadList=new ArrayList<>();
List<Thread> supportThreadList=new ArrayList<>();
//核心线程:会一直执行
class CoreThread extends Thread{
@Override
public void run() {
//死循环,一直存活
while(true){
//阻塞从任务队列里拿任务
try {
Runnable task = workQueue.take();
//运行任务
task.run();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
//辅助线程,有存活时间
class SupportThread extends Thread{
@Override
public void run() {
//死循环,一直存活
while(true){
//阻塞从任务队列里拿任务
try {
// timeout – how long to wait before giving up
Runnable task = workQueue.poll(timeout,timeUnit);
//如果一直拿不到任务就返回null
if (task==null){
break;
}
//运行任务
task.run();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}


}

线程池八大参数

1
2
3
4
5
6
7
8
this.workQueue = workQueue;
this.corePoolSize=corePoolSize;
this.maxPoolSize=maxPoolSize;
this.timeUnit = timeUnit;
//非核心线程最大活跃时间
this.timeout = timeout;
this.rejectHandle = rejectHandle;
this.线程工厂

线程池核心流程

核心就阻塞队列的三个方法

  • Object take() 一直阻塞获取队列的元素
  • Object poll(timeOut,TimeUtil) 在规定时间内阻塞获取任务,如果获取不到就返回 null
  • boolean offer() 往队列里塞任务,如果塞不进去就返回false 塞得进去就返回true
  1. 先判断当前线程数大小是否小于corePoolSize,是的话创建核心线程数,不是的话就直接执行2
  2. 把任务塞到队列里,看任务是否塞得进去
  3. 塞得进去返回,塞不进去判断TreadSize是否< maxSize
  4. 是:创建非核心线程
  5. 再一次把任务塞进队列里,判断是否能成功,失败就触发拒绝策略

拒绝策略

  • AbortPolicy****:丢弃任务并抛出 RejectedExecutionException 异常,这是默认的策略。
  • DiscardOldestPolicy:丢弃队列最前面的任务,执行后面的任务
  • CallerRunsPolicy:由调用线程处理该任务
  • DiscardPolicy:也是丢弃任务,但是不抛出异常,相当于静默处理。

线程池参数设置?

线程池参数设置

线程池默认有线程吗

没有。如果想要先创建好一些线程,来提高性能,我们可以通过预热机制,preStartThread or

preStartAllCoreThread来预热线程,然后线程池等着接任务就行了

线程池如何保证线程安全操作的

keepAliveTime对核心线程有效吗

默认无效,但我们可以开启 allowedCoreThreadTimeOut 来生效

如果线程池里的任务出异常了,我们是否可以捕获这个异常,且这个工作线程会不会被销毁

可以捕获,但需要手动捕获。如果不捕获,线程就相当于崩溃了工作线程会被销毁,且后面会重新new一个出

来,这是线程的恢复机制

如何对线程池的内存使用情况进行一个预估

线程池的任务如何进行持久化

引入MQ,发到MQ,利用MQ消费者的ack机制,防止没执行的任务在线程池所在的机器宕机时发生丢失

线程死循环获取任务,会浪费cpu吗?

  • 如果是忙轮询(busy loop),线程会一直占用 CPU 核,确实会浪费 CPU,哪怕没有任务也会不断执行指令。
  • 如果加了sleep / wait / condition variable 机制,那么线程会在没有任务时阻塞,让 CPU 去干别的事,这样几乎不浪费 CPU。

所以关键是有没有阻塞机制
比如 BlockingQueue.take() 就会在没任务时阻塞,不会忙等

while(true){

//取不到任务就阻塞住

task = queue.take();

}

take():

public E take() throws InterruptedException {

//中断锁,即不会死等到拿到锁,期间能响应中断

lock.lockInterruptibly();

try {

    while (count == 0) { // 队列空

        notEmpty.await(); // 等待条件满足

    }

    return dequeue();

} finally {

    lock.unlock();

}

}

不断沉睡唤醒,内核态和用户态不断切换,会不会有开销?

会有,但相对 CPU 忙等来说,这个开销小很多。

  • 沉睡:线程会调用系统调用(syscall)让内核把它挂起(从用户态切到内核态)。
  • 唤醒:任务到来时,内核会切回用户态让线程运行。
    这个切换属于上下文切换,大概是微秒级的开销,除非唤醒特别频繁,否则影响不大。

线程池有什么缺陷?

常见缺陷:

  1. 资源占用:线程是重量级对象,占内存(线程栈)和内核资源。
  2. 线程数固定:线程池满了,新任务要么排队,要么拒绝,容易导致延迟变大。
  3. 任务阻塞问题:如果线程执行的任务是阻塞型(特别是 I/O 阻塞),会占住线程,导致其他任务排队等待,甚至造成线程池“假死”。
  4. 调优复杂:核心线程数、队列长度、拒绝策略等要根据业务特点调,不同场景差异大。

IO密集型任务适合放线程池吗?

注意两点:

线程的核心任务就是执行 CPU 指令,因为这是它能对世界产生效果的唯一方式。

IO操作不是 CPU 做的运算,而是 外设/设备控制器 做的工作。CPU 只负责发起操作检查状态,真正的数

据传输是硬件自己完成的。

那么

I/O 密集型任务不适合直接放传统线程池,因为线程容易被阻塞占用,导致线程池空转和任务堆积。更适合异

步 I/O 或事件驱动模型,让线程资源得到充分利用。


IO密集型任务为什么不适合放线程池?

  • 线程池的线程是稀缺资源,而 I/O 阻塞期间,线程只是“等数据”,没用 CPU 干活。
  • 阻塞 I/O 会让有限的线程数被浪费掉,新任务只能等它们完成。
  • 解决办法是异步化加大线程池线程数(但这样会增加上下文切换和内存开销)。

画板

你说IO任务堵住了,为什么会堵住?

  • 阻塞 I/O(Blocking I/O)调用,比如 read()accept()recv(),在数据没到之前,线程会停在这里等。
  • 数据没到可能是因为:
  1. 对方(客户端/服务器)没发送数据。
  2. 网络延迟高。
  3. 磁盘读写速度慢(比如从 HDD 读大文件)。
  • 在阻塞期间,这个线程既不释放线程池的线程,也不能去干别的事,导致任务堆积。

关于如何尽快释放宝贵资源,业务最佳实践,dubbo服务端线程模型

jdk线程池的一个bug,但官方没修复,把它当成了一个feature,但我认为这其实是一个设计不好的地方

netty的eventloopgroup

核心线程数为0的场景有哪些?

核心线程数和最大线程数一样,会有什么场景?

线程池优化

利用cpu亲和性优化上下文切换

什么地方用到了,netty

其他类线程池

  • forkjionpool
  • Tomcat pool
  • eventLoopGroup

forkjionpool

双端队列

头和尾都可以进行添加和删除操作的队列,本质就是stack和queue的结合

工作窃取算法

工作窃取算法是一种动态、负载均衡高效的多线程任务分配机制,空闲线程可主动“窃取”其它线程未完成任务,使系统资源利用率最大化。


举一个例子:

假设有线程A/B/C,每个线程一开始都有一份任务队列:

- <font style="color:rgb(78, 78, 78);">线程A:任务A1, A2, A3</font>
- <font style="color:rgb(78, 78, 78);">线程B:任务B1, B2</font>
- <font style="color:rgb(78, 78, 78);">线程C:任务C1</font>
- <font style="color:rgb(78, 78, 78);">线程C完成C1后,将自己任务队列清空。</font>
- <font style="color:rgb(78, 78, 78);">它随机选取别的线程(比如线程A),“偷”走A的最后一个任务A3执行。</font>
- <font style="color:rgb(78, 78, 78);">这样被称为“工作窃取”。</font>

线程安全的保证:

执行流程及原理:

什么时候用threadpool,什么时候用forkjoin?

可重入锁

Lock的实现类都是通过 聚合了一个同步队列的子类来 实现线程访问的控制

类基本结构

方法


非公平锁的lock先自旋一次,成功就获取锁,失败再正常获取

tryAcquire调用父类Sync的非公平获取方法

公平锁的lock,与非公平锁的唯一区别就是多了个hasQueuePredecessors()方法判断等待队列是否有有效节点

释放锁的流程,因为是可重入锁,会通过state判断是否为0来决定是否释放锁

ReentrantLock的上锁流程

非公平锁:总的来说就两bu:每个线程都会尝试获取锁,获取锁失败就进入阻塞队列

    * <font style="color:rgb(0, 0, 0);">一开始会先通过CAS操作判断是否可以把state从0改为1,如果可以,则表示加锁成功,并通过set</font><font style="color:rgb(37, 41, 51);">ExclusiveThread()将exclusiveThread设置为当前线程</font><font style="color:rgb(0, 0, 0);">;</font>
    * <font style="color:rgb(0, 0, 0);">cas操作不成功,那么就会进入acquire(1)方法 </font>
    * <font style="color:rgb(0, 0, 0);">然后回进入tryAcquire方法,这时候还是会通过CAS去获取锁,看看getState是否为0,如果			</font>

公平锁:不管3721,都给我先入队列

    * 

为什么ReentrantLock,synchronized默认要为非公平锁

提升锁竞争的性能,如果是公平锁的话,往往要比非公平锁多出一个进入队列阻塞等待,然后再被唤醒。这会

涉及到一个内核态的切换,这对性能是有很大影响。而非公平锁的话,当前线程正好处于上一个线程释放锁时

的临界点,也就意味着当前线程不需要切换到内核态,虽然说对等待中的线程不公平,但这大大提高了锁竞争

时的性能

强引用

JAVA中最常见的一种引用,强引用的对象就算出现了OOM,JVM也不会对他进行回收的。当一个对象被强引用指向时,处于可达状态,即使它永远不会被用到,也不会被JVM的回收机制回收的。

对于普通对象而言,只要他超出了引用范围or手动把引用变量置null,它就可以认为被JVM回收

软引用

内存不够时才回收,够时就不回收

适用场景

假如有一个应用需要读取大量的本地图片:

  • 如果每次读取图片都从硬盘读取则会严重影响性能,
  • 如果一次性全部加载到内存中又可能造成内存溢出。

这时候就可以利用软引用的内存不够才回收的特性,采用一个Map把图片路径和图片内容关联起来,可以有效避免OOM

Map<String, SoftReference> imageCache = new HashMap<String, SoftReference>();


弱引用

WeakReference

不管内存够不够用,只要JVM一触发回收,弱引用指向的对象就会被回收

虚引用

必定会被回收,一般配合引用队列使用,可用于监控回收

java内存模型

本身只是一个抽象概念,只是一种规范,用来实现线程与主内存之间的关系还有就是屏蔽各个硬件平台和操作

系统的内存访问差异

关键技术点都是围绕多线程的原子性,可见性和有序性展开的

在该模型下,多线程对变量的读写过程

所有的共享变量是存储在物理主内存中的

而每个线程都有自己独立的工作内存,里面保存着该线程使用到的变量的副本

线程对共享变量的读写都得先在工作内存中操作,后写回主内存,不能直接在主内存中读写

happens-before

他就是说在JMM中,如果一个操作执行的结果需要对另外一个操作可见性,那么这两个操作之间

必然存在happens-before关系

有什么用?

有没有发现,我们没有时刻添加volatile和synchronized来完成程序,这是因为java语言中JMM原则下有一个happenss-before原则限制和规则

依赖这个原则,我们可以通过几条简单的规则一下子解决并发下两个操作之间是否可能存在冲突的有

所有问题