美文网首页
多线程知识点总结(2)

多线程知识点总结(2)

作者: 天渊hyominnLover | 来源:发表于2018-08-15 10:35 被阅读7次

    多线程到底好不好?

    如果任务耗时短,线程太多会造成上下文切换时间超过任务执行时间
    任务耗时长的话,看任务类型:
    CPU密集型任务:线程太多的话会造成占用过多的CPU资源进行上下文切换
    IO密集型任务:线程多一些更好,可以更充分利用CPU资源完成耗时的IO操作

    简述上下文切换

    上下文切换,有时也称做进程切换或任务切换,是指CPU从一个进程或线程切换到另一个进程或线程,包括保存当前任务运行环境及恢复将要运行任务的运行环境。上下文切换需要消耗大量的CPU时间

    三种情况下会发生上下文切换:中断处理(锁竞争)、多任务处理(时间片切换)、用户态切换

    上下文切换分为两种:

    • 让步式上下文切换:多线程环境下产生锁竞争,线程主动释放CPU,可通过减少锁竞争来避免

    • 抢占式上下文切换:线程分配的时间片用尽而被迫放弃CPU或被其他优先级更高的线程抢占,可通过适当减少线程数来避免

    • 为何synchronized会影响程序运行性能:synchronized是一种悲观锁,未获得锁的线程需要进入阻塞状态,待获得锁后再进入就绪状态开始执行;java程序多线程运行环境下,频繁加锁解锁(从Blocked到Runnable以及从Running到Blocked)会导致额外的线程上下文切换,因为java线程是基于操作系统内核线程实现的,所以如果阻塞或者唤醒线程都需要切换到内核态操作,这需要耗费许多CPU时钟

    • 如何减少多线程环境下的上下文切换:无锁并发(数据id根据Hash分段)、CAS、最小线程、协程
      无锁并发编程:为了避免在多线程环境下使用锁,将数据id根绝hash算法取模来分段,不同线程处理不同段数据
      CAS:当某个变量面临线程间共享时,使用Atomic包中的变量,这些变量使用CPU的CAS算法来更新数据,可以实现数据更新的原子性和可见性,不需要加锁
      最小线程:任务少就不要创建过多线程
      协程:在单线程中实现多任务调度和切换(kilim框架)

    synchronized锁原理及优化措施

    原理:该关键字编译后,在同步块或同步方法前后加上monitorenter和monitorexit两个字节码,包含一个对应的锁定和解锁对象的引用;如果是同步方法,则该引用指向Class对象;synchronized可以同时保证操作的原子性、可见性、有序性

    锁优化:
    如果实在不得不加锁保证原子性的情况下,那可以通过以下手段对加锁解锁的性能耗费进行优化

    • 自旋锁(spin lock):
      1)不同于互斥锁,线程访问被占有的共享资源时,不会被挂起,而是进行自旋并查看锁持有者是否已经释放了锁,避免了额外的上下文切换
      2)缺点是如果不能尽快获得锁,长时间自旋也会降低CPU效率,并且递归调用时容易造成死锁(某个申请了自旋锁的线程进行了递归操作,在没释放该锁之前再次申请了这个锁,那其他线程永远无法获得这个锁)

    • 锁消除:代码上要求同步,但是虚拟机在进行JIT编译时,通过上下文扫描检测到不存在共享数据竞争的情况下(例如同步方法的局部变量,不会“逃逸”到方法之外,那这些变量就不是线程共享的),会进行锁消除,避免不必要的申请锁和解锁的资源耗费

    • 锁粗化:如果一系列的连续操作都对同一个对象反复加锁和解锁,即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗,所以JVM探测到存在这种情况将会把加锁同步的范围扩展到整个操作序列外部,避免了频繁加锁解锁带来的性能损耗

    • 轻量级锁:轻量级锁的实现原理也是JVM中的CAS操作(compare and swap),在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗,具体原理如下:

    1) 一个或多个线程执行同一个同步体并访问共享变量a前,JVM会在每个线程的虚拟机栈中创建锁记录(lockRecord),并将a的Markword拷贝到这个lockRecord中

    2)虚拟机将尝试使用CAS方式将a对象的Marckword替换为指向该线程的虚拟机栈中的lockRecord的指针

    3)如果这个CAS操作成功了(a对象的Markword还未被改变),则当前线程顺利获取到锁并执行同步体

    4)如果这个CAS操作失败了(a对象的Markword已经被改变为其他线程的lockRecord指针),那就说明有其他线程占有了该共享变量,那么这个线程就会不停循环使用CAS操作将Markword替换为自己的lockRecord指针

    5)这个循环有次数限制,循环结束前若成功执行了CAS操作,则锁获取成功;循环结束依旧无法执行CAS操作,那么a对象的Markword中的锁标志位将会变为重量级锁(10),膨胀为重量级锁,这些个获取失败的线程将会阻塞

    6)当占有a对象锁的线程释放锁后,会发现锁已经膨胀为重量级锁,该线程就会唤起阻塞中的其他线程,开始新一轮夺锁之争

    7)轻量级锁膨胀为重量级锁过程不可逆

    8)设置轻量级锁的目的是防止在线程竞争不激烈的时候进行额外的加锁和解锁消耗,某种程度上提高了CPU执行性能,
    详细的还得参阅《深入理解JAVA虚拟机》

    (关于Marckword:虚拟机将对象头(object header)分为“Mark word”和对象类型指针,其中“Mark Word”用于存储Hashcode、GC分代年龄(Generation GC Age)和锁标志位(未锁定时为01,轻量级锁定为00,膨胀为重量级锁后为10),
    轻量级锁适用于线程竞争不激烈的场景

    • 偏向锁:如果完全没有线程竞争,维护轻量级锁就很浪费资源。偏向锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步

    实现原理:
    第一个申请锁的线程会使用锁,在MarkWord中进行CAS操作记录owner(本质上也是更新,但初始值为空),如果记录成功,则偏向锁获取成功,记录锁状态为偏向锁,以后当前线程等于owner就可以零成本的直接获得锁;若CAS操作失败,说明有其他线程竞争,膨胀为轻量级锁。

    JDK1.6后默认开启偏向锁和轻量级锁,可通过-XX:+UseHeavyMonitors命令关闭,强制启用重量级锁,启用偏向锁使用-XX:+UseBiasedLocking命令,偏向锁适用于几乎没有线程竞争的场景

    Java中的线程Thread方法:suspend()和resume()

    suspend()方法将线程暂停,resume()方法将这个线程重新启动,目前这两个方法已经不推荐使用

    相关文章

      网友评论

          本文标题:多线程知识点总结(2)

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