首先大家对java中的synchronized关键字肯定很熟悉了,也是java多线程中实现线程同步用的最多的关键字之一,但是最近笔者在使用关键字synchronized对Integer进行加锁时,却发现了一个很奇怪的现象,借此机会对相关知识进行了查阅总结,得出了下面的新的体会,如果有什么不合理的地方,欢迎留言。
1. 最基本的使用方法
首先我们看一个很普通的多线程例子:账户取钱
a)我们先定义一个简单的账户类如下:
public class Account {
private double amount;
private String countId;
public Account(String id,double amount){
this.countId = id;
this.amount = amount;
}
public double draw(double money){
this.amount = this.amount - money;
return this.amount;
}
public String getCounntId() {
return countId;
}
public void setCounntId(String counntId) {
this.countId = counntId;
}
public double getAmount() {
return amount;
}
public void setAmount(double money) {
this.amount = money;
}
}
b) 接下来实现线程实体:
public class ThreadDemo implements Runnable {
private Account account;
public ThreadDemo(Account account){
this.account = account;
}
@Override
public void run() {
for(int i =0; i<500;i++) {
synchronized (account) {
account.draw(1);
System.out.println(System.identityHashCode(account));
System.out.println(Thread.currentThread().getName() +
":id =" + account.getCounntId() + ":amount=" + account.getAmount());
}
}
}
这个例子很普通,在main函数中声明和启动线程:可以很好的实现账户金额的同步问题。
Account account = new Account("XXXX",1000);
Thread thread1 = new Thread(new ThreadDemo(account));
Thread thread2= new Thread(new ThreadDemo(account));
thread1.start();
thread2.start();
2. 如果我们把加锁对象换成了Integer时,并且声明为static类型:
public class ThreadDemo implements Runnable {
public static Integer a = 0;
@Override
public void run() {
try {
for (int i=0;i<500;i++){
synchronized (a) {
// System.out.println(System.identityHashCode(a));
a++;
// int b = a;
System.out.println(Thread.currentThread() + ":" + a);
}
}
}catch (Exception e){}
}
这时如果启动线程后,我们启动线程后,会发现如下情况:两个线程输出的数据会有重叠的数字,那么这是不是两个线程对同一个值进行了自增操作,但是多次试验后,笔者发现最终a的值都会稳定的自增到1000,也就是说,两个线程的自增操作并没有重叠,最终都加在了a上,而且如果我们把上面的代码int b =a;这行代码添加进去,并且输出b,就会发现b显示的值是稳步增加的。下面我们就对该情况进行深入的分析。
(1)首先synchronized关键字是“无法锁住”integer对象的。
注意上面的无法锁住是加了引号的,首先看一下Integer的源码:
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]之间时,Integer会在常量池中取值,如果不在这之间时,此时会生成新的Integer对象,也就是Integer对象发生了变化。
那么既然锁不住Integer对象,为什么b的值会稳步增加,而且最终a会安全稳定的自增到1000 ?
所以上面的无法锁住是加了引号的,并不是真的锁不住,而是针对一个对象只锁住了一次,例如a = 10时,这时另外的线程是对a=10的Integer对象无法操作的,要等待a++操作完成,但是一旦a++操作完成后,a对象就发生了变化,这时另外一个线程就可以获得a=11的对象锁,至于为什么b的值不会出现重复值,应该是线程内部操作太快,另外一个线程还来不及执行加锁自增操作,b已经获得加1之后的值,所以b只自增了一次;
为了验证上述猜想,做了以下实验,就是在int b =a之前增加一行代码 Thread.sleep(10):
这时候可以清晰的发现,b也会输出相同的值,但是重复度最多为2,这就是说发生了下述过程:
a=0时,线程0对a加锁,自增后,sleep(10)之间,线程1对a加锁,自增后,变成了2,这时线程0和线程1都没对a=2加锁,线程0此时运行b=a,输出b,所以会输出b的值为2,这时线程0进行下一次循环,对a=2加锁,重复上述过程。
并且通过代码 System.out.println(System.identityHashCode(a)); 也可以发现a对象发生了变化。
通过以上分析,所以a还是会稳定的自增至1000。同时这也解释了为什么println的值也出现了上述情况。
3. 关于println的实验
首先run方法里面的操作变成了如下,其中b为static int b = 1:
@Override
public void run() {
while (true){
//System.out.println(b);
if(b.equals(2))
break;
}
System.out.println("A is finished!");
}
启动线程:
ThreadDemo a = new ThreadDemo();
new Thread(a).start();
Thread.sleep(3000);
a.b = 2;
//阻塞住主线程
while (true){}
相信很多人看到这两个代码的时候,大多数人会认为线程在启动一段时间后,随着代码
a.b = 2;的运行,线程会停止,但是很遗憾,并不会停止。
因为在while循环内会缓存变量的值,其实while循环的代码和如下代码的机制是相同的:
if(b == 1) while (true){
// System.out.println(b);
if(b.equals(2))
break;
}
System.out.println("A is finished!");
也就是说,b = 1 的值被缓存了,但是如果将代码System.out.println(b);的注释去掉,线程就可以正常退出,这里的原因是println()函数中会进行加锁操作,而jvm对于这个加锁操作,会做一件事,不缓存线程变量!也就是b =1;这个值将不会再被缓存,其他详细的讲解可以参看:点击打开链接
public void println(int x) {
synchronized (this) {
print(x);
newLine();
}
}
4. 总结
通过上面一系列的介绍,这里做一个小小的总结:
- 在多线程中最好不要直接对基本数据类型对象进行加锁来实现线程同步,对于static全局变量,最好的方式是对类(不是类对象)进项加锁。
其实最后一个例子中为b变量增加关键字volatile修饰也可以实现线程退出,或者将println换成syn(this)也可以,其实synchronized关键字的很重要的作用也是保证线程能够获得修改后的共享变量的值,但是如果syn(this)放在while的外面就没办法停止,因为while还是会缓存线程变量。
分享到此结束,谢谢阅读!