死锁

deadlock或deadly embrace

对于那些容易发生死锁的代码,遵循以下原则有助于规避死锁:

  • 只在必要的最短时间内持有锁。考虑使用同步语句块代替整个同步方法。
  • 尽量编写不在同一时刻需要持有多个锁的代码。如果不可避免,则确保线程持有第二个锁的时间尽量短暂。
  • 创建和使用一个大锁来代替若干小锁。把这个锁用于互斥,而不是用作单个对象的对象级别锁。

    1.byte[] lock = new byte[0];  // 特殊的instance变量,零长度的byte数组对象创建起来将比任何对象都经济――查看编译后的字节码:生成零长度的byte[]对象只需3条操作码,而Object lock = new Object()则需要7行操作码。

    2.资源获取即初始化(RAII, Resource Acquisition Is Initialization)

       区域锁 (Scoped Lock)

      Monitor Object 设计模式

    http://www.ibm.com/developerworks/cn/java/j-lo-synchronized/

    线程间通信

    线程间通信比进程间通信要容易,因为线程之间可以共享进程的内存空间。可以共享位于进程全局数据区和栈和堆上的所有内容。唯一只属于某个线程的就是线程的栈--------它可以存放只属于线程的对象。

    线程间通信的方式:共享进程的变量、TLS(Thread Local Storage)、并发同步和互斥。

    调用线程的interrupt是当希望某个线程中断并消亡时的粗鲁的线程间通信方法。

    线程观察等待某个变量值发生变化有两种机制:

    • 繁忙/等待机制
    • 等待/通知机制

    等待/通知机制

    等待/通知机制允许一个线程等待来自另一个线程的通知。一般情况下,第一个线程检查某个变量的值是否是它所需要的。第一个线程调用wait,并进入休眠状态(几乎使用零处理器资源),直到接收到变量值改变的通知为止。最终,第二个线程起作用,更改变量的值,并调用notify或调用notifyAll(),通知休眠线程该变量已经更改。
    等待/通知机制并不要求一个线程检查变量,另一个线程设置变量。不过,一般来说,至少结合一个变量来使用这个机制是一种理想的做法。这样做有助于避免遗漏通知,有助于检测到早期通知。

    调用对象的wait方法要求调用线程已获得了对象的对象级别锁。调用wai方法的线程会释放对象级别锁,并进入休眠,直到接到通知或被中断为止。

    如果等待线程被中断,它将与另一个线程竞争获取对象级别锁,同时从wait内抛出interuptedException。

    如果等待线程接到通知,它将与另一个线程竞争获取对象级别锁,然后从wait返回。

    遗漏通知和早期通知

    遗漏通知:未进入等待而先发出了通知,结果有wait而不再有notify。

    在发出通知后不再wait即规避了遗漏通知,使用另一个变量来判断如果没有还没有通知再执行wait。设置检查指示器,判断是否需要等待。

    早期通知:wait提前返回没有等待nofity。

    当一个线程从wait()返回时,检查所等待的条件是否满足。在wait返回后检查判断继续wait。

    防止早期通知的一般原则:应当将wait()语句放在while循环内部。这样不管wait()语句返回的理由是什么,都可以确保只有满足适当的条件时才继续往下执行,否则继续wait。

    还有等待完全超时的技术,以及Bo0leanLock的使用。只能容纳一个项的FIFO队列对线程间通信非常有用,因为它可以完美地封装等待/通知机制的复杂性。

    数据管道

    jpvaio包提供了从流中读写数据的许多类。大部分时间,数据从文件或网络连接中读写
    使用线程后,不是数据流动到文件,而可以通过管道(pipe)流动到另一个线程。第一个线程写入管道,第二个线程从管道中读取。管道不是一个文件也不是一个网络连接,而是内存中的一个结构,它保存读之前所写入的数据。通常,管道有一个固定的容量。当管道充满容量时,试图写入更多的数据将导致阻塞,直到另一个线程从管道移走(读取)了一些数据后,才能解除阻塞。同样当管道为空时,试图从管道读取数据将被阻塞,一直到另一个线程写入数据为止。
    在java.io 包中,有4个管道相关的类可用于在线程间流动数据:

    • PipedInputStream,继承于InputStream
    • PipedOutputStream,继承于OutputStream
    • PipedReader,继承于Reader
    • PipedWriter,继承于Writer

    PipedlnputStream l PipedOutputStream 互配合在线程间传输字节。PipedReader 和PipedWriter相互配合在线程间传输字符数据。PipedOutputStream对象使用它所连接的PipedlnputStream对象的引用。PipedWriter使用一个它所连接的PipedReader对象的引用。PipedInputStream 和PipedOuputStream均代表管道的一个末端,在发送数据前需要相互连接
    由PipedInputStream和PipedOutputStream组成的管道的容量是1024个字节。这意味着,进行写的线程可以在进行读之前进行多达1024个字节的写入。这个缓存使得数据传输效率比单字节传输效率更高。由PipedReader和PipedWriter组成的管道容量为1024个字符。同样,这个缓存使得进行写入工作的线程比进行读取工作的线程更超前一些。API文档没有提供内部管道大小的任何信息和保证。因此编程中不应当把1024当成一个万用值。

    特定线程变量

    即把同一个特殊公共变量放在不同的线程中,可以达到线程内部私有变量的效果。

    线程内部的局部变量可以在多线程环境下访问时,能保证各个线程的变量相对独立于其他线程的变量。线程内部的局部变量,不同的线程之间不会相互干扰,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量传递的复杂度。

    ThreadLocal

    ThreadLocal类用来提供线程内部的局部变量。

    ThreadLocal有两个公有方法:public object get() 和 public void set(object value),一个保护方法:protected Object initialvalue()。

    从ThreadLocal的get方法返回的值取决于哪个线程调用该方法。

    ThreadLocal不是abstract,但它需要生成子类才能发挥作用。

    ThreadLocal与Synchronized的区别

    也可以使用synchronized关键字加锁来达到同样的线程隔离的效果,ThreadLocal与synchronized关键字都用于处理多线程并发访问变量的问题。

    Synchronized

    ThreadLocal

    原理区别

    同步机制采用“以时间换空间”的方式,提供同一份变量,让不同的线程排队访问。

    ThreadLocal采用“以空间换时间”的方式,为每一个线程各提供了一份变量,实现同时访问不干扰。

    原子类和锁的区别

    • 锁

      • 工作流程:
        • 线程A获取锁后操作资源
        • 线程B尝试获取失败进入阻塞
        • 线程A释放锁后唤醒线程B
      • 适用场景:写操作多的场景,保证数据强一致性
    • 原子类

      • CAS机制:
        • 比较内存值与预期原值
        • 相等则修改内存值为新值,不相等则自旋重试
        • 通过do-while循环实现无锁更新
      • 性能优势:读操作多的场景,避免阻塞带来的性能损耗
      • 典型实现:AtomicInteger使用Compare-And-Swap指令

    原子类和volatile的区别

    1. 原子类

    • 实现原理:使用CAS(Compare-And-Swap)机制实现无锁数据更新,通过自旋设计避免线程阻塞-唤醒带来的系统资源开销
    • 适用场景:多线程计数、原子操作、并发量小的场景(客户端场景)
    • 性能特点:在并发量大的服务端场景(每秒几千万并发)会因自旋消耗更多资源,但在客户端场景下性能表现良好
    1)调用API
    • 构造方法:AtomicInteger atomicInteger = new AtomicInteger(1),可传入初始值
    • 常用方法:
      • getAndIncrement():值加1,返回加操作前的值
      • getAndAdd(2):值加指定数(如2),返回加操作前的值
      • getAndDecrement():值减1,返回减操作前的值
      • getAndAdd(-2):值减指定数(如-2),返回减操作前的值
    2)应用案例
    • 例题:原子类volatile关键字线程安全演示
      • 实验设计:
        • 定义两个变量:一个用AtomicInteger修饰,一个用volatile修饰
        • 开启两个线程,每个线程循环10000次对两个变量进行自增操作
        • 预期结果:两个变量最终值都应为20000
      • 实验结果:
        • 原子类结果:20000(正确)
        • volatile结果:19915(不正确)
      • 原因分析:
        • 原子类能保证复合操作的原子性
        • volatile不能保证非原子操作(如count++)的线程安全,因为count++实际包含"取值-计算-赋值"三个非原子操作
    2. 原子类介绍
    1)原子类数组
    • 实现类:AtomicIntegerArray
    • 特点:提供对数组元素的原子更新操作
    • 底层实现:使用sun.misc.Unsafe类实现底层数组操作
    3. volatile关键字
    • 工作原理:
      • 强制线程每次访问都从共享内存重新读取值
      • 值变化时强制将新值写回共享内存
      • 为了提高处理器的执行速度,在处理器和内存之间增加了多级缓存来提升,volatile用来解决缓存数据不一致问题。
      • Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程中是用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。所以,就可能出现线程1改了某个变量的值,但是线程2不可见的情况。
      • volatile除了可以保证数据的可见性,还可以禁止指令重排优化。 普通的变量仅仅会保证在该方法的执行过程中所依赖的赋值结果的地方都能获得正确的结果,而不能保证变量的赋值操作的顺序与程序代码中的执行顺序一致。volatile可以禁止指令重排,这就保证了代码的程序会严格按照代码的先后顺序执行。这就保证了有序性。被volatile修饰的变量的操作,会严格按照代码顺序执行。
      • 原子性是指一个操作是不可中断的。线程是CPU调度的基本单位,CPU有时间片的概念,会根据不同的调度算法进行线程调度。当一个线程获得时间片之后开始执行,在时间片耗尽之后,就会失去CPU使用权。所以在多线程场景下,由于时间片在线程间轮换,就会发生原子性问题。 为了保证原子性,需要通过字节码指令monitorenter和monitorexit,但是volatile和这两个指令之间是没有任何关系的。
    • 局限性:
      • 不能解决非原子操作的线程安全问题
      • 性能低于原子类
    • 适用场景:
      • 仅适用于简单的原子赋值操作(如count=5)
      • 不适用于复合操作(如count=count+1或count++)
      • 使用双重锁校验的形式实现单例,其中使用volatile关键字修饰可能被多个线程同时访问到的singleton。
      • volatile关键字保证变量在多线程之间的可见性,它是java.util.concurrent包的核心。

    Synchronized的优势劣势

    优势:

    • 同步方法中出现异常,jvm也能够自动释放锁,主动规避死锁。
    • 不需要手动释放锁。

    劣势:

    • 必须要等到获取锁对象的线程执行完成,或者出现异常,才能释放掉。不能中途释放锁,不能中断一个正在试图获得锁的线程。
    • 不知道多个线程竞争锁的时候,获取锁成功与否,不够灵活。
    • 每个锁仅有单一的条件(某个对象)不能设定超时。

    并发安全

    并发安全优化需根据场景选择合适方案:

    方案

    适用场景

    特性

    synchronized

    简单同步需求

    JDK - 6后性能优化,谷歌推荐使用

    Lock接口

    需精细控制锁获取/释放

    支持读写锁分离

    原子类

    低并发+读多写少场景

    CAS自旋机制

    并发容器

    高频访问共享数据

    ConcurrentHashMap/CopyOnWriteArrayList等

    1) synchronized

    synchronized关键字的现代特性:

    • 锁升级机制:从偏向锁到重量级锁的渐进式优化
    • 适用性:无需关注锁细节时的首选方案
    2) 锁

    锁优化三大原则:

    • 减少持锁时间:同步块仅包含必要代码(示例代码优化后效率提升2秒)
    • 锁分离:读写锁应对读多写少场景
    • 锁粗化:合并相邻同步块降低锁请求/释放开销
    • 减少持锁时间

    持锁时间优化通过提取非同步代码实现。

    • 锁分离

    读写锁(ReentrantReadWriteLock)的并发优势:

    • 读读并行:无锁冲突
    • 写操作独占:保证数据一致性
    • 锁粗化
      

    锁粗化通过合并相邻同步块降低系统开销,适用于短耗时非同步操作穿插场景。

    3) 原子类

    原子类(AtomicInteger等)的适用边界:

    • 优势场景:低并发+高频读操作
    • 自旋缺陷:高并发时性能劣于锁机制
    4) 并发容器

    并发容器解决方案:

    • ConcurrentHashMap:分段锁技术
    • CopyOnWriteArrayList:写时复制机制

    线程协作

    线程协作主要实现方式对比:

    机制

    唤醒精度

    安全性

    典型应用

    wait/notify

    全局唤醒

    基础线程同步

    Condition

    分组唤醒

    复杂等待条件

    CountDownLatch

    一次性屏障

    多任务完成检测

    Semaphore

    许可控制

    资源访问限流

    1) Object.wait-notify/notifyAll

    wait/notify机制的局限性:

    • 执行顺序强制:wait必须早于notify调用
    • 唤醒粒度:无法定向唤醒特定线程
    2) Condition.await-signal/signalAll

    Condition接口的优势:

    • 精准唤醒:配合ReentrantLock实现分组唤醒
    • 安全性:await/signal调用顺序检测机制
    3) CountDownLatch

    CountDownLatch实现原理:

    • 原子计数器:初始化值=需等待线程数
    • 关键方法:
      • countDown():计数器减1
      • await():阻塞至计数器归零
    4) Semaphore

    Semaphore核心机制:

    • 许可证管理:
      • acquire():阻塞获取许可
      • tryAcquire():非阻塞尝试
      • release():释放许可
    • 公平性选择:构造参数指定公平/非公平模式

    LockSupport

    有三种情况会使得Runnable状态到waiting状态:

    • 调用无参的Object.wait()方法。等到notifyAll()或者notify()唤醒就会回到Runnable状态。
    • 调用无参的Thread.join()方法。也就是比如在主线程里面建立了一个线程A,调用A.join(),那么主线程是得等A执行完了才会继续执行,这时主线程就是等待状态。
    • 调用LockSupport.park()方法。

    LockSupport是Java6引入的一个工具类,Java并发包中的锁都是基于它实现的,再调用LocakSupport.unpark(Thread thread),就会回到Runnable状态。

    与 Object.wait()/notify() 的对比

    特性 LockSupport.park()/unpark() Object.wait()/notify()
    作用对象 线程为操作目标。 作用于对象监视器锁,必须在 synchronized 块内使用。
    调用顺序 顺序灵活unpark 可以在 park 之前调用,不会导致线程永久阻塞。 顺序严格。如果 notify 先于 wait 发生,wait 的线程可能永远无法被唤醒。
    灵活性 更底层,是构建高级同步工具的基础。 较高级,但灵活性差,必须配合监视器锁使用。
    性能 基于更现代的同步原语(条件变量+FUTEX),通常更高效。 早期实现可能有更多开销。

    LockSupport.park() 会响应中断,但不会抛出 InterruptedException,也不会自动清除中断状态。

    与其他阻塞方法的对比

    行为 LockSupport.park() Thread.sleep()  Object.wait()
    中断响应 ✅ 立即返回,不抛异常 ✅ 抛出 InterruptedException ✅ 抛出 InterruptedException
    中断状态 ❌ 不清除 ✅ 自动清除 ✅ 自动清除
    是否释放锁 ❌ 不释放 ❌ 不释放 ✅ 释放

      Handler面试八问

      1.为什么主线程不会因为Looper.loop()里的死循环卡死

      主线程通过nativePollOnce阻塞释放CPU资源:

      • 消息队列无消息时进入阻塞状态
      • 新消息到达或延迟时间触发后唤醒线程
      • 阻塞期间不占用CPU资源,避免性能损耗。
      2.post和sendmessage两类发送消息的方法有什么区别

      方法

      实现差异

      处理优先级

      post

      封装Runnable为Message.callback

      优先执行

      sendMessage

      直接发送Message对象

      次优执行

      最终均调用sendMessageAtTime插入消息队列,由dispatchMessage按callback→Handler.callback→handleMessage顺序分发。

      3.使用message.obtain方法的原因

      obtain方法通过对象池复用Message:

      • 减少对象创建开销
      • 降低GC频率
      • 避免内存抖动
      4.发送延迟消息的原因

      延迟消息实现机制:

      • SystemClock.uptimeMillis计算目标时间戳 - 按时间戳排序插入消息队列
      • nativePollOnce阻塞至延迟时间到达
      5.同步屏障消息的作用

      同步屏障功能:

      • 强制优先处理异步消息(如UI测绘流程)
      • 需配合Message.setAsynchronous使用
      • 典型场景:ViewRootImpl渲染优化
      6.IdleHandle的作用

      IdleHandler核心功能:

      • 监听消息队列空闲状态
      • 回调queueIdle执行懒加载/日志上报
      • 避免资源竞争,提升性能
      7.非静态Handler导致内存泄露的原因

      内存泄露链:

      Message持有Handler → Handler持有Activity → Activity无法回收

      解决方案:

      • 使用静态Handler+弱引用
      • 生命周期结束时清除消息队列
      8.子线程中弹出toast的方法

      子线程使用Looper必备步骤:

      • 显式调用Looper.prepare() - 调用Looper.loop()启动循环
      • 任务完成后执行quit/quitSafely退出
      Logo

      AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

      更多推荐