zoukankan      html  css  js  c++  java
  • (五)并发编程与锁机制

    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不操作

  • 相关阅读:
    模拟测试20190806
    替罪羊树学习日记
    [Usaco2015 Jan]Moovie Mooving
    [NOIP2016]愤怒的小鸟
    [BZOJ1556]墓地秘密
    [SDOI2009]学校食堂Dining
    [SCOI2008]奖励关
    [洛谷3930]SAC E#1
    [BZOJ2809/APIO2012]dispatching
    [Usaco2018 Open]Disruption
  • 原文地址:https://www.cnblogs.com/huasky/p/14735425.html
Copyright © 2011-2022 走看看