jdk学习笔记
类加载
classloader
classloader就是负责加载类的一种对象。ClassLoader是抽象类。classloader通过Class的binary name定位和生成定义该类所需要的数据。经典的策略是将binary name转换成文件名,然后从文件系统读取.class文件
每个Class对象都包含一个classLoader引用,指向定义了该Class对象的类加载器
数组的Class对象不是由类加载器创建的,是由Java runtime自动创建
调用数组Class对象的getClassLoader方法返回的是其元素类型的类加载器,如果元素类型是基本类型,那么数组对象就没有类加载器
许多应用通过继承ClassLoader扩展了JVM动态加载类的方式
类加载器还负责定位资源。
类加载器通过委派模型取寻找class文件和资源文件。每个类加载器实例都有父类加载器,当类加载器被要求去搜索某个class或资源时,它会先维护父类加载器去找,找不到的话再自己找
并发加载类
内置类加载器
运行时内置的类加载器:
1.Bootstrap class loader
虚拟机内置的类加载器,通常是null,且没有父
2.Platform class loader
所有platform classes对于platform class loader都是可见的,可以作为类加载器实例的父
Platform classes包括Java SE platform APIs,它们的实现类和由platform class loader或其祖先定义的JDK-specific run-time classes
动态代理
juc
线程池
核心参数
corePool:核心线程池大小
maximumPool:最大线程池的大小
BlockingQueue:用来暂时保存任务的工作队列
RejectedExecutionHandler:当ThreadPoolExecutor已经关闭或ThreadPoolExecutor已经饱和(达到了最大线程池大小且工作队列已满),execute方法将要调用的Handler
keepAliveTime:超出核心线程数量的空闲线程的生存时间
内部变量allowCoreThreadTimeOut:默认是false,表示核心线程即使空闲仍可生存;设置为true的话keepAliveTime也对空闲的核心线程有效
新任务到达时,如果线程池中线程数量未达到核心线程数,则创建新的线程去执行任务;如果线程池线程数量不超过最大线程数,且任务队列已满的情况下,创建新的线程执行任务
volatile
Java内存模型保证对于volatile字段,所有线程都会看到一致的值
如果一个volatile变量也同时被final关键字修饰,编译时会报错?
cpu部分术语
- 内存屏障
一组cpu指令,实现对内存操作的顺序限制 - 缓存行
cpu高速缓存中可以分配的最小存储单元。处理器填写缓存行时会加载整个缓存行,现代cpu需要执行击败第cpu指令 - 原子操作
不可中断的一个或一系列操作 - 缓存行填充
当cpu识别到从内存中读取操作数是可缓存的,cpu读取整个高速缓存行到适当的缓存 - 缓存命中
如果进行高速缓存行填充操作的内存位置仍然是下次cpu访问的地址时,cpu直接从缓存中读操作数 - 写命中
当cpu要将操作数写回内存时,先检查这个数据是否已经存在于缓存行中,如果存在一个有效的缓存行,则cpu将这个操作数写回到缓存行,而不是写回内存 - 写缺失
一个有效的缓存行被写入到不存在的内存区域
Lock前缀指令
对volatile修饰的共享变量进行写操作时,jvm会加上一条Lock前缀指令
Lock前缀的指令在多核处理器下会引发两件事:
- 将当前处理器缓存行的数据写回到系统内存
- 这个写回内存的操作会使在其他cpu里缓存了该内存地址的数据无效
为了提高处理速度,cpu不跟内存直接通信,而是将内存数据读入高速缓存后在操作,高速缓存的数据何时刷新回内存的时机并不确定。
对声明了volatile的变量进行写操作时,jvm会向cpu发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但在多处理器下,其他处理器的高速缓存还是存的旧值,因此要实现缓存一致性协议,让每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,如果过期,就让缓存行失效,下次要读缓存行中的数据时,重新到内存读取
volatile的实现原则
- Lock前缀指令会引起处理器缓存刷新回内存
不同处理器的处理机制:Lock信号锁总线;Lock信号锁缓存;锁定内存区域的缓存并写回内存 - 一个处理器的缓存刷新回内存会导致其他处理器的缓存无效
处理器使用嗅探技术保证它的内部缓存、系统内存和其他处理器的缓存数据在总线上保持一致。
如有些处理器通过嗅探一个处理器来检测其他处理器打算写内存,而这个地址当前处理共享状态,那么正在嗅探的处理器将使它的缓存行无效
LinkedTransferQueue通过volatile和追加字节的方式优化队列出队入队的性能
synchronized
3种使用形式
- 对于普通同步方法,锁是当前实例对象
- 对于类静态同步方法,锁是当前类的Class对象
- 对于同步代码块,锁是synchronized括号里配置的对象
原理
jvm基于进入和退出Monitor对象来实现方法同步和代码块同步,但实现细节不同
- 代码块同步
使用monitorenter和monitorexit指令实现 - 方法同步
也是使用这两个指令,但细节不详
任何对象都有一个monitor与之关联
锁的升级与对比
Java SE1.6中,锁一共4种状态,级别从低到高依次是无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,随着竞争情况逐渐升级。锁可以升级,不能降级,目的是为了提高获得锁和释放锁的效率
- 偏向锁
大多数情况,锁不存在多线程竞争,且总数由同一个线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁
当一个线程访问同步块并获取锁时,回在对象头和栈帧中的所记录里存储锁偏向的线程ID,以后该线程进出同步块时只需要检测对象头的MarkWord里是否存了当前线程的偏向锁,是的话表示线程已经获得锁;如果不是,则检查MarkWord中偏向锁标识是否为1(偏向锁状态):
- 如果没设置,就使用cas竞争锁
- 如果设置了,则使用cas将对象头的偏向锁指向当前线程
偏向锁的撤销
使用了一种等到竞争出现才释放锁的机制,当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁
- 暂停拥有偏向锁的线程,检查该线程是否或者,如果不处于活动状态,则将对象头设置成无锁状态;如果线程还活着,拥有偏向锁的栈会被执行,遍历偏向对象的所记录,栈中的所记录和对象头的MarkWord要么重新偏向于其他线程,要么恢复到无锁,或者标记对象不适合作为偏向锁
- 轻量级锁
- 加锁
线程在执行同步块之前,在栈帧中创建锁记录,然后把对象头的MarkWord复制到锁记录(Displaced Mark Word)里,再使用cas尝试把对象头中的MarkWord改成指向锁记录的指针。若成功,则线程获得锁,若失败则表示其他线程在竞争锁,当前线程就尝试自旋获取锁。
当自旋获取锁仍然失败时,表示存在其他线程竞争锁(两条或两条以上的线程竞争同一个锁),则轻量级锁会膨胀成重量级锁。 - 解锁
使用cas将Displaced Mark Word替换回对象头。如果成功,表示没有竞争,如果失败,表示锁存在竞争,锁就膨胀成重量级锁
原子操作
- 处理器如何实现原子操作
- 通过总线锁保证原子性
- 通过缓存锁定保证原子性
- Java如何实现原子操作
- 使用循环cas实现原子操作
利用了处理器提供的CMPXCHG指令,循环进行cas操作直到成功为止
3个问题:a)ABA问题,通过版本号解决;b)循环时间开销大;c)只能保证一个共享变量的原子操作,但可以把多个共享变量合并成一个共享变量来操作(如i=2,j=a,合并成ij=2a,用cas操作ij,Java提供了AtomicReference类保证引用对象之间的原子性,可以把多个对象放在一个对象里进行cas操作) - 使用锁机制实现原子操作
锁机制保证了只有获得锁的线程才能操作锁定的内存区域。除了偏向锁,jvm实现锁的方式都用了循环cas,即线程进入同步块时用循环cas获取锁,退出同步块时用循环cas释放锁
Java内存模型
并发编程的两个关键问题:线程间如何通信、线程间如何同步
线程间通信:共享内存、消息传递
共享内存:线程共享程序的公共状态,通过写-读内存中的公共状态进行隐式通信
消息传递:线程之间没有公共状态,必需通过发送消息来显式地进行通信
同步:指程序中用于控制不同线程间操作发生相对顺序的机制
- 共享内存模型里,同步是显式的,必须显式指定某个方法或某段代码需要在线程之间互斥执行
- 消息传递模型里,由于消息的发送必须在消息的接收之前,因此同步是隐式进行的
Java并发采用的是共享内存模型
重排序
顺序一致性
锁的内存语义
final的内存语义
- final域的重排序规则
- 在构造函数内对一个final域的写入,与随后把被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序
- 初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序
写final域的重排序规则保证在对象引用为任意线程可见之前,对象的final域已经被正确初始化过了,普通域没有这个保证
读final域的重排序规则保证在读一个对象的final域之前,一定会先读包含这个final域的对象的引用
happens-before
双重检查锁定与延迟初始化
Java线程基础
线程优先级
线程状态:
- new
- runnable
- blocked,阻塞状态,表示线程阻塞于锁
- waiting,等待状态,表示当前线程需要等待其他线程做出一些特定动作(通知或中断)
- time_waiting:超时等待
- terminated:进程执行完毕
java.lang.Thread类关键字段
- Runnable target
target的run方法会由线程执行 - ThreadGroup group
线程组内部维护了一个Thread数组和ThreadGroup数组,所有ThreadGroup形成一棵树,除了初始的ThreadGroup,其他的ThreadGroup都有一个父
thread可以访问它所在的线程组,但不能访问它所在线程组的父或者其他线程组
Thread的重要方法
- public static native Thread currentThread()
放回当前正在运行的线程的引用 - public static native void yield()
提示调度器当前线程不需要使用cpu了,调度器可以选择忽视 - public static native void sleep(long millis) throws InterruptedException
使当前线程沉睡mills毫秒,但线程不会释放它所持有的monitor - public static void onSpinWait() {}
让当前线程忙等待 - public synchronized void start()
jvm会调用这个thread对象的run方法。start方法的执行结果是两个线程并发运行,一个是当前线程(start方法返回的那个线程),另一个线程去执行run方法
thread不可能启动两次 - public final void stop()
停止线程 - public void interrupt()
调用thread.interrupt()后thread被中断
启动和终止线程
线程间通信
线程应用实例
- 生产者消费者
- 线程池
Java的锁
Lock接口
AQS
同步队列
######## 同步队列
- 同步队列的节点Node
用于保存获取同步状态失败的线程引用、等待状态、前驱节点以及后继节点
同步队列里的节点状态只有CANCELLED,SIGNAL,PROPAGATE,INITIAL四种
CONDITION状态表明节点在等待队列中 - setHead方法
同步队列遵循FIFO,首节点是获取同步状态成功的节点,首节点的线程在释放同步状态时,唤醒其后继节点,后继节点将会在获取同步状态成功时将自己设置为首节点。由于只有一个线程能成功获取到同步状态,所以设置头结点的方法不需要使用cas来保证 - 节点入队方法
线程获取同步状态失败后,被包成Node节点并加入到同步队列中,加入队列的过程要保证线程安全(可能会有多个线程并发入队),同步器基于CAS方法compareAndSetTail设置尾节点
独占式获取与释放状态
######## 独占式同步状态获取与释放
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
先调用自定义同步器实现的tryAcquire方法,它线程安全地获取同步状态,如果获取失败则将线程包成Node节点(独占式的)并加入到同步队列尾部,最后调用acquireQueued方法让该节点以死循环的方式获取同步状态,如果获取不到则阻塞节点中的线程
Node(Node nextWaiter) {
this.nextWaiter = nextWaiter;
//将当前获取同步状态失败的线程包起来
THREAD.set(this, Thread.currentThread());
}
// 通过死循环保证节点的正确添加,只有通过cas成功将节点设置成为尾节点后当前线程才能从此方法返回
private Node addWaiter(Node mode) {
Node node = new Node(mode);
for (;;) {
Node oldTail = tail;
if (oldTail != null) {
// 将node的pre节点指向尾节点
node.setPrevRelaxed(oldTail);
if (compareAndSetTail(oldTail, node)) {
//如果成功通过CAS将节点指向node,则更新oldtail的next引用
oldTail.next = node;
return node;
}
} else {
// 如果队列尾指针指向null,则先初始化同步队列
initializeSyncQueue();
}
}
}
final boolean acquireQueued(final Node node, int arg) {
boolean interrupted = false;
try {
for (;;) {
final Node p = node.predecessor();
//如果node的前驱节点是头节点,则当前节点可以尝试获取同步状态
if (p == head && tryAcquire(arg)) {
//成功获取到同步状态的话将node设置尾头节点,让gc回收p
setHead(node);
p.next = null; // help GC
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node))
interrupted |= parkAndCheckInterrupt();
}
}
}
独占式释放同步状态
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
node.compareAndSetWaitStatus(ws, 0);
Node s = node.next;
//找到node的下一个不是cancelled状态的节点,将其唤醒
if (s == null || s.waitStatus > 0) {
s = null;
for (Node p = tail; p != node && p != null; p = p.prev)
if (p.waitStatus != CANCELLED)
s = p;
}
if (s != null)
//唤醒node的后继第一个非cancelled状态的节点
LockSupport.unpark(s.thread);
}
共享式同步状态获取与释放
共享式地获取同步状态,至少调用一次tryAcquireShared,成功则返回;否则当前线程进队列,可能会反复的阻塞/非阻塞,不停地调用tryAcquireShared直到成功为止
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);
boolean interrupted = false;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
return;
}
}
}
}
}
共享式释放同步状态与独占式不同,独占式因为只有一个线程能成功获取同步状态,在释放时不存在并发问题;而共享式释放同步状态会存在多线程安全问题,需要通过循环+CAS来保证
独占式超时获取同步状态
通过调用同步器的doAcquireNanos方法可以超时获取同步状态,成功获取返回true,否则返回false。提供了与传统Java同步操作如synchronized关键字所不具备的特性
自定义同步组件TwinsLock
设计一个工具类,该工具类在同一时刻只能最多有两个线程同时访问,超过两个线程的访问将被阻塞
public class TwinsLock implements Lock {
private final Sync sync = new Sync(2);
private static class Sync extends AbstractQueuedSynchronizer {
public Sync(int cnt) {
if (cnt <= 0){
throw new IllegalArgumentException("参数不能小于等于0");
}
setState(cnt);
}
@Override
public int tryAcquireShared(int reduceCount){
while (true){
int current = getState();
int newCnt = current - reduceCount;
if (newCnt >=0 && compareAndSetState(current, newCnt)){
return newCnt;
}
}
}
@Override
public boolean tryReleaseShared(int returnCnt){
while (true){
int oldCnt = getState();
int newCnt = oldCnt + returnCnt;
if (compareAndSetState(oldCnt, newCnt)){
return true;
}
}
}
}
@Override
public void lock() {
sync.tryAcquireShared(1);
}
@Override
public void unlock() {
sync.tryReleaseShared(1);
}
}
重入锁
用一个继承了AQS的Sync内部类实现了公平锁和非公平锁
可重入的实现是通过每次加锁就让state增1,每次解锁让state减1
公平锁与非公平锁实现的区别只在于当线程自旋获取同步状态时,公平锁版本的线程要判断它在队列中是否有前驱节点,有的话就不能获取同步状态;非公平版本的线程没有这个限制,队列里的所有线程都去竞争锁
读写锁
读写锁的实现
- 读写状态设计
由于要支持可重入,但由只有一个int变量,因此将state分成两部分,state变量的高16位表示读,state变量的低16表示写
写锁实现
######## 写锁的获取与释放
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
// 如果已经有线程获取到了写锁或读锁
if (c != 0) {
// 如果w为0,说明读锁被某些线程获取了,返回false;如果w不为零,说明有线程拿到了写锁,判断持有写锁的线程是不是当前线程
if (w == 0 || current != getExclusiveOwnerThread())
return false;
//走到这里,说明是当前线程持有写锁,只需要让写锁的state部分增1
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
setState(c + acquires);
return true;
}
//如果读锁写锁都没被任何线程持有,尝试用CAS获取写锁
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
//获取写锁后将写锁持有线程设置为当前线程
setExclusiveOwnerThread(current);
return true;
}
读锁实现
######## 读锁的获取和释放
state变量的高16位记录所有线程获取读锁的总次数
为了返回每个线程各自获取读锁的次数,通过ThreadLocal记录线程各自获取读锁的次数
锁降级
指持有写锁的情况下再获取到读锁,随后释放写锁的过程
ReentrantReadWriteLock不支持锁升级(持有读锁、获取写锁,最后释放读锁的过程),也是为了保证可见性
LockSurpport工具
park方法:阻塞当前线程
parkNanos:超时阻塞当前线程
parkUntil
unpark(Thread thread):唤醒处于阻塞状态的线程
Java 5之前线程阻塞(使用synchronized关键字)在某个对象上时,通过线程dump能看到该线程的阻塞对象
Java 5推出的Lock工具却遗漏了这一点,导致dump线程时无法提供阻塞对象的信息
Java 6中LockSupport新增了3个含有阻塞对象的park方法
park(Object blocker)
parkNanos(Object blocker, long nanos)
parkUntil(Object blocker, long deadline)
Condition接口
通过Lock.newCondition()获得信号量
要先获取锁,再调用condition的方法
实现分析
COnditionObject是AQS的内部类,每个Condition对象都包含一个等待队列,该队列是Condition对象实现等待/通知功能的关键
- 等待队列
等待队列也是一个FIFO队列,每个节点包含了一个线程引用,该线程就是在condition上等待的线程。线程调用Condition.await()时,将会释放锁、构造成节点加入等待队列并进入等待状态。这里节点的定义复用了AQS的Node
一个Condition包含一个等待队列,Condition拥有首节点和尾节点。当前线程调用Condition.await()时,以当前线程构造节点,并将节点从队列尾部加入。入队不需要cas操作,因为当前线程定是拥有锁的,保证了线程安全
Object的监视器模型,一个对象拥有一个同步队列和一个等待队列;而AQS拥有一个同步队列和多个等待队列
- 等待
线程调用await方法,相当于从同步队列首节点移动到了等待队列的尾节点
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
//将当前线程包成Node加入到等待队列尾部
Node node = addConditionWaiter();
//释放同步状态
int savedState = fullyRelease(node);
int interruptMode = 0;
//如果当前线程不在同步队列里就一直循环
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
- 通知
线程持有锁后调用Condition.signal()方法,此方法将等待队列首节点移动到同步队列尾部,并唤醒节点中的线程
Java并发容器和框架
ConcurrentHashMap
引入ConcurrentHashMap的原因
HashMap的put操作在并发插入的时候会丢数据;
HashMap扩容会出现死循环;
HashTable使用synchronized保证线程安全,但效率太低
ConcurrentHashMap的锁分段技术可以有效提升并发访问率
老版本实现:锁分段
Java 12:synchronized + CAS
ConcurrentLinkedQueue
Java的阻塞队列
Fork/Join框架
- 工作窃取算法
指某个线程从其他队列里窃取任务来执行
通常使用双端队列,被窃取任务的队列从队列头部取任务,窃取任务的队列从队列尾部拿任务执行 - 框架设计
- 分割任务
用fork类把大任务不停地分割成子任务直到分割出的子任务足够小 - 执行任务并合并结果
分割的子任务放在双端队列里,几个启动线程分别从双端队列里获取任务执行,子任务执行结果同一放在一个队列里,启动一个线程从队列里拿数据,然后合并数据
原子类
Java的并发工具类
等待多线程完成的CountDownLatch
同步屏障CyclicBarrier
控制并发线程数的Semaphore
线程间交换数据的Exchanger
Java线程池
实现原理
- 线程池主要处理流程
- 判断核心线程池是否已满,没有的话创建新的工作线程执行任务,否则将任务放入工作队列
- 判断最大线程池是否已满,没有的话创建新的工作线程执行任务
- 按照策略处理无法执行的任务
创建新线程需要获取全局锁
将多于核心线程数的任务放入队列中不需要获取quan’ju’suo
主要参数
- corePoolSize,线程池的基本大小
- runnableTaskQueue,任务队列
- maximumPoolSize,线程池最大数量
- ThreadFactory,用于设置创建线程的工厂
- RejectedExecutionHandler,任务饱和时的拒绝策略
- keepAliveTime,线程活动保持时间(多于核心线程数的线程空闲下来后能存活的最大时间)
Executor框架
异常体系
来源:https://blog.csdn.net/xipitu/article/details/100972399