目录
1.Thread抽象类与Runable接口
都是用于创建多线程。
Runnable接口是一个函数式接口,它只包含一个run()方法,用于定义线程的执行逻辑。通过实现Runnable接口,并将其传递给Thread类的构造函数,可以创建一个新的线程。
Thread类是Java中表示线程的一个抽象类,它实现了Runnable接口,并提供了一些其他方法来控制线程的状态和行为。通过继承Thread类并重写其run()方法,可以创建一个新的线程。
public class MyRunnable implements Runnable {
public void run() {
// 线程执行的代码
}
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
}
}
public class MyThread extends Thread {
public void run() {
// 线程执行的代码
}
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
}
}
2.并发与并行
线程由cpu调度执行,cpu同时处理线程(并行)的数量是有限的,cpu会轮询执行所有线程,由于cpu切换速度快,给我们的感觉像是“同时”执行,但实际上是交替执行的,这就是并发。
并行是多个线程 同时 执行。
并发是多个线程 交替 执行。
3.线程任务周期
NEW:新建状态,线程还未启动,可以通过start()方法启动。
RUNABLE:可执行状态,在新建状态 线程调用了start()方法
在阻塞状态 线程获得锁
在无限等待状态 被其他线程notify唤醒后 获得锁
在计时等待状态
时间到 获得锁
被其他线程notify唤醒后 获得锁
BLOCKED:阻塞状态,在可执行状态 未获得锁
在无线等待状态 被其他线程notify唤醒后 未获得锁
在计时等待状态
时间到 未获得锁
被其他线程notify唤醒后 未获得锁
TIMED WAITING:计时等待状态,在可执行状态,调用了wait(毫秒)和sleep(毫秒)方法
WAITING:无限等待状态,在可执行状态,调用wait()方法
TERMINATED:终止状态,在可执行状态,执行完毕或出现异常
4.线程池
没有线程池之前,每发起一次请求,就创建一个新的线程来处理,当请求量非常大的时候,创建线程、销毁线程的资源开销就会很大。所以线程池应运而生。
总结:线程池是复用线程的技术,用来减小资源开销。
在JDK5中提供了代表线程池的接口ExecutorService,使用实现类 ThreadPoolExecutor 来创建线程池。
ExecutorService pool = new ThreadPoolExecutor(
3, //核心线程数有3个
5, //最大线程数有5个。 临时线程数=最大线程数-核心线程数=5-3=2
8, //临时线程存活的时间8秒。 意思是临时线程8秒没有任务执行,就会被销毁掉。
TimeUnit.SECONDS,//时间单位(秒)
new ArrayBlockingQueue<>(4), //任务阻塞队列,没有来得及执行的任务在,任务队列中等待
Executors.defaultThreadFactory(), //用于创建线程的工厂对象
new ThreadPoolExecutor.CallerRunsPolicy() //拒绝策略
);
线程池可执行 Runable(无返回值) 和 Callable(有返回值)任务
5.工具类
hutool 的 ThreadUtil
6.锁
多线程伴随着线程安全问题,为了解决此问题,就要用到锁。
线程操作某个共享资源之前,先对资源加一层锁,保证操作期间没有其他线程访问资源,当操作完成后,再释放锁。
以java来说,大体分为 悲观锁 和 乐观锁,其他基本都是这两种锁的实现。
悲观锁:认为共享资源都是不安全的,随时会被其他线程操作、更改,在访问资源前加锁,访问完再解锁。
乐观锁:认为共享资源是安全的,通过CAS机制来确保线程安全。
6.1 悲观锁
synchronized
基于java同步器AQS的各种实现类
6.1.1 synchronized
Java中的关键字,底层由Java虚拟机实现同步机制。通过两条监听器指令:MONITORENTER(进入) 和MONITOREXIT(退出)来实现同步效果(在class文件中可以看到指令)。
synchronized有三种使用方式
修饰静态方法:锁住的是该类,类下所有属性均被锁住。
修饰实例方法:锁住的是当前对象,其他对象不受影响。
修饰代码块:静态代码块----锁住的是整个类
实例代码块----锁住的是当前实例对象。
6.1.2 基于AQS的实现类
AQS全称(Abstract Queued Synchronizer),基于Java程序实现的一种 抽象队列同步器框架。
AQS定义了一个volatile int state 来控制是否同步,提供了一个unsafe实现的原子方法来更新state(更新锁状态,是否上锁)。
基于AQS,Java提供了一些同步类,在java.util.concurrent包下
ReentrantLock:可重入锁,AQS体系下用的最多的锁。
ReentrantReadWriteLock:基于ReenTrantLock的读写锁,读锁之间共享资源,读写、写写之间互斥资源,相较于普通互斥锁,并发能力好一点,但要考虑切入点。
StampedLock:基于读写锁优化,对读锁更加优化了一层,更加复杂,用的不多。
Semaphore:信号量,可用于限流。
CountDownLatch:可用于计数,一般用于多线程环境下需要执行固定次数的地方。
6.2 乐观锁
认为资源都是安全的,不需要对资源加锁,怎么保证线程安全?
CAS(Compare And Set)机制。
可以用volatile + CAS 来实现乐观锁。
6.2.1 volatile
Java内置关键字,只能修饰变量,用来保证变量在内存中的可见性、有序性。
可见性:volatile修饰的变量被修改后,在内存中是立即可见的。举个例子,有两个线程A、B。有一个被vilatile修饰的变量Y,当A线程修改Y后,线程B能够立即感知,并且将Y的最新值读取到自己的内存空间。
有序性:单线程环境下,代码是一行一行运行的。
但在多线程环境下,Java编译器在编译的时候会进行指令重排(为了提高性能),这就会出现安全问题。volatile可以做到禁止指令重排。
6.2.2 CAS(Compare And Set)
Java内存模型
Java内存分为 主内存 和 工作内存 。线程在操作资源时,将用到的资源复制到自己的工作内存中,完成操作后,再把资源同步回 主内存。
CAS可以理解为先比较后赋值。
举例:两个线程A、B,操作共享资源Y。根据Java内存模型,A和B会分别复制一份副本到自己的工作内存中:Ya1和Yb1。完成操作后,将Ya1和Yb1再同步会主内存中。
然而,在CAS机制下,会复制两份副本,Ya1,Ya2,Yb1,Yb2。当线程A修改完Ya1,将Ya1同步到主内存之前,会先拿Ya2与主内存中的资源Y对比,
如果一致,立即同步
如果不一致,会重复上面操作,重新从主内存中获取资源,修改,同步。
CAS在Java底层是一个原子操作,可以保证数据同步回主内存是安全的。
这点可以参考sun.misc.Unsafe类。这个类提供了原生的CAS能力,直接调native方法于系统底层交互
6.2.3Valotile+CAS
上面分别介绍了Valotile和CAS。
Valotile是为了保证资源的可见性,任何一个线程修改了资源后。其他线程都能立刻感知并重新获取资源。
CAS是保证资源的安全性,由于是原子操作,任何一个线程在修改资源时,都是一体的。其他线程是不可操作的。
所以Valotile的特性+CAS的机制就组成了一个完美的乐观锁,既保证了线程安全,对性能影响也不大。
Valotile的特性+CAS的机制这种组合也可以叫做:Valotile+原子操作
6.2.4 Java中的乐观锁有哪些
ava没有提供可直接使用的乐观锁,不过内置了一些由底层由乐观锁实现的类。例如:java.util.concurrent.atomic下的几个原子类。
7.偏向级锁、轻量级锁、重量级锁
首先我们确认一点,这三种锁 只针对synchronized。
虽然synchronzied是悲观锁,但是在实际应用中,考虑到性能和资源,会使用乐观锁的技术,比如CAS操作。
任意一个Java对象都可以作为锁,锁信息被存储在对象头中的Mark Word 字段中。
Mark Word默认存储对象的Hash Code ,分代年龄和锁标记位。
Java 中锁主要存在四种状态:无锁态、偏向锁、轻量级锁、重量级锁。
随着竞争的激烈,锁只能升级,不能降级。
7.1 无锁态
无锁态 有点像 乐观锁的实现,通过volatile+CAS机制实现同步。
7.2 偏向锁
为了解决单线程 以及 少量多线程 场景下的性能问题引入的锁机制。
由于反复获取锁会消耗资源,降低性能,于是出现了偏向级锁。
偏向锁不需要操作系统的介入。 JVM使用CAS操作把线程ID记录到对象的Mark Word当中,并修改标识位。当前线程就拥有了这把锁。当 此线程 再次执行到这个Synchronized的时候,JVM通过锁对象的Mark Word 判断:当前线程ID还存在,还持有这个锁,就可以直接执行,不需要重新获取锁。
如果在运行中,遇到了其他线程抢占锁,当前线程会被挂起,JVM会消除当前偏向锁,升级到轻量级锁。
总结一点:偏向锁就是为了消除资源无竞争情况下的同步原语,进一步提高程序的运行性能。
7.3 轻量级锁
偏向锁 升级后 会成为轻量级锁。
首先,JVM将锁对象的Mark Word 恢复为无锁状态,在当前两线程的栈帧中各自分配一个空间,叫做 Lock Record,把 Mark Word各自复制一份,官方称为 Displaced Lock Record。
然后一个线程尝试使用CAS操作将Mark word 替换为 指向自己的Lock record的指针。如果替换成功,则当前线程获得锁,如果失败,则当前线程自旋重新获取锁。
当自旋(一般为10次)获取锁任然失败时,表示存在多个线程同时竞争锁,则轻量级锁会膨胀成重量级锁。
7.4 重量级锁
上面说到,当多个线程同时竞争锁,除了锁的拥有者,其他线程都会 自旋 等待获取锁,这会消耗大量cpu资源,所以要将轻量级锁升级为重量级锁。
重量级锁需要操作系统介入。
依赖操作系统底层的Muptex Lock。
JVM会创建一个monitor对象,把这个对象的地址更新到Mark Word 的数据字段。通过操作monitor来实现同步。(之前说过的两条指令MONITORENTOR 和 MONITOREXIT)。
当一个线程获取了该锁后,其余线程想要获取锁,必须等到这个线程释放锁后才可能获取到,没有获取到锁的线程,就进入了阻塞状态。