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 + synchronized123456789101112131415161718priv ...
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()
锁和资源
synchronized12345678910111213141516 ...
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而言不具备可见性
代码验证123456789101112131415161718192021222324252 ...
Java并发 -- 概述
核心问题分工
JUC中的Executor、Fork/Join和Future本质上都是一种分工方法
并发编程领域还总结了一些设计模式,基本上都是和分工方法相关
生产者-消费者
Thread-Per-Message
Worker Thread
同步
在并发编程领域里的同步,主要指的就是_线程间的协作_
一个线程执行完了一个任务,如何通知执行后续任务的线程开始工作
协作一般是与分工相关的
JUC中的Executor、Fork/Join和Future本质上都是一种分工方法
但同时也能解决线程协作的问题
例如,用Future可以发起一个异步调用
当主线程调用get()方法取结果时,主线程会等待
当异步执行的结果返回时,get()方法就自动返回了
Future工具类已经帮我们解决了_主线程和异步线程之间的协作_
JUC中的CountDownLatch、CyclicBarrier、Phaser和Exchanger都是用来解决线程协作问题的
但很多场景还是需要自己处理线程之间的协作,问题基本可以描述为
当某个条件不满足时,线程需要等待,当某个条件满足时,线程需要被唤醒执行
在Java并发编程领域,解 ...
Kafka -- 可靠性
可靠性保证
可靠性保证:确保系统在各种不同的环境下能够发生一致的行为
Kafka的保证
保证_分区消息的顺序_
如果使用同一个生产者往同一个分区写入消息,而且消息B在消息A之后写入
那么Kafka可以保证消息B的偏移量比消息A的偏移量大,而且消费者会先读取消息A再读取消息B
只有当消息被写入分区的所有同步副本时(文件系统缓存),它才被认为是已提交
生产者可以选择接收不同类型的确认,控制参数acks
只要还有一个副本是活跃的,那么已提交的消息就不会丢失
消费者只能读取已经提交的消息
复制
Kafka可靠性保证的核心:_复制机制_ + 分区的多副本架构
把消息写入多个副本,可以使Kafka在发生崩溃时仍能保证消息的持久性
Kafka的主题被分成多个分区,分区是基本的数据块,分区存储在单个磁盘上
Kafka可以保证分区里的事件总是有序的,分区可以在线(可用),也可以离线(不可用)
每个分区可以有多个副本,其中一个副本是首领副本
所有的事件都直接发送给首领副本,或者直接从首领副本读取事件
其他副本只需要与首领副本保持同步,并及时复制最新的事件即可
当首领副本不可用时,其中一个同步副本将成为新的首领
...
Kafka -- 内部原理
群组成员关系
Kakfa使用ZooKeeper来维护集群成员的信息
每个Broker都有一个唯一的ID,这个ID可以在配置文件里面指定,也可以自动生成
在Broker启动的时候,通过创建临时节点把自己的ID注册到ZooKeeper
Kakfa组件订阅ZooKeeper的/brokers/ids路径,当有Broker加入集群或者退出集群时,Kafka组件能获得通知
如果要启动另一个具有相同ID的Broker,会得到一个错误,这个Broker会尝试进行注册,但会失败
在Broker停机,出现网络分区或者长时间垃圾回收停顿时,Broker会从ZooKeeper上_断开连接_
此时,Broker在启动时创建的临时节点会从ZooKeeper上自动移除(ZooKeeper特性)
订阅Broker列表的Kafka组件会被告知该Broker已经被移除
在关闭Broker时,它对应的临时节点也会消失,不过它的ID会继续存在于其他数据结构中
例如,主题的副本列表里可能会包含这些ID
在完全关闭了一个Broker之后,如果使用相同的ID启动另一个全新的Broker
该Broker会立即加入集群,并拥有与旧Broker相同的 ...