java并发(二)synchronized和volatile

二,synchronized

2.1 临界区

  • 一个程序运行多个线程本身是没有问题的
  • 问题出在多个线程访问共享资源
    • 多个线程读共享资源其实也没有问题
    • 在多个线程对共享资源读写操作时发生指令交错,就会出现问题
  • 一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区
static int counter = 0;
 
static void increment() 
// 临界区 
{ 
    counter++; 
}

2.2 竞态条件

多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件

所以我们需要避免临界区的竞态条件发生
- 阻塞式解决:lock,synchronized
- 非阻塞式解决:原子变量

2.3 synchronized应用及原理

synchronized也就是对象锁,他采用互斥的方式,保证同一时刻最多只有一个线程能持有对象锁,其他线程想要获取对象锁时会被阻塞住,这样保证只有一个线程能进入到临界区,安全的执行临界区的代码

变量的线程安全分析:

  • 成员变量和静态变量是否线程安全?
    • 如果变量没有被共享,则是线程安全
    • 如果他们被共享了又分两种情况讨论:
      • 如果只有读操作,则线程安全
      • 如果有读有写,则线程不安全
  • 局部变量是否线程安全?
    • 局部变量肯定是线程安全的
    • 但是局部变量引用的对象未必线程安全,如果该对象逃离方法的作用范围,需要考虑线程安全

2.3.1 synchronized应用

synchronized可以加在实例对象,类对象和方法上

加在实例对象,其实和加在实例方法上是一样的,锁住的都是当前实例对象

            synchronized (this){
                for (int i = 0; i < 5000; i++) {
                    newCount--;
                }
            }
        public synchronized void sync(){
            newCount++;
        }

类对象

            synchronized (Object.class){
                for (int i = 0; i < 5000; i++) {
                    newCount--;
                }
            }
            
        //synchronized加在静态方法上,相当于synchronized(B.class),相当于锁住类对象
        public synchronized static void syncStatic(){

        }

2.3.2 synchronized原理

Java对象头

对象大致可以分为3部分组成,对象头,实例数据和填充字节

以32位虚拟机为例

在这里插入图片描述

在这里插入图片描述

  • thread:持有偏向锁的线程ID。
  • epoch:偏向时间戳。
  • ptr_to_lock_record:指向栈中栈桢锁记录的指针。
  • ptr_to_heavyweight_monitor:指向管程Monitor的指针。
  • klass_word:用于存储对象的类型指针,该指针指向它的类元数据,JVM通过这个指针确定对象是哪个类的实例

锁标记:

其中biased_lock:对象是否启用偏向锁标记,为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。

在这里插入图片描述

在这里插入图片描述

在无锁状态下和偏向锁下,markword会存储hashcode等信息,在轻量级锁状态下,hashcode等信息在线程私有的栈中栈桢的锁记录中,重量级锁状态下,位置被锁指针占据,hashcode等信息存储在对象关联的monitor上

monitor

monitor是一个同步工具,每个对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式,monitor可以和对象一起创建销毁,或当线程试图获取对象锁时自动生成,但是当一个monitor被一个线程持有后,monitor中的_owner就会写上持有线程的id,其他线程就无法获取到该monitor进入monitor中的_EntryList

其主要数据结构如下:

ObjectMonitor() {
   _header       = NULL;
   _count        = 0;
   _waiters      = 0,
   _recursions   = 0;
   _object       = NULL;
   _owner        = NULL;
   _WaitSet      = NULL; //处于wait状态的线程,会被加入到_WaitSet,只有获取到锁的线程才能wait
   _WaitSetLock  = 0 ;
   _Responsible  = NULL ;
   _succ         = NULL ;
   _cxq          = NULL ;
   FreeNext      = NULL ;
   _EntryList    = NULL ; //处于等待锁block状态的线程,会被加入到该列表
   _SpinFreq     = 0 ;
   _SpinClock    = 0 ;
   OwnerIsThread = 0 ;
 }

monitor中有两个重要的队列

  • waitSet:当持有monitor的线程调用wait方法时,当前线程会放弃monitor进入waitSet队列等待,其他线程可以竞争获取monitor,waitSet中的线程被唤醒后,不会立马获取到锁,需要进入entrySet竞争获取锁
  • entrySet:上边也提到了,被synchronized阻塞在临界区外边的线程将进入entrySet,获取到锁的线程释放锁后会唤醒entrySet中的线程

java字节码分析

public class SyncCodeBlock {
    public int i;

    public void syncTask(){
        synchronized (this){
            i++;
        }
    }
}

字节码:

public class com.fufu.concurrent.SyncCodeBlock {
  public int i;

  public com.fufu.concurrent.SyncCodeBlock();
    Code:
       0: aload_0
       1: invokespecial #1                  //构造方法
       4: return

  public void syncTask();
    Code:
       0: aload_0                           //锁对象的引用
       1: dup                               //复制一份  
       2: astore_1                          //放到一个临时变量slot_1
       3: monitorenter                   //注意此处,进入同步方法,将
       4: aload_0
       5: dup
       6: getfield      #2                  // Field i:I
       9: iconst_1
      10: iadd
      11: putfield      #2                  // Field i:I
      14: aload_1
      15: monitorenter                    //注意此处,退出同步方法
      16: goto          24
      19: astore_2
      20: aload_1
      21: monitorexit                    //注意此处,退出同步方法
      22: aload_2
      23: athrow
      24: return
    Exception table:
       from    to  target type
           4    16    19   any
          19    22    19   any
}

monitorenter做了什么?

将lock对象和操作系统的monitor关联,即将lock对象的markword指向关联的monitor,owner改为获取到锁的线程

monitorexit做了什么?

将lock对象的markword重置,唤醒entrySet中等待的线程,owner置为null

值得注意的是编译器将会确保无论方法通过何种方式完成,方法中调用过的每条 monitorenter 指令都有执行其对应 monitorexit 指令,而无论这个方法是正常结束还是异常结束。为了保证在方法异常完成时 monitorenter 和 monitorexit 指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,它的目的就是用来执行 monitorexit 指令。从字节码中也可以看出多了一个monitorexit指令,它就是异常结束时被执行的释放monitor 的指令。

如果synchronized加到方法上,则不是monitorenter和monitorexit,方法级的同步是隐式的同步,无需通过字节码指令来控制,它实现在方法调用和返回中,当调用方法时,会先检查方法访问标志中ACC_SYNCHRONIZED是否被设置,如果设置了执行线程会先持有monitor,方法返回时释放monitor,如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的monitor将在异常抛到同步方法之外时自动释放。

2.4 synchronized优化

2.4.1 轻量级锁

轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以 使用轻量级锁来优化。
轻量级锁对使用者是透明的,即语法仍然是 synchronized

  • 每个线程都会包含一个锁记录的结构,内部可以存储锁定对象的markword,线程的锁记录结构中有锁对象引用和锁记录地址,锁状态标识

在这里插入图片描述

加锁过程:

  • 让锁记录中的对象引用指向锁对象,并尝试CAS替换锁对象中的markword

在这里插入图片描述

  • 如果cas替换成功,锁对象的对象头中就会存储了线程锁记录结构中的锁记录地址和锁状态,表示该线程已经对该对象加锁

在这里插入图片描述

  • 如果cas失败,则分两种情况:
    • 其他线程已经持有该对象锁,加锁失败,存在竞争,则进入锁膨胀
    • 该线程锁重入,则count++,该线程在添加一条锁记录,锁对象引用指向对象锁,无需cas交换则原来用于交换的锁记录和锁记录地址为null

在这里插入图片描述

解锁过程:

  • 当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重 入计数减一
  • 当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头

在这里插入图片描述

总结

  • 加锁过程:
    • 线程在栈桢中创建锁记录结构用于存储锁对象markword
    • 锁记录结构的对象引用指向锁对象
    • cas替换锁对象的markword和锁记录结构的锁记录地址
    • 此时锁对象的对象头中存储了锁记录的地址,可以通过锁记录地址找到相应线程还有锁状态,轻量级锁是00
    • 线程栈桢中存储锁对象的hashcode,分代年龄等信息
    • 如果cas交换失败,如果是锁重入现象的话,那么在栈中在压入一个栈桢,锁记录指向锁对象,否则进入锁膨胀
  • 解锁过程:
    • 当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重 入计数减一
    • 当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头

2.4.2 重量级锁

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

在这里插入图片描述

即对象头中的锁状态和线程1中的锁记录结构中的锁状态相同

在这里插入图片描述

自旋优化

重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步 块,释放了锁),这时当前线程就可以避免阻塞

在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会 高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。 Java 7 之后不能控制是否开启自旋功能

2.4.3 偏向锁

大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获取锁的代价更低,引入了偏向锁

只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现 这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有

如果有其他线程竞争,则升级为轻量级锁

原先轻量级锁每次锁重入都需要cas替换markword

在这里插入图片描述

偏向锁只有第一次加锁时cas设置下线程id即可

在这里插入图片描述

偏向锁撤销

  • 调用对象的hashcode,偏向锁时markword中存的是线程id
  • 其他线程使用偏向锁,则升级为轻量级锁
  • 调用wait/notify,升级为重量级锁

三,volatile

  • 当写一个volatile变量时,JMM会保证该线程对应的本地内存的共享变量值刷新回主内存
  • 当读一个volatile变量时,JMM会使线程本地内存中原值失效,从主内存中读取

volatile适合一个写,多个读的场景

3.1 可见性

保证了不同线程对一个共享变量操作时的可见性,即一个线程修改了共享变量的值会保证这个新值对其他线程可见

3.2 原子性

一个或多个操作,要么全部执行并且执行过程中不会被中断,要么全部不执行,即使在多个线程一起执行时,一旦开始操作,就不会受其他线程影响

volatile实现了可见性和有序性,只能保证单条操作的原子性,不能保证复合操作的原子性

3.3 有序性

通过插入内存屏障禁止了处理器和编译器某些类型的指令重排序,java编译器在生成指令序列时,在适当的位置插入内存屏障

3.3.1 指令重排序

指令重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段

指令重排的前提是指令重排后不能改变运行结果,存在数据依赖的指令不能重排

3.4 volatile原理

volatile的底层实现原理是内存屏障

  • 对volatile变量的写指令后加入写屏障
  • 对volatile变量的读指令前加入读屏障

3.4.1 可见性保证

写屏障:保证在写屏障之前对共享变量的改动都会同步到主内存中

    public void func(){
        //volatile变量前操作
        ready = true;
        //插入写屏障:保证写屏障前对共享变量的改动都同步到主内存中,没有被volatile修饰的,在volatile变量前的共享变量都会被写会到主存中
        //-----------------
        //volatile变量后操作
    }

读屏障:保证在读屏障之后对共享变量的读取,加载的都是主内存中的最新数据

    public void func1(){
        //读屏障
        //对volatile变量读之前插入读屏障,保证volatile读取都是都是主内存中的最新数据
        if(ready){

        }else{

        }
    }

3.4.2 保证有序性

写屏障:保证在写屏障之前的操作不会被重排序到写屏障之后

    public void func(){
        //volatile变量前操作
        ready = true;
        //插入写屏障
        //-----------------
        //volatile变量后操作
    }

读屏障:保证读屏障之前的操作不会被重排序到读屏障之后

    public void func1(){
        //读屏障
        //对volatile变量读之前插入读屏障,保证volatile读取都是都是主内存中的最新数据
        if(ready){

        }else{

        }
    }

有序性只能保证本线程内的代码不被重排序

3.4.4 happens-before规则

happens-before规则是用来描述两个操作的可见性的,如果A happens-before B 那么A的结果对B可见

  • synchronized :线程解锁m之前对共享变量的写对接下来对m加锁的其他线程对该变量的读可见
  • volatile:对volatile变量的写对volatile变量的读可见
  • Thread:线程start()前的操作,对线程开始后的操作可见
  • Thread:线程结束前的操作对线程结束后的操作可见
  • Thread:T1中断T2,对T2发现被中断可见
  • Object:一个对象的构造函数的结束对这个对象的finalizer开始可见

猜你喜欢

转载自blog.csdn.net/weixin_41922289/article/details/104666389