目录
一、为什么需要线程同步
我们知道,线程的使用可以适用于大量并发的场景,比如购买车票这样的场景,当多个线程需要共享对同一数据的存储,比如车的座位数,如果多个窗口卖票,相当于多个线程对这辆车的剩余座位数做修改,卖票或退票。这样将会导致多个线程相互覆盖,最终实际剩余座位数和记录的座位数不相符。这种情况就是“竞态条件”。
二、为什么会出现竞态条件
出现“竞争条件”现象的原因是由于我们的操作不熟原子性的,比如对剩余座位数count的处理如下:
count = count - 1;
这行代码执行需要做下面三步:
1、将count加载到寄存器;
2、减去1;
3、将结果写回到count。
假如现在剩余座位数为10个,线程一操作减1,执行步骤1、2后,它的运行权被强占交给给了线程二,线程二减1,执行步骤1、2、3。这个时候剩余座位数就是9。这个时候线程一被唤醒了,线程一接着执行步骤三,最后写回到的结果还是9。那线程二减去的一张票相当于被抹除掉了。最后是不是就会出现拿着车票的人去坐车,发现车票总数大于车的总座位数。
三、锁对象
有两种机制可以防止并发访问代码块:ReentrantLock和Synchronized。
1、ReentrantLock-重入锁
用法如下
public class Car {
private Condition condition;
private ReentrantLock reentrantLock;
public Car() {
reentrantLock = new ReentrantLock();//构造重入锁
condition = reentrantLock.newCondition();//返回一个与这个锁相关联的条件对象
}
public void calculate(int remain, int buy) {
condition = reentrantLock.newCondition();
reentrantLock.lock();//获得锁,如果锁当前被其他线程占用,则阻塞
try {
while(remain < buy) {//如果不满足当前操作条件,比如购买数大于剩余数
condition.await();//将该线程放入这个条件的等待集中
}
remain -= buy;
condition.signalAll();//解除该条件等待集中的所有线程的阻塞状态
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
reentrantLock.unlock();/释放锁,unlock必须放在finally中,也就是说最后锁必须释放,不然其他线程会永远阻塞。
}
}
}
ReentrantLock能够保证只有一个线程进入临界区,一旦有一个线程锁定了锁对象,其他线程都无法通过loak语句,当其他线程调用lock时,他们会暂停,直到第一个线程释放这个锁对象。有时即使这个线程成功进入临界区,却发现线程需进行的操作依然无法执行,比如,并不满足执行的条件,这个时候我们可以通过一个条件对象来管理这个线程reentrantLock.newCondition()。在当前线程不满足执行的条件下,调用condition.await()进入这个条件的等待集中,线程暂停,并放弃锁,有其他线程进行操作,直到其他线程调用condition.signalAll()激活这个条件的等待集中的所有线程,一旦锁可用,这些线程会重新竞争这个锁,等到锁后,会从之前暂停的地方继续执行。
调用signalAll来唤醒所有等待集中的所有线程能有效的避免死锁,死锁的原因是所有的线程都被阻塞,程序永远挂起。使用signalAll只是解锁等待线程的阻塞,给他们在当前线程释放锁后竞争访问对象的机会。
还有一种解除等待状态的方法condition.signal(),signal只能随机解除等待集中一个线程的阻塞状态,这可定要比signalAll更更高效,但也会更容易存在被解除阻塞状态的线程仍旧无法满足条件,重新进入等待集中,如果没有其他线程再来调用signal,系统就会进入死锁。
2、synchronized
synchronized是Java的一个内部锁,从1.0版本开始,Java中的每个对象都有一个内部锁,如果一个方法声明时有synchronized关键字,那么这个对象的锁就会抱回整个方法。synchronized内部锁只有一个关联条件,wait方法将一个线程增加到等待集中,notify()和notifyAll()可以解除等待线程的阻塞。使用方法如下:
public synchronized void calculate(int remain, int buy) throws InterruptedException {
while(remain < buy) {
wait();
}
remain -= buy;
notifyAll();
}
此外,还有一种机制可以获得锁,就是同步块,用法如下:
private Object lock = new Object();
public void calculate(int remain, int buy) {
synchronized (lock) {
remain -= buy;
}
}
四、volatile
volatile关键字为实例字段的同步访问提供了一种免锁机制。用法如下:
private volatile boolean isChecked;
在单例模式中双重校验锁实现方式使用的就是volatile和synchronized,如下:
public class Singleton {
private volatile static Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
public void showMessage() {
System.out.println("......!!!!");
}
}