【条件竞争
在多线程的开发中,两个及其以上的线程需要共享统一数据的存取。如果两个线程存取相同的对象,并且每一个线程都调用一个修改该对象状态的方法,根据线程访问数据的顺序,可能会出现错误的数据结果,这种现象成为条件竞争。因为修改对象状态的方法并不是一个原子操作,通常步骤是:
1. 读取当前状态值到线程工作内存。
2. 在线程工作内存中修改状态值。
3. 将修改后的状态值重新写入主内存。
而问题往往就是有多个线程同时在执行步骤2。
【有两种机制代码受并发访问的干扰
- synchronized关键字。
- Reentrantlock类。
【Reentrantlock类
可重入的互斥锁,又被称为“独占锁”。Lock和synchronized机制的主要区别:
1. synchronized机制提供了对与每个对象相关的隐式监视器锁的访问, 并强制所有锁获取和释放均要出现在一个块结构中, 当获取了多个锁时,它们必须以相反的顺序释放。
2. synchronized机制对锁的释放是隐式的, 只要线程运行的代码超出了synchronized语句块范围, 锁就会被释放;而Lock机制必须显式的调用Lock对象的unlock()方法才能释放锁, 这为获取锁和释放锁不出现在同一个块结构中, 以及以更自由的顺序释放锁提供了可能。
public class LockObject {
private ReentrantLock lock = new ReentrantLock();
public void lockSet(int value){
lock.lock();
try{
System.out.println("lock writing : " + value);
// while (1==1){}
}finally {
lock.unlock();
}
}
public void unLock(){
System.out.println("unlock");
}
}
如果我们把while(1==1){}放开的话,可以看见只有一个线程能进入该方法中,说明锁有效。
【读写锁
不过有一个问题出现了,如果两个线程有写的操作,那么上锁是没有问题的。 但是如果都是读的操作那么还用不用上锁呢?应该不用了,因为锁是很消耗资源的,能不用就不用。基于这样的考虑,jdk提供了读写锁:
private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private Lock writeLock = readWriteLock.writeLock();
private Lock readLock = readWriteLock.readLock();
总结一些读写锁最重要的特点:
- 多个线程包含至少一个写操作:锁生效。
- 多个线程全部是读操作:锁不生效。
这样一来,我们就把读写分离开来,在系统查询是大部分操作,如果将读写分离出来,性能就会大大受益。 有人会问:读操作本来就不需要加锁啊,只在写操作上用锁就可以了。如果是这样的话,试想:共享对象正在执行写操作,这时一个读操作执行了, 但是读操作读出来的数据与执行完写操作的数据不一致,这就会影响使用读操作返回值的逻辑,这种现象就是:不可重复读! 是的,数据库中就使用了读写锁的思想,并且划分了事务的隔离级别。
public class LockObject2 {
private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private Lock writeLock = readWriteLock.writeLock();
private Lock readLock = readWriteLock.readLock();
//共享数据
private int shareValue = 100;
public void setLock(int value){
writeLock.lock();
try{
System.out.println("writing : " + value);
shareValue = value;
while (1==1){
}
}finally {
writeLock.unlock();
}
}
public void getLock(){
readLock.lock();
try{
System.out.println("getting : " + shareValue);
while (1==1){
}
}finally {
readLock.unlock();
}
}
}
【常用方法
- void lock():获得锁,如果锁同时被另一个线程持有则发生阻塞。
- void unlock():释放锁,必须在finally{}中。
【构造方法
- ReentrantLock():构建一个可以用来保护临界区的可重入锁对象。
- ReentrantLock(boolean fair):构建一个带有公平策略的锁。公平锁偏爱等待时间最长的线程,但是会大大降低性能,所以默认情况下,锁不公平。 貌似公平锁更加的合理,但是公平锁会比常规锁慢很多,只有当你确定自己要做什么并且对于你要解决的问题有一个特定的理由时,才使用公平锁。并且就算是公平锁也无法绝对的公平因为这和线程的调度器有关。
【条件对象
些时候,线程进入临界区之后,发现需要满足一定的条件才可以执行。要使用一个条件对象来管理那些已经获得锁但是不能工作的线程。我们使用银行转账的例子:
private ReentrantLock bankLock = new ReentrantLock();
public void lockTrans(int to ,int amount){
bankLock.lock();
try{
while (this.amount < amount){
//wait
}
}finally {
bankLock.unlock();
}
}
现在,账户没有足够金额的时候,只有等待另一个线程向账户中转账。但是这个线程已经获得了锁,其他线程无法执行转账操作啊。这就是为什么我们要使用条件对象的原因。 一个对象锁,可以有多个相关的条件对象。
private ReentrantLock bankLock = new ReentrantLock();
private Condition sufficientFunds = bankLock.newCondition();
public void lockTrans(int to ,int amount) throws InterruptedException {
bankLock.lock();
try{
while (this.amount < amount){
//wait
sufficientFunds.await();
}
}finally {
bankLock.unlock();
}
}
现在当前线程被阻塞了,并放弃了锁。另一个线程有机会执行转账操作,为当前账户打钱。等待获得锁的线程与调用了await()的线程有本质区别,一旦一个线程调用了await(),它就进入该条件的等待集,当锁可以使用时,它不能马上解锁。相反的,它处于阻塞状态,直到另一个线程调用同一条件的signalAll()。
signalAll():唤醒所有处于该条件中的等待线程,这些线程将重新竞争锁。
private ReentrantLock bankLock = new ReentrantLock();
private Condition sufficientFunds = bankLock.newCondition();
public void lockTrans(int to ,int amount) throws InterruptedException {
bankLock.lock();
try{
while (this.amount < amount){
//wait
sufficientFunds.await();
//处理转账逻辑
sufficientFunds.signalAll();
}
}finally {
bankLock.unlock();
}
}
【完整的转账代码如下
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Bank {
public static void main(String[] args) {
Bank b = new Bank(5,100);
Random random = new Random();
List<CompletableFuture> cfs = new ArrayList<>();
for (int i = 0; i < b.size(); i++) {
int from = i;
int to = (int) (b.size() * Math.random());
int max = 100;
int min = 10;
int amount = random.nextInt(max) % (max - min + 1) + min;
cfs.add(CompletableFuture.runAsync(() -> {
try {
b.transfer(from, to, amount);
} catch (InterruptedException e) {
e.printStackTrace();
}
})
);
}
CompletableFuture.allOf(cfs.toArray(new CompletableFuture[cfs.size()])).join();
}
private final int[] accounts;
private Lock bankLock;
private Condition sufficientFunds;
public Bank(int n,int initialBalance){
accounts = new int[n];
Arrays.fill(accounts,initialBalance);
bankLock = new ReentrantLock();
sufficientFunds = bankLock.newCondition();
}
//转账逻辑
public void transfer(int from,int to,int amount) throws InterruptedException {
bankLock.lock();
try {
while (accounts[from] < amount){
sufficientFunds.await();
}
System.out.println(Thread.currentThread());
accounts[from] -= amount;
System.out.println("---------------------------------------");
System.out.println(String.format("%d from %d to %d",amount,from,to));
accounts[to] += amount;
System.out.println(String.format("Total balance:%d",getTotalBalance()));
System.out.println("Detail:");
for (int i = 0; i < accounts.length; i++) {
System.out.println(String.format("account[%d]'s amount is : %d",i,accounts[i]));
}
System.out.println("---------------------------------------");
sufficientFunds.signalAll();
}finally {
bankLock.unlock();
}
}
public int getTotalBalance(){
bankLock.lock();
try{
int sum = 0;
for(int a : accounts){
sum += a;
}
return sum;
}finally {
bankLock.unlock();
}
}
public int size(){
return accounts.length;
}
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。