学习笔记:Java之并发:synchronized

来源于Java高并发之魂: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
public class Demon1 implements Runnable{
static Demon1 instance = new Demon1();
static int i = 0;

public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(instance);
Thread thread2 = new Thread(instance);

thread1.start();
thread2.start();

thread1.join();
thread2.join();

// 结果并不会是 j*2
// 1 读取 count 2 count+1 3 count写回内存
System.out.println(i);
}
@Override
public void run() {
for (int j = 0; j < 100000000; j++) {
i++;
}
}
}

对共享变量的操作不做同步处理就会导致错误,Java执行 i++分为三步

  • 从共享主内存中读取i至当前线程的内存
  • i进行加一操作
  • i写回共享主内存

所有,当线程1执行到第二步是,线程2执行第一步,那么必然导致获取的i的值不一样,所有,必须要求同一时刻最多只能有一个线程执行该段代码,以达到保证并发安全的效


地位

  • 是Java关键词,被Java原生支持
  • 是最基本的同步互斥手段

两种用法

对象锁

填入Thread中的是同一个对象实例

同步代码块

同一个对象之间的

  • 同步代码块锁(使用实例对象本身this作为加锁对象)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Demon2 implements Runnable {
// ...
@Override
public void run() {
synchronized (this) {
}
}

static Demon2 instance = new Demon2();

public static void main(String[] args) {
Thread thread1 = new Thread(instance);
Thread thread2 = new Thread(instance);

thread1.start();
thread2.start();

// 等待
while (thread1.isAlive() || thread2.isAlive()) {
}
}
}
  • 自己指定加锁对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Demon3 implements Runnable{
static Object obj1 = new Object();
@Override
public void run() {
synchronized (obj1) {
}
}

static Demon3 instance = new Demon3();
public static void main(String[] args) {
Thread thread1 = new Thread(instance);
Thread thread2 = new Thread(instance);

thread1.start();
thread2.start();

// 等待
while (thread1.isAlive() || thread2.isAlive()) {
}
}
}

方法锁

默认锁定对象为this当前实例对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Demon4 implements Runnable {
@Override
public void run() {
this.func();
}
public synchronized void func() {
}

static Demon4 instance = new Demon4();
public static void main(String[] args) {
Thread thread1 = new Thread(instance);
Thread thread2 = new Thread(instance);

thread1.start();
thread2.start();

// 等待
while (thread1.isAlive() || thread2.isAlive()) {
}
}
}

类锁

填入Thread中的是同一类的不同实例,同一个类之间的,只有一个类对象

  • 修饰静态的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Demon5 implements Runnable {
@Override
public void run() {
Demon5.func();
}
public synchronized static void func() {
}

static Demon5 instance1 = new Demon5();
static Demon5 instance2 = new Demon5();
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(instance1);
Thread thread2 = new Thread(instance2);

thread1.start();
thread2.start();

thread1.join();
thread2.join();

}
}
  • 指定锁为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
public class Demon6 implements Runnable{
@Override
public void run() {
this.func();
}
public void func() {
// 失败
// synchronized (this) {
synchronized (Demon6.class) {
}
}

static Demon6 instance1 = new Demon6();
static Demon6 instance2 = new Demon6();

public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(instance1);
Thread thread2 = new Thread(instance2);

thread1.start();
thread2.start();

thread1.join();
thread2.join();
}
}

其实,Java中几乎所有的类都是Class的实例,所有第二大类其实也是可以归到第一大类中的。也就是每一个实例都有一把锁


线程访问同步方法的七种情况

  • 1 两个线程同时访问一个对象的同步方法 ✔️
  • 2 两个线程同时访问两个对象的同步方法 ❌ 不同的实例
  • 3 两个线程同时访问同步静态方法 ✔️
  • 4 同时访问同步方法非同步方法 ❌ 非同步方法不受影响
  • 5 同一对象不同的普通同步方法 ✔️ 同一个类对象,synchronized默认加到this上,受影响
  • 6 同时访问静态的synchronized方法和非静态的.. ❌ 前者加类锁,后者加对象锁,不是同一个锁
  • 7 方法抛异常,会释放锁
  • 核心思想
    • 把锁只能同时被一个线程获取,没有拿到锁的线程必须等待(对应第1、5种情况)
    • 每个实例都对应有自己的一把锁,不同实例之前互不影响,所有的类都是Class类的实例
    • 无论是方法正常执行完毕或者方法抛出异常,都会释放锁

一些理论

重入

同一线程外层函数获得锁之后,内层函数可以直接再次获取该锁,可以提升封装性,避免死锁

  • 不要求是同一个方法
  • 不要求为同一类
  • 粒度:线程范围

不可中断

无法中途释放锁

等价形式

以下两者是等价的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Demon9 {
Lock lock = new ReentrantLock();

public synchronized void func1() {
System.out.println("Synchronized方法");
}

public void func2() {
lock.lock();
try {
System.out.println("Lock方法");
} finally {
lock.unlock();
}
}

public static void main(String[] args) {
Demon9 demon9 = new Demon9();
demon9.func1();
demon9.func2();
}
}

代码进行反编译转换成为字节码

先写一个简单的含有其关键词的类

1
2
3
4
5
6
7
8
public class Demon10 {
final Object obj = new Object();

public void func() {
synchronized (obj) {
}
}
}
1
2
javac Demon10.java
javap -verbose Demon10 > Demon10_decode.txt

打开文件 Demon10_decode.txt,就会发现其中有monitorentermonitorexit,后者数量大于前者,因为可能有多种退出情况,对应于操作系统的进程同步,前者相当于是p(),后者相当于是v()

1
2
3
4
5
6
7
8
9
10
11
12
...
6: monitorenter
7: aload_1
8: monitorexit
9: goto 17
12: astore_2
13: aload_1
14: monitorexit
15: aload_2
16: athrow
17: return
...

JVM追踪对象加锁次数,线程第一次给对象加锁时,计数变为1。之后每次加锁会递增每次任务完成计数减一,当计数器减为零时锁完全释放

线程有本地内存,线程间通过主内存通信


缺陷

  • 效率低 释放条件少结束,异常),无法设置超时,无法中断一个试图获取锁的线程
  • 不够灵活 加减锁时机单一(读写锁更灵活)
  • 无法知道释放成功获得该锁

面试问题

  • 使用注意点:锁对象不能为空,作用域不宜过大,避免死锁

  • 如何选择Lock和Synchronized关键字
    优先使用java.util.concurrent包中的
    其次,使用synchronized减少代码量
    最后,..

  • 多线程访问同步方法的七种情况


思考题

锁的升级降级,偏斜锁,轻量级锁,重量级锁