JAVA并发编程
并发和并行
并发
两个及两个以上的作业在同一时间段内执行。
指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。
并行
两个及两个以上的作业在同一时刻执行。
指在同一时刻,有多条指令在多个处理器上同时执行。所以无论从微观还是从宏观来看,二者都是一起执行的。
并发与并行最关键的区别点是:是否同时执行。
同步和异步
同步
发出调用之后,没有得到结果之前,该调用不可以返回,需一直等待。
异步
发出调用之后,不用等待返回结果,该调用直接返回,可以执行下一步。
JAVA线程的生命周期
NEW 初始状态
线程被创建出来,还没有调用start()
RUNNABLE 运行状态
线程被调用了start 等待运行的状态
BLOCKED 阻塞状态
当线程进入 synchronized 方法快或调用 wait后(被 notify)重新进入 synchronized 方法快,但是锁被其他线程占有,那么该线程就会进入 BLOCKED 状态
WAITING 等待状态
线程需要等待其他线程做出一些特定的操作(通知或中断),如调用了wait()
TIME_WAITING 超时等待状态
指定的等待时间,而不是像WAITING状态那样一直等待,调用了sleep(long millis)或wait(long millis)可以讲线程置于 TIME_WAITING 状态。当超时时间结束后,线程将会返回到 RUNNABLE 状态。
TERMINATED 终止状态
表示线程已经运行完毕,执行完了run()
线程上下文切换
线程在执行过程中会有自己的运行条件和状态(也就是上下文)。
线程从占用CPU的状态中退出,这时会保存线程的上下文,并加载下一个将要占用CPU的线程上下文,这就完成了线程上下文切换。
也就是当前正在执行的线程发生了切换。
保存的上下文用于下次线程占用CPU时可以恢复执行。
什么时候线程会从占用CPU的状态中退出?
线程主动退出CPU占用,如调用了sleep(),wait()等
调用阻塞类型的系统中断,如请求IO,线程被阻塞
时间片用完,这个与操作系统相关,操作系统为了防止一个线程或进程长期占用CPU而导致其它线程与进程没有资源。
被终止或结束运行
多线程概念与问题
为什么要使用多线程?
从计算机来讲,线程是程序执行的最小单位,线程之间的切换和调用的成本低于进程。
从项目上来讲,多线程并发可以提高系统整体的并发能力与性能,可以支持更多的用户。
多线程会带来什么问题?
多线程开发的目的是为了提高程序的执行效率和运行速度,但是在使用过程中会遇到内存泄漏,死锁,线程不安全等问题。
什么是线程不安全?
多线程环境下对同一份数据的访问是否能够保证其正确性和一致性。
安全代表着无论多少格线程同时访问一份数据,都不会出现数据混乱、错误或者丢失等问题。
什么是死锁?
多个线程同时被阻塞,它们中一个或全部都在等待某个资源释放,由于线程被无限期的阻塞,导致程序无法正常终止。
死锁的四个必要条件
互斥:该资源任意时刻只由一个线程占用
请求与保持:一个线程因请求资源而阻塞时,对已获得的资源保持不放
不剥夺:线程已获得的资源在未使用完之前,不能被其他线程强行剥夺,只有自己使用完毕后才会的都释放
循环等待:若干线程之间,形成一种头尾相接的循环等待资源关系
简单示例:
线程1持有A资源,线程2持有B资源,它们同时都在申请对方的资源,而线程没有执行完,资源没有被释放,导致它们互相在等待对方执行完毕释放资源,这就进入了死锁状态。
如何预防和避免死锁?
破坏形成死锁的必要条件即可预防死锁的发生。
破坏请求与保持:一次性申请所有的资源
破坏不剥夺条件:占用部分资源的线程,申请其他资源,如果申请不到,可以主动释放持有资源
破坏循环等待条件:按照顺序申请资源,释放资源则按反顺序释放
在之前线程1持有A,线程2持有B,它们同时申请对方资源导致死锁的问题。
就可以让线程1与线程2都持有AB(一次性申请所有的资源),这样执行过程中一定有一方拿不到AB资源从而进入等待。
而另一方拿到了AB资源可以正常执行,然后释放资源。
sleep() 方法和 wait() 方法对比
sleep()与wait()的区别
共同点
两者都可以暂停线程的执行。
区别
sleep()没有释放锁,而wait()释放了锁(当前线程占有的对象锁)
wait()通常被用于线程间交互/通信,sleep()通常被用于线程暂停执行
wait()方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的notify()或notifyAll(),当然,可以使用wait(long timeout)超时后线程会自动苏醒
sleep()方法执行完成后,线程会自动苏醒
sleep()是 Thread 类的静态本地方法,wait()是Object类的本地方法
为什么wait()方法不定义在Thread中?
wait()是让获得对象锁的线程实现等待,会自动释放当前线程占有的对象锁
每个对象(Object)都拥有对象锁,既然要释放当前线程占有的对象锁,并让其进入WAITING状态,自然操作对应的对象(Object),而非当前的线程(Thread)
为什么sleep()方法定义在Thread中是一样的道理
sleep()是让当前线程暂停执行,操作的是线程,不涉及到对象类,也不需要获取对象锁。
可以调用Thread类的run方法码?
new一个Thread,线程进入了初始状态,调用start(),线程进入就绪状态,分配到时间片后就会开始运行。
start()会执行线程相应的准备工作,然后自动执行run()方法的内容,这是多线程。
但是直接执行run()方法会把run()方法当成一个main线程(主线程)下的普通方法去执行,并不会在其他线程中执行它,所以这就不是多线程了。
调用start()方法可以启动线程并使线程进入运行状态,然后自动执行run()方法
直接调用run()方法则是在主线程下执行,并不会以多线程的方式执行
什么是悲观锁
悲观锁是一种思想,它总是假设每次都是最坏的情况。
认为共享资源每次被访问都会发生问题,所以在每次对资源的操作都会加锁。
这样当一个线程拿到资源后,其他线程获取该资源都会进入阻塞状态,需要等待该资源被释放。
java 中 synchronized 和 ReentrantLock 等独占锁就是悲观锁思想的实现。
public void performSynchronisedTask() {
synchronized (this) {
// 需要同步的操作
}
}
private Lock lock = new ReentrantLock();
lock.lock();
try {
// 需要同步的操作
} finally {
lock.unlock();
}
在高并发的场景下,激烈的锁竞争会造成大量大线程阻塞,线程阻塞会导致系统频繁的触发线程上下文切换,增加系统的性能开销。
并且悲观锁会存在死锁问题,影响程序的运行。
什么是乐观锁?
乐观锁是一种思想,它总是假设每次都是最好的情况。
认为共享资源每次都被访问都不会出现问题,线程可以不停的执行,无需加锁,也无需等待。
只有在提交修改的时候去验证对应资源是否被其他线程修改了
java 中 java.util.concurrent.atomic 包下面的原子变量类(比如AtomicInteger、LongAdder)就是使用了乐观锁的一种实现方式 CAS 实现的。
// LongAdder 在高并发场景下会比 AtomicInteger 和 AtomicLong 的性能更好 // 代价就是会消耗更多的内存空间(空间换时间)
LongAdder sum = new LongAdder();
sum.increment();
高并发的场景下,乐观锁相比悲观锁来说,不存在锁竞争造成线程阻塞,也不会有死锁的问题,在性能上往往会更胜一筹。
但是,如果冲突频繁发生(写占比非常多的情况),会频繁失败和重试,这样同样会非常影响性能,导致 CPU 飙升。
乐观锁一般会使用版本号机制或CAS算法实现。
版本号机制
比如数据库中加一个数据的版本号version字段。
然后修改的时候先获取,提交修改时对比版本号是否一致。
如果一致,修改版本号的值,然后提交修改。
如果不一致,表示中途被其他线程修改,应该要驳回本次请求。
CAS算法
CAS的全称时 Compare And Swap(比较与交换),用于实现乐观锁,被广泛的应用各大框架中。
CAS的思想就是用一个预期值和要更新的变量值进行比较,两值相等才会更新数据。
CAS是一个原子操作,底层依赖于一条CPU的原子指令。
CAS涉及到三个操作数:
V:要更新的变量值(Var)
E:预期值(Expected)
N:拟写入的新值(New)
仅当V的值等于E时,CAS通过原子方式来将N的值更新到V。
如果不相等,说明V已经被其他线程更新,本线程应该放弃更新。
简单案例
int a = 1;
需要将 i 的值更新为 6
那么 V = a,E = 1,N = 6
V与E进行比较,相等,说明 a 还没有被其他线程修改,可以将值更新为6
如果不相等,说明 a 已经被其他线程修改,本次放弃更新。
CAS算法的ABA问题
使用CAS算法时,因为V读取到的值是预期值,可能已经发生修改了,比如1修改为5然后又被修改为1,那么CAS操作就会认为它从来没有被修改过,这就是ABA问题。
ABA问题的解决思路是在变量前面追加上版本上或者时间戳。
synchronized 有什么用?
synchronized 是JAVA的一个关键字,用于控制多线程对共享资源的访问,实现线程同步。
需要注意的是 synchronized 在使用中锁住的范围。
// 修饰实例方法 (锁当前对象实例)
// 即同一时间只有一个线程可以访问该实例的此方法
synchronized void method() {
//业务代码
}
// 修饰静态方法 (锁当前类)
// 即同一时间只有一个线程可以访问该类的所有实例的此静态方法
synchronized static void method() {
//业务代码
}
// 修改代码块
// synchronized(object) 表示进入同步代码库前要获得指定对象的锁。
// synchronized(类.class) 表示进入同步代码前要获得指定 Class 的锁
public void method() {
// 同步代码块,对当前实例加锁(对象锁)
synchronized (this) {
}
// 同步代码块,指定Class加锁
synchronized (类.class) {
}
}
总结:
synchronized 关键字加到 static 静态方法和 synchronized(class) 代码块上都是是给 Class 类上锁
synchronized 关键字加到实例方法上是给对象实例上锁
注意:
尽量不要使用 synchronized(String a) 因为 JVM 中,字符串常量池具有缓存功能
构造方法不能使用 synchronized 关键字修饰,因为构造方法本身就属于线程安全
ReentrantLock 有什么用?
ReentrantLock 实现了 Lock 接口,是一个可重入且独占式的锁,和 synchronized 关键字类似。不过,ReentrantLock 更灵活、更强大,增加了轮询、超时、中断、公平锁和非公平锁等高级功能。
ReentrantLock 默认使用非公平锁,也可以通过构造器来显式的指定使用公平锁。
// 传入一个 boolean 值,true 时为公平锁,false 时为非公平锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
公平锁和非公平锁有什么区别?
公平锁
锁被释放之后,先申请的线程先得到锁。
性能较差一些,因为公平锁为了保证时间上的绝对顺序,上下文切换更频繁。
非公平锁
锁被释放之后,后申请的线程可能会先获取到锁,是随机或者按照其他优先级排序的。
性能更好,但可能会导致某些线程永远无法获取到锁
共享锁和独占锁有什么区别?
共享锁
一把锁可以被多个线程同时获得
独占锁
一把锁只能被一个线程获得