synchronized关键字

​并发编程的重点也是难点是数据同步、线程安全、锁。要编写线程安全的代码,其核心在于对共享和可变的状态的访问进行管理。

共享意味着变量可以由多个线程访问,而可变则意味着变量的值在其生命周期内可以发生变化。

当多个线程访问某个状态变量且其中有一个线程执行写入操作时,必须采用同步机制来协同这些线程对变量的访问。

Java中的主要同步机制是关键字synchronized,它提供了一种独占的加锁方式。

勾勾从一下几个方面来学习synchronized:

 

目录

关键字synchronized的特性

关键字synchronized的用法

锁膨胀

总结


关键字synchronized的特性

 

synchronized关键字可以实现一个简单的策略来防止线程干扰和内存一致性错误,如果一个对象对多个线程是可见的,那么该对象的所有读和写都需通过同步的方式。

synchronized的特性:

  • 不可中断

    synchronized关键字提供了独占的加锁方式,一旦一个线程持有了锁对象,其他线程将进入阻塞状态或者等待状态,直到前一个线程释放锁,中间过程不可中断。

  • 原子性

        synchronized关键字的不可中断性保证了它的原子性。

  • 可见性
    synchronized关键字包含了两个JVM指令:monitor enter和monitor exit,它能够保证在任何时候任何线程执行到monitor enter时都必须从主内存中获取数据,而不是从线程工作内存获取数据,在monitor exit之后,工作内存被更新后的值必须存入主内存,从而保证了数据可见性。

  • 有序性

    synchronized关键字修改的同步方法是串行执行的,但其所修饰的代码块中的指令顺序还是会发生改变的,这种改变遵守java happens-before规则。

  • 可重入性

    如果一个拥有锁持有权的线程再次获取锁,则monitor的计数器会累加1,当线程释放锁的时候也会减1,直到计数器为0表示线程释放了锁的持有权,在计数器不为0之前,其他线程都处于阻塞状态。

     

关键字synchronized的用法

 

synchronized关键字锁的是对象,修饰的可以是代码块和方法,但是不能修饰class对象以及变量。

  • 代码块,锁对象即是object

public void sync(){        synchronized (new Object()){                            }           }
  • 方法,锁对象即是this

 public synchronized void syncMethod(){   }
  • 静态方法,锁对象既是class

 public synchronized static void syncStaticMethod(){   }

勾勾在开发中最常用的是用synchronized关键字修饰对象,可以控制锁的粒度,所以针对最常用的场景勾勾去了解了它的字节码文件,先来看看勾勾的测试用例:

public class TestSynchronized {    private int index;    private final static int MAX = 100;    public void sync(){               synchronized (new Object()){                            while (index < MAX){                                        index ++;            }        }    }}

运行命令 “javac -encoding UTF-8 TestSynchronized.java”编辑成class文件,然后

运行命令“javap -c TestSynchronized.class”得到字节码文件:

 public com.example.demo.articles.thread.TestSynchronized();     Code:       0: aload_0       1: invokespecial #1                  // Method java/lang/Object."<init>":()V       4: return  public void sync();    Code:       0: new           #2                  // class java/lang/Object       3: dup       4: invokespecial #1                  // Method java/lang/Object."<init>":()V       7: dup       8: astore_1       9: monitorenter  //进入同步代码块      10: aload_0       //加载数据      11: getfield      #3                  // Field index:I      14: bipush        100      16: if_icmpge     32      19: aload_0      20: dup      21: getfield      #3                  // Field index:I      24: iconst_1      25: iadd          // 加1操作      26: putfield      #3                  // Field index:I      29: goto          10 //跳转至10行      32: aload_1            33: monitorexit  // 退出同步代码块      34: goto          42 //跳转至42行      37: astore_2     // 刷新数据      38: aload_1      39: monitorexit        40: aload_2      41: athrow      42: return    Exception table:       from    to  target type          10    34    37   any          37    40    37   any}

monitorenter和monitorexit是成对出现的,有时候你看到的是一个monitorenter对应多个monitorexit,但是能肯定的一定点是每一个monitorexit之前必有一个monitorenter。

从字节码文件中可以看到monitorenter之后执行了aload操作,monitorexit之后执行了astore操作。

TIPS:在使用synchronized关键字时注意事项

  1. 锁的对象不能为空;

  2. 锁的范围不宜太大;

  3. 不要试图使用不同的monitor来锁同一个方法;

  4. 避免多个锁交叉等待导致死锁;

 

锁膨胀

 

在jdk1.6之前,线程在获取锁时,如果锁对象已经被其他线程持有,此线程将挂起进入阻塞状态,唤醒阻塞线程的过程涉及到了用户态和内核态的切换,性能损耗比较大。

synchronized作为亲儿子,混的太差肯定不行,在jdk1.6对其进行了优化,将锁状态分为了无锁状态,偏向锁,轻量级锁,重量级锁。

锁的升级过程既是:

 

在了解锁的升级过程之前,勾勾重点理解了monitor和对象头。

在第一次研究锁膨胀的时候因为没有花时间去理解这两个概念,勾勾对锁升级的记忆只持续了3天,最后勾勾又用了两天的时间去学习对象头和monitor,才算是真正的理解锁的膨胀原理。所以大家在学习一个知识的时候,不要靠背去记忆一个知识点,一定要知其然。

每一个对象都与一个monitor相关联,monitor对象与实例对象一同创建并销毁,monitor是C++支持的一个监视器。锁对象的争夺既是争夺monitor的持有权。

勾勾在OpenJdk源码中找到了ObjectMonitor的源码:

 // initialize the monitor, exception the semaphore, all other fields  //  are simple integers or pointers      ObjectMonitor() {      _header       = NULL;    _count        = 0;    _waiters      = 0,    _recursions   = 0;    _object       = NULL;    _owner        = NULL;    _WaitSet      = NULL;    _WaitSetLock  = 0 ;    _Responsible  = NULL ;    _succ         = NULL ;    _cxq          = NULL ;    FreeNext      = NULL ;    _EntryList    = NULL ;    _SpinFreq     = 0 ;    _SpinClock    = 0 ;    OwnerIsThread = 0 ;  } protected:                         // protected for jvmtiRawMonitor  void *  volatile _owner;          // pointer to owning thread OR BasicLock  volatile intptr_t  _recursions;   // recursion count, 0 for first entry private:  int OwnerIsThread ;               // _owner is (Thread *) vs SP/BasicLock  ObjectWaiter * volatile _cxq ;    // LL of recently-arrived threads blocked on entry.                                    // The list is actually composed of WaitNodes, acting                                    // as proxies for Threads. protected:  ObjectWaiter * volatile _EntryList ;     // Threads blocked on entry or reentry. private:  Thread * volatile _succ ;          // Heir presumptive thread - used for futile wakeup throttling  Thread * volatile _Responsible ;  int _PromptDrain ;                // rqst to drain cxq into EntryList ASAP}

owner:指向线程的指针。即锁对象关联的monitor中的owner指向了哪个线程表示此线程持有了锁对象。

waitSet:进入阻塞等待的线程队列。当线程调用wait方法之后,就会进入waitset队列,可以等待其他线程唤醒。

entryList:当多个线程进入同步代码块之后,处于阻塞状态的线程就会被放入entryList中。

那什么是对象头呢,它与synchronized又有什么关系呢?

在JVM中,对象在内存中分为3块区域:

  • 对象头

    Mark Word(标记字段):用于存储对象的hashcode,分代年龄,锁标志位,是否可偏向标志,在运行期间,其存储的数据会发生变化。

    Klass Point(类型指针):该指针指向它的类元数据,JVM通过这个指针确定对象是哪个类的实例。该指针的位长度为JVM的一个字大小,即32位的JVM为32位,64位的JVM为64位。

  • 实例数据

    用于存放类的数据信息

  • 填充数据

    虚拟机要求对象起始地址必须是8字节的整数倍,当不满足时需对其填充。

 

我们先通过一张图了解下在锁升级的过程中对象头的变化:

 

接下来我们分析锁升级的过程:

第一个分支锁标志为01

当线程运行到同步代码块时,首先会判断锁标志位,如果锁标志位为01,则继续判断偏向标志。

如果偏向标志为0,则表示锁对象未被其他线程持有,可以获取锁。此时当前线程通过CAS的方法修改线程ID,如果修改成功,此时锁升级为偏向锁。

如果偏向标志为1,则表示锁对象已经被占有。

进一步判断线程id是否相等,相等则表示当前线程持有的锁对象,可以重入。

如果线程id不相等,则表示锁被其他线程占有。

需进一步判断持有偏向锁的线程的活动状态,如果原持有偏向锁线程已经不活动或者已经退出同步代码块,则表示原持有偏向锁的线程可以释放偏向锁。释放后偏向锁回到无锁状态,线程再次尝试获取锁。主要是因为偏向锁不会主动释放,只有其他线程竞争偏向锁的时候才会释放。

如果原持有偏向锁的线程没有退出同步代码块,则锁升级为轻量级锁。

偏向锁的流程图如下:

 

 

第二个分支锁标志为00

在第一个分支中我们了解到在如果偏向锁已经被其他线程占有,则锁会被升级为轻量级锁。

此时原持有偏向锁的线程的栈帧中分配锁记录Lock Record,将对象头中的Mark Word信息拷贝到锁记录中,Mark Word的指针指向了原持有偏向锁线程中的锁记录,此时原持有偏向锁的线程获取轻量级锁,继续执行同步块代码。

如果线程在运行同步块时发现锁的标志位为00,则在当前线程的栈帧中分配锁记录,拷贝对象头中的Mark Word到锁记录中。通过CAS操作将Mark Word中的指针指向自己的锁记录,如果成功,则当前线程获取轻量锁。

如果修改失败,则进入自旋,不断通过CAS的方式修改Mark Word中的指针指向自己的锁记录。

当自旋超过一定次数(默认10次),则升级为重量锁。

轻量锁的锁是主动释放的,持有轻量锁的线程在执行完同步代码块后,会先判断Mark Word中的指针是否依然指向自己,且自己锁记录中的Mark Word信息与锁对象的Mark Word信息一致,如果都一致,则释放锁成功。

如果不一致,则锁有可能已经被升级为重量锁。

轻量级流程图如下图:

 

 

第三个分支锁标志位为10

锁标志为10时,此时锁已经为重量锁,线程会先判断monitor中的owner指针指向是否为自己,是则获取重量锁,不是则会挂起。

整个锁升级过程中的流程图如下,如果看懂了一定要自己画一遍。

 

 

总结

synchronized关键字是一种独占的加锁方式,不可中断,保证了原子性,可见性,和有序性。

synchronized关键字可用于修饰方法和代码块,不能用于修饰变量和类。

多线程在执行同步代码块时获取锁的过程在不同的锁状态下不一样,偏向锁是修改Mark Word中的线程ID,轻量锁是修改Mark Word的指针指向自己的锁记录,重量锁是修改monitor中的指针指向自己。

今天就学到这里了!收工!

我是勾勾,一直在努力的程序媛,感谢您的点赞和转发!

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值