开始


类有一个特性叫封装,如果一个类,所有的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的锁,来锁定一个代码块。

对整个方法上锁,如:

 publicsynchronizedvoid aMethod(){
// some code...
}

这个时候它使用的是当前实例this的锁,相当于下面的模式:

publicvoid aMethod(){
synchronized(this){
// some code...
}
}

2. 两个代码块的互斥

一个代码块,被上了锁,就无法同时接纳多个线程的访问。如果是2个不同的代码块,都被上了锁,它们之间是否会有影响呢?请看下面的代码:

 class SyncData {
public void do1() {
synchronized(this) {
for (int i=0; i < 4; i++) {
System.out.println(Thread.currentThread().getName() + "-do1-" + i);
try{
Thread.sleep(1000);
}catch(InterruptedException e) {
e.printStackTrace();
}
}
} } public void do2() {
synchronized(this) {
for (int i=0; i < 4; i++) {
System.out.println(Thread.currentThread().getName() + "-do2-" + i);
try{
Thread.sleep(1000);
}catch(InterruptedException e) {
e.printStackTrace();
}
}
}
}
}

创建1个SyncData的实例,开启2个线程,一个线程调用实例的do1方法,另一个线程调用实例的do2方法,你会看到他们之间是互斥的——即使2个线程访问的是实例的不同的方法,依然不能同时访问。因为决定是否可以同时访问的不再是门,而是锁。只要使用的是相同的对象锁,就会互斥访问

上文中关于门的比喻已经不合适了,因为在代码中你可以发现两个门(do1、do2)使用了同一把锁(this),而这和我们的常识经验是相违背的,下文也不会再出现“门”。

3. 锁的识别

可以使用任何对象的锁,比如你可以专门创建一个对象,只提供锁的功能:

 class SyncData {
private Object lock = new byte[0]; public void do1() {
synchronized(lock) {
for (int i=0; i < 4; i++) {
System.out.println(Thread.currentThread().getName() + "-do1-" + i);
try{
Thread.sleep(1000);
}catch(InterruptedException e) {
e.printStackTrace();
}
}
}
}
}

思考下面的代码是否能起到互斥访问的作用:

 class SyncData {
public void do1() {
Object lock = new byte[0];
synchronized(lock) {
for (int i=0; i < 4; i++) {
System.out.println(Thread.currentThread().getName() + "-do1-" + i);
try{
Thread.sleep(1000);
}catch(InterruptedException e) {
e.printStackTrace();
}
}
}
}
}

这个是不能起到互斥作用的,因为每一次调用,局部变量lock都是不同的实例。也就是说,synchronized使用的锁总是变化的。所以我们再补充一点:只有使用相同的对象锁,才能互斥访问。所以识别所使用的锁,是很重要的。

 
下面再看一段代码:

 class SyncData {
public void do1() {
synchronized(this) {
for (int i=0; i < 4; i++) {
System.out.println(Thread.currentThread().getName() + "-do1-" + i);
try{
Thread.sleep(1000);
}catch(InterruptedException e) {
e.printStackTrace();
}
}
} }
}

创建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方法之间,也是互斥的。

总结


总结一下我们的结论:
  1. 任何对象实例都有一把内部锁,只有一把。
  2. 相同的对象锁是互斥访问的充要条件。
这2个结论已经够了,重要的是识别使用的对象的锁是不是相同的。
 
多线程设计,考虑同步问题,我有几点想法:
  1. 一个类的实例,可能被多个线程并发访问,才考虑同步控制。
  2. 在1的前提下,只有会导致数据状态出现一段时间的不一致,相关的代码片段才需要同步控制。
  3. 在2的前提下,只有两块代码会相互干扰时,才必须使用同一把对象锁,来实现互斥;如果相互之间没有影响,建议使用不同的对象锁,以保持并发性能。
当然,在判断“数据状态是否会不一致”、“两块代码是否有干扰”的时候,是比较困难的,所以再补充2点:
  1. 在不能确认数据状态是否会不一致的情况下,按照会不一致的情况考虑
  2. 在不能确认两块代码是否有干扰的情况下,按照会有干扰的情况考虑
我们的讨论到此结束。
 

参考


  1. Java中Synchronized的用法
    介绍了使用synchronized的几种方式,以及相互的区别,写的很好,建议也看一下,相互印证。

最新文章

  1. SQLServer 客户端远程访问配置
  2. 论velocity在不同后台语言下的不同
  3. jQuery获取Select选择的Text和 Value(转)用时比较方便寻找
  4. java代码调用oracle存储过程
  5. 《编写高质量代码:改善Python程序的91个建议》读后感
  6. javascript 写状态模式
  7. EXCEL VBA 选择文件对话框
  8. Java中的浅复制和深复制 Cloneable clone
  9. 让.NET程序快速释放内存的办法
  10. 菲菲更名宝贝 得意非凡版 v1.9 免费绿色版
  11. 基于redis的分布式锁(不适合用于生产环境)
  12. swipper的一个小坑
  13. Go命令行参数解析flag包
  14. 关于DOM级别的一些问题,DOM0,DOM1,DOM2
  15. 《MYSQL必知必会2
  16. MIME协议(详解范例)
  17. ZOJ3435_Ideal Puzzle Bobble
  18. 【刷题】BZOJ 3295 [Cqoi2011]动态逆序对
  19. CPU线程 和 Java线程
  20. lattice diamond 3.7安装破解

热门文章

  1. 重构改善既有代码设计--重构手法14:Hide Delegate (隐藏委托关系)
  2. 【LibreOJ】#6299. 「CodePlus 2018 3 月赛」白金元首与克劳德斯
  3. win10本地搭建php运行环境
  4. XGBoost与LightGBM对比分析(转)
  5. ckeditor+ckfinder+java
  6. pytesser模块WindowsError错误解决方法
  7. python模块分析之sqlite3数据库
  8. dlmalloc(一)【转】
  9. 64_r2
  10. Oracle安装出现报错