Synchronized 易错点之锁Integer对象
背景:
继承ThreadPoolExecutor,重写afterExecute方法,实现监听线程池任务结束。
错误:
public class ThreadPools extends ThreadPoolExecutor {
private Integer tasksNum;
public int getTasksNum() {
return tasksNum;
}
public void setTasksNum(int tasksNum) {
this.tasksNum = tasksNum;
}
public ThreadPools(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
minuxTask();
}
@Override
protected void beforeExecute(Thread t, Runnable r) {
super.beforeExecute(t, r);
}
private void minuxTask() {
synchronized (tasksNum) {
//这里对象变了, 竞争的资源发生了改变,即存在了多把锁
// 错误出现在这里,taskNum对象不仅数值发生了变化,指向的对象也发生了变化
tasksNum -= 1;
Log.d("tag", "afterExecute: After " + taskNum);
}
}
}
分析:
首先,synchronized是java的关键字,用锁的来实现同步,一般用来修饰共享资源。比如票务系统的票数,银行卡里的余额此类的。上述代码在运行的时候,就出现了如下问题taskNum跳着变化,而不是“27-26-25-24·······”。乍看好像没什么问题,仔细一看还是没什么问题,但为什么锁不住呢?这时,就需要考虑——这里多线程竞争的锁是不是同一个(或在这里是不是存在多把锁)。注意,synchronized锁的是对象,Object,不能是int、long这样的基础类型。所以,我在这修饰的是Integer类型,**注意是Integer类型,Integer,Integer**,基础好的同学估计已经明白过来了。对,就是因为锁的是Integer类型,才出现的同步错误。现在,继续分析。
先来个小例子
Integer a = new Integer(10);
Integer b = new Integer(10);
System.out.println(a == b);
System.out.println(a.equals(b);
第一个输出,打出来的是false。第二个,打出来的是true。因为,在Integer类中,重写了equals方法:
public boolean equals(Object obj) {
if (obj instanceof Integer) {
return value == ((Integer)obj).intValue();
}
return false;
}
// 这里是返回的值--int
public int intValue() {
return value;
}
可以发现,Integer的equal方法是在对值进行比较。而“==”是在比较Object对象,也就是在a和b的指向,很明显a和b虽然值相同,但所指的对象并不相同。假如我现在对a加锁,对b会有什么影响吗?显然是一点也没有。明白这里,我们返回到上面的taskNum那块。在Integer类中,做了缓存。
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
这里,我们可以看见,如果low<=i<=high,直接返回cache[i + (-IntegerCache.low)]对象,如果超过,在new一个新对象。比如taskNum = 29,则 taskNum -= 1,taskNum = 28,数值是对的,但是此时的taskNum指向的对象就发生了变化。如下:
指向的对象,发生了变化。原本对taskNum(29)加的锁,现在taskNum指向了另一个对象(28),然而taskNum(28)可并没有上锁,对于堵在外面的线程,一看现在taskNum没有锁,就开始竞争taskNum(28)这个对象了,所以就出现了锁不住的情况,也就是竞争的共享资源名字虽然一样,但是具体的对象实例并不一样了(地址改变了)。
解决:
首先,根本没有必要把思维固定在synchronized上,可以用其他方法解决。比如这个大佬的解法[Synchronized-锁失效问题剖析_synchronized 失效-优快云博客
我还没完全明白大佬的解法,我这里就是退一步,扩大锁的范围。即
synchronized (this) {
tasksNum -= 1;
}
通过锁具体的线程池实例,来解决同步问题。当然具体情况,具体分析。我这里可以这样写,是因为我的并发小。如果并发量,特别大,就很容易造成线程阻塞的问题。锁变量的性能肯定要优于锁对象的性能。
总结:
又学到了新知识好吧。一看多线程资料全会,简单。一写,就出错。哈哈,好活当赏。希望大佬纠错,也希望能够帮助遇到这个问题的同学们。下篇再见😀