线程同步
线程安全问题
-
线程安全
- 当多个线程访问某一个类(对象或方法)时,这个对象始终都能表现出正确的行为,那么这个类(对象或方法)就是线程安全的。 synchronized
- 可以在任意对象及方法上加锁,而加锁的这段代码称为”互斥区”或”临界区”
- 关键字synchronized取得的锁都是对象锁,而不是把一段代码(方法)当做锁,
- 所以代码中哪个线程先执行synchronized关键字的方法,哪个线程就持有该方法所属对象的锁(Lock),
- 在静态方法上加synchronized关键字,表示锁定.class类,类一级别的锁(独占.class类)。
同步即共享
异步即独立
线程安全需要滿足兩個特性
- 原子性(同步)
- 可見性
多线程情况下,当多个线程访问同一个数据时,很容易出现线程安全问题。
经典的问题——银行取钱问题。几个人同时取一个帐号里的钱,就可能出现问题。下面模拟一下。
//账户类
public class Account {
private String accountNo;
private double balance;
public Account() {
}
public Account(String accountNo, double balance) {
super();
this.accountNo = accountNo;
this.balance = balance;
}
public String getAccountNo() {
return accountNo;
}
public void setAccountNo(String accountNo) {
this.accountNo = accountNo;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
@Override
public int hashCode() {
return accountNo.hashCode();
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj != null && obj.getClass() == Account.class) {
Account target = (Account) obj;
return target.getAccountNo().equals(accountNo);
}
return false;
}
}
//模拟取钱线程
public class DrawThread extends Thread {
private Account account;
private double withDraw;
public DrawThread(String name,Account account, double withDraw) {
super(name);
this.account = account;
this.withDraw = withDraw;
}
@Override
public void run() {
//账户余额大于取钱数目
if(account.getBalance() >= withDraw){
System.err.println(getName() + "取钱成功!吐出:" +withDraw+" 文");
//让线程切换
try {
Thread.sleep(1);
} catch (Exception e) {
e.printStackTrace();
}
//减去取出的钱
account.setBalance(account.getBalance() - withDraw);
System.out.println("\t 余额为: " + account.getBalance());
} else {
System.out.println(getName() + "你的余额不足!");
}
}
}
//测试
public class DrawTest {
public static void main(String[] args) {
Account account = new Account("12345678",10000);
new DrawThread("A", account, 8000).start();
new DrawThread("B", account, 8000).start();
}
}
结果:
原因:
- run()线程执行体不具有同步安全性
- 程序中有两个并发线程在修改Account对象
同步监视器
为解决上述问题,Java多线程引入同步监视器。其目的:
阻止两个线程对同一个共享资源并发访问
通常,推荐使用可能被并发访问的共享资源充当同步监视器。
流程:
加锁->修改->释放锁
同步代码块
使用同步监视器的通用方法就是同步代码块。其含义是:线程开始执行同步代码块前,必须先获得对同步监视器的锁定。因此,修改线程类,使用同步代码块给Account对象上锁。
public class DrawThread extends Thread {
...
@Override
public void run() {
synchronized (account) {
// 账户余额大于取钱数目
if (account.getBalance() >= withDraw) {
System.err.println(getName() + "取钱成功!吐出:" + withDraw + " 文");
try {
Thread.sleep(1);
} catch (Exception e) {
e.printStackTrace();
}
// 减去取出的钱
account.setBalance(account.getBalance() - withDraw);
System.out.println("\t 余额为: " + account.getBalance());
} else {
System.out.println(getName() + "你的余额不足!");
}
}
}
...
同步方法
与同步代码块对应,还有同步方法。使用sychronized关键字修饰方法,无须显示指定同步监视器,同步方法的同步监视器就是this,就是调用该方法的对象。
那么,可以修改Account类,提供一个线程安全的draw方法
...
public synchronized void draw(double withDraw){
// 账户余额大于取钱数目
if (balance >= withDraw) {
System.err.println(Thread.currentThread().getName() + "取钱成功!吐出:" + withDraw + " 文");
try {
Thread.sleep(1);
} catch (Exception e) {
e.printStackTrace();
}
// 减去取出的钱
balance = balance - withDraw;
System.out.println("\t 余额为: " + balance);
} else {
System.out.println(Thread.currentThread().getName() + "你的余额不足!");
}
}
...
同步锁Lock
JDK 1.5开始,Java提供了更强大的线程同步机制
显式定义同步锁对象来实现同步
同步锁由Lock对象充当
与synchronized方法,synchronized代码块相比的优势:
更广泛的锁定操作
实现更灵活的的结构
可以具有差别很大的属性
支持多个相关的Condition对象
Lock是控制多个线程对共享资源访问的工具。锁提供了独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源前,须先获得Lock对象
JDK 1.5 提供了Lock,ReadWriteLock两个根接口,ReentrantLock可重入锁是Lock的实现类,ReentrantReadWriteLock是ReadWriteLock的实现类。
JDK 1.8 提供StampedLock类,大多数情况下,可替代传统的ReentrantReadWriteLock.
ReentrantReadWriteLock为读写操作提供3种锁模式:
Writing
ReadingOptimistic
Reading
下面以ReentrantReadWriteLock为例,修改Account类
public class Account {
private final ReentrantLock lock = new ReentrantLock();
...
public void draw(double withDraw){
// 加锁
lock.lock();
try {
// 账户余额大于取钱数目
if (balance >= withDraw) {
System.err.println(Thread.currentThread().getName() + "取钱成功!吐出:" + withDraw + " 文");
try {
Thread.sleep(1);
} catch (Exception e) {
e.printStackTrace();
}
// 减去取出的钱
balance = balance - withDraw;
System.out.println("\t 余额为: " + balance);
} else {
System.out.println(Thread.currentThread().getName() + "你的余额不足!");
}
} finally {
// 解锁
lock.unlock();
}
}
-
鎖
- 對象鎖
- 類鎖
字符串常量作為鎖的話,鎖不會被釋放