一、产生了什么问题(What)
设想这样一个场景,我定义了一个全局静态变量count来统计次数,在多线程的情景下,我们的一个解决方案就是对count对象操作时进行同步。然后我们写出了这样一段代码:
public class Main {
static Integer count=0;
public static void main(String[] args) {
Runnable run=()->{
//在count对象上进行同步
synchronized (count){
System.out.println(Thread.currentThread().getName()+"进入");
count++;
try {
//为了演示效果,阻塞线程
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
System.out.println(Thread.currentThread().getName()+"退出");
}
}
};
Thread t1=new Thread(run,"thread-1");
Thread t2=new Thread(run,"thread-2");
t1.start();
try {
//主线程休眠100ms,以保证线程t1修改count
Thread.sleep(1000);
t2.start();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
在理想情况下,允许结果应该为:
//0s后
thread-1进入
//5s后
thread-1退出
//5s后
thread-2进入
//10s后
thread-2退出
然后,实际允许结果为:
//0s后
thread-1进入
//1s后
thread-2进入
//5s后
thread-1退出
//6s后
thread-2退出
可见,在Integer类型的变量count上同步失败。
二、问题怎么产生的(Why)
在count上同步失败,说明t1和t2线程拿到的不是同一个锁,也就是说count对象的地址发生了改变。下面借助joI-core来验证地址是否发生改变:
测试代码:
import org.openjdk.jol.vm.VM;
public class Main {
static Integer count=0;
public static void main(String[] args) {
System.out.println(VM.current().addressOf(count));
count++;
System.out.println(VM.current().addressOf(count));
}
}
运行结果:
3590308504
3590308520
count++操作使对象地址发生了改变,因此导致同步失败。事实上对count对象做运算都会导致对象地址改变。类似的还有String以及基本类型的包装类。
三、如何解决(How)
3.1 通过Integer.valueOf()获取到[-128,127]的对象。
以下是Integer.valueOf()方法的源码
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
如果i在[-128,127]范围内,是直接返回cache中的对象的,自身的count对象不会地址不会发生改变。
这个方法虽然可行,但最大只能计数到127。
3.1 将++操作封装成一个方法,对方法同步。
将count++操作封装为一个同步的方法:
static Integer count=0;
public static synchronized void add(){
count++;
}
Runable调用add()方法:
Runnable run=()->{
//调用同步方法
add();
};