• 面试官:synchronized可以锁字符串吗?


    你好呀,我是小邹。

    今天给大家分享一个Java面试题:synchronized可以锁字符串吗?

    前言

    在日常项目中可能存在需要防止用户数据并发操作的问题,需要对代码块进行加锁保护。

    例如:用户输入存数据库,重复数据不存DB;用户操作缓存数据等,这里想尽可能把锁的对象放小,因此通常都是锁用户而不是锁整个类或者代码块;然而在用synchronized(userId)的时候可能会存在一些问题。

    synchronized 锁字符串的问题

    使用synchronized锁一个字符串👇

    package thread;
    
    /**
     * @author: 邹祥发
     * @date: 2022/8/22 11:52
     */
    public class ThreadTest implements Runnable {
        @Override
        public void run() {
            synchronized (new String("字符串常量")) {
                //线程进入
                System.out.println(" thread start");
                try {
                    //进入后睡眠
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //线程结束
                System.out.println(" thread end");
            }
        }
    
        public static void main(String[] args) {
            for (int i = 0; i < 3; i++) {
                Thread thread = new Thread(new ThreadTest(), "dd");
                thread.start();
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    运行结果如下:

     thread start
     thread start
     thread start
     thread end
     thread end
     thread end
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    可以发现还是并发执行了,因为synchronized (new String("字符串常量"))锁的对象不是同一个,仅仅是值相等,此时的字符串是在堆栈中。将代码修改为如下

    @Override
    public void run() {
        String str = "字符串常量";
        synchronized (str) {
            //线程进入
            System.out.println(" thread start");
            try {
                //进入后睡眠
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
           }
            //线程结束
           System.out.println(" thread end");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    或者在修改锁的内容为synchronized (new String("字符串常量").intern()),得到运行结果为

     thread start
     thread end
     thread start
     thread end
     thread start
     thread end
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    通过上面结果可以看出此时synchronized 是可以锁住字符串了,但是我们日常中如果通过锁字符串对象的方式是锁不住字符串的。因此字符串对象不是同一个地址,因此如果想要锁住用户ID,需要把用户ID添加到字符串常量池中。如果通过User user = new User()的方式锁user.getUserId()是无法有效锁住用户的

    看下下面的例子

    public static void main(String[] args) {
        String name = "xiaozou";
        String nameObj = new String("xiaozou");
        System.out.println(name.equals(nameObj));
        System.out.println(name == nameObj);
        System.out.println(name == nameObj.intern());
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    运行结果为

    true
    false
    true
    
    • 1
    • 2
    • 3

    通过上面的结果可以看出,name字符串常量和nameObj字符串对象的值相等,地址不同。通过new的对象是在堆栈中,字符串常量是存放在常量池中,通过nameObj.intern()把字符串对象放入常量池中,则地址是同一个

    synchronized 锁字符串用String的intern()存在的问题

    通过上面的demo可以得出,使用synchronized 锁字符串,需要将字符串添加到字符串常量池中。日常使用中通过通过new对象的方式创建对象,再取对象的字段,因此需要使用intern把字符串放入常量池中,但是直接使用String的intern全部把字符串放入常量池会存在一些问题。显然在数据量很大的情况下,将所有字符串都放入常量池是不合理的,常量池大小依赖服务器内存,且只有等待fullGC,极端情况下会导致频繁fullGC。并且在数据量很大的情况下,将字符串放入常量是存在性能问题

    可以用google的guava包的interner类:

    public class test{
        private static Interner<String> lock = Interners.newWeakInterner();
        public void test() {
            synchronized (lock.intern(id.toString())){
    		//do...
    		}
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    Interner是通过MapMaker构造ConcurrentMap来实现弱引用,ConcurrentMap用分段的方式保证安全。这里个人觉得比常量池的优点就在于这里是弱引用的方式,便于map的回收,常量池只能依赖于fullGC,这里的回收在不使用或内存不够用条件下即可被回收(Minor GC阶段)。

    总结

    • synchronized可以锁存活于字符串常量池中的值,不能锁存活于堆栈中的字符串(字符串地址要相同)
    • 可以使用String对象.intern()将该字符串放入字符串常量池中,但是常量池的回收只能依赖于fullGC,故不推荐使用
    • 推荐使用guava包下的interner类,使用弱引用的方式,在内存不足的时候自动进行垃圾回收
  • 相关阅读:
    vue实现a-model弹窗拖拽移动
    Mavenir融合分组核心解决方案将为德国电信在德国的5G独立组网(SA)网络提供支持
    香港写字楼等级如何划分?从3A到C级一文讲明白
    【毕设必备】手把手带你用Python搭建一个简单的后端服务- API的创建,前后端交互的数据传递,GET,POST,JSON,FLASK
    < Python全景系列-3 > Python控制流程盘点及高级用法、神秘技巧大揭秘!
    再谈Android重要组件——Handler(Native篇)
    【前端知识】Three 学习日志(四)—— 相机控件
    算法系列七:十大经典排序算法之——希尔排序
    程序员脱发怎么办
    python 获取视频时长
  • 原文地址:https://blog.csdn.net/Zou_05/article/details/126463778