synchronized关键字详解

synchronized锁简介

锁是在执行多线程时用于强行限制资源访问的同步机制,即用于在并发控制中保证对互斥要求的满足。简而言之 synchronized就是同步锁,目的是保持结果的一致性。
学习synchronized必须要了解锁的作用、锁的作用域、锁的升级、以及锁的应用场景。

synchronized的作用

在了解锁的作用之前,我们看一个经典的单例问题。

public class Singleton1 {

    private Singleton1() {}

    private static Singleton1 singletonInstance = null;

    public static Singleton1 getInstance() {
        if (singletonInstance == null) {
            singletonInstance = new Singleton1();
        }
        return singletonInstance;
    }
}

上面这段代码在单线程中调用getInstance()方法,无论调用多少次,都可以保证每次获取的是同一个Singleton1对象。

那多线程情况会怎么样呢?

从上图可以看出在多线程环境下一旦产生并发,返回的就不再是单例了。此时我们只需要加上synchronized就能保证同一时间只能有一个线程访问getInstance()这个方法,其他线程被阻塞,从而达到单例的目的。这就是synchronized的作用,保证一个对象在同一时间只能被一个线程访问,被锁定的代码块称之为临界区,保护临界区同一个时间只能被一个线程访问的锁称之为互斥锁,其他等待的这个锁的线程都是被这个锁阻塞了。

public class Singleton1 {

    private Singleton1() {}

    private static Singleton1 singletonInstance = null;

    public synchronized static Singleton1 getInstance() {
        if (singletonInstance == null) {
            singletonInstance = new Singleton1();
        }
        return singletonInstance;
    }
}

synchronized锁的作用域

通常按作用域把锁分为类锁和对象锁,从严格意义上来说,他们都是对象锁,因为java中万事万物皆对象。不过划分概念后更加容易让人理解。

  • 类锁有两种书写形式,一种是锁定类的静态方法,另一种是在代码块中锁定类的class对象。
   /**
     * 类锁-在静态方法上使用synchronized关键字
     */
    public synchronized static void classLock(){
        log.info("我是类锁");
    }

    /**
     * 类锁-代码中锁定类的class对象
     */
    public void classLock2(){
        synchronized (SynchronizedType.class){
           log.info("我也是类锁");
        }
    }

加了类锁后class对象就被锁定了,那么当前类的其他对象会被锁定吗?用代码演示

    public synchronized static void classLock()  {
        log.info("我是类锁");
        try {//休眠三秒,保证在线程执行期间有足够的时间异步执行其他线程
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.info("类锁方法执行完成");
    }

    public static void oneStaticMethod(){
        log.info("我是一个普通的静态方法");
    }
    public synchronized static void twoStaticMethod(){
        log.info("我是一个加锁的静态方法");
    }

    public static void main(String[] args) {
        new Thread(()->{
            classLock();
        },"线程1").start();
        try {//休眠一秒后执行oneStaticMethod();
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(()->{
            oneStaticMethod();
        },"线程2").start();
        new Thread(()->{
            twoStaticMethod();
        },"线程3").start();
    }

输出结果

17:06:22.148 [线程1] INFO - 我是类锁
17:06:23.146 [线程2] INFO - 我是一个普通的静态方法
17:06:25.151 [线程1] INFO - 类锁方法执行完成
17:06:25.151 [线程3] INFO - 我是一个加锁的静态方法

从结果可以看出,当锁定class对象后并不会锁定类中其他静态方法,但如果访问其他synchrsonized修饰的静态方法时会被阻塞,因为一个类的类锁在同一个classLoader中只有一把,我们可以认为它是单例的。

  • 对象锁有两种书写形式,一种是锁定非静态方法,另一种是在代码块中锁定一个具体对象。
    /**
     * 对象锁-非静态方法级别的对象锁
     */
    public synchrsonized void objectLock(){
        log.info("我是对象锁-方法级别");
    }

    /**
     * 对象锁-代码块中锁定具体对象
     */
    public synchronized void objectLock2(){
        synchronized (this) {
            log.info("我也是对象锁-锁定某个具体对象(class对象除外)");
        }
    }

synchronized锁的升级

先熟悉一下锁升级经历的几个概念名词

  • 偏向锁:只有一个线程获取锁,没有其他线程竞争,则当前线程得到一把偏向锁。

  • 轻量级锁:只要大于一个线程同时获取锁,存在竞争就会成为轻量级锁,也称非阻塞同步乐观锁。

  • 重量级锁:发生多线程请求时,没有获取到锁的线程会被阻塞,而唤醒被阻塞的线程需要操作系统帮忙,不是JVM直接唤醒,经历比较耗时的用户态切换到内核态过程。这种锁称为重量级锁。

JDK1.6之前所有的锁都是重量级锁,非常耗费资源。从JDK1.6开始加入锁升级概念。意思是当一个线程A获取某个对象Y的锁的时候,先看看这个对象有没有被其他线程加锁,如果没有,就给这个对象加一把偏向锁,过了一段时间,线程A又来访问这个对象,发现这把锁是自己加的偏向锁,就当作没有锁直接访问。但是如果线程B也过来访问对象Y,发现对象Y被线程A加了偏向锁,那线程A肯定不能让你访问 ,于是把锁升级为轻量级锁,线程B想访问访问就先等着吧。这时候线程B就在原地自旋等待,时不时尝试获取一次锁,直到线程A释放锁。假如线程A一直不释放锁,线程B等太久了,于是就放弃了原地等待,跑到阻塞队列里面去了,此时锁升级为重量级锁。

锁的使用场景

锁的类型 使用场景
偏向锁 很少使用多线程
轻量级锁 请求的线程数量不是特别多,因为自旋是要耗费CPU的,并且每个线程占用锁的时间较短
重量级锁 每个线程占用资源时间较长,或者并发请求锁的线程数量过多,在阻塞队列中的线程不会占用CPU资源

使用锁的注意事项

在锁定对象的时候不要使用锁定String、Integer、Long对象。为什么呢?

我们来看看为什么不能用String

   public static void main(String[] args) {
        String a = "123456";
        String b = "123456";
        System.out.println(a == b);
    }
    输出结果: true

由于字符串在java使用是十分频繁的,为了优化内存提升效率,java创建了一个字符串池(String pool),采用上面的方式生成一个字符串变量会把地址指向字符串池中已经存在的对象。那么造成什么问题?假如你引入了一个第三方包,这个包也用了synchronized锁定 “a” 字符串,此时你会发现你的代码永远获取不到锁,却一直找不到问题所在。

为什么不能用Integer、Long?

我们看一下Integer装箱的源码

    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

Integer对象在计算的时候会自动拆箱装箱,装箱时,如果值超过IntegerCache中的值,会生成一个新的Integer对象,因此当你锁定一个Interger对象的时候,如果进行运算,很容易造成原本锁定的对象和计算后的对象不是同一个对象,达不到同步的效果,因为他们的内存地址已经是不同的了。

你可能感兴趣的:(synchronized关键字详解)