八股文系列-JUC

java并发类库提供的线程池有哪些,分别有什么特点?

newCachedThreadPool()多用来处理时间短且简单的工作任务,这个线程池会主动缓存线程,当线程闲置60秒后会被移除,如果没有线程可用用于工作,就会创建新的线程。内部使用SynchronousQueue作为工作队列。

newFixedThreadPool(int nThread)固定大小的线程池,线程数量超过设置值的时候就会将新线程加入工作队列,低于设置大小时会主动创建线程。内部工作队列是一个无边界的队列。如果工作线程数量设置太小,工作队列中有大量的等待线程,可能导致OOM

newSingleThreadExecutor()只有一个线程,保证所有线程按顺序执行。内部是一个无边界的队列。

newSingleThreadScheduledExecutor() 和 newScheduledThreadPool(int corePoolSize),创建的是个 ScheduledExecutorService,可以进行定时或周期性的工作调度,区别在于单一工作线程还是多个工作线程

HashMap和ConcurrentHashMap区别?

一是HashMap是线程不安全的,并发情况下容易形成环状链表,ConcurrentHashMap是线程安全的,JDK1.7: ConcurrentHashMap是通过分段锁,默认有16个segment, 每一个segment通过继承ReentrantLock来实现的线程安全,JDK1.8:取消了Segment,直接用table数组的元素通过CAS和Synchronized实现并发控制。

高并发HashMap的环是如何产生的?

出现在transfer代码中,也就是扩容之后的数据迁移部分,可能会将next节点,设置为自己,导致自己使用next引用自己,因此产生链表环,导致死循环

线程数量该设置多少?

根据任务类型是IO密集型还是CPU密集型(计算多),如果计算比较多,可用创建CPU核数+1或者CPU核数。如果等待任务较多比如IO密集型,可用按照如下公式。
线程数 = CPU核数 × 目标CPU利用率 ×(1 + 平均等待时间/平均工作时间)

如果批量的执行异步任务?

可用使用java并发包下面的completionService。
异步执行任务后,会将执行结果按执行返回的顺序存入阻塞队列中,有利于任务执行完过后的有序性。比如:想要异步调用3个任务分别去获取一个相同的信息,只要有一个返回就返回结果,这个时候就可用使用completionService。

// 创建线程池
ExecutorService executor = 
  Executors.newFixedThreadPool(3);
// 创建CompletionService
CompletionService<Integer> cs = new 
  ExecutorCompletionService<>(executor);
// 异步向电商S1询价
cs.submit(()->getPriceByS1());
// 异步向电商S2询价
cs.submit(()->getPriceByS2());
// 异步向电商S3询价
cs.submit(()->getPriceByS3());
// 将询价结果异步保存到数据库
for (int i=0; i<3; i++) {
  Integer r = cs.take().get();
  executor.execute(()->save(r));
}

ReadWriteLock实现

基于AQS,AQS中提供了一个volatile修饰的int变量state,int占4个字节32位,读锁根据左边16位,写锁根据右边16位。根据state的值判断是否有锁,然后通过state和16进制的0XFFFF做and运算,如果运算结果不是0就是写锁,如果是0就是读锁。

Java虚拟机是如何实现Synchronized的?

当声明代码块的时候,字节码会包含monitorenter和monitorexit. 当修饰方法的时候,方法的访问标记flags会有一个ACC_SYNCHRONIZED标记,表名在进入该方法的时候,需要用monitorenter和monitorexit(隐式的)。当执行monitorenter的时候,需要判断目标锁对象的计数器是否为0,为0就可以持有锁,为1就需要等待锁的释放。

说一说对JMM内存模型的理解?

JMM为了防止多线程环境下的数据一致性问题,提出了JMM,JMM就是多线程环境下的一系列规范。

volatile作用?

volatile的作用主要是保持可见性和指令重排。可见性是通过一个汇编指令lock cmpxchg指令,当CPU读取到这个指令的时候,会将缓存中的数据写入主内存,并且通过MESI协议让其他线程的值失效。指令重排是通过内存屏障来实现的,比如每一个volatile写操作的签名都会有一个StoreStore指令,后面有一个StoreLoad指令。

Atomic类如何保证原子性?

通过依赖Unsafe类来保证原子性。Unsafe类可以进行CAS操作,CAS操作的底层是通过汇编指令Lock cmpxchg来实现。CAS由于不经过内核,循环的时候比较消耗CPU。但是会有ABA问题

ReentrantLock实现原理是什么?

ReentrantLock在普通加锁解锁功能上还提供了可重入,以及锁的公平性。加锁是通过调用成员变量sync的lock方法实现的,sync的实例化是通过NonFairSync和FairSync类,这也是实现锁公平性的地方,默认是非公平的。这两个类都是Sync的子类,而Sync又继承了AQS,sync的lock方法是通过CAS实现,成功将state的值+1,就占用锁成功,公平锁和非公平锁实现lock方法的区别在于,公平锁需要判断队列前面是否有线程在等待,非公平锁会直接进行获取,非公平锁可能会导致线程饥饿的问题。线程在获取锁失败后会进入一个队列等待,acquireQueued方法会对队列中等待的线程进行获取锁的操作。释放锁是通过sync类的release方法,他会唤醒队列中的第一个节点。可重入是通过AQS中的一个被volatile修饰的变量,每获取一次锁就会对该字段+1。

聊一聊AQS?

AQS 指的是AbstractQueuedSynchronizer, 是JUC包下一个工具类。是构建其他同步类的基础框架

synchronized和lock/ReentrantLock区别是什么?

  1. 发生异常的时候Synchronized会自动释放锁,lock不会,所以lock必须在finally中手动释放不然会导致死锁
  2. lock功能更多,可以用interrupt来中断等待,而synchronized只能等待锁的释放,Lock可以通过trylock来知道有没有获取锁,而synchronized不能
  3. ReentrantLock可用实现公平性,让等待时间长的线程更容易获取锁,防止线程饥饿。满足公平性需要另外的性能开销,当程序确实有公平性需求时才使用。
  4. ReentrantLock支持重新获取锁

ThreadLocal的实现和原理是什么?

ThreadLocal主要是用做线程隔离,相当于是线程的私有变量。Thread内部有一个ThreadLocalMap变量threadLocals. ThreadLocalMap是一个Entry数组,key是一个弱引用指向threadlocal变量,value是存储的值。用弱引用的原因是因为防止threadlocal=null的时候不能被GC。常用的场景是数据库连接,session管理等,ThreadLocal存在内存泄露的问题,需要用remove()删除entry.

内存泄露问题,threadLocalMap中的entry指向的对象被回收过后就存在一个null值和有值得value,如果存在大量的这种键值对就会导致内存泄露。解决办法是调用remove方法来删除entry

线程池ThreadPoolExcutor的核心参数有哪些?

corePoolSize,MaxPoolSize,keepAliveTime,workQueue,RejectedExecutionHandler

ThreadPool拒绝策略有哪些?

  • AbortPolicy:直接丢弃任务,抛出异常,这是默认策略

  • CallerRunsPolicy:在任务被拒绝添加后,会调用当前线程池的所在的线程去执行被拒绝的任务,可能会阻塞主线程

  • DiscardOldestPolicy:丢弃等待队列中最近的任务,并执行当前任务

  • DiscardPolicy:直接丢弃任务,也不抛出异常

线程池的使用原理是什么?

提交任务后,线程池根据CorePoolSize创建线程大小。任务数量超过CorePoolSize后,其余线程进入阻塞队列排队,当阻塞队列满了后,创建MaxPoolSize-CorePoolSize个线程,任务完成后,新创建的线程会在KeepAliveTime后销毁。如果达到MaxPoolSize阻塞队列还是满的就根据不同的拒绝策略来处理

如何使用不可变性(immutability)实现线程安全?

类和属性都是 final 的,所有方法均是只读的。
在使用 Immutability 模式的时候一定要确认保持不变性的边界在哪里,是否要求属性对象也具备不可变性。
Bar 的属性 foo 虽然是 final 的,依然可以通过 setAge() 方法来设置 foo 的属性 age

class Foo{
  int age=0;
  int name="abc";
}
final class Bar {
  final Foo foo;
  void setAge(int a){
    foo.age=a;
  }
}

为什么会出现线程不安全的情况?

计算机系统进程中会同时会有多个线程在工作,这些线程共享进程内的资源。多线程问题可以从原子性,可见性和有序性来看,比如一系列操作在中途由于异常等被中断,一个线程对共享变量的修改没有及时的被其他线程看见,或者由于cpu指令重排导致的有序性问题。

java锁的优化过程?

为什么需要锁升级?
synchronize主要是通过操作系统底层的mutex lock实现的,有内核态到用户态转换的消耗,性能不好。

表示在对象头MarkWord中
在这里插入图片描述

当有一个线程访问同步资源的时候,升级为偏向锁,锁标志位为01
偏向锁主要用来优化同一线程多次申请同一个锁的竞争,比如单线程操作一个线程安全集合时,同一线程每次都需要获取和释放锁,每次操作都会发生用户态与内核态的切换

偏向锁实现就是
线程再次访问的时候,判断是否偏向锁标志位是否是1,然后看是否有偏向锁指向它,就无需再通过monitor来竞争。

轻量级锁,锁标志位00
另外一个线程访问同步资源,首先通过CAS获取偏向锁,如果没有获取成功,表示竞争比较激烈,就转换为轻量级锁,轻量级锁适合线程交替执行,不存在长时间阻塞竞争的情况。自旋达到一定程度后还没有获取到,就升级为重量级锁。

重量级锁
因为在竞争很激烈的时候,CAS操作会占用大量的CPU资源。
所以在高并发的情况下我们可以关闭JVM的自旋锁设置。重量级锁下,所有等待的线程都会被阻塞在一个waitSet

当多个线程同时访问一段同步代码时,多个线程会先被存放在 ContentionList 和 _EntryList 集合中,处于 block 状态的线程,都会被加入到该列表

动态编译实现锁消除 / 锁粗化
JAVA也使用了编译器对锁进行了优化,利用了一种逃逸分析的技术,判断同步块使用的锁对象是否只能被一个线程访问,不会被其他线程访问。如果是这样,在编译的时候就不会生成synchronize相关的机器码。

如果线程调用 wait() 方法,就会释放当前持有的 Mutex,并且该线程会进入 WaitSet 队列中,等待下一次被唤醒

减小锁粒度
当我们的锁对象是一个数组或队列时,集中竞争一个对象的话会非常激烈,锁也会升级为重量级锁。我们可以考虑将一个数组和队列对象拆成多个小对象,来降低锁竞争,提升并行度。 最经典的减小锁粒度的案例就是 JDK1.8 之前实现的 ConcurrentHashMap 版本

redolog binglog undolog

redolog 是InnoDB存储引擎层的日志, 用于记录事务操作的变化,记录的是数据修改之后的值.在一条更新语句进行执行的时候,InnoDB引擎会把更新记录写到redo log日志中,然后更新内存,此时算是语句执行完了,然后在空闲的时候或者是按照设定的更新策略将redo log中的内容更新到磁盘中,这里涉及到WAL即Write Ahead logging技术,他的关键点是先写日志,再写磁盘。

binlog日志模块属于MySQL Server层面的,又称为归档日志

区别:

redo log是属于innoDB层面,binlog属于MySQL Server层面的,这样在数据库用别的存储引擎时可以达到一致性的要求。
redo log是物理日志,记录该数据页更新的内容;binlog是逻辑日志,记录的是这个更新语句的原始逻辑
redo log是循环写,日志空间大小固定;binlog是追加写,是指一份写到一定大小的时候会更换下一个文件,不会覆盖。
binlog可以作为恢复数据使用,主从复制搭建,redo log作为异常宕机或者介质故障后的数据恢复使用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值