Java多线程基础

来源:互联网 发布:mysql 间隙锁 编辑:程序博客网 时间:2024/06/05 15:09

java内存模型

java内存模型用来屏蔽掉各种硬件和操作系统的内存访问差异,实现java程序在各种平台下都能达到一致的内存访问效果

线程 各自的工作内存 主内存 java线程A 工作内存a 主内存中存放共享变量 java线程B 工作内存b 包括了实例字段,静态字段和构成数组对象的元素,不包括局部变量、方法参数、异常处理器参数 java线程C 工作内存c 这里的主内存只是虚拟机的一部分,主要对应于java堆中的对象实例数据部分。工作内存对应于虚拟机栈部分,还可能涵盖缓存、写缓冲区、寄存器等等

线程可以通过主内存来进行通信

而内存间交互操作,有下面8种,虚拟机实现时会保证下面的操作都是原子的,不可再分的。

  • lock:锁定主内存的变量。如果一个变量被lock,会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。
  • unlock:把主内存变量解锁
  • read:读取主内存变量
  • load:把read到的主内存变量放入到工作内存的变量副本
  • use:把工作内存中一个变量的值传递给执行引擎
  • assign:把一个从执行引擎接收到的值赋给工作内存的变量
  • store:把工作内存中一个变量的值传送到主内存中
  • write:把store操作从工作内存汇总得到的变量的值放入主内存的变量中
    java内存模型(JMM)还规定了在执行上述8种基本操作时必须满足的规则,来确保执行的正确性。

原子操作

  • 处理器如何实现原子操作
    • 使用总线锁保证原子性
    • 使用缓存锁保证原子性
  • java中实现原子操作
    • 使用循环CAS实现原子操作,java的CAS操作会使用现代处理器上提供的高效机器级别的原子指令。
      CAS存在的问题
      • ABA问题
      • 循环时间开销长
      • 只能保证一个共享变量的原子操作
    • 使用锁机制 (JVM实现锁机制也都使用了循环CAS)

程序执行的顺序

在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序,大致有三种
1)编译器优化的重排序
编译器重新安排语句的执行顺序
2)指令级并行重排序
现代处理器采用了指令级并行技术来将多条指令重叠执行。
3)内存系统的重排序
现代处理器使用写缓冲区临时保存向内存写入的数据,以提高效率,这样会找出指令执行效果上的重排序。
为了保证内存可见性,java编译器在生成指令序列的适当位置会掺入内存屏障指令来禁止特定类型的处理器重排序。JMM把内存屏障指令分为4类。LoadLoad Barriers StoreStoreBarriers LoadStore Barriers StoreLoad Barriers。

所以我们在程序中看到的执行顺序不一定是实际上的执行顺序,heppens-before原则(现行发生原则)与as-if-serial语义保证我们能够正确写出按序执行的程序,保证了满足的规则的指令能够影响到在它之后执行的指令。

数据依赖性

控制依赖与猜测执行

as-if-serial

  • 不管怎么重排序,单线程程序的执行结果不能被改变
  • 在单线程中对存在控制依赖的操作重排序,不会改变执行结果;但在多线程程序中,对存在控制依赖的操作重排序,可能会改变程序的执行结果。

heppens-before

来确定哪些操作可以影响到那些操作
- 程序顺序原则
- 管程锁定规则 :一个unlock操作先行发生于后面对同一个锁的lock操作。后面是指时间上的先后
- volatile变量规则 :对一个volatile变量的写先行发生于后面对这个变量的读操作。后面是指时间上的先后
- 线程启动规则
- 线程终止规则
- 线程中断规则
- 对象终结规则
- 传递性
时间的先后顺序与先行发生原则之间基本没有太大关系,happens-before仅仅要求前一个操作(执行的结果)对后一个可见。前一个操作按顺序排在第二个操作之前。
- 对于会改变程序结果的重排序,JMM要求编译器和处理器必须禁止;不会改变程序的结果的,会被允许。

volatile

  • volatile的特性
    • 保证此变量对所有线程可见(对声明了volatile的变量进行写操作,jvm就会向处理器发送一条lock前缀的指令)
    • 禁止指令重排序(通过内存屏障)
    • 每次使用volatile变量前,必须先从主内存刷新最新的值
    • 每次修改后必须立刻同步会主内存中
  • volatile变量与普通变量的比较
    • 对于普通变量,线程A修改了一个普通变量的值,然后向主内存进行回写,另一条线程B在线程A回写完成后,再从主内存进行读取操作,新变量的值才会对线程B可见。
    • 而写一个volatile的变量的时候 ,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存;在读的时候,会把该线程对应的本地内存置为无效,线程接下来从主内存中读取共享变量。
  • volatile内存语义的实现:插入内存屏障,禁止重排序。

synchronized

  • synchronized的特性
    • 让临界区互斥执行
    • 让释放锁的线程向获取同一个锁的线程发送消息
  • synchronized的内存语义
    • 线程释放一个锁,JMM会把线程对应的本地内存中的共享变量刷新到主内存中,线程向接下来要获取这个锁的某个线程发出修改了共享变量的消息
      • 线程获取一个锁,JMM会把该线程对应的本地内存置为无效,从而使得被监视器保护的临界区代码必须从主内存中读取共享变量;也意味着该线程接收了之前某个线程发出的消息。
  • 实现:volatile和CAS 。JVM基于进入和退出Monitor对象来实现方法同步和代码块同步。Synchronized用的锁存在Java对象头中。
  • 锁的升级:偏向锁(对象头的Mark word存储着指向当前线程的偏向锁)–>轻量级锁(在执行同步块之前,JVM会先在当前线程的栈帧中创建用于存储锁记录的空间,将对象头中的Mark Word复制到锁记录中,然后尝试使用CAS将对象头中的Mark Word替换为指向记录的指针。如果成功,当前线程获得锁;如果失败,表示其他线程竞争锁,当前线程尝试自旋来获取锁)–>重量级锁
锁 优点 缺点 适用场景 偏向锁 加锁和解锁不需要额外的消耗 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 适用于单个线程 轻量级锁 竞争的线程不会阻塞,提高了程序的响应速度 如果始终得不到锁竞争的线程,使用自旋会消耗CPU资源 追求响应时间 重量级锁 线程竞争不适用自旋,不会消耗CPU 线程阻塞,响应时间缓慢 追求吞吐量,同步块执行时间较长

final

原创粉丝点击