美文网首页
Java 多线程并发

Java 多线程并发

作者: 雷涛赛文 | 来源:发表于2021-07-07 17:19 被阅读0次

    一.进程和线程

    a.进程

          进程是程序的一次动态执行过程,它需要经历从代码加载、代码执行到执行完毕的一个完整过程,这个过程也是进程本身从产生,发展到最终消亡的过程。

    b.线程

          线程是比进程更小的执行单位,线程是进程的基础之上进行进一步的划分。


    image.png

    二.多线程

    a.为什么出现

          众所周知,CPU、内存、I/O设备的速度是有极大差异的,为了合理利用CPU的高性能,平衡这三者的速度差异。
          操作系统增加了进程、线程,以分时复用CPU,进而均衡CPU与I/O设备的速度差异。

    b.并发与并行
    image.png
          并发三大特性
          原子性
                是指在一个操作中就是cpu不可以在中途暂停然后再调度,既不被中断操作,要不执行完成,要不就不执行。
          可见性
                是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
          有序性
                即程序执行的顺序按照代码的先后顺序执行。
    c.调度方式

          当系统存在大量线程时,系统会通过时间片轮转的方式调度线程,因此线程不可能做到绝对的并发,处于就绪状态的线程都会进入到线程队列中等待CPU资源;
          同一时刻在队列中可能有很多个线程;
          在采用时间片的系统中,每个线程都有机会获得CPU的资源以便进行自身的线程操作;当线程使用CPU资源的时间到了后,即使线程没有完成自己的全部操作,JVM也会中断当前线程的执行,把CPU资源的使用权切换给下一个队列中等待的线程;
          被中断的线程将等待CPU资源的下一次轮回,然后从中断处(程序计数器会记录中断位置)继续执行;
          用一直观图表示如下:

    image.png
    d.调度优先级

          Java虚拟机(JVM)中的线程调度器负责管理线程,并根据以下规则进行调度:
          a.根据线程优先级(高-低),将CPU资源分配给各线程
          b.具备相同优先级的线程以轮流的方式获取CPU资源
          举例
          存在A、B、C、D四个线程,其中:A和B的优先级高于C和D(A、B同级,C、D同级)
          那么JVM将先以轮流的方式调度A、B,直到A、B线程死亡,再以轮流的方式调度C、D;

    三.wait、notify、notifyAll、synchronized的使用机制

    a.基本概念

          wait():使持有该对象的线程把该对象的控制权交出去,然后处于等待状态(这句话很重要,也就是说当调用wait的时候会释放锁并处于等待的状态);
          notify():通知某个正在等待这个对象的控制权的线程可以继续运行(这个就是获取锁,使自己的程序开始执行,最后通过notify同样去释放锁,并唤醒正在等待的线程);
          notifyAll():会通知所有等待这个对象控制权的线程继续运行(和上面一样,只不过是唤醒所有等待的线程继续执行);

    private Object obj = new Object();//必须是唯一的,不能synchronized(new Object())
    synchronized(obj) {
       while(!condition) {
           obj.wait();
       }
       obj.doSomething();
    }
    

          当线程A获得了obj锁后,发现条件condition不满足,无法继续下一处理,于是线程A就wait(),放弃对象锁,所有的java书籍都会建议开发者永远都要把wait()放到循环语句里面
          之后在另一线程B中,如果B更改了某些条件,使得线程A的condition条件满足了,就可以唤醒线程A,执行操作如下:

    synchronized(obj) {
        condition = true;
        obj.notify();
    }
    
    b.执行流程

          a.调用obj的wait()、notify()方法前,必须获得obj锁,也就是必须写在synchronized(obj) {…} 代码段内。
          b.调用obj.wait()后,线程A就释放了obj的锁,否则线程B无法获得obj锁,也就无法在synchronized(obj) {…} 代码段内唤醒A。
          c.当obj.wait()方法返回后,线程A需要再次获得obj锁[可能有多个线程在等待锁,需要一个一个来获取],才能继续执行。
          d.如果A1、A2、A3都在obj.wait(),则B调用obj.notify()只能唤醒A1、A2、A3中的一个(具体哪一个由JVM决定)。
          e.obj.notifyAll()则能全部唤醒A1、A2、A3,但是要继续执行obj.wait()的下一条语句,必须获得到obj锁才可以,因此A1、A2、A3只有一个有机会获得锁继续执行,例如A1,其余的需要等待A1释放obj锁之后才能继续执行。
          f.当B调用obj.notify()/notifyAll()的时候,B正持有obj锁,因此A1、A2、A3虽被唤醒,但是仍无法获得obj锁。直到B退出synchronized块,释放obj锁后,A1、A2、A3中的一个才有机会获得锁继续执行。

    c.锁

          锁机制存在以下问题:
          a.在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。
          b.一个线程持有锁会导致其它所有需要此锁的线程挂起。
          c.如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能风险。
          悲观锁
          对于同一个数据的并发操作,悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改;即会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁;Java中,synchronized关键字和Lock的实现类都是悲观锁。

    悲观锁.png
          乐观锁
          认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据;如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作;
    乐观锁.png
          最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现。CAS 的意思是(compare and swap),比较并交换,简单来说:从某一内存上取值V,和预期值A进行比较,如果内存值V和预期值A的结果相等,那么我们就把新值B更新到内存,如果不相等,那么就重复上述操作直到成功为止;主要解决多线程并发安全的问题。
    cas.png
          可重入锁
          也叫递归锁,指的是同一线程外层函数获得锁之后,内层递归函数仍然有获取该锁的代码,但是不受影响。ReentrantLock和synchronized都是可重入锁。
          最大作用:避免死锁

    四.线程同步&联合

    a.线程同步

          定义:当线程A使用同步方法A时,其他线程必须等到线程A使用完同步方法A后才能使用,同步方法用关键字 Synchronized 进行修饰

    public synchronized void method(){
    }
    
    b.线程联合

          定义:线程A在占有CPU资源期间,通过调用join()方法中断自身线程执行,然后运行联合它的线程B,直到线程B执行完毕后线程A再重新排队等待CPU资源,这个过程称为线程A联合线程B;线程A联合线程B,即在线程A的执行操作里定义:

    B.join();
    

          t.join()方法只会使主线程(或者说调用t.join()的线程)进入等待池,并等待t线程执行完毕后才会被唤醒,并不影响同一时刻处在运行状态下的其他线程。[实际上主线程(或调用t.join()的线程)wait(),但是源码中未调用Notify()来进行唤醒,是因为线程在die时会调用notifyAll()]。

    五.线程安全与不安全

    a.线程安全

          多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程访问完毕,其他线程才可使用,不会出现数据不一致或者数据污染;
          互斥同步
          1.synchhronized
          在多线程并发编程中synchronized一直是来解决线程安全问题,它可以保证原子性、可见性以及有序性;
          synchronized有三种方式来加锁,分别是:方法锁synchronized method()、对象锁synchronized(this)、类锁synchronized(test.class)。
          ①.修饰实例方法:作用于当前实例加锁,进入同步代码前要获得当前实例的锁;
          ②.静态方法:作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁;
          ③.修饰代码块:指定加锁对象,对给定对象加锁,进入同步代码前获得给定对象的锁;
          synchhronized不足
          ①.效率低:锁的释放情况少,只有代码执行完毕后或者异常结束才会释放锁,试图获取锁的时候不能设定超时,不能中断一个正在使用锁的线程,相对而言Lock可以中断和设置超时;
          ②.不够灵活,加锁和释放的时机单一,每个锁仅有一个单一的条件(某个对象),相对而言,读写锁更加灵活;
          ③.无法知道能否成功获得锁,相对而言,Lock可以拿到状态,获取成功或失败;
          2.ReentrantLock
          ①.加锁和解锁的过程需要手动进行;
          ②.可以响应中断;
          ③.公平锁:多个线程等待一个锁时,必须按照申请锁的时间顺序获得锁,Synchronized锁非公平锁,ReentrantLock默认的构造函数是创建的非公平锁,可以通过设置参数true设置为公平锁,但是公平锁表现的性能不是很好;
          ④.锁绑定多个条件,一个ReentrantLock对象可以同时绑定多个对象,ReentrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像Synchronized要么随机唤醒一个线程,要么唤醒全部线程;

    b.线程不安全

          不提供数据访问保护,有可能出现多个线程先后更改数据造成得到的数据是脏数据;

    六 .JMM:Java内存模型

    image.png

          Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程中是用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。
          不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。
          Java中的volatile关键字提供了一个功能,那就是被其修饰的变量在被修改后可以立即同步到主内存,被其修饰的变量在每次是用之前都从主内存刷新。因此,可以使用volatile来保证多线程操作时变量的可见性。

    volatile可见性实现

          基于内存屏障(Memory Barrier)实现,内存屏障,又称内存栅栏,是一个CPU指令。
          在程序运行时,为了提高执行性能,编译器和处理器会对指令进行重排序,JMM为了保证在不同的编译器和CPU上有相同的结果,通过插入特定类型的内存屏障来禁止特定类型的编译器重排序和处理器重排序,插入一条内存屏障会告诉编译器和CPU:不管什么指令都不能和这条Memory Barrier指令重排序。

    synchronized 和volatile 关键字的区别

          1.volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住;
          2.volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的;
          3.volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性;
          4.volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞;
          5.volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化;

    相关文章

      网友评论

          本文标题:Java 多线程并发

          本文链接:https://www.haomeiwen.com/subject/wcgoultx.html