Java锁与并发编程


Java并发编程

并发编程三大概念

原子性

原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。Java中,只有简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作。

x = 10;         //语句1
y = x;         //语句2
x++;           //语句3
x = x +1;      //语句4

只有1是原子性操作,其他的都涉及到读内存和写内存至少两个步骤

Java内存模型只保证了基本读取和赋值是原子性操作

通过synchronized和Lock可以获得更大范围的原子性操作。当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。

有序性

有序性:即程序执行的顺序按照代码的先后顺序执行。

发生指令重排序(Instruction Reorder)代码的执行顺序可能会发生改变。

在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

可见性

可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

对于可见性,Java提供了volatile关键字来保证可见性。

volatile

volatile的定义与实现原理

如果一个字段被声明成volatile,java线程内存模型确保所有线程看到这个变量的值是唯一的。


一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。

2)禁止进行指令重排序。


实现原则:

有volatile修饰的共享变量在进行写操作的时候汇编代码中会出现lock

Lock前缀的指令:

​ 1.将当前处理器缓存行的数据写回到系统内存

​ 2.这个写回操作会使其它cpu中缓存了该内存地址的数据无效

​ 通过缓存一致性协议实现

volatile的使用优化

追加字节优化性能。(例子LinkedTransferQueue)

追加64字节能够提高并发编程的效率

并不是在使用volatile变量的时候都应该追加到64字节

  • 缓存行非64字节宽的处理器
  • 共享变量不会被频繁的读写 (追加字节的方式会带来一定的性能消耗)

注意!!⚠️ 该方法在Java 7 下可能不生效,因为java更加智慧,我们得采用其他的方式追加字节

synchronized

java锁

java中每一个对象都可以做为锁。

表现形式

  • 普通同步方法,锁是当前实例对象

  • 静态同步方法,锁是当前类的Class对象

  • 同步方法块,锁是Synchroized括号里配置的对象


当一个线程访问同步代码块的时候,它首先必须得到锁,推出或者抛出异常的时候必须释放锁。

synchronized在jvm中的实现原理

jvm基于进入退出Monitor对象来实现方法同步代码块同步,但两者的实现细节并不一样。

代码块同步


monitroentermonitorexit实现的

  • monitroenter是在编译期过后被插入到同步代码块的开始位置
  • monitorexit是插入到方法结束处和异常处

jvm保证enter和exit配对

任何对象都有一个monitor与之关联,当一个monitor被劫持后,它属于锁定状态。

当线程执行到monitroenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获取该对象的锁。

java中的获取锁时间上是获取monitor对象所有权。


方法同步

other

java对象头

synchronized用的锁是存在java对象头里的。

数组类型 用3个 字宽(word)存储对象头

非数组类型,用2个字宽。

32位虚拟机中 1个字宽 = 4个字节 = 32bit

CAS

CAS,在Java并发应用中通常指CompareAndSwap或CompareAndSet,即比较并交换。

1、CAS是一个原子操作,它比较一个内存位置的值并且只有相等时修改这个内存位置的值为新的值,保证了新的值总是基于最新的信息计算的,如果有其他线程在这期间修改了这个值则CAS失败。CAS返回是否成功或者内存位置原来的值用于判断是否CAS成功。

2、JVM中的CAS操作是利用了处理器提供的CMPXCHG指令实现的。

锁的升级与对比

偏向锁和轻量级锁是在Java1.6中引入的

偏向锁

偏向锁的来源是因为Hotsopt的作者研究发现大多数情况下,锁不仅不存在多线程竞争,而且总是由统一线程多次获得,而线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作,为了让线程获得锁的代驾更低而引入了偏向锁。偏向锁获得锁的过程分为以下几步:

1)初始时对象的Mark Word位为1,表示对象处于可偏向的状态,并且ThreadId为0,这是该对象是biasable&unbiased状态,可以加上偏向锁进入2)。如果一个线程试图锁住biasable&biased并且ThreadID不等于自己ID的时候,由于锁竞争应该直接进入4)撤销偏向锁。

2)线程尝试用CAS将自己的ThreadID放置到Mark Word中相应的位置,如果CAS操作成功进入到3),否则进入4)

3)进入到这一步代表当前没有锁竞争,Object继续保持biasable状态,但此时ThreadID已经不为0了,对象处于biasable&biased状态

4)当线程执行CAS失败,表示另一个线程当前正在竞争该对象上的锁。当到达全局安全点时(cpu没有正在执行的字节)获得偏向锁的线程将被挂起,撤销偏向(偏向位置0),如果这个线程已经死了,则把对象恢复到未锁定状态(标志位改为01),如果线程还活着,则把偏向锁置0,变成轻量级锁(标志位改为00),释放被阻塞的线程,进入到轻量级锁的执行路径中,同时被撤销偏向锁的线程继续往下执行。

5)运行同步代码块

轻量级锁


文章作者: Bxan
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Bxan !
  目录