欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

synchronized关键字的理解以及和Lock,ReentrantLock的区别

程序员文章站 2022-05-05 09:57:31
...

一、synchronized关键字的了解

synchronized解决的是多个线程之间访问资源的同步性,synchronized可以保证被它修饰的方法或代码块在任意时刻只能有一个线程执行。

二、synchronized关键字主要的使用方式

  • 修饰实例方法:作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
  • 修饰静态方法:给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象(static表明这是该类的一个静态资源,不管new了多少个对象,只有一份)。所以,如果一个线程A调用一个实例对象的非静态方法,而线程B需要调用这个对象所属类的静态synchronized方法,是允许的,不会发生互斥现象。因为访问静态synchronized方法占用的锁是当前类的锁,而访问非静态synchronized方法占用的是当前实例对象的锁,两者占用的锁不一致。
  • 修饰代码块:指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

总结:

synchronized关键字加到static静态方法和synchronized(class)加到代码块上都是给Class类加锁。synchronized关键字加到实例方法上是给对象实例加上锁。尽量不要用synchronized(String a),因为,在JVM,字符串常量有缓存功能。

具体使用:

双重校验锁实现对象单例

public class App {
    private volatile static App uniqueInstance;

    private App() {

    }

    public synchronized static App getUniqueInstance() {
        // 先判断对象是否被实例过
        if (uniqueInstance == null) {
            // 类对象加锁
            synchronized (App.class) {
                if (uniqueInstance == null) {
                    uniqueInstance = new App();
                }
            }
        }
        return uniqueInstance;
    }
}

需要注意 uniqueInstance 采⽤volatile 关键字修饰也是很有必要。

uniqueInstance = new Singleton();

这段代码其实是分为三步执⾏:

  1. uniqueInstance 分配内存空间
  2. 初始化 uniqueInstance
  3. uniqueInstance 指向分配的内存地址

但是由于 JVM 具有指令重排的特性,执⾏顺序有可能变成 1>3>2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致⼀个线程获得还没有初始化的实例。例如,线程 T1 执⾏了 1 和 3,此时 T2 调⽤ getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回uniqueInstance,但此时 uniqueInstance 还未被初始化。

使⽤ volatile 可以禁⽌ JVM 的指令重排,保证在多线程环境下也能正常运⾏。

三、synchronized关键字的底层原理

1. synchronized同步语句块情况

public class App {
    public static void main(String[] args) {
        App app = new App();
        app.method();
    }

    public void method() {
        synchronized (this) {
            System.out.println("synchronized 代码块");
        }
    }
}

先执行javac App.java命令编译生成字节码.class文件,然后执行javap -c -s -v -l App.class可以查看具体信息:

相关命令可输入 javac -help 和 javap -help 查看

synchronized关键字的理解以及和Lock,ReentrantLock的区别

从上面可以看出:
synchronized同步语句块使用的是monitorentermonitorexit指令,其中,monitorenter指令指向同步代码块的开始位置,monitorexit指令指明同步代码块的结束位置。

当执行monitorenter指令时,线程试图获取锁monitor(存在于每个java对象的对象头中,synchronized锁就是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因)的所有权。当计数器为0时可以成功获取,获取后将锁计数器设为1也就是加1.在执行monitorexit后将锁计数器设为0,表面锁被释放。如果获取对象失败,则当前线程就要阻塞等待,直到锁被另一个线程释放为止。

2. synchronized修饰方法情况

public class App {
    public static void main(String[] args) {
        App app = new App();
        app.method_two();
    }

    public synchronized void method_two() {
        System.out.println("synchronized 代码块");
    }
}

synchronized关键字的理解以及和Lock,ReentrantLock的区别
修饰方法时,并没有monitorentermonitorexit指令,而是ACC_SYNCHRONIZED标识,该标识指明该方法是一个同步方法。JVM通过该ACC_SYNCHRONIZED访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。

四、synchronizedLock的区别

synchronized无法通过破坏不可抢占条件来避免死锁。原因是synchronized申请资源的时候,如果申请不到,线程已经进入阻塞状态,无法做任何事情,释放不了已经占有的资源。

Lock提供了一组无条件、可轮询、定时的以及可中断的锁操作,所有获取锁、释放锁的操作都是显示操作。

  • 能响应中断synchronized的问题是,当持有锁A后,如果尝试获取锁B失败,那么线程就会进入阻塞状态,就会发生死锁,而且没有任何机会来唤醒阻塞的线程。但是如果阻塞状态的线程能够响应中断信号,即给阻塞线程发送中断信号,能够唤醒它,那这个阻塞线程的就有机会释放曾经持有的锁A,这样就破坏了不可抢占条件。
  • 支持超时:如果线程在一段时间内没有获得锁,不是进入阻塞状态,而是返回一个错误,那么这个线程也有机会释放曾经获得的锁,这样也可以破坏不可抢占条件。
  • 非阻塞的获取锁:如果尝试获取锁失败,并不进入阻塞状态,而是直接返回,这个线程也有机会释放曾经获取的锁,也可以破坏不可抢占条件。

五、synchronizedReentrantLock的区别

1. 两者都是可重入锁

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

2. synchronized依赖于JVMReentrantLock依赖于API

synchronized 是依赖于 JVM 实现的,并没有直接暴露给我们。

ReentrantLock 是 JDK 层⾯实现的(也就是 API 层⾯,需要 lock() 和 unlock() ⽅法配合try/finally 语句块来完成),所以可以通过查看它的源代码,来看它是如何实现的。

3. ReentrantLocksynchronized 增加了⼀些⾼级功能

(1). 等待可中断:

ReentrantLock提供了⼀种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情

(2). 可实现公平锁:

ReentrantLock可以指定是公平锁还是⾮公平锁。⽽synchronized只能是⾮公平锁所谓的公平锁就是先等待的线程先获得锁ReentrantLock默认情况是⾮公平的,可以通过 ReentrantLock类的 ReentrantLock(boolean fair)构造⽅法来制定是否是公平的。

(3). 可实现选择性通知(锁可以绑定多个条件)

synchronized关键字与wait()notify()/notifyAll()⽅法相结合可以实现等待/通知机制,ReentrantLock类当然也可以实现,但是需要借助于Condition接⼝与newCondition()⽅法。

线程对象可以注册在指定的Condition中,从⽽可以有选择性的进⾏线程通知,在调度线程上更加灵活。 在使⽤notify()/notifyAll()⽅法进⾏通知时,被通知的线程是由 JVM 选择的,⽤ReentrantLock类结合Condition实例可以实现“选择性通知”,这个功能是Condition接⼝默认提供的.

synchronized关键字就相当于整个Lock对象中只有⼀个Condition实例,所有的线程都注册在它⼀个身上。如果执⾏notifyAll()⽅法的话就会通知所有处于等待状态的线程这样会造成很⼤的效率问题,⽽Condition实例的signalAll()⽅法 只会唤醒注册在该Condition实例中的所有等待线程。

相关标签: Java