Java高并发(五):synchronized

synchronized

synchronized的基本用法

  • 1、修饰实例方法,作用于当前实例对象,进入同步代码前需要获取当前实例对象的锁
  • 2、修饰静态方法,作用于当前类对象,进入同步代码前需要获取当前类对象的锁
  • 3、修饰代码块,指定加锁对象,给对象加锁,进入同步代码块前需要获得指定对象的锁
public class Demo3 {
    private static Object object;
    //修饰静态方法
    public synchronized static void test1() {
        //TODO
    }
    //修饰实例方法
    public synchronized void test2() {
        //TODO
    }
    public void test3() {
    	//修饰代码块
        synchronized (object) {
            //TODO
        }
    }
}

锁升级

对象在内存中布局

  • 在HotSpot虚拟机中,对象在内存中的存储布局可以分为三部分:对象头、实例数据、对齐填充,其中对象头主要存储了对象的hashCode、GC信息和锁信息,对象头中的MarkWord记录了对象和锁有关的信息。
  • Java中的每个对象都派生自Object类,线程在获取锁的过程中,其实就是获取对象的监视器(Monitor),所有的对象都带有Monitor,所以,任何对象都可以实现锁。另外,多线程访问对象时,其实就是抢占对象监视器,并修改对象中的锁标识信息。

锁升级的原因

  • 使用锁能够保证数据的线程安全性,但是会降低程序运行的性能,为了优化锁的性能,提出了锁升级的概念,并把synchronized锁分为:无锁状态、偏向锁、轻量级锁和重量级锁四种状态。
  • HotSpot虚拟机的作者发现大部分情况下,加锁的代码不仅仅不存在锁竞争的情况,而且还是由同一个线程多次获得同一个锁。基于这一发现,在JDK1.6之后对synchronized做了优化,并提出了四种锁状态的概念,synchronized是基于JVM层面的锁。

锁升级过程

1、无锁状态
  • 当对象刚被创建,并且没有被任何线程访问过的时候,是处于无锁状态的
2、偏向锁
  • 当一个线程访问了加同步锁的代码块时,会在对象头中存储当前线程的线程ID,后续这个线程再次进入加了锁的代码块时不需要再次加锁。
偏向锁的获取和撤销
偏向锁的获取
  • 1、首先获取锁对象的Mark Word,判断是否处于可偏向状态(biased_lock=1,且ThreadID为空)。
  • 2、如果是可偏向状态,那么通过CAS操作,把当前线程的ID写入MarkWord,如果CAS写入成功,就会获得了对象的偏向锁,并且开始执行同步代码块中的代码;如果获取失败,则表示有其他线程已经获取了偏向锁,这种情况表示当前锁存在锁竞争,需要撤销已经获取偏向锁的线程,并升级为轻量级锁。
  • 3、如果是已偏向状态,需要检查MarkWord中的线程ID是否等于当前线程的ThreadID,如果相等,不需要再次获得锁,直接进入同步代码块执行;如果不相等,说明当前锁偏向于其他线程,需要撤销偏向锁并升级到轻量级锁
偏向锁的撤销
  • 偏向锁的撤销并不是把对象恢复到无锁状态(偏向锁并不存在锁释放的概念),而是在获取偏向锁的过程中,通过CAS失败发现存在锁竞争时,直接把被偏向的锁对象直接升级到了轻量级锁。
  • 对已经获取偏向锁的对象进行锁撤销时,如果原获得了偏向锁的线程退出了临界区,那么表示同步代码块已经执行完毕,这时会把对象头设置为无锁状态,同时争抢对象锁的线程可以继续基于CAS重新偏向当前线程;如果原获得了偏向锁的线程还没有执行完同步代码块,处于临界区内,这时会把原获到偏向锁的线程升级为轻量级锁后再继续执行同步代码块
  • 可以根据UseBiasedLocking来开启或者关闭偏向锁
3、轻量级锁
  • 锁由偏向锁升级为轻量级锁之后,MarkWord会发生变化,升级为轻量级锁的过程为:首先线程在自己的栈帧中创建LockRecord;将锁对象中的MarkWord复制到LockRecord中;将锁记录LockRecord中的Owner指针指向锁对象;将锁对象头的MarkWord替换为指向锁记录的指针
轻量级锁的解锁
  • 轻量级锁的释放逻辑其实就是获得锁的逆向过程,通过CAS操作把线程栈帧中的LockRecord替换回到锁对象的MarkWord中,成功表示没有竞争,失败则表示存在竞争,会膨胀为重量级锁
自旋锁
  • 轻量级锁在加锁的过程中,使用的是自旋锁,所谓自旋,就是指当有另外一个线程来竞争锁时,这个线程会原地等待而不是阻塞,直到获得锁的线程释放锁之后,等待的线程可以立即获得锁。自旋锁在自旋的过程中会消耗CPU,所以对于很快就会执行完同步代码块的情况来说,使用自旋锁消耗的性能比上下文切换更小(线程阻塞挂起会由原来的用户态转变为内核态,成为上下文切换,十分消耗性能),自旋锁默认自旋十次还没有获得锁的话,就会把轻量级锁升级为重量级锁。
  • 适应性自旋锁,JDK1.6之后引入,如果在同一个锁对象上,自旋锁刚刚成功获得锁,并且持有锁的线程正在运行,那么虚拟机就会认为这次自旋也可以成功获得锁,所以会允许自旋的时间长一点;如果对于某个锁很少获取到,就会忽略自旋,直接阻塞,避免浪费处理器资源。
4、重量级锁
  • 对于重量级锁,意味着线程抢占锁时只能被挂起进入等待队列,等待唤醒了。
  • 从字节码层级来看,加了同步代码块以后,在字节码中会看到一个monitorenter和monitorexit。每一个Java对象都会与一个监视器monitor相关联,可以把monitor理解为一把锁,当一个线程想要执行被synchronized修饰的同步代码块时,该线程需要先获得synchronized修饰的对象的monitor。monitorenter表示去获取一个对象监视器,monitorexit表示释放一个对象监视器的所有权,使得其他等待获取该锁的线程尝试这个监视器。monitor是依赖于操作系统的互斥锁(MutexLock)来实现的,线程处于阻塞状态后会进入内核调度状态,会导致系统在用户态和内核态之间来回切换,严重影响锁的性能。
  • 任何线程对由synchronized保护的对象访问时,都需要获得object对象的监视器,获取失败,会进入同步队列,线程状态变为BLOCKED,当锁被释放时,这个释放操作就会从等待队列中唤醒线程,使得该线程尝试对监视器获取。
wait/notify/notifyAll
  • wait表示持有锁的线程准备释放锁的权限,释放CPU资源并进入等待队列,处于等待状态
  • notify表示通知jvm唤醒某个等待获取锁的线程,当持有锁的线程执行结束并释放锁之后,被唤醒的线程才可以获得锁,等待队列中的其他线程继续等待
  • notifyAll与notify不同的是,notifyAll会唤醒所有等待的线程,使得他们一起竞争锁
  • 这三个方法都必须在synchronized所限定的作用范围内使用,否则会报java.lang.IllegalMonitorStateException,因为没有同步,这些对象的状态是不确定的,不能调用这些方法。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值