Synchronized和Lock的区别

这两者都是解决Java并发常见的一种方法,确保在同一时刻只能有一个线程访问某个方法或者代码块,但为什么有了Synchronized之后,还需要Lock呢?

一、Synchronized

对成员函数加锁,必须获得该类的实例对象的锁才能进入同步代码块
对静态方法加锁,必须获得类的锁才能进入同步块
被static修饰的静态方法、静态属性都是归类所有,同时该类的所有实例对象都可以访问。
但是普通成员属性、成员方法是归实例化的对象所有,必须实例化之后才能访问,这也是为什么静态方法不能访问非静态属性的原因。

sychronized是java的内置锁,基于底层的操作系统实现,要了解底层的实现,需要明白以下知识。

1.Java对象头

在这里插入图片描述
Klass Word:指向对象所储存的Class,简单理解,根据指针可以找到类对象
在这里插入图片描述
在这里插入图片描述
当obj对象获得锁之后,会将mark word的前30位指向Monitor对象,并将标志位从01修改为10

在这里插入图片描述
synchronized用到的锁基于对象的对象头,对象在内存中的布局分为三块区域:对象头、实例数据、对齐填充。对象头中有着指向Moniter的指针,通过这个指针就可以实现并发情况下操作数据的安全性。这也是为什么java中任何对象都可以作为锁的原因

2. 原理介绍

在这里插入图片描述
编译成字节码指令:
在这里插入图片描述

2.Monitor

Moniter是synchronized重量级锁实现的关键。synchronized在jdk5之前一直是一个重量级的锁,非常的消耗性能。在jdk5之后做了一定的优化(轻量级锁和偏向锁)。Moniter是由objectMoniter实现的,其主要数据结构如下:

ObjectMonitor() {
    _header       = NULL;
    _count        = 0; //记录个数
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL; //当前拥有锁的线程
    _WaitSet      = NULL; //处于wait状态的线程,会被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; //处于等待锁block状态的线程,会被加入到该列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

上述代码中_WaitSet 和_EntrySet用于存放等待锁的线程对象,_owner用于指向持有锁对象的线程,线程拥有锁之后,会将_count加1,若线程调用wait()方法,将会释放当前的moniter,并将count减1,owner变量也会直为null。sysnchronize关键字便是通过这种方式来获取锁的。

二.虚拟机对sysnchronize的优化

锁的状态分为四种,无锁、偏向锁、轻量级锁、重量级锁。随着锁的竞争,锁可以从偏向锁依次升级到重量级锁。升级是单向的。

  • 偏向锁:在大多数情况下,锁总是由同一线程多次请求获得,因此为了减少同一线程获取锁的代价(底层涉及CAS操作,进行加入等待队列。)而引入了偏向锁。其核心思想就是如果一个线程获得了锁,该锁就为偏向模式,当该线程再次请求锁时,就会省去一些同步的操作,提高了锁的性能。但是对于一些竞争比较激烈情况下,偏向锁就失效了,失效之后就会升级成轻量级锁。
  • 轻量级锁:偏向锁失效之后,会采用轻量级锁进行优化,轻量级锁的核心依据是:对于绝大部分锁,在整个同步时期都不存在竞争。轻量级锁分为两种:(1)自旋锁:虚拟机为了避免在线程在操作系统层面(需要从用户态切换到系统内核态,比较消耗时间)挂起,会采用自旋的方式优化。其实就相当与执行一个空的for循环,防止其进入内核升级成重量级锁。(2)自适应自旋锁:原理和自旋锁差不多,只不过虚拟机会根据线程上次自旋获得锁的结果来调整这次自旋的时间。如果上次自旋之后没有获得锁,那么很有可能升级为重量级锁,避免浪费cpu资源。
  • 上述都失效之后,升级为重量级锁,也就是切换到内核状态进行阻塞

自旋锁会占用 CPU的时间,单核CPU自旋就是浪费,多核CPU自旋才能发挥优势

为什么说重量级锁开销大呢?

系统检测到是重量级锁之后,会将要等待的线程进行阻塞,被阻塞的进程虽然不消耗cpu资源,但是阻塞或者唤醒一个线程时,都需要操作系统进行帮助,这就需要从用户态切换到内核态,这个转换比较消耗时间。

1.轻量级锁的原理

(1)创建锁记录(Lock Record)对象,每个线程的栈帧中都会包含一个锁记录,内部可以存储锁定对象的Mark Word
在这里插入图片描述
(2)让锁记录中的对象引用指向对象,并尝试用cas替换对象的Mark Word,将Mark Word的值存入锁记录
在这里插入图片描述
(3)在这里插入图片描述
(4)在这里插入图片描述
(5)在这里插入图片描述
(6)在这里插入图片描述

2.升级为重量级锁

在这里插入图片描述
在这里插入图片描述

3.偏向锁的原理

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

4.撤销偏向锁的方法

(1)调用对象.hashcode(),会禁用偏向锁
(2)当有其他线程使用偏向锁时,会将偏向锁升级为轻量级锁
(3)调用wait/nofity

三.缺点

讨论过synchronized的上述特征之后,接下来谈谈synchronized关键字的缺点。
如果代码块被synchronized修饰了,当一个线程获取了对象的锁,执行相应的代码块时,其他线程只能一直等待该线程释放锁。而这里释放锁只存在以下两种情况。

  1. 同步代码块中的内容执行结束
  2. 线程执行发生异常, JVM让线程自动释放锁

假如同步代码块执行时间过长,就比较影响效率。

除此之外,当多个线程对一个文件进行操作时,写操作和写操作需要同步,但读操作和读操作并不会发生冲突现象。因此就需要一种机制来确保多线程读操作不会同步。而这点synchronized关键字无法做到。

1.小例子

在这里插入图片描述
上面的简单意思就是用两个线程分别对i加100万次,理论结果应该是200万,而且我还加了synchronized锁住了add方法,保证了其线程安全性。可是!!!我无论运行多少次都是小于200万的,为什么呢?

原因就在于synchronized加锁的函数,这个方法是普通成员方法,那么锁就是加给对象的,但是在创建线程时却new了两个Test2实例,也就是说这个锁是给这两个实例加的锁,并没有达到同步的效果,所以才会出现错误。至于为什么小于200万,要理解i++的过程就明白了

四、Lock

首先lock,需要我们手动去释放锁

1.实现原理

通过查看源码发现,Lock相关实现类依赖与 AbstractQueuedSynchronize 这个类。AQS使用一个volatile的int类型的成员变量state来表示同步状态,通过内置的FIFo队列来完成资源获取的排队工作将每条要去抢占资源的线程封装成一个Node,节点来实现锁的分配,通过CAS完成对Ste值的修改。
AbstractQueuedSynchronizer内部类Node源码

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {

    ...

     * Creates a new {@code AbstractQueuedSynchronizer} instance
    protected AbstractQueuedSynchronizer() { }

     * Wait queue node class.
    static final class Node {
        //表示线程以共享的模式等待锁
        /** Marker to indicate a node is waiting in shared mode */
        static final Node SHARED = new Node();
        
        //表示线程正在以独占的方式等待锁
        /** Marker to indicate a node is waiting in exclusive mode */
        static final Node EXCLUSIVE = null;

        //线程被取消了
        /** waitStatus value to indicate thread has cancelled */
        static final int CANCELLED =  1;

        //后继线程需要唤醒
        /** waitStatus value to indicate successor's thread needs unparking */
        static final int SIGNAL    = -1;
        
        //等待condition唤醒
        /** waitStatus value to indicate thread is waiting on condition */
        static final int CONDITION = -2;
        
        //共享式同步状态获取将会无条件地传播下去
        * waitStatus value to indicate the next acquireShared should     
        static final int PROPAGATE = -3;

        //当前节点在队列中的状态(重点)
        //说人话:
        //等候区其它顾客(其它线程)的等待状态
        //队列中每个排队的个体就是一个Node
        //初始为0,状态上面的几种
         * Status field, taking on only the values:
        volatile int waitStatus;

        //前驱节点(重点)
         * Link to predecessor node that current node/thread relies on
        volatile Node prev;

        //后继节点(重点)
         * Link to the successor node that the current node/thread
        volatile Node next;

        //表示处于该节点的线程
         * The thread that enqueued this node.  Initialized on
        volatile Thread thread;

        //指向下一个处于CONDITION状态的节点
         * Link to next node waiting on condition, or the special
        Node nextWaiter;

         * Returns true if node is waiting in shared mode.
        final boolean isShared() {

        //返回前驱节点,没有的话抛出npe
         * Returns previous node, or throws NullPointerException if null.
        final Node predecessor() throws NullPointerException {

        Node() {    // Used to establish initial head or SHARED marker

        Node(Thread thread, Node mode) {     // Used by addWaiter

        Node(Thread thread, int waitStatus) { // Used by Condition
    }
	...
}

node其实是一个双向列表,头结点是一个虚拟节点,也就是空的
大致总结一下原理:当一个线程执行lock方法的时候,会首先利用cas操作来更新标志位state的状态,如果更新失败,则会通过CAS操作添加至等待队列,然后通过头部来使用cas更新索的状态,如果成功,则获取锁成功,从头部移除。

五、总结

  1. synchronized采用的是CPU悲观锁机制,即线程获得的是独占锁。独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。而在CPU转换线程阻塞时会引起线程上下文切换,当有很多线程竞争锁的时候,会引起CPU频繁的上下文切换导致效率很低。但随着Synchronized关键字的优化,其性能已经有了很大的提升,但是灵活性还是不如Lock。

  2. Lock用的是乐观锁方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁实现的机制就是CAS操作。相对于Synchronized关键字,它比较灵活,主要体现在对于等待的进程,有中断机制,防止其进一步等待。同时支持读写锁,一定程度上提高效率。


版权声明:本文为weixin_44978620原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。