synchronized 是 Java 关键字。用来修饰方法或代码块时,能保证同一时刻最多只有一条线程执行这段代码。
它有两种用法:synchronized 方法 和 synchronized 块。锁永远加在对象上,不是加在「一段代码」上。每个对象只有一把锁。同步很贵,还可能死锁,能少则少。
一、五条规则(先看结论)
-
两个并发线程访问同一个对象里的
synchronized(this) 块时,同一时间只能有一条线程执行,另一条必须等。
-
一条线程进了
synchronized(this) 块,另一条仍可以跑该对象里非 synchronized 的代码。
-
一条线程进了
synchronized(this) 块,其它线程对该对象里所有其它 synchronized(this) 块的访问都会被堵住。
-
第三条对「其它同步代码」同样成立:拿到的是这个 object 的对象锁,该对象上所有同步部分都被暂时阻塞。
-
以上规则对其它对象锁同样适用(不一定是 this)。
| 场景 |
另一条线程能不能进 |
| 同一对象,另一段 synchronized(this) / synchronized 方法 |
不能,要等锁 |
| 同一对象,普通非同步方法 |
能,互不干扰 |
| 另一对象的同步方法 |
能,锁不是同一把 |
| synchronized(inner) 对 Inner 上锁,另一线程跑 Inner 的非同步方法 |
能 |
| synchronized(inner) 对 Inner 上锁,另一线程跑 Inner 的同步方法 |
不能 |
二、同一把 this 锁:互斥
规则一:两个线程共用同一个 Runnable 实例,synchronized(this) 把循环包起来,A 跑完 0~4,B 才能进:
package ths;
public class Thread1 implements Runnable {
public void run() {
synchronized(this) {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " synchronized loop " + i);
}
}
}
public static void main(String[] args) {
Thread1 t1 = new Thread1();
Thread ta = new Thread(t1, "A");
Thread tb = new Thread(t1, "B");
ta.start();
tb.start();
}
}
结果:
A synchronized loop 0
A synchronized loop 1
A synchronized loop 2
A synchronized loop 3
A synchronized loop 4
B synchronized loop 0
B synchronized loop 1
B synchronized loop 2
B synchronized loop 3
B synchronized loop 4
三、非同步方法不受影响
规则二:t1 进 m4t1 的同步块时,t2 仍能跑没有 synchronized 的 m4t2,输出交错:
package ths;
public class Thread2 {
public void m4t1() {
synchronized(this) {
int i = 5;
while( i-- > 0) {
System.out.println(Thread.currentThread().getName() + " : " + i);
try {
Thread.sleep(500);
} catch (InterruptedException ie) {
}
}
}
}
public void m4t2() {
int i = 5;
while( i-- > 0) {
System.out.println(Thread.currentThread().getName() + " : " + i);
try {
Thread.sleep(500);
} catch (InterruptedException ie) {
}
}
}
public static void main(String[] args) {
final Thread2 myt2 = new Thread2();
Thread t1 = new Thread( new Runnable() { public void run() { myt2.m4t1(); } }, "t1" );
Thread t2 = new Thread( new Runnable() { public void run() { myt2.m4t2(); } }, "t2" );
t1.start();
t2.start();
}
}
结果:
t1 : 4
t2 : 4
t1 : 3
t2 : 3
t1 : 2
t2 : 2
t1 : 1
t2 : 1
t1 : 0
t2 : 0
四、其它 synchronized(this) 会被堵住
规则三:给 m4t2 也加上 synchronized(this),两条线程串行:
public void m4t2() {
synchronized(this) {
int i = 5;
while( i-- > 0) {
System.out.println(Thread.currentThread().getName() + " : " + i);
try {
Thread.sleep(500);
} catch (InterruptedException ie) {
}
}
}
}
结果:
t1 : 4
t1 : 3
t1 : 2
t1 : 1
t1 : 0
t2 : 4
t2 : 3
t2 : 2
t2 : 1
t2 : 0
五、同步方法同样拿对象锁
规则四:把 m4t2 改成同步方法,效果和同步块相同——拿到的仍是这个对象的锁:
//修改Thread2.m4t2()方法如下:
public synchronized void m4t2() {
int i = 5;
while( i-- > 0) {
System.out.println(Thread.currentThread().getName() + " : " + i);
try {
Thread.sleep(500);
} catch (InterruptedException ie) {
}
}
}
结果与上一节相同。
六、锁可以是别的对象
规则五:用内部类 Inner 当锁。t1 通过 synchronized(inner) 调 m4t1;t2 直接调非同步的 m4t2:
package ths;
public class Thread3 {
class Inner {
private void m4t1() {
int i = 5;
while(i-- > 0) {
System.out.println(Thread.currentThread().getName() + " : Inner.m4t1()=" + i);
try {
Thread.sleep(500);
} catch(InterruptedException ie) {
}
}
}
private void m4t2() {
int i = 5;
while(i-- > 0) {
System.out.println(Thread.currentThread().getName() + " : Inner.m4t2()=" + i);
try {
Thread.sleep(500);
} catch(InterruptedException ie) {
}
}
}
}
private void m4t1(Inner inner) {
synchronized(inner) { //使用对象锁
inner.m4t1();
}
private void m4t2(Inner inner) {
inner.m4t2();
}
public static void main(String[] args) {
final Thread3 myt3 = new Thread3();
final Inner inner = myt3.new Inner();
Thread t1 = new Thread( new Runnable() {public void run() { myt3.m4t1(inner);} }, "t1");
Thread t2 = new Thread( new Runnable() {public void run() { myt3.m4t2(inner);} }, "t2");
t1.start();
t2.start();
}
}
尽管 t1 拿到了 Inner 的对象锁,t2 访问的是同一 Inner 里的非同步部分,所以互不干扰:
t1 : Inner.m4t1()=4
t2 : Inner.m4t2()=4
t1 : Inner.m4t1()=3
t2 : Inner.m4t2()=3
t1 : Inner.m4t1()=2
t2 : Inner.m4t2()=2
t1 : Inner.m4t1()=1
t2 : Inner.m4t2()=1
t1 : Inner.m4t1()=0
t2 : Inner.m4t2()=0
现在给 Inner.m4t2 加上 synchronized:
private synchronized void m4t2() {
int i = 5;
while(i-- > 0) {
System.out.println(Thread.currentThread().getName() + " : Inner.m4t2()=" + i);
try {
Thread.sleep(500);
} catch(InterruptedException ie) {
}
}
}
t1 与 t2 访问的是 Inner 里两段不相干的代码,但 t1 先拿到 Inner 的对象锁,t2 进同步方法 m4t2 也被堵住:
t1 : Inner.m4t1()=4
t1 : Inner.m4t1()=3
t1 : Inner.m4t1()=2
t1 : Inner.m4t1()=1
t1 : Inner.m4t1()=0
t2 : Inner.m4t2()=4
t2 : Inner.m4t2()=3
t2 : Inner.m4t2()=2
t2 : Inner.m4t2()=1
t2 : Inner.m4t2()=0
七、synchronized 方法 vs 块
声明同步方法:
public synchronized void accessVal(int newVal);
每个类实例一把锁。同步方法必须先拿到调用该方法的那个实例的锁才能执行,否则所属线程阻塞;方法一旦执行就独占锁,返回才释放。这样保证:同一实例上,所有 synchronized 成员函数同一时刻最多一个在跑,从而避免成员变量被同时改(前提是所有可能改成员的方法都声明为 synchronized)。
不光实例有锁,每个 Class 也有一把锁。静态同步方法拿的是类锁,用来保护静态成员。
缺陷:大方法整段 synchronized 会严重拖慢。若把线程的 run() 做成 synchronized,线程活着期间一直占锁,本类其它同步方法永远进不去。可以把访问成员的代码抽到小的同步方法里,由主方法去调;更好的办法是用同步块,只锁真正需要互斥的那几行。
同步块语法:
synchronized(syncObject) {
// 允许访问控制的代码
}
必须拿到 syncObject(实例或 Class)的锁才能进。可以对任意代码块、任意指定上锁对象,更灵活。
八、大房子比喻
把一个 object 比作永远敞着大门的大房子,里面很多房间(方法):有的上锁(同步方法),有的不上锁(普通方法)。门口一把钥匙,能开所有上锁的房间。想调这个对象方法的线程,就是想进某间房的人。前提:对象至少有一个同步方法,否则钥匙没意义。
普通情况下钥匙原则是「随用随借,用完即还」:进上锁房间前到门口拿钥匙,用完马上还。即使连续用两间上锁房间,中间也要还再取。不上锁的房间谁都能进,几个人挤一间也行。
多人等钥匙时,还回来之后谁先拿到?规范不保证。Thread.sleep 睡多久回来、同优先级谁先跑、等待池里谁先拿到锁,Java 规范多处明确说不保证,最终由 JVM 按多条条件决定。计算机里的「随机」其实是伪随机;不定,往往是因为要定下来太贵、也没多大意义。
同步块和同步方法有两点不同:
-
尺寸更小:相当于没上锁房间里用带锁屏风隔出的一块。
-
可以指定用另一栋房子的钥匙开这面屏风。拿到那栋房子的钥匙,并不妨碍别人进那栋房子里没锁的房间。
为什么用同步块?同步覆盖的代码越多,效率越差。方法里常常先建局部变量再运算、显示;只把该互斥的运算锁住即可。另外,同步块可以在退出之前一直霸占指定对象的 key,不再「用完即还」。可以另起一条线程,把同步块的锁指向这栋房子,只要进块时抓到钥匙,就能一直留到退出——甚至 sleep 十分钟,门口一千条线程还在等。
sleep() 和钥匙:睡着也不还钥匙。没做完同步内容就被 sleep,钥匙还在身上,避免别人进屋把东西搞乱。
为什么一把钥匙开所有上锁的门,而不是一门一钥?一门一钥更安全,但钥匙的产生、保管、获取、归还会随同步方法增加呈几何级复杂,效率崩掉。这是安全性和复杂度的权衡。
参考:http://hi.baidu.com/sunshibing/blog/item/5235b9b731d48ff430add14a.html。
九、一个简单例子
四个线程抢同一把 str 锁,把 num 从 100 减到 0。中间 Thread.sleep 是为了拉开时间差、制造出错机会:
public class TextThread {
public static void main(String[] args) {
TxtThread tt = new TxtThread();
new Thread(tt).start();
new Thread(tt).start();
new Thread(tt).start();
new Thread(tt).start();
}
}
class TxtThread implements Runnable {
int num = 100;
String str = new String();
public void run() {
synchronized (str) {
while (num > 0) {
try {
Thread.sleep(1);
} catch (Exception e) {
e.getMessage();
}
System.out.println(Thread.currentThread().getName()
+ "this is " + num--);
}
}
}
}
十、锁加在不同地方,锁定的对象不一样
无论关键字加在方法上还是对象上,拿到的锁都是对象。每对象一把锁。同步开销大,还可能死锁。
假设 P1、P2 是同一类的两个实例,下面几种同步它们都能调。
1. synchronized 修饰实例方法
Public synchronized void methodAAA()
{
//….
}
锁定的是调用这个方法的那个对象。P1 在不同线程里调这个方法会互斥;P2 仍可以随便调——P1 那把锁跟 P2 无关。等价于:
public void methodAAA()
{
synchronized (this) // (1)
{
//…..
}
}
这里的 this 就是调用方(如 P1)。实质是 synchronized 作用于 object reference。
2. 同步块,锁是参数或私有数组
public void method3(SomeObject so)
{
synchronized(so)
{
//…..
}
}
这时锁是 so。谁拿到谁跑。没有现成对象当锁时,可以造一个专用 instance 变量:
class Foo implements Runnable
{
private byte[] lock = new byte[0]; // 特殊的instance变量
Public void methodA()
{
synchronized(lock) { //… }
}
//…..
}
零长度 byte 数组比 new Object() 更便宜:字节码大约 3 条操作码对 7 条。
3. synchronized 作用于 static / Class
Class Foo
{
public synchronized static void methodAAA() // 同步的static 函数
{
//….
}
public void methodBBB()
{
synchronized(Foo.class) // class literal(类名称字面常量)
}
}
methodAAA 是同步静态方法,methodBBB 用 Foo.class 当锁,效果一样:拿到的是 Class 这把锁,不是某个实例。
《Effective Java》指出 Foo.class 和 P1.getClass() 作同步锁并不等同,不要用 getClass() 去锁这个 Class。
若一个类既有 synchronized static 方法 A,又有 synchronized 实例方法 B,同一对象在多线程里分别调 A 和 B,不会互斥:A 的锁是 Class,B 的锁是实例。
十一、写更安全的同步
-
instance 变量做成 private,只给 get,不要 public/protected。做成 public,外面就能绕过同步方法直接改——这也是 JavaBean 的常见写法。
-
如果 instance 是数组或 ArrayList 这类对象,get 出去的是引用,调用方可以把它改掉。这时 get 也要 synchronized,并且只返回
clone(),让调用方拿到副本。
一句话总结:synchronized 锁的是对象:同一把锁才互斥,非同步代码不受影响;能锁块就别锁整方法,静态方法和实例方法根本不是同一把锁。