并发编程——关于synchronized(内置锁)错误的加锁和原因分析

探讨了在Java并发编程中使用synchronized关键字时常见的误区,特别是针对Integer对象加锁的无效性,分析了原因并提供了改进方案。

并发编程——关于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()方法里面第一行和最后一行打印,只查看数据输出的结果的打印。下图是我本地运行结果的几个样图。
synchronized-demo1
synchronized-demo2
  我们会发现,虽然运行结果有正确的,但是偶尔也会出现我上面的图一样的情况,缺少不同的中间运行结果,这是为什么呢?然后,我们注意到我们打印输出的“标识哈希码”似乎每次也不一样。这又是为什么呢?这里我们先放开先注释掉的打印,再执行看下,执行结果如下(提示:不同的操作系统以及Java环境都会影响运行的结果,你们运行的结果肯定和我这里展示会不一样):
synchronized-demo3
  我们发现每次我们在执行 i++ 之前和我们执行之后的“标识哈希码”都会发生变化,这个变化保持的时间是在执行下次 i++ 之前,虽然我们已经对 i 这个Integer 对象的实例加了锁,但这里并没有起作用,这就说明我们这里加锁是无效的。
  为了究其原因,我们吧这个类编译的class对象进行反编译,发现 i++ 这个方法在jdk的源码里面是这样实现的。jdk实现i++
我们发现在jdk的源码里面它是通过Integer valueOf(int i)这个方法来实现的 i++,然后我们通过看源码发现Integer valueOf(int i)方法jdk的实现方法是这样的:Integer valueOf(int i)实现
这里我们看到jdk的源码在执行了一系列的操作之后,最后返回了一个new Integer(i)对象,我们都知道new关键字代表你申明了一个新的对象,也就是说无论是怎么对Integer的对象实例加锁,只要调用了valueOf(int i)这个方法,jdk最后都给你返回了一个新的对象,我们加锁的对象也就发生了变化,而synchronized是要让多个线程抢同一个对象的锁,这里每个线程锁的对象都不一样,也就使得这里的加锁无效。
  通过上述demo我们发现对Integer的加锁我们需要去重新考虑直接对Integer对象实例加锁的方式了,其实我们只要明白synchronized关键字锁的对象不发生变化这一点就可以了,这里提供了两种改进方式(仅供参考)。

  1. 申明一个新的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));
            }
        }
    }
  1. 锁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));
            }
        }
    }

评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值