Java多线程并发
-
Java线程实现/创建方式
线程实现方式包括继承Thread类与实现Runnable接口
线程启动的唯一方式是Thread::start,start()是一个本地方法,执行之后将启动一个新线程并等待CPU调度运行run()方法
如果涉及有返回值的线程方法,则需要实现callable接口,并以ExecutorService方式调用 -
4种线程池
Java线程池顶级接口为Executor,仅包含execute(Runnable command)一个方法,真正实现线程池功能的为ExecutorService接口。线程池主要实现类有:
ThreadPoolExecutor:
创建出与核心线程数一致的线程数量,执行与核心线程数对应的任务数量,如果还有未执行的任务并且当前等待队列未满,加入等待队列,如果等待队列满,且还有任务提交进来,判断当前线程数是否等于池内最大线程数,不等于且小于的话,开启临时线程执行任务,若最大线程数等于池内线程数且等待队列已满,且还有任务继续提交进来,执行执行的拒绝策略reject handle,核心线程执行完任务后调用take()方法继续接任务执行或者阻塞等待keep alive,存活在线程池中等待下一次任务,达到线程复用,非核心线程执行完任务后假如没有任务执行了,且超出了预定的过期时间后,执行poll()方法销毁;
int corePoolSize:初始线程数
int maximumPoolSize:最大线程数
long keepAliveTime:线程存活时间
TimeUnit unit:线程存活时间单位
BlockingQueue workQueue:阻塞队列
ArrayBlockingQueue:数组阻塞队列,先进先出原则
LinkedBlockingQueue:链表阻塞队列,先进先出,吞吐量高于数组阻塞队列
SynchronousQueue:不存储元素的无界阻塞队列,任务插入操作等待,有空闲的线程调用或者创建新线程时候移除操作,否则插入操作一直阻塞,吞吐量高于链表阻塞队列,不属于一种容器队列,只是类似一种任务分发
PriorityBlockingQueue:优先无界阻塞队列,按照优先级对元素进行排序
ThreadFactory threadFactory:创建线程
RejectedExecutionHandler handler:饱和拒绝策略
(1)AbortPolicy(默认)直接拒绝
(2)CallerRunsPolicy:使用调用者的线程执行任务,不抛出异常不抛弃任务,将任务返还给调用主线程去执行,趁这段时间处理线程池正在执行的任务
(3)DiscardOldestPolicy:放弃队列中等待最久的任务(注意不能跟优先级队列组合使用,不然会把优先级最高的最老的任务抛弃)
(4)DiscardPolicy:抛弃当前任务
/**
* Creates a new {@code ThreadPoolExecutor} with the given initial
* parameters.
*
* @param corePoolSize the number of threads to keep in the pool, even
* if they are idle, unless {@code allowCoreThreadTimeOut} is set
* @param maximumPoolSize the maximum number of threads to allow in the
* pool
* @param keepAliveTime when the number of threads is greater than
* the core, this is the maximum time that excess idle threads
* will wait for new tasks before terminating.
* @param unit the time unit for the {@code keepAliveTime} argument
* @param workQueue the queue to use for holding tasks before they are
* executed. This queue will hold only the {@code Runnable}
* tasks submitted by the {@code execute} method.
* @param threadFactory the factory to use when the executor
* creates a new thread
* @param handler the handler to use when execution is blocked
* because the thread bounds and queue capacities are reached
* @throws IllegalArgumentException if one of the following holds:<br>
* {@code corePoolSize < 0}<br>
* {@code keepAliveTime < 0}<br>
* {@code maximumPoolSize <= 0}<br>
* {@code maximumPoolSize < corePoolSize}
* @throws NullPointerException if {@code workQueue}
* or {@code threadFactory} or {@code handler} is null
*/
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
ScheduledThreadPoolExecutor:
线程池工具类Executors提供构造4中具体含义的线程池:
newCachedThreadPool:提供一个带60s缓冲的线程池,设定初始线程数与最大线程数
newFixedThreadPool:固定大小线程池,线程执行结束如果无新任务立即销毁
newScheduledThreadPool:定时调度线程池
newSingleThreadExecutor:单线程池
-
线程生命周期与终止线程的4种方式
新建:使用new关键字创建线程之后,处于新建状态JVM分配内存并初始化属性
就绪:调用start()方法之后,JVM为其创建虚拟机栈和程序计数器并等待CPU调度
运行:获得CPU调度,执行run()方法体
阻塞:
等待阻塞(o.wait->等待对列):运行的线程执行 o.wait()方法,JVM 把该线程放入等待队列中
同步阻塞(lock->锁池):运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线程放入锁池(lock pool)中。
其他阻塞(sleep/join):运行的线程执行 Thread.sleep(long ms)或 t.join()方法,或者发出了 I/O 请求时,JVM 会把该线程置为阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时或者 I/O处理完毕时,线程重新转入可运行(runnable)状态。
死亡:
正常结束:run()或 call()方法执行完成,线程正常结束。
异常结束:线程抛出一个未捕获的 Exception 或 Error。
调用 stop:直接调用该线程的 stop()方法来结束该线程—该方法通常容易导致死锁,不推荐使用。
线程终止方式:
1. 正常运行结束:线程运行run方法完毕,或设置终止状态,在线程运行过程中更改状态完成循环跳出
interrupt方法终止:
-
Interrupt与sleep/wait
线程sleep过程中如果被打断,会抛出异常 InterruptedException,sleep会提前结束,并且此时线程的打断标志会被清除。如果需要在被打断时做出响应,可以捕获异常后处理。
-
interrupt与synchronized
interrupt无法干扰正在阻塞等待 synchronized 锁的线程
-
interrupt与lock
interrupt无法打断等待lock锁的线程,如果需要线程等待锁的过程中可以被打断,使用 lockInterruptibily
-
小结
interrupt可以通过设置线程的中断状态来达到通知的目的,具体是否需要做出响应可以视情况而定。比如想在线程外部终止线程的执行而又不破坏数据一致性,比如多线程争抢锁并操作数据的情况,可以利用interrupt来通知线程,线程内部通过中断状态来优雅的终止,而不是强制杀死线程。
调用线程的interrupt方法,会设置线程的终止状态为true,并产生InterruptedException异常,可以通过isInterrupt()方法能取得终止状态。但设置interrupt后在catch中捕获异常,并不会直接终止线程,需要在异常补货之后使用break终止循环状态;另外,使用ExecutorService启动的线程对interrupt不生效
1. stop方法终止:废弃,会发生不可预知的异常情况
-
sleep,wait,start,run,join
-
Java锁机制
- CAS
Compare and Swap,更改前先与原来的值比对,如果不一致则更新失败。多应用于Unsafe类中,采用自旋锁+CAS机制更新
- AQS
AbstractQueuedSynchronizer,一个抽象类,定义了一套多线程下访问共享资源的方案。内部主要包含一个volatile int state的属性标记当前状态,和一个FIFO的阻塞队列用来存储等待资源的线程。state状态更新是采用自旋锁+CAS机制。AQS中设置了3中共享方式:独享,共享,独享+共享,主要取决于各个实现类内部的实现方案。主要实现类有:ReentrantLock(独享),CountDownLatch(共享),ReentrantReadWriteLock(独享+共享)
-
Java锁类型
- 乐观锁
读多写少,读取的时候不加锁,写入是通过CAS机制比较版本,加锁写入。
- 悲观锁
悲观锁在写多读少的场景下,任务每次读写前其他线程都可能更新了数据所以要加锁读写,典型如synchronized。
- 自旋锁
如果持有锁的线程可能在很短的时间内释放锁,则等待竞争锁的线程就不必进入阻塞状态,而是自旋等待锁释放后获取锁,避免线程状态切换的损耗。
优点:合适场景下,避免线程切换上下文带来的消耗
缺点:如果线程持锁时间长,或锁竞争激烈则CPU消耗更大。
自适应自旋锁:JDK6开始JVM对于自旋执行时间根据状态判断确定,而非之前的固定值。
- Synchronized同步锁
Synchronized关键字将任意一个非null对象当作锁。属于独占式悲观锁,也属于可重入锁,非公平锁
Synchronized作用范围:
- 作用于方法上,则将当前对象this加锁,访问当前对象的同步方法时会阻塞
作用于静态方法上,则将Class对象加锁,由于Class对象存储于方法区,属于全局共享对象,因此静态方法锁相当于一个类的一个全局锁,对于此类的所有对象调用静态同步方法的线程都会加锁。
package eric.study.jvm.thread;
import java.util.concurrent.TimeUnit;
/**
* 1.加锁后只对后序其他带锁的代码或方法有影响,非同步代码不影响
* 2.静态方法同步后,由于Class实例为全局共享,所以此类的所有对象的同步代码都一并竞争同一个锁资源
* 3.静态代码块锁住的是Class实例,不影响当前类的实例,即静态代码同步锁不影响非静态代码同步,只是对于所有对象调用静态同步代码会阻塞。
* @since 2022-04-21
*/
public class SynchronizedStaticTest {
public static void main(String[] args) {
System.out.println("main start");
SynchronizedStaticThread test1 = new SynchronizedStaticThread();
SynchronizedStaticThread test2 = new SynchronizedStaticThread();
Thread t1 = new Thread(test1, "A");
Thread t2 = new Thread(test2, "B");
t1.start();
t2.start();
System.out.println("main end");
}
public static class SynchronizedStaticThread implements Runnable {
public synchronized static void syncStaticMethod() {
System.out.println(Thread.currentThread().getName() + " syncStaticMethod start");
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " syncStaticMethod end");
}
public synchronized void syncMethod() {
System.out.println(Thread.currentThread().getName() + " syncMethod start");
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " syncMethod end");
}
@Override
public void run() {
syncStaticMethod();
// syncMethod();
}
}
}
- 作用于属性对象,锁住的是以此特殊对象为锁的代码块。
Synchronized实现:
ContentionList:竞争队列,一个非具体的队列存放竞争线程,从头部放入尾部取出
EntryList:为了增加性能,将有资格竞争锁资源的一部分线程从CotentionList中取出
WaitSet:阻塞线程存放集合
OnDeck:当前竞争锁资源的线程,每次只有一个线程可以竞争资源
Owner:当前持有锁的线程
!Owner:释放锁的线程
当一个线程开始竞争当前锁资源时,先自旋尝试获取锁资源(非公平性),如果超过自旋时间阀值还未获取到资源,则从头部插入ContentionList;由于ContetionList中太多线程不断通过CAS并发访问会导致资源消耗,因此JVM会将一部分线程转移至EntryList中;Owner线程Unlock时,JVM从ContentionList尾部将一部分线程转移至EntryList,并指定某一个线程为OnDeck(一般是最先进来的),OnDeck线程并不直接获得锁,而是获得竞争权(与自旋的线程一并竞争),虽然失去公平性,但是增加了吞吐量;OnDeck线程获得锁资源后变成Owner,如果出发wait后阻塞进入WaitSet,直到notify再次唤醒会重新进入EntryList,ContentionList,EntryList,WaitSet中的线程都属于阻塞线程;
锁升级:从偏向锁升级到轻量锁再到重量锁,这也是JDK8开始对synchronized的优化
1. ReetrantLock
ReetrantLoc是一种可重入锁,与Synchronized功能基本相同,只不过
- 额外提供了诸如可响应中断锁、可轮询锁请求、定时锁,指定唤醒等功能,避免多线程死锁的方法。
- 需要手动加锁与解锁
```other
private final Lock lock = new ReetrantLock(false);// 默认非公平锁,可设置为true,即公平锁
private final Condition condition = lock.newCondition();
try{
lock.lock();// 锁定
...
condition.await();// 等待,等同于Object中的wait()方法
...
condition.signal();// 唤醒,等同于Object中的notify(),ReentrantLock可以指定唤醒条件,Object只能随机唤醒
condition.sinalAll()// 唤醒所有等待线程,等同于Object中的notifyAll()
}finally{
lock.unlock(); // 解锁,需要手动操作否则导致死锁
}
```
1. Samaphore信号量
2. AtomicInteger
自旋锁+CAS机制,属于乐观锁,独占锁
1. volatile关键字
2. 保证线程可见性
多线程读取volatile修饰的变量时不再加载到线程缓冲区,而是直接读取主内存的变量值
多线程写入volatile修饰的变量时,会主动通知其他线程刷新
1. 不保证原子性
volatile修饰的变量不能保证原子性,例如i++在编译后的字节码指令分为多个步骤,volatile不能保证这一组指令运行不会被打断,因此多个线程同时计算i++时通常不能得到预期结果。java使用锁机制来保证原子性,如synchronized或lock。
1. 禁止指令重排序
jvm在编译和运行期会优化字节码指令,对于不影响运行结果的字节码顺序可能有变化,例如实例化对象时分为:
1. 申请内存空间
2. 初始化变量
3. 变量关联
但步骤2和3没有关联性因此可能会被重排序为1,3,2这时当外部线程判断对象是否为空时返回false,然而此时的对象还未创建完成导致无法使用。
如果使用volatile关键字修饰后,虚拟机会保证上述步骤按顺序完成这时获取到的对象才能保证可用。
注:
1. 双重校验的线程同步单例模式变量必须使用volatile修饰
2. 当线程调用了synchronized修饰的方法时,会主动从主内存刷新变量值