Java高级程序设计笔记 • 【第3章 多线程(二)】

时间:2021-12-14 13:20:20

全部章节   >>>>


本章目录

3.1 同步代码块

3.1 线程安全

3.1.1 模拟银行取款

3.1.2 同步代码块的使用

3.1.3 实践练习

3.2 同步方法

3.2.1 同步方法

3.2.2 同步方法的使用

3.2.3 实践练习

3.3 死锁

3.3.1 死锁的概述

3.3.2 死锁的产生

3.3.3 实践练习

3.4  ThreadLocal类

3.4.1 ThreadLocal 类的概述

3.4.2 ThreadLocal 类的常用方法

3.4.3 ThreadLocal的使用

3.4.4 实践练习

总结:


3.1 同步代码块

3.1 线程安全

  • 多线程编程时,由于系统对线程的调度具有一定的随机性,所以,使用多个线程操作同一个数据时,容易出现线程安全问题
  • 当多个线程访问同一个资源时,如果控制不好,也会造成数据的不正确性
  • 以银行取钱为例:
  1. 用户输入账户、密码,系统判断用户的账户、密码是否匹配
  2. 用户输入取款金额 系统判断账户余额是否大于取款金额
  3. 如果余额大于等于取款金额,则取款成功,否则取款失败

3.1.1 模拟银行取款

示例:创建模拟两个线程的取款类 DrawThread,该类继承 Thread 类。取钱的业务逻辑为当余额不足时无法提取现金,当余额足够时系统吐出钞票,减少余额

public class DrawThread extends Thread {
// 模拟用户账户
private Account account;
// 当前线程索取钱数
private double drawAccount;
//完成数据初始化工作
public DrawThread(String name, Account account, double drawAccount) {
super(name);
this.account = account;
this.drawAccount = drawAccount;}
public void run() {
// 账户余额大于取钱数据
if (account.getBalance() >= drawAccount) {
System.out.println(this.getName() + "\t 取款成功 ! 吐钞 :" + drawAccount);
// 修改余额
account.setBalance(account.getBalance() - drawAccount);
System.out.println("\t 余额 : " + account.getBalance());
} else {
System.out.println(this.getName() + " 取钱失败!余额不足 ");}
}
}

// 当多个线程同时修改同一个共享数据时,将涉及数据安全问题

说明:

  • 由于多线程并发问题,一个线程执行余额操作可能未完毕,另外一个线程读取或者也在操作余额,必然会引起数据的不准确性
  • 这个时候需要在线程中加入对数据的保护机制,从而达到防止并发引起的数据不准确。

3.1.2 同步代码块的使用

Java中多线程中引入了同步监视器,使用同步监视器的常用方式是使用同步代码块,保证同一时间只能一个线程对敏感数据的操作

语法:

synchronized( 要锁定的对象 ){
// 同步代码块的执行体
}

注意:任何时刻只能有一个线程可以获得对同步监视器的锁定,当同步代码块执行完毕后,该线程会释放对该同步监视器的锁定

示例:使用同步代码块改进银行取款程序

synchronized (account) {
// 账户余额大于取钱数据
if (account.getBalance() >= drawAccount) {
System.out.println(this.getName() + "\t 取款成功 ! 吐钞 :" + drawAccount);
// 修改余额
account.setBalance(account.getBalance() - drawAccount);
System.out.println("\t 余额 : " + account.getBalance());
} else {
System.out.println(this.getName() + " 取钱失败!余额不足 ");
}
}

在run方法中加入同步代码块,锁定当前账户对象,保证其他线程无法同时操作

说明:

  • 方法体中使用 synchronized 将account对象进行锁定,此种操作符合“加锁 修改 释放锁”的逻辑
  • 任何线程在修改指定资源前,首先要对该资源进行锁定,在加锁期间,其他线程无法修改该资源,当该线程修改完成后,该线程释放对该资源的锁定,供下一个获取 CPU 资源的线程使用

3.1.3 实践练习

3.2 同步方法

3.2.1 同步方法

与同步代码块对应,Java 的多线程还提供了同步方法。同步方法就是使用 synchronized 关键字来修饰某个方法。

语法:

访问修饰符 synchronized 返回值类型 方法名称 ( 方法参数 ){
// 同步方法的方法体
}

说明:同步方法的同步监视器就是 this 当前对象本身,如果某个线程调用对象上的同步方法,首先就请求给对象上锁,然后执行方法体,最后释放锁。另一个调用同一个对象上同步方法的线程会被阻塞,直到锁被释放

3.2.2 同步方法的使用

示例:使用同步方法实现取款时数据安全控制

public class Account {
//账号、余额属性略…
// 提供一个线程安全的 draw() 方法来完成取钱操作
public synchronized void draw(double drawAmount) {
//部分代码略
// 修改余额关键代码
this.balance -= drawAmount;
System.out.println(" 账户 <" + this.getNo() + "> 余额为:" + this.getBalance() + " 元 ");}
//getter/setter方法略
}
public class SyncMethodThread extends Thread {
//账户对象和余额属性略
……
//线程类中的run主体方法,调用账户对象的取钱方法
public void run() {
// 直接调用 account 对象的 draw() 方法来执行取钱
// 同步方法的同步监视器是 this,this 代表调用 draw() 方法的对象
// 也就是说,线程进入 draw() 方法之前,必须先对 account 的对象加锁
account.draw(drawAmount); }
}

说明:线程对象的run方法中,未直接编写取款代码,而是调用Account账户对象的取款方法,而账户类中的取款方法使用synchronized关键字修饰,保证了同时只允许一个线程对账户对象完成取款操作,从而解决了并发时数据安全问题

3.2.3 实践练习

3.3 死锁

3.3.1 死锁的概述

同步就是指一个线程要等待另外一个线程执行完毕后才会继续执行的一种操作形式。虽然在一个程序中使用同步可以保证资源共享操作的正确性,但是过多同步或者同步控制不正确也会产生问题

Java高级程序设计笔记 • 【第3章 多线程(二)】

3.3.2 死锁的产生

所谓死锁,就是指两个线程都在等待彼此先完成,造成了程序的停滞状态。一般情况下,程序的死锁都是在程序运行时设计不当引发出现

模拟父亲操作:

public class Father {
// 定义父亲说话的方法
public void say() {
System.out.println(" 父亲对孩子说:把考试成绩单 给我,就给你玩具。");
}
// 定义得到试卷的方法
public void get() {System.out.println(" 父亲得到考试成绩单 ");}
}

模拟儿子操作:

public class Child {
// 定义孩子说话的方法
public void say() {
System.out.println(“ 孩子对父亲说:把玩具给我,就给你考试成绩单。");
}
// 定义孩子得到玩具的方法
public void get() {System.out.println(" 孩子得到玩具 ");}
}

示例:创建线程类,分别控制父子对象的执行

public class ThreadLock implements Runnable {
// 实例化一个静态 Father 类型的对象
static Father father = new Father();
// 实例化一个静态 Child 类型的对象
static Child child = new Child();
// 声明标记,用于判断哪个对象先执行
private boolean flag = false;
public void run() {
//分别控制父子执行,进行同步控制,代码略…
}
}

经验:

多个线程访问同一资源时,一定要考虑到同步问题,但过多的同步或者范围过大的同步会容易带来死锁

在进行多线程开发时,如果遇到同步问题,尽量缩小同步代码块范围或做好全面的测试工作,尽量避免出现死锁,否则程序将出现无限等待状态

3.3.3 实践练习

3.4  ThreadLocal类

3.4.1 ThreadLocal 类的概述

线程局部变量(ThreadLocal)是 Java 提供的一个线程安全类,通过使用 ThreadLocal 类可以简化多线程编程时的并发访问,使用这个工具类可以很便捷地隔离多线程程序的竞争资源

ThreadLocal 类的功能就是为每一个使用该变量的线程都提供一个变量值的副本,使每一个线程都可以独立地改变自己的副本,而不会和其他线程的副本冲突

3.4.2 ThreadLocal 类的常用方法

ThreadLocal类为我们提供了大量的方法以便使用

方法名

作用

T get()

返回当前线程所对应的线程局部变量值

void remove()

删除当前线程局部变量的值

void set(T value)

设置当前线程的线程局部变量值

3.4.3 ThreadLocal的使用

示例:作家写了 3 本书,每本书出版 5 次,模拟书籍出售情况

public class Writer {
private String name; // 作家姓名
// 定义一个 ThreadLocal 类型的变量,该变量将是一个线程的局部变量,用来保存作家每本作品的 总出版数量
private ThreadLocal<Integer> publishNumber = new ThreadLocal<Integer>();
public Writer(String name) {this.name = name; // 初始化姓名构造函数}
// 通过 ThreadLocal 类的 get() 方法,返回保存在线程中的变量值
public Integer getPublishNumber() {return publishNumber.get();}
//……
public class CalculationNumber implements Runnable {
private Writer writer; // 用于保存作家对象
private Random random = new Random(); // 实例化一个随机数对象
private int perEditionNumber = 0; // 用于保存每次出版的册数
public CalculationNumber(Writer writer) {this.writer = writer;}
public void run() {//加入线程延迟,模拟初版书籍5次……}
  • ThreadLocal的实例代表了一个线程局部的变量,每条线程都只能看到自己的值,并不会意识到其它的线程中也存在该变量
  • 它采用采用空间来换取时间的方式,解决多线程中相同变量的访问冲突问题
  • 每个Thread的对象都有一个ThreadLocalMap,当创建一个ThreadLocal的时候,就会将该ThreadLocal对象添加到该Map中,其中键就是ThreadLocal,值可以是任意类型

3.4.4 实践练习

总结:

  • 多线程编程时,由于系统对线程的调度具有一定的随机性,所以,使用多个线程操作同一个数据时,容易出现线程安全问题
  • 使用synchronized关键字可以实现多线程资源共享时的安全问题,包括同步代码块和同步方法
  • 同步虽然可以解决线程之间数据安全,但是同步范围过大或者过多容易引发死锁,所以必须进行优化设计
  • ThreadLocal Java 提供的一个线程安全类,通过使用 ThreadLocal 类可以简化多线程编程时的并发访问,使用这个工具类可以很便捷地隔离多线程程序的竞争资源