- JMM:主内存物理内存线程共享,工作内存CPU缓存线程独占
- volatile:可见性、禁止指令重排,不可保证原子性;用于懒汉单例模式(双重检测)或状态标记
- Synchronized:保证代码块或方法同步化执行,可以对当前类(class)对象(静态方法或静态对象)或对象加锁(同步代码块和非静态方法),若修饰多个方法时,也只能被一个线程调用;有锁的升级,偏向锁,轻量级锁,重量级锁;锁的消除(需开启逃逸分析);synchronized可对字符串加锁,若字符串对象是new String()或者StringBuilder生成的则是会创建不同的对象,不能充当对象锁
- Synchronized:两个指令monitor enter获取monitor对象、当monitor的count为0时,即可解除当前对象的锁,执行monitor exit命令释放锁
- 偏向锁,轻量级锁,重量级锁:偏向锁会在对象头Markword记录占有偏向锁的线程id,此时该线程执行同步代码块时不需要加锁和释放锁;若有线程竞争该锁并获取不到,就升级为轻量级锁,竞争该锁的线程进行自旋尝试获取锁;自旋达到一定次数时,会升级为重量级锁,竞争锁的线程会被挂起。(线程上下文切换很消耗资源)
- 对象的组成结构:对象头、实例数据、对齐填充(8个字节的整数倍,关于存储效率)
-
对象头:Mark Word、Klass Pointer类型指针、数组长度
-
Mark Word:锁的状态、是否偏向锁、偏向锁的线程Id、GC分代年龄、hashcode、指针
-
- 锁的消除:利用逃逸分析,认定对象不会被其他线程访问,可以不考虑同步问题,消除不可能存在资源竞争的锁,节省毫无意义的请求锁的时间。
- ReentrantLock:可重入、公平或非公平、等待可中断、手动加解锁
- ReentrantLock用法:1.默认是非公平锁、2.加锁多少次就要解锁多少次、3.解锁unlock()要写在finally中,避免其他线程获取不到锁而阻塞、4.加锁lock()写在try块的前一行,而不写在try中,因为若try中的加锁操作出现异常,就执行解锁,但是持有锁的线程非当前线程,所以解锁操作会抛出异常
- AQS:可重入、公平\非公平、可中断、独占\共享、阻塞等待队列
- AQS(AbstractQueuedSynchronizer),抽象同步框架,当共享资源空闲时,请求资源的线程可以去占有并将共享资源锁住,用同步等待队列(CLH)将其他线程排队等待获取锁并阻塞线程,当占有资源的锁被释放时又可以从同步等待队列中唤醒线程去获取资源。很多并发工具类如ReentrantLock、Semophore、CountDownLetch、CyclicBarrier都是基于继承AQS实现功能。同步等待队列是基于双向链表实现的先进先出队列,会将等待锁的线程和等待状态封装成一node节点并加入队列。
- AQS定义了队列中节点的5种状态:0等待获取锁;1CANCELLED表示当前线程被取消;-1SIGNAL表示当前线程要被唤醒;-2CONDITION,表示线程在条件队列中;-3表示当前场景下后续的acquireShared能够得以执行;
- ReentrantLock源码:继承AQS抽象类,通过自旋、CAS获取释放锁、同步等待队列、LockSupport(用于将线程阻塞、唤醒)
- ReentrantLock的加锁逻辑:1.先尝试去获取锁,用CAS更改状态0到1,若修改成功则代表获取到锁并记录占有该锁的线程;若锁被占有了,看是否是当前线程占有,线程可以重复占有锁,锁状态用CAS加1;2.若获取锁失败,则将线程对象封装成节点并用自旋和CAS插入队列尾部,如果节点是队列的头节点,则再让该线程尝试获取锁,获取到锁则将节点从队列中移除,仍获取不到锁则将线程阻塞。
- 理解线程堵塞等待锁时被thread2.interrupt()中断唤醒线程去获取锁,然后有中断标识接着获取到锁后再次currentThread.interrupt(), 目的是为了将中断原因抛出去
- ReentrantLock的lockInterruptibly(),与lock()区别在于,lockInterruptibly()方法中线程发生中断会终止去获取锁,而lock()仍旧会等待获取锁但最终会把中断原因异常抛出去
- ReentrantLock的解锁逻辑:判断占有锁的线程是否为当前线程,否则抛出异常,将锁状态减1,若为0则将占有锁的线程移除,然后将队列的头节点移除并唤醒unpark()头节点的线程
- ReentrantReadWriteLock的读写锁,适用于读多写少场景,如读写缓存,但可能造成写锁饥饿(写锁一直等待读锁释放)。写锁是独占锁,读锁是共享锁;写锁优先于读锁、读锁会阻塞写锁、写锁会阻塞写锁和读锁、读锁不会阻塞读锁,获取读锁时会判断是否存在写锁和写锁是否是当前线程占有
- 锁的升级或降级,用于一个线程中又读又写的场景,升级:读锁变写锁,ReentrantReadWriteLock不支持锁升级,会导致死锁,因为写锁时要求不存在读锁;降级:写锁变为读锁,ReentrantReadWriteLock支持锁降级。锁降级的存在意义:再写完再读场景中,若全程都用写锁,性能消耗大,若是写完释放锁再读,则读取到数据可能不一致,其他线程会写入修改数据。
- StampedLock邮戳锁JDK1.8,对ReentrantReadWriteLock再优化,三种模式(读锁、写锁、乐观锁),支持锁的升级和降级。使用读操作用乐观锁lock.tryOptimisticRead(),再判断lock.validate(stamp),若校验不对则再用读锁,乐观锁即是无锁。StampedLock是不可重入锁,若一个线程持有写锁再重新获取则会死锁。
- Semaphore:信号量,能够限制访问资源的线程数,共享锁,应用:限流降级
- CountdownLatch与CyclicBarrier:两者都能等待其他线程再继续执行。前者只能一次性使用,后者能重复使用;前者强调一个线程等待其他多个线程,后者强调多个线程间相互等待。
- CAS:比较并交换,原子操作(操作系统层面,多线程不能对同一内存区域进行操作),通过查看变量的内存地址上的值,与原值作比较,相同即可修改值。存在ABA问题,加版本号;只能保证一个变量的原子性,多个就要用Synchronized与ReentrantLock、AutomicReference
- wait()、notify()、notifyAll():三者都能释放当前线程拥有的锁,前提必须当前线程拥有锁,所以三者都必须写在同步代码中;wait()强调的是等待,等待某共享变量发生变化后再继续执行,后两者强调通知,某共享变量发生变化后去通知其他线程。三者都是跟锁有关的操作,锁是对象级锁,所以都是在Object定义的方法。
- 线程池核心参数:核心线程数、最大线程数、空闲等待时间、阻塞队列、拒绝策略
- 避免死锁:保证一定的顺序去获取锁;设置等待获取锁的时间,避免线程一直堵塞
- 保证多个线程顺序执行,用Thread类的join方法
- LongAdder:是AtomicLong的优化版,为了提高吞吐量,用空间换时间思维,高并发竞争大性能会更好,sum结果只是接近值最终一致性。内部维护一个值base,没有竞争时修改base,额外添加一数组用于冲突时存储数值,将base和数组的值累加即是最新结果。
- 线程池异常处理,无法感知任务出现异常,要trycatch捕获或者
- ForkJoinPool:分而治之,大任务拆分多个子任务,空闲线程从线程的任务队列中窃取任务执行,任务可拆分,任务可窃取。应用在计算型任务。ForkJoinPool可接收执行ForkJoinTask、Runnable、Callable。ForkJoinTask的fork()提交任务,ForkJoinTask的join()获取任务结果,阻塞当前线程等待子任务执行结束返回结果。示例计算数组中一亿个数的总和。
- 阻塞队列BlockQueue:执行方法:入队:add(抛异常)、offer(true、false)、put(阻塞);出队:remove(抛异常)、poll(空返回null)、take(阻塞);获取队首元素:element(抛异常)、peek(空返回null)
- 阻塞队列,任何时刻,只能有一个线程操作入队put或出队take。基于这个特性,所以put或take操作方法就加ReentrantLock锁。但是阻塞队列为有界时,所以就造成阻塞队列的出入队操作实现瞬间复杂,有两个条件对象,notEmpty和notFull。
- 大概思路:当入队时,队列满了,触发条件notFull.await(),所以不能再加入元素,所以持有该锁的线程要主动释放锁。主动释放锁大体流程是,先创建成新的节点,节点的waitStatus是-2(CONDITION),组成条件等待队列,然后释放锁(复用ReentrantLock的unlock底层代码),如果下个获取到锁的线程仍然触发条件notFull.await(),就仍进行上面操作(放入条件等待队列,主动释放锁)。如果没触发是线程执行出队成功,则在出队操作dequeue()去调用notFull.signal(),signal()大概就去将从头到尾遍历条件等待队列,节点转换成waitStatus=-1(SIGNAL)节点,就放入到CLH队列。出队时亦是相同。总结条件等待队列特点:节点的waitStatus是-2(CONDITION),不能去获取锁的,然后触发.signal()时,所有节点转换成CLH队列的节点,所以节点要么全是出队的,要么全是入队的。
- 阻塞队列ArrayBlockingQueue:基于数组实现的有界阻塞队列,先进先出,指定容量大小不可扩容,入队和出队共用一把ReentrantLock锁,存取互斥
- 阻塞队列LinkedBlockingQueue:基于链表实现的理论上无界的阻塞队列,先进先出,可以指定容量大小,入队和出队是用两把锁,存取不互斥,这也是为什么线程池用LinkedBlockingQueue而不用ArrayBlockingQueue
- CopyOnWriteArrayList:并发容器,允许并发对容器写入,写入时将对创建副本,使得同时能读取原数据,适用于读多写少场景,会占用过多内存导致gc,不能实时读。
- ThreadLocal:threadlocal value为什么不是弱引用? - 知乎 (zhihu.com)
-
- ThreadLocal是什么?项目中用到过吗?SimpleDateFormatDemo线程不安全、多数据源连接
- ThreadLocal的结构是怎么样的?
- 使用ThreadLocal需要注意哪些问题?用static,否则用普通变量的话,在其他处地方要先实例化新的threadLocal引用,导致get不到
- 为什么key要设置成弱引用呢?若是强引用,threadLocal引用置为null,则会导致key(threadLocal对象内存泄露),弱引用是gc会回收对象,并且对ThreadLocalMap操作get/set/remove或者扩容都会把key为null的entry清空,所以ThreadLocal不太会发生内存泄漏,规范做法是用完后remove。
- 那为什么value不设置成弱引用呢?ThreadLocalMap不是持有对这个value的强引用,它还会被回收吗?若value是弱引用,则GC会回收对象,导致get到空
- ThreadLocalMap的hash冲突与扩容:hash冲突则往后遍历放在空值上,扩容扩大原来的2倍,重新hash。
- threadLocalMap的key为null的两种清理方式,探测清理和启发清理,在get或set方法可能会附带条件触发,并不能保障一定清理。