Java:Synchronized和lock锁分析

Synchronized有什么用

在多线程并发执行过程中,如果对某个公用变量的操作需要做到单线程,那么就需要锁来保证多线程环境下的某个操作是顺序执行。

如何实现的

synchronized首先是一个悲观锁,支持的同步方法和同步语句都是使用monitor来实现的。Monitor可以理解为一个同步工具或一种同步机制,通常被描述为一个对象。每一个Java对象就有一把看不见的锁,称为内部锁或者Monitor锁。Monitor是线程私有的数据结构,每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个monitor关联,java对象头Mark Word中有指向Monitor对象(存储的指针的指向),叫做 Lock word,Synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因,同时notify/notifyAll/wait等方法会使用到Monitor锁对象,所以必须在同步代码块中使用。

同时monitor中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。synchronized通过Monitor来实现线程同步,Monitor是依赖于底层的操作系统的Mutex Lock(互斥锁,也叫重量级锁)来实现的线程同步。
在执行流程角度来看,每个对象都与一个monitor相关联,当一个线程执行到一个monitor监视下的代码块中的第一个指令时,该线程必须在引用的对象上获得一个锁,这个锁是monitor实现的。在HotSpot虚拟机中,monitor是由ObjectMonitor实现,使用C++编写实现,具体代码在HotSpot虚拟机源码ObjectMonitor.hpp文件中。

Synchronized锁范围

  • 普通同步方法,锁是当前实例对象
    同步方法依靠的是方法修饰符上的ACC_SYNCHRONIZED标记符隐式实现
	public synchronized void test(){
        // TODO
    }

    public void test(){
        synchronized (this) {
            // TODO
        }
    }
  • 静态同步方法,锁是当前类的class对象
    public static synchronized void test(){
        // TODO
    }

    public static void test(){
        synchronized (TestSynchronized.class) {
            // TODO
        }
    }
  • 同步方法块,锁是括号里面的对象
    同步代码块是使用monitorenter和monitorexit指令显式实现的

Synchronized:偏向锁、轻量级锁、重量级锁

这三种锁是指锁的状态,并且是针对Synchronized。在Java 5通过引入锁升级的机制来实现高效Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。

  • 1、偏向锁
    偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。线程是不需要触发同步的,减少加锁/解锁的一些CAS操作(比如等待队列的一些CAS操作),降低获取锁的代价。偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态。撤销偏向锁后恢复到无锁(标志位为“01”)或轻量级锁(标志位为“00”)的状态。
    通过-XX:-UseBiasedLocking取消偏向锁,偏向锁原本是一种锁优化,让同一个线程进来不需要同步操作,所以在锁竞争少的情况下有优化作用,但多线程 竞争非常激烈的环境下,因为大量的竞争会导致持有锁的线程不停地切换,锁也很难一直保持偏向模式,此时,使用偏向锁不仅不能优化程序,反而有可能降低程序性能,所以通过此参数取消偏向锁。偏向锁通过对比Mark Word解决加锁问题,避免执行CAS操作。

  • 2、轻量级锁:标志位为“00”
    轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能,自旋到一定程度,升级到重量级锁。
    为什么可以减少时间呢?阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失。如果物理机器有多个处理器,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃CPU的执行时间,看看持有锁的线程是否很快就会释放锁。而为了让当前线程“稍等一下”,我们需让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销。这就是自旋锁。自旋超过了限定次数(默认是10次,可以使用-XX:PreBlockSpin来更改)没有成功获得锁,就应当挂起线程。
    缺点同样也有:线程占用资源较长,那么自旋就会消耗cpu时间片,这段时间cpu啥都没干,就不停的被你占用自旋。所以后面提出来自适应自旋锁,实际效果中自旋锁很少成功,少量是成功的,轻量级锁是通过用CAS操作和自旋来解决加锁问题,避免线程阻塞和唤醒而影响性能

  • 3、重量级锁:标志位为“10”
    重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。

    Java面试Offer直通车

跟lock的区别

两者区别:

  • 1.首先synchronized是java内置关键字,在jvm层面,Lock是个java类;

  • 2.synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;

  • 3.synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;

  • 4.用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;

  • 5.synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可)

  • 6.Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。

参考博客

java对一个对象加锁,锁的是什么东西?
美团Java琐事

深入分析Synchronized原理(阿里面试题)
深入分析synchronized的实现原理

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章