首先需要了解String的内存模型,有常量池,堆,那么何时访问的是常量池,何时又是访问的堆,需要提前了解,当然下文也会介绍,此处就不在过多说明;另外线程的内存模型也要了解一下,尤其是线程栈。接下来我们就来做一个简单的实验,代码如下:
public class Test { public static void main(String[] args) throws InterruptedException { final Test1 test1 = new Test1(); CountDownLatch latch = new CountDownLatch(20); for(int i=0;i<20;i++){ new Thread(new Runnable() { @Override public void run() { try { String a = new String("1"); test1.add(a); } catch (InterruptedException e) { e.printStackTrace(); } latch.countDown(); } }).start(); } latch.await(); System.out.println(test1.a); } } class Test1{ public int a =0; void add(String i) throws InterruptedException { synchronized (i){ int c=a+1; Thread.sleep(200); a=c; } } }
从上面看,开启20个线程,分别对a进行加1操作,觉得这段代码的执行结果会是多少?是20吗?显然不是。此处new String会在内存的堆中单独创建对象,每个线程都是各自的对象,然后在synchronize中对这个对象加锁,其实是互不竞争的,所以最后的运行结果可能是1,也可能是其他。那么如何修改才能得到结果20呢?那就是让20个线程相互竞争,可以通过String的intern获取常量池中相同内容的对象,然后对该对象加锁,也就是说,只要内容相同,返回的对象就是同一个,然后用synchronize加锁就可以成功,只需调整一下同步块的代码即可:
synchronized (i.intern())
此时获取到结果就是20。
有的人可能会说,直接在方法上同步就可以了,当然这种方式也是可以的,不过性能肯定要差,主要体现在两方面来说:
1、对方法加同步,那整个方法执行完成后才被其他线程执行,锁的粒度太大了。
2、锁的范围太大了,此次测试只是将i设置成为相同了,所以20个线程都对这个i竞争,如果i不同,方法上的同步也会造成20个线程竞争;而采用i.intern(),可以把锁给分散开,范围更小,性能就会更好。