并发编程——关于synchronized(内置锁)错误的加锁和原因分析
关于synchronized(内置锁)
首先我们需要了解下关于synchronized关键字的一些基本概念。
- synchronized 内置锁
- Java支持多个线程同时访问一个对象或者对象的成员变量,关键字 synchronizied 可以修饰方法或者代码块的形式来进行使用,它主要是保证了多个线程同一时刻,只能有一个线程处于方法或者代码块中,它保证了线程对变量的可见性和排他性,又称为 内置锁机制。
- 对象锁和类锁
- 对象锁是用于对象实例方法或者一个对象实例上的。类锁是用于类的静态方法或者一个类的class对象上的。
- 一个类可以存在多个对象锁,且不同对象实例的对象锁互不干扰。
- 每个类只有一个class对象,所以每个类只有一个类锁(个人认为类锁的本质其实还是对象锁,只不过它锁的本质是类的class对象)。
- 对象锁和类锁之间互不干扰。
错误的加锁和原因分析
这里我们以Integer为例。
首先,我们写一个类来实现线程,代码如下:
public class TestIntegerSyn {
static class Worker implements Runnable {
private Integer i;
public Worker(Integer i) {
this.i = i;
}
@Override
public void run() {
synchronized (i) {
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + "--@" + System.identityHashCode(i));
i++;
System.out.println(thread.getName() + "-------" + i + "-@" + System.identityHashCode(i));
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(thread.getName() + "-------" + i + "--@" + System.identityHashCode(i));
}
}
}
public static void main(String[] args) throws InterruptedException {
Worker worker = new Worker(1);
Thread.sleep(50);
for (int i = 0; i < 5; i++) {
new Thread(worker).start();
}
}
}
这里,说明下System.identityHashCode(obj)这个方法,jdk官方文档是这样描述的:标识哈希码,返回给定对象的哈希码,该代码与默认的方法 hashCode() 返回的代码一样,无论给定对象的类是否重写 hashCode()。简单点理解就是无论你是否多次重写hashCode()这个方法。System.identityHashCode(obj)这个方法都会返回原生的哈希码。因此,这里我们可以把System.identityHashCode(obj)返回的值近似认为它是一个内存地址(只能近似认为,它实际不是)。
我们先注释掉run()方法里面第一行和最后一行打印,只查看数据输出的结果的打印。下图是我本地运行结果的几个样图。


我们会发现,虽然运行结果有正确的,但是偶尔也会出现我上面的图一样的情况,缺少不同的中间运行结果,这是为什么呢?然后,我们注意到我们打印输出的“标识哈希码”似乎每次也不一样。这又是为什么呢?这里我们先放开先注释掉的打印,再执行看下,执行结果如下(提示:不同的操作系统以及Java环境都会影响运行的结果,你们运行的结果肯定和我这里展示会不一样):

我们发现每次我们在执行 i++ 之前和我们执行之后的“标识哈希码”都会发生变化,这个变化保持的时间是在执行下次 i++ 之前,虽然我们已经对 i 这个Integer 对象的实例加了锁,但这里并没有起作用,这就说明我们这里加锁是无效的。
为了究其原因,我们吧这个类编译的class对象进行反编译,发现 i++ 这个方法在jdk的源码里面是这样实现的。
我们发现在jdk的源码里面它是通过Integer valueOf(int i)这个方法来实现的 i++,然后我们通过看源码发现Integer valueOf(int i)方法jdk的实现方法是这样的:
这里我们看到jdk的源码在执行了一系列的操作之后,最后返回了一个new Integer(i)对象,我们都知道new关键字代表你申明了一个新的对象,也就是说无论是怎么对Integer的对象实例加锁,只要调用了valueOf(int i)这个方法,jdk最后都给你返回了一个新的对象,我们加锁的对象也就发生了变化,而synchronized是要让多个线程抢同一个对象的锁,这里每个线程锁的对象都不一样,也就使得这里的加锁无效。
通过上述demo我们发现对Integer的加锁我们需要去重新考虑直接对Integer对象实例加锁的方式了,其实我们只要明白synchronized关键字锁的对象不发生变化这一点就可以了,这里提供了两种改进方式(仅供参考)。
- 申明一个新的Object对象加锁:
static class Worker implements Runnable {
private Object obj = new Object();
private Integer i;
public Worker(Integer i) {
this.i = i;
}
@Override
public void run() {
synchronized (obj) {
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + "--@" + System.identityHashCode(i));
i++;
System.out.println(thread.getName() + "-------" + i + "-@" + System.identityHashCode(i));
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(thread.getName() + "-------" + i + "--@" + System.identityHashCode(i));
}
}
}
- 锁this(this等同于Worker的对象实例)
static class Worker implements Runnable {
private Integer i;
public Worker(Integer i) {
this.i = i;
}
@Override
public void run() {
synchronized (this) {
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + "--@" + System.identityHashCode(i));
i++;
System.out.println(thread.getName() + "-------" + i + "-@" + System.identityHashCode(i));
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(thread.getName() + "-------" + i + "--@" + System.identityHashCode(i));
}
}
}
探讨了在Java并发编程中使用synchronized关键字时常见的误区,特别是针对Integer对象加锁的无效性,分析了原因并提供了改进方案。
1054





