Java---synchronized

一.概述

1、synchronized作用

  • 原子性:synchronized保证语句块内操作是原子的
  • 可见性:synchronized保证可见性(通过“在执行unlock之前,必须先把此变量同步回主内存”实现)
  • 有序性:synchronized保证有序性(通过“一个变量在同一时刻只允许一条线程对其进行lock操作”)

2、synchronized的使用

  • 修饰实例方法,对当前实例对象加锁
  • 修饰静态方法,多当前类的Class对象加锁
  • 修饰代码块,对synchronized括号内的对象加锁

二.原理

Jvm基于进入和退出Monitor对象来实现方法同步和代码块同步

1.方法同步:

JVM可以从方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。

当方法调用时,调用指令将会 检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有monitor, 然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放monitor。

2.代码块同步:

利用monitorenter和monitorexit这两个字节码指令。它们分别位于同步代码块的开始和结束位置。

当jvm执行到monitorenter指令时,当前线程试图获取monitor对象的所有权,如果未加锁或者已经被当前线程所持有,就把锁的计数器+1;当执行monitorexit指令时,锁计数器-1;当锁计数器为0时,该锁就被释放了。如果获取monitor对象失败,该线程则会进入阻塞状态,直到其他线程释放锁。

public class test {
    public synchronized void tongbu_method() {    //这个是同步方法
        System.out.println("Hello world_同步方法");
    }

    public void tongbu_code() {
        synchronized (this) {        //这个是同步代码块
            System.out.println("Hello world_同步代码块");
        }
    }

    public static void main(String[] args) {

    }
}

javac test.java  编译文件

使用javap -verbose test.class反编译

同步方法:

同步代码块:

三.Java对象头

在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。
在这里插入图片描述

1.实例数据:存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。

2.填充数据:由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐。

3.HotSpot虚拟机的对象头分为两部分信息:

第一部分用于存储对象自身运行时数据(状态),如哈希码、GC分代年龄等,官方称为Mark Word。

另一部分用于存储指向对象类型数据的指针,如果是数组对象的话,还会有一个额外的部分存储数组长度。

4.对象的Mark World (对象头文件):存储着对象的状态,主要是四种状态,无锁状态,偏向状态、轻量级状态和重量级状态

四.锁优化

偏向锁获取过程:(01)

(1)访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01——确认为可偏向状态。

(2)如果为可偏向状态,则测试线程ID是否指向当前线程,如果是,进入步骤(5),否则进入步骤(3)。

(3)如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行(5);如果竞争失败,执行(4)。

(4)如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。

(5)执行同步代码。


轻量级锁的加锁过程:(00)

(1)在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。这时候线程堆栈与对象头的状态如图2.1所示。

(2)拷贝对象头中的Mark Word复制到锁记录中。

(3)拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word。如果更新成功,则执行步骤(4),否则执行步骤(5)。

(4)如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态。

(5)如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。 而当前线程便尝试使用自旋来获取锁,自旋就是为了不让线程阻塞,而采用循环去获取锁的过程。

轻量级锁的加锁过程:(10)

每个对象都有一个monitor监视器,调用monitorenter就是尝试获取这个对象,成功获取到了就将值+1,离开就将值减1。如果是线程重入,在将值+1,说明monitor对象是支持可重入的。

注意,如果synchronize在方法上,那就没有上面两个指令,取而代之的是有一个ACC_SYNCHRONIZED修饰,表示方法加锁了。它会在常量池中增加这个一个标识符,获取它的monitor,所以本质上是一样的。

synchronized 锁处理过程(个人通俗理解):给大家讲一个故事吧,在一个村庄,有口井,村里的每户人家都需要到这口井里面打水,但这口井,只能有一个人同时打水,这样有存在很多人来打水的并发线程问题啦。井就是锁,人就是线程,那么打水就是每个线程要执行的任务呀。这个村长大人为了解决并发问题,提出了一套制度,就叫synchronized 准则吧。

准则的内容是:当一个人A去打水,如果这是,井口没人打水,他就可以直接打水了。但,如果此时,有人正在打水,那这个人就不能直接去打水,A只能站在打水等待的队伍的最后面。然而,由于synchronized不公平锁,所以,如果A不是一个文平村民的话,他可以有一个办法,那就是来到井口旁边,如果此时,打水的人正好打完水,即释放了锁,而等待打水队伍的第一个人还没有反应过来,这样,A就可以趁机“插队”,抢占了井口,即,获取了锁,自己打水(这样感觉对等待队伍中的人很不公平,但是没办法,synchronized 就是支持这样不公平的操作呀)。synchronized 是支持可重入锁,具体体现在,假如A在打水,此时,A的妻子来了,也要打水,那么A妻就不用去排队,就可以直接来井口旁边打水,这因为他们是一家人,就把他们视为一个对象了,一个对象只要没打完水,就可以一直占用井口。因为这个准则,其他的人在等待的队伍中也不能离开去做别的事情,就一直等着(线程阻塞)。

这样下来,如果在打水高峰期的话,分多人都在打水,就可能存在效率很低的问题了(因为大家只能排队,不能离开)。这样,细心的村长大人,就决定把这个准则改一改,就出现了加强版的synchronized 。新的准则支持偏向锁,就是,假如,村长里面人们都不喜欢在早上去打水,村长就把早上只有一个人打水的状态称为偏向状态,但由于A的家就住在井口旁,他喜欢早起一个人去打水,这样每天早上就只有A自己去打水(相当于单线程工作)。那么就需要那么A每天早上就在井口旁边的公告栏上面写下自己的名字,告诉别人,现在这个井被A占用了,当A打完以一次再去打第二次的时候,只需要看看公告栏上面是否还是自己的名字,如果时,就直接打水,不需要获取锁和释放锁。但是,由于早上只有A打水,B就感觉村里的井在早上只属于A,所以很嫉妒,B就决定他也要早上来打水。一旦有第二个人来打水,因为此时为偏向状态,通告栏上面写着A的名字,不会主动释放锁。所以B只能查看A是否已经打完其需要的所有水了,如果此时A没有打完水,那么就存在竞争打水的情况了,村长规定,一旦有多人打水,就可以把偏向状态改为轻量级状态,即,B就站在井口旁,一直问(处于自旋状态)A是否打完全部的水了,但是是有时间要求的,如果在一定时间内,A打完所有水了。那么这口井就是空闲的了。就又重新回到只有一个人来打水的偏行状态了,那么B就可以把通告栏上的名字改成自己的名字,来实现这口井只能B来使用。但如果B一直问了一定时间,A还是没有打完水,那么就将轻量级状态转为重量级状态,即,B不再询问,而是来到旁边静静的等待A打完水,就有回到了在开始synchronized 的尊则了。简单的来说就是,当只有一个人来打水,处于偏向状态,当处于偏向状态时,有同时第二个人也来打水,就变成了轻量级状态,第二个人在井口自旋询问超过一定时间了就变成了重量级状态了。这就是自旋锁、偏向锁、轻量级锁、重量级锁。

五.ReentrantLock与synchronized 的区别

① 两者都是可重入锁

两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。

② synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API

synchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReentrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。

③ ReentrantLock 比 synchronized 增加了一些高级功能

相比synchronized,ReentrantLock增加了一些高级功能。主要来说主要有三点:①等待可中断;②可实现公平锁;③可实现选择性通知(锁可以绑定多个条件)

  • ReentrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
  • ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。 ReentrantLock默认情况是非公平的,可以通过 ReentrantLock类的ReentrantLock(boolean fair)构造方法来制定是否是公平的。
  • synchronized关键字与wait()和notify()/notifyAll()方法相结合可以实现等待/通知机制,ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition() 方法。Condition是JDK1.5之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify()/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知” ,这个功能非常重要,而且是Condition接口默认提供的。而synchronized关键字就相当于整个Lock对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程。

所以,我们写同步的时候,优先考虑synchronized,如果有特殊需要,再进一步优化。ReentrantLock和Atomic如果用的不好,不仅不能提高性能,还可能带来灾难。

ReentrantLock获取锁的方式:

  • lock(), 如果获取了锁立即返回,如果别的线程持有锁,当前线程则一直处于休眠状态,直到获取锁;
  • tryLock(), 如果获取了锁立即返回true,如果别的线程正持有锁,立即返回false
  • tryLock(long timeout,TimeUnit unit), 如果获取了锁定立即返回true,如果别的线程正持有锁,会等待参数给定的时间,在等待的过程中,如果获取了锁定,就返回true,如果等待超时,返回false;
  • lockInterruptibly:如果获取了锁定立即返回,如果没有获取锁定,当前线程处于休眠状态,直到获取锁定,或者当前线程被别的线程中断

(2)非阻塞同步:都是乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

乐观锁的实现方式:乐观锁一般会使用版本号机制或CAS算法实现。

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值