1.并发编程核心问题
并发编程过程中也就是多线程并发执行问题。包含分工、同步、互斥。
分工:大型任务进行拆分,单个线程执行小量的任务。
同步:当前某个线程执行完任务,通知其他线程进行开始执行,也就是管程。
互斥:同一时刻,只允许有单个线程占用资源,保证资源的共享安全。
2.进程&线程
1.含义
进程:进程是程序的单次执行过程,是JVM进行资源分配的最小单位(加载指令、管理IO等)。
线程:线程是比进程更轻量级的执行单位,是最小的调度单位。
2.二者关系
1.进程之间基本相互独立、而线程属于进程、是进程中的子集;
2.进程拥有共享的资源、内存空间等。可以提供给线程共享;
3.进程之间通信比线程复杂。
进程可在单机上、网络中通信。但线程只能在同一个进程间通信。
4.线程更加轻量级,上下文切换成本低于进程切换。
3.并行&并发
并发:同一时刻,只能有单个线程运行,多见于单核CPU中。
并行:同一时间段,可存在多个线程运行,多见于多核CPU中。
3.Java线程
1.线程创建
Java线程中,提供了三种创建方式。继承Thread、实现Runnable接口、实现Callable接口。
1.继承Thread
public class MyThread extends Thread{
@Override
public void run(){
// 代码块
}
psvm(){
MyThread th = new MyThread();
th.start();
}
}
2.实现Runnable
public class MyThread implements Runnable{
@Override
public void run(){
// 代码块
}
psvm(){
Thread th = new Thread(new MyThread());
th.start();
}
}
3.实现Callable
继承FutureTask、具有返回值信息
public class MyThread implements Callable{
@Override
public Object run(){
return null;
}
}
2.线程运行
栈:每个线程启动,则JVM专门分配一块虚拟机栈内存空间供线程使用。
栈帧:线程中方法的调用息息相关,是栈的组成单位。
上下文切换:指的是线程占有资源开始执行、到线程时间片用完进入阻塞或结束的这一阶段。
3.start&run
start():正确启动线程的方法,会在父级线程中创建一个新的线程,达到异步效果;
run():一般方法,不会在父级线程中创建线程,作为一般方法执行,无异步效果。
4.sleep&yield&wait()
sleep():
1、调用sleep()会让当前线程进入阻塞状态;
2、sleep中的线程被其他线程调用interrupt会抛出打断异常;
3、sleep结束后,线程未必会立刻进入执行。
yield():
1、调用yield()会让当前线程进入就绪状态,从而调用其他线程执行;
2、具体实现依赖于操作系统调度;
sleep()&wait():
1、sleep不会释放锁、wait会释放锁
2、两者都可以暂停线程执行;
3、sleep可自动结束休眠、但wait需要其他线程调用notify()¬ifyAll()唤醒;
4、sleep应用于线程休眠、wait应用于线程通信。
5.join()
下列代码中,在t1线程之后又使用了r变量,这时,就需要使用join进行同步,等待上一线程执行完毕,在执行其他线程。
static int r = 0;
public static void main(String[] args) throws InterruptedException {
test1();
}
private static void test1() throws InterruptedException {
log.debug("开始");
Thread t1 = new Thread(() -> {
log.debug("开始");
sleep(1);
log.debug("结束");
r = 10;
});
t1.start();
log.debug("结果为:{}", r);
log.debug("结束");
}
4.Java内存模型
Java内存模型,也就是JMM。定义了主存、工作内存抽象概念、也就是说虚拟机中对于变量的存取细节。
1.Java内存模型体现
2.原子性
1.注意点
单线程情况下,所有的操作都是原子性的。
多线程情况下,多个线程之间在上述Servlet执行过程中就没有遵循原子性问题,导致出现资源共享出错问
题。
2.案例分析
两个线程对同一静态变量为0则核心资源分别进行自增、自减、各进行5000次,结果为0吗?
public class Solution{
public static count = 0;
psvm(){
Thread th1 = new Thread(() -> {
for(int i = 0; i < 5000; i++){
count++;
}
}, "t1");
Thread th2 = new Thread(() -> {
for(int i = 0; i < 5000; i--){
count--;
}
}, "t2");
th1.start();
th2.start();
th1.join();
th2.join();
}
}
问题分析:上述过程中,单线程无问题,但在多线程中会出现正数、负数、等多中情况。
单线程情况:
多线程负数情况:
原因:
多线程访问共享资源,读写操作过程中由于分时系统下线程频繁的上下文切换出现指令交错。
临界区:多线程共享的那一部分资源。
竞态条件:多线程在临界区执行,代码执行顺序不同导致结果无法预测。
3.解决方案
互斥锁:Synchronized、Lock
非互斥锁:原子变量
3.可见性
可见性:线程之间对同一核心进行操作后,后续线程可以看见核心操作后的状态。
单线程情况下,所有线程操作统一核心,线程有先后顺序,后续线程可以看见核心状态的变化。
多线程情况下,所有线程访问未加锁的核心,会导致数据不一致问题。
例如:统计一个servlet中的访问次数,多个线程进入service方法中,都同时读取到了count值为9的状态,此时,多线程操作后,count值都各自变为了10,。本应该变为11,但由于线程不安全导致结果精度缺失。
public class MyServlet implements Servlet{
// 访问计数器变量
private long count = 0;
public long getCount(){
return count;
}
public void service(ServletRequest req, ServletResponse resp){
++count;
// 其他代码块
}
}
4.有序性
编译器有时会将代码顺序打乱来进行编译优化,单线程中是没有问题的。但在多线程环境中会出现意想不到的事情。
public class Singleton{
private Singleton singleton;
private Singleton(){}
public static Singleton getInstance(){
if(singleton == null){
synchronized(Singleton.class){
if(singleton == null){
singleton = new Singleton();
}
}
}
return singleton;
}
}
上述单例模式中,虽然对初始化进行了synchronized锁对象,但由于编译器优化会出现问题。<br />**原始new过程:**<br />1、堆内存中分配内存空间<br />2、内存中对该对象进行初始化<br />3、将内存地址赋值给singleton实例<br />**优化过程后:**<br />1、堆内存中分配内存空间<br />2、将内存地址赋值给singleton实例<br />3、内存中对该对象进行初始化<br />编译器优化之后,线程1进入后,正在初始化实例。但线程2进入后发现singleton不为空(有了内存地址),就会结束执行,直接返回,此时线程2就会出现NullPointException问题。<br />![](https://cdn.nlark.com/yuque/0/2022/png/22681993/1647415524767-a43a09f7-446d-47af-bb34-6dd36a7ee9b1.png#crop=0&crop=0&crop=1&crop=1&from=url&id=VJB5B&margin=%5Bobject%20Object%5D&originHeight=586&originWidth=929&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
5.原子性-Synchronized
1.界定
synchronized(对象锁)是通过互斥方式,让同一时刻,至多有一个线程占用资源,其他线程时区资源则会进入阻塞状态。
synchronized实际上是通过对象锁保证了临界区代码块的原子性操作。
2.注意点
synchronized属于独占式的悲观锁、也是可重入锁。
Java中互斥与同步都可以采用synchronized关键字来完成,但之间存在着区别
互斥:是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
同步:是由于线程执行的先后,顺序不同、需要一个线程等待其他线程运行到某个点。
3.语法
public class Solution{
// 同步代码块——类锁
synchronized(this){
// 代码块
}
// 同步实例方法——对象实例锁
public synchronized int sum(){
// 代码块
}
// 同步静态方法——类锁
public synchronized static int sum(){
// 代码块
}
}
4.原子性分析
synchronized实际上使用对象锁保证了临界区内代码的原子性,将临界区代码作为一个不可分割则原子性。
5.变量线程安全分析
1.成员&静态
1、无共享,则属于线程安全
2、有共享,则需要分情况讨论
只读:只读操作中,属于线程安全
读写:读写操作中,成为了临界区,需要线程安全
2.局部变量
1、局部变量是线程安全的
2、局部变量引用的对象未必
未逃离、则线程安全
public static void method(){
int i = 10;
i++;
}
逃离&被外界使用,则线程不安全
public class Solution{
ArrayList<String> list = new ArrayList<>();
public void method1(int loopNumber){
for(int i = 0; i < loopNumber; i++){
// 临界区、竞态条件
method2();
method3();
}
}
public void methos2(){
list.add("1");
}
public void method3(){
list.remove(0);
}
psvm(){
Solution so = new Solution();
for(int i = 0; i < 2; i++){
new Thread(() -> {
so.method1(200);
}).start();
}
}
}
分析:上述过程中,list属于临界区资源,会产生竞态条件,因为非局部变量,发生了读写操作<br /> 将list变为局部变量即可。
6.synchronized底层原理
1.Monitor
Java为面向对象思想,在JVM中存储大量对象信息,存储时为了实现额外功能,需要在对象中添加一些额外的信息增强对象功能,这些标记信息就组成了Java对象头。
对象结构如下:32位
2.Moniter原理
Moniter被翻译成监视器或者管程。
每个Java对象都可以关联一个Moniter对象,如何使用synchronized对象锁(重量级锁),该对象头Moniter中的Mark word中就被设置指向Moniter对象的指针。
1、刚开始Moniter中的Owner为null;
2、Thread-2获取synchronized对象锁,则将Moniter所有者Owner设置为Thread-2,且只存在一个Owner;
3、此时,Thread-2,4,5也申请获取对象锁,此时Thread-3,4,5进入阻塞队列EntryList中;
4、当Thread-2,执行完同步代码块中内容时,则唤醒EntryList中的线程,非公平唤醒;
5、Thread-0,1是在之前获取对象锁,但不满足条件的线程。
注意点:
1、synchronized必须是进入同一对象的Moniter才有上述效果。
2、不加synchronized的对象不会关联监视器、不遵从上述规则。
Mark Word结构:
3.synchronized原理
synchronized底层是通过持有Moniter的对象来完成的。方法级别的synchronized不会在字节码中体现。
底层维护了三个双向链表来存储锁信息,一个是Contention List(竞争队列)、EntryList(候选队列)、WaitSet(阻塞队列)。
public class Solution{
public static final Object lock = new Object();
public static int count = 0;
public static void main(Stirng[] args){
synchronized(lock){
count++;
}
}
}
// 对应字节码信息
Code:
stack=2, locals=3, args_size=1
0: getstatic #2 // <- lock引用 (synchronized开始)
3: dup // 复制
4: astore_1 // lock引用 -> 存储到 slot 1 方便后续解锁
5: monitorenter // 将 lock对象 MarkWord 置为 Monitor 指针
6: getstatic #3 // <- i
9: iconst_1 // 准备常数 1
10: iadd // +1
11: putstatic #3 // -> i
14: aload_1 // <- lock引用 获取到lock对象引用
15: monitorexit // 将 lock对象 MarkWord 重置, 释放锁 唤醒 EntryList
16: goto 24
// 下列代码是为了防止出现异常导致无法释放锁
19: astore_2 // e -> slot 2
20: aload_1 // <- lock引用
21: monitorexit // 将 lock对象 MarkWord 重置, 唤醒 EntryList
22: aload_2 // <- slot 2 (e)
23: athrow // throw e
24: return
7.synchronized锁优化
JDK6之前,synchronized都是重量级锁、在JDK6及其以后,对该锁进行了大量优化。
包含:轻量级锁、锁膨胀、自旋锁、偏向锁、锁消除
1.轻量级锁
多线程交替执行,但加锁时间是错开的(无竞争),这时可以使用轻量级锁进行优化;
多线程交替执行,存在竞争条件,则会升级为重量级锁。
轻量级锁中,通常采用CAS(比较并交换算法)进行替换来进行加锁。
1、创建锁记录对象,每个线程的栈帧中存储一个锁记录结构,内部可以锁定对象的Mark Word。
2、让锁记录中的reference指向锁对象,然后尝试用CAS替换Object中的Mark Word,将Mark Word的值存入锁记录中。
3、如果替换成功,则将地址存储锁记录地址和状态00,则表示由该线程进行加锁。
4、CAS替换失败,有两种情况。
1.如果其他线程已经占用了轻量锁,则会出现竞争条件,升级为锁膨胀;
2.如果自己已经占用了synchronized可重入,则再增加一条锁记录。
2.锁膨胀
在尝试添加轻量级锁的时候,出现其他线程已占用竞争条件,则会升级为锁膨胀。轻量级升级为重量级。
1、线程1进行轻量级加锁时时,线程2已经添加了轻量级锁,则会进行锁升级。
2、这时,线程1添加锁失败、进入锁膨胀阶段
为Object申请Moniter锁,让其指向重量级锁
然后将自己加入到Moniter的EntryList中候选阻塞
3、当线程0执行完释放轻量级锁时,会出现失败。则会进入到重量级解锁流程中。
3.自旋优化
重量级锁竞争时,可以通过自旋优化。
线程1获取同步代码块锁,加了重量级锁。此时,线程2尝试申请同步代码块锁,发现已经被占用,则不会进入阻塞,会通过自旋重试来等待。
如果持续得不到锁,则会进入阻塞。
4.偏向锁
线程进入轻量级锁后,再次重入时,会检查是否是自身线程占有,如果是自身占有则不会进行CAS。
5.锁消除
6.原子性-ReentrantLock
1.界定
ReentrantLock也是可重入锁、具备可中断、设置超时变量、设置公平锁、支持多条件变量。是JUC并发包下的一个类。
2.语法
public class Solution{
// 获取锁
ReentrantLock reentrantLock = new ReentrantLock();
reentrantLock.lock();
try{
// 临界区
}finally{
// 释放锁
reentrantLock.unlock();
}
}
3.特点
锁中断:lock锁提供了可以中断等待中的线程锁,通过lock.lockInterruptibly()来实现。
锁超时:lock中可设置等待时间,超过该时间未获得锁则返回,lock.tryLock();实现
公平锁:默认为不公平锁,可通过构造器设置是否公平。
绑定多条件变量:
7.Sync&Lock区别
相同点:两者都是可重入锁、可以解决原子性问题
不同点:
Synchronized依赖于JVM、ReentrantLock依赖于Api;
ReentrantLock提供锁中断、公平锁、锁超时、多条件变量;
Synchronized为非公平锁。
8.可见性-Volatile
1.内存屏障
现代计算机系统为了实现高性能处理能力、通常采用乱序执行指令(指令重排)。内存屏障是一种同步屏障指令、内存并发访问中的同步点、保证一些操作只能在该屏障点之后执行。
可见性:
1.写屏障:该屏障之前,保持共享数据已经同步到主存中。之后的指令才能执行。
2.读屏障:该屏障之后,保持共享数据加载的主存中最新数据。
有序性:
1.写屏障:指令重排时、 不会将屏障前需要执行的指令排在屏障之后执行
2.读屏障:指令重排时、不会将屏障后需要执行的指令排在屏障之前执行
2.Volatile原理
Volatile底层实现原理是内存屏障
volatile变量:写指令之后加入写屏障、读指令之后加入读屏障
9.final原理
写入过程:final变量的赋值过程中,会涉及到putfield指令、在该条指令之后会增加一个写屏障,保障其他线程访问该变量时不会为0值
10.无锁-CAS
CAS全称为CompareAndSwap(比较并交换),是对多线程中使用锁造成性能消耗的一种优化机制。
1.操作
CAS包含三个要素:内存位置、原始值、新值。如果内存中的原始值和新值一样,则更新原始值为新值,否则不做改动。
2.特点
CAS是乐观锁、Synchronized是一种悲观锁。
CAS在执行过程中、如果有线程在修改变量,则会进行重试;
CAS多线程无锁并发、无阻塞并发。
无阻塞并发必然重试、重试必然消耗性能
3.CAS实现-原子整数
JUC并发包下提供了多种原子整数实现,AtomicInteger、AtomicBoolean、AtomicLong三种类型都依据了CAS进行实现。
/**
* Atomically sets to the given value and returns the old value.
*
* @param newValue the new value
* @return the previous value
*/
public final int getAndSet(int newValue) {
return unsafe.getAndSetInt(this, valueOffset, newValue);
}
Atomic类中的所有方法又都是基于sun包下的Unsafe类进行实现、Unsafe中的方法为Native方法、最终依赖于C、C++在操作系统上的实现。
3.CAS实现-原子引用
AtomicReference、AtomicMarkedReference等
3.CAS实现-原子数组
11.线程池-ThreadPoolExecutor
1.定义
ThreadPoolExecutor指的是一种多任务机制的任务多线程任务队列。
2.线程池状态
ThreadPoolExecutor使用int的高3位来表示状态、低29位来表示线程数量
线程池五大状态:Running、Shutdown、Stop、Tidyint、Terminated
Running:线程池处于正常运行状态、可以处理任务
Shutdown:线程池不能接收新任务、但可以处理已经添加的任务
Stop:停止状态、不接收、不处理、中断正在执行的线程任务
Tiding:所有任务已终止、ctl中记录的“任务数量为0”
Terminated:线程池彻底终止
3.线程池参数
corePoolSize:线程池中常驻线程数量
maxinumPoolSize:线程池最大线程任务数量
keepAliveTime:空闲线程任务存活时间
workQueue:阻塞队列
threadFactory:线程工厂
handler:拒绝策略
4.线程工作方式
1.无线程时、添加线程任务到线程池中,线程池创建线程并执行任务;
2.线程数达到corePoolSize时,则新加入的线程任务进入workQueue等待;
3.如果为有界队列,超出界限时,则会创建maximumPoolSize-corePoolSize急救线程来处理
4.如果达到maximumPoolSize,任然有线程任务加入,则会启动拒绝策略。
5.四种线程池
newFixedThreadPool:固定大小的线程池,最大数量等于核心数量
newCachedThreadPool:可缓存的线程池,队列使用了synchronized实现
newSingleThreadPool:单个线程的线程池,唯一线程不会释放
newScheduledThreadPool:可定时的线程池
12.同步器-AQS
AQS-AbstractQueuedSychronizer抽象队列同步器、用来构建Lock锁和同步器的基础框架