开始
类有一个特性叫封装,如果一个类,所有的field都是private的,而且没有任何的method,那么这个类就像是四面围墙+天罗地网,没有门。看起来就是一个封闭的箱子,外面的进不来,里面的出不去,一般来说,这样的类是没用的。
现在为这个类定义一个public的method,这个method能够修改这个类的field,相当于为这个箱子开了一个门。门有了,然后访问者就有了,当一个时间段,有多个访问者进来,就可能会发生并发问题。
并发问题是个什么问题?最经典的例子就是转账,一个访问者从账户A扣取一部分金额,加到账户B上。在A账户扣取之后,B账户转入之前,数据处于不一致的状态,另一个访问者如果在这个时候访问B账户,获取的数据就是有问题的。这就是并发问题,导致这个问题的出现基于2个条件:1.访问者的操作导致数据在一段时间内是不一致的;2.可以有多个访问者同时操作。如果能够破坏其中一个条件,就可以解决并发问题了。我们的关注点是在第2个条件上。
回到那个箱子,回到那个门。我们设想为这个门加一把锁,一个访问者进了这个门,就上锁,期间其他访问者不能再进来;等进去的访问者出来,锁打开,允许另一个访问者进去。
1. 给一个代码块上锁
synchronized可以上锁、解锁。但是它本身并不是锁,它使用的锁来自于一个对象:任何对象实例都有一把内部锁,只有一把。synchronized不仅仅可以对整个method上锁,还可以对method内的某个代码块上锁。
比如下面这种用法:
synchronized(obj){ // some code... }
这个用法就是使用了obj的锁,来锁定一个代码块。
对整个方法上锁,如:
1 publicsynchronizedvoid aMethod(){ 2 // some code... 3 }
这个时候它使用的是当前实例this的锁,相当于下面的模式:
publicvoid aMethod(){ synchronized(this){ // some code... } }
2. 两个代码块的互斥
一个代码块,被上了锁,就无法同时接纳多个线程的访问。如果是2个不同的代码块,都被上了锁,它们之间是否会有影响呢?请看下面的代码:
1 class SyncData { 2 public void do1() { 3 synchronized(this) { 4 for (int i=0; i < 4; i++) { 5 System.out.println(Thread.currentThread().getName() + "-do1-" + i); 6 try{ 7 Thread.sleep(1000); 8 }catch(InterruptedException e) { 9 e.printStackTrace(); 10 } 11 } 12 } 13 14 } 15 16 public void do2() { 17 synchronized(this) { 18 for (int i=0; i < 4; i++) { 19 System.out.println(Thread.currentThread().getName() + "-do2-" + i); 20 try{ 21 Thread.sleep(1000); 22 }catch(InterruptedException e) { 23 e.printStackTrace(); 24 } 25 } 26 } 27 } 28 }
创建1个SyncData的实例,开启2个线程,一个线程调用实例的do1方法,另一个线程调用实例的do2方法,你会看到他们之间是互斥的——即使2个线程访问的是实例的不同的方法,依然不能同时访问。因为决定是否可以同时访问的不再是门,而是锁。只要使用的是相同的对象锁,就会互斥访问。
上文中关于门的比喻已经不合适了,因为在代码中你可以发现两个门(do1、do2)使用了同一把锁(this),而这和我们的常识经验是相违背的,下文也不会再出现“门”。
3. 锁的识别
可以使用任何对象的锁,比如你可以专门创建一个对象,只提供锁的功能:
1 class SyncData { 2 private Object lock = new byte[0]; 3 4 public void do1() { 5 synchronized(lock) { 6 for (int i=0; i < 4; i++) { 7 System.out.println(Thread.currentThread().getName() + "-do1-" + i); 8 try{ 9 Thread.sleep(1000); 10 }catch(InterruptedException e) { 11 e.printStackTrace(); 12 } 13 } 14 } 15 } 16 }
思考下面的代码是否能起到互斥访问的作用:
1 class SyncData { 2 public void do1() { 3 Object lock = new byte[0]; 4 synchronized(lock) { 5 for (int i=0; i < 4; i++) { 6 System.out.println(Thread.currentThread().getName() + "-do1-" + i); 7 try{ 8 Thread.sleep(1000); 9 }catch(InterruptedException e) { 10 e.printStackTrace(); 11 } 12 } 13 } 14 } 15 }
这个是不能起到互斥作用的,因为每一次调用,局部变量lock都是不同的实例。也就是说,synchronized使用的锁总是变化的。所以我们再补充一点:只有使用相同的对象锁,才能互斥访问。所以识别所使用的锁,是很重要的。
下面再看一段代码:
1 class SyncData { 2 public void do1() { 3 synchronized(this) { 4 for (int i=0; i < 4; i++) { 5 System.out.println(Thread.currentThread().getName() + "-do1-" + i); 6 try{ 7 Thread.sleep(1000); 8 }catch(InterruptedException e) { 9 e.printStackTrace(); 10 } 11 } 12 } 13 14 } 15 }
创建2个实例,分别交给2个线程中的1个去访问,能互斥吗?
不可以,因为每一个实例使用的都是自身的锁,相互之间是不同的锁,所以不能互斥。如果把代码改成这样呢:
class SyncData { public void do1() { synchronized(this.getClass()) { for (int i=0; i < 4; i++) { System.out.println(Thread.currentThread().getName() + "-do1-" + i); try{ Thread.sleep(1000); }catch(InterruptedException e) { e.printStackTrace(); } } } } }
可以互斥,不管一个类有多少个实例,它们调用getClass()返回的结果都是同一个实例。
讨论这个问题,是因为可以在static的method上使用synchronized,而其本质,就是使用了上面那种实例的锁,所以不同的synchronized static方法之间,也是互斥的。
总结
总结一下我们的结论:
- 任何对象实例都有一把内部锁,只有一把。
- 相同的对象锁是互斥访问的充要条件。
这2个结论已经够了,重要的是识别使用的对象的锁是不是相同的。
多线程设计,考虑同步问题,我有几点想法:
- 一个类的实例,可能被多个线程并发访问,才考虑同步控制。
- 在1的前提下,只有会导致数据状态出现一段时间的不一致,相关的代码片段才需要同步控制。
- 在2的前提下,只有两块代码会相互干扰时,才必须使用同一把对象锁,来实现互斥;如果相互之间没有影响,建议使用不同的对象锁,以保持并发性能。
当然,在判断“数据状态是否会不一致”、“两块代码是否有干扰”的时候,是比较困难的,所以再补充2点:
- 在不能确认数据状态是否会不一致的情况下,按照会不一致的情况考虑
- 在不能确认两块代码是否有干扰的情况下,按照会有干扰的情况考虑
我们的讨论到此结束。
参考
- Java中Synchronized的用法
介绍了使用synchronized的几种方式,以及相互的区别,写的很好,建议也看一下,相互印证。
来源:https://www.cnblogs.com/ywjy/p/5650141.html