为了账号安全,请及时绑定邮箱和手机立即绑定
慕课专栏

目录

索引目录

再学经典:《Effective Java》独家解析

原价 ¥ 68.00

立即订阅
加餐:一道线程安全性面试题
更新时间:2020-07-07 09:40:22
每个人都是自己命运的主宰。——斯蒂尔斯

1.前言

前面小节讲到了线程安全问题的概念,产生线程安全的原因,以及解决线程安全问题的常见方法。

前面章节也讲到了字符串的本质,讲到了字符串的不可变性等概念。

本节将两方面的知识综合运用起来解读一个经典面试题。

2.经典面试题

2.1 面试问题

题目

使用字符串作为 synchronized 的锁对象,在多线程情况下有没有线程安全问题?

一个同学的回答

如果字符串在字符串池中就线程安全,new 出来的字符串在堆中就不是线程安全。

解析

上面同学的回答存在一些问题,

synchronized 关键字要保证线程安全,就要锁的对象为同一个才行。

因此不管是在字符串池中还是非字符串池的堆区域,只要是同一个字符串对象就可以。

另外 Java 8 字符串池已经在堆内,允许垃圾回收。

上面一个看似简单的问题,可以看出一个人的基础是否真正扎实。

2.2 理论和实践相结合

学习最忌讳的是理论脱离实际,结合上面的面试题的场景编写一个模拟的代码。

import java.util.concurrent.TimeUnit;

public class LockThread implements Runnable {

    private static final String PREFIX = "Order-LOCK:";

    private String orderId;

    public LockThread(String orderId) {
        this.orderId = orderId;
    }

    private static String buildLock(String orderId) {
        return PREFIX + orderId;
    }

    @Override
    public void run() {
        synchronized (buildLock(orderId)) {
            System.out.println(Thread.currentThread().getName()+"拿到锁..");
            try {
                TimeUnit.SECONDS.sleep(5);
               // 其他逻辑
            } catch (InterruptedException ignored) {
            }
            System.out.println(Thread.currentThread().getName()+"释放锁..");
        }
    }
}

示例:

public class SafeDemo {

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(new LockThread("127.0.0.1")).start();
        }
    }
}

一次运行的结果:

Thread-0拿到锁…
Thread-1拿到锁…
Thread-2拿到锁…
Thread-3拿到锁…
Thread-4拿到锁…
Thread-0释放锁…
Thread-1释放锁…
Thread-2释放锁…
Thread-4释放锁…
Thread-3释放锁…

发现虽然使用了 synchronized 关键字,但是仍然没有实现多线程同步的效果。

大家可以使用 System.identityHashCode 函数来打印锁对象的哈希值:

String lock = buildLock(orderId);
System.out.println(System.identityHashCode(lock));

可以明显地看到每次都是新的字符串对象。

按照惯例,我们用反编译的方式来查看 buildLock 背后的原理:

buildLock 反编译后的字节码:

 0 new #3 <java/lang/StringBuilder>
 3 dup
 4 invokespecial #4 <java/lang/StringBuilder.<init>>
 7 ldc #6 <Order-LOCK:>
 9 invokevirtual #7 <java/lang/StringBuilder.append>
12 aload_0
13 invokevirtual #7 <java/lang/StringBuilder.append>
16 invokevirtual #8 <java/lang/StringBuilder.toString>
19 areturn

基本等价于下面的写法:

private static String buildLock(String orderId) {
    StringBuilder stringBuilder = new StringBuilder();
    stringBuilder.append(PREFIX);
    stringBuilder.append(orderId);
    return stringBuilder.toString();
}

其中 StringBuilder#toString 的源码如下:

@Override
public String toString() {
    // Create a copy, don't share the array
    return new String(value, 0, count);
}

因此字符串常量和局部变量的 + 号拼接方式,转换为 StringBuilder 方式,最终通过 toString 底层通过 new String 的方式创建新的字符串对象。

那么如何保证用到同一把锁呢

我们去 String 类里可以找到答案, 我们可以使用 String.intern 函数:

/**
 * 该函数的主要功能是返回字符串对象的规范表示形式。
 * 如果字符串池已经包含了通过 equals 函数判断到和当前字符串对象相等的对象,则返回字符串池中的对象。
 * 否则将此字符串对象添加到池中,返回这个字符串对象的引用。
 * 任何两个字符串对象 s 和 t , `s.equals(t) ` ,那么 `s.intern() == t.intern()` 。
 *
 * 所有的字符串字面量和字符串常量表达式都是 interned。
 *
 * @return  返回和此字符串对象内容相同的字符串对象, 保证字符串池中对象的唯一性.
 */
public native String intern();

因此 synchronized (buildLock(orderId)) 改为 synchronized (buildLock(orderId).intern()) 即可。

@Override
public void run() {
    synchronized (buildLock(orderId).intern()) {
        System.out.println(Thread.currentThread().getName() + "拿到锁..");
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException ignored) {
        }
        System.out.println(Thread.currentThread().getName() + "释放锁..");
    }
}

修改后的输出:

Thread-0拿到锁…
Thread-0释放锁…
Thread-4拿到锁…
Thread-4释放锁…
Thread-3拿到锁…
Thread-3释放锁…
Thread-2拿到锁…
Thread-2释放锁…
Thread-1拿到锁…
Thread-1释放锁…

3.总结

本文我们通过读源码、反编译,结合前面讲到的字符串、线程安全相关知识,来研究一个看似简单的面试题。

希望通过这个简单的面试题,让大家明白本专栏所讲述方法的通用性,让大家了解如何学以致用。

}
立即订阅 ¥ 68.00

你正在阅读课程试读内容,订阅后解锁课程全部内容

千学不如一看,千看不如一练

手机
阅读

扫一扫 手机阅读

再学经典:《Effective Java》独家解析
立即订阅 ¥ 68.00

举报

0/150
提交
取消