java高并发之Synchronized关键字详解

4 篇文章 0 订阅
2 篇文章 0 订阅

在java编码中经常会使用到Synchronized关键字,之前对Synchronized关键字不甚明白,借此机会梳理一下相关的知识点。

作用

synchronized是java中用于解决并发情况下数据同步访问的一个很重要的关键字,如果想保证一个共享资源在同一时间只会被一个线程访问,我们可以在代码中使用synchronized关键字对类或者对象加锁。

使用方式

synchronized的特性

  1. 原子性
    所谓的原子性就是指一个操作或者多个操作,要么全部执行并执行的过程不会被任何因素打断,要么就都不执行。
    在java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断,要么执行,要么不执行。
    被synchronized修饰的类或者对象的所有操作都是原子性的,因为在执行操作之前必须先获取类或者对象的锁,知道执行完成才会被释放,这中间的过程无法被中断,即保证了原子性。
    注意:synchronized和volatile最大的区别就在于原子性,volatile不具备原子性。
  2. 可见性
    可见性是指多个线程访问一个资源时,该资源的状态,值信息等对于其他线程都是可见的。
    synchronized和volatile都是具有可见性,其中synchronized对一个类或者对象加锁,一个线程如果需要访问该类或者对象必须先获取锁,而这个锁的状态对于其他任何线程都是可见的,并且在释放锁之前会将对变量的修改刷新到内存当中,保证资源变量的可见性,如果其中某个线程占用了该锁,其他线程就必须等待这个锁的释放。
    volatile的实现类似,被volatile修饰的变量,每当值修改会立即更新,所有线程共享这个值,确保其他线程读取到的变量永远是最新值,保证可见性。
  3. 有序性
    是指程序的执行是按照代码先后执行的。
    synchronized和volatile都具有有序性,java允许编译器和处理器对指令进行重新排列,但是指令的重排并不会影响单线程的执行顺序,它影响的是多线程并发执行的顺序性。
    synchronized保证了每个时刻都只有一个线程访问同步代码块,也就确定了线程执行同步代码块是区分先后顺序的,保证了有序性。
  4. 可重入性
    synchronized和ReentrantLock都是可重入锁。当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但是当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁。通俗一点就是一个线程拥有了锁仍然可以重复申请锁。
synchronized使用方式
  • 修饰代码块,即同步代码块,其作用的范围是{}括起来的代码,作用的对象是调用这个代码块的对象
public void lockTest1(){
        synchronized (lockTest){
            Log.e(TAG, "lockTest: ---");
        }
    }
  • 修饰普通方法,即同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象
 public synchronized void lockTest2(){
        Log.e(TAG, "lockTest: ---");
    }
  • 修饰静态方法,其作用的范围是整个静态方法,作用的对象是这个类的对象。
 //对静态方法加锁,必须获得类的锁才能进入
    public static synchronized void lockTest3(){

    }

如下图所示:
在这里插入图片描述

synchronized使用原理

java对象的理解
 public void lockTest(){
        synchronized (this){
            Log.e(TAG, "lockTest: ---");
        }
    }

如上代码所示,synchronized(this)是如何锁住对象的?
synchronized上锁其实就是改变对象的对象头。
在JVM中,java对象在内存中组成部分分为三块区域:

  1. java对象的实例数据—不固定
    包含:属性数据信息
  2. 对象头—固定
  3. 数据对齐(数据填充部分)
    由于JVM要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅视为了数据对齐。
    如下图所示:
    整个对象一共16Byte,其中对象头12byte,还有3byte是数据对齐填充数据(因为在64位虚拟机上对象的大小必须是8的倍数),对象的实例数据占1byte的
    在这里插入图片描述
什么是java对象头

每个gc管理的堆对象开头的公共结构(每个OOP都指向一个对象头),包括堆对象的布局、类型、GC状态、同步状态和标识哈希码的基本细心。它是实现Synchronized的锁对象的基础。java对象和JVM内部对象都有一个通用的对象头格式。
JVM规范的对象头组成:

占用大小名称说明
4/8bytemark word存储对象哈希值、锁信息、age、GC标志等
4/8byteklass pointer类型指针指向对象的实例数据,JVM通过这个指针确定该对象是哪个类的实例

MarkWord的设计是一个非固定的数据结构,以便存储更多有效的数据,它会根据对象本身的状态复用自己的存储空间,如下图所示:

Object Header(128 bits)
锁状态Mark Word(64bit)klass word(64bits)
无锁unused:25;hashcode:31;unused:1;age:4;是否是偏向锁:1;lock_Mark:2OOP to metadata object
偏向锁线程:54;epoch:2;unused:1;age:4;是否是偏向锁:1;lock_Mark:2OOP to metadata object
轻量锁指向栈中锁记录的指针:62;lock_Mark:2OOP to metadata object
重量锁指向重量锁的指针:62;lock_Mark:2OOP to metadata object
gc标记OOP to metadata object

每一个锁都对应一个monitor对象,在HotSpot虚拟机中它是由ObjectMonitor实现的(c++实现)。每个对象都存在着一个monitor与之关联,对象与其monitor之间的关系有多种实现方式,如monitor可以与对象一起创建销毁或者当线程试图获取对象锁时自动生成,但是当一个monitor被某个线程持有后,它便处于锁定状态。
monitor对象存在于每个java对象的对象头中(存储指针的指向),synchronized锁是通过这种方式获取锁的,这也是为什么java中任意对象可以作为锁的原因。
接下来我们继续分析synchronized在字节码层面的实现。
通过编译demo同步代码块并使用javap反编译得到的字节码如图:
在这里插入图片描述
从上图中可知同步的实现使用了monitorenter和monitorexit指令,其中monitorenter指令指向同步代码块的开始位置,monitorexit指令则指明同步代码块结束的位置,当执行monitorenter时,当前线程试图获取对象锁所对应的monitor持有权,当对象锁的monitor的进入计数器为0,那线程可以成功获取monitor的持有权,并修改计数器值为1,取锁成功。如果当前线程已经拥有对象锁的monitor的持有权,那他可以重入,计数器继续加1。倘若其他线程已经拥有对象锁的monitor的持有权,那当前线程会被阻塞,直到正在执行的新城执行完毕,即monitorexit指令被执行,执行线程释放monitor并将计数器设置为0,其他的线程将有机会持有monitor。方法中调用过的每条monitorenter指令都会执行其对应的monitorexit指令,无论这个方法是正常结束还是异常结束。为了保证在方法异常的时候monitorenter和monitorexit可以正确配对执行,编译器会自动产生一个异常处理器,这个处理器生命可处理所有的异常,它的目的就是用来执行monitorexit指令。从图中可以看出多了一个monitorexit指令,就是异常结束时被执行释放monitor的指令。

而方法的同步执行是隐式执行,即无需通过字节码指令来控制,它实现在方法调用和返回操作之中。JVM可以从方法常量池中的方法表结构中的ACC——SYNCHRONIZED访问标志区分一个方法是否是同步方法。

无锁&偏向锁&轻量级锁&重量级锁

锁的状态总共有四种,无锁状态、偏向锁、轻量级锁、重量级锁。锁可以从无锁状态到偏向锁,随着锁的竞争,会从偏向锁升级到轻量级锁,在升级到重量级锁,这就是jdk1.6之后java虚拟机对synchronized进行的优化,但是锁的升级是单向的,只能从低级到高级,不会出现锁的降级。

  1. 偏向锁
    在大多数的情况下,锁不仅不存在多线程竞争,而且总是由同一个线程多次获得,为了让线程获得锁消耗更低的性能而引入了偏向锁。
    当一个线程访问同步代码块并获取锁时,会在对象头和栈帧中的锁记录中存储锁偏向的ThreadID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需要简单测试一下MarkWord里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要在测试一下MarkWord中偏向锁的表示是否设置成1:如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。
    偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁的时候,持有偏向锁的线程才会释放锁。
  2. 轻量级锁
    线程在执行同步块之前,JVM会先在当前线程的栈帧中创建用于存储记录的空间,并将对象头中的MarkWord复制到锁记录中,然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。
  3. 重量级锁
    重量级锁是由轻量级锁升级而来,当同一时间有多个线程竞争锁时,锁就会被升级成重量级锁,此时其申请锁带来的开销也就变大。
    重量级锁一般使用场景会在追求吞吐量,同步块或者同步方法执行时间较长的场景。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值