• Threads and Locks


    1.1同步(synchronization)

    Java中多线程之间通信机制最基本的就是使用同步,其使用监视器(monitor)实现,java中每个对象都关联了一个监视器,线程可以对其进行加锁和解锁操作

    如果被 synchronized 修饰的方法是实例方法,那么这个实例的监视器会被锁定。如果是 static 方法,线程会锁住相应的 Class 对象的监视器。方法体执行完成或者异常退出后,会自动执行解锁操作。

    对 Class 对象加锁、对对象加锁,它们之间不构成同步, 但是同一个类中的两个synchronized static 方法构成同步,即调用任意方法(A)后需等该方法执行完成才能调用其他方法(B),因为static方法是单实例的,A持有的是Class锁,Class锁可以对类的所有对象实例起作用;

    1.2 等待集合 和 唤醒(Wait Sets and Notification)

    每个JAVA对象都关联了一个监视器也关联了一个等待集合,等待集合是一个线程集合。

    1.2.1 等待(Wait)

    如果wait方法没有抛出InterruptedException 异常则表示正常返回

    ep:线程 t 中对对象 m 调用 m.wait() 方法,n代表加锁编号(非IllegalMonitorStateException没有持有m的锁 ,IllegalArgumentException wait 方法参数错了,IllegalArgumentException 中断 )

    1.t会加入到对象m的等待集合中,执行加锁编号n对应的解锁操作,要注意wait方法的调用必须是线程获取到了对象的监视器锁,到这步进行解锁操作;

    2.线程t不会执行任何进一步的指令,直到它从m的等待集合中移出(即等待唤醒),然后再之后的某个时间点恢复(线程不是移出等待队列就马上往下执行,还需要重新获取锁),并继续执行之后的指令;

    1.2.2 通知(Notification)

    通知操作发生于调用 notify 和 notifyAll 方法 ,从等待集合中移出线程

    1.2.3中断(Interruptions)

    中断发生于 Thread.interrupt 方法的调用;

    实例方法 thread.isInterrupted() 可以知道线程的中断状态;

    调用静态方法 Thread.interrupted() 可以返回当前线程的中断状态,同时将中断状态设置为false;

    ep:如果有一个对象 m,而且线程 u 此时在 m 的等待集合中,那么 u 将会从 m 的等待集合中移出。这会让 u 从 wait 操作中恢复过来,u 此时需要获取 m 的监视器锁,获取完锁以后,发现线程 u 处于中断状态,此时会抛出 InterruptedException 异常

    流程:t 设置 u 的中断状态 => u 线程恢复 => u 获取 m 的监视器锁 => 获取锁以后,抛出 InterruptedException 异常

    1.2.4 等待、通知和中断的交互(Interactions of Waits, Notification, and Interruption)

    wait方法会把当前线程加入到对象的等待队列中,执行notify且线程被选中从等待集合中移除/notifyAll等待集合中所有线程会被移除(这些线程恢复后,只有一个线程可以锁住监视器 ),interrupt中断操作,等待时间,JVM假唤醒都能实现将线程从等待集合中移除

    1.3 休眠和礼让(Sleep and Yield)

    yield 告诉操作系统的调度器:我的cpu可以先让给其他线程。注意,调度器可以不理会这个信息;

    休眠期间,线程不会释放任何的监视器锁。在Thread.sleep 和 Thread.yield 方法调用之前,不要求虚拟机将寄存器中的缓存刷出到共享内存中,同时也不要求虚拟机在这两个方法调用之后,重新从共享内存中读取数据到缓存

    ep:如下代码块this.done 定义为一个 non-volatile 的属性,初始值为 false

    while (!this.done)
        Thread.sleep(1000);
    
    • 1
    • 2

    编译器可以只读取一次 this.done 到缓存中,然后一直使用缓存中的值,也就是说,这个循环可能永远不会结束,即使是有其他线程将 this.done 的值修改为 true。

    1.4内存模型(Memory Model)

    “线程内语义” :在单线程中,我们可以通过一行一行看代码来预测执行结果的,只不过,代码中使用到的读取内存的值我们是不能确定的,这取决于在内存模型这个大框架下,我们的程序会读到的值。也许是最新的值,也许是过时的值

    1.4.1 共享变量(Shared Variables)

    所有线程都可以访问到的内存称为共享内存堆内存

    所有的实例属性,静态属性,还有数组的元素都存储在堆内存中。在本章中,我们用术语变量来表示这些元素。

    局部变量、方法参数、异常对象,它们不会在线程间共享,也不会受到内存模型定义的任何影响。

    两个线程对同一个变量同时进行读-写操作写-写操作,我们称之为“冲突”。

    1.4.2 操作(Actions)

    线程间操作:由一个线程执行的动作,可以被另一个线程检测到或直接影响到 ;

    外部操作: 可能被观察到的在外部执行的操作,同时它的执行结果受外部环境控制,可以理解为Java调用了一个native方法,Java可以得到这个方法的返回值,但对于具体的执行不感兴趣,意味着Java不能对这种语句进行重排序,因为Java无法知道方法体会执行哪些指令;

    线程分歧操作(§17.4.9):此操作只由处于无限循环的线程执行,在该循环中不执行任何内存操作、同步操作、或外部操作。如果一个线程执行了分歧操作,那么其后将跟着无数的线程分歧操作。

    1.4.3 程序和程序顺序(Programs and Program Order)

    如果所有操作的执行顺序和代码中的顺序一致,那么一组操作就是连续一致的

    连续一致性:对于可见性和程序执行顺序是一个非常强的保证。在这种场景下,所有的单个操作(比如读和写)构成一个统一的执行顺序,这个执行顺序和代码出现的顺序是一致的,同时每个单个操作都是原子的,且对所有线程来说立即可见。如果程序没有任何的数据竞争,那么程序的所有执行操作将表现为连续一致。

    连续一致性的核心在于每一步的操作都是原子的,同时对于所有线程都是可见的,而且不存在重排序。所以,Java 语言定义的内存模型肯定不会采用这种策略,因为它限制了编译器和 JVM 的各种优化措施。

    注意:很多地方所说的顺序一致性就是这里的连续一致性,英文是 Sequential consistency

    1.4.4 同步顺序(Synchronization Order)

    同步顺序可以理解为对某资源的释放先于其他操作对同一资源的获取

    1.4.5 Happens-before顺序(Happens-before Order)

    当程序出现两个没有 happens-before 关系的操作对同一数据进行访问时,我们称之为程序中有数据竞争

    1.5 final

    用 final 修饰的类不可以被继承,用 final 修饰的方法不可以被覆写,用 final 修饰的属性一旦初始化以后不可以被修改。

    对象只有在构造方法结束了才被认为完全初始化了。如果一个对象完全初始化以后,一个线程持有该对象的引用,那么这个线程一定可以看到正确初始化的 final 属性的值。

    这个隐含了,如果属性值不是 final 的,那就不能保证一定可以看到正确初始化的值,可能看到初始零值。

    1.6 字分裂(Word Tearing)

    字分裂(word tearing)即你更新一个字段,数组中的一个元素,会影响到另一个字段,数组中的另一个元素的值。例如处理器没有提供写单个 byte 的功能,假设最小维度是 int,在这样的处理器上更新 byte 数组,若只是简单地读取 byte 所在的整个 int,更新对应的 byte,然后将整个 int 再写回,这种做法是有问题的。

    实现 Java 虚拟机需要考虑的一件事情是,每个对象属性以及数组元素之间是独立的,更新一个属性或元素不能影响其他属性或元素的读取与更新。尤其是,两个线程在分别更新 byte 数组相邻的元素时,不能互相影响与干扰,且不需要同步来保证连续一致性。

    1.7 double 和 long 的非原子处理 (Non-Atomic Treatment of double and long)

    Java内存模型要求lock、unlock、read、load、assign、use、store、write这8个操作都具有原子性,但是对于64位的数据类型(double、long)定义了相对宽松的规定:允许虚拟机将没有被volatile修饰的64位数据的读写操作划分为两次的32位操作来进行,即允许虚拟机可以不保证64位数据类型的load、store、read和write操作的原子性。

    1.7.1 非原子性协定可能导致的问题

    如果有多个线程共享一个未申明为volatile的long或double类型的变量,并且同时对其进行读取和修改操作,就有可能会有线程读取到"半个变量"的数值或者是一半正确一半错误的失效数据。

    1.7.2 在实际应用中的解决

    因为上述可能造成的问题,势必在对long和double类型变量操作时要加上volatile关键字,实际上如下:

    1、64位的java虚拟机不存在这个问题,可以操作64位的数据

    2、目前商用JVM基本上都会将64位数据的操作作为原子操作实现

    所以我们编写代码时一般不需要将long和double变量专门申明为volatile

  • 相关阅读:
    系统升级丨VR会议主动呼叫,开启云洽谈新模式
    微信小程序中使用wx.showToast()进行界面交互
    c++11~c++20 -05-thread_local
    Label Assignment
    Java高级工程师常见面试题(答案)
    HTML中Input elements should have autocomplete attributes的解决方案
    邮件归档系统
    ✔ ★【备战实习(面经+项目+算法)】 10.13学习时间表
    Web渗透_手动漏洞挖掘
    基于NSQ搭建高可用分布式消息队列
  • 原文地址:https://blog.csdn.net/jingli456/article/details/125887476