Java并发编程笔记

103 阅读8分钟

第01讲:为何说只有 1 种实现线程的方法?

1、创建线程实际只有一种方式

  • 实现Runnable接口,重写run()方法;实现Callable接口(有返回值),重写call()方法
  • 继承Thread类,重写run()方法;线程池

2、实现Runnable接口比继承Thread好

  • 从代码架构考虑,实现了RunnableThread类的解耦,Runnable是可执行内容,Thread负责线程的启动
  • Thread每执行一次任务,需要开启新的线程,执行完成后销毁;Runnable可直接传入线程池,不需要每次新建销毁线程,可复用
  • Java不支持多继承,继承Thread限制了可拓展性

第02讲:如何正确停止线程?为什么 volatile 标记位的停止方法是错误的?

最正确的停止线程的方式是使用interruptThread.currentThread().interrupt(),但interrupt仅仅起到通知被停止线程的作用。 Java希望程序间能够相互通知、相互协作地管理线程,贸然强制停止线程就可能会造成一些安全的问题

使用volatile标记位停止线程,如果线程休眠或阻塞无法及时感知标记信号,使用interrupt能及时感知中断信号,休眠或阻塞时抛出InterruptedException,同时清除中断信号

第03讲:线程是如何在 6 种状态之间转换的?

线程的 6 种状态

  1. New(新创建)
  2. Runnable(可运行)
  3. Blocked(被阻塞)
  4. Waiting(等待)
  5. Timed Waiting(计时等待)
  6. Terminated(被终止)

第04讲:wait/notify/notifyAll 方法的使用注意事项?

为什么 wait 必须在 synchronized 保护的同步代码中使用?

因为while判断和wait方法必须是原子操作

image.png

第05讲:有哪几种实现生产者消费者模式的方法?

  1. BlockingQueue
  2. ReentrantLock.newCondition()
  3. Object.wait/notify

第06讲:一共有哪 3 类线程安全问题?

第07讲:哪些场景需要额外注意线程安全问题?

第08讲:为什么多线程会带来性能问题?

  • 调度开销:上下文切换、缓存失效
  • 协作开销

第09讲:使用线程池比手动创建线程好在哪里?

  1. 线程复用,减小线程的创建、销毁开销
  2. 统筹内存和CPU,控制线程数量,避免线程过多导致内存溢出,线程太少浪费资源
  3. 统一管理线程,管理任务

第10讲:线程池的各个参数的含义?

第11讲:线程池有哪 4 种拒绝策略?

image.png

  • AbortPolicy:拒绝时抛出RejectedExecutionException异常
  • DiscardPolicy:直接丢弃新任务,有一定风险
  • DiscardOldestPolicy:丢弃队列中最老的任务,为新任务让出空间,有一定风险
  • CallerRunsPolicy:将任务交于提交任务的线程执行,谁提交,谁执行(不会被丢弃,提交任务线程被占用,减缓任务提交速度,相当于负反馈)

第13讲:线程池常用的阻塞队列有哪些?

image.png

  • LinkedBlockingQueue:容量Integer.MAX_VALUE,由于FixedThreadPool的线程数量固定,所需要没有容量限制的阻塞队列来存放任务
  • SynchronousQueue:由于CachedThreadPool的最大线程数是Integer.MAX_VALUE,所以并不需要队列来存储任务
  • DelayedWorkQueue:可延迟执行任务队列,内部按照时间对任务进行排序

第14讲:为什么不应该自动创建线程池?

第15讲:合适的线程数量是多少?CPU 核心数和线程数的关系?

第16讲:如何根据实际需要,定制自己的线程池?

第17讲:如何正确关闭线程池?shutdown 和 shutdownNow 的区别?

  • shutdown():调用后并不会立刻关闭,执行完当前任务和队列中的任务才真正关闭,新任务走拒绝策略
  • shutdownNow():调用后首先会给所有线程发送interrupt中断信号,尝试中断任务,然后将队列中任务转移到List中返回
  • isShutdown():是否执行了shutdownshutdownNow方法,返回true并不代表真正关闭
  • isTerminated():是否真正关闭了
  • awaitTermination():尝试等待一定时间,如果等待期间内线程池真正关闭,返回true,否则返回false,等待期间线程被中断,抛出InterruptedException

第18讲:线程池实现“线程复用”的原理?

通过取WorkerfirstTask或者getTask方法从workQueue中取出新任务,并直接调用Runnablerun方法来执行任务,每个线程都始终在一个大循环中,反复获取任务,执行任务,从而实现线程的复用

第19讲:你知道哪几种锁?分别有什么特点?

  • 偏向锁/轻量级锁/重量级锁(特指synchronized

    • 偏向锁:如果自始至终,对于这把锁都不存在竞争,那么就没必要上锁,打个标记就行了,开销小,性能最好
    • 轻量级锁:如果synchronized中的代码被多个线程交替执行,不存在实际竞争,或者只有短时间的竞争,用自旋或者CAS就可以解决,不会陷入阻塞
    • 重量级锁:重量级锁是互斥锁,利用操作系统的同步机制实现,开销较大
  • 可重入锁/非可重入锁

    可重入指当前线程已经持有这把锁了,能在不释放这把锁的情况下,再次获取这把锁,如ReentrantLock

  • 共享锁/独占锁

    共享锁指的是同一把锁可以被多个线程同时获得,独占锁指的是一把锁只能被一个线程获得,如读写锁

  • 公平锁/非公平锁

    公平锁先到先得

  • 悲观锁/乐观锁

    悲观锁获取资源前必须独占锁,乐观锁利用CAS理念,不会独占资源

  • 自旋锁/非自旋锁

    自旋锁指当线程拿不到锁时,不直接陷入阻塞或释放CPU资源,而是利用循环不停的尝试获取锁,非自旋锁拿不到锁就放弃,直接陷入阻塞等

  • 可中断锁/不可中断锁

    synchronized是不可中断锁,一旦申请了锁,只能等拿到锁才能进行其他操作,ReentrantLock是可中断锁,如果在申请过程中不想等待,使用lockInterruptibly获取锁可进行中断

第20讲:悲观锁和乐观锁的本质是什么?

悲观锁

悲观锁比较悲观,认为如果不锁住这个资源,别的线程就会争抢,造成数据错误,所以为了确保结果的正确性,每次获取并修改数据时,都把数据锁住(如synchronized关键字和Lock接口)

乐观锁比较乐观,认为在操作资源的时候不会有其他线程干扰,并不会锁住被操作对象,但为了确保数据的正确性,在更新之前,会对比数据有没有修改,如果没有修改,就说明只有自己在操作,可正常修改数据,如果修改了,就放弃本次修改(如原子类AtomicInteger

第21讲:如何看到 synchronized 背后的“monitor 锁”?

第22讲:synchronized 和 Lock 孰优孰劣,如何选择?

相同点

都是用来保护资源线程安全的,都可以保证可见性,都可重入

不同点

用法不同,加解锁顺序不同,synchronized不够灵活,synchronized只能同时被一个线程拥有,synchronized不能设置公平非公平,原理不同,性能不同

第23讲:Lock 有哪几个常用方法?分别有什么用?

  • lock():最基础的获取锁的方法,在获取锁时如果锁已被其他线程获取,则进行等待
  • tryLock():尝试获取锁,如果锁没有被其他线程占用,则获取成功,返回true,否则返回false
  • tryLock(long time, TimeUnit unit):多了等待时间
  • lockInterruptibly():可响应中断获取锁
  • unlock():释放锁

第24讲:讲一讲公平锁和非公平锁,为什么要“非公平”?

image.png

第25讲:读写锁 ReadWriteLock 获取锁有哪些规则?

有两把锁,读锁和写锁,读锁只能查看数据,可被多个线程同时持有,写锁可以读取和修改数据,只能被一个线程持有

要么是一个或多个线程同时有读锁,要么是一个线程有写锁,两者不会同时出现,可总结为:读读共享、其他都互斥

第26讲:读锁应该插队吗?什么是读写锁的升降级?

ReentrantReadWriteLock插队策略

  • 公平策略:只要队列中有线程等待,hasQueuedPredecessors()返回true,都不允许插队
  • 非公平策略:如果允许读锁插队,读锁可以同时被多个线程持有,可能造成后续读锁一直插队成功,写锁一直等待,所以不允许读锁插队;写锁可以随时插队,因为写锁并不容易插队成功,写锁只有当没有其他线程持有读锁和写锁的时候,才能插队成功,同时写锁一旦插队失败就会进入等待队列,所以很难造成“饥饿”的情况,允许写锁插队是为了提高效率

第27讲:什么是自旋锁?自旋的好处和后果是什么呢?

自旋就是利用循环不停的尝试,直到目标达成

线程的阻塞和唤醒开销较大,如果使用自旋能在较短时间内获得锁,就提高了效率;但如果一直获取不到锁,浪费了时间和资源

第28讲:JVM 对锁进行了哪些优化?

  • 自适应的自旋锁
  • 锁消除
  • 锁粗化
  • 偏向锁/轻量级锁/重量级锁