上一篇文章主要讲解了synchronized关键字的作用和在各种情况下的各种用法,本篇将对于并发访问对象的相关知识进行一些补充。
一、String的常量池特性
在JVM中String具有常量池的缓存功能,比如
String a = "abc";
String b = "abc";
System.out.println(a==b);
此程序将输出true,这是因为用这种方式初始化a和b,结果就是ab所指向的是同一个对象,a、b只是这个“abc”字符串的两个不同的引用,而要使他们只想不同的对象,则需要分别调用String的构造函数new String("abc");
如下定义一个类,其中print方法内将传入的String参数当作对象监视器,每隔两秒输出当前线程的线程名。
public class Service { public void print(String str) { try { synchronized(str) { while(true) { System.out.println("ThreadName="+Thread.currentThread().getName()); Thread.sleep(1000); } } }catch(InterruptedException e) { e.printStackTrace(); } } }
定义两个线程ThreadA和ThreadB
public class ThreadA extends Thread{ private Service service; public ThreadA(Service service) { super(); this.service = service; } @Override public void run() { service.print("aa");; } } public class ThreadB extends Thread{ private Service service; public ThreadB(Service service) { super(); this.service = service; } @Override public void run() { service.print("aa"); } }
main函数如下:
public static void main(String args[]) { Service service = new Service(); ThreadA t1 = new ThreadA(service); ThreadB t2 = new ThreadB(service); t1.setName("A"); t2.setName("B"); t1.start(); t2.start(); }
运行结果为:
ThreadName=A
ThreadName=A
ThreadName=A
ThreadName=A
ThreadName=A
可以看到线程A执行而线程B没有得到执行,因为两个线程在调用print方法的时候输入的参数是同一个String。
将上述的service.print(new String("aa"));再次运行程序可以看到结果为:
ThreadName=B
ThreadName=B
ThreadName=A
ThreadName=B
ThreadName=A
ThreadName=A
ThreadName=B
二、用同步块解决无限等待
设想一下有两个线程同时调用一个对象的两个不同的synchronized方法,但是方法A内部存在一个无限循环,则线程1调用A后,进入无限循环,同时无限持有对象object的锁,导致线程2不能获得锁,从而陷入无限等待。
这时,我们可以将两个方法改为非同步方法,然后分别在两个方法内部把需要同步执行的代码放入同步块内,创建两个不同的object对象用来设置同步块,这样两个方法执行的时候便持有不同的对象锁,也就可以异步执行了。
三、内置类的同步访问
首先我们要分清楚内置类和静态内置类这两个概念。内置类是存在于一个类内部的另一个不同的类。若有一个类PublicClass的实例publicClass,此类内部有一个内部类PrivateClass,则在本类内部实例化内置类的时候,可以使用PrivateClass privateClass = publicClass.new PrivateClass(); 而对于静态内置类,实例化对象时则直接使用PrivateClass privateClass = new PrivateClass();即可。
而在访问内置类的时候,有关锁的原则其实与实例变量同步访问是相同的,关键就是看“谁持有谁的锁”,只要两个不同的线程持有的是同一个内部类的锁,则两个线程的任务同步执行;如果一个任务持有了一个内部类的锁,另一个任务要访问此内部类的同步方法时需要等待,但是要访问非同步方法时则可异步执行。
四、对象发生改变时锁改变
假设有两个线程任务,要同步访问一个String类型的实例变量str="aaa",我们将str设为对象监视器即synchronized(str),当第一个线程访问结束时,执行str="bbb";并让线程1 sleep(1000);如果在启动线程1一小段时间后启动线程2,我们会发现,虽然看似线程1在休眠期间仍旧持有str的锁,但是线程2可以异步执行,这是为什么呢?
原因就是当我们将str="bbb"以后,线程2执行到竞争str锁的代码时,会自动去竞争"bbb"的锁,而线程1持有的仍然是"aaa"的锁,这两个线程竞争的锁变得不再一样,自然可以异步执行。所以我们还是要强调,锁是用来锁住对象的,是对象的锁,不是方法的锁也不是引用名的锁。
但是如果一开始就连续启动两个线程,则两个线程都要去竞争"aaa"的锁,就算线程1将str改成"bbb",线程2依旧会继续等待"aaa"的锁释放;如果线程1和线程2竞争的是一个object对象,线程1将object内的属性做了修改,仍然不会影响线程2对于这个对象锁的竞争,因为对象本身还是那个对象,所以线程依旧竞争这个对象的锁。