一起学并发编程 - synchronized详解

文章目录
  1. 1. 概述
    1. 1.1. 写法
    2. 1.2. 案例一:静态同步方法
    3. 1.3. 案例二:同步方法单一对象锁
    4. 1.4. 案例三:Lock对象锁
    5. 1.5. 案例四:同步到多个对象锁
  2. 2. - 说点什么

synchronized是JAVA语言的一个关键字,使用 synchronized 来修饰方法或代码块的时候,能够保证多个线程中最多只有一个线程执行该段代码 …

概述

  • synchronized关键字可以作为函数的修饰符,也可作为函数内的语句,也就同步方法同步代码块块。细分为 instance variable(实例变量)、object reference(对象引用)、static method(静态方法) 和 class literals(常量类)。
  • 无论·synchronized·关键字加在方法上还是对象上,它获取的都是对象锁,而不是将一段代码或一个函数当作锁,而且同步方法很可能还会被其他线程的对象访问。
  • 每个对象只有一个锁(lock)与之相关联。
  • 实现同步则是以系统开销作为代价,甚至可能造成死锁,所以尽量避免滥用。

同步方法: 使用 synchronized 标记的方法,只有获得该方法类实例的锁才能执行,否则所属线程将被阻塞,方法一旦执行,就独占该锁,直到该方法执行完毕将锁释放,被阻塞的线程才能获得锁从而执行。这种机制确保了同一时刻该类实例,所有声明为 synchronized 的函数中只有一个方法处于可执行状态,从而有效避免了类成员变量访问冲突。

同步方法缺陷:若将一个大的方法声明为 synchronized 将会大大的影响效率,典型的,若将线程类的方法 run() 声明为 synchronized,由于在线程的整个生命期中它一直在运行,因此将导致对本类任何 synchronized 方法的调用都不会成功。因此在这种环境下,可以使用同步代码块的方式

同步代码块: 除了方法前用synchronized关键字,还可以用于方法中的某个区块中,表示只对该区域内的资源进行互斥操作。用法是: synchronized(this){/区块/},它的作用域是当前对象。也可以创建一个特殊的instance变量(它得是一个对象)来充当锁

写法

类的范围写法,防止多个线程同时访问这个类中的synchronized method,它可以对类的所有对象实例起作用

1
2
3
4
5
6
7
8
9
static synchronized void transferAccount() {
//...
}
//等同
static void transferAccount() {
synchronized(Bank.class) {
//...
}
}

对象实例内写法,多个线程同时访问该对象的synchronized 方法,如果该对象实例有多个synchronized方法,任意线程访问了其中的一个synchronized方法,剩余线程则不能并发访问该对象中任何一个synchronized方法(不同对象实例的 synchronized方法是互不干扰的。其它线程依然可以并发访问相同对象类不同实例中的synchronized方法,如果想做到在不同对象实例同步需要使用class literal的方式)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
synchronized void transferAccount() {
//...
}

void transferAccount() {
synchronized(this) {
//...
}
}

private final byte[] LOCK = new byte[0]; // 特殊的实例化对象
void transferAccount() {
synchronized(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
33
34
35
class Bank1 {
synchronized static void transferAccount() {
System.out.println("开始转账:" + Thread.currentThread().getName());
try {
Thread.sleep(3 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("转账完毕");
}

synchronized static void debit() {
System.out.println("开始扣款:" + Thread.currentThread().getName());
try {
Thread.sleep(3 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("扣款完毕");
}
}

public class BankMain {

public static void main(String[] args) {
new Thread(Bank1::transferAccount, "北京银行").start();
new Thread(Bank1::debit, "上海银行").start();
}
}
////////////////////////日志////////////////////////
开始转账:北京银行
转账完毕
开始扣款:上海银行
扣款完毕
////////////////////////日志////////////////////////

分析:通过日志看到在使用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
class Bank2 implements Runnable {

@Override
public synchronized void run() {
System.out.println("查询数据:" + Thread.currentThread().getName());
System.out.println("开始转账:" + Thread.currentThread().getName());
try {
Thread.sleep(5 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("转账完毕");
}
}

public class BankMain {

public static void main(String[] args) {
Bank2 bank2 = new Bank2();
new Thread(bank2, "北京银行").start();
new Thread(bank2, "上海银行").start();
}
}
////////////////////////日志////////////////////////
查询数据:北京银行
开始转账:北京银行
转账完毕
查询数据:上海银行
开始转账:上海银行
转账完毕
////////////////////////日志////////////////////////

分析:方法同步执行,谁获得锁谁先执行

案例三: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
33
34
class Bank3 implements Runnable {
private final byte[] LOCK = new byte[0]; // 特殊的实例化变量

@Override
public void run() {
System.out.println("查询数据:" + Thread.currentThread().getName());
synchronized (LOCK) {//该种方式只能锁
System.out.println("开始转账:" + Thread.currentThread().getName());
try {
Thread.sleep(5 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("转账完毕");
}
}
}

public class BankMain {

public static void main(String[] args) {
Bank3 bank = new Bank3();
new Thread(bank, "北京银行").start();
new Thread(bank, "上海银行").start();
}
}
////////////////////////日志////////////////////////
查询数据:北京银行
查询数据:上海银行
开始转账:北京银行
转账完毕
开始转账:上海银行
转账完毕
////////////////////////日志////////////////////////

分析:互斥部分上锁,查询数据部分则并发执行

案例四:同步到多个对象锁

前文说过一个实例对象一把锁,在案例三案例四中,都只实例化了一个对象,当对象为多实例化的时候,需使用class literal的方式,它和synchronized static method方式产生的结果一样,取得的锁很特别,为当前调用该方法对象所属的类(而不再是由这个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
class Bank4 implements Runnable {
@Override
public void run() {
System.out.println("查询数据:" + Thread.currentThread().getName());
synchronized (Bank3.class) {
System.out.println("开始转账:" + Thread.currentThread().getName());
try {
Thread.sleep(5 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("转账完毕");
}
}
}

public class BankMain {

public static void main(String[] args) {
new Thread(new Bank4(), "北京银行").start();
new Thread(new Bank4(), "上海银行").start();
}
}

////////////////////////日志////////////////////////
查询数据:北京银行
查询数据:上海银行
开始转账:北京银行
转账完毕
开始转账:上海银行
转账完毕
////////////////////////日志////////////////////////

可以推断:如果一个类中定义了一个synchronized static methodA,也定义了一个 synchronized 的 instance methodB,该类同一个对象在多线程中分别访问A和B两个方法时,并不会构成同步,因为它们的锁都不一样。methodA的锁是它的所属Class,而methodB的锁是当前对象(该部分代码未贴出,可以自己实现或者看GIT

- 说点什么

全文代码:https://gitee.com/battcn/battcn-concurent/tree/master/Chapter1-1/battcn-thread/src/main/java/com/battcn/chapter5

  • 个人QQ:1837307557
  • battcn开源群(适合新手):391619659

微信公众号:battcn(欢迎调戏)