有死锁的版本
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();
}
}
}
这么写会死锁:比如线程池只有一个对象
-
线程A获得对象,退出getInstance()释放锁,但是它一致占用了一个信号量。
-
线程B调用getInstance(),获得锁,但是semaphore.acquire()阻塞,所以一直会持有lock
-
线程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()随机休眠,没有发现有什么并发问题。大神发现如果有什么问题,望不灵赐教
总结
-
防止并发问题的最好的办法就是别写并发程序,忘记谁说的,但是有道理
-
真的在写并发程序的时候,一定要注意死锁问题:
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(),千万别搞混了。