























Thread类Runnable接口Callable<T>接口, 泛型类型与重写的call()方法返回值类型相同Runnable和Callable不同:
Runnable的run方法没有返回值,Callable的call方法有返回值,搭配FutureTask可以获取异步执行的结果Callable的call方法可以抛出异常,Runnable的run方法不能向上抛异常
同: 让当前线程暂时放弃 CPU 使用权, 进入阻塞状态
异:
方法归属不同
sleep(long)是Thread的静态方法wait(), wait(long)是Object的成员方法, 每个对象都有醒来时机不同
sleep(long)和wait(long)都在等待对应毫秒后醒来wait(long)和wait()可以被notify()唤醒, wait()不被唤醒就一直等下去锁特性不同
wait()方法调用必须先获取 wait 对象的锁, sleep()没有这个限制wait方法执行后会释放对象锁, 允许其他线程获取该对象锁sleep如果在synchronized代码块中执行则不会释放对象锁while(flag){...}, flag=false时退出stop()方法 (强行打断, 不推荐)interrupt()方法, 阻塞线程被打断会抛异常, 线程打断会改变线程打断状态. isInterrupted()被打断后为true, 正常线程可以通过判断这个来决定是否退出.底层由 monitor 实现, 线程获得锁需要使用对象关联 monitor, monitor 有三个属性
wait()方法(处于 waiting 状态)的线程monitor 由 jvm 提供,涉及到用户态和内核态的切换、进程的上下文切换,是重量级锁,成本较高,性能较低。
HotSpot 虚拟机中,对象在内存中存储的布局由三部分:

对象上重量级锁后,对象头的 Mark Word 中就被设置指向 monitor 对象的指针
在很多的情况下,在 Java 程序运行时,同步块中的代码都是不存在竞争的,不同的线程交替的执行同步块中的代码。这种情况下,用重量级锁是没必要的。因此 JVM 引入了轻量级锁的概念。

加锁过程
解锁过程
偏向锁:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 中,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有。
加锁过程
解锁过程
偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁状态的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的撤销需要等待全局安全点(即没有字节码正在执行),它会暂停拥有偏向锁的线程,撤销后偏向锁恢复到未锁定状态或轻量级锁状态。
java 内存模型,定义共享内存中多线程程序读写操作的行为规范。内存分为两块,私有线程的工作区域(工作内存)和所有线程的共享区域(主内存)。线程之间相互隔离,交互需要通过主内存。

CAS: 乐观锁思想,在无锁的情况下保证线程操作共享数据的原子性。
底层依赖Unsafe类直接调用操作系统底层 CAS 指令
保证线程之间的可见性: 防止编译器优化(JIT), 让一个线程对共享变量的修改对另一个线程可见
禁止指令重排序: 在读写共享变量时加入不同的屏障, 阻止其他读写操作越过屏障, 从而阻止重排序.

写变量让volatile修饰的变量在代码最后位置
读变量让volatile修饰的变量在代码最前位置
AQS: AbstractQueuedSynchronizer, 是构建锁或者其他同步组件的基础框架
| synchronized | AQS |
|---|---|
| 关键字, c++实现 | java 实现 |
| 悲观锁, 自动释放锁 | 悲观锁, 手动开启和关闭 |
| 锁竞争激烈时是重量级锁, 性能差 | 锁竞争激烈时有多种解决方案 😅 |
常见实现类:

非公平锁: 新的线程和队列中的线程共同抢占资源
公平锁: 新线程到队列中等待, 只让队列的 head 线程获取锁
使用 CAS+AQS 实现, 对比synchronized的特点:
lockInterruptibly()方法是中断锁tryLock()可以设置超时时间

死锁条件:
1.7: 分段的数组+链表, segment 分段锁使用 ReentrantLock, HashEntry 使用 CAS

1.8: 数组+红黑树+链表, CAS+synchronized 保证并发安全

并发特性:

假设:服务器 CPU 核心数为 $N$
IO 密集型: $2N+1$
CPU 密集型: $N+1$
FixedThreadPool: 固定线程数线程池. 核心线程和最大线程数相同, 阻塞队列为 LinkedBlockingQueue, 最大容量为Integer.MAX_VALUE
SingleThreadPool: 单线程化线程池, 使用唯一线程执行任务, 保证所有任务按 FIFO 执行. 核心线程数和最大线程数都为 1, 阻塞队列为 LinkedBlockingQueue, 最大容量为Integer.MAX_VALUE
CachedThreadPool: 可缓存线程池. 核心线程数为 0, 最大线程数为Integer.MAX_VALUE, 阻塞队列为 SynchronousQueue.
ScheduledThreadPool: 可以执行延迟任务的线程池, 支持定时和周期性任务执行. 最大线程数为Integer.MAX_VALUE, 存活时间为 0, 阻塞队列为 DelayedWorkQueue.
FixedThreadPool 和 SingleThreadExecutor 因为阻塞队列没有限制, 可能会导致 OOM; CachedThreadPool 因为最大线程数没有限制, 也可能导致 OOM.
所以推荐使用 ThreadPoolExecutor 自己填各个参数来创建线程池.
@Async异步调用, 可以自定义线程池
程序计数器:线程私有的,内部保存的字节码的行号。用于记录正在执行的字节码指令的地址。
java 堆:线程共享区域,保存对象实例、数组等,当堆中没有内存空间可分配给实例,也无法再扩展时,则 OOM。

栈内存存储局部变量和方法调用,线程私有
堆内存存储 java 对象和数组,线程共有
方法区(元空间)存储类的信息和运行时常量池,是各个线程共享的内存区域
不属于 JVM 中的内存结构,不由 JVM 管理,是虚拟机的系统内存。常见于 NIO 操作,用于数据缓冲区。分配回收成本较高,但读写性能高。

类加载器:将字节码加载到 JVM 中

双亲委派机制:加载一个类时,先委托上一级加载,若上级加载器也有上级,则继续向上委托,如果该类委托上级没有被加载,则子加载器尝试加载该类。
双亲委派机制好处:

引用计数法:每个对象都有一个引用计数器,当对象被引用时,计数器加一,当引用失效时,计数器减一,当计数器为 0 时,对象被回收。循环引用无法解决。
可达性分析:从 GC Roots 出发,向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的。
GC Roots 包括:
标记清除方法
优点:标记和清除速度较快
缺点:碎片化较为严重,内存不连贯
标记整理算法
先标记,将存活对象都向内存的另一端移动,然后清理边界外的垃圾。
优点:无内存碎片
缺点:需要移动对象,效率低
老年代使用
复制算法
复制一块相同的区域,在一个区域进行标记后,将存货的对象复制到另一块区域,然后原区域全部清空。
优点:垃圾对象较多时效率较高,清理后内存无碎片
缺点:分配两块内存空间,同一时刻只能用一块,内存使用率较低
新生代使用
在堆中:新生代 : 老年代 = 1 : 2
在新生代中:Eden : from : to = 8 : 1 : 1
工作机制:
STW: stop the world,暂停所有线程,等待垃圾回收完成。
串行垃圾收集器
使用单线程进行垃圾回收,垃圾回收时只有一个线程工作,java 应用中所有线程都要暂停(STW),堆内存较小,适合个人电脑。
并行垃圾收集器
JDK8 默认,垃圾回收时多个线程工作,java 应用中所有线程都要暂停(STW)
CMS 垃圾收集器
CMS(Concurrent Mark Sweep)并发的,用于老年代,标记清除算法,进行垃圾回收时应用仍然能正常运行

G1 垃圾收集器
G1(Garbage First)用于新生代和老年代,复制算法,JDK9 后默认

划分为多个区域,每个区域都可以充当为 eden, survivor, old, humongous(大对象)
并发失败(回收速度赶不上创建新对象速度)时触发 Full GC
新生代回收
并发标记
混合收集
复制完成后进入下一轮回收
强引用:只有所有 GC Roots 对象都不通过强引用引用该对象,该对象才能被垃圾回收
软引用:第一次垃圾回收后内存依旧不足,触发第二次垃圾回收时会被回收
弱引用:垃圾回收时无论是否内存充足都会就被回收
虚引用:必须配合引用队列使用,被引用对象回收时会将虚引用入队,由 Reference Handler 线程调用相关方法释放直接内存
静态代理是指在编译时就已经确定了代理类和被代理类的关系。代理类和被代理类都实现了同一个接口,代理类在调用被代理类的方法前后可以添加额外的逻辑。
实现步骤
// 定义一个接口
interface Service {
void doSomething();
}
// 被代理类
class RealService implements Service {
@Override
public void doSomething() {
System.out.println("执行 RealService 的 doSomething 方法");
}
}
// 代理类
class ServiceProxy implements Service {
private Service realService;
public ServiceProxy(Service realService) {
this.realService = realService;
}
@Override
public void doSomething() {
// 调用前的逻辑
System.out.println("调用前的逻辑");
// 调用被代理类的方法
realService.doSomething();
// 调用后的逻辑
System.out.println("调用后的逻辑");
}
}
// 测试类
public class StaticProxyDemo {
public static void main(String[] args) {
Service realService = new RealService();
Service proxyService = new ServiceProxy(realService);
proxyService.doSomething();
}
}
JDK 动态代理:基于 Java 的反射机制实现。它要求目标对象必须实现至少一个接口。通过 java.lang.reflect.Proxy 类和 java.lang.reflect.InvocationHandler 接口,在运行时动态生成一个实现了与目标对象相同接口的代理对象。当调用代理对象的方法时,实际会调用到 InvocationHandler 接口中的 invoke 方法,在该方法中可以添加额外的逻辑,然后调用目标对象的实际方法。
CGLIB 动态代理:基于字节码操作技术实现。它通过字节码生成库(如 ASM)在运行时对目标类生成一个子类,并覆盖其中非 final 和非 private 的方法来创建代理对象。代理对象在调用方法时会触发拦截器,通过拦截器可以在方法执行前后进行逻辑的处理。
JDK 动态代理:在创建代理对象阶段相对较快,因为它只需基于接口信息利用反射生成简单代理类结构。但在方法调用时,由于每次都要经过反射查找方法等操作,性能开销较大。
CGLIB 动态代理:在创建代理对象时需要通过复杂的字节码生成技术创建子类,耗时较长。但在方法调用时,由于方法已在字节码层面重写优化,若频繁调用代理方法,CGLIB 在性能上更具优势。
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。