简单对象池(享元模式)的实现

有死锁的版本

public class ObjectPool<T> {
    private static final Logger LOGGER = LoggerFactory.getLogger(ObjectPool.class);
    private final Lock lock = new ReentrantLock();

    private Semaphore semaphore;
    private int size;
    private List<T> objs;
    private volatile boolean[] checkOut;

    public ObjectPool(Class<T> clazz, int size) {
        semaphore = new Semaphore(size);
        objs = new ArrayList<>(size);
        this.size = size;
        checkOut = new boolean[size];
        for (int i = 0; i < size; i++) {
            try {
                objs.add(clazz.newInstance());
            } catch (InstantiationException e) {
                LOGGER.error("初始化失败", e);
                throw new BaobabRuntimeException("初始化失败:检查clazz是否有默认构造方法");
            } catch (IllegalAccessException e) {
                LOGGER.error("类访问权限不足", e);
                throw new BaobabRuntimeException("初始化失败:检查clazz是否为public");
            }
        }
    }

    public T getInstance() {
        // 多个线程同时获得信号量,然后去改变checkOut数组,就会有问题,所以这里需要同步
        int idleIndex = -1;
        try {
            lock.lock();
            semaphore.acquire();
        
            for (int i = 0; i < checkOut.length; i++) {
                if (!checkOut[i]) {
                    idleIndex = i;
                    break;
                }
            }
            if (idleIndex == -1) {
                semaphore.release();
                return null;
            }

            checkOut[idleIndex] = true;
            return objs.get(idleIndex);
        } catch (InterruptedException e) {
            LOGGER.error("获取对象被中断,{}", checkOut[idleIndex], e);
            return null;
        } finally {
            lock.unlock();
        }
    }

    public void release(T obj) {

        try {
            lock.lock();

            int index = objs.indexOf(obj);
            if (index < 0) {
                LOGGER.error("不是池中的对象");
                return;
            }
            checkOut[index] = false;
            initObj(obj);
            semaphore.release();
        } finally {
            LOGGER.info("释放对象:thread={},checkout={}", Thread.currentThread().getId(), Arrays.toString(checkOut));
            lock.unlock();
        }
    }

    public void destroy() {
        semaphore = null;
        objs = null;
        size = 0;
        checkOut = null;
    }

    private void initObj(T obj) {
        Field[] fields = obj.getClass().getDeclaredFields();
        Method[] methods = obj.getClass().getDeclaredMethods();
        try {
            for (Field f : fields) {
                f.setAccessible(true);
                f.set(obj, null);
            }
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

这么写会死锁:比如线程池只有一个对象

  1. 线程A获得对象,退出getInstance()释放锁,但是它一致占用了一个信号量。

  2. 线程B调用getInstance(),获得锁,但是semaphore.acquire()阻塞,所以一直会持有lock

  3. 线程A使用完对象,调用relase(),需要去获得lock,但是lock被线程A占用

    这就形成了:线程A占用信号量,等待lock;线程B占用lock,等待信号量,死锁了。

解决死锁

解决方式就是:

semaphore.acquire()使用具有超时的接口,如果形成死锁,超时后摆脱死锁返回null。但是这就会导致,明明对象池是有可用对象(线程A释放后就有了),但是拿到的对象是空。

解决这个的方式:semaphore.acuuire()使用非阻塞,如果获得信号量失败,释放锁然后休眠。在其他线程释放锁的时候唤醒。

public class ObjectPoolV1<T> {
    private static final Logger LOGGER = LoggerFactory.getLogger(ObjectPoolV1.class);
    private final Lock lock = new ReentrantLock();
    private final Condition acquired = lock.newCondition();

    private Semaphore semaphore;
    private int size;
    private List<T> objs;
    private volatile boolean[] checkOut;

    public ObjectPoolV1(Class<T> clazz, int size) {
        semaphore = new Semaphore(size);
        objs = new ArrayList<>(size);
        this.size = size;
        checkOut = new boolean[size];
        for (int i = 0; i < size; i++) {
            try {
                objs.add(clazz.newInstance());
            } catch (InstantiationException e) {
                LOGGER.error("初始化失败", e);
                throw new BaobabRuntimeException("初始化失败:检查clazz是否有默认构造方法");
            } catch (IllegalAccessException e) {
                LOGGER.error("类访问权限不足", e);
                throw new BaobabRuntimeException("初始化失败:检查clazz是否为public");
            }
        }
    }

    public T getInstance() {
        // 多个线程同时获得信号量,然后去改变checkOut数组,就会有问题,所以这里需要同步
        int idleIndex = -1;
        try {
            lock.lock();

            // TODO 这里加次数限制
            // 目的是防止死锁:当不能获得信号量的时候,释放锁休眠,一遍让其他线程可以获得锁来释放对象。
            while (!semaphore.tryAcquire(1)) {
                acquired.await();
            }

            lock.lock();
            for (int i = 0; i < checkOut.length; i++) {
                if (!checkOut[i]) {
                    idleIndex = i;
                    break;
                }
            }
            if (idleIndex == -1) {
                semaphore.release();
                return null;
            }

            checkOut[idleIndex] = true;
            return objs.get(idleIndex);
        } catch (InterruptedException e) {
            LOGGER.error("获取对象被中断,{}", checkOut[idleIndex], e);
            return null;
        } finally {
            lock.unlock();
        }
    }

    public void release(T obj) {

        try {
            lock.lock();

            int index = objs.indexOf(obj);
            if (index < 0) {
                LOGGER.error("不是池中的对象");
                return;
            }
            checkOut[index] = false;
            initObj(obj);
            semaphore.release();
        } finally {
            LOGGER.info("释放对象:thread={},checkout={}", Thread.currentThread().getId(), Arrays.toString(checkOut));
            acquired.signalAll();
            lock.unlock();
        }
    }

    public void destroy() {
        semaphore = null;
        objs = null;
        size = 0;
        checkOut = null;
    }

    private void initObj(T obj) {
        Field[] fields = obj.getClass().getDeclaredFields();
        Method[] methods = obj.getClass().getDeclaredMethods();
        try {
            for (Field f : fields) {
                f.setAccessible(true);
                f.set(obj, null);
            }
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

信号量和Lock对调

看起来好像是没问题了,但是这么写真的好么?

思考一个问题:有了信号量,为哈还要用Lock进行防护?

因为信号量是允许多个线程访问临界区的(池化也应该如此),但是多个线程更改checkOut数组就会有问题,所以需要加锁。

所以上面的写法是有问题的,应该是用信号量来防护管程,而不是管程防护信号量,即Semaphore#acquire应该在lock外面。

public class ObjectPoolV2<T> {
    private static final Logger LOGGER = LoggerFactory.getLogger(ObjectPoolV2.class);
    private final Lock lock = new ReentrantLock();

    private Semaphore semaphore;
    private int size;
    private List<T> objs;
    private volatile boolean[] checkOut;

    public ObjectPoolV2(Class<T> clazz, int size) {
        semaphore = new Semaphore(size);
        objs = new ArrayList<>(size);
        this.size = size;
        checkOut = new boolean[size];
        for (int i = 0; i < size; i++) {
            try {
                objs.add(clazz.newInstance());
            } catch (InstantiationException e) {
                LOGGER.error("初始化失败", e);
                throw new BaobabRuntimeException("初始化失败:检查clazz是否有默认构造方法");
            } catch (IllegalAccessException e) {
                LOGGER.error("类访问权限不足", e);
                throw new BaobabRuntimeException("初始化失败:检查clazz是否为public");
            }
        }
    }

    public T getInstance() {

        int idleIndex = -1;
        try {
            semaphore.acquire();
        } catch (InterruptedException e) {
            LOGGER.error("获得对象被中断", e);
            return null;
        }

        // 多个线程同时获得信号量,然后去改变checkOut数组,就会有问题,所以这里需要同步
        try {
            lock.lock();

            for (int i = 0; i < checkOut.length; i++) {
                if (!checkOut[i]) {
                    idleIndex = i;
                    break;
                }
            }
            if (idleIndex == -1) {// 有Semaphore,实际上进不到这个if里面
                semaphore.release();
                return null;
            }

            checkOut[idleIndex] = true;
            return objs.get(idleIndex);
        } finally {
            lock.unlock();
        }
    }

    public void release(T obj) {

        try {
            lock.lock();

            int index = objs.indexOf(obj);
            if (index < 0) {
                LOGGER.error("不是池中的对象");
                return;
            }
            checkOut[index] = false;
            initObj(obj);
            semaphore.release();
        } finally {
            lock.unlock();
        }
    }

    public void destroy() {
        semaphore = null;
        objs = null;
        size = 0;
        checkOut = null;
    }


    private void initObj(T obj) {
        Field[] fields = obj.getClass().getDeclaredFields();
        Method[] methods = obj.getClass().getDeclaredMethods();
        try {
            for (Field f : fields) {
                f.setAccessible(true);
                f.set(obj, null);
            }
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

但是这里需要注意的是,示例中用的是lock.lock(),是不响应中断的,如果使用响应中断信号的,那么需要在catch InterruptedException后Semaphore#release(),防止有对象始终被信号量锁住,不能共享。

性能优化

对象池中所有对象和释放公用一把锁,那么对象的获取和释放都是串行的,所以效率不是很高,但是好在获得锁和释放锁的操作都很轻量,锁占用时间比较短只是获得锁中有个for循环,是O(n)的操作,以及释放对象 时候有个indexOf(),同样也是O(n)操作。但是好在对象池一般也不会特别大,否则失去意义了,所以O(n)操作也可以接受。

这里也可以用两个队列存储对象:一个是占用的,一个是空闲的。 * 获得对象,直接从空闲队列出队一个,放大占用队列。回收的时候,从占用队列出队,放入空闲队列。

public class ObjectPoolV3<T> {
    private static final Logger LOGGER = LoggerFactory.getLogger(ObjectPoolV3.class);
    private final Lock lock = new ReentrantLock();

    private Semaphore semaphore;
    private int size;
    private LinkedList<T> acquiredObjes;
    private LinkedList<T> idleObjects;
    private volatile boolean[] checkOut;

    public ObjectPoolV3(Class<T> clazz, int size) {
        semaphore = new Semaphore(size);
        idleObjects = new LinkedList<>();
        acquiredObjes = new LinkedList<>();
        this.size = size;
        checkOut = new boolean[size];
        for (int i = 0; i < size; i++) {
            try {
                idleObjects.add(clazz.newInstance());
            } catch (InstantiationException e) {
                LOGGER.error("初始化失败", e);
                throw new BaobabRuntimeException("初始化失败:检查clazz是否有默认构造方法");
            } catch (IllegalAccessException e) {
                LOGGER.error("类访问权限不足", e);
                throw new BaobabRuntimeException("初始化失败:检查clazz是否为public");
            }
        }
    }

    public T getInstance() {
        try {
            semaphore.acquire();
        } catch (InterruptedException e) {
            LOGGER.error("获得对象被中断", e);
            return null;
        }

        // 多个线程同时获得信号量,然后去改变checkOut数组,就会有问题,所以这里需要同步
        try {
            lock.lock();

            T e = idleObjects.remove();
            if (e == null) { // 其实semaphore保证idleObjects.remove()不会是null
                return null;
            }
            acquiredObjes.add(e);
            return e;
        } finally {
            lock.unlock();
        }
    }

    public void release(T obj) {

        try {
            lock.lock();

            boolean removed = acquiredObjes.remove(obj);
            if (!removed) {
                LOGGER.error("不是池中的对象");
                return;
            }
            initObj(obj);
            idleObjects.add(obj);
            semaphore.release();
        } finally {
            lock.unlock();
        }
    }

    public void destroy() {
        semaphore = null;
        acquiredObjes = null;
        idleObjects = null;
        size = 0;
        checkOut = null;
    }

    private void initObj(T obj) {
        Field[] fields = obj.getClass().getDeclaredFields();
        try {
            for (Field f : fields) {
                f.setAccessible(true);
                f.set(obj, null);
            }
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

这种方式既节省了时间,又节省了空间。少了一个checkOut数组,虽然对象集合变成两个,但是总的内存没有增加,两者之和一定等于size

注意:示例代码中,有些npe的判断就没写了,暂时忽略吧。

一个简单的对象池测试程序

public class ObjectPoolTest {
    public static void main(String[] args) throws InterruptedException, BuzException {
        ObjectPoolV3<Customer> objPool = new ObjectPoolV3<>(Customer.class, 1);

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程1开始获得:"+Thread.currentThread().getId());

                Customer obj = objPool.getInstance();
                obj.setCustomerId(1L);
                System.out.println("多线程=" + Thread.currentThread().getId() + "obj.customerId=" + obj.getCustomerId());
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                objPool.release(obj);
            }
        }, "objectPooltest1").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程2开始获得:"+Thread.currentThread().getId());
                Customer obj = objPool.getInstance();
                obj.setCustomerId(2L);
                System.out.println("多线程=" + Thread.currentThread().getId() + "obj.customerId=" + obj.getCustomerId());
                objPool.release(obj);
            }
        }, "objectPooltest2").start();

        TimeUnit.SECONDS.sleep(5);
        System.out.println("done");

    }
}

大概面熟一下改进思路:

其实一开始写的时候,写出来的是第二版,按预想,两个线程都能获得对象,但是,运行的时候,一定是有npe。又仔细看代码后发现问题,所以干脆补充一个死锁的写法,为了解决死锁,自己意淫出一个Condition的写法,这个啥时候突然想起《think in java》中介绍Semaphore好像有个对象池的例子,记忆中一定没用Condition,大牛的例子,怎么可能会有死锁。所以去翻了一下,发现区别就在于Semaphore在lock的外面,所以有了第三版。

在第三版的基础之上,感觉缺点啥,就是所有的操作都是串行的,所以想到了第四版。这就是大概写这个对象池的思路。

凡是在细粒度锁控制的时候,或者多个锁组合的时候,一定要注意死锁的问题。比如这里Semaphore和Lock,比较容易忽略死锁的问题。

对象池没有经过严格的压测,仅仅是在上述的测试程序多建线程以及getInstance()和release()随机休眠,没有发现有什么并发问题。大神发现如果有什么问题,望不灵赐教

总结

  1. 防止并发问题的最好的办法就是别写并发程序,忘记谁说的,但是有道理

  2. 真的在写并发程序的时候,一定要注意死锁问题:

    a、尽量使用带有超时参数的锁获取接口,这样即使出现了死锁,因为超时会破坏死锁必要条件的"不可抢夺"条件,也能从死锁中挣脱,不至于随着程序运行一堆线程都会锁死。

    b、凡是用到多个锁组合的时候,特别注意。

    (1) 谨防多把锁防护一个共享资源的情况,一旦出现这种情况,并发控制会失效,且比较不好排查。使用Lock其实不太容易出现这个问题的,因为一个Lock对象对应的就是一把锁,但是使用synchronized的时候就要格外注意了,

    相对来说,synchronized的用法,锁的概念体现的不强。

    (2) 特别注意死锁。如本例。感觉上只是用到了一把锁lock,所以容易忽略这个问题。但实际上:锁可以认为是互斥量的实现,Semaphore是信号量的实现。当信号量中信号数为1(permissionCount=1),即new Semaphore(1)的

    时候,Semaphore就是一个锁,所以可以说互斥量仅仅是信号量的一个特例而已。

    c. 如果使用了条件变量(Lock中的Condition接口,或者synchronized配合Object#wait()/Object#notify()/notifyAll())的时候,谨防错过notify信号,导致有线程无限阻塞到条件上。

    d. 如果使用Lock的Condition接口,一定是和Condition#await()和Condition#signal()/Condition#signalAll(),以为Condition的实现一定是继承Object的,所以也会有Object#wait()/Object#notify()/notifyAll(),千万别搞混了。

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值