synchronized其他知识

文章目录


上篇文章提到过,synchronized可以保证多线程之间的可见性和原子性。而且我们平时用的时候,很多时候都是直接在方法上加一个synchronized,这片文章主要介绍synchronized在使用过程中需要或者在synchronized实现上需要我们了解的一些细节以及注意事项

拿什么作为锁

在使用synchronized时候,有一点需要注意的就是,我们要知道当前各个线程来抢夺的这个锁,它到底是一个什么,这样在控制竞争的时候,我们知道线程是在一个什么范围才会去竞争这个锁。其实可以作为锁的资源就两种,一种是对象,一种是Class类。但是这两个锁资源又会分为不同的情况。

使用类实例对象作为锁

synchronized直接加在方法上、synchronized(this){}作为代码块、或者自定义一个对象作为锁,都是使用类对象作为锁,这种情况有一个要注意的是,如果当前类存在多个实例对象的时候,对象之间的锁不是一个锁,各自是各自的,如果是单例不会存在问题,但是如果当前类存在多个对象,要注意不同对象之间是会竞争不同的锁。

package cn.yarne;

import java.util.concurrent.*;

/**
 * Created by yarne on 2021/9/12.
 */
public class Main {

    public static void main(String[] args) throws InterruptedException {
        Main main = new Main();
        new Thread(main::add).start();
        new Thread(main::reduce).start();

       /* Main main = new Main();
        Main main2 = new Main();
        new Thread(main::add).start();
        new Thread(main2::reduce).start();*/
    }

    public synchronized void add() {
        System.out.println("增加");
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public synchronized void reduce() {
        System.out.println("减少");
    }
}

使用类class对象作为锁

使用类class对象作为锁,有两种方式,一种是在static静态方法上加锁,用的是当前类的Class,一种是synchronized代码块直接用某个类的.class进行锁定。使用类作为锁可以保证不论创建多少对象,最终能争抢的资源始终是一个,就是指定的类Class,因为不管对象创建多少个,最终所有的类只有一个Class。

package cn.yarne;

import java.util.concurrent.*;

/**
 * Created by yarne on 2021/9/12.
 */
public class Main {

    public static void main(String[] args) throws InterruptedException {
        new Thread(Main::add).start();
        new Thread(Main::reduce).start();
    }

    public static synchronized void add() {
        System.out.println("增加");
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static synchronized void reduce() {
        System.out.println("减少");
    }
}

synchronized的四种状态

在这里要提一下的就是,我们知道在原本的java版本中,非常不推荐使用synchronized关键字来锁定资源,因为它是一个重量级锁,性能差,但是在Java1.6之后,对synchronized做了优化。那到底重量级锁是啥,如何做的优化,下面简单介绍一下。

锁升级

在1.6之前,synchronized直接就是一个重量级锁,没有状态一说,但是在之后的优化中,给它增加了四种锁状态做优化,由轻到重分别是无锁、偏向锁、轻量级锁、重量级锁,通过一个锁升级的一个过程,让线程在竞争锁的时候,尽量以最小的代价得到一个锁,提升整体的性能,在了解锁升级的过程中,需要有另外两个知识的了解,一个是对象头,一个是monitor。

对象头

对象在存到内存中的时候,在内存中的布局可以分为四部分:mark word、class pointer、instance、padding,我们最常知道也最常用到的就是instance,对象的实例数据,我们只要操作一个对象的时候,就会使用到,class pointer是对象的类型指针,表示当前对象是什么类型。padding的作用就是补位,比如我们知道平时在操作一些短数据类型的变量,一般都会通过补位将其转化为int,还有就是如果不是8的倍数,就会通过补位将其补成8的倍数。还有没有说到的就是mark wordmark word中存了三类信息,包括对象的Hash值GC信息,还有我们要提到的锁信息。mark word这部分的信息,被我们一般叫做对象头,对象头中保存了四种锁状态,synchronized就是通过操作对象头中的锁信息,来达到升级的过程。

monitor

在上篇文章我们看到加了synchronized关键字的代码块,会有monitorenter,monitorexit的指令,monitor可以理解为是一个监视器,可以理解为它跟对象头中锁信息的所用很类似,如果说对象头的作用是记录当前对象作为锁的一个被使用的状态,那么monitor可以理解成是要记录每个不同的线程自己对象锁的一个操作状态,保证不论在什么情况下,只有一个线程是正常拿到锁的一个状态。当线程执行到monitorenter指令的时候,线程会去对象头中获取出来一个monitor对象。我拿出来monitor对象中几个比较我们常见操作用到的信息介绍一下

  • _recursions:线程每进入一次synchronized代码块,就会+1,每出来一次就会-1.最终是0的时候,标识锁占用结束
  • _object:存储了该monitor对应的对象头信息,也就是说用来做monitor和对象头的绑定
  • _owner:标识当前的monitor所属的线程
  • _WaitSet:如果线程在占用锁资源的时候,调用了Object的wait方法,那么当前线程会被记录到_WaitSet中,等待被唤醒
  • _cxq:所有在竞争锁的线程信息,都会记录到这里
  • _EntryList:如果线程在抢锁的过程中,一直没抢上,进入到重量级锁的阻塞状态,就会存在这个里

简单的看下其实就可以看出来,,每个monitor其实就是记录了所有线程对当前锁的一个竞争,或者使用的信息

升级过程

上边了解了对象头和monitor,我们也知道这两个互相配合,才能完成多线程情况下,对一个锁头的竞争以及竞争的状态的记录,下边简单的说一下锁的升级过程

无锁

首先对象头默认是会处于一个无锁的一个状态,因为线程还没有来竞争

偏向锁

当第一个线程来的时候,拿到了这个锁,就会给对象头打上一个偏向锁的状态,并且将自己的线程ID记录在对象头,如果下次来拿锁的还是这个线程,并且看到现在对象头是一个偏向锁的状态,就直接判断存储的偏向的线程ID是不是自己,如果是自己的话,就不需要进程到下一个状态,直接就能拿到锁,甚至可以理解为,锁本来就是自己的,可以直接去操作锁着的资源。

如果下一个线程来竞争的时候,发现是偏向锁,并且线程ID不是自己,就会去判断对应线程ID的线程是否还存活,如果是存活,并且还在执行锁内部的代码,不能释放锁,就将锁升级为下一个状态,如果对应的线程不存在或者不使用锁了,就将锁对象设置为无锁状态,重新走偏向锁的流程。

轻量级锁

如果开始发生锁竞争,首先发生的就是用轻量级(CAS)的方式来竞争锁资源。这种情形下以下几种情况发生

  1. 如果对象头是无锁的状态,线程首先会去对象头中取出来一个线程本地monitor对象,并且将monitor的_owner线程信息设置为自己的信息,并且去通过CAS的方式尝试将自己本地的monitor中的owner信息更新到对象头的那个公共的monitor中。更改成功就得到锁
  2. 如果当前线程已经拿到了锁,再一次遇到拿锁的情况的时候,直接更改自己的monitor中的_recursions,表示重入锁
  3. 如果已经有线程占用了,就开始走类似于第一步的操作,进行CAS自旋,如果到了几次之后,仍然没有拿到锁,那么当前线程就会进入到重量级竞争状态,自己进到对象头的那个公共的monitor的_EntryList中阻塞,等待被唤醒。
重量级锁

如果锁进入到了重量级的状态,有线程被阻塞到_EntryList中,那么其他所有来的线程,如果发现_EntryList里面有了,就会直接进入其中,等待被唤醒。

释放

拿到锁之后,执行完锁住的代码块,就会执行monitorexit指令,执行指令的时候。要做两个操作,一个是接触对象头中和自己monitor的互相关联绑定,然后就是如果_EntryList中有阻塞的线程,就会去唤醒一个让其去竞争锁。

总结

本片文章主要介绍了synchronized更详细的一些知识,通过这些内容可以让我们了解或者去理解synchronized优化到了一个怎样的程度,让我们可以对锁有有一个认知,在使用锁的时候,自己可以有一个衡量。当前也有一些没有讲到的内容。个人也会去多多了解这块更细致的知识点,分享出来,下片文章对于锁这部分的知识,做一个全面的梳理

参考文章:

https://www.jianshu.com/p/19f861ab749e

上一篇:在Entity Framework 4.0中使用 Repository 和 Unit of Work 模式


下一篇:Synchronized原理