Java SE线程安全问题
CAMELLIA线程安全问题
在多线程编程中,多个线程可以同时访问和修改共享数据,如果不进行适当的同步处理,会导致数据不一致和不可预测的行为。
一、什么时候考虑安全问题?
1.1 需要考虑线程安全问题的情况
- 条件1:多线程的并发环境下。
- 条件2:有共享的数据。
- 条件3:共享数据涉及到修改的操作。
1.2 一般情况下的线程安全性分析
局部变量:一般情况下局部变量不存在线程安全问题。特别是基本数据类型的局部变量,因为它们在栈中存储,栈是线程私有的,不是共享的。如果是引用数据类型的局部变量,就另当别论了,因为它们可能指向堆中的共享对象。
实例变量:可能存在线程安全问题。实例变量存储在堆中,堆是多线程共享的。因此,如果多个线程同时访问和修改实例变量,可能会引起数据不一致。
静态变量:也可能存在线程安全问题。静态变量在堆中存储,多个线程可能会同时访问和修改静态变量,从而引发安全问题。
1.3 解决多线程并发操作中的安全问题
为了解决多线程并发操作中的安全问题,可以使用线程同步机制,即让线程t1和线程t2排队执行,保证同一时间只有一个线程能够对共享数据进行修改。
二、同步代码块
2.1 线程同步机制保证多线程并发环境下的数据安全
1. 线程同步的本质
线程同步的本质是让多个线程排队执行共享资源的操作,确保同一时间只有一个线程能够访问和修改共享资源。这可以防止数据竞争和不一致问题。
2. 语法格式
使用 synchronized
关键字来实现线程同步。语法格式如下:
1 2 3
| synchronized (共享对象) { }
|
这里的“共享对象”是用于线程同步的锁对象,必须是所有需要同步的线程共享的对象。如果选择不当,可能会导致不必要的线程同步,从而降低效率。
3. 原理
synchronized
关键字使用对象锁来实现同步。当一个线程进入同步代码块时,它需要先获取对象锁。只有获得对象锁的线程才能执行同步代码块,其它线程必须等待锁被释放。
4. 注意同步代码块的范围
同步代码块的范围应该尽可能小,以提高效率。过大的同步范围会导致更多的线程等待,从而降低程序性能。
代码解析
lock
对象用于实现线程同步。
synchronized (lock)
块确保同一时间只有一个线程可以执行其中的代码。
- 当一个线程进入
synchronized
块,它需要获取 lock
对象的锁。
- 当一个线程持有锁时,其他线程必须等待,直到锁被释放。
- 确保
synchronized
块尽可能小,以提高并发效率。
2.2 示例代码
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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
| package com.camellia.thread.ThreadSafe;
public class ThreadSafeSolve { public static void main(String[] args) { AccountSolve act = new AccountSolve("act-001", 10000); Thread t1 = new Thread(new WithdrawSolve(act)); Thread t2 = new Thread(new WithdrawSolve(act)); t1.start(); t2.start(); } }
class WithdrawSolve implements Runnable {
private AccountSolve act;
public WithdrawSolve(AccountSolve act) { this.act = act; }
@Override public void run() { this.act.withdraw(1000); } }
class AccountSolve {
private static Object object = new Object(); private String actNo; private double balance;
public AccountSolve() {}
public AccountSolve(String actNo, double balance) { this.actNo = actNo; this.balance = balance; }
public double getBalance() { return balance; }
public void setBalance(double balance) { this.balance = balance; }
public String getActNo() { return actNo; }
public void setActNo(String actNo) { this.actNo = actNo; }
public void withdraw(double money) { synchronized (this) { double before = this.balance; System.out.println(Thread.currentThread().getName() + "线程正在取款" + money + ",当前" + this.getActNo() + "账户余额" + before);
try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } this.setBalance(before - money); System.out.println(Thread.currentThread().getName() + "线程取款成功,当前" + this.getActNo() + "账户余额" + this.getBalance()); } }
}
|
这里act是锁,多个线程抢一把锁。
2.3 结论
线程同步机制通过使用对象锁来确保多线程环境下的数据安全。选择合适的锁对象并合理控制同步代码块的范围,可以有效地解决数据竞争问题,并尽量减少对程序性能的影响。
三、同步实例方法
在 Java 中,除了使用同步代码块外,还可以使用同步实例方法来实现线程同步。同步实例方法会自动使用方法所属对象(即调用方法的对象)作为锁对象,从而确保同一时间只有一个线程可以执行该实例方法。
3.1 同步实例方法的语法
在 Java 中,如果将 synchronized
关键字放在实例方法的声明上,就可以将该方法声明为同步方法。其语法如下:
1 2 3
| public synchronized void methodName() { }
|
3.2 示例代码
假设我们有一个银行账户类 BankAccount
,其中包含一个同步实例方法 withdraw
,用来模拟多个线程同时对同一个账户进行取款操作:
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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85
| package com.camellia.thread.ThreadSafe;
public class ThreadSafeSolve2 { public static void main(String[] args) { AccountSolve act = new AccountSolve("act-001", 10000); Thread t1 = new Thread(new WithdrawSolve(act)); Thread t2 = new Thread(new WithdrawSolve(act)); t1.start(); t2.start(); } }
class WithdrawSolve2 implements Runnable {
private AccountSolve act;
public WithdrawSolve2(AccountSolve act) { this.act = act; }
@Override public void run() { this.act.withdraw(1000); } }
class AccountSolve2 {
private static Object object = new Object(); private String actNo; private double balance;
public AccountSolve2() {}
public AccountSolve2(String actNo, double balance) { this.actNo = actNo; this.balance = balance; }
public double getBalance() { return balance; }
public void setBalance(double balance) { this.balance = balance; }
public String getActNo() { return actNo; }
public void setActNo(String actNo) { this.actNo = actNo; }
public synchronized void withdraw(double money) { double before = this.balance; System.out.println(Thread.currentThread().getName() + "线程正在取款" + money + ",当前" + this.getActNo() + "账户余额" + before);
try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } this.setBalance(before - money); System.out.println(Thread.currentThread().getName() + "线程取款成功,当前" + this.getActNo() + "账户余额" + this.getBalance()); } }
|
代码解析
withdraw
方法使用了 synchronized
关键字修饰,成为一个同步实例方法。
- 当线程调用
withdraw
方法时,会自动获取当前对象(account
)的锁对象,因此同一时间只有一个线程可以执行 withdraw
方法。
- 其他线程在执行
withdraw
方法时,如果此时有线程已经占有了 account
对象的锁,则必须等待锁被释放后才能继续执行。
3.3 使用同步实例方法的优势
- 简单性:不需要显式指定锁对象,直接使用方法所属对象作为锁。
- 可读性:代码更加清晰,易于理解。
- 一致性:同一对象的不同同步方法共享同一把锁,保证了对象内部状态的一致性。
3.4 注意事项
- 同步实例方法使用的是当前对象作为锁,因此在多线程环境中,需要确保同一对象的同步方法互斥执行,以防止竞态条件和数据不一致问题。
- 同步实例方法的锁粒度比较粗,如果需要更细粒度的控制,可以考虑使用同步代码块并指定特定的锁对象。
注意:若synchronized加在静态方法上就是对类加锁,仔细思考就是对类的静态变量加了锁。