一、并发
1、并发与并行的区别
并发:在一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理器上运行,但任一个时刻点上只有一个程序在处理器上运行。
并行:多个事情一起做
并发的关键是你有处理多个任务的能力,不一定要同时。
并行的关键是你有同时处理多个任务的能力。
2、对volatile的理解?在什么地方使用过volatile?
它是轻量级的同步机制,具有3大特性:可见性、不保证原子性、防止指令重排序
使用双重检查锁和volatile实现单例模式
双重检查锁定模式首先验证锁定条件(第一次检查),只有通过锁定条件验证才真正的进行加锁逻辑并再次验证条件(第二次检查)。 —— Wiki
class Foo {
private Helper helper = null;
public Helper getHelper() {
if (helper == null) {
synchronized(this) {
if (helper == null) {
helper = new Helper();
}
}
}
return helper;
}
// other functions and members...
}
直觉上,这个算法看起来像是该问题的有效解决方案。然而,这一技术还有许多需要避免的细微问题。例如,考虑下面的事件序列:
- 线程A发现变量没有被初始化, 然后它获取锁并开始变量的初始化。
- 由于某些编程语言的语义,编译器生成的代码允许在线程A执行完变量的初始化之前,将变量指向部分初始化的对象。
- 线程B发现共享变量已经被初始化,并返回变量。由于线程B确信变量已被初始化,它没有获取锁。如果在A完成初始化之前共享变量对B可见(这是由于A没有完成初始化或者因为一些初始化的值还没有覆盖B使用的内存(缓存一致性)),程序很可能会崩溃。
DCL(双重检查锁)机制不一定线程安全,原因是有指令重排序的存在,加入volatile可以禁止指令重排。
创建对象的过程其实是由下面的3步完成(伪代码):
memory = allocate(); // 1.分配对象内存空间
instance(memory); // 2.初始化对象
instance = memory; // 3.设置instance指向刚分配的内存地址,此时instance! =null
由于步骤2和步骤3不存在数据依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中并没有改变,所以下面这种重排优化是允许的。
memory = allocate(); // 1.分配对象内存空间
instance = memory; // 3.设置instance指向刚分配的内存地址,此时instance! =null, 但是对象还没有初始化完成!
instance(memory); // 2.初始化对象
由于指令重排只会保证串行语义的执行的一致性(单线程),但并不会关心多线程间的语义一致性。所以当一条线程访问instance不为null时,由于instance实例未必已初始化完成,也就造成了线程安全问题。所以需要加上volatile字段防止指令重排序。
class Foo {
private volatile Helper helper = null;
public Helper getHelper() {
Helper result = helper;
if (result == null) {
synchronized(this) {
result = helper;
if (result == null) {
helper = result = new Helper();
}
}
}
return result;
}
// other functions and members...
}
3、JMM是什么
3.1 JMM简介
JMM(Java内存模型Java Memory Model, 简称JMM)本身是一种抽象的概念并不真实存在,它描述的是一组规则或规范,Java内存模型定义了多线程之间共享变量的可见性以及如何在需要的时候对共享变量进行同步。
JMM关于同步的规定:
- 线程解锁前,必须把共享变量的值刷新回主内存
- 线程加锁前,必须读取主内存的最新值copy到自己的工作内存
- 加锁解锁是同一把锁
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图:
从上图来看,线程A与线程B之间如要通信的话,必须要经历下面2个步骤:
- 首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去。
- 然后,线程B到主内存中去读取线程A之前已更新过的共享变量。
这两个步骤实质上是线程A在向线程B发送消息,而且这个通信过程必须要经过主内存。JMM通过控制主内存与每个线程的本地内存之间的交互,来为java程序员提供内存可见性保证。
JMM是围绕并发编程中原子性、可见性、有序性三个特征来建立的。
3.2 原子性
某个线程在做某个具体的业务的时候,中间不可以被分割,要么一起成功要么一起失败。
Java 基本类型数据的访问大多数都是原子操作,例如:long 和 double 类型是 64 位,在 32 位 JVM 中会将 64 位数据的读写操作分成两次 32 位来处理,所以 long 和 double 在 32 位 JVM 中是非原子操作,也就是说在并发访问时是线程非安全的,要想保证原子性就得对访问该数据的地方进行同步操作,譬如 synchronized 等。
3.3 可见性
就是说当一个线程对共享变量(主内存中)做了修改后其他线程可以立即感知到该共享变量的改变,从 Java 内存模型我们就能看出来多线程访问共享变量都要经过线程工作内存到主存的复制和主存到线程工作内存的复制操作,所以普通共享变量就无法保证可见性了;Java 提供了 volatile 修饰符来保证变量的可见性,每次使用被volatile修饰的变量都会主动从主内存中取,除此之外 synchronized、Lock、final 都可以保证变量的可见性。
3.3.1 synchronized如何保证的可见性?
JMM关于synchronized的两条规定:
- 线程解锁前,必须把共享变量的最新值刷新到主内存中;
- 线程加锁时,先清空工作内存中共享变量的值,因此使用共享变量时需要从主内存重新读取最新值。
https://blog.csdn.net/hxqneuq2012/article/details/52190784
3.4 有序性
计算机在执行程序时,为了提高性能,编译器和处理器的常常会对指令做重排,一般分以下3种
单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致。
处理器在进行重排序时必须要考虑指令之间的数据依赖性,比如:
public class A {
int a,b,c,d;
public void func() {
a = 1; // 语句1
b = 2; // 语句2
c = a * a; // 语句3
d = a + b; // 语句4
}
// 可能重排的情况有: 1234 1324 2143 但是3、4不能在前面,因为指令之间有依赖性
}
多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。比如:
pulic class A {
int a = 0;
boolean flag = false;
// 线程1调用func1
public void func1() {
a = 1; // 语句1
flag = true; // 语句2
}
// 线程2调用func2
public void func2() {
if (flag) {
a = a + 5;
sout("a:" + a);
}
}
}
这段代码理论上有两种情况:
1、func2先执行,什么都不打印
2、func1先执行,func2打印:a:6
但是由于指令重排与线程上下文切换将会导致出现先执行语句2,之后被线程2抢占下去,从而导致输出的是a:5
volatile可以实现禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象。
先了解一个概念,内存屏障(Memory Barrier) 又称内存栅栏,是一个CPU指令,它的作用有两个:一是保证特定操作的执行顺序,二是保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。
由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。内存屏障另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。
4、CAS是什么?CAS的底层原理?谈谈你对Unsafe类的理解?AtomicInteger为什么使用CAS而不是synchronized?CAS的缺点?
4.1 CAS是什么
CAS(compare and swap 比较并交换):
CAS是原子操作的一种,可用于在多线程编程中实现不被打断的数据交换操作,从而避免多线程同时改写某一数据时由于执行顺序不确定性以及中断的不可预知性产生的数据不一致问题。该操作通过将内存中的值与指定数据进行比较,当数值一样时将内存中的数据替换为新的值。 —— Wiki
CAS的含义为比较并交换,它是一条CPU并发原语(CPU指令),它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。
在Java中,CAS操作由sun.misc.Unsafe类中方法进行调用,当调用其中的方法时,JVM会帮我们实现出CAS汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。
4.2 CAS的底层原理
Unsafe类是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存中的数据。Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中CAS操作的执行依赖于Unsafe类的方法。
注意Unsafe类中的方法基本都是native修饰的,也就是说Unsafe类中的方法是直接调用操作系统底层资源执行相应任务。
AtomicInteger的自增操作是调用下面的这个方法来实现的:
/**
* @param var1 AtomicInteger对象本身
* @param var2 该变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的
* @param var4 需要变动的数量
* @return 内存中的当前值(不是修改后的)
*/
public final int getAndAddInt(Object var1, long var2, int var4) {
// 通过var1、var2找出的主内存中真实的值
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
自旋 + CAS操作(乐观锁):
1、从内存中获取当前的值
2、调用CAS比较并替换,如果失败(其它线程抢先修改了),那么就循环再次尝试
3、由于var5这个值被volatile修饰了,如果它被修改其它线程立刻知道,所以this.getIntVolatile(var1, var2);
方法可以拿到最新的值,继续尝试CAS操作直到成功
4.3 CAS的缺点
- 如果CAS操作一直失败,那么就会一直在那死循环,会给CPU造成很大的开销
- 只能保证一个操作原子完成,如果想要多个原子操作就要借助锁了
- 单核CPU不适于使用自旋锁,这里的单核CPU指的是单核单线程的CPU,因为,在同一时间只有一个线程是处在运行状态
- ABA问题: 比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且线程two进行了一些操作将值变成了B,然后线程two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后线程one操作成功。尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。
如果要解决这个问题,可以采用原子时间戳引用来解决:
AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<Integer>(0, 1);
5、ConcurrentModificationException产生的原因
由于容器不是可并发的,所以在遍历的时候导致实际长度与之前的元素长度不同(集合中数据前后不一致),从而报错。
问题:为什么下面这种方式不会抛出异常?
答:因为移除的整好是倒数第二个元素;抛出异常的操作是在继续向下循环时调用next方法
时抛出,而移除了倒数第二个元素,hasNext方法
返回的false,就不在向下循环了,从而不会抛出异常。
ArrayList<Object> objects = new ArrayList<>();
objects.add("x");
objects.add("x1");
objects.add("x2");
for (Object object : objects) {
System.out.println("object = " + object);
if (object.equals("x1")) {
objects.remove("x1");
}
}
可以采用CopyOnWriteArrayList解决,详情请见:线程安全问题
6、公平锁与非公平锁
公平锁(Fair):加锁前检查是否有排队等待的线程,优先排队等待的线程,先来先得
非公平锁(Nonfair):加锁时不考虑排队等待问题,直接尝试获取锁,获取不到自动到队尾等待
非公平锁性能比公平锁高5~10倍,因为公平锁需要将排队的线程休眠,之后轮到它的时候再唤醒。
7、读写锁
如果允许并发读,写的时候不允许其它线程读写的时候,可以使用读写锁
ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
8、使用过CountDownLatch、CyclicBarrier、Semaphore吗
作用:线程排序
8.1 CountDownLatch
利用它可以实现类似计数器的功能。比如有一个任务A,它要等待其他4个任务执行完毕之后才能执行,此时就可以利用CountDownLatch来实现这种功能了。
火箭发射前的各种指标检测通过才能发射火箭
public class CountDownLatchDemo {
public static void main(String[] args) {
CountDownLatch countDownLatch = new CountDownLatch(4);
for (int i = 0; i < 4; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "天,过去了");
// 做完事情,拿下一把锁
countDownLatch.countDown();
}, Objects.requireNonNull(SeasonEnum.getSeasonDesc(i))).start();
}
try {
countDownLatch.await();
System.out.println(Thread.currentThread().getName() + "新的一年开始了!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 可以通过枚举将代码解耦,这样如果要改的时候,只需要更改枚举中的内容就行了
public enum SeasonEnum {
Spring(0, "春"),
Summer(1, "夏"),
Autumn(2, "秋"),
Winter(3, "冬"),
;
private Integer code;
private String desc;
SeasonEnum(Integer code, String desc) {
this.code = code;
this.desc = desc;
}
public Integer getCode() {
return code;
}
public String getDesc() {
return desc;
}
public static String getSeasonDesc(Integer code) {
for (SeasonEnum seasonEnum : SeasonEnum.values()) {
if (Objects.equals(seasonEnum.code, code)){
return seasonEnum.desc;
}
}
return null;
}
}
8.2 CyclicBarrier
字面意思回环栅栏,通过它可以实现让一组线程等待至某个状态之后再全部同时执行。叫做回环是因为当所有等待线程都被释放以后,CyclicBarrier可以被重用。
public class CyclicBarrierDemo {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(7, new Runnable() {
@Override
public void run() {
System.out.println("召唤神龙!");
}
});
for (int i = 0; i < 7; i++) {
final int tempInt = i;
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "收集到第" + tempInt + "颗龙珠");
// 收集到了之后,要在这里阻塞,等待其它的也收集好
try {
for (int i=0; i<3 ; i++){
System.out.println("第" + i + "次收集龙珠!");
// 当本次任务结束,会自动重置
cyclicBarrier.await();
}
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}
区别:CyclicBarrier做完的那个线程要阻塞,CountDownLatch做完的就可以先溜了
8.3 Semaphore
多个线程争夺多个资源(6辆车抢3个车位),synchronized是争夺一个资源。
public class SemaphoreDemo {
public static void main(String[] args) {
// 三个停车位
Semaphore semaphore = new Semaphore(3);
for (int i = 0; i < 6; i++) {
new Thread(() -> {
try {
// 获取信号量
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + "抢到车位!");
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName() + "离开车位!");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放信号量
semaphore.release();
}
}, i + "").start();
}
}
}
在生活场景中,不可能一辆车走了,立刻就有一辆车进来,所以可以这样改进:
import java.util.concurrent.Semaphore;
public class AbnormalSemaphoreSample {
public static void main(String[] args) throws InterruptedException {
Semaphore semaphore = new Semaphore(0);
for (int i = 0; i < 10; i++) {
Thread t = new Thread(new MyWorker(semaphore));
t.start();
}
System.out.println("Action...GO!");
// 释放给定数量的许可,将它们返回到信号量。
semaphore.release(5);
System.out.println("Wait for permits off");
// 返回此信号量中可用的当前许可数。
// 轮询调用availalePermits来检测信号量获取情况,这都是很低效并且脆弱的,通常只是用在测试或者诊断场景。
while (semaphore.availablePermits() != 0) {
Thread.sleep(100L);
}
System.out.println("Action...GO again!");
semaphore.release(5);
}
}
class MyWorker implements Runnable {
private Semaphore semaphore;
public MyWorker(Semaphore semaphore) {
this.semaphore = semaphore;
}
@Override
public void run() {
try {
semaphore.acquire();
System.out.println("Executed!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
如果Semaphore的数值被初始化为1,那么一个线程就可以通过acquire进入互斥状态,本质上和互斥锁是非常相似的。但是区别也非常明显,比如互斥锁是有持有者的,而对于Semaphore这种计数器结构,虽然有类似功能,但其实不存在真正意义的持有者,除非我们进行扩展包装。
8.4 CountDownLatch和CyclicBarrier二者有什么区别?
CountDownLatch是不可以重置的,所以无法重用;而CyclicBarrier则没有这种限制,可以重用。
CountDownLatch的基本操作组合是countDown/await。调用await的线程阻塞等待countDown足够的次数,不管你是在一个线程还是多个线程里countDown,只要次数足够即可。所以就像Brain Goetz说过的,CountDownLatch操作的是事件。
CyclicBarrier的基本操作组合,则就是await,当所有的伙伴(parties)都调用了await,才会继续进行任务,并自动进行重置。注意,正常情况下,CyclicBarrier的重置都是自动发生的,如果我们调用reset方法,但还有线程在等待,就会导致等待线程被打扰,抛出BrokenBarrierException异常。CyclicBarrier侧重点是线程(),而不是调用事件,它的典型应用场景是用来等待并发线程结束。
9、知道阻塞队列吗?
9.1 什么是阻塞队列
BlockingQueue是java.util.concurrent包下提供的线程安全的容器,它提供了下列队列访问方式:
- 当阻塞队列是空时,从队列中获取元素的操作将会被阻塞。
- 当阻塞队列是满时,往队列里添加元素的操作将会被阻塞。
9.2 都有啥
ArrayBlockingQueue:由数组结构组成的有界限塞队列。
LinkedBlockingQueue:由链表结构组成的有界(但大小默认值为Integer.MAX_ _VALUE)阻塞队列。
SynchronousQueue:不存储元素的阻塞队列,也即单个元素的队列。(队列中的元素有且仅有一个,在take()的时候才put(e))
PriorityBlockingQueue:支持优先级排序的无界阻塞队列。
DelayQueue:使用优先级队列实现的延迟无界阻塞队列。
LinkedTransferQueue:由链表结构组成的无界阻塞队列。
LinkedBlockingDeque:由链表结构组成的双向阻塞队列
9.3 怎么用
方法\类型 | 抛出异常组 | 返回boolen组 | 阻塞组(不建议使用) | 超时组(建议使用) |
---|---|---|---|---|
插入 | add(e) | offer(e) | put(e) | offer(e, time, unit) |
移除 | remove() | poll() | take() | poll(time, unit) |
检查顶部元素 | element() | Peek() | 无 | 无 |
抛出异常
- 当阻塞队列满时,再往队列里add插入元素会拋illegalStateException: Queue full
- 当阻塞队列空时,再往队列里remove移除元素会拋NoSuchElementException
返回boolen
- 插入方法,成功ture 失败false
- 移除方法,成功返回出队列的元素,队列里面没有就返回null
一直阻塞
- 当阻塞队列满时,生产者线程继续往队列里put元素,队列会一直阻塞 生产线程直到put数据or响应中断退出。
- 当阻塞队列空时,消费者线程试图从队列里take元素,队列会- .直阻塞消费者线程直到队列可用。
超时退出
- 当阻塞队列满时,队列会阻塞生产者线程一定时间,超过后限时后生产者线程会退出
9.4 用途
9.4.1 生产者消费者(线程交互)
1、使用ReentrantLock实现
public class ProducerAndConsumer2 {
public static void main(String[] args) {
A a = new A();
// 线程1负责生产
new Thread(() -> {
for (int i = 0; i < 5; i++) {
a.increment();
}
}).start();
// 线程2负责消费
new Thread(() -> {
for (int i = 0; i < 5; i++) {
a.decrement();
}
}).start();
}
private static class A {
private Integer num = 0;
// 代替synchronized
private Lock lock = new ReentrantLock();
// 代替wait、notify
private Condition condition = lock.newCondition();
void increment() {
lock.lock();
try {
// 如果数量等于0,才进行生产
while (num != 0) {
condition.await();
}
num++;
System.out.println(Thread.currentThread().getName() + ":" + num);
// 唤醒其它线程
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
void decrement() {
lock.lock();
try {
// 如果数量不等于0,才进行消费
while (num == 0) {
condition.await();
}
num--;
System.out.println(Thread.currentThread().getName() + ":" + num);
// 唤醒其它线程
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
}
注意:判断要使用while,而不是if,因为在多线程环境下被唤醒之后数量可能不是预计的(可能被其它线程改过),所以要在进行一次判断。
2、使用阻塞队列实现
public class ProducerAndConsumer3 {
private static BlockingQueue blockingQueue = new ArrayBlockingQueue(3);
public static void main(String[] args) {
// 创建一个生产者
new Thread(() -> {
for (int i = 0; i < 100; i++) {
try {
System.out.println("生产者生产成功状态:" + blockingQueue.offer(i, 2, TimeUnit.SECONDS));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
// 消费者
new Thread(() -> {
for (int i = 0; i < 1000; i++) {
try {
System.out.println("消费者消费了:" + blockingQueue.poll(2, TimeUnit.SECONDS));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
- 线程池
- 消息中间件
10、synchronized与Lock有什么区别?用Lock有什么好处?
1)原始构成
synchronized关键字属于JVM层面,编译成字节码有以下2个指令:
- monitor enter(底层是通过monitor对象来完成,其wait/notify等方法也依赖于monitor对象只有在同步块或方法中力能调wait/notify等方方法)
- monitorexit
Lock是具体类(java. util. concurrent. locks. Lock)是api层面的锁
2)使用方法
synchronized不需要用户去手动释放锁,当synchronized代码执行完后系统会自动让线程释放对锁的占用。
Reentrantlock则需要用户去手动释放锁若没有主动释放锁,就有可能导致出现死锁现象。需要Lock()和unlock()方法配合try/finally语句块来完成。
3)是否可中断
- synchronized不可中断,除非抛出异常或者正常运行完成
- ReentrantLock可中断,如果使用lockInterruptibly()获取锁,可以调用interrupt()中断
4)加锁是否公平
- synchronized非公平锁
- ReentrantLock两者都可以,默认非公平锁,构造方法可以传入boolean值,true为公平锁, false为非公平锁
5)锁绑定多个条件Condition
synchronized没有
ReentrantLock用来实现分组唤醒需要唤醒的线程们,可以精确唤醒,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。
11、Callable的使用
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<Integer> futureTask = new FutureTask<>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
return 10086;
}
});
new Thread(futureTask).start();
while (!futureTask.isDone()) {}
System.out.println(futureTask.get());
}
12、线程池
12.1 线程池7大参数:
1.corePoolSize:线程池的基本大小,即在没有任务需要执行的时候线程池的大小,并且只有在工作队列满了的情况下才会创建超出这个数量的线程。这里需要注意的是:在刚刚创建ThreadPoolExecutor的时候,线程并不会立即启动,而是要等到有任务提交时才会启动。
2.maximumPoolSize:线程池能够容纳同时执行的最大线程数,此值必须大于等于1。
3.keepAliveTime:多余的空闲线程的存活时间。
当前线程池数量超过corePoolSize时,当空闲时间达到keepAliveTime值时,多余空闲线程会被销毁直到只剩下corePoolsize个线程为止
4.unit: keepAliveTime的 单位。
5.workQueue:任务队列,被提交但尚未被执行的任务。
6.threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程一般用默认的即可。
7.handler:拒绝策略,表示当队列满了并且工作线程大于等于线程池的最大线程数和阻塞队列大小的和(maximumPoolSize + workQueue。size())时如何来拒绝未执行任务(Runable)的策略
1.在创建了线程池后,等待提交过来的任务请求。
2.当调用execute()方法添加一个请求任务时,线程池会做如下判断:
- 2.1如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;
- 2.2如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;
- 2.3如果这时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
- 2.4如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。
3.当一个线程完成任务时,它会从队列中取下一个任务来执行。
4.当一个线程无事可做超过一定的时间(keepAliveTime)时,线程池会判断当前运行的线程数大于corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后它最终会收缩到corePoolSize的大小。
12.2 JDK内置的拒绝策略
AbortPolicy(默认):直接抛出RejectedExecutionException异常阻止系统正常运行。
CallerRunsPolicy: 采用"调用者运行"一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者(还给main线程,让main自己干)。
DiscardoldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务。
DiscardPolicy:直接丢弃任务,不予任何处理也不抛出异常。如果允许任务丢失,这是最好的一种方案。
12.3 Executors中提供的线程池选用哪个?
线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 —— p3c规范
当13亿个用户同时请求,就会导致blockingQueue容量爆炸,从而导致OOM
Executors提供的线程池有哪些?
newCachedThreadPool(),它是一种用来处理大量短时间工作任务的线程池,具有几个鲜明特点:它会试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程;如果线程闲置的时间超过60秒,则被终止并移出缓存;长时间闲置时,这种线程池,不会消耗什么资源。其内部使用SynchronousQueue作为工作队列。
newFixedThreadPool(int nThreads),重用指定数目(nThreads)的线程,其背后使用的是无界的工作队列,任何时候最多有nThreads个工作线程是活动的。这意味着,如果任务数量超过了活动队列数目,将在工作队列中等待空闲线程出现;如果有工作线程退出,将会有新的工作线程被创建,以补足指定的数目nThreads。
newSingleThreadExecutor(),它的特点在于工作线程数目被限制为1,操作一个无界的工作队列,所以它保证了所有任务的都是被顺序执行,最多会有一个任务处于活动状态,并且不允许使用者改动线程池实例,因此可以避免其改变线程数目。
newSingleThreadScheduledExecutor()和newScheduledThreadPool(int corePoolSize),创建的是个ScheduledExecutorService,可以进行定时或周期性的工作调度,区别在于单一工作线程还是多个工作线程。
newWorkStealingPool(int parallelism),这是一个经常被人忽略的线程池,Java 8才加入这个创建方法,其内部会构建ForkJoinPool,利用Work-Stealing算法,并行地处理任务,不保证处理顺序。
12.4 参数的大小如何设置?
分为CPU密集型操作,和IO密集型操作。
CPU密集型:线程数量应该尽可能小,一般是当前主机的CPU核心数+1
Runtime.getRuntime().availableProcessors() // 获取核心数
IO密集型:
2 * CPU核心数
IO密集型,即该任务需要大量的IO,即大量的阻塞。在单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力浪费在等待。所以在IO密集型任务中使用多线程可以大大的加速程序运行,即使在单核CPU上,这种加速主要就是利用了被阻塞浪费掉的时间。
参考公式: CPU核数/1-阻塞系数
阻塞系数在0.8~0.9之间
比如8核CPU: 8/1 -0.9 = 80个线程数
13 死锁是怎么导致的?如何定位到问题的?
13.1 死锁是怎么导致的?
多个线程在争夺资源时互相等待的情况。
public class DeadlockDemo {
public static void main(String[] args) {
String lock1 = "lock1";
String lock2 = "lock2";
A a = new A(lock1, lock2);
Thread t1 = new Thread(a, "线程1");
A a1 = new A(lock2, lock1);
Thread t2 = new Thread(a1, "线程2");
t1.start();
t2.start();
t1.join();
t2.join();
}
static class A implements Runnable {
private String lock1;
private String lock2;
public void run() {
synchronized (lock1) {
System.out.println("线程:" + Thread.currentThread().getName() + "拿着" + lock1 + "正在获取" + lock2);
synchronized (lock2) {
System.out.println("线程:" + Thread.currentThread().getName() + "进来了!");
}
}
}
public A(String lock1, String lock2) {
this.lock1 = lock1;
this.lock2 = lock2;
}
}
}
13.2 如何定位到问题的?
jps -l // 查看运行的java程序
jstack 进程号 // 查看堆栈信息
二、基础
1、自增变量
public static void main(String[] args) {
int i = 1;
i = i++;
int j = i++;
int k = i + ++i * i++;
System.out.println("i = " + i);
System.out.println("j = " + j);
System.out.println("k = " + k);
}
>> i = 4
>> j = 1
>> k = 11
L0
LINENUMBER 9 L0
ICONST_1
ISTORE 1
L1
LINENUMBER 10 L1
ILOAD 1
IINC 1 1 // 让i自增1
ISTORE 1 // 将i赋值回1
L2
LINENUMBER 12 L2
ILOAD 1
IINC 1 1
ISTORE 2
L3
LINENUMBER 14 L3
ILOAD 1
IINC 1 1
ILOAD 1
ILOAD 1
IINC 1 1
IMUL
IADD
ISTORE 3
先将i的值压入栈(2),进行++操作之后将值入栈(3),将值入栈(3)再进行++操作,先算乘法 3*3 将结果入栈(9),在算加法 ==> 2 + 9 = 11
赋值操作(=)最后计算,=右边的从左到右加载值依次压入操作数栈
实际先算哪个,看运算符优先级
自增、自减操作都是直接修改变量的值,不经过操作数栈
最后的赋值之前,临时结果也是存储在操作数栈中
2、什么是单例模式?怎么实现?
单例模式:某个类在整个系统中只能有一个实例对象可被获取和使用的代码模式。
特点:
- 构造器私有化
- 自行创建,并且用静态变量保存
- 向外提供这个实例
- 强调该对象这是一个单例, 我们可以用final修饰
实现方式:
饿汉式:直接创建对象,不存在线程安全问题
- 直接实例化饿汉式(简洁直观)
- 枚举式(最简洁)
- 静态代码块饿汉式(适合复杂实例化)
懒汉式:延迟创建对象
- 线程不安全(适用于单线程)
- 线程安全(适用于多线程)
- 静态内部类形式(适用于多线程)
/*
* 在内部类被加载和初始化时,才创建INSTANCE实例对象
* 静态内部类不会自动随着外部类的加载和初始化而初始化,它是要单独去加载和初始化的。
* 因为是在内部类加载和初始化时,创建的,因此是线程安全的
*/
public class Singleton6 {
private Singleton6(){
}
private static class Inner{
private static final Singleton6 INSTANCE = new Singleton6();
}
public static Singleton6 getInstance(){
return Inner.INSTANCE;
}
}
3、实例初始化过程
非静态实例变量显示赋值代码和非静态代码块代码从上到下顺序执行,而对应构造器的代码最后执行。
public class Father {
private Integer i = this.text();
private static int j = method();
static {
System.out.println("1");
}
Father() {
System.out.println("2");
}
{
System.out.println("3");
}
private Integer text() {
System.out.println("4");
return 1;
}
private static int method() {
System.out.println("5");
return 1;
}
}
public class Son extends Father {
private Integer i = this.text();
private static int j = method();
static {
System.out.println("6");
}
public Son() {
System.out.println("7");
}
{
System.out.println("8");
}
private Integer text() {
System.out.println("9");
return 1;
}
private static int method() {
System.out.println("10");
return 1;
}
public static void main(String[] args) {
// 如果都是静态的,那么谁在前谁先执行
// 父类的静态方法 method()
// 父类的静态代码块
// 子类的静态方法 method()
// 子类的静态代码块
// 父类的text()方法(非静态变量)
// 父类的构造代码块
// 父类的构造方法
// 子类的text()方法(非静态变量)
// 子类的构造代码块
// 子类的构造方法
Son s1 = new Son();
System.out.println();
// 父类的text()方法(非静态变量)
// 父类的构造代码块
// 父类的构造方法
// 子类的text()方法(非静态变量)
// 子类的构造代码块
// 子类的构造方法
// 这就是变量被子类的方法结果重写了的原因
Son s2 = new Son();
}
}
4、方法的参数传递机制
如果传递的是基本数据类型:int、char等,传递的是值
如果是引用类型的话:传递的是内存地址的引用(String类和包装类等对象不可变性)
5、有n步台阶,一次只能上1步或2步,共有多少种走法?(日后再战)
5.1 递归
6、局部变量与成员变量的区别
1、声明的位置
- 局部变量:方法体{}中,形参,代码块{}中
- 成员变量:类中方法外
- 类变量:有static修饰
- 实例变量:没有static修饰
2、修饰符
- 局部变量:final
- 成员变量:public、protected、private、final、static、volatile、transient
3、值存储的位置
- 局部变量:栈
- 实例变量:堆
- 类变量:方法区
4、作用域
- 局部变量:从声明处开始,到所属的}结束
- 实例变量:在当前类中“this.”(有时this.可以缺省),在其他类中“对象名.”访问
- 类变量:在当前类中“类名.”(有时类名.可以省略),在其他类中“类名.”或“对象名.”访问
5、生命周期
- 局部变量:每一个线程,每一次调用执行都是新的生命周期
- 实例变量:随着对象的创建而初始化,随着对象的被回收而消亡,每一个对象的实例变量是独立的
- 类变量:随着类的初始化而初始化,随着类的卸载而消亡,该类的所有对象的类变量是共享的
三、Spring
1、事务的传播行为
- @Transactional(propagation=Propagation.REQUIRED) :如果有事务, 那么加入事务, 没有的话新建一个(默认情况下)
- @Transactional(propagation=Propagation.NOT_SUPPORTED) :以非事务方式执行操作,如果当前存在事务,就把当前事务挂起
- @Transactional(propagation=Propagation.REQUIRES_NEW) :不管是否存在事务,都创建一个新的事务,原来的挂起,新的执行完毕,继续执行老的事务(它拥有自己的隔离范围,自己的锁,Commit和rollback不受外部事物影响。)
- @Transactional(propagation=Propagation.MANDATORY) :如果没有事务,抛出异常
- @Transactional(propagation=Propagation.NEVER) :如果有事务,则抛出异常
- @Transactional(propagation=Propagation.SUPPORTS) :如果有事务, 那么加入事务, 没有的话就不用事务。
- @Transactional(propagation=Propagation.NESTED) :如果有事务在运行,当前的方法就应该在这个事务的嵌套事务内运行。否则,就启动一个新的事务,并在它自己的事务内运行。
四、Git
## 创建分支
git branch <分支名>
## 查看所有分支
git branch -v
## 切换分支
git checkout <分支名>
## 创建并切换分支
git checkout -b <分支名>
## 将分支合并到master上
git checkout master
git merge <分支名>
## 删除分支
git branch -D <分支名>
五、Redis
5.1 redis持久化有几种方式?
2种,RDB和AOF
5.1.1 RDB
在指定的时间间隔内将内存中的数据集快照写入磁盘,也就是行话讲的Snapshot快照,它恢复时是将快照文件直接读到内存里。
备份是如何执行的?
Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。RDB的缺点是最后一次持久化后的数据可能丢失。
rdb的优点
- 节省磁盘空间
- 恢复速度快
rdb的缺点
- 虽然Redis在fork时使用了写时拷贝技术,但是如果数据庞大时还是比较消耗性能。
- 在备份周期在一定间隔时间做一次备份(达到一定条件才备份), 所以如果Redis意外down掉的话,就会丢失最后一次快照后(还未达到条件时)的所有修改。
5.1.2 AOF
以日志的形式来记录每个写操作,将Redis执行过的所有写指令记录下来(读操作不记录),只许追加文件但不可以改写文件,Redis启动之初会读取该文件重新构建数据,换言之,Redis重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。
优点
- 备份机制更稳健,丢失数据概率更低。
- 可读的日志文本,通过操作AOF稳健,可以处理误操作。
缺点
- 比起RDB占用更多的磁盘空间。
- 恢复备份速度要慢。
- 每次读写都同步的话,有一定的性能压力。
- 存在个别Bug,造成无法恢复。
六、MySQL
1、什么是索引
MySQL官方对索引的定义为:索引(Index)是帮助MySQL高效获取数据的数据结构。
可以得到索引的本质:索引是数据结构。
优势:查询、排序快
劣势:增删改操作慢,占用磁盘空间
2、什么情况下适合建立索引
- 主键自动建立唯一索引
- 频繁作为查询条件的字段应该创建索引
- 查询中与其它表关联的字段,外键关系建立索引
- 单键/组合索引的选择问题,组合索引性价比更高.
- 查询中排序的字段,排序字段若通过索引去访问将大大提高排序速度
- 查询中统计或者分组字段
3、什么情况下不适合建立索引
- 表记录太少
- 经常增删改的表或者字段
- Where条件里用不到的字段不创建索引
- 过滤性不好的不适合建索引(例如:性别)