Synchronized是Java中的关键字,是一种同步锁,其最主要的使用场景是在多线程并发编程时实现线程同步。
但,Synchronized默认是非公平锁。这意味着如果多个线程同时请求锁,哪个线程能够获得锁是由操作系统的调度算法决定的。
Synchronized默认没有阻塞超时时间,需要注意内部程序的设计。
本文主要讲了synchronized的修饰对象以及使用方法。
修饰对象
synchronized 是 Java 中的关键字,是一种同步锁。它修饰的对象有以下几种:
- 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号 {} 括起来的代码,作用的对象是调用这个代码块的对象;
- 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
- 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
- 修改一个类,其作用的范围是 synchronized 后面括号括起来的部分,作用主的对象是这个类的所有对象。
修饰一个代码块
【Demo1】:synchronized 的用法
一个线程访问一个对象中的 synchronized (this) 同步代码块时,其他试图访问该对象的线程将被阻塞。
1 | /** |
SyncThread 的调用:
1 | SyncThread syncThread = new SyncThread(); |
结果如下:
1 | SyncThread1:0 |
两个并发线程 (thread1 和 thread2) 访问同一个对象 (syncThread) 中的 synchronized 代码块时,在同一时刻只能有一个线程得到执行,另一个线程受阻塞,必须等待当前线程执行完这个代码块以后才能执行该代码块。Thread1 和 thread2 是互斥的,因为在执行 synchronized 代码块时会锁定当前的对象,只有执行完该代码块才能释放该对象锁,下一个线程才能执行并锁定该对象。
我们再把 SyncThread 的调用稍微改一下:
1 | Thread thread1 = new Thread(new SyncThread(), "SyncThread1"); |
结果如下:
1 | SyncThread1:0 |
不是说一个线程执行 synchronized 代码块时其它的线程受阻塞吗?为什么上面的例子中 thread1 和 thread2 同时在执行。这是因为 synchronized 只锁定对象,每个对象只有一个锁(lock)与之相关联,而上面的代码等同于下面这段代码:
1 | SyncThread syncThread1 = new SyncThread(); |
这时创建了两个 SyncThread 的对象 syncThread1 和 syncThread2,线程 thread1 执行的是 syncThread1 对象中的 synchronized 代码 (run),而线程 thread2 执行的是 syncThread2 对象中的 synchronized 代码 (run);我们知道 synchronized 锁定的是对象,这时会有两把锁分别锁定 syncThread1 对象和 syncThread2 对象,而这两把锁是互不干扰的,不形成互斥,所以两个线程可以同时执行。
【Demo2】:多个线程访问 synchronized 和非 synchronized 代码块
当一个线程访问对象的一个 synchronized (this) 同步代码块时,另一个线程仍然可以访问该对象中的非 synchronized (this) 同步代码块。
1 | class Counter implements Runnable{ |
调用代码:
1 | Counter counter = new Counter(); |
结果如下:
1 | A:0 |
上面代码中 countAdd 是一个 synchronized 的,printCount 是非 synchronized 的。从上面的结果中可以看出一个线程访问一个对象的 synchronized 代码块时,别的线程可以访问该对象的非 synchronized 代码块而不受阻塞。
【Demo3】: 指定要给某个对象加锁
1 | /** |
调用代码:
1 | Account account = new Account("zhang san", 10000.0f); |
结果如下:
1 | Thread3:10000.0 |
在 AccountOperator 类中的 run 方法里,我们用 synchronized 给 account 对象加了锁。这时,当一个线程访问 account 对象时,其他试图访问 account 对象的线程将会阻塞,直到该线程访问 account 对象结束。也就是说谁拿到那个锁谁就可以运行它所控制的那段代码。
当有一个明确的对象作为锁时,就可以用类似下面这样的方式写程序。
1 | public void method3(SomeObject obj) |
当没有明确的对象作为锁,只是想让一段代码同步时,可以创建一个特殊的对象来充当锁:
1 | class Test implements Runnable |
说明:零长度的 byte 数组对象创建起来将比任何对象都经济――查看编译后的字节码:生成零长度的 byte [] 对象只需 3 条操作码,而 Object lock = new Object () 则需要 7 行操作码。
【Demo4】:synchronized 修饰一个方法
Synchronized 修饰一个方法很简单,就是在方法的前面加 synchronized,public synchronized void method (){//todo}; synchronized 修饰方法和修饰一个代码块类似,只是作用范围不一样,修饰代码块是大括号括起来的范围,而修饰方法范围是整个函数。如将【Demo1】中的 run 方法改成如下的方式,实现的效果一样。
1 | public synchronized void run() { |
Synchronized 作用于整个方法的写法。
写法一:
1 | public synchronized void method() |
写法二:
1 | public void method() |
写法一修饰的是一个方法,写法二修饰的是一个代码块,但写法一与写法二是等价的,都是锁定了整个方法时的内容。
在用 synchronized 修饰方法时要注意以下几点:
- synchronized 关键字不能继承。
虽然可以使用 synchronized 来定义方法,但 synchronized 并不属于方法定义的一部分,因此,synchronized 关键字不能被继承。如果在父类中的某个方法使用了 synchronized 关键字,而在子类中覆盖了这个方法,在子类中的这个方法默认情况下并不是同步的,而必须显式地在子类的这个方法中加上 synchronized 关键字才可以。当然,还可以在子类方法中调用父类中相应的方法,这样虽然子类中的方法不是同步的,但子类调用了父类的同步方法,因此,子类的方法也就相当于同步了。这两种方式的例子代码如下:
在子类方法中加上 synchronized 关键字
1 | class Parent { |
在子类方法中调用父类的同步方法
1 | class Parent { |
2. 在定义接口方法时不能使用 synchronized 关键字。
2. 构造方法不能使用 synchronized 关键字,但可以使用 synchronized 代码块来进行同步。
【Demo5】:synchronized 修饰静态方法
Synchronized 也可修饰一个静态方法,用法如下:
1 | public synchronized static void method() { |
静态方法是属于类的而不属于对象的。同样的,synchronized 修饰的静态方法锁定的是这个类的所有对象。我们对 Demo1 进行一些修改如下:
1 | /** |
调用代码:
1 | SyncThread syncThread1 = new SyncThread(); |
结果如下:
1 | SyncThread1:0 |
syncThread1 和 syncThread2 是 SyncThread 的两个对象,但在 thread1 和 thread2 并发执行时却保持了线程同步。这是因为 run 中调用了静态方法 method,而静态方法是属于类的,所以 syncThread1 和 syncThread2 相当于用了同一把锁。这与 Demo1 是不同的。
【Demo6】: 修饰一个类
Synchronized 还可作用于一个类,用法如下:
1 | class ClassName { |
把 Demo5 再作一些修改:
1 | /** |
其效果和【Demo5】是一样的,synchronized 作用于一个类 T 时,是给这个类 T 加锁,T 的所有对象用的是同一把锁。
总结
A. 无论 synchronized 关键字加在方法上还是对象上,如果它作用的对象是非静态的,则它取得的锁是对象;如果 synchronized 作用的对象是一个静态方法或一个类,则它取得的锁是对类,该类所有的对象同一把锁。
B. 每个对象只有一个锁(lock)与之相关联,谁拿到这个锁谁就可以运行它所控制的那段代码。
C. 实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。