Java 多线程同步机制
文章目录
- Java 多线程同步机制
- 1 线程并发问题
- 2 synchronized 关键字
- 2.1 包裹同步代码块
- 2.2 修饰非静态方法
- 2.3 修饰静态方法
- 3 线程同步实例演示
- 3.1 线程不安全情况演示
- 3.2 使用 synchronized 关键字包裹同步代码块
- 4 实际开发中遇到线程安全问题的解决思路
- 5 扩展细节知识
- 5.1 Java 三大变量
- 5.2 JDK 中常用类线程安全
1 线程并发问题
- 实际开发中,项目运行在服务器上,服务器会定义线程、创建线程对象、启动线程等。我们更多关注的是编写的程序放到多线程环境下运行是否安全。
经典银行案例
- 两条线程同时操作,或者是在一方取款时出现了网络延迟,导致取完钱后未及时更新数据。
- 此时另一人用相同的账户继续取钱,并且取出了钱,那么就是线程不安全的。
需要考虑线程安全问题的前提条件:
- 程序存在多线程并发
- 有共享数据
- 共享数据有修改的行为
线程同步机制解决多线程数据安全问题:
- 让线程排队,不能并发,可以保证数据安全,专业术语称 线程同步机制。
- 线程排队会牺牲一部分效率,从大局看,数据安全排在第一位,只有数据安全了,考虑效率才有意义。
2 synchronized 关键字
-
synchronized
是用于实现线程同步机制的关键字,可以包裹代码块、可以修饰方法、可以修饰。
2.1 包裹同步代码块
synchronized(共享对象){
同步代码块
}
- 共享对象是需要排队线程的共享对象
- 同步代码块是需要排队执行的代码片段
- Java 语言中,任何一个对象都有一把“锁”(标记)
- 在运行状态中的线程遇到
synchronized
关键字后会进入锁池寻找对象锁 - 如果找到对象锁,就回到就绪状态抢夺 CPU 时间片。
- 如果该对象锁被占用,则会在锁池中等待该锁被归还,可以理解为阻塞在锁池中。
2.2 修饰非静态方法
public synchronized void doIt(){
同步代码
}
- 代码简洁,但不灵活,因为共享对象只能是 this
- 修饰方法时,表示整个方法体都需要同步,可能会扩大锁范围,降低效率,根据实际情况选择加锁方式。
- 当共享方法的整个方法体都需要加锁,且共享对象就为 this,则建议采用这种方式加锁,代码更加简洁。
2.3 修饰静态方法
public static synchronized void doIt(){
同步代码
}
- 如果修饰某个类的静态方法,表示给当前类加锁,所有的类对象都共享同一个把锁。
- 静态方法属于类,非静态方法属于对象。
以上锁都是排他锁,还有互斥锁
3 线程同步实例演示
线程同步 demo 有很多,如售票系统、银行取款等。这里演示前面提出的银行取款模拟。
3.1 线程不安全情况演示
账户类 Account
public class Account {
// 名字
private String name;
// 余额
private double money;
public Account(String name, double money) {
this.name = name;
this.money = money;
}
// 以下省略 setter 、 getter 、 toString 等方法
// 取钱方法
public boolean takeMoney(double money) {
// 查询余额
double before = this.getMoney();
// 判断取款金额是否大于账户余额
if (money > before) return false;
// 取款操作
double after = before - money;
System.out.println(Thread.currentThread().getName() + "取款操作完成!");
// 模拟网络延迟,放大问题
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 更新余额
this.setMoney(after);
System.out.println(Thread.currentThread().getName() + "余额更新完成");
return true;
}
}
线程类 AccountThread
public class AccountThread extends Thread {
// 两个线程共享一个对象
private Account account;
// 取款金额
private double money;
public AccountThread(Account account, double money) {
this.account = account;
this.money = money;
}
@Override
public void run() {
// 取款操作,线程不安全
boolean flag = account.takeMoney(money);
if (flag) {
System.out.println(Thread.currentThread().getName() + "取款成功,取完后余额为:" + account.getMoney());
} else {
System.out.println(Thread.currentThread().getName() + "取款失败");
}
}
}
主线程 main 方法
public class Test {
public static void main(String[] args) {
// 初始化账户
Account account = new Account("aaa", 10000.0);
// 线程对象
Thread t1 = new AccountThread(account, 5000);
Thread t2 = new AccountThread(account, 5000);
// 线程名
t1.setName("t1");
t2.setName("t2");
// 启动线程
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("最终账户余额:" + account.getMoney());
}
}
运行结果:
- 这种情况就是不安全的,账户一共 10000 元,取了两次 5000 后还剩下 5000!
- 如果多次运行也会出现线程安全的情况,取了两次 5000 后,余额为 0
3.2 使用 synchronized 关键字包裹同步代码块
public boolean takeMoney(double money) {
synchronized (this) { // 能锁住,只有一个 Account 对象,且是共享的,共享该对象的线程需要排队,如果不是同一个 Account 对象,则不需要排队
//synchronized (name) { // 能锁住,只有一个 String 对象,且是共享的
//synchronized (new Object()) { // 不能锁住,多个 Object 对象,这个是局部变量,不被共享。
//synchronized ("abc"){ // 能锁住,"abc" 在常量池中,被所有线程共享,所以所有线程执行这里时都会排队。
double before = this.getMoney();
if (money > before) return false;
double after = before - money;
System.out.println(Thread.currentThread().getName() + "取款操作完成!");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.setMoney(after);
System.out.println(Thread.currentThread().getName() + "余额更新完成");
return true;
}
}
- 共享对象有很多选择,根据实际情况选择即可。
- 不做其他修改,继续运行刚刚的主函数,结果如下:
- 可以看出,使用 synchronized 包裹同步代码块后,线程安全执行。
- 从输出的信息中可以看出,当 t1 线程更新完成后,t2 线程才进入 takeMoney() 方法。
4 实际开发中遇到线程安全问题的解决思路
- synchronized 会降低程序执行效率,用户体验不好。
- 在不得已的情况下使用线程同步机制。
- 方案一:尽量使用局部变量代替实例变量和静态变量,局部变量处于栈中,不被多线程共享,所以不存在线程安全问题。
- 方案二:如果必须实例化对象,可以考虑实例化多个对象,一个线程对应一个对象,这样实例变量的内存就不被共享了。
- 方案三:如果不能使用局部变量,也不能创建多个对象,就只能选择 synchronized 实现线程同步机制了,此时应当尽量缩小同步范围。
- 同步代码机制必然会降低运行效率,所以尽量避免使用。
5 扩展细节知识
5.1 Java 三大变量
- 实例变量:存放在堆中,只有一个,被多线程共享,可能存在线程安全问题。
- 静态变量:存放在方法区,只有一个,被多线程共享,可能存在线程安全问题。
- 局部变量:存放在栈中,一个线程一个栈,栈数据不被多线程共享,不存在安全问题。
5.2 JDK 中常用类线程安全
- StringBuffer 是线程安全的,方法使用 synchronized 关键字修饰
- StringBuilder 是非线程安全的。
- 局部变量使用 StringBuilder ,因为局部变量保存在栈中,不存在线程安全问题,效率会更高。
- ArrayList 是非线程安全的
- Vector 是线程安全的
- HashMap、HashSet 是非线程安全的
- Hashtable 是线程安全的
参考文章: