JAVA中synchronized和String引出的一系列内容

背景

最近,在开发一个功能,由于会存在并发问题(发生几率不大),因此想上个锁避免一下,但是又因为处于性能考虑,不想锁整个方法或者都去锁住同一个对象,这样会使得所有请求进入这个方法后,都会变成串行进行排队,但是很多时候,不同的请求之间是没有资源竞争的,应该是可以并行的,对于有竞争的请求,才应该采取串行的方式。该方法只有一个String类型的入参,于是我一开始就打算用这个入参作为被锁的对象,因为一开始简单的想着,string类型的都是在常量池中的,返回的都是同一个对象,理论上可以做到入参为一样的字符串时,串行执行,不一样的字符串时,并行执行。但是谨慎起见,在自己写了几个相关测试后,发现背后内容挺多的。


测试一:

测试结果:符合预期。一样的字符串,执行变为串行。不一样的字符串,执行仍然是并行。

测试代码:

public class Test {

	public static void main(String... args) throws Exception {
		new Thread() {
			public void run() {
				try {
					remoteCall("102017071807514199868487451");
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			};
		}.start();

		new Thread() {
			public void run() {
				try {
					remoteCall("102017071807514199868487451");
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			};
		}.start();

		new Thread() {
			public void run() {
				try {
					remoteCall("1111111111111111111111111111");
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			};
		}.start();

	}

	private static void remoteCall(String aaa) throws InterruptedException {
		System.out.println("进入方法:" + Thread.currentThread().getName() + "数据为:" + aaa + " hashcode:" + aaa.hashCode() + " 执行时间:" + System.currentTimeMillis());

		synchronized (aaa) {
			System.out.println("进入锁内:" + Thread.currentThread().getName() + "数据为:" + aaa + " hashcode:" + aaa.hashCode() + " 执行时间:" + System.currentTimeMillis());
			Thread.sleep(5000);
		}
		System.out.println("退出锁:" + Thread.currentThread().getName() + "数据为:" + aaa + " hashcode:" + aaa.hashCode() + " 执行时间:" + System.currentTimeMillis());
	}

}
输出结果:

进入方法:Thread-0数据为:102017071807514199868487451 hashcode:618974246 执行时间:1501125434326
进入锁内:Thread-0数据为:102017071807514199868487451 hashcode:618974246 执行时间:1501125434326
进入方法:Thread-1数据为:102017071807514199868487451 hashcode:618974246 执行时间:1501125434327
进入方法:Thread-2数据为:1111111111111111111111111111 hashcode:-872251968 执行时间:1501125434328
进入锁内:Thread-2数据为:1111111111111111111111111111 hashcode:-872251968 执行时间:1501125434328
退出锁:Thread-0数据为:102017071807514199868487451 hashcode:618974246 执行时间:1501125439327
进入锁内:Thread-1数据为:102017071807514199868487451 hashcode:618974246 执行时间:1501125439327
退出锁:Thread-2数据为:1111111111111111111111111111 hashcode:-872251968 执行时间:1501125439329
退出锁:Thread-1数据为:102017071807514199868487451 hashcode:618974246 执行时间:1501125444327

从输出结果来看,我们的目的好像是达到了。但是如果我们对String类型有一定了解的话,其实就会发现上面的方法有一个很大的漏洞,那就是我们的字符串并不是动态传入的。所谓的不是动态传入的意思就是,我们要传入方法的字符串,其实在程序编译期间就已经知道了。问题就出在了这里:

在程序编译期,编译程序先去字符串常量池检查,是否存在“102017071807514199868487451”,如果不存在,则在常量池中开辟一个内存空间存放“102017071807514199868487451”;如果存在的话,则不用重新开辟空间。然后在栈中开辟一块空间,命名为变量名,存放的值为常量池中“102017071807514199868487451”的内存地址。

因此,上面的两个字符串“102017071807514199868487451”是同一个对象,因此锁就生效了。但是可惜的是,我的程序关于这个入参的字符串内容,很多时候都是动态生成的,也就是编译期间是不知道的,于是就有了下面的测试二。

测试二:

测试结果:不符合预期。一样内容的字符串都是并行了。

测试代码:

public class Test {

	public static void main(String... args) throws Exception {
		new Thread() {
			public void run() {
				try {
					remoteCall("102017071807514199868487451");
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			};
		}.start();

		new Thread() {
			public void run() {
				try {
					Scanner scanner = new Scanner(System.in);
					remoteCall(scanner.nextLine());
					scanner.close();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			};
		}.start();
	}

	private static void remoteCall(String aaa) throws InterruptedException {
		System.out.println("进入方法:" + Thread.currentThread().getName() + "数据为:" + aaa + " hashcode:" + aaa.hashCode() + " 执行时间:" + System.currentTimeMillis());
		synchronized (aaa) {
			System.out.println("进入锁内:" + Thread.currentThread().getName() + "数据为:" + aaa + " hashcode:" + aaa.hashCode() + " 执行时间:" + System.currentTimeMillis());
			Thread.sleep(5000);
		}
		System.out.println("退出锁:" + Thread.currentThread().getName() + "数据为:" + aaa + " hashcode:" + aaa.hashCode() + " 执行时间:" + System.currentTimeMillis());
	}

}
输出结果:
进入方法:Thread-0数据为:102017071807514199868487451 hashcode:618974246 执行时间:1501126502734
进入锁内:Thread-0数据为:102017071807514199868487451 hashcode:618974246 执行时间:1501126502734
102017071807514199868487451
进入方法:Thread-1数据为:102017071807514199868487451 hashcode:618974246 执行时间:1501126503698
进入锁内:Thread-1数据为:102017071807514199868487451 hashcode:618974246 执行时间:1501126503698
退出锁:Thread-0数据为:102017071807514199868487451 hashcode:618974246 执行时间:1501126507735
退出锁:Thread-1数据为:102017071807514199868487451 hashcode:618974246 执行时间:1501126508699
从输出结果可以明显看出,我在控制台中再一次输入102017071807514199868487451后,发现它立刻就获得锁了。虽然hashcode和字符串内容都一样,但是从它立马获得锁我们就可以明显看出,两个102017071807514199868487451字符串并不是同一个对象。

要知道hashcode虽然相同,但是因为这个是一个可以复写的方法,所以因此对于String的hashcode而言,它是不具有唯一性的,这一点我们从可以从String的源码中看出:

public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }
又因为,当我们使用Scanner通过System.in从控制台读取用户的输入时,其实是产生了一个新的对象,这一点也可以轻松从Scanner的源码中看出:
 public String nextLine() {
        if (hasNextPattern == linePattern())
            return getCachedResult();
        clearCaches();

        String result = findWithinHorizon(linePattern, 0);
        if (result == null)
            throw new NoSuchElementException("No line found");
        MatchResult mr = this.match();
        String lineSep = mr.group(1);
        if (lineSep != null)
            result = result.substring(0, result.length() - lineSep.length());
        if (result == null)
            throw new NoSuchElementException();
        else
            return result;
    }
其实,我们可以修改一下remoteCall方法,再测试一次,就非常明白了:

private static void remoteCall(String aaa) throws InterruptedException {
		System.out.println("真正的内存地址:" + System.identityHashCode(aaa));
		System.out.println("进入方法:" + Thread.currentThread().getName() + "数据为:" + aaa + " hashcode:" + aaa.hashCode() + " 执行时间:" + System.currentTimeMillis());
		synchronized (aaa) {
			System.out.println("进入锁内:" + Thread.currentThread().getName() + "数据为:" + aaa + " hashcode:" + aaa.hashCode() + " 执行时间:" + System.currentTimeMillis());
			Thread.sleep(5000);
		}
		System.out.println("退出锁:" + Thread.currentThread().getName() + "数据为:" + aaa + " hashcode:" + aaa.hashCode() + " 执行时间:" + System.currentTimeMillis());
	}
此时的输出为:
真正的内存地址:1616247035
进入方法:Thread-0数据为:102017071807514199868487451 hashcode:618974246 执行时间:1501126946265
进入锁内:Thread-0数据为:102017071807514199868487451 hashcode:618974246 执行时间:1501126946266
102017071807514199868487451
真正的内存地址:455326124
进入方法:Thread-1数据为:102017071807514199868487451 hashcode:618974246 执行时间:1501126947367
进入锁内:Thread-1数据为:102017071807514199868487451 hashcode:618974246 执行时间:1501126947367
退出锁:Thread-0数据为:102017071807514199868487451 hashcode:618974246 执行时间:1501126951266
退出锁:Thread-1数据为:102017071807514199868487451 hashcode:618974246 执行时间:1501126952367
可以明显的看出,其实两者的内存地址是不一样的,是明显的两个不同的对象。

那对于这种情况,有没有方法解决呢?能不能做到字符串内容的时候,就串行执行,内容不一样的时候就并行执行呢?答案是有的,就是利用String的intern()方法。

测试三:

测试结果:符合预期。一样的字符串,执行变为串行。不一样的字符串,执行仍然是并行。

测试代码:

public class Test {

	public static void main(String... args) throws Exception {
		new Thread() {
			public void run() {
				try {
					remoteCall("102017071807514199868487451");
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			};
		}.start();

		new Thread() {
			public void run() {
				try {
					Scanner scanner = new Scanner(System.in);
					remoteCall(scanner.nextLine());
					scanner.close();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			};
		}.start();
	}

	private static void remoteCall(String aaa) throws InterruptedException {
		System.out.println("真正的内存地址:" + System.identityHashCode(aaa));
		aaa = aaa.intern();
		System.out.println("真正的内存地址:" + System.identityHashCode(aaa));
		System.out.println("进入方法:" + Thread.currentThread().getName() + "数据为:" + aaa + " hashcode:" + aaa.hashCode() + " 执行时间:" + System.currentTimeMillis());
		synchronized (aaa) {
			System.out.println("进入锁内:" + Thread.currentThread().getName() + "数据为:" + aaa + " hashcode:" + aaa.hashCode() + " 执行时间:" + System.currentTimeMillis());
			Thread.sleep(5000);
		}
		System.out.println("退出锁:" + Thread.currentThread().getName() + "数据为:" + aaa + " hashcode:" + aaa.hashCode() + " 执行时间:" + System.currentTimeMillis());
	}

}

输出结果:

真正的内存地址:1616247035
真正的内存地址:1616247035
进入方法:Thread-0数据为:102017071807514199868487451 hashcode:618974246 执行时间:1501127094470
进入锁内:Thread-0数据为:102017071807514199868487451 hashcode:618974246 执行时间:1501127094470
102017071807514199868487451
真正的内存地址:1917485542
真正的内存地址:1616247035
进入方法:Thread-1数据为:102017071807514199868487451 hashcode:618974246 执行时间:1501127095710
进入锁内:Thread-1数据为:102017071807514199868487451 hashcode:618974246 执行时间:1501127099471
退出锁:Thread-0数据为:102017071807514199868487451 hashcode:618974246 执行时间:1501127099471
退出锁:Thread-1数据为:102017071807514199868487451 hashcode:618974246 执行时间:1501127104471

看着输出应该就一目了然了,这里就不多加解释了。

需要注意的是,intern可以使得内容一样的字符串,指向同一个内存地址,这会使得大量字符串难以清理,导致内存上升。因此我们,使用这种方法的时候,还要对入参的字符串的取值范围进行一定估量。如果经常调用这个方法的时候,传入的参数都是不一样,并且变化很大的字符串,可能就需要小心使用了。在我实际的工作环境中,调用这个方法时,传入的字符串都是在一定的范围内的,并且总体个数我可以接受,因此我不必考虑这个内存消耗的问题。因此,我可以做到牺牲一部分内存和一部分方法的执行效率,以此来提高整个方法执行的并行度。

  • 5
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值