跳至主要內容

synchronized关键字

牧歌...大约 9 分钟

synchronized关键字

双重检查锁定技术

源码实例:

protected Collection<ApplicationListener<?>> getApplicationListeners(
        ApplicationEvent event, ResolvableType eventType) {

    Object source = event.getSource();
    Class<?> sourceType = (source != null ? source.getClass() : null);
    ListenerCacheKey cacheKey = new ListenerCacheKey(eventType, sourceType);

    // Quick check for existing entry on ConcurrentHashMap...
    ListenerRetriever retriever = this.retrieverCache.get(cacheKey);
    if (retriever != null) {
        return retriever.getApplicationListeners();
    }

    if (this.beanClassLoader == null ||
            (ClassUtils.isCacheSafe(event.getClass(), this.beanClassLoader) &&
                    (sourceType == null || ClassUtils.isCacheSafe(sourceType, this.beanClassLoader)))) {
        // Fully synchronized building and caching of a ListenerRetriever
        synchronized (this.retrievalMutex) {
            retriever = this.retrieverCache.get(cacheKey);
            if (retriever != null) {
                return retriever.getApplicationListeners();
            }
            retriever = new ListenerRetriever(true);
            Collection<ApplicationListener<?>> listeners =
                    retrieveApplicationListeners(eventType, sourceType, retriever);
            this.retrieverCache.put(cacheKey, retriever);
            return listeners;
        }
    }
    else {
        // No ListenerRetriever caching -> no synchronization necessary
        return retrieveApplicationListeners(eventType, sourceType, null);
    }
}









 







 

 














又有看到两次判断

一、使用

在Java中,“双重检查锁定”(double checked locking)是一种用来实现延迟初始化的常见技术。但是,这种技术在多线程环境下可能会导致问题,因为它不一定能正确地工作,这个问题被称为“双重检查问题”。

问题的原因在于,在某些情况下,JVM可能不会正确地初始化对象,因为它可能没有按照你期望的那样执行指令。这通常是由于JVM的重排序(reordering)机制导致的。重排序是指编译器和处理器为了优化程序性能而进行的指令重新排序。

解决这个问题的一个常见方法是使用volatile关键字来确保禁用指令重排序。volatile关键字会提示JVM,被修饰的变量是共享的且可以被多个线程同时访问,因此应当避免对其进行优化。

下面是使用volatile修饰的双重检查锁定示例代码:

public class Singleton {
    private volatile static Singleton instance;
 
    private Singleton() {
        // 初始化工作
    }
 
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

二、由来

首先我们来看一下非线程安全的初始化单例模式

public class Singleton {
    private static Singleton instance;

    public static Singleton getInstance() {
        if (instance == null) {                 // 1. 线程A执行
            instance = new Singleton();         // 2. 线程B执行
        }
        return instance;
    }
}

在 Singleton 类中,假设线程A执行到代码1的时候,线程B执行到代码2, 这时候线程A 可能 看到 instance 引用对象还没有完成初始化,就会再次进入判断创建两个对象,导致对象不一致。

对于 Singleton 类,我们可以对getInstance()方法做同步处理来实现来实现线程安全的延迟初始化,示例代码如下:

public static synchronized Singleton getInstance() {
	if (instance == null) {                 // 1. 线程A执行
		instance = new Singleton();         // 2. 线程B执行
	}
	return instance;
}

由于上述代码对getInstance()方法做了同步处理,这样可能导致同步程序开销加大。 如果getInstance()被多个线程频繁调用,将会导致程序执行性能降低,反之如果不是被多个线程调用,那个这个getInstance()方法的延迟初始化方法将影响性能。

JVM 1.6之前 synchronized是重量级锁,所以很耗费性能,所以人们想到了一个种双重校验锁(Dobule-check Locking)的方案来提高性能,示例代码如下:

public class Singleton {                                    // 1. 
    private static Singleton instance;                      // 2.
    public static Singleton getInstance() {                 // 3.
        if (instance == null) {                             // 4. 第一次检查,可能有多个线程都通过了这个检测
            synchronized (Singleton.class) {                // 5. 加锁
                if (instance == null) {                     // 6. 第二次检查,是防止通过第一个检查的多个线程中除第一个抢到锁的其他线程再次进入
                    instance = new Singleton();             // 7. 问题根源
                }                                           // 8.
            }
        }
        return instance;
    }
}

上面代码我们执行到步骤7的时候,instance = new Instance(); ,创建了一个对象,这个创建对象的步骤可以分为三步,如下:

  1. 分配空间
  2. 初始化内存空间
  3. 设置instance指向内存

synchronized的使用

关于下面一段代码,以下说法正确的是A、C

public class Test {
    // 同步方法
    private synchronized void a() {
    }

    private void b() {
        // 同步块:用的是对象锁
        synchronized (this) {
        }
    }

    // 静态同步方法
    private synchronized static void c() {
    }
    private void d() {
        // 同步块:用的是类锁
        synchronized (Test.class) {
        }
    }
}

A:同一个对象,分别调用方法a和b,锁住的是同一个对象

B:同一个对象,分别调用方法a和c,锁住的是同一个对象

C:同一个对象,分别调用方法b和c,锁住的不是同一个对象

D:同一个对象,分别调用方法a、b、c,锁住的不是同一个对象

解析: 方法a为同步方法,方法b里面的是同步块,同步方法使用的锁是固有对象this,同步块使用的锁可以是任意对象,但是方法b里面的同步块使用的锁是对象this,所以方法a和方法b锁住的是同一个对象。

方法 c为静态同步方法,使用的锁是该类的字节码文件,也就是Test.class。方法d里面的也是同步块,只不过使用的锁是Test.class,所以方法c和方法d锁住的是同一个对象。

提示

  • 有线程访问对象的同步代码块时,另外的线程可以访问该对象的非同步代码块
  • 若锁住的是同一个对象,一个线程在访问对象的同步代码块时,另一个访问对象的同步代码块的线程会被阻塞。
  • 若锁住的是同一个对象,一个线程在访问对象的同步方法时,另一个访问对象的同步方法的线程会被阻塞。
  • 若锁住的是同一个对象,一个线程在访问对象的同步代码块时,另一个访问对象同步方法的线程会被阻塞,反之亦然。
  • 同一个类的不同对象的锁互不干扰
  • 类锁由于也是一种特殊的对象锁,因此表现和上述一致,而由于一个类只有一把对象锁,所以同一个类的不同对象使用类锁将会是同步的
  • 类锁和对象锁互不干扰

synchronized(this) 与synchronized(class) 之间的区别open in new window

一、概念

synchronized 是 Java 中的关键字,是利用锁的机制来实现同步的。

锁机制有如下两种特性:

  • 互斥性:即在同一时间只允许一个线程持有某个对象锁,通过这种特性来实现多线程中的协调机制,这样在同一时间只有一个线程对需同步的代码块(复合操作)进行访问。互斥性我们也往往称为操作的原子性。

  • 可见性:必须确保在锁被释放之前,对共享变量所做的修改,对于随后获得该锁的另一个线程是可见的(即在获得锁时应获得最新共享变量的值),否则另一个线程可能是在本地缓存的某个副本上继续操作从而引起不一致。

二、对象锁和类锁

1. 对象锁

在 Java 中,每个对象都会有一个 monitor 对象,这个对象其实就是 Java 对象的锁,通常会被称为“内置锁”或“对象锁”。类的对象可以有多个,所以每个对象有其独立的对象锁,互不干扰。

2. 类锁

在 Java 中,针对每个类也有一个锁,可以称为“类锁”,类锁实际上是通过对象锁实现的,即类的 Class 对象锁。每个类只有一个 Class 对象,所以每个类只有一个类锁。

三、synchronized 的用法分类

synchronized 的用法可以从两个维度上面分类:

1. 根据修饰对象分类

synchronized 可以修饰方法和代码块

  • 修饰代码块

    • synchronized(this|object) {}

    • synchronized(类.class) {}

  • 修饰方法

    • 修饰非静态方法

    • 修饰静态方法

2. 根据获取的锁分类

  • 获取对象锁

    • synchronized(this|object) {}

    • 修饰非静态方法

  • 获取类锁

    • synchronized(类.class) {}

    • 修饰静态方法,非静态方法

四、synchronized 的用法详解

这里根据获取的锁分类来分析 synchronized 的用法

1、对象锁

这个对象是新建的,跟其他对象没有任何关系:

/**
     * synchronized 修饰非静态方法
     */
    private void sync5() {
        Log.d(TAG,Thread.currentThread().getName() + "_Sync5: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
        synchronized (new SyncThread()) {
            try {
                Log.d(TAG, Thread.currentThread().getName() + "_Sync5_Start: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
                Thread.sleep(2000);
                Log.d(TAG, Thread.currentThread().getName() + "_Sync5_End: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

文章后面所有例子都是采用这四个线程,只是对方法进行修改:

SyncThread syncThread = new SyncThread();
        Thread F_thread1 = new Thread(new SyncThread(), "F_thread1");
        Thread F_thread2 = new Thread(new SyncThread(), "F_thread2");
        Thread F_thread3 = new Thread(syncThread, "F_thread3");
        Thread F_thread4 = new Thread(syncThread, "F_thread4");
        F_thread1.start();
        F_thread2.start();
        F_thread3.start();
        F_thread4.start();

运行结果如下:

image.png|650
image.png|650

四个线程同时开始,同时结束,因为作为锁的对象与线程是属于不同的实例。

2、采用类锁,无所谓哪个类,都会被拦截:

/**
     * synchronized 修饰非静态方法
     */
    private void sync5() {
        Log.d(TAG,Thread.currentThread().getName() + "_Sync5: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
        synchronized (MainActivity.class) {
            try {
                Log.d(TAG, Thread.currentThread().getName() + "_Sync5_Start: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
                Thread.sleep(2000);
                Log.d(TAG, Thread.currentThread().getName() + "_Sync5_End: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

运行结果如下:

image.png|650
image.png|650

可以发现,采用类锁一次只能通过一个。即使采用的是 MainActivity.class 这个类锁。

3、采用 this 对象锁:

/**
     * synchronized 修饰非静态方法
     */
    private void sync5() {
        Log.d(TAG,Thread.currentThread().getName() + "_Sync5: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
        synchronized (this) {
            try {
                Log.d(TAG, Thread.currentThread().getName() + "_Sync5_Start: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
                Thread.sleep(2000);
                Log.d(TAG, Thread.currentThread().getName() + "_Sync5_End: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

运行结果如下:   image.png|650

可以发现线程1,2同时结束,3,4有先后,原因是3,4同属于一个实例。

4、synchronized 修饰方法

作用范围是整个方法,所以方法中所有的代码都是同步的:

/**
     * synchronized 修饰非静态方法
     */
    private synchronized void sync5() {
        Log.d(TAG, Thread.currentThread().getName() + "_Sync5: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
        try {
            Log.d(TAG, Thread.currentThread().getName() + "_Sync5_Start: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
            Thread.sleep(2000);
            Log.d(TAG, Thread.currentThread().getName() + "_Sync5_End: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

修饰非静态方法:

image.png|650
image.png|650

对于非静态方法,同一个实例的线程访问会被拦截,非同一实例可以同时访问。 即此时是默认对象锁(this)。

修饰静态方法结果

image.png|650
image.png|650

可以看出来,静态方法默认类锁。

小结

  1. 对于静态方法,由于此时对象还未生成,所以只能采用类锁;
  2. 只要采用类锁,就会拦截所有线程,只能让一个线程访问。
  3. 对于对象锁(this),如果是同一个实例,就会按顺序访问,但是如果是不同实例,就可以同时访问。
  4. 如果对象锁跟访问的对象没有关系,那么就会都同时访问。
上次编辑于:
贡献者: wenleiwang
评论
  • 按正序
  • 按倒序
  • 按热度