Java并发 -- 线程生命周期
通用的线程生命周期 初始状态 线程已经被创建,但还不允许分配CPU执行 该状态属于编程语言所特有,仅仅在编程语言层面被创建,在操作系统层面,真正的线程还没有创建 可运行状态 线程可以分配CPU执行,该状态下真正的操作系统线程已经被创建 运行状态 当有空闲的CPU时,操作系统会将其分配给处于可运行状态的线程,被分配到CPU的线程的状态就转换为运行状态 休眠状态 处于运行状态的线程如果调用一个阻塞的API或者等待某个事件,那么线程状态就会切换为休眠状态 切换为休眠状态的同时会释放CPU使用权,_处于休眠状态的线程永远没有机会获得CPU使用权_ 当等待的事件出现后,线程就会从休眠状态切换到可运行状态 终止状态 线程执行完或者出现异常就会进入终止状态,处于终止状态的线程不会切换到其它状态 进入终止状态意味着线程生命周期的结束 简化合并 通用的线程生命周期里的5种状态在不同的编程语言会有简化合并 Java把可运行状态和运行状态合并了 这两个状态对操作系统调度层是有价值的,但JVM把线程调度交给了操作系统处理,JVM并不关心这两个状态 JVM同时也细化了休眠状态 Java线程的生...
Java核心 -- 字符串
String String是Java语言非常基础和重要的类,提供了构造和管理字符串的各种基本逻辑,是典型的Immutable类 String是Immutable类的典型实现,原生的保证了基础线程安全,因为无法对它内部数据进行任何修改 被声明为final class,由于String的不可变性,类似拼接、裁剪字符串等动作,都会产生新的String对象 由于字符串操作的普遍性,所以相关操作的效率往往对应用性能有明显影响 StringBuffer StringBuffer是为了解决拼接产生太多中间对象的问题而提供的一个类 StringBuffer本质是一个线程安全的可修改字符串序列,保证了线程安全,但也带来了额外的性能开销 StringBuffer的线程安全是通过把各种修改数据的方法都加上synchronized关键字实现的 这种方式非常适合常见的线程安全类的实现,不必纠结于synchronized的性能 过早的优化是万恶之源,可靠性、正确性和代码可读性才是大多数应用开发的首要考虑因素 StringBuilder StringBuilder在能力上和StringBuffer没有本质区别,但去掉...
Java并发 -- 管程
概述 Java语言在1.5之前,唯一提供的并发原语是管程 在Java 1.5提供的JUC包中,也是以管程技术为基础的 管程是一把解决并发问题的万能钥匙 管程 在Java 1.5之前,仅仅提供synchronized关键字和wait/notify/notifyAll方法 Java采用的是管程技术,synchronized关键字以及wait/notify/notifyAll方法都是管程的组成部分 管程和信号量是等价的(即用管程能实现信号量,用信号量也能实现管程),但管程更容易使用,所以Java选择了管程 Monitor,在Java领域会翻译成监视器,在操作系统领域会翻译成管程 管程:_管理共享变量以及对共享变量的操作过程,让它们支持并发_ 对应Java领域:管理类的成员变量和成员方法,让这个类是线程安全的 MESA模型 在管程的发展史上,先后出现了三种不同的管程模型,分别是:Hasen模型、Hoare模型和MESA模型 现在广泛应用的是MESA模型,Java管程的实现也参考了MESA模型 管程可以解决并发领域的两大核心问题:_互斥+同步_ 互斥:在同一时刻...
Java核心 -- 引用
强引用、软引用、弱引用、幻象引用主要差别:对象不同的可达性(reachable)和对垃圾收集的影响 强引用 - Strong 最常见的普通对象引用,只要还有强引用指向一个对象,就表明该对象还存活,垃圾收集器不会处理这种对象 一个普通的对象,如果没有其他的引用关系,一旦超过了引用的作用域或者显式地将强引用赋值为null,就有可能被收集 软引用 - Soft 软引用是一种相对于强引用弱化一些的引用,可以让对象豁免一些垃圾收集(内存不足) 只有当JVM认为内存不足时,才会去试图回收软引用指向的对象 JVM会确保在抛出OutOfMemoryError之前,清理软引用指向的对象,软引用通常用来实现内存敏感的缓存 弱引用 - Weak 弱引用不能使对象豁免垃圾收集,仅仅只是提供一种访问在弱引用状态下对象的途径 弱引用可以用来构建一种没有特定约束的关系,例如维护一种非强制性的映射关系 如果试图获取时对象还在,就使用它,否则重新实例化 弱引用也是很多缓存实现的选择 虚引用 - Phantom 不能通过虚引用访问到对象,虚引用仅仅只是提供一种机制:_确保对象被finalize以后执行某些事情_ 可以利用虚引...
Java并发 -- 安全性、活跃性、性能
安全性问题 线程安全的本质是正确性,而正确性的含义是程序按照预期执行 理论上线程安全的程序,应该要避免出现可见性问题(CPU缓存)、原子性问题(线程切换)和有序性问题(编译优化) 需要分析是否存在线程安全问题的场景:_存在共享数据且数据会发生变化,即有多个线程会同时读写同一个数据_ 针对该理论的解决方案:不共享数据,采用线程本地存储(Thread Local Storage,TLS);不变模式 数据竞争数据竞争(Data Race):多个线程同时访问同一数据,并且至少有一个线程会写这个数据 add12345678910private static final int MAX_COUNT = 1_000_000;private long count = 0;// 非线程安全public void add() { int index = 0; while (++index < MAX_COUNT) { count += 1; }} add + synchronized123456789101112131415161718p...
Java并发 -- 等待-通知机制
循环等待 在《Java并发 – 死锁》中,通过破坏占用且等待条件来规避死锁,核心代码如下 while (!allocator.apply(this, target)) {} 如果apply()操作的时间非常短,并且并发不大,该方案还能应付 一旦apply()操作比较耗时,或者并发比较大,该方案就不适用了 因为这可能需要循环上万次才能获得锁,非常_消耗CPU_ 最好的方案:_等待-通知_ 当线程要求的条件不满足,则线程阻塞自己,进入等待状态 当线程要求的条件满足后,通知等待的线程,重新开始执行 线程阻塞能够避免因循环等待而消耗CPU的问题 Java语言原生支持等待-通知机制 线程首先获取互斥锁,当线程要求的条件不满足时,释放互斥锁,进入等待状态 当要求的条件满足时,通知等待的线程,重新获取互斥锁 等待-通知机制Java语言原生支持的等待-通知机制:**synchronized + wait + notify/notifyAll** wait 每个互斥锁有两个独立的等待队列,如上图所示,等待队列L和等待队列R 左边有一个等待队列(等待队列L),_在同一时刻,只允...
Java并发 -- 死锁
Account.class 在《Java并发 – 互斥锁》中,使用了Account.class作为互斥锁来解决银行业务的转账问题 虽然不存在并发问题,但所有账户的转账操作都是串行的,性能太差 例如账户A给账户B转账,账户C给账户D转账,在现实世界中是可以并行的,但该方案中只能串行 账户和账本 每个账户都对应一个账本,账本统一存放在文件架上 银行柜员进行转账操作时,需要到文件架上取出转出账本和转入账本,然后转账操作,会遇到三种情况 如果文件架上有转出账本和转入账本,都同时拿走 如果文件架上只有转出账本或只有转入账本,那需要等待那个缺失的账本 如果文件架上没有转出账本和转入账本,那需要等待两个账本 两把锁123456789101112131415161718public class Account { // 账户余额 private int balance; // 转账 public void transfer(Account target, int amt) { // 锁定转出账户 synchronized (this...
Java并发 -- 互斥锁
解决什么问题互斥锁解决了并发程序中的原子性问题 禁止CPU中断 原子性:一个或多个操作在CPU执行的过程中不被中断的特性 原子性问题点源头是线程切换,而操作系统依赖CPU中断来实现线程切换的 单核时代,禁止CPU中断就能禁止线程切换 同一时刻,只有一个线程执行,禁止CPU中断,意味着操作系统不会重新调度线程,也就禁止了线程切换 获得CPU使用权的线程可以不间断地执行 多核时代 同一时刻,有可能有两个线程同时在执行,一个线程执行在CPU1上,一个线程执行在CPU2上 此时禁止CPU中断,只能保证CPU上的线程不间断执行,但并不能保证同一时刻只有一个线程执行 互斥:同一时刻只有一个线程执行 如果能保证对共享变量的修改是互斥的,无论是单核CPU还是多核CPU,都能保证原子性 简易锁模型 临界区:一段需要互斥执行的代码 线程在进入临界区之前,首先尝试加锁lock() 如果成功,则进入临界区,此时该线程只有锁 如果不成功就等待,直到持有锁的线程解锁 持有锁的线程执行完临界区的代码后,执行解锁unlock() 锁和资源 synchronized12345678910111213141...
Java并发 -- Java内存模型
解决什么问题Java内存模型解决了并发程序中的可见性问题和有序性问题 Java内存模型按需禁用 CPU缓存会导致可见性问题,编译优化会导致有序性问题 解决可见性和有序性最直接的办法:_禁用CPU缓存和编译优化_ 问题虽然解决了,但程序性能会大大下降 合理的方案:_按需禁用CPU缓存和编译优化_ 为了解决可见性和有序性的问题,只需要给程序员提供按需禁用CPU缓存和编译优化的方案即可 程序员视角 Java内存模型规范了_JVM如何提供按需禁用CPU缓存和编译优化的方法_ 具体方法包括:volatile、synchronized和final关键字,以及六个Happens-Before规则 volatile volatile关键字不是Java语言的特产,在古老的C语言也有,最原始的意义就是_禁用CPU缓存_ volatile int x = 0:告诉编译器,对这个变量的读写,不能使用CPU缓存,必须从内存中读取或者写入 Java代码1234567891011121314151617public class VolatileExample { private int x = 0...
Java并发 -- 问题源头
CPU、内存、IO设备 核心矛盾:三者的_速度差异_ 为了合理利用CPU的高性能,平衡三者的速度差异,计算机体系结构、操作系统和编译程序都做出了贡献 计算机体系结构:CPU增加了缓存、以均衡CPU与内存的速度差异 操作系统:增加了进程、线程,分时复用CPU,以平衡CPU和IO设备的速度差异 编译程序:优化指令执行次序,使得缓存能够得到更加合理地利用 CPU缓存 -> 可见性问题单核 在单核时代,所有的线程都在一颗CPU上执行,CPU缓存与内存的数据一致性很容易解决 因为所有线程操作的都是同一个CPU的缓存,一个线程对CPU缓存的写,对另外一个线程来说一定是可见的 可见性:一个线程对共享变量的修改,另一个线程能够立即看到 多核 在多核时代,每颗CPU都有自己的缓存,此时CPU缓存与内存的数据一致性就没那么容易解决了 当多个线程在不同的CPU上执行时,操作的是不同的CPU缓存 线程A操作的是CPU-1上的缓存,线程B操作的是CPU-2上的缓存,此时线程A对变量V的操作对于线程B而言不具备可见性 代码验证123456789101112131415161718192021222324...













