线程方面的总结

1. sychronied 修饰普通方法和静态方法的区别, 什么是可见性

对象锁是用于对象实例方法, 或者一个对象实例上的, 类锁是用于类的静态方法或者一个类的 class 对象上的. 类的对象实例可以有多个, 但是每个类只有一个 class 对象, 所以不同对象实例的对象锁是互不干扰的, 但是每个类只有一个类锁. 但是类锁只是一个概念上的, 并不是真实存在的, 类锁其实是每个类对应的 class 对象. 类锁和对象锁之间是互不干扰的.

可见性是指当多个线程访问同一个变量时, 一个线程修改了这个变量的值, 其他线程能够立即看得到修改的值. 由于变成对变量的所有操作都必须在工作内存中进行, 而不能直接读写主内存中的变量, 那么对于共享变量首先是在自己的工作内存, 之后再同步到主内存. 可是并不会及时刷新到主内存中, 而是会存在一定的时间差, 所以这时候线程 A 对共享变量的操作对于线程 B 来说, 就不具备可见性了. 要解决可见性的问题 , 可以使用 volatile 关键字或者加锁.

详情见:从 Synchronized 到锁的优化
 

2. synchronized 的原理以及与 ReentrantLock 的区别.

synchronized 属于独占式悲观锁, 是通过 JVM 隐式实现的, synchronized 只允许同一时刻只有一个线程操作资源. 它涉及了两条指令 monitorenter / monitorexit, 每个对象都有一个监视器锁 monitor, monitor 被占用时就会处于锁定状态, 线程执行 monitorenter 指令时尝试获取 monitor 的所有权, 执行 monitorexit 时释放 monitor 对象. 当其他线程没有拿到 monitor 对象时, 则需要阻塞等待获取该对象.
而对于同步方法是依赖了方法修饰符 flags 上的 ACC_SYNCHRONIZED 实现的, JVM 就是根据该标识符来实现方法同步的. 当方法被调用时, 调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置, 如果设置了, 执行线程将先获取 monitor, 获取成功之后才能执行方法体. 执行完后再释放 monitor. 在方法执行期间, 其他任何线程无法再获得同一个 monitor 对象.

ReentrantLockLock 的默认实现方式之一, 它是基于 AQS 实现的. 通过内部的一个 state 字段来表示锁是否被占用. 0 表示未占用, 此时线程就可以通过 CAS 操作将 state 改为 1成功获取锁. 而其他线程只能排队等待获取资源.

两者相同点:

  • 都是用来协调多线程对共享对象, 变量的访问
  • 都是可重入锁, 同一线程可以多次获得同一把锁
  • 都保证了可见性和互斥性.

两者不同点

  • ReentrantLock 显示的获得,释放锁, synchronized 隐式的获得, 释放锁.
  • ReentrantLock 可相应中断, synchronized 无法相应中断.
  • ReentrantLock 同时实现了公平锁.
  • ReentrantLock 可以知道有没有成功获取锁.
  • ReentrantLock 在发生异常时, 如果没有在 finally 中主动通过 unlock() 释放锁, 则可能造成死锁线程. 而 synchronized 发生异常时, 会主动释放锁.

ReentrantLock的可以看 从 LockSupport 到 AQS 的简单学习中有源码分析
 

3. synchronized 做了哪些优化

JDK 1.6 引入了自旋锁, 适应性自旋锁, 锁消除, 锁粗化, 以及锁的升级等技术来减少锁操作的开销.

  • 锁消除, 会通过逃逸分析的方式, 去分析加锁的代码是否被一个或者多个线程使用, 或者等待被使用. 如果分析证实, 只有一个线程访问, 在编译这个代码段的时候, 就不会生成 synchronized 关键字, 仅仅生代码对应的机器码
  • 适应性自旋锁: 简单来说, 就是线程如果自旋成功了, 则下次自旋的次数会更多, 如果自旋失败了, 则自旋的次数减少.
  • 锁粗化: 将临近的代码块用同一个锁合并起来. 消除无意义的锁获取和释放, 可以提高程序运行性能
  • 锁升级: 无锁状态 -> 偏向锁状态 -> 轻量级锁状态 -> 重量级锁.
     

4. volatile 原理

通过使用 Lock 前缀的指令将当前处理器缓存行的数据写回到主内存, 将其他处理器的缓存无效. 需要数据操作的时候需要再次去主内存中读取.
通过插入内存屏障指令来禁止会影响变量可见性的指令重排序.指令如下

  • 在每个 volatile 写操作的前面插入一个 StoreStore 屏障.
  • 在每个 volatile 写操作的后面插入一个 StoreLoad 屏障.
  • 在每个 volatile 读操作的后面插入一个 LoadLoad 屏障.
  • 在每个 volatile 读操作的后面插入一个 LoadStore 屏障.

详情见:从 java 内存模型到 volatile 的简单理解
 

5. volatilesynchronize 有什么区别?

volatile 是最轻量的同步机制, 它保证了不同线程对这个变量进行操作时的可见性, 即一个线程修改了某个变量的值, 这个值对其他线程来说是立即可见的. 但是无法保证操作的原子性, 因此多线程下写的复合操作会导致线程安全问题.

synchronize 可以修饰方法或者以同步块的形式来进行使用, 确保了多个线程在同一时刻只能有一个线程处于方法或者同步块中, 保证了线程对变量访问的可见性和排他性, 又称为内置锁机制.
 

6. CAS 无锁编程的原理

线程处理器基本都支持 CAS 指令, 只不过实现的算法不同, 每一个 CAS 操作都分为三个运算符, 一个内存地址 V, 一个期望值 A 和一个新值 B. 操作的时候如果这个地址上存放的值等于期望值 A, 则将地址上的值更新为 B. 否则不做任何操作.

CAS 的基本思路就是: 如果这个地址上的值和期望值相等, 则给其赋予新值, 否则不做任何事. 但是要返回原值是多少. 循环 CAS 就是在一个循环里不断的做 CAS 操作, 直到成功为止.

使用 CAS 同时带来了三大问题

  • ABA 问题: 简单来说就是说一个原值是 A , 有一个线程将其变成了 B 接着又变成了 A, 那么使用 CAS 进行比较检查的时候发现值是没有任何变化的. 但是实际上也是发生了变化. 解决方法是使用版本号的方式来解决. , 就是在变量前追加上版本号. 每次变量更新的时候把版本号加 1, 那么 A-B-A, 就变成了 1A-2B-3A. JDK 也同样提供了两个类来帮助我们实现这个版本号的问题, 分别是 AtomicStampedReference, AtomicMarkableReference.

  • 循环时间久开销大: 自旋 CAS 如果长时间都不成功, 那么会给 CPU 带来非常大的执行开销. 如果 JVM 能支持处理器提供的 pause 指令, 那么效率会有一定的提升. pause 指令有两个作用, 第一可以延迟流水线执行指令, 使 CPU 不会消耗过多的执行资源. 第二可以避免在退出循环的时候因内存顺序冲突而引起的 CPU 流水线被清空. 从而提高 CPU 执行效率.

  • 只能保证一个共享变量的原值操作: 当对一个共享变量进行操作时, 可以怂恿自旋 CAS 的方式来保证原子性, 但是对于多个共享变量操作时, 自旋 CAS 就无法保证其原子性, 这时候就可以使用锁机制, 也可使用 JDK 提供的 AtomicReference 原子操作类来保证引用对象之间的原子性, 就可以将多个原子变量放到一个对象里进行 CAS 操作.

详情见:java 基础回顾 - 基于 CAS 实现原子操作的基本理解
 

7. AQS 原理

AQS 即抽象的队列同步器. 是用来构建锁或者其他同步组件的基础框架. 不能被实例化, 设计之初就是为了让子类通过继承 AQS 并实现它的抽象方法来管理同步状态. 如 ReentrantLock, ReentrantReadWriteLock, CountDownLatch 就是基于 AQS 实现的.

AQS 是基于 CLH 队列的变体实现的, 是一个双向同步队列. 它获取不到共享资源的线程封装成为一个 Node 节点加入到队列中, 每个 Node 节点维护一个 prevnext引用. 分别指向自己的前驱节点与后置节点. 通过 CAS, 自旋, 以及 LockSuppor.park() 等方式维护内部的一个使用 volatile 修饰的 int 类型共享变量 state 的状态, 使并发达到同步的效果.
详情见: 从 LockSupport 到 AQS 的简单学习
 

8. 线程的声明周期

Java 中线程的状态分为 6 种.

  • 初始状态: 新建了一个线程对象, 但是还未调用 start 方法.
  • 运行状态: Java 线程中奖就绪(ready) 和运行(running) 两种状态笼统的成为 运行状态.
    • 线程对象创建后, 其他线程(比如 main 线程) 调用了该对象的 start() 方法, 该状态的线程位于可运行线程池中, 等待被线程调度选中, 从而获得 CPU 的使用权, 此时就处于就绪状态(ready) .
    • 就绪状态的线程在获得CPU 时间片后变为运行中状态(running).
  • 阻塞状态.
  • 等待状态: 进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)
  • 超时等待: 该状态不同与等待状态, 它可以在指定的时间后执行返回.
  • 终止状态: 表示该线程已经执行完毕.
    详情见: java 基础回顾 - 线程基础

     

9. sleep, wait, yield 的区别, wait 的线程如何唤醒

sleep, yield 被调用后, 都不会释放当前线程所持有的锁.
调用 wait 方法会释放当前线程持有的锁, 而且被唤醒后会重新去竞争锁, 锁竞争到后才会执行 wait 方法后面的代码.
wait 通常被通用语线程间交互.
sleep 通常被用于暂停执行.
yield 方法使当前线程让出 CPU 执行权.
调用wait 方法后使用 notify / notifyAll 进行唤醒.
 

9. ThreadLocal 是什么

ThreadLocal 为每个线程都提供了变量的副本, 是的每个线程在某一时间访问到的并非同一对象, 这样就隔离了多个线程对数据的数据共享. 在内部实现上, 每个线程内部都有一个 ThreadLocalMap, 用来保存每个线程所拥有的变量副本.
详情可见: Android 消息机制之 ThreadLocal 深入源码分析 [ 二 ]
 

10. 为什么要使用线程池

合理的使用线程池能够带来下面三个好处.

  • 降低资源消耗. 通过重复利用已创建的线程降低线程创建和销毁造成的消耗.
  • 可以控制最大并发数. 避免大量线程之间因相互抢占系统资源而导致的阻塞现象.
  • 提高线程的可管理性. 线程是稀缺资源, 如果无限制的创建, 不仅会消耗系统资源, 还会降低系统的稳定性. 使用线程池可以进行统一分配, 调优和监控.

线程池执行任务的流程

  • 如果当前运行的线程小于 corePoolSize, 则创建新线程来执行任务.
  • 如果运行的线程等于或大于 corePoolSize, 则将新任务加入到 BlockingQueue.
  • 如果无法加入 BlockingQueue (队列已满), 则创建新的线程来处理任务.
  • 如果创建新线程使当前运行的线程超出了 maximumPoolSize, 那么执行拒绝策略.

线程池中的几种拒绝策略为一下几种

  • AbortPolicy: 直接抛出异常. 线程池中默认的拒绝策略.
  • DiscardOldestPolicy : 直接丢弃阻塞队列中最老的任务, 也就是最前面的任务, 并执行当前任务.
  • CallerRunsPolicy: 提交任务所在的线程来执行这个要提交的任务.
  • DiscardPolicy: 直接丢弃最新的任务, 也就是最后面的.
    也可以根据场景来实现 RejectedExecutionHandler 接口, 自定义拒绝策略, 比如记录日志或者持久化存储被拒绝的任务.
    详情见: 重识 java 线程池
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章