Java并发_synchronized锁(3)

synchronized

synchronized 锁保证被加锁的代码在同一时刻只有一个线程能执行,进而保证并发安全

有四种情况,可分为对象锁和类锁,其中对象锁又分为代码块锁和方法锁,类锁则包括 Class 对象锁和静态方法锁

核心是四种情况对应的锁对象,锁对象相同线程才会产生竞争,锁对象不同线程拿锁则不会冲突,同一把锁同时只能被一个线程获取,没有获取到锁的线程将阻塞,进入 BLOCKED 状态

synchronized 的缺点是不够灵活,一个锁对应一个对象,获取锁、让出锁的时机单一,且无法知道锁是否已成功获取、让出,线程等待获取锁不能设置超时时间,且等待过程中不能主动中断

对象代码块锁

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class Test {
  public static void main(String[] args) {
    MyRunnable mr = new MyRunnable();
    //传入相同任务类的同一对象
    Thread t1 = new Thread(mr);
    Thread t2 = new Thread(mr);
    t1.start();
    t2.start();
  }
}
class MyRunnable implements Runnable {
  //创建锁对象
  Object lock = new Object();
  @Override
  public void run() {
    //可以直接用当前对象作为锁对象
    //synchronized (this) {
    //也可以单独创建一个对象来做锁对象
    synchronized (lock) {
      System.out.println(
        Thread.currentThread().getName() + "获取到lock对象锁开始执行同步代码块");
      try {
        Thread.sleep(1000);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      System.out.println(
        Thread.currentThread().getName() + "执行完同步代码块让出lock对象锁");
    }
  }
}
//Thread-0获取到lock对象锁开始执行同步代码块
//Thread-0执行完同步代码块让出lock对象锁
//Thread-1获取到lock对象锁开始执行同步代码块
//Thread-1执行完同步代码块让出lock对象锁

对象方法锁

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class Test {
  public static void main(String[] args) {
    MyRunnable mr = new MyRunnable();
 		//传入相同任务类的同一对象
    Thread t1 = new Thread(mr);
    Thread t2 = new Thread(mr);
    t1.start();
    t2.start();
  }
}
class MyRunnable implements Runnable {
  @Override
  public void run() {
    method();
  }
  //锁对象为this对象
  public synchronized void method() {
    System.out.println(
      Thread.currentThread().getName() + "获取到this对象锁开始执行method方法");
    try {
      Thread.sleep(1);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    System.out.println(
      Thread.currentThread().getName() + "执行完method方法让出this对象锁");
  }
}
//Thread-1获取到this对象锁开始执行method方法
//Thread-1执行完method方法让出this对象锁
//Thread-0获取到this对象锁开始执行method方法
//Thread-0执行完method方法让出this对象锁

类 Class 对象锁

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class Test {
  public static void main(String[] args) {
    MyRunnable mr1 = new MyRunnable();
    MyRunnable mr2 = new MyRunnable();
    //传入相同任务的不同对象
    Thread t1 = new Thread(mr1);
    Thread t2 = new Thread(mr2);
    t1.start();
    t2.start();
  }
}
class MyRunnable implements Runnable {
  @Override
  public void run() {
    //锁对象为MyRunnable.class对象
    synchronized (MyRunnable.class) {
      System.out.println(
        Thread.currentThread().getName() + "获取到类锁开始执行同步代码块");
      try {
        Thread.sleep(1);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      System.out.println(
        Thread.currentThread().getName() + "执行完同步代码块让出类锁");
    }
  }
}
//Thread-0获取到类锁开始执行同步代码块
//Thread-0执行完同步代码块让出类锁
//Thread-1获取到类锁开始执行同步代码块
//Thread-1执行完同步代码块让出类锁

类静态方法锁

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class Test {
  public static void main(String[] args) {
    MyRunnable mr1 = new MyRunnable();
    MyRunnable mr2 = new MyRunnable();
    //传入相同任务的不同对象
    Thread t1 = new Thread(mr1);
    Thread t2 = new Thread(mr2);
    t1.start();
    t2.start();
  }
}
class MyRunnable implements Runnable {
  @Override
  public void run() {
    method();
  }
  //锁对象为this对象对应的Class类的对象
  public static synchronized void method() {
    System.out.println(
      Thread.currentThread().getName() + "获取到类锁开始执行method方法");
    try {
      Thread.sleep(1);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    System.out.println(
      Thread.currentThread().getName() + "执行完method方法让出类锁");
  }
}
//Thread-0获取到类锁开始执行method方法
//Thread-0执行完method方法让出类锁
//Thread-1获取到类锁开始执行method方法
//Thread-1执行完method方法让出类锁

异常自动让出锁

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class Test {
  public static void main(String[] args) {
    MyRunnable mr = new MyRunnable();
    //传入相同任务类的同一对象
    Thread t1 = new Thread(mr);
    Thread t2 = new Thread(mr);
    t1.start();
    t2.start();
  }
}
class MyRunnable implements Runnable {
  @Override
  public void run() {
    method();
  }
  public synchronized void method() {
    System.out.println(
      Thread.currentThread().getName() + "获取到this对象锁开始执行method方法");
    try {
      Thread.sleep(1);
      //测试抛出异常
    } catch (Exception e) {
      e.printStackTrace();
    }
    throw new RuntimeException();
  }
}
//Thread-0获取到this对象锁开始执行method方法
//Thread-1获取到this对象锁开始执行method方法
//Exception in thread "Thread-0"
//Exception in thread "Thread-1"
//java.lang.RuntimeException

不可中断和可重入

不可中断是指一个线程获取到一把锁,只要该线程不让出该锁,其他线程就只能等待,执行完毕或抛出异常就会让出锁

可重入是指一个线程获取到一把锁,执行完上了该锁的代码块或方法,不需要将锁让出后重新竞争,就能继续执行上了该锁的其他代码块或方法,不可重入即需要先让出锁,然后重新参与竞争,同步锁 synchronized 的粒度即加锁范围为整个线程,即只要线程拿到一把锁,只要不让出锁,则该线程可以执行所有上了该锁的代码块或方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
public class Test {
  public static void main(String[] args) {
    MyRunnable mr = new MyRunnable();
    Thread t = new Thread(mr);
    t.start();
  }
}
class MyRunnable implements Runnable {
  @Override
  public void run() {
    method1();
    synchronized (this) {
      System.out.println("执行同步代码块1");
    }
    synchronized (this) {
      System.out.println("执行同步代码块2");
    }
    otherTask();
  }
  int i = 1;
  public synchronized void method1() {
    System.out.println("第" + i + "次执行method1方法");
    //调用另一个同步方法method2
    method2();
    //递归调用一次method1方法
    if (i == 1) {
      i++;
      method1();
    }
  }
  public synchronized void method2() {
    System.out.println("执行method2方法");
  }

  //传入同一把锁
  Task task = new Task(this);
  public void otherTask() {
    task.otherTask();
  }
}
//另外一个类
class Task {
  Object lock;
  Task(Object lock) {
    this.lock = lock;
  }
  public void otherTask() {
    synchronized (lock) {
      System.out.println("执行其他对象的方法");
    }
  }
}

//第1次执行method1方法
//执行method2方法
//第2次执行method1方法
//执行method2方法
//执行同步代码块1
//执行同步代码块2
//执行其他对象的方法

实现原理

需要指定一个对象作为锁,即 monitor object ,通过进入和退出指定的 monitor object 来实现同步

1
2
3
4
5
6
7
8
public class Test {
  public synchronized void method1() {
  }
  public void method2() {
    synchronized (this) {
    }
  }
}

反编译字节码文件得到以下代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
public synchronized void method1();
		descriptor: ()V
		Access flags: public, synchronized
    Code:
        0 return
public void method2();
		descriptor: ()V
		Access flags: public
    Code:
        0 aload_0
        1 dup
        2 astore_1
        3 monitorenter
        4 aload_1
        5 monitorexit
        6 goto 14 (+8)
        9 astore_2
        10 aload_1
        11 monitorexit
        12 aload_2
        13 athrow
        14 return

同步代码块在编译后,有 monitorenter、monitorexit 命令分别在同步代码块开始、结束或异常的位置,一个线程通过 monitorenter 进入 monitor,即线程获取到锁,由于是可重入的,每通过 monitorenter 进入一次,则锁计数器 + 1,monitorexit 退出一次,则锁计数器 - 1,若计数器为 0,则表示让出锁,在 monitor 被锁定时,即计数器不为 0,则其他线程只能等待

同步方法则是在 Access flags 中添加了 synchronized ,表示该方法是同步方法,并使用调用该方法的对象做为锁对象

死锁

最简单的死锁是一个线程占有锁后不释放,其他需要请求该锁的线程将永远等待,称为抱死死锁

锁顺序死锁则是两个线程试图以不同的顺序来获取一组相同的锁,即可能会发生线程 A 请求的锁被线程 B 占用,同时也占用了线程 B 要请求的锁,例如经典的哲学家吃饭问题

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class Test {
  private static String s1 = "左筷子";
  private static String s2 = "右筷子";
  public static void main(String[] args) {
    new Thread() {
      public void run() {
        while (true) {
          synchronized (s1) {
            System.out.println(this.getName() + "有" + s1 + ",等待" + s2);
            synchronized (s2) {
              System.out.println(this.getName() + "拿到" + s2 + ",吃饭");
            }
          }
        }
      }
    }.start();
    new Thread() {
      public void run() {
        while (true) {
          synchronized (s2) {
            System.out.println(this.getName() + "有" + s2 + ",等待" + s1);
            synchronized (s1) {
              System.out.println(this.getName() +"拿到" + s1 + ",吃饭");
            }
          }
        }
      }
    }.start();
  }
}
//...
//Thread-0有左筷子,等待右筷子
//Thread-0拿到右筷子,吃饭
//Thread-0有左筷子,等待右筷子
//Thread-0拿到右筷子,吃饭
//Thread-0有左筷子,等待右筷子
//Thread-1有右筷子,等待左筷子

若每次执行能保证只获取一把锁则不会发生锁顺序死锁,如果一定需要获取一组锁,则需要指定获取这组锁的顺序,且尽量使用同步代码块的方式显式地定义锁对象

或者给锁设置一个超时时间,若等待超时则放弃,然后再重新尝试,但 synchronized 锁不支持设置超时时间,则可以使用 JUC 提供的 Lock 锁

对于不能设置加锁顺序且也不能设置超时时间的情况,可以将线程请求、获得锁的情况记录下来,若某个线程获取锁失败,则可以通过遍历记录得知当前锁的占用情况,若监测到发生死锁了,则将所有锁释放,程序回退重试

与 ReentrantLock 比较

ReentrantLock 的意义同 synchronized 相同,但提供了例如设置超时时间、可中断锁、公平性等功能,并且实现了非代码块的加锁方式,在 Java 6 之前性能还要远远高于 synchronized ,但 Java 6 对 synchronized 优化之后两个性能相差不多

synchronized 的代码块加锁方式不用手动释放锁,但 ReentrantLock 需要在 finally 中手动释放

所以建议仅在需要 ReentrantLock 的高级功能时才使用,一般情况使用还是使用 synchronized

JVM 对 synchronized 的优化

对于锁竞争不强且总是被同一个线程请求获取的锁,则采用锁偏向的机制,即一个线程获取到锁,若再次请求该锁则可以直接获取不用再次进行同步操作,对于锁竞争强烈的情况可以通过设置 JVM 参数 -XX:+UseBiasedLocking=false 来关闭锁偏向

在轻度锁竞争的情况,则采用轻量级锁,JVM 执行到同步块时,会在线程的栈中开辟一块空间用来保存锁记录,通过判断对象头中的锁信息和栈中的信息是否匹配来判断线程是否获取到锁

若轻量级锁失败则变为自旋锁,JVM 让线程自旋等待即做空循环,若超过最大自旋次数才将线程挂起,即膨胀为重量级锁

对于调用包含加锁操作的 JDK API,但实际上并不需要加锁,则 JVM 会在编译时将锁去除


以上内容是玉山整理的笔记,如有错误还请指出