(五)并发编程与锁机制
1、进程、线程、协程,并发、并行,同步、异步
进程是进⾏资源分配和调度的基本单位;独立的数据空间;
线程是进⾏运算调度的最⼩单位;共享的数据空间;
协程⼜称为微线程,是⼀种⽤户态的轻量级线程,协程不像线程和进程需要进⾏系统内核上的上下⽂切换,协程的上下⽂切换是由⽤户⾃⼰决定的;
关系:⼀个进程可以有多个线程,它允许计算机同时运⾏两个或多个程序。线程是进程的最⼩执⾏单位,CPU的调度切换的是进程和线程,进程和线程多了之后调度会消耗⼤量的CPU,CPU上真正运⾏的 是线程,线程可以对应多个协程;
协程:协程不需要系统内核CPU切换上下文,不存在同时写冲突,本质是单线程;无法运行在多个CPU上,JAVA没有成熟第三方库;
并发:CPU将运行时间分段,分配给多个线程,涉及到抢占CPU资源逻辑与算法;(一段时间在处理多个程序)
并行:多个CPU同时在运行多个线程,这些线程是同时在运行;(多个程序确实在同时运行)
2、线程状态
创建(NEW): ⽣成线程对象,未调用start(), new Thread();
就绪(Runnable):调⽤start()⽅法后,进⼊就绪状态,但是没获得CPU使⽤权。 等待被唤醒或者到睡眠时间,也会进⼊就绪状态;
运⾏(Running) :获得CPU使⽤权,由就绪状态进⼊运⾏状态,开始运⾏run方法;
阻塞(Blocked)
等待阻塞:需要等待其他线程作出⼀定动作(通知或中断),被唤醒,重新进⼊就绪状态,调⽤wait(状态就会变成 WAITING状态);
同步阻塞:加锁等待,线程获取synchronized同步锁失败,锁被其他线程占⽤,它就会进⼊同步阻塞状态;
超时等待:在指定的时间后⾃⾏进⼊就绪状态,调⽤sleep(状态就会变成TIMED_WAITING);
终止:⼀个线程run⽅法执⾏结束/抛出异常/被stop,该线程就终止了;
线程内常用方法:
sleep:属于线程Thread的⽅法,让线程进入超时等待阻塞,交出CPU使⽤权,不会释放锁;进⼊阻塞状态TIME_WAITGING,等待预计时间结束后变为就绪Runnable;
yeild:暂停当前线程,执⾏其他线程,交出CPU使⽤权,不会释放锁,和sleep类似;不进入阻塞,直接进入就绪,等待重新获得CPU使⽤权;让相同优先级的线程轮流执⾏,但是不保证⼀定轮流;
join:属于线程Thread的⽅法,优先让调⽤join的线程先执⾏,主线程 交出CPU使⽤权 不释放锁;
wait:属于Object的⽅法,会释放锁,进⼊等待阻塞队列,需要notify/notifyAll唤醒,或者wait(timeout)时间⾃动唤醒 才能进入就绪状态;
notify:属于Object的⽅法,唤醒在对象监视器上等待的单个线程,选择是任意的;
notifyAll:属于Object的⽅法 唤醒在对象监视器上等待的全部线程;
3、进程/线程 调度算法
进程调度算法:
先来先服务:按照到达时间的先后顺序;长作业占时间长,不利于短作业;
短作业优先:短作业需要时间较短且占比较高;不利于长作度;
⾼响应⽐优先:优先权=响应⽐=响应 时间/要求服务时间,响应时间=等待时间+要求服务时间;计算优先权增加系统开销;
时间⽚轮转:每个进程在⼀定时间内都可以得到响应;高频率切换,增加开销,不分紧急程度;
优先级调度:根据任务的紧急程度进⾏调度,⾼优先级的先处理;不利于低优先级的任务;
线程调度算法(程分配CPU使⽤权):
协同式:执⾏时间由线程本身控制,自身执行完毕后通知系统切换另一个;执行时间不可控,线程若有问题可能会一直阻塞;
抢占式:由系统来分配每个线程的执⾏时间;Java线程调度就是抢占式,根据优先级;notify是进入就绪不是运行;自身调用wait,notify随机唤醒等待队列中线程;
4、锁
脏读:线程有自己的工作内存,变量存在主内存,线程是复制变量到自己工作内存操作 而不是操作主内存;
指令重排:根据计算机自己的策略调整代码执行顺序;JVM在编译java代码或CPU执⾏JVM字节码时,对现有的指令进⾏重新排序,⽬的是优化运⾏效率(不改变程序结果的前提);
多线程可能会有问题,解决方法-内存屏障,是一种特殊指令,是CPU/编译器对屏障指令之前和之后的内存操作执⾏结果的⼀种约束;
先⾏发⽣原则:happens-before,volatile的可见性就是依据该原则,先执行先发生,后续线程可见;
引申八种原则:1、程序次序规则 2、管程锁定规则 3、volatile变量规则 4、线程启动规则 5、线程中断规则 6、线程终⽌规则 7、对象终结规则 8、传递性;
volatile:轻量级synchronized,每次读取前必须从主内存取最新值,每次写⼊需⽴刻写到主内存中;保证共享变量可见性,值变化其他线程⽴刻可见,避免脏读;基本已经被synchronized取代;
区别:volatile:保证可⻅见性,但是不能保证原⼦性; synchronized:保证可⻅见性,也保证原⼦性
使⽤场景 1、用当前值做修改操作的变量,不能使用;⽐如num++、num=num+1,JVM字节码层⾯不⽌⼀步-指令重排;2、由于禁⽌了指令重排,所以JVM相关的优化没了,效率偏弱;
synchronized:解决线程安全问题;每个对象有⼀个锁和⼀个等待队列;锁只能被⼀个线程持有,其他线程阻塞等待,锁被释放后随机唤醒一个线程;-- ⾮公平、可重⼊
两种形式-底层都是通过monitor来实现同步
方法级别-隐式同步:方法执行时检查ACC_SYNCHRONIZED标志位,若存在标志,则先获取monitor,方法获取成功且执⾏完后再释放monitor,其他线程⽆法获得同⼀monitor对象;
代码块-显式同步:monitor、monitorenter和monitorexit,执行monitorenter时计数器+1,执行monitorexit时-1;计数器为0时,monitor将被释放;
推荐使用synchronized:jdk6进⾏优化,增加了从偏向锁到轻量级锁再到重量级锁,重量级锁性能较低;jdk6之前都是重量级锁;
底层原理:对象头,标记字段+类对象地址;
ReentrantLock:继承AQS类,有非公平锁NonfairSync方法和公平锁FairSync方法,构造方法是否公平
ReentrantLock和synchronized都是独占锁
synchronized: 1、悲观锁,阻塞其他线程,java关键字
2、⽆法判断锁状态,锁可重⼊、不可中断、只能是⾮公平
3、加锁解锁的过程是隐式的,⽤户不⽤⼿动操作,优点是操作简单但显得不够灵活
4、⼀般并发场景使⽤、可以放在被递归执⾏的⽅法上,且不⽤担⼼线程最后能否正确 释放锁
5、synchronized操作的应该是对象头中mark word,参考原先原理图⽚
ReentrantLock: 1、Lock接⼝的实现类,悲观锁,
2、可以判断锁状态,可重⼊、可判断、可公平可不公平
3、需要⼿动加锁和解锁,且 解锁的操作尽量要放在finally代码块中,保证线程正确释放锁
4、在复杂的并发场景中使⽤在重⼊时,要却确保重复获取锁的次数必须和重复释放锁的次数⼀样,否则可能导致 其他线程⽆法获得该锁。
5、创建的时候通过传进参数true创建公平锁,如果传⼊的是false或没传参数则创建的是 ⾮公平锁
6、底层是AQS的state和FIFO队列来控制加锁
ReentrantReadWriteLock:读写锁
1、实现了ReadWriteLock,实现读写锁的分离, 2、⽀持公平和⾮公平,底层基于AQS实现 3、允许从写锁降级为读锁,流程:先获取写锁,然后获取读锁,最后释放写锁;但不能从读锁升级到写锁,4、重⼊:读锁后还可以获取读锁;获取了写锁之后既可以再次获取写锁⼜可以获取读锁 5核⼼:读锁是共享的,写锁是独占的。 读和读之间不会互斥,读和写、写和读、写和写之 间才会互斥,主要是提升了读写的性能
ReentrantLock是独占锁且可重⼊,读写均获取独占锁;ReentrantReadWriteLock读写锁分离,读锁是共享的,非常适合读多写少的情况;
CAS:Compare And Swap,即⽐较再交换;内存地址原值(V)、预期原值(A)和新值 (B);无锁,是乐观锁,比悲观锁性能好;
判断若V==A,则更新V=B,否则一直自旋重新获取内存地址原值-V,再次进行操作,会一直占用cpu;
避免ABA问题;原值与新值相同(原值为1,改成2,又改成1,此时1不是初始1,再次改成1时,就是ABA,结果一样/过程不一样);加版本号标志来解决;
AQS:AbstractQueuedSynchronizer,抽象队列同步器,先进先出队列;
state状态计数器:⽤于计数器0/1,类似gc的回收计数器; 锁线程标记:当前线程是谁加锁的; 阻塞队列:⽤于存放其他未拿到锁的线程;
CountDownLatch、ReentrantLock, Semaphore,ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于底层同步工具类AQS;ReentrantLock是独占式,Semaphore是共享式,其他是组合式;
方法acquire(int arg) 源码讲解,好⽐加锁lock操作,tryAcquire-tryRelease;
方法tryAcquire()尝试直接去获取资源;继承类一般会重写;
方法addWaiter() 根据不同模式将线程加⼊等待队列的尾部;
方法acquireQueued()使线程在等待队列中获取资源,⼀直获取到资源后才返回,如果在等待过程 中被中断,则返回true,否则返回false
方法release(int arg)源码讲解 好⽐解锁unlock,tryRelease()的返回值来判断该线程是 否已经完成释放掉资源了;
方法unparkSuccessor⽤于唤醒等待队列中下⼀个线程;
并发编程解决生产消费模型方式--原理缓存区满时不生产,空时不消费,一般采用 信号量或加锁机制:
1、wait() / notify()⽅法
2、await() / signal()⽅法,⽤ReentrantLock和Condition实现等待/通知模型
3、Semaphore信号量
4、BlockingQueue阻塞队列(先进先出,ArrayBlockingQueue-基于数组必须制定大小、LinkedBlockingQueue-基于链表、PriorityBlockingQueue-优先级、DelayQueue-延期队列,队首为过期时间最短):put到队尾,满则阻塞;take在队首取值,空则阻塞;
并发非阻塞队列-ConcurrentLinkedQueue:线程安全,基于链表,先进先出,volatile声明变量属性(有序+可见),更新通过cas保证原子性;
悲观锁:认为其他线程总会修改数据,每次操作数据时都上锁,其他线程去操作数据时就会阻塞,⽐如synchronized;-- 适合写操作多操作场景;
乐观锁:认为其他线程不会修改数据,每次更新数据时根据版本等信息判断数据是否被更新,若被其他线程更新则取消本次操作 ,⽐如CAS、数据库乐观锁;--适合读操作多场景,吞吐量高;
公平锁:多个线程按照申请顺序来获取锁,底层是先进先出队列FIFO,比如ReentrantLock(构造函数可以设置其公平性);
⾮公平锁:获取锁的⽅式是随机获取的,不保证每个线程都能获取锁,⽐如synchronized、ReentrantLock;--性能更高
可重入锁: 递归锁,外层获取锁之后,内层仍然可以获取锁,且不发生死锁;(方法A调方法B,A获取a锁,B内获取b锁);---避免来死锁
不可重⼊锁:某方法已经获取锁,方法内会因无法再次获取锁而阻塞;
⾃旋锁:若锁已被其它线程获取,则该线程循环等待并判断锁是否能够被成功获取,直到获取锁才退出循环,比如:TicketLock,CLHLock,MSCLock;--不改变状态,减少上下文切换,循环耗cpu;
共享锁:也叫S锁/读锁,并发读,不可修改和删除,锁可被多个线程所持有,⽤于资源数据共享;
互斥锁:也叫X锁/排它锁/写锁/独占锁/独享锁,锁只被一个线程占用,其他线程只能阻塞;
死锁:多个线程因 竞争资源/相互通信 而造成阻塞,无外力作用下无法执行下去;
偏向锁-jvm优化锁效率:⼀段同步代码⼀直被⼀个线程所访问,该线程会⾃动获取锁,获取锁的代价更低;
轻量级锁-jvm优化锁效率:当锁是偏向锁时,被其他线程访问,锁会升级为轻量级锁,其他线程通过⾃旋形式尝试获取锁,但不会阻塞,且性能会⾼点;
重量级锁-jvm优化锁效率:当锁是轻量级锁时,当其他线程⾃旋⼀定次数时还没有获取到锁,就会进⼊阻塞,该锁升级为重量级锁,重量级锁会让其他申请的线程进⼊阻塞,性能也会降低;
死锁四必要条件(必须都要成立才是死锁):
互斥条件:资源不能共享,只能由⼀个线程使⽤;
请求与保持条件:线程已获得⼀些资源,但因请求其他资源发⽣阻塞,对已经获得的资源保持不释放;
不可抢占:有些资源是不可强占的,当某个线程获得这个资源后,系统不能强⾏回收,只能由线程使⽤完后⾃⼰释放;
循环等待条件:多个线程形成环形链,每个都占⽤对⽅申请的下个资源;
死锁例子(非必现):AB方法互补相让,相互等待释放
1 public class DeadLockDemo { 2 private static String locka = "locka"; 3 private static String lockb = "lockb"; 4 5 public void methodA(){ 6 synchronized (locka){ 7 System.out.println("⽅法A中获锁A "+Thread.currentThread().getName() ); 8 //让出CPU执⾏权,不释放锁 9 try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } 10 synchronized(lockb){ 11 System.out.println("⽅法A中获锁B "+Thread.currentThread().getName() ); 12 } 13 } 14 } 15 public void methodB(){ 16 synchronized (lockb){ 17 System.out.println("⽅法B中获锁B "+Thread.currentThread().getName() ); 18 //让出CPU执⾏权,不释放锁 19 try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } 20 synchronized(locka){ 21 System.out.println("⽅法B中获锁A "+Thread.currentThread().getName() ); 22 } 23 } 24 } 25 26 public static void main(String [] args){ 27 System.out.println("主线程运⾏开始运⾏:"+Thread.currentThread().getName()); 28 DeadLockDemo deadLockDemo = new DeadLockDemo(); 29 for(int i=0; i<10;i++) { 30 new Thread(() -> { deadLockDemo.methodA(); }).start(); 31 new Thread(() -> { deadLockDemo.methodB(); }).start(); 32 } 33 System.out.println("主线程运⾏结束:"+Thread.currentThread().getName()); 34 35 } 36 }
解决死锁方法:调整申请锁的范围、调整申请锁的顺序
1 //方法B也进行下边类似修改,调整锁范围 2 public void methodA(){ 3 synchronized (locka){ 4 System.out.println("⽅法A中获锁A "+Thread.currentThread().getName() ); 5 //让出CPU执⾏权,不释放锁 6 try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } 7 } 8 synchronized(lockb){ 9 System.out.println("⽅法A中获锁B "+Thread.currentThread().getName() ); 10 } 11 }
不可重入锁:若当前线程某个⽅法已获取该锁,当⽅法中尝试再次获取锁时,就会获取不到被阻塞;
1 //锁类 2 public class UnreentrantLock{ 3 private boolean isLocked = false; 4 5 //判断是否已被锁,如果被锁则等待 6 public synchronized void lock() throws InterruptedException { 7 while (isLocked){ 8 System.out.println("进⼊wait等待 "+Thread.currentThread().getName()); 9 wait(); 10 } 11 //进⾏加锁 12 isLocked = true; 13 } 14 public synchronized void unlock(){ 15 isLocked = false; 16 //唤醒对象锁池⾥⾯的⼀个线程 17 notify(); 18 } 19 }
20 //执行类--A调B方法,AB方法都获取同一个锁 21 public class Main { 22 private UnreentrantLock unreentrantLock = new UnreentrantLock(); 23 24 public void methodA(){ 25 try { 26 unreentrantLock.lock(); 27 System.out.println("methodA⽅法被调⽤"); 28 methodB(); 29 }catch (InterruptedException e){ 30 e.fillInStackTrace(); 31 } finally { 32 unreentrantLock.unlock(); 33 } 34 } 35 36 public void methodB(){ 37 try { 38 unreentrantLock.lock(); 39 System.out.println("methodB⽅法被调⽤"); 40 }catch (InterruptedException e){ 41 e.fillInStackTrace(); 42 } finally { 43 unreentrantLock.unlock(); 44 } 45 } 46 47 public static void main(String [] args){ 48 //演示的是同个线程 49 new Main().methodA(); 50 } 51 }
可重⼊锁:也叫递归锁,在外层使⽤锁之后,在内层仍然可以使⽤,并且不发⽣死锁;--线程的锁 自己解
1 //锁类 2 public class ReentrantLock{ 3 private boolean isLocked = false; 4 //⽤于记录是不是重⼊的线程 5 private Thread lockedOwner = null; 6 //累计加锁次数,加锁⼀次累加1,解锁⼀次减少1 7 private int lockedCount = 0; 8 9 //判断是否同一个线程,已被锁,如果被锁则等待 10 public synchronized void lock() throws InterruptedException { 11 Thread thread = Thread.currentThread(); 12 //判断是否是同个线程获取锁, 引⽤地址的⽐较 13 while (isLocked && lockedOwner != thread ){ 14 System.out.println("进⼊wait等待 "+Thread.currentThread().getName()); 15 System.out.println("当前锁状态 isLocked = "+isLocked); 16 System.out.println("当前count数量 lockedCount = "+lockedCount); 17 wait(); 18 } 19 //进⾏加锁 20 isLocked = true; 21 lockedOwner = thread; 22 lockedCount++; 23 } 24 25 public synchronized void unlock(){ 26 Thread thread = Thread.currentThread(); 27 //线程A加的锁,只能由线程A解锁,其他线程B不能解锁 28 if(thread == this.lockedOwner){ 29 lockedCount--; 30 if(lockedCount == 0){ 31 isLocked = false; 32 lockedOwner = null; 33 //唤醒对象锁池⾥⾯的⼀个线程 34 notify(); 35 } 36 } 37 } 38 }
并发编程三要素:
原子性:一或多个操作,要么同时成功,要么同时失败,期间不能打断,不能进行上下文切换,定义变量是原子操作,运算就不是,解决方法---加锁synchronized 或 Lock(⽐如ReentrantLock)
有序性:按照代码的先后顺序执行;JVM在编译java代码或者CPU执⾏JVM字节码时进行的指令重排虽然会提高效率但是会影响到有序性;
可见行:一个线程修改共享变量后,另一个线程立即可以看到;synchronized、lock和volatile能够保证线程可⻅见性;
5、多线程实现方式
多线程场景:异步刷数据,定时任务处理数据
继承Thread:继承Thread,重写⾥⾯run⽅法,创建实例,执⾏start;
优点:代码编写简单 缺点:没返回值,没法继承其他类,拓展性差
实现Runnable接口:实现Runnable接口,实现run⽅法,创建Thread类,使⽤Runnable接⼝的实现对象 作为参数传递给Thread对象,调⽤Strat⽅法;
优点:线程类可以实现多个⼏接⼝,可以再继承⼀个类 缺点:没返回值,不能直接启动,需要通过构造⼀个Thread实例传递进去启动
1 JDK8之后采⽤lambda表达式 2 Thread thread = new Thread(()->{ 3 //要实现的逻辑 4 }); 5 thread.start();
Callable和FutureTask⽅式:实现callable接⼝,实现call⽅法,FutureTask类实例对象传入Callable对象,Thread实例对象再传入FutureTask类对象;
优点:有返回值,拓展性也⾼ 缺点:需要重写call⽅法,结合多个类⽐如FutureTask和Thread类;
线程池:ThreadPoolExecutor/Executors,设置线程池大小等信息,pool.execute(实现Runnable接口的对象);实现Runnable接⼝,实现run⽅法,创建线程池,调⽤执⾏⽅法并传⼊对象;
优点:安全⾼性能,复⽤线程 缺点: 需要结合Runnable进⾏使⽤
6、线程池
最佳实践:不同模块线程名称不同,同步代码范围尽量小,多用并发集合-ConcurrentHashMap/CopyOnWriteArrayList 少用同步集合-Hashtable/Vector,优先使用线程池;
线程池优点:线程重复使用,减少对象创建销毁的开销,有效的控制最⼤并发线程数,提⾼系统资源的使⽤率,同时避免过多资源竞争,避免堵塞,且可以定时定期执⾏、单线程、并发数控制,配置任务过,多任务后的拒绝策略等功能
类别:
newFixedThreadPool,定⻓长线程池,可控制线程最⼤并发数
newCachedThreadPool,可缓存线程池
newSingleThreadExecutor,单线程化的线程池,⽤唯⼀的⼯作线程来执⾏任务
newScheduledThreadPool,定⻓长线程池,⽀持定时/周期性任务执⾏
使用:优先使用ThreadPoolExecutor,而不是Executors:Executors也是调用ThreadPoolExecutor,传参数较多-参数、队列、策略,不易控制和掌握参数等;直接使用ThreadPoolExecutor,参数规则更清晰;比如:队列长或线程数最大都设置为了Integer.MAX_VALUE,容易导致过多堆积或线程过多;
1 public ThreadPoolExecutor(int corePoolSize, 2 int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
corePoolSize:核⼼线程数,线程池维护线程的最少数量,默认情况下核⼼线程会⼀直存活, 即使没有任务也不会受存keepAliveTime控制
注意:在刚创建线程池时线程不会⽴即创建核心数的线程,是慢慢的增加,有任务提交时才开始创建线程并逐步线程数⽬达到 corePoolSize
maximumPoolSize:线程池维护线程的最⼤数量,超过将被阻塞
注意:当核⼼线程满,且阻塞队列也满时,才会判断当前线程数是否⼩于最⼤线程数,才决定是否创建新线程
keepAliveTime:⾮核⼼线程的闲置超时时间,超过这个时间就会被回收,直到线程数量等于corePoolSize
unit:指定keepAliveTime的单位,如TimeUnit.SECONDS、TimeUnit.MILLISECONDS
workQueue:线程池中的任务队列,常⽤的是 ArrayBlockingQueue、LinkedBlockingQueue、 SynchronousQueue
threadFactory:创建新线程时使⽤的⼯⼚
handler: RejectedExecutionHandler是⼀个接⼝且只有⼀个⽅法,数量⼤于 maximumPoolSize,拒绝策略,默认有4种策略AbortPolicy抛弃、 CallerRunsPolicy归还、DiscardOldestPolicy抛弃最老、DiscardPolicy不操作