并发
Day3:并发
1 | Java 并发 |
目标:
1 | 并发 |
1. 为什么 Java 已经有 Thread,还要设计 Runnable?
Runnable还可以做到线程与任务解耦。任务可以复用线程。
1.1 什么叫任务和线程解耦?
创建一个Thread的时候,就把任务和线程绑定在了一起。
2. 为什么 Runnable 之后又设计了 Callable?
Runnable 没有返回值,如果需要返回执行结果,只能借助共享变量或回调等方式实现,不够优雅。因此 Java 又提供了 Callable,支持返回值和受检异常。
3. Future 到底是什么?为什么需要它?
Future 表示一个异步任务未来的执行结果。提交任务后,主线程不需要立即等待,可以在需要的时候通过 get() 获取结果。CompletableFuture 是 Future 的增强版本,支持任务编排、回调和组合,因此我项目中主要使用 CompletableFuture。
4. 为什么不能大量使用 new Thread()?
线程是系统资源,不能无限获取,并且创建、销毁、线程上下文切换耗费性能。
4.1 为什么线程切换耗性能?
cpu执行任务的时候需要寄存器、计数器、栈等信息,当发生线程切换的时候,需要保存现场,恢复现场等操作,这些都是耗时的。
5. 线程池最大的作用是什么?除了复用线程,还有什么作用?
- 最大的作用就是复用线程,防止创建线程的开销。还有一个主要目的就是控制线程数量。防止无限创建线程导致系统崩溃。可以统一管理,监控线程。比如线程名字,关闭线程等。
6. 结合你的 GIS 平台,说说为什么你选择 Spring Async + 自定义线程池,而不是直接在 Controller 里执行分析
上传下载shp/gdb,分析叠加等是一个耗时的操作,如果不用线程池,提交任务后一直等待,首先是用户操作层面就不合理。然后还会一直占用tomcat的线程,可能会导致其他请求阻塞。放到后台异步执行任务的话,不影响前台操作。
Day4:并发-线程池
目标:
1 | 并发 |
1. 为什么线程池不是核心满直接创建新线程,而是核心满先放队列?
线程属于昂贵资源,创建和销毁成本较高。所以java优先复用线程,先排队。并且任务很可能很快就执行完了。当队列也满了说明任务积压严重,需要提高线程数量。
线程池优先使用阻塞队列,是因为线程属于昂贵资源,创建和销毁成本较高。如果核心线程都忙碌,先让任务进入队列等待,很多情况下线程很快就会空闲并继续处理队列中的任务,这样可以减少线程数量,提高资源利用率。只有队列也满了,说明任务积压严重,才会继续创建非核心线程提高吞吐量
2. execute()和submit()最大的区别是什么?除了:Future。还有:什么?
submit 更关注任务生命周期。execute就是执行任务,不管任务是否成功失败。submit可以获取结果,停止任务,捕获异常。
3. 假设GIS线程池core=4,max=8,queue=100。突然来了500个空间分析。会发生什么?如果你是架构师。你怎么优化?
- 限制用户提交任务数量
- 增加mq削峰,慢慢消费
- 分布式部署
Day5:JUC
目标:
1 | JUC(java.util.concurrent) |
1 | 并发安全 |
1. 为什么会出现线程安全问题?本质是什么?
多个线程同时竞争同一个资源。假如两个线程同时对同一个变量a=100进行操作,线程1对a-10,线程b对a-20。结果可能并不是70。因为线程会读取变量a到自己的线程工作内存里,再写回主线程,读到的可能是100,也可能是另一个线程操作后的结果,导致每个线程看到的共享变量可能不是最新值。
2. 为什么 Java 的锁是锁对象,而不是锁代码?
因为多线程操作的是资源,资源需要加锁。对象是资源,代码不是资源。
3. 为什么synchronized(new Object())几乎等于没加锁?
因为每次都是new出来的新对象,每次的锁都不是同一把锁。
4. 分布式系统为什么用分布式锁,不用jvm提供的锁?
synchronized 本质是 JVM 内部锁,它只能保证同一个 JVM 内多个线程互斥。如果系统部署到多台服务器,每个 JVM 都有自己的锁对象,它们彼此独立,因此无法实现跨 JVM 的互斥。而 Redisson 基于 Redis 实现分布式锁,所有节点竞争的是 Redis 中同一个 Key,因此能够保证整个集群范围内的互斥。
分布式系统不在一个jvm下面,获取到的锁对象肯定也不是同一个。根本锁不住。分布式锁可以达到不同的系统使用的同一把锁,例如redisson分布式锁。
5. 为什么synchronized可以重入(Reentrant)?
当发现是同一个线程获取锁的时候就放行了。Monitor 里面维护了 Owner 和 Count
当发现再次进入synchronized(lock){的时候发现Owner == 当前线程,就不阻塞,而是Count++。当退出同步代码块时进行Count–。当Count为0时释放锁。
1 | Java 的锁(Monitor)里面会记录两个东西: |
6. 为什么 Java 要设计可重入?
1 | public synchronized void A(){ |
如果不可重入,执行A的时候获取到锁,然后执行B的时候也需要同一把锁,然后死锁了。
6. 为什么volatile不能保证i++;线程安全?
i++;的执行步骤:
1 | ① 读取 i |
假设线程1读取i为100,进行+1;线程2也读取100,进行+1。结果是101,少了一次。
volatile只能保证线程每次都是从主内存读取的,但是问题不是读错,而是同时写回会覆盖。
volatile 保证的是每次读取都能看到最新值,但 i++ 并不是一个原子操作,它实际上包含”读取、计算、写回”三个步骤。即使每一步都能看到最新值,多个线程仍然可能在读取同一个值后分别计算并覆盖彼此的结果,因此 volatile 无法保证 i++ 的线程安全。
volatile保证看到最新的值。不是保证整个修改过程不能被别人打断。
7. 既然 synchronized 能保证可见性,为什么还需要 volatile?
因为成本问题,synchronized太重。
假设
1 | boolean running = true; |
只需要保证线程B能看到最新running。volatile就够了,根本不用加锁
只有可见性,没有互斥需求。
Day6:CAS
目标:
1 | CAS |
volatile
volatile 写 (volatile 写 = 发布(Publish))
1 | 线程A |
volatile 读 (volatile 读 = 同步(Synchronize))
1 | 线程B |
1. CAS为什么必须配合volatile?
因为比较的值必须是最新的值。
volatile写会在volatile写操作周围增加memory barrier,然后cpu执行barrier,cpu缓存一致性协议,让其他cpu的Cache line失效。
volatile读可以利用 ‘volatile写 happens-before volatile读’让其他线程得到最新的值。
2. CAS为什么叫乐观锁?
CAS 认为大部分情况下不会发生竞争,所以不会先加锁,而是先尝试修改。如果修改失败,再重新读取最新值继续尝试,因此称为乐观锁。
3. 为什么AtomicInteger内部不是if(){}而是while(true)
getAndIncrement是通过CAS改变的值,cas如果修改失败需要不断的重试,所以需要while。
4. ABA到底有什么危害?为什么版本号能解决?
加入一个变量x,一开始是A。然后线程1要使用cas把x变为B,比较的时候如果x是旧值A就改为B。但是在线程1修改之前,已经有其他线程修改了x,把x变为了B又变回了A。这时候如果不加版本号线程1就不知道x已经被修改过了,并不是他预期的A。
5. 为什么CAS没有线程切换却也可能CPU100%。为什么?
因为CAS不停的自旋读取、比较。而synchronized获取锁失败直接阻塞挂起,cpu反而空闲。CAS是拿 CPU 换线程切换。
6. 假设1000线程全部AtomicInteger.incrementAndGet();CAS是不是一定比synchronized快?为什么?
不一定,如果cas一直发生冲突,那么重试的次数太多,会导致cpu把算力浪费在比较上。
CAS 适合低竞争场景;锁适合高竞争场景
7. CAS 最大的缺点是什么?
CAS 只能保证一个共享变量的原子更新。
8. 超卖问题
假设
1 | AtomicInteger stock = new AtomicInteger(10) |
卖票:
1 | if(stock.get()>0){ |
请问有没有线程安全问题?
还是会有安全问题,虽然stock.get()是volatile的,每次if读取的时候也都是最新值。但是有可能,线程1读取为1,然后cas成功减为了0。同时线程2也读取为1,然后去执行减没有成功,然后重新读取为0,然后再减1,最终的值可能会比0小。
Day 7:AQS
AQS关键流程图
1 | Thread |
AQS实现类
| AQS实现类 | state含义 |
|---|---|
| ReentrantLock | 锁重入次数 |
| Semaphore | 剩余许可证数量 |
| CountDownLatch | 剩余计数 |
| ReentrantReadWriteLock | 高16位读锁,低16位写锁(这个了解即可) |
1. AQS为什么一定要维护一个volatile int state; 为什么不是boolean?
因为AQS是一个抽象类,他还有很多实现类。各个实现类不只是用两个状态就可以解决问题。
2. 为什么AQS不用CAS一直while(true)而是park()?
因为一直while会浪费cpu,在竞争大的时候,很多情况都会失败。而park()会使线程休眠,释放cpu,当锁释放出来的时候再unpark进行一次cas。
3. ReentrantLock为什么叫Reentrant?它怎么做到可重入?
它会判断是否是当前线程持有锁,当进入加锁的同步代码时,如果是当前线程持有锁,他就会对state进行+1,当退出同步代码时,state会-1。
4. 为什么ReentrantLock功能比synchronized多?
因为ReentrantLock是利用AQS使用Java代码实现的。而synchronized的功能已经写死在了jvm。
5. 假设你的GIS平台。服务器16核。你希望最多只有4个空间分析同时运行。其它任务等待。请问你用AtomicInteger还是Semaphore还是ReentrantLock?
选择Semaphore。因为4个空间分析是4个线程。用ReentrantLock肯定只有一个线程获得锁。AtomicInteger只能保证一个元素是原子的,而启动任务肯定是多个操作,实现起来太复杂。
Semaphore使用起来更方便,启动一个任务就Semaphore-1
6. ReentrantLock: 为什么lock.unlock()必须放finally?
1 | lock.lock(); |
不管执行是否出现异常,都必须保证锁最终要释放。否则会造成死锁。
ReentrantLock 不像 synchronized 会自动释放锁,它必须由程序员手动调用 unlock()。如果中间发生异常而没有执行 unlock(),state 永远不会减到 0,其它线程将一直 park 在 AQS 队列中,导致死锁。
7. ReentrantLock: 为什么下面代码会死锁?
1 | lock.lock(); |
虽然是同一线程可重入,但是这个线程每次加锁的时候都会state++,而它加锁了两次最后只减了一次,所以锁没有释放。
8. 为什么ReentrantLock公平锁比非公平锁慢?
公平锁严格先进先出,即使刚来的线程刚好碰到state为0,也要必须排队。
非公平锁可以插队,刚来的线程碰到state为0可以直接拿锁,少一次拍断排队唤醒。











