Java并发
线程创建
创建线程的方式
- Runnable或Callable接口。新建类时实现Runnable接口,然后在Thread类的构造函数中传入MyRunnable的实例对象,最后执行start()方法。
- 继承Thread类,重写
run()
- lambda精简代码:Runnable接口中只有一个抽象化方法且被@FunctionalInterface修饰,这种接口就可以用lambda简化。
lambda是一个匿名函数的简写形式。
(参数列表)->{代码};
class MyThread extends Thread {
@Override
public void run() {
System.out.println("My thread is running!");
}
}
class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("My Runnable is running!");
}
}
public class Solution {
public static void main(String[] args) {
Thread t = new MyThread();
t.start();
Runnable r1 = new MyRunnable();
Thread t1 = new Thread(r1,tt1);//线程t1创建新的线程tt1
t1.start();
Runnable r1 = new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println("Running");
}
};
Runnable r2 = () -> {System.out.println("Running");};
Thread t2 = new Thread(r1);
t2.start();
}
}
Runnable和Callable区别:
Callable规定重写的方法是call(),Runnable规定的是run()
callable的任务执行后可返回值,Runnable的任务不能返回值
call()可以抛出异常,run()不行
运行Callable任务可以拿到一个Future对象,表示异步计算的结果,提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况。
start()和run()
调用start()
会执行线程的相应准备工作,然后自动执行run()
的内容,并不会等待run()
的返回,而是直接继续往下运行。也就是说JVM会另起一条线程执行run()
方法,起到多线程的效果。
直接运行run()
,会将其当作main线程下的普通方法执行,并不会在某个线程中执行它,还是在主线程里执行。
join()
因为start()
不等待run()
的返回,所以如果run()
中有对于数据的操作而没有及时返回的话,start()
的取值可能并不正确。t.join()
:主线程等待t线程运行结束。
线程状态[[进程与线程1#线程的状态转换|相关]]
新建状态:Thread t = new MyThread();
就绪状态:t.start()
运行状态:CPU开始调度就绪状态的线程
阻塞状态:运行状态的线程执行wait()
、线程在获取synchronized同步锁失败、线程的sleep()
或I/O阻塞。
死亡状态:线程执行完毕或因异常退出了run()。
shutdown()和shutdownNow()的区别
shutdown():关闭线程池,线程池状态为SHUTDOWN,不会再接受新任务,但是队列里的任务得执行完毕。
shutdownNow():关闭线程池,线程池状态为STOP,终止当前正在运行的任务,并停止处理排队的任务并返回正在等待执行的List。
线程阻塞和死亡
sleep()和wait(),yield()
sleep()
:Thread类的静态方法。当前进程将睡眠n毫秒,线程进入阻塞状态。时间到了解除阻塞,进入可运行状态,等待CPU的调度。
wait()
:Object方法,必须与synchronized关键字一起使用,线程进入阻塞状态。当notify或notifyall被调用后,解除阻塞。只有重新占用互斥锁之后才会进入可运行状态。线程不会自动苏醒,需要别的线程调用同一个对象上的notify方法。
yield()
:暂停当前正在执行的线程对象,让其他有相同优先级的线程执行。只能保证当前线程放弃CPU占用而不能保证其他线程一定能占用CPU。
三种阻塞情况
- 等待阻塞:运行状态的线程执行
wait()
方法后,JVM将线程放入等待序列。 - 同步阻塞:运行状态的线程在获取对象的同步锁时,若该同步锁被其他线程占用,则JVM将其放入锁池中。
- 其他阻塞:运行状态的线程执行
Thread.sleep()
或Thread.join()
方法,或发出I/O请求时,JVM会将该线程设置为阻塞状态。
死亡的三种方式
- 正常结束:线程执行完
run()
或call()
。 - 异常结束:线程抛出一个未捕获的Exception或Error
- 调用
stop()
:不推荐使用,易导致死锁。
线程安全
对临界资源的竞争。
synchronized
阻塞方式,即用对象锁保证了临界区内代码的原子性。采用互斥的方式让同一时刻至多只有一个线程能持有对象锁,其他线程想获得该对象锁时就会被阻塞。必须等执行完synchronized的代码,获得对象锁的线程才会释放锁,并唤醒被阻塞的线程。synchronized会创建一个内存屏障,内存屏障指令保证了所有CPU操作结果都会直接刷到主存中,从而保证操作的内存可见性。
synchronized用法
- 修饰方法:
成员方法:作用于当前对象实例,进入同步代码前要获得当前对象实例的锁
静态方法:作用于当前类,进入同步代码前要获得当前类对象的锁。 - 修饰代码块:作用于当前对象实例,指定加锁对象,对给定对象加锁。
//面向过程
public class Solution {
static int counter = 0;
static Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (obj) {
counter++;
}
}
}, "t3");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (obj) {
counter--;
}
}
}, "t4");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("counter:" + counter);
}
}
//面向对象
class Room {
private int counter = 0;
public void increment() {
synchronized (this) {
counter++;
}
}
//或将关键词写在方法上
public synchronized void decrement() {
counter--;
}
public int getCounter() {
synchronized (this) {
return counter;
}
}
}
//main函数
public static void main(String[] args) throws InterruptedException {
Room room = new Room();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
room.increment();
}
}, "t3");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
room.decrement();
}
}, "t4");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("counter:" + room.getCounter());
}
synchronized和[[JVM#JMM|volatile]] 的区别
volatile仅能使用在变量级别,synchronized可以修饰变量、方法和类。
volatile仅能保证变量的修改可见性,不能保证原子性。而synchronized可以保证变量修改可见性和原子性
volatile不会造成线程的阻塞,本质是告诉JVM当前变量在工作内存中的值是不确定的,需要从主存中读取。synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞。
volatile 标记的变量不会被编译器优化;synchronized 标记的变量可以被编译器优化。
synchronized底层实现原理
Monitor(锁/[[进程与线程2#管程|管程]])。synchronized 同步代码块的实现是对应字节码中的 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor的持有权。(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因)
其内部包含一个计数器,当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的再执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止
synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取而代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
轻量级锁与重量级锁
如果一个对象虽然有多个线程访问,但多线程访问的时间是错开的,就可以使用轻量级锁来优化。语法仍然是synchronized,即先调用轻量级锁,如果失败再调用重量级锁。
- 创建锁记录(lock record)对象,每个线程的栈帧包含一个锁记录结构,内部存储锁定对象的MarkWord。
- 让锁记录的object reference指向所对象,并尝试用cas替换object的markword,将markword值存入锁记录。
- 如果cas替换成功,对象头存储所记录地址和状态00,表示由该线程给对象加锁。
- 如果cas失败:
- 如果其他线程已持有该object的轻量级锁,表示有竞争,进入锁膨胀。
- 如果自己执行了synchronized锁重入,那么再添加一条lock record作为重入的计数。
- 当退出synchronized代码块,如果有取值为null的锁记录,表示有重入,这是重置锁记录,表示重入计数减一。如果不为null,使用cas将markword的值恢复给对象头,成功则解锁成功,失败则说明轻量级锁进行了锁膨胀或升级成重量级锁。
CAS锁
Compare and Swap,比较并交换,是一条CPU同步原语,是一种硬件对并发的支持,用于管理对共享数据的并发访问。
当且仅当需要读写的内存值V等于旧的预期值A时,CAS通过原子方式用新值B来更新V,否则不会执行任何操作。
CAS的缺陷:
- ABA问题:只能判断共享变量是否一致,但无法获知共享变量是否被修改过。假设初始条件是A,修改数据时发现是A就会进行修改。但是看到的虽然是A,中间可能发生了A变为B再变为A的情况,数据即使成功修改,也可能有问题。
- 循环时间长开销:自旋CAS,如果一直循环执行,会给CPU造成风长达的执行开销。
- 只能保证一个变量的原子操作:如果对多个变量操作,CAS无法保证操作的原子性。
偏向锁
轻量级锁在没有竞争时,每次重入仍然需要执行cas操作,产生锁记录。因此引入偏向锁优化,只有第一次调用synchronized使用cas,减少同一线程获取锁的代价。
如果开启偏向锁(默认开启),markword后三位为101,此时thread、epoch、age都为0。偏向锁默认延迟,不会在程序启动时立即生效,除非加入VM参数-xx:BiasedLockingStartupDelay=0
禁用延迟。解锁后该锁偏向于该线程,所以并不会使用cas换回原来的值,而是保持不变。
VM参数-xx:-UseBiasedLocking
禁用偏向锁。或者使用代码对象.hashCode();
撤销对象的偏向状态。或多个线程访问同一个锁,撤销偏向锁后会将偏向锁升级为轻量级锁。即markword后三位从101->000->001。
批量重偏向
如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程T1的对象仍有机会重新偏向T2,重偏向会重置对象的Thread ID。
当撤销偏向锁阈值超过20次后,jvm会给这些对象加锁时重新偏向至加锁线程。
当撤销偏向锁阈值超过40次后,JVM会认为根本不该偏向,所以整个类的所有对象都会变为不可偏向,新建的对象也是不可偏向的。
锁膨胀
线程1加轻量级锁失败,进入锁膨胀流程:
- 为object对象申请monitor锁,让object指向重量级锁地址。
- 自己进入monitor的entrylist blocked中
当线程0解锁时,使用cas将markword值恢复给对象头,失败。进入重量级解锁流程,按照monitor地址找到monitor对象,设置owner=null,唤醒entrylist中blocked线程
膨胀方向:偏向锁->轻量级锁->重量级锁。且膨胀方向不可逆。
自旋优化
重量级锁竞争时,如果当前线程自旋成功,即这时持锁线程已经释放了锁,当前线程就可以避免阻塞。(自旋会占用CPU时间,多核CPU才有意义)
体现了synchronized是非公平锁:
- 当持有锁线程释放锁时,a)先将锁的持有者owner赋null,b)然后唤醒等待链表中的一个线程。如果有其他线程刚好在尝试获取锁(比如自旋),则可以马上获得锁。(来的早不如来的巧)
- 当线程尝试获取锁失败进入阻塞时,放入链表的顺序和最终被唤醒的顺序是不一致的。
锁消除优化
在JIT编译时,丢运行上下文进行扫描,去除不可额能存在竞争的锁,提高运行效率,因为加锁过程很耗时耗力。锁消除功能默认打开,如果需要关闭,需要通过java参数-XX:-EliminateLocks
线程池
使用线程池的好处:
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗
- 提高响应速度。
- 提高线程的可管理性。使用线程池可以进行统一的分配、调优和监控。
execute()和submit()的区别
execute()
用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否
submit()
用于提交需要返回值的任务,线程池会返回一个future类型的对象,通过该对象可以判断任务是否执行成功,同时也可以通过该对象来获取返回值。
线程池核心参数
- corePoolSize:核心线程大小,正在运行的线程个数
- maximumPoolSize:线程池最大线程数=核心线程+救急线程
- keepAliveTime:救急线程的心跳时间,如果救急线程在此时间内没有运行任务,该线程消亡。
- unit:时间单位——针对救急线程
- workQueue:阻塞队列。
- handler:饱和策略。当线程池满了后需要执行的策略。通过RejectedExecutionHandler接口实现。
- AbortPolicy(默认):让调用者抛出RejectedExecutionException异常
- CallerRunsPolicy:让调用者运行任务
- DiscardPolicy:放弃本次任务
- DiscardOldestPolicy:放弃队列中最早的任务,本任务取而代之
- ...
- Dubbo:抛出异常前会记录日志,并dump线程栈的信息
- Netty:创建一个新线程来执行任务
- ActiveMQ:带超时等待尝试放入队列
- PinPoint:使用一个拒绝策略链,逐一尝试策略链中每种拒绝策略。
- ThreadFactory:线程工厂,可以为线程创建时起个名字
线程池执行任务流程
- 线程池执行execute/submit方法向线程池中添加任务,当任务小于核心线程数时,线程池可以创建新的线程
- 当任务大于核心线程时,加入阻塞队列中
- 如果阻塞队列已满,需要通过比较是否小于线程池最大线程数,当小于则创建救急线程,当大于则执行饱和策略。
一些工厂线程池
- newFixedThreadPool:创建一个指定工作线程数量的线程池。只有核心线程没有救急线程。阻塞队列是无界的,可以放任意数量的任务。
- newCachedThreadPool:核心线程数为0,救急线程的空闲生存时间是60s。意味着创建的线程全部都可以回收,且无限创建。队列采用了SynchronousQueue,特点是没有容量,没有线程取则放不进去(一手交钱,一手交货)
- newSingleThreadPool:希望多任务排队执行,线程固定数为1。
- 单线程与单线程池的区别:如果任务执行出现异常,单线程则直接退出了,而线程池还会新建一个线程,保证池的正常工作。
- 单线程池和固定大小为1的线程池的区别:固定大小线程池初始为1,以后还可以修改,而单线程池线程个数始终为1,不能修改。
源码中线程池如何复用线程
源码中ThreadPoolExecutor中有一个内置对象Worker,每个worker都是一个线程,worker线程数量和参数有关,每个worker会从阻塞队列中取数据,通过置换worker中Runnable对象,运行其run方法起到线程置换的效果,这样做的好处是避免多线程频繁线程切换,提高程序运行性能。
AQS
AbstractQueueSynchronizer,是阻塞式锁和相关的同步器工具的框架,定义了锁的实现机制,并开放出扩展的地方,让子类去实现。
用state属性表示资源的状态(独占或共享),子类需要定义如何维护这个状态,控制如何获取和释放锁。
提供了基于FIFO的等待队列+条件队列,等待队列类似于Monitor的EntryList,管理着获取不到锁的线程的排队和释放。条件队列是在一定场景下,对同步队列的补充,实现等待、唤醒机制,类似于Monitor的WaitSet。