Java使用String作为同步锁的问题

由于String会被存储到常量池中,我们知道,一般不会使用String来作为同步锁,从两方面考虑

  1. 我们用String作为锁,并希望它能像Object一样,不同变量加锁互不影响。然而,有时2个String对象可能指向常量池中同一个字符串,导致其加锁互相影响。一个例子如下,若在2个类中,使用了字符串字面量赋值的方式声明2个String对象,并用synchronized关键字对两个String对象分别加锁,由于字符串常量池,2个String对象中的字符串对象指向的是常量池中同一个内存区域,故2个加锁方法会彼此影响

    有2个类StringSyn1和StringSyn2,它们各自有个lock方法

    package com.yogurt.test;
    
    import java.util.concurrent.TimeUnit;
    
    public class StringSyn1 {
          
          
    
    	private String lockString = "yogurt";
    
    	public void lock() {
          
          
    		synchronized (lockString) {
          
          
    			System.out.println("StringSyn1 get lock");
    			try {
          
          
    				TimeUnit.SECONDS.sleep(2);
    			} catch (InterruptedException e) {
          
          
    				e.printStackTrace();
    			}
    			System.out.println("StringSyn1 is about to exit");
    		}
    	}
    }
    
    package com.yogurt.test;
    
    import java.util.concurrent.TimeUnit;
    
    public class StringSyn2 {
          
          
    
    	private String lockString = "yogurt";
    
    	public void lock() {
          
          
    		synchronized (lockString) {
          
          
    			System.out.println("StringSyn2 get lock");
    			try {
          
          
    				TimeUnit.SECONDS.sleep(2);
    			} catch (InterruptedException e) {
          
          
    				e.printStackTrace();
    			}
    			System.out.println("StringSyn2 is about to exit");
    		}
    	}
    }
    

    再创建一个测试类,运行

    public static void main(String[] args) {
          
          
    		StringSyn1 syn1 = new StringSyn1();
    		StringSyn2 syn2 = new StringSyn2();
    		(new Thread(() -> syn1.lock())).start();
    		(new Thread(() -> syn2.lock())).start();
    }
    

    在这里插入图片描述

    可以看到syn1和syn2依次打印,因为2个类的lockString变量是通过字符串字面量进行赋值,这样的方式使得2个类的lockString变量都指向了常量池中的同一个字符串对象,2个类的lock方法在同一个String对象上进行了同步,故syn2被syn1阻塞。**在实际开发中,我们很难知道用作锁的String对象,是否来自于常量池,是否有其他地方也将其用作锁。**当然,如果将上方lockString的部分改为private String lockString = new String("yogurt");这样2个类中的lockString则分别是2个不同的String对象,则可以看到2个类的lock方法互不影响,syn2没有等到syn1结束后才执行

    在这里插入图片描述

  2. 当String具有业务含义,我们希望,相同内容的字符串,加的是同一把锁。比如使用订单号来加锁,我们希望某一个订单号,同一时刻只能有一个针对该订单号的操作。那么只要订单号相同,加的应是一把锁。当有一个订单号正在被处理时,多个相同订单号的请求进来后,都应该在这把锁上排队等待。这是我们期望的,然而,字符串内容相同,也不能保证是同一个String对象。如上面例子所示,若2个字符串对象都引用自常量池,则它们是同一个对象,可以达到相同内容字符串加同一把锁的效果。但如果2个字符串对象具有相同内容,但却是2个不同的对象呢?比如上面使用new关键字来创建字符串。假设有2个请求,同时到来,它们携带的订单号相同,但是存储订单号的字符串对象是不同的对象,此时加锁就会失败,会导致对同一个订单号同时进行了临界操作

一般只有字符串具有业务含义时,才会需要使用String作为锁,即第二种情况才需要用String做锁。而第一种情况用普通的Object作为锁即可。

那么有什么解决方法呢?最简单的就是利用字符串常量池的特性,调用一下String的intern方法,将String对象放入常量池,再用其做锁,这样就能保证相同内容的字符串变量,指向的是同一个对象了。

但是java原生的intern,不会自动清理常量池,可能会导致频繁的GC。

好在Google的guava包中有一个Interner,可以用其创建基于弱引用的常量池,避免GC问题。使用例子如下

package com.yogurt.test;

import com.google.common.collect.Interner;
import com.google.common.collect.Interners;

public class StringSyn {
    
    

	private Interner<String> stringPool = Interners.newWeakInterner();

	/**
	 * 针对具体订单号,执行临界操作
	 * **/
	public void doCriticalOperation(String orderId) {
    
    
		synchronized (stringPool.intern(orderId)) {
    
    
			//do something
		}
	}
}

猜你喜欢

转载自blog.csdn.net/vcj1009784814/article/details/108982051