问题来源https://www.bilibili.com/read/cv4654469

线程参数的含义?

估计是想问线程池参数的含义吧

图1
图1

int corePoolSize:常驻线程数
int maximumPoolSize:线程池同时执行的最大线程数,>=1
long keepAliveTime:空闲线程的存活时间
TimeUnit unit:keepAliveTime的单位
BlockingQueueworkQueue:被提交等待被执行的任务
ThreadFactory threadFactory:工作线程的线程工厂
RejectedExecutionHandler handler:线程池拒绝策略

线程池拒绝策略

1
2
3
4
5
6
7
8
9
当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize时,
如果还有任务到来就会采取任务拒绝策略,通常有以下四种策略:
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。

ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常。

ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务

ThreadPoolExecutor.CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务

–可能引申线程个数的设置分为CPU密集型和IO密集型

线程池大小与 CPU 处理器的利用率之比可以用下面公式估算

ThreadPool
ThreadPool

CPU密集型多为cpu运算频繁的:设置CPU核数+1
IO密集型:设置cpu核数*10

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
/**
* Support class for thread pool size
*
* @author Nadeem Mohammad
*
*/
public final class ThreadPoolUtil {

private ThreadPoolUtil() {

}
/**
* Each tasks blocks 90% of the time, and works only 10% of its
* lifetime. That is, I/O intensive pool
* @return io intesive Thread pool size
*/
public static int ioIntesivePoolSize() {

double blockingCoefficient = 0.9;
return poolSize(blockingCoefficient);
}

/**
*
* Number of threads = Number of Available Cores / (1 - Blocking
* Coefficient) where the blocking coefficient is between 0 and 1.
*
* A computation-intensive task has a blocking coefficient of 0, whereas an
* IO-intensive task has a value close to 1,
* so we don't have to worry about the value reaching 1.
* @param blockingCoefficient the coefficient
* @return Thread pool size
*/
public static int poolSize(double blockingCoefficient) {
//cpu核数
int numberOfCores = Runtime.getRuntime().availableProcessors();

int poolSize = (int) (numberOfCores / (1 - blockingCoefficient));
return poolSize;
}
}

Redis的使用,分布式锁的实现

  1. 数据库乐观锁;
  2. 基于Redis的分布式锁;
  3. 基于ZooKeeper的分布式锁
  4. 算了直接用redisson的红锁做吧,引入API接口一调就完了,分布式锁还有守护线程问题,A锁阻塞没有完成任务时,B锁进入导致最后释放了B锁问题,直接调API吧[2020年9月18日15:07:03]

一 基于数据库
a.数据库建一张表,字段方法名并且作为唯一性,当一个方法执行时插入,则相当于获得锁,其他线程将无法访问,方法执行完则释放锁。

但是上面这种存在问题:

1、数据库单点,出现故障则将导致系统不可用。

2、没有失效时间,一旦操作方法异常,导致一直没有解锁,也将导致其他不可用用。

b.使用select * from user u where username = ‘’ for update 来对记录加上排他锁。操作完成后使用commit命令释放锁。
二基于缓存 redis

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
public class RedisTool {

private static final String LOCK_SUCCESS = "OK";
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "PX";

/**
* 尝试获取分布式锁
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @param expireTime 超期时间
* @return 是否获取成功
*/
public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {

String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);

if (LOCK_SUCCESS.equals(result)) {
return true;
}
return false;

}

}

三基于zk
大致思路:每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。 判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。 当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。

ZK中创建和删除节点只能通过Leader服务器来执行,然后将数据同步到所有的Follower机器上,所以性能上不如基于缓存实现。
综合比较:1.3性能低,推荐redis
如果对数据有强一致性要求,不能放缓存

TCP 三次握手和四次挥手

为了防止服务器端开启一些无用的连接增加服务器开销以及防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误。
三次握手也是最少次数的保证

图2
图2

①客户端发送报文===>
②服务端收到报文,结束监听,返回一段报文
③客户端确认收到TCP报文,并返回最后一段TCP报文
即SYN建立连接报文与ACK确认接收报文是在同一次”握手”当中传输的,所以”三次握手”不多也不少,正好让双方明确彼此信息互通
所谓的四次挥手即TCP连接的释放(解除)。连接的释放必须是一方主动释放,另一方被动释放
都是由客户端发起

TCP4次挥手即TCP链接的释放

图3
图3

①客户端想要释放链接,向服务端发送一段TCP报文
②服务器端接受,表示确认,进入半关闭状态,并返回一段
前”两次挥手”既让服务器端知道了客户端想要释放连接,也让客户端知道了服务器端了解了自己想要释放连接的请求。于是,可以确认关闭客户端到服务器端方向上的连接了
③服务器端确认报文,释放链接,再次向客户端发送
④客户端收到报文,确认准备释放,向服务端发送一段报文
TCP释放连接时之所以需要“四次挥手”,是因为FIN释放连接报文与ACK确认接收报文是分别由第二次和第三次”握手”传输的。为何建立连接时一起传输,释放连接时却要分开传输

volatile

1.解决的是多核CPU带来的缓存与CPU之间数据的可见性
JMM:java内存模型
1.线程解锁前,必须把共享变量刷新回主内存
2.线程加锁前,必须读取主内存的最新值到自己的工作内存
3.加锁与解锁必须是同一把锁

图4
图4

volatile实现内存指令重排,保证可见性和禁止指令重排,
可保证一段内存中一个变量的原子性,原生类型都是原子性的。所以java中 volatile long,volatile double都是线程安全的

9.乐观锁,悲观锁

乐观锁(Optimistic Lock):

1
2
每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。
两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果经常产生冲突,上层应用会不断的进行retry,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适

悲观锁(Pessimistic Lock):

1
每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。

10.HashMap结构,是否线程安全?ConcurrentHashMap如何保证线程安全。

HashMap在java1.7之前底层数据结构是数组+链表,1.8之后是数组+链表+红黑树,
在1.7以前的put方法采用的是头插法,当hash碰撞次数到达8,且桶内元素到达64个的时候形成链表,但是在极端情况下会造成链表过长,效率变低,并且在rehash的时候,头插法会造成回环链首尾相连,形成死锁,在java1.8以后采用红黑树,除了添加效率都高,是线程不安全的,不安全示例

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
public class HashMapTest {

public static void main(String[] args) {
HashMapThread thread0 = new HashMapThread();
HashMapThread thread1 = new HashMapThread();
HashMapThread thread2 = new HashMapThread();
HashMapThread thread3 = new HashMapThread();
HashMapThread thread4 = new HashMapThread();
thread0.start();
thread1.start();
thread2.start();
thread3.start();
thread4.start();
}
}

class HashMapThread extends Thread {
private static AtomicInteger ai = new AtomicInteger();
private static Map<Integer, Integer> map = new HashMap<>();

@Override
public void run() {
while (ai.get() < 1000000) {
map.put(ai.get(), ai.get());
ai.incrementAndGet();
}
}
}

1.通常代替HashMap的安全由HashTable代替,但是多线程下他的put.get方法都是synchronized,效率太低,
2.Collections.synchronizedMap(),底层仍是synchronized
3.java9实现

图5
图5

ConcurrentHashMap 与 ConcurrentSkipListMap
ConcurrentHashMap 加锁
ConcurrentSkipListMap 不需要加锁,浪费空间,
4.ConcurrentHashMap
ConcurrentHashMap如何保证线程安全,在1.7以前由划分segment分段锁机制,共计16个并发级别,隔离级别太大,有很多空间就浪费了,太小就段内的元素过多
1.8以后是cas算法C语言写得,无锁算法,put添加的时候,链表+红黑树
put方法(无锁添加)

图6
图6

之前用过哪些设计模式

举例设计模式的单一职责原则,开闭原则等,模板模式,策略模式
目前项目再用的是责任链设计模型,像动态代理,装饰者,工厂模式,在Spring的源码中都有体现,责任链模式旨在降低处理请求流程的耦合
责任链模式.
二面

说一下B树和B+树的区别

InnoDb的索引实现

B+tree

为什么是用B+tree

B+树中的B代表平衡(balance),而不是二叉(binary)
二叉查找树
二叉树具有以下性质:左子树的键值小于根的键值,右子树的键值大于根的键值。
平衡二叉树
如果在AVL树中进行插入或删除节点,可能导致AVL树失去平衡,这种失去平衡的二叉树可以概括为四种姿态:LL(左左)、RR(右右)、LR(左右)、RL(右左)。它们的示意图如下:

图

B+Tree
B+Tree相对于B-Tree有几点不同:
非叶子节点只存储键值信息。
所有叶子节点之间都有一个链指针。
数据记录都存放在叶子节点中。
查询速度快,但是占用空间
索引结构:B-Tree B+Tree B:balance
B-Tree:平衡二叉树
特点:
1.具有数据节点
2.指向下层指针
3.指向数据指针
缺页查询,产生IO
B+Tree:
特点:
1.具有数据节点
2.指向下层指针
命中数据3层查找后查询数据指针
加载更快,产生更少IO
效率:BTree更高,但从IO角度,Mysql选择B+Tree

说一下HashMap的实现,扩容机制,扩容时如何保证操作

put 方法比较复杂,实现步骤大致如下:
1、先通过 hash 值计算出 key 映射到哪个桶。
2、如果桶上没有碰撞冲突,则直接插入。
3、如果出现碰撞冲突了,则需要处理冲突:
(1)如果该桶使用红黑树处理冲突,则调用红黑树的方法插入。
(2)否则采用传统的链式方法插入。如果链的长度到达临界值,则把链转变为红
黑树。
4、如果桶中存在重复的键,则为该键替换新值。
5、如果 size 大于阈值(8),则进行扩容

根据hash算法得到hash码值,也就是数组的索引值,在判断是否有对象,如果没有则放入
如果有则先通过equals比较两个对象的内容,如果内容一样,则覆盖value,
如果内容不一样,形成链表,1.7后加的放前面,这种情况叫做hash碰撞,这种情况我们是尽可能避免的,如果这里的元素过多的话,插入效率过低,为了避免的话,重写hashcode和equals方法保持一致,这种情况避免不了
加载因子,当到达元素个数的0.75,进行扩容,扩容则每个元素重新运算位置,,如果到达100%其他位置可能会不存入,如果太小,则频繁扩容,可浪费空间。这样碰撞的概率会降低,但是极端情况下还是需要查询每个元素比较,效率极低。
1.8以后,数组+链表+红黑树
当碰撞的个数大于8时,并且总容量大于64时,将链表转为红黑树,除了添加以外其他的效率都高,jdk1.8加到链表末尾,扩容以后不需要运行hash算法计算hashcode值。原来hash表的总长度,加上hash表的现在的位置,就放到第8个位置即可。
3.redis扩容机制(渐进式单线程扩容)
Redis是一个键值对(key-value pair)数据库服务器,Redis服务器结构是redis.h/redisServer结构表示,Redis服务器中的所有数据库保存在db数组中,数据库的结构是redis.h/redisDb,其中,redisDb结构的dict字典保存了数据库中的所有键值对,所以,说起Redis的扩容机制,指的就是字典中哈希表的rehash(重新散列)操作
在实际开发过程中,这个rehash 操作并不是一次性、集中式完成的,而是分多次、渐进式地完成的。
    渐进式rehash 的详细步骤:
      1、为ht[1] 分配空间,让字典同时持有ht[0]和ht[1]两个哈希表
      2、在几点钟维持一个索引计数器变量rehashidx,并将它的值设置为0,表示rehash 开始
      3、在rehash 进行期间,每次对字典执行CRUD操作时,程序除了执行指定的操作以外,还会将ht[0]中的数据rehash 到ht[1]表中,并且将rehashidx加一
      4、当ht[0]中所有数据转移到ht[1]中时,将rehashidx 设置成-1,表示rehash 结束
    采用渐进式rehash 的好处在于它采取分而治之的方式,避免了集中式rehash 带来的庞大计算量。
作者:13693160765
链接:https://www.jianshu.com/p/ea5a747ade5d
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

SpringAop,ioc的原理,如何解决循环依赖

SpringIoc可以对我们应用程序中的java对象做一个集中化的管理,从而使我们从繁琐的new Object();中解脱出来
Spring中AOP的有两种实现方式:
1、JDK动态代理
2、Cglib动态代理
在没有修改原有类的代码的情况下,对原有类的功能进行了增强
静态代理模式:静态代理说白了就是在程序运行前就已经存在代理类的字节码文件,代理类和原始类的关系在运行前就已经确定
动态代理模式:动态代理类的源码是在程序运行期间通过JVM反射等机制动态生成,代理类和委托类的关系是运行时才确定的
使用jdk生成的动态代理的前提是目标类必须有实现的接口。但这里又引入一个问题,如果某个类没有实现接口,就不能使用JDK动态代理,所以Cglib代理就是解决这个问题的
Cglib使用的前提是目标类不能为final修饰。因为final修饰的类不能被继承。
核心原理是使用动态代理模式在方法执行前后或出现异常时加入相关逻辑。
通过定义和前面代码我们可以发现3点:
1.AOP是基于动态代理模式。
2.AOP是方法级别的(要测试的方法不能为static修饰,因为接口中不能存在静态方法,编译就会报错)。
3.AOP可以分离业务代码和关注点代码(重复代码),在执行业务代码时,动态的注入关注点代码。切面就是关注点代码形成的类。

循环依赖解决

1.在字段上使用@Autowired注解,让Spring决定在合适的时机注入
2.用基于setter方法的依赖注入

两个线程对变量i进行加1操作,结果如何?为什么?如何解决?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static int i=0;
public static void add(){
i=i+1;
action();
}
public static void action(){
System.out.println("==>"+Thread.currentThread().getName()+":"+i);
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(SysUserServiceImpl::add,"t1");
Thread t2= new Thread(SysUserServiceImpl::add,"t2");
t1.start();
t2.start();
}

==>t1:1
==>t2:2

==>t1:2
==>t2:1

==>t1:2
==>t2:2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
线程安全问题,对共享变量进行修改
改进方法1:
public static volatile int i=0;
public static void add(){
synchronized (SysUserServiceImpl.class){
i=i+1;
action();
}
}
public static void action(){
System.out.println("==>"+Thread.currentThread().getName()+":"+i);
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(SysUserServiceImpl::add,"t1");
Thread t2= new Thread(SysUserServiceImpl::add,"t2");
t1.start();
t2.start();

}

synchronized在多jvm情况下不生效,且效率低下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
方法2
private static AtomicInteger num = new AtomicInteger(0);
public static void add(){
int i = num.getAndIncrement();
action(i);
}
public static void action(int i){
System.out.println("由"+i+"==>"+Thread.currentThread().getName()+":"+num);
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(SysUserServiceImpl::add,"t1");
Thread t2= new Thread(SysUserServiceImpl::add,"t2");
t1.start();
t2.start();
}

方法3

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
public static volatile int i=0;
public static void action(){
System.out.println("==>"+Thread.currentThread().getName()+":"+i);
}

static Lock lock=new ReentrantLock();
public static void inc() {
lock.lock();
try {
Thread.sleep(1);
i=i+1;
action();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(SysUserServiceImpl::inc,"t1");
Thread t2= new Thread(SysUserServiceImpl::inc,"t2");
t1.start();
t2.start();

}

CAS概念,原子类实现

CAS锁是比较偏重的操作?
CAS在操作锁时,执行比较并交换操作,相对synchronized瘦锁是比较重的锁,偏向锁在这里避免了CAS操作。UseBiaseLocking对synchronize有用
比较并交换,判断取出内存中某时刻的数据并在当下时刻进行交换,缺点:循环时间长,只能保证一个共享变量的原子操作,引来ABA问题?
CAS核心是由native修饰的Unsafe类,其中valueOff为内存偏移量地址,变量由volatile修饰。
private static final Unsafe unsafe
private volatile int value;
unsafe类是CAS的核心类,是由C语言native方法来访问的

1
2
3
4
5
6
7
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}

其中getAndAddInt方法,this表示当前对象valueoffset表示内存中的偏移地址,delta是当前value增加的变量
CAS即比较当前值与预设值,交换并增加,如果与预想一致就交换,否则再次自旋,所以带来循环开销问题,进而引来ABA问题。
原子类的话经典类:AtomicInteger,其共享变量是由volatile修饰的,
getAndIncrement是unsafe类操作,底层也是cas

1
2
3
4
5
6
7
8
9
10
11
12
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}

public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

return var5;
}

synchronized

synchronized和Lock有什么区别?
①:synchronized是JVM层面实现的,java提供的关键字,Lock是API层面的锁。
②:synchronized不需要手动释放锁,底层会自动释放,
Lock则需要手动释放锁,否则有可能导致死锁
③:synchronized等待不可中断,除非抛出异常或者执行完成
Lock可以中断,通过interrupt()可中断
④:synchronized是非公平锁
Lock是默认公平锁,当传入false时是非公平锁
⑤:synchronized不可绑定多个条件
Lock可实现分组唤醒需要唤醒的锁
monitorenter
monitorexit
synchronized通过监控对象来完成,本质是锁一个对象
同步方法

1
2
3
4
5
public class Demo {  
public synchronized void method() {
System.out.println("synchronized....");
}
}

修饰方法与修饰代码块产生字节码不同
如何实现lock,AQS:AbstractQueuedSynchronizer,AQS是ReentrantLock的核心实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}

ReentrantLock的子类Sync类的final static子类FairSync和NonFairSync用于支持公平锁和非公平锁。
AQS的tryAcquire()和FairSync的tryAcquire()判定是否为公平锁,其实现也是偏向锁UseBiaseLock的实现

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
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;

final void lock() {
acquire(1);
}

/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}

该方法首先会判断当前线程的状态,如果c==0 说明没有线程正在竞争锁。(反过来,如果c!=0则说明已经有其他线程已经拥有了锁)。如果c==0,则通过CAS将状态设置为acquires(独占锁的acquires为1),后续每次重入该锁都会+1,每次unlock都会-1,当数据为0时则释放锁资源。其中精妙的部分在于:并发访问时,有可能多个线程同时检测到c为0,此时执行compareAndSetState(0, acquires))设置,可以预见,如果当前线程CAS成功,则其他线程都不会再成功,也就默认当前线程获取了锁,直接作为running线程,很显然这个线程并没有进入等待队列。如果c!=0,首先判断获取锁的线程是不是当前线程,如果是当前线程,则表明为锁重入,继续+1,修改state的状态,此时并没有锁竞争,也非CAS,因此这段代码也非常漂亮的实现了偏向锁。