1、多线程内存情况简析
参考:aHR0cHM6Ly93d3cuYmlsaWJpbGkuY29tL3ZpZGVvL0JWMXVKNDExazd3eT9wPTMwOQ==
同样声明,对于 Java 程序运行时的内存、JVM 等问题,可能阐述的并不会非常准确,仅仅是大致上阐述以有助于更好地理解一些复杂的事情。
代码一(单线程):
public class MyThread implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(i);
}
}
}
public class MyThreadTest {
public static void main(String[] args) {
Thread myThread = new Thread(new MyThread());
myThread.run(); // 人工直接调用 run 方法,相当于调用类中的普通的方法
}
}
此时,内存中的情形如下图:
单线程时,按照代码的顺序,main
方法在栈中开辟空间(入栈),随后调用的run
方法也同样在栈中开辟空间(入栈)。CPU 等只需对这一个栈进行“操作”(运行一个栈即可)。
- 代码二(多线程):
上图中每个栈用不同的颜色来表示。
在多线程的模型中,一个线程需要一个单独的栈区来支持。
当调用start
方法的时候,JVM 会为该线程开辟一个属于它的栈区,由该栈区负责该线程中涉及到的方法、变量等。
对于 CPU 等而言,所要做的就是基于时间片轮换、自定义等各类的规则来对每一个栈区进行交替“操作”(交替“运行”每一个栈区)。最终达到多线程的并发的目的。
public class MyThread implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(i);
}
}
}
public class MyThreadTest {
public static void main(String[] args) {
Thread myThread = new Thread(new MyThread());
myThread.start();
}
}
此时,内存中的情形如下图:
补充一点:每一个线程拥有各自的栈区,但是堆区是被所有线程共享的,所以,从这个角度出发,需要线程同步机制来确保堆区中的数据等正确、安全。
2、关于创建线程补充
请先阅读38、【JavaSE】【Java 核心类库(下)】多线程(1)中的标题号为3.2部分的内容。
先看下面的代码:
/* 实现 java.lang.Runnable 接口 */
public class MyOperation implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getId() + ": " + i);
}
}
}
public class Main {
public static void main(String[] args) {
Thread thread1 = new Thread(new MyOperation());
Thread thread2 = new Thread(new MyOperation());
thread1.start();
thread2.start();
}
}
public class Main {
public static void main(String[] args) {
Runnable runnable = new MyOperation();
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
thread2.start();
}
}
好,通过比较两个Main
类中的代码,可以知道要探讨的问题是:在使用Thread(Runnable runnable)
构造方法的时候,传入同一个java.lang.Runnable
引用以及传入不同的java.lang.Runnable
的引用,有什么区别。
首先明确一点,在上面的两个Main
类代码中,不管传入的是同一个java.lang.Runnable
引用还是不同的java.lang.Runnable
的引用,所创建的线程是两个,不是说因为传入的是同一个java.lang.Runnable
引用而就是一个线程,线程的个数看的是new Thread(···)
的次数。上面的代码会印证这一点,因为输出的时候加了Thread.currentThread().getId()
。
下面的问题,就是说,“传入的引用相同与不同”是否有区别?正常情况下是没有区别!因为实现java.lang.Runnable
接口的类所定义出的可以理解为是一个“操作说明书”,把“操作说明书”交给java.lang.Thread
类让其按照“说明书”的步骤来执行即可。这样的话,“给两份一样的操作说明书”或者“只给同一份的操作说明书”,并没有任何区别,反倒是“两份一样的操作说明书”比较“浪费资源”(多new
出了一个java.lang.Runnable
类)。
但是,为什么还要在这里说讨论这个问题,因为在“线程同步”中,如果出现上面的情况,又会有什么出现问题?!
3、线程同步
3.1、概述
之所以要有“线程同步”这样的一个机制,主要是因为线程之间可能会存在着资源共享的情况,即多个线程要去使用同一资源(可以表述为“共享资源”、“临界资源”等),那么就会出现数据不一致、脏数据等问题。
举个例子:
假设:银行卡里存有500元,张三与李四都可以取出这银行卡里面的钱,张三通过 ATM 取出全部的钱,而李四想将全部的钱存入手机的支付软件的余额中。巧合的是,两人差不多在同一时间进行着各自的操作。李四成功完成了操作,看到支付软件上的余额显示为500元,而此时,张三这边也进行到 ATM 机正在“吐钱”,最终拿到了现金500元。
当然,现实情况中是不会发生这样的“好事”的,否则银行要关门了。
但是,分析一下上面所叙述的过程,两种方式看作两个线程,目的都是“取出同一张银行卡中的钱”,但是两个线程通过不同途径的查询方式,均得出了“余额有500元”这样的结论,然后都成功的取出。这是一个非常重大的错误!
多个线程并发读写同一个临界资源时会发生线程并发安全问题!
线程同步机制的目的就是为了在多线程并发条件下,对线程之间进行通信与协调,最终能够正确地处理数据等。
下面用代码模拟一下上面所讲的这样的一个过程:
/* 银行账户类,balance 表示账户中的余额 */
public class Balance {
private int balance;
public Balance() {
}
public Balance(int balance) {
this.balance = balance;
}
public int getBalance() {
return balance;
}
public void setBalance(int balance) {
this.balance = balance;
}
}
/* 定义一个“取款”的操作 */
public class GetBalanceOperation implements Runnable {
private Balance balance; // 取款的目标账户
private int account; // 取款金额
public GetBalanceOperation(Balance balance, int account) {
this.balance = balance;
this.account = account;
}
@Override
public void run() {
if (balance != null) {
int temp = balance.getBalance();
if (temp >= account && account > 0) {
temp -= account;
System.out.println("交易处理中···");
try {
Thread.sleep(300); // 一方面为了模拟真实的取款过程,另一方面为了使多线程所会引发的问题更明显
balance.setBalance(temp);
System.out.println("交易完成!");
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
System.out.println("余额不足!");
}
}
}
}
public class Main {
public static void main(String[] args) {
Balance balance = new Balance(500);
Runnable operation = new GetBalanceOperation(balance, 600);
Thread thread1 = new Thread(operation);
Thread thread2 = new Thread(operation);
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
System.out.println("余额:" + balance.getBalance());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
最后,输出的结果是“300”,而现实生活中,两次取款后,最后的余额应该是“500-200-200=100”。
为了使“多个线程并发对同一个临界资源所会产生的问题”体现的更明显,上面的代码中,特别是
run
方法的写法是稍微有些讲究的,比如说不直接在成员变量balance
上进行减法,setBalance
方法调用位置在sleep
方法后等。
线程1与线程2并发执行,由于线程1是先启动的,当线程1执行到sleep
的时候,此时“取款后更新余额”的方法setBalance
方法并没有调用,而这时,线程2虽然比线程1略晚一点,但run
方法已经是在执行中了,问题就出现了,线程1由于还没及时“更新余额”所以线程2读取到的“余额”仍然是“最初的余额”(实例代码自中给的是500),所以线程2是基于“最初的余额”进行“取款”的。最后的局面是,两个线程中的temp
值都是“500-200=300”,所以,就出现了输出的结果是“300”这样的局面。(总结:线程一执行取款时还没来得及将取款后的余额更新,线程二就已经开始取款)
3.2、线程同步机制(如何实现线程同步)
- 继续用上面的“余额”代码来探讨,首先,来看一下用下面的代码来解决“余额不正确”的问题:
public class Main {
public static void main(String[] args) {
Balance balance = new Balance(500);
Runnable operation = new GetBalanceOperation(balance, 200);
Thread thread1 = new Thread(operation);
Thread thread2 = new Thread(operation);
thread1.start();
try {
thread1.join();
thread2.start(); // thread2 的 start 的调用位置
thread2.join();
System.out.println("余额:" + balance.getBalance());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
从代码中也能明确地看出解决方案,就是先让子线程1启动但不让子线程2启动,同时让主线程等待子线程1完成后再继续,因为子线程2的启动仍然要在主线程中,所以代码这样一写,就能够解决问题。
但是,这并不是线程同步机制,这样的做法,不是不可以,但是这样的做法,是一种“用着多线程的语法,写着单线程思路的代码”,意思就是线程的调度顺序还是由代码的编写顺序控制的,我们多线程的目标是,在指定我们该指定的调度规则后,线程的调度由系统自行完成,而不是主观通过控制代码编写顺序实现所谓的“多线程”,如果是这样,上面的例子中,两个线程的代码还好编写,那如果是高访问量、高读写等特点的大型系统,那又如何去编写!
3.2.1、使用 synchronized 关键字实现线程同步
使用
synchronized
关键字来实现“同步锁”(有的地方称“对象锁”、“同步监视器”等)机制从而保证线程在某一执行阶段的原子性。原子性,所谓原子性是指不可分割的一系列操作指令,在执行完毕前不会被任何其他操作中断,要么全部执行,要么全部不执行。
“锁”,这个字在多线程体系中会经常见到。多线程场景下,会出现资源竞争等,需要对部分代码等进行“加锁”,进而达到某个线程可以短暂“独自占有、使用、操作”的相应的资源这一目的。
3.2.1.1、synchronized 代码块
使用
synchronized
修饰代码块(也称“同步代码块”),即表示线程可以对这部分代码“加锁”。“加锁”意味着线程在执行这部分代码的时候具有原子性,也就是说,要执行就必须执行完,中间不能被打断(某一线程执行“加锁”的代码时,其他的线程是不可能再同时执行这段被“加锁”的代码,其他线程将会被变为“阻塞”状态)。语法格式如下:
synchronized(类类型的引用) {
编写所有需要锁定的代码;
}
- 下面,使用
synchronized
代码块来解决“多线程取款”问题:
/* 银行账户类,表示一个银行账户,balance 为账户中的金额 */
public class Balance {
private int balance;
public Balance() {
}
public Balance(int balance) {
this.balance = balance;
}
public int getBalance() {
return balance;
}
public void setBalance(int balance) {
this.balance = balance;
}
}
/* 定义一个类,由该类的实例将来作为“锁” */
public class GetBalanceLock {
}
public class GetBalanceOperation implements Runnable {
private Balance balance;
private int account;
private GetBalanceLock lock = new GetBalanceLock(); // “锁”
public GetBalanceOperation(Balance balance, int account) {
this.balance = balance;
this.account = account;
}
@Override
public void run() {
synchronized (lock) {
if (balance != null) {
int temp = balance.getBalance();
if (temp >= account && account > 0) {
temp -= account;
System.out.println("交易处理中···");
try {
Thread.sleep(300);
balance.setBalance(temp);
System.out.println("交易完成!");
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
System.out.println("余额不足!");
}
}
}
}
}
public class Main {
public static void main(String[] args) {
Balance balance = new Balance(500);
Runnable operation = new GetBalanceOperation(balance, 200);
Thread thread1 = new Thread(operation);
Thread thread2 = new Thread(operation);
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
System.out.println("余额:" + balance.getBalance());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
交易处理中···
交易完成!
交易处理中···
交易完成!
余额:100
下面讨论一下
synchronized
括号中的“锁”,“锁”的要求是,如果希望多个线程在执行到synchronized
代码块的时候能够实现“临时串行”,那么就要求这多个线程都用的是同一个“锁”对象,也就是这个过程中必须保证“锁”对象的唯一性。可以将
synchronized
代码块想象成“衣服店里的试衣间”,而“锁”对应的就是“试衣间门上的锁”。
“衣服店里的试衣间”要是没有人的话是一直开放的。顾客想试穿衣服,如果试衣间没有人,可以直接进去使用,然后从门的内侧把门锁上,这样试衣间就暂时归这位顾客使用、其他想试穿的顾客就无法进入了;等这位顾客试穿完后,将试衣间的门锁重新从里面打开,其他顾客才能进去试穿。
某个线程获取到 CPU 等资源(想要执行synchronized
代码块的大前提)并要开始执行synchronized
代码块中的内容时:首先需要“判断锁”(试衣间是否有人);可行的话再去“拥有(占有)锁”(如果试衣间没有人,可以进入试衣间,从门内锁门);synchronized
代码块执行完毕后,“释放锁”(从试衣间开门出来)。
判断锁(“判断试衣间是否有人”可以表述为“判断锁”)
-->
获取锁(“从门内将试衣间锁上”可以表述为“获取、占有、拥有锁”)
-->
释放锁(“试穿完后从试衣间开锁出来”可以表述为“释放锁”)
- 如果不能在过程中保证上面所提到的“唯一性”的话,导致多个线程在执行
synchronized
语句块的时候不会再像“多个顾客用一个试衣间试穿衣服”那样“串行”,synchronized
语句块便是无意义的存在。下面看看具体的案例:
public class GetBalanceOperation implements Runnable {
private Balance balance;
private int account;
public GetBalanceOperation(Balance balance, int account) {
this.balance = balance;
this.account = account;
}
@Override
public void run() {
// synchronized 中使用 new
synchronized (new GetBalanceLock()) {
if (balance != null) {
int temp = balance.getBalance();
if (temp >= account && account > 0) {
temp -= account;
System.out.println("交易处理中···");
try {
Thread.sleep(300);
balance.setBalance(temp);
System.out.println("交易完成!");
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
System.out.println("余额不足!");
}
}
}
}
}
public class Main {
public static void main(String[] args) {
Balance balance = new Balance(500);
Runnable operation = new GetBalanceOperation(balance, 200); // 两个线程“共用一份操作说明书”
Thread thread1 = new Thread(operation);
Thread thread2 = new Thread(operation);
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
System.out.println("余额:" + balance.getBalance());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
交易处理中···
交易处理中···
交易完成!
交易完成!
余额:300
输出的结果很明显,并没有实现“线程同步”。其原因就是违反了之前提到的“唯一性”。用synchronized (new GetBalanceLock())
意味着任何一个线程执行到这里的时候都会创建新的“锁”对象,进而让线程“认为试衣间里没人”,然后“进入试衣间”,这样的话“试衣间”没有任何存在的意义了。
public class GetBalanceOperation implements Runnable {
private Balance balance;
private int account;
private GetBalanceLock lock = new GetBalanceLock();
public GetBalanceOperation(Balance balance, int account) {
this.balance = balance;
this.account = account;
}
@Override
public void run() {
synchronized (lock) {
if (balance != null) {
int temp = balance.getBalance();
if (temp >= account && account > 0) {
temp -= account;
System.out.println("交易处理中···");
try {
Thread.sleep(300);
balance.setBalance(temp);
System.out.println("交易完成!");
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
System.out.println("余额不足!");
}
}
}
}
}
public class Main {
public static void main(String[] args) {
Balance balance = new Balance(500);
// new 出了两个相同的操作
Runnable operation1 = new GetBalanceOperation(balance, 200);
Runnable operation2 = new GetBalanceOperation(balance, 200);
Thread thread1 = new Thread(operation1);
Thread thread2 = new Thread(operation2);
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
System.out.println("余额:" + balance.getBalance());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
交易处理中···
交易处理中···
交易完成!
交易完成!
余额:300
输出的结果很明显,也没有实现“线程同步”。其原因也是违反了之前提到的“唯一性”。两个线程一人一份“操作说明书”。new
出来两个“操作说明书”对象,但是对应的GetBalanceLock
“锁”就有两个,在执行synchronized
代码块时也一定不会是“串行”。
这种情况下,可以将“锁”设为静态即用static
修饰,不管new
多少份一样的“操作说明书”,“锁”就只有一个,满足“唯一性”。
public class GetBalanceOperation implements Runnable {
private Balance balance;
private int account;
private static GetBalanceLock lock = new GetBalanceLock(); // 用 static 修饰
public GetBalanceOperation(Balance balance, int account) {
this.balance = balance;
this.account = account;
}
@Override
public void run() {
synchronized (lock) {
if (balance != null) {
int temp = balance.getBalance();
if (temp >= account && account > 0) {
temp -= account;
System.out.println("交易处理中···");
try {
Thread.sleep(300);
balance.setBalance(temp);
System.out.println("交易完成!");
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
System.out.println("余额不足!");
}
}
}
}
}
- 上述,我们将
synchronized
代码块比作“试衣间”,但也不希望大家误解一个事情,就是synchronized
所处理的代码必须是一模一样的。因为上面所给出的代码例子所有线程执行的代码是一样的,“取款”,就好像“试衣间”只能“试穿衣服”。
我们使用各种手段实现线程同步,所希望的是资源的安全性,像使用synchronized
代码块“包裹”代码,根本原因是“这段代码会涉及到共享资源(共享的变量等)使用,需要对线程加以控制,防止共享资源出现安全问题(变量读写‘错乱’等)”。
public class MyLock {
}
/* 通过这里的代码,想传达出 synchronized 代码块中的代码可以代表的是不同的任务 */
/* 不要受前面的代码的影响,认为 synchronized 代码块中的代码只能做同样的任务 */
/* 未来可能会遇到,执行的代码不同,但是代码所要涉及的资源是同样的,这个时候仍需要合适的线程同步机制解决 */
public class OperationOne implements Runnable {
private final MyLock lock;
public OperationOne(MyLock lock) {
this.lock = lock;
}
@Override
public void run() {
synchronized (lock) {
for (int i = 0; i < 5; i++) {
System.out.println("CHN");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public class OperationTwo implements Runnable {
private final MyLock lock;
public OperationTwo(MyLock lock) {
this.lock = lock;
}
@Override
public void run() {
synchronized (lock) {
for (int i = 0; i < 5; i++) {
System.out.println("PRC");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public class Main {
public static void main(String[] args) {
MyLock lock = new MyLock();
Thread thread1 = new Thread(new OperationOne(lock));
Thread thread2 = new Thread(new OperationTwo(lock));
thread1.start();
thread2.start();
}
}
输出结果:先打印5个“CHN”,再打印5个“PRC”。
因为两个线程用的“锁”对象是同一个,所以执行synchronized
代码块时,线程串行执行
- “锁”的引用建议用
final
修饰,防止中途被修改。非final
的对象可以被重新赋值,“锁”对象就不受管控了。当一个“锁”被其他线程占有时,当前线程可以对“锁”对象重新赋值(相当于从新创建了一个“锁”对象),从而也拿到了运行的权利。
3.2.1.2、synchronized 方法
可以使用
synchronized
关键字来修饰方法(也称“同步方法”),所达到的效果与synchronized
代码块的效果相同,即意味着整个方法被“加锁”,涉及的代码范围更大。用
synchronized
关键字来修饰方法的时候,对于静态方法、非静态方法会略有区别。用
synchronized
关键字修饰非静态方法,先看下面的代码:
/* 银行账户类,balance 表示余额 */
public class Balance {
private int balance;
public Balance(int balance) {
this.balance = balance;
}
public int getBalance() {
return balance;
}
public void setBalance(int balance) {
this.balance = balance;
}
}
/* “取款”操作 */
public class GetBalanceOperation implements Runnable {
private Balance balance;
private int account;
public GetBalanceOperation(Balance balance, int account) {
this.balance = balance;
this.account = account;
}
// synchronized 修饰 run 方法是可以的!!!
// synchronized 修饰 run 方法即表示 run 方法是一个“同步方法”
// 即当多个线程执行 run 方法中所有的代码,将会“串行”执行。某个线程要将 run 方法全部执行完后,其他线程才能执行 run 方法
@Override
public synchronized void run() {
if (balance != null) {
int temp = balance.getBalance();
if (account > 0 && account <= temp) {
temp -= account;
System.out.println("交易处理中···");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
balance.setBalance(temp);
System.out.println("交易完成!");
}
}
}
}
public class Main {
public static void main(String[] args) {
Balance balance = new Balance(800);
GetBalanceOperation getBalanceOperation = new GetBalanceOperation(balance, 200);
Thread thread1 = new Thread(getBalanceOperation);
Thread thread2 = new Thread(getBalanceOperation);
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("余额:" + balance.getBalance());
}
}
看完上面的代码,大家可能有疑问,在synchronized
代码块中“费了九牛二虎之力”所讲的“锁”对象到哪里去了?在用synchronized
修饰非静态方法时,这个“锁”对象仍是存在的,只不过这个“锁”对象就是“调用该非静态方法的对象本身”。通过下面的两个等价代码,就能理解为什么这么说:
@Override
public synchronized void run() {
if (balance != null) {
int temp = balance.getBalance();
if (account > 0 && account <= temp) {
temp -= account;
System.out.println("交易处理中···");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
balance.setBalance(temp);
System.out.println("交易完成!");
}
}
}
@Override
public void run() {
synchronized (this) {
if (balance != null) {
int temp = balance.getBalance();
if (account > 0 && account <= temp) {
temp -= account;
System.out.println("交易处理中···");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
balance.setBalance(temp);
System.out.println("交易完成!");
}
}
}
}
用synchronized
修饰非静态方法,等价于使用synchronized (this)
代码块然后代码块中是整个方法体。“锁”对象的引用就是synchronized (this)
中的this
就上面的“取款”代码而言,“锁”对象是main
方法中定义的GetBalanceOperation
类的对象:getBalanceOperation
。
因为
synchronized
修饰的是run
方法,而前面提到这种情况下“锁”对象就是“调用该非静态方法的对象本身”,那么是谁调用的run
方法?如果大家对java.lang.Thread
类的源码有有印象的话,对于使用Thread(Runnable target)
构造方法创建的线程来说,java.lang.Thread
对象使用start
启动线程后,由 JVM 去调用java.lang.Thread
对象中的run
方法,此时的run
方法,本质上是由所传参数target
中的run
方法。
@Override
public void run() {
if (target != null) {
target.run();
}
}
理清头绪之后,再来看看,这里的“锁”对象是否是“唯一”的,很显然是“唯一”的。所以线程能够实现同步。
- 补充代码:
public class GetBalanceOperation implements Runnable {
private Balance balance;
private int account;
public GetBalanceOperation(Balance balance, int account) {
this.balance = balance;
this.account = account;
}
// synchronized “间接加修饰 run 方法”
// 这样写的话,相对来说,代码结构会比较清晰
private synchronized void get() {
if (balance != null) {
int temp = balance.getBalance();
if (account > 0 && account <= temp) {
temp -= account;
System.out.println("交易处理中···");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
balance.setBalance(temp);
System.out.println("交易完成!");
}
}
}
@Override
public void run() {
get();
}
}
- 用
synchronized
关键字修饰静态方法,先看下面的代码:
public class Product {
private static int count;
public static void setCount(int count) {
Product.count = count;
}
public static int getCount() {
return count;
}
}
public class BuyThread extends Thread {
private synchronized static void buy() {
int temp = Product.getCount();
if (temp >= 10) {
System.out.println("正在出货···");
temp -= 10;
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Product.setCount(temp);
System.out.println("出货成功!");
}
}
@Override
public void run() {
buy();
}
}
public class Main {
public static void main(String[] args) {
Product.setCount(30);
Thread thread1 = new BuyThread();
Thread thread2 = new BuyThread();
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("剩余:" + Product.getCount());
}
}
正在出货···
出货成功!
正在出货···
出货成功!
剩余:10
首先,上面的代码可能与之前的代码有所不同,主要是因为要创造能够展现“使用synchronized
修饰静态方法”的条件。
public synchronized static xxx xxx() {···}
对于通过“继承java.lang.Thread
类创建线程类,然后再去使用new
创建多个线程”的方式,如果需要进行线程同步的话,那必须确保“锁”对象是唯一的。之前可能大家看到,面对这样的情景,可以使用synchronized
代码块然后使用static
修饰的一个对象作为唯一的“锁”对象,实现线程同步;而在这里所展示的是“使用synchronized
修饰静态方法”也是能够面对这样相似的情景。不过相对来说,这种“使用synchronized
修饰静态方法”相对来说还是有些局限性的,毕竟在静态方法中只能使用静态的。
下面的讨论一下,为什么“使用synchronized
修饰静态方法”也能实现线程同步?还是同样的道理,这个唯一的“锁”对象是什么?这里的“锁”对象是类对象,每个类都有唯一的一个类对象,获取类对象:类名.class
。
类对象:在“面向对象编程”中,“万物皆对象”理念是一直贯穿始终的,所有的类,包括 Java 本身提供的类、由我们自定义的类等最终都会归为一个“类”,所有的类都是这个“类”的对象。
可以这样理解,为什么 JVM 能够识别带class
关键字的就能判定它是一个类?说明在底层中,所有的类有一个“模板”,通过这个“模板”会很好的判定哪些是类,哪些是其他的。而 Java 中“模版”不就可以说成类吗!(粗浅理解,详细内容,见TODO 反射机制)
这样的话,上面的代码可以等价于:
public class BuyThread extends Thread {
private static void buy() {
synchronized (BuyThread.class) {
int temp = Product.getCount();
if (temp >= 10) {
System.out.println("正在出货···");
temp -= 10;
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Product.setCount(temp);
System.out.println("出货成功!");
}
}
}
@Override
public void run() {
buy();
}
}
正在出货···
出货成功!
正在出货···
出货成功!
剩余:10
注意:静态方法与非静态方法同时使用了synchronized
后它们之间是非互斥关系的,原因在于静态方法的“锁”对象是类对象而非静态方法的“锁”对象的是当前方法所属对象。
3.2.1.3、synchronized 注意事项
多个需要同步的线程在访问同步块时(当多个线程因为资源等问题希望“串行”执行某些代码时),使用的应该是同一个锁对象引用。
在使用同步块时应当尽量减少同步范围以提高并发的执行效率,即
synchronized
关键字影响的范围尽可能小,比如“一般情况下,能小范围地使用synchronized
代码块的就没必要用synchronized
修饰整个方法”。