Back to Posts

设计模式——单例模式中的DCL运用

Posted in DesignPattern

单例模式如果说最常用到的,应该就是枚举吧,枚举中的构造器即是单例,并且枚举的每个实例在实现上都是静态的;
或者手写单例,通过静态内部类或者双检索;

双检索

双检索实现的单例,主要是因为该实现方式是基于懒汉式,而懒汉又有线程安全问题,比如以下场景:某次活动发放了数张邀请码,每个邀请码只能用一次,在竞态环境下如何保证该邀请码只能被使用一次

package com.git.toolbox.util;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * Created by poan on 2017/11/03.
 */

class InviteCode {

    /**
     * 1 已经使用 0 未使用
     */
    private int status;

    public InviteCode(int status) {
        this.status = status;
    }

    public int getStatus() {
        return status;
    }

    public void setStatus(int status) {
        this.status = status;
    }
}


public class ConcurrentTest {

    private static ReadWriteLock lock = new ReentrantReadWriteLock();

    public static void main(String[] args) {
        InviteCode inviteCode = new InviteCode(0);

        ExecutorService executorService = Executors.newFixedThreadPool(3);
        for (int i = 0; i < 3; i++) {
            executorService.execute(() -> {
                        System.out.println(System.currentTimeMillis());
                        use(inviteCode);
                    }
            );
        }
        if (executorService.isTerminated() || executorService.isShutdown()) {
            System.exit(0);
        }

    }


    
    public static void use(InviteCode inviteCode) {
    
        // 1 假设有A、B、C三个线程同时执行到这一步,三者都会判断此时邀请码未使用过    
        if (inviteCode.getStatus() == 0) {
            try {
                // 2 A、B、C只能有一个来使用邀请码的状态,所以需要加锁,保重其余两者不能修改邀请码的状态
                lock.writeLock().lock();
                // 3 这里为什么还要判断邀请码的状态?因为该状态可能被A、B、C三者以外的线程先修改过了,而三者还不知情; 或者,ABC其中一个已经有人执行过4步骤,并释放了锁,其余两者并没有进入1的判断而是直接在等着锁的释放,所以需要再判断一次
                if (inviteCode.getStatus() == 0) {
                    // 4 确保邀请码还没人用过,将其更新为使用
                    inviteCode.setStatus(1);
                    System.out.println("已激活!");
                } else {
                    System.out.println("卡已经被使用!");
                    throw new RuntimeException("卡已经被激活");
                }

            } finally {
                lock.writeLock().unlock();
            }

        }
    }


}


上述的例子,实际上说的是双检索的运用;单例模式如果用双检索的话网上有很多,这里记一个实际运用好了;

上述例子的use方法中,第3步的判断,原因基本就是所写的那些,由于竞态下,可能AB线程停在了2步骤,此时他们认为资源还是可用的;所以当C释放了锁之后,AB线程往下执行时还是要判断一次资源的可用状态;不然就会出现并发问题;
另外,这里使用的是读写锁,读操作不会锁资源,只有写的时候会独占;并没有使用大多数教程写的synchronize轻量同步锁,具体比较另一篇再总结吧。

你看那个人好像一条狗啊

Read Next

设计模式——代理模式与装饰者的关系