java synchronized详解

    |     2015年4月19日   |   多线程编程   |     0 条评论   |    1383

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 锁的是对象:同一把锁才互斥,非同步代码不受影响;能锁块就别锁整方法,静态方法和实例方法根本不是同一把锁。

转载请注明来源:java synchronized详解
本文链接地址:https://ai.zhousir.top/?p=412
回复 取消