飞道的博客

synchronized 关键字详解

358人阅读  评论(0)

synchronized 关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。另外,在 Java 早期版本中,synchronized 属于重量级锁,效率比较低。
 

1. synchronized 关键字的三种使用方法

1.1. 修饰实例方法

作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁

synchronized void method() {
   
  //业务代码
}

2.2. 修饰静态方法

给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得 当前 class 的锁。因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管 new 了多少个对象,只有一份)。

所以,如果一个线程 A 调用一个实例对象的非静态 synchronized 方法,而线程 B 需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。

synchronized static void method() {
   
//业务代码
}

3.3. 修饰代码块

指定加锁对象,对给定对象/类加锁。synchronized(this|object) 表示进入同步代码库前要获得给定对象的锁。synchronized(类.class) 表示进入同步代码前要获得 当前 class 的锁

synchronized(this) {
   
  //业务代码
}

总结:

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

 

2. 双重检验锁方式实现单例模式

public class Singleton {
   

    private volatile static Singleton uniqueInstance;

    private Singleton() {
   
    }

    public  static Singleton getUniqueInstance() {
   
       //先判断对象是否已经实例过,没有实例化过才进入加锁代码
        if (uniqueInstance == null) {
   
            //类对象加锁
            synchronized (Singleton.class) {
   
                if (uniqueInstance == null) {
   
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}

为什么是双重校验锁实现单例模式呢?

第一次校验:

也就是第一个if (uniqueInstance == null),这个是为了代码提高代码执行效率,由于单例模式只要一次创建实例即可,所以当创建了一个实例之后,再次调用getUniqueInstance()方法就不必要进入同步代码块,不用竞争锁。直接返回前面创建的实例即可。

第二次校验:

也就是第二个if (uniqueInstance == null),这个校验是防止二次创建实例,假如有一种情况,当uniqueInstance还未被创建时,线程 t1 调用getUniqueInstance()方法,由于第一次判断uniqueInstance == null,此时线程 t1 准备继续执行,但是由于资源被线程t2抢占了。

此时线程 t2 也调用getUniqueInstance()方法,同样的,由于uniqueInstance并没有实例化,线程 t2 同样可以通过第一个 if,然后继续往下执行,第二个 if 也通过。然后线程 t2 创建了一个实例uniqueInstance。此时线程 t2 完成任务,资源又回到线程 t1,t1此时也进入同步代码块。

如果没有这个第二个 if,那么,t1 就也会创建一个uniqueInstance实例。此时就会出现创建多个实例的情况,但是加上第二个 if,就可以完全避免这个多线程导致多次创建实例的问题。

所以说:两次校验都必不可少。

另外,需要注意 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 的指令重排,保证在多线程环境下也能正常运行。

3. 构造方法可以使用 synchronized 关键字修饰么?

 

构造方法本身就属于线程安全的,不存在同步的构造方法一说。因此,构造方法不能使用 synchronized 关键字修饰。

 

4. JDK1.6 之后的 synchronized 关键字底层做了哪些优化?

JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。

锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。

关于这几种优化的详细信息可以查看下面这篇文章:Java6及以上版本对synchronized的优化

 

5. synchronized 和 ReentrantLock 的区别

5.1. synchronized 和 ReentrantLock都是可重入锁

可重入锁指的是自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的。

如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增 1,所以要等到锁的计数器下降为 0 时才能释放锁。

5.2.synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API

synchronized 是依赖于 JVM 实现的,也就是说它是一个关键字,前面也讲到了虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReentrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。

5.3.ReentrantLock 比 synchronized 增加了一些高级功能

主要来说有三点:

  1. 等待可中断 :

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

  1. 可实现公平锁 :

ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。

所谓的公平锁就是先等待的线程先获得锁。ReentrantLock默认情况是非公平的,可以通过 ReentrantLock类的ReentrantLock(boolean fair)构造方法来制定是否是公平的。

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

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

Condition是 JDK1.5 之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify()/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的。

ReentrantLock类结合Condition实例可以实现“选择性通知” ,这个功能非常重要,而且是 Condition 接口默认提供的。而synchronized关键字就相当于整个 Lock 对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的signalAll()方法只会唤醒注册在该Condition实例中的所有等待线程。

如果你想使用上述功能,那么选择 ReentrantLock 是一个不错的选择。性能已不是选择标准

 

6. synchronized 关键字与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. synchronized锁适合代码少量的同步问题,Lock锁适合大量同步的代码的同步问题

 

7. synchronized 关键字和volatile 关键字

7.1. volatile 关键字

在 JDK1.2 之前,Java 的内存模型实现总是从主存(即共享内存)读取变量,是不需要进行特别的注意的。

而在当前的 Java 内存模型下,线程可以把变量保存本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。

这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。

要解决这个问题,就需要把变量声明为volatile,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。

所以,volatile 关键字 除了防止 JVM 的指令重排 ,还有一个重要的作用就是保证变量的可见性。

7.3. 并发编程的三个重要特性

  1. 原子性 : 一个的操作或者多次操作,要么所有的操作全部都得到执行并且不会收到任何因素的干扰而中断,要么所有的操作都执行,要么都不执行。synchronized 可以保证代码片段的原子性。
  2. 可见性 :当一个变量对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新值。volatile 关键字可以保证共享变量的可见性。
  3. 有序性 :代码在执行的过程中的先后顺序,Java 在编译器以及运行期间的优化,代码的执行顺序未必就是编写代码时候的顺序。volatile 关键字可以禁止指令进行重排序优化。

7.4. synchronized 关键字和 volatile 关键字的区别

synchronized 关键字和 volatile 关键字是两个互补的存在,而不是对立的存在!

  1. volatile 关键字是线程同步的轻量级实现,所以volatile 性能肯定比synchronized关键字要好。但是volatile 关键字只能用于变量而 synchronized 关键字可以修饰方法以及代码块。
  2. volatile 关键字能保证数据的可见性,但不能保证数据的原子性。synchronized 关键字两者都能保证。
  3. volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized 关键字解决的是多个线程之间访问资源的同步性。
  4. 多线程访问volatile关键字不会发生阻塞,而synchronized关键字可能会发生阻塞

 
参考:

https://blog.csdn.net/yuan_qh/article/details/99962482

https://www.cnblogs.com/iyyy/p/7993788.html

https://github.com/Snailclimb/JavaGuide


转载:https://blog.csdn.net/weixin_43901865/article/details/115484277
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场