• 并发编程之线程第二篇


    3.12 五种状态

    这是从操作系统层面来描述的
    在这里插入图片描述

    • 【初始状态】仅是在语音层面创建了线程对象,还未与操作系统线程关联
    • 【可运行状态】(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由CPU调度执行
    • 【运行状态】指获取了CPU时间片运行中的状态
      (1)当CPU时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换
    • 【阻塞状态】
      (1)如果调用了阻塞API,如BIO读写文件,这时该线程实际不会用到CPU,会导致线程上下文切换,进入【阻塞状态】
      (2)等BIO操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】
      (3)与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑调度它们
    • 【终止状态】表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态

    3.13 六种状态

    这是从Java API层面来描述的
    根据Thread.State枚举,分为六种状态
    在这里插入图片描述

    • NEW 线程刚被创建,但是还没有调用start()方法
    • RUNNABLE 当调用了start()方法之后,注意,Java API层面的RUNNABLE状态涵盖了操作系统层面的【可运行状态】、【运行状态】和【阻塞状态】(由于BIO导致的线程阻塞,在Java里无法区分,任然认为是可运行)
    • BLOCKED、WAITING、TIMED_WAITING都是Java API层面对【阻塞状态】的细分。
    • TERMINATED当线程代码运行结束

    4.1 共享带来的问题

    Java的体现
    两个线程对初始值为0的静态变量一个做自增,一个做自减,各做5000次,结果是0吗?
    在这里插入图片描述
    问题分析
    y以上的结果可能是正数、负数、零。为什么呢?因为Java中对静态变量的自增,自减并不是原子操作,要彻底理解,必须从字节码来进行分析
    例如对于 i++而言(i为静态变量),实际会产生如下的JVM字节码指令 :
    getstatic i // 获取静态变量i的值
    iconst_1 // 准备常量1
    iadd // 自增
    putstatic i // 将修改后的值存入静态变量i
    而对应 i-- 也是类似
    getstatic i // 获取静态变量i的值
    iconst_1 // 准备常量1
    isub // 自减
    putstatic i // 将修改后的值存入静态变量i
    而Java的内存模型如下,完成静态变量的自增,自减需要在主存和工作内存中进行数据交换 :
    在这里插入图片描述
    如果是单线程以上8行代码是顺序执行(不会交错)没有问题 :
    在这里插入图片描述
    但多线程下这8行代码可能交错运行 :
    出现负数的情况 :
    在这里插入图片描述
    出现正数的情况
    在这里插入图片描述
    临界区Critical Section

    • 一个程序运行多个线程本身是没有问题的
    • 问题出在多个线程访问共享资源
      • 多个线程读共享资源其实也没有问题
      • 在多个线程对共享资源读写操作时发生指令交错,就会出现问题
    • 一段代码块如果存在对共享资源的多线程读写操作,称这段代码块为临界区
      例如,下面代码中临界区
      在这里插入图片描述
      竞态条件 Race Condition
      多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件

    4.2 synchronized解决方案

    • 应用之互斥
      为了避免临界区的竞态条件发生,有多种手段可以达到目的。
    • 阻塞式的解决方案 : synchronized,Lock
    • 非阻塞式的解决方案 : 原子变量
      本次使用阻塞式的解决方案 : synchronized,来解决上述问题,即俗称的【对象锁】,它采用互斥的方式让同一个时刻最多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换
      注意
      虽然java中互斥和同步都可以采用synchronized关键字来完成,但它们还是有区别的 :
    • 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
    • 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点
      synchronized
      语法
      在这里插入图片描述
      解决
      在这里插入图片描述
      在这里插入图片描述
      所谓的线程八锁
      其实就是考察synchronized锁住的是哪个对象
      情况1 :

    在这里插入图片描述
    用图来解释
    在这里插入图片描述
    在这里插入图片描述
    思考
    synchronized实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断。
    为了加深理解,请思考下面的问题

    • 如果把synchronized(obj)放在for循环的外面,如何理解?-- 原子性
    • 如果t1 synchronized(obj1) 而 t2 synchronized(obj2)会怎样运作? – 锁对象
    • 如果t1 synchronized(obj) 而t2 没有加会怎么样?如何理解? – 锁对象
      面向对象改进
      把需要保护的共享变量放入一个类
      在这里插入图片描述

    4.4 变量的线程安全分析

    成员变量和静态变量是否线程安全?

    • 如果它们没有共享,则线程安全
    • 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
      • 如果只有读操作,则线程安全
      • 如果有读写操作,则这段代码是临界区,需要考虑线程安全
        局部变量是否线程安全?
    • 局部变量是线程安全的
    • 但局部变量引用的对象则未必
      • 如果该对象没有逃离方法的作用访问,它是线程安全的
      • 如果该对象逃离方法的作用范围,需要考虑线程安全
        局部变量线程安全分析
        在这里插入图片描述
        每个线程调用test1()方法时局部变量i,会在每个线程的栈帧内存中被创建多份,因此不存在共享
        在这里插入图片描述
        如图
        在这里插入图片描述
        局部变量的引用稍有不同
        先看一个成员变量的例子
        在这里插入图片描述
        在这里插入图片描述
        其中一种情况是,如果线程2还未add,线程1remove就会报错 :
        在这里插入图片描述
        分析 :
    • 无论哪个线程中的method2引用的都是同一个对象中的list成员变量
    • method3与method2分析相同
      在这里插入图片描述
      将list修改成局部变量
      在这里插入图片描述
      分析 :
    • list是局部变量,每个线程调用时会创建其不同实例,没有共享
    • 而method2的蚕食是从method1中传递过来的,与method1中引用同一个对象
    • method3的参数分析与method2相同
      在这里插入图片描述
      方法访问修饰符带来的思考,如果把method2和method3的方法修改为public会不会代理线程安全问题?
    • 情况1 :有其它线程调用method2和method3
    • 情况2 :在情况1的基础上,为ThreadSafe类添加子类,子类覆盖method2或method3方法。
      ![在这里插入图片描述](https://img-blog.csdnimg.cn/2020020816151116.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3poYW8xMjk5MDAyNzg4,size_16,color_FFFFFF,t_7
      从这里例子中可以看出private或final提高【安全】的意义所在,请体会开闭原则中的【闭】
      常见线程安全类
    • String
    • Integer
    • StringBuffer
    • Random
    • Vector
    • HashTable
    • java.util.concurrent包下的类
      这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。也可以理解为
    • 它们的每个方法是原子的
    • 但注意它们多个方法的组合不是原子的,见后面分析
      线程安全类方法的组合
      分析下面代码是否线程安全?
      在这里插入图片描述
      在这里插入图片描述
      不可变类线程安全性
      String、Integer等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的。
      在这里插入图片描述
      在这里插入图片描述
      其中foo的行为是不确定的,可能导致不安全的发生,被称之为外星方法
      在这里插入图片描述
      请比较JDK中String类的实现,为什么是final修饰的?因为防止子类去实现,这样会引起线程安全问题。也是符合开闭原则

    4.6 Monitor概念

    Java对象头
    以32位虚拟机为例
    普通对象
    在这里插入图片描述
    数组对象
    在这里插入图片描述
    其中Mark Word结构为
    在这里插入图片描述
    Monitor
    Monitor被翻译为监视器或管程
    每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁(重量级)之后,该对象头的Mark Word中就被设置指向Monitor对象的指针
    Monitor结构如下
    在这里插入图片描述

    • 刚开始Monitor中Owner为null
    • 当Thread-2指向synchronized(obj)就会将Monitor的所有者Owner置为Thread-2,Monitor中只能有一个Owner
    • 在Thread-2上锁的过程中,如果Thread-3,Thread-4,Thread-5也来执行synchronized(obj),就会进入EntryList BLOCKED
    • Thread-2执行完同步代码块的内容,然后唤醒EntryList中等待的线程来竞争锁,竞争的时是非公平的
    • 图中WaitSet中的Thread-0,Thread-1是之前获得过锁,但条件不满足进入WAITING状态的线程,后面讲wait-notify时会分析
      注意 :
      • synchronized必须是进入同一个对象的monitor才有上述的效果
      • 不加synchronized的对象不会关联监视器,不遵从以上规则
        原理之synchronized
        在这里插入图片描述
        对应的字节码
        在这里插入图片描述

    1. 轻量级锁

    轻量级锁的使用场景 :如果一个对象虽然有多线程访问,但多线程访问的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化
    轻量级锁来优化。
    轻量级锁对使用者是透明的,即语法任然是synchronized
    假设有两个方法同步块,利用同一个对象加锁
    在这里插入图片描述

    • 创建锁记录(Lock Record)对象,每个线程都的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word
      在这里插入图片描述
    • 让锁记录中Object reference指向锁对象,并尝试用cas替换Object的Mark Word,将Mark Word的值存入锁记录
      在这里插入图片描述
    • 如果cas替换成功,对象头中存储了锁记录地址和状态 00,表示由该线程给对象加锁,这时图示如下
      在这里插入图片描述
    • 如果cas失败,有两种情况
      • 如果是其他线程已经持有了该Object的轻量级锁,这时表面有竞争,进入膨胀过程
      • 如果是自己执行了synchronized锁重入,那么再添加一条Lock Record作为重入的计数
        在这里插入图片描述
    • 当退出synchronized代码块(解锁时)如果有取值为null的锁记录,表示有重入,这时重置锁记录,表示重入计数减一
      在这里插入图片描述
    • 当退出synchronized代码块(解锁时)锁记录的值不为null,这时使用cas将Mark Word的值恢复给对象头
      • 成功,则解锁成功
      • 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程

    2. 锁膨胀

    如果在尝试加轻量级锁的过程中,CAS操作无法成功,这时一种情况就是有其他线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。
    在这里插入图片描述

    • 当Thread-1进行轻量级加锁时,Thread-0已结对该对象加了轻量级锁
      在这里插入图片描述
    • 这时Thread-1加轻量级锁失败,进入锁膨胀流程
      • 即为Object对象申请Monitor锁,让Object指向重量级锁地址
      • 然后自己进入Monitor的EntryList BLOCKED
        在这里插入图片描述
    • 当Thread-0退出同步块解锁时,使用cas将Mark Word的值恢复给对象头,失败。这时会进入重量级解锁流程,即 按照Monitor地址找到Monitor对象,设置Owner为null,唤醒EntryList中BLOCKED线程。
  • 相关阅读:
    关于自动分裂的思考 | Solrex 杨文博的博客,记录我的生活、技术、思想和梦想
    在STL中,map按值来排序的实现方法_永不言弃是生命的基调!_百度空间
    C/C++学习路线(教材推荐)_Hello World!_百度空间
    Google C++ Style中允许使用的Boost库(1) 程序即人生 博客频道 CSDN.NET
    STL中map按值(value)排序
    程序即人生 » 移动平台现在可用的C++ 11特性
    开发者应该了解的 12 款 Eclipse 插件 编程语言 ITeye资讯
    Lisp的给力特性(V.S. Python3) 第二篇 程序即人生 博客频道 CSDN.NET
    Solidot | 地球上有多少Java程序员?
    在STL中,map按值来排序的实现方法
  • 原文地址:https://www.cnblogs.com/haizai/p/12285404.html
Copyright © 2020-2023  润新知