线程同步
- 线程安全问题
下面的代码是线程1执行了取钱操作,但是还没来记及修改余额时,切换给线程2取钱,出现错误(也可能看不到错误的结果,可把/**/放开,就一定出现错误结果)
public class DrawThread extends Thread {
private Account account;
private double drawAmount;// 取款金额
public DrawThread(String name,Account account,double drawAmount){
super(name);
this.account=account;
this.drawAmount=drawAmount;
}
@Override
public void run() {
if(account.getBalance()>=drawAmount){
System.out.println("取钱成功,吐出钞票!");
/*
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
*/
account.setBalance(account.getBalance()-drawAmount);
System.out.println("\t余额为: "+account.getBalance());
}else{
System.out.println(getName()+"取钱失败!余额不足!");
}
}
public static void main(String[] args) {
Account ac = new Account("1",100);
new DrawThread("线程1",ac,100).start();
new DrawThread("线程2",ac,100).start();
/*
取钱成功,吐出钞票!
取钱成功,吐出钞票!
余额为: 0.0
余额为: -100.0
*/
}
}
- 同步代码块
1.为了解决线程同步,Java的多线程引入了同步监视器来解决这个问题,使用同步监视器的通用方法就是同步代码块,语法如下:
synchronized(obj){
//...
//此处的代码就是同步代码块
}
@Override
public void run() {
// 使用account作为同步监视器,任何线程进入下面同步代码块之前
// 必须先获得account账户的锁定--其他线程无法获得锁,也就无法修改它
// 这种做法符合 加锁--修改--释放锁的逻辑
synchronized (account){
if(account.getBalance()>=drawAmount){
System.out.println("取钱成功,吐出钞票!");
// try {
// Thread.sleep(1);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
account.setBalance(account.getBalance()-drawAmount);
System.out.println("\t余额为: "+account.getBalance());
}else{
System.out.println(getName()+"取钱失败!余额不足!");
}
}
}
2.共享资源的代码区,被称为临界区
- 同步方法
1.Java的多线程安全支持还提供了同步方法,使用synchronized关键字来修饰某个方法,该方法被称为同步方法,同步方法的同步监视器是this,也就是调用该方法的对象
2.上面取钱的例子中,可以考虑把Account类中的setBalance方法去掉,增加一个同步方法,如下
// 注意synchronized修饰
public synchronized void draw(double drawAmount){
if(account.getBalance()>=drawAmount){
// ...
}else{
// ...
}
- 释放同步监视器的锁定
任何线程进入同步方法、同步代码块之前,必须先获得对同步监视器的锁定,那么何时会释放同步监视器的锁定呢?
- 线程执行同步代码块或同步方法时,执行结束
- …,遇到return break
- …,遇到未处理的Error Exception
- …,程序执行了同步监视器对象的wait()方法,则当前线程暂停,并释放同步监视器
如下情况下,线程不会释放同步监视器:
1. …,程序调用Thread.sleep(),Thread.yield()方法来暂停当前线程的执行,当前线程不会释放同步监视器。
2. …,其他线程调用了该线程的suspend()方法将该线程挂起。当然,应尽量避免使用suspend()和resume()方法来控制线程。
* 同步锁(Lock)
1. Lock接口,实现类是ReentrantLock(可重入锁)
2. ReadWriteLock(读写锁)接口,实现类是ReentrantReadWriteLock,该实现类在java8中大多数场景下可被StampedLock替代。
3. 实现线程安全的控制中,比较常用的是ReentrantLock,代码格式如下:
4.同步锁和同步方法、同步代码块的比较:虽然同步方法和同步代码块的范围机制使得多线程安全变成非常方便,而且还可以避免很多设计锁的常见变成错误,但有时也需要以更灵活的方式使用锁。Lock提供了同步方法和同步代码块所没有的其他功能。
class x{
// 定义锁对象
private final ReentrantLock lock = new ReentrantLock();
// 定义需要保证线程安全的方法
public void m(){
lock.lock();
try{
// 需要保证线程安全的代码
// ...method body
}finally {
lock.unlock();
}
}
}
- 死锁
1.当两个线程互相等待对方释放同步监视器时就会发生死锁,Java虚拟机没有检测,也没有采取措施来处理死锁情况。一旦出现,整个程序不会发出任何异常,也不会给提示,只是所有的线程处于阻塞状态,无法继续。
- 由于Thread类的suspend方法很容易导致死锁,所以Java不推荐使用该方法来暂停线程的执行
线程通信
- 传统的线程通信
- 使用Condition控制线程通信
- 使用阻塞队列(BlockingQueue)控制线程通信
线程组和未处理的异常
线程池
- Java8改进的线程池
- Java8增强的ForkJoinPool
线程相关类
- ThreadLocal类
- 包装线程不安全的集合
- 线程安全的集合类