本文主要是介绍JavaConcurrency- Synchronization(同步),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
竞争条件(race condition)
在大多数实际的多线程应用中,两个or两个以上的线程需要共享对同一数据的存取。如果两个线程存取相同的对象,并且每一个线程都调用了一个修改该对象的方法,线程之间彼此踩了对方的脚,根据各县城访问数据的次序,可能会产生错误的对象。这种情况就是竞争条件(race condition)
account[to]+=account;
问题在于这不是原子操作。该指令可能被处理如下:
1)将accounts[to] 加载到寄存器
2)增加amount
3)将结果写回accounts[to]
现在假定 第一个线程 执行步骤1 和步骤2 然后,它被剥夺了运行权。假定第二个线程被唤醒并修改了accounts数组中的同一项。然后 第一个点成被唤醒并完成第三步
这一操作擦去了第二个线程所做的更新。于是金额不在更新。
如何避免并发干扰:
有两种机制防止代码块收到并发访问干扰
1 synchronized关键字
2 ReentrantLock类
Synchronized 关键字自动提供了一个锁以及相关的“条件”,对于大多数需要显式锁的情况,是很便利的。
ReentrantLock代码块基本结构:
myLock.lock(); // a ReentrantLock object
try{critical section
}
finally{mylock.unlock()// make sure the lock is unlocked even if an exception is thrown
}
这个结构确保任何时刻只有一个线程进入临界区。一旦一个线程封锁了锁对象,其他任何线程都无法通过lock语句。当其他线程调用lock时,它们被阻塞,直到第一个线程释放锁对象。
把解锁操作扩在finally子句之内饰至关重要的。如果在临界区的代码抛出异常,锁必须被释放。否则,其他线程将永远阻塞。
非同步示意图:
{——————————————————————————}线程1{——————}线程2
同步示意图:
{——————}线程1{——————}线程2
每个对象都有自己的ReentrantLock对象。如果两个线程试图访问同一个对象,那么锁以串行的方式提供服务。
但是,如果两个线程访问不同的对象,每个线程得到不同的锁对象,两个线程都不会发生阻塞。
本该如此,因为线程在操纵不同的实例的时候,线程之间不会相互影响。
锁是可重入的,因为线程可以重复地获得已经持有的锁。锁保持一个持有计数(hold count)来跟踪lock方法的潜逃调用。线程在每一个调用lock都要调用unlock来释放锁。由于这一特性,被一个锁保护的代码可以调用另一个使用相同锁的方法。
例如:
transfer方法调用getTotalBalance方法,这是也会封锁bankLock对象,此时bankLock对象的计数为2.当getTotalBalance方法退出的时候,持有计数变回1。当transfer方法退出的时候,持有计数变为0。线程释放锁。
注意: 临界区中的代码,不要因为一场的抛出而跳出临界区。如果在临界区代码结束之前抛出了异常,finally子句将释放锁,但会使得对象可能处于一种受损状态。
ReentrantLock()// 构建一个可以被用来保护临界区的可重入锁
ReentrantLock(boolean fair)// 构建一个带有公平策略的锁,一个公平锁偏爱等待时间最长的线程。但是,这一公平的保证将大大降低性能。
默认情况下,锁没有被强制为公平的。
条件对象:
通常,线程进入临界区,却发现某一条件满足之后它才能执行。要使用一个条件对象来管理那些已经获得了一个锁但是却不能做有用工作的线程。(条件对象经常被称为条件变量(conditional variable)) 条件对象和锁一起使用
现在来细化银行模拟程序:
if(bank.getBalance(from)>= amount){
//thread might be deactivated at this pointbnak.transfer(from,to,amount);
}
在调用transfer方法之前有可能被中断,在线程再次运行前,账户余额可能已经低于提余额。必须要保证没有其他线程在本检查余额与转账活动之间修改余额。通过使用锁来保护检查 与转账动作来做到这一点:
public void transfer(int from, int to, int amount){bankLock.lock();try{while(accounts[from]<amount){//wait}//transfer funds}finally{bankLock.unlock();}
}
当账户中没有足够的余额时,线程等待直到另一个线程向账户中注入资金。但是 线程刚刚获得了对bankLock的排他性访问,因此别的线程没有进行存款操作的机会。这就是为什么我们需要条件对象的原因
一个锁对象可以有一个or多个相关的条件对象。可以用newCondition方法获得一个条件对象。习惯上给每一个条件对象命名为可以反映它所表述的条件的名字。
例如:在此设置一个条件对象来表达“余额充足”条件
class Bank
{private Condition sufficientFunds;......public Bank(){....sufficientFunds=bankLock.newCondition();// 在lock对象的基础上new出来条件对象}
}
如果transfer方法发现余额不足,它调用
sufficientFunds.await();
当前线程现在被阻塞了,并放弃了锁。我们希望这样可以使得另一个线程可以进行增加账户余额的操作。
等待获得锁的线程和调用await方法的线程存在本质上的不同。一旦一个线程调用await方法,它进入该条件的等待集。当锁可用时,该线程不能马上解除阻塞。相反,它处于阻塞状态,直到另一个线程调用同一个条件上signalAll方法为之。
当另一个线程转账时,它应该调用:
sufficientFunds.signalAll();
这一调用重新激活因为这一条件而等待的所有线程。当这些线程从等待集当中移出时,他们再次成为可运行的,调度器再次激活他们。
同时,他们将试图重新进入该对象。一旦锁成为可用的,他们中的某个将从await调用返回,获得该锁并从被阻塞的地方继续执行。
sigalAll方法仅仅是通知正在等待的线程,无法确保该条件被满足,此时有可能已经满足条件, 等待线程应该再次测试该条件。
while(!(ok to proceed))condition.await();
至关重要的是,最终要某个其他线程调用signalAll方法。当一个线程调用await时,它没有办法重新激活自身。它寄希望于其他线程。如果没有其他线程来重新激活等待的线程,它就永远不再运行了。这将导致令人不快的死锁(deadlock)现象。如果所有其他线程被阻塞,最后一个活动线程在解除其他线程的阻塞状态之前就调用await方法。那么它也被阻塞。 没有任何线程可以解除其他线程的阻塞,那么该程序就挂起了。
经验上讲:在对象的状态有利于等待线程的方向改变时调用signalAll。
例如,当一个账户余额发生改变时,等待的此案称会应该有机会检查余额。在之前的例子中,当完成了转账时,调用了signalAll方法。
注意调用signalAll不会立即激活一个等待线程。它仅仅解除等待线程的阻塞,以便这些线程可以在当前线程退出同步方法之后,通过竞争实现对 对象的访问。
另一个方法signal,则是随机解除等待集中某个线程的阻塞状态。这比解除所有线程的阻塞更加有效,但也存在危险。如果随机选择的线程发现自己仍然不能运行。那么它再次被阻塞,如果没有其他线程再次调用signal,那么系统就死锁了。
public class Bank{private final double[] accounts;private Lock bankLock;private Condition sufficientFunds;public Bank(int n, double initialBalance){accounts=new double[n];Arrays.fill(accounts.initialBalance);bankLock=new ReentrantLock(0;sufficientFunds=bankLock.newCondition();}public void transfer(int from , int to, double amount) throws InterruptException{bankLock.lock();try{while(account[from]<amount)sufficientFunds.await();//等待条件System.out.print(Thread.currentThread());accounts[from]-=amount;System.out.printf(amount + from + to);accounts[to]+=amount;System.out.printf(Total Balane: getTotalBalance());sufficientFunds.signalAll();}finally{bankLock.unlock();}}public double getTotalBalance(){bankLock.lock();try{double sum=0;for(double a :accounts)sum+=a;return sum;}finally{bnakLock.unlock();}}public int size(){return accounts.length;}
}
总结一下有关锁和条件的关键之处:
- 锁用来保护代码片段,任何时候只能有一个线程执行被保护的代码
- 锁可以用来管理试图进入被保护代码端的线程
- 锁可以拥有一个or多个相关的条件对象
- 每个条件对象管理那些已经进入被保护的代码段但还不能运行的线程
Lock和Condition接口为程序设计人员提供了高度的锁定控制。
Synchronized关键字:
Java中每一个对象都有一个内部锁。如果一个方法用synchronized关键字来声明,那么对象的锁将保护整个方法。调用该方法,线程必须获得对象内部的对象锁。
public synchronized void method(){//method body
}
等价于
public void method(){this.intrinsicLock.lock();try{// method.body}finally{tis.intrinsicLock.unlock();}
}
内部对象锁只有一个相关条件。wait方法添加一个线程到等待集中,notifyAll/notify方法解除等待线程的阻塞状态。换句话说:
调用wait 或者 notifyAll等价于 intrinsicCondition.await(); intrinsicCondition.signalAll();
class Bank{private double[] accounts;public synchronized void transfer(int from, int to, int amount) throws InterruptedException{while(account[from]<amount)wait();//wait on intrinsic object lock's single conditionaccount[from]-=amount;account[to]+=amount;notifyAll();//notify all threads waiting on the condition}public synchronized double getTotalBalance(){......}
}
wait,notifyAll 以及notify方法是Object类的final方法。Condition方法必须被命名为await,singleAll 和signal以便他们不会与那些方法发生冲突。
内部锁和条件存在一些局限:
- 不能中断一个正在试图获得锁的线程
- 视图获得锁时不能设定超时
- 每个锁仅有单一的条件,可能是不够的
代码中应该使用哪一种?
最好两种都不使用,许多情况下你可以使用java.util.concurrent包中的一种机制,它会为你处理所有的加锁。
如果synchronized关键字适合你的程序,尽量使用它,这样可以减少编写代码的数量,减少出错的几率。
//java.lang.Object 类的方法
void notifyAll();// 解除那些在该对象上调用wait方法的线程阻塞状态。该方法只能在同步方法or同步代码块内部调用,
//如果当前线程不是对象锁的持有者,该方法会抛出一个IllegalMonitorStateException异常
void notify();//随机选择一个在该对象上调用wait方法的线程,解除其阻塞状态。该方法只能在一个同步方法or同步代码块内部调用,
//如果当前线程不是对象锁的持有者,该方法会抛出一个IllegalMonitorStateException异常
void wait();//导致线程进入等待状态直到它被通知。该方法只能在一个同步方法or同步代码块内部调用,
//如果当前线程不是对象锁的持有者,该方法会抛出一个IllegalMonitorStateException异常
void wait(long millis);//
void wait(long mills , int nanos);//导致线程进入等待状态直到它被通知or经过指定的时间。这些方法只能在同一个方法中调用。
//如果当前线程不是对象锁的持有者,该方法会抛出一个IllegalMonitorStateException异常
synchronized 其他用法 同步阻塞,看其他博文。
Volatile域:
仅仅为了读写一个or连个实例就使用同步,显得开销过大了,
但是
多处理器的计算机能够暂时在寄存器or本地内存缓存去中保存内存中的值。
结果是运行在不同处理器上的线程可能在同一个内存位置取到不同的值编译器可以改变指令执行的顺序以使得吞吐量最大化。这种顺序上的变化不会改变代码的语义,但是编译器假定内存的值仅仅在代码中有显试的修改指令时才会改变。然而内存的值可以被另外一个线程改变。
volatile关键字为实例域的同步访问提供了一种免锁机制。如果声明一个域为voatile,那么编译器和虚拟机就知道该域是可能被另外一个线程并发更新的。
public boolean done;
public sychronized boolean isDone(){ return done;}
public synchronized void setDone(){done = true;};
可以这样写代码,或许使用内部锁不是个好主意。如果另一个线程已经对该对象加锁,isDone和setDone方法可能阻塞。
private volatile boolean done;
public boolean isDone(){ return done;}
public void setDone(){done = true;};
这样写代码最好,但是需要注意:
Volatile变量不能提供原子性。如下操作会发生问题:
public void flipDone(){ donw= !done;}// not atmoic
不能确保翻转域中的值。不能保证读取,翻转和写入不被中断。
原子性:
假设对共享变量除了赋值之外并不完成其他操作,那么可以将这些共享变量声明为volatile
java.util.concurrent.atomic包中有很多类使用了很高效的机器指令来保证其他操作的原子性。
比如 AtomicInteger类提供了方法incrementAndGet和decrementAndGet。他们分别以原子方式将一个整数自增or自减。
public static AtomicLong nextNumber = new AtomicLong();
// in some thread....
long id =nextNumber.incrementAndGet();
incrementAndGet方法以原子方式将AtomicLong自增,并返回自增后的值。也就是说活的值,增1并设置然后生成新的值得操作不会被中断。可以保证即使是多个线程并发的访问同一个实例,也会计算并返回正确的值。
如果希望完成更加复杂的更新,就必须使用compareAndSet方法:
public static AtomicLong largest = new AtomicLong();
//在某些线程中
largest.set(Math.max(!largest.get(),observed));//错误--竞争条件
上面这段代码是错误的,理由是这个更新过程不是原子性的。不是原子性,也就意味着在多线程并发的情况下,有可能这个变量会被修改。对于上面那个操作,并不能说要么一次就执行成功,或者要么就不执行。
下面是对上面那段代码的修改:
do {oldValue = largest.get();newValue = Math.max(oldValue, observed);} while (!largest.compareAndSet(oldValue, newValue));//largest oldValue 换成newValue
在这个循环条件下,假设有多个线程在同时执行这段代码,即使有一个线程在产生了新值之后,它还需要让它自己产生的新值与旧值比较之后才能决定要不要这个新值。也就是说,如果oldValue是10,而产生的新值是20,然后程序还没到while的时候,有其他线程修改了newValue值,并走完while操作,更新了largest,那当这个线程到while判断的时候,会出现comparAndSet方法larget的预期值(10)不跟实际值(另一个线程更新后的largest)一样,导致方法返回false,直到没有其他线程干扰。这时就确定了新产生的值。(简而言之,如果另一个线程也在更新largest,就阻止当前线程更新。返回false,循环会多次尝试,读取更新后的值,并尝试修改。最终,它会成功地使用心智来替换原来的值。)
另外,这Java 8中,可以直接通过与lambda表达式的配合达到此目的:
largest.updateAndGet(x->Math.max(x,observed));largest.accumulateAndGet(observed, Math::max);
accumulateAndGet 方法利用一个二元操作符来合并原子值和所提供的参数。
还有getAndUpdate和getAndAccumulate方法可以返回原值
类 AtomicInteger,AtomicIntegerArray,AtomicIntegerFieldUpdater,AtomicLongArray,AtomicLongFieldUpdater,AtomicReference,AtomicReferenceArray 和AtomicReferenceFieldUpdater也提供了这些方法。
如果有大量线程要访问相同的原子值,性能会大幅下降,因为乐观更新需要太多次重试。Java8提供了LongAdder和LongAccumulator类来解决这个问题。LongAdder包括多个变量(加数),其总和为当前值。可以有多个线程更新不同的加数,线程个数增加时会自动提供新的加数。通常情况下,只有当所有的工作都完成之后才需要总和的值。
如果认为可能存在大量竞争,只需要使用LongAdder而不是AtomicLong。
final LongAdder adder=new LongAdder();
for(...){pool.submit(()->{ while(...){...if(...) adder.increment(); }});
}
....
long total=adder.sum();
还可以这么做
LongAccumulator adder=new LongAccumulator(Long:: sum, 0);
// in some thread...
adder.accumlate(value);
DoubleAdder 和DoubleAccumulator也采用同样的方式,只不过处理的是double值。
死锁:
有可能会因为每一个线程要等待更多的钱存款存入而导致所有线程都被阻塞。这样的状态被称为死锁(deadLock)。
Java中没有任何东西可以避免or打破这种死锁现象。必须仔细设计程序,以确保不会出现死锁。
线程局部变量:
在线程之间共享变量的风险。有时有可能要避免共享变量。使用ThreadLocal辅助类为各个线程提供各自的实例。
public static final SimpleDateFormat dateFormat=new SimpleDateFormat("yyyy-MM-dd");
//如果连个线程都要执行以下操作
String dateStamp=dataFormat.format(new Date());
结果就可能操作混乱,因为dateFormat使用的内部数据结构可能会被并发的访问所破环。当然可以使用同步,但开销很大
public static final ThreadLocal<SimpleDateFormat> dataFormat=ThreadLocal.withInitial(()->new SimpleDateFormat("yyyy-MM-dd"));
// 要访问具体的格式化方法,可以调用
String dateStamp=dateFormat.get().format(new Date());
在一个给定线程中首次调用get时,会调用initialValue方法。在此之后,get方法会返回属于当前线程的那个实例。
在多个线程中生成随机数也存在类似的问题。java.util.Random类是线程安全的。但是如果多个线程需要等待一个共享的随机数生成器,这会效率很低
可以使用ThreadLocal辅助类为各个线程提供一个单独的生成器,不过Java7 还另外提供了一个便利类。
int random= ThreadLocalRandom.current().nextInt(upperBound);
锁测试与超时:
线程在调用lock方法来货的另外一个线程持有的锁的时候,很有可能发生阻塞,应该更加谨慎低申请锁。
tryLock方法试图申请一个锁,在成功获得锁后返回true 否则,立即返回false,而且线程可以立即离开去做其他事情。
if(myLocl.tryLock()){//now the thread owns the locktry{.....}finally(myLock.unlock();)
}else{....
}
也可以调用tryLock时,使用超时参数
if(myLock.tryLock(100,TimeUnit.MILLISECONDS))
lock方法不能被中断。如果一个线程在等待获得一个锁时被中断,由于中断线程在获得锁之前一直处于阻塞状态(未激活状态)。如果出现死锁,那么 ,lock方法就无法终止。
然后,如果调用带有用超时参数的tryLock,那么如果线程在等待期间被中断,将抛出InterruptedException异常。允许程序打破死锁
也可以调用lockInterruptibly方法。相当于一个超时设置为无限的tryLock方法。
在等待一个条件时,也可以提供一个超时
myCondition.await(100,.TimeUNit.MILLISECONDS)
如果一个线程被另一个线程通过调用signalAll 或者signal激活,或者超时时限已到达,or线程被中断,那么await方法将返回。
如果等待的线程被中断,await方法将抛出一个InterruptedException异常。
java.util.concurrent.locks.Lock
boolean tryLock()//尝试获得锁而没有发生阻塞,如果成功返回真。这个方法会抢夺可用额锁,即使该锁有公平加锁策略,即使其他线程已经等待很久
boolean tryLock(long time,TimeUnit unit)//尝试获得加锁策略,阻塞时间不会超过给定的值,如果成功返回true
void lockInterruptibly()//获得锁,但是会不确定地发生阻塞。如果线程被中断,抛出一个InterruptedException异常java.util.concurrent.locks.Condition
boolean await(long time, TimeUnit unit)//进入该条件的等待集,知道线程从等待集中移除 or等待了指定时间之后才解除阻塞。
//如果因为等待时间到了而返回就返回false,否则返回true
void awatiUninterruptibly()//进入该条件的等待集,知道线程从等待集移出彩解除阻塞。如果线程被中断,该方法不会抛出InterruptedException异常。
读/写锁
java.util.concuttent.locks 包定义了两个锁类,
- ReentrantLock(充入锁)
- ReentrantReadWriteLock(读写锁)// 如果很多鞋厂从一个数据结构读取数据,而很少的线程修改其中的数据的话,很好用
允许读者线程共享访问是合适的。但是,写者线程依然必须是互斥访问的。
使用读写锁的必要步骤:
- 构造一个ReentrantReadWriteLock对象
private ReentrantReadWriteLock rwl =new ReentrantReadWriteLock();
- 抽取读锁和写锁
private Lock readLock=rwl.readLock();
private Lock writeLock=rwl.writeLock();
- 对所有的获取方法加读锁
public double getTotalBalance(){readLock.lock();try{.....}finally{readLock.unlock();}
}
- 对所有的修改方法加写锁
public void transfer(){writeLock.lock();try{...}finally{writeLock.unlock();}
}
总结:
java.util.concurrent.locks.ReentrantReadWriteLock
Lock.readLock()//得到一个可以被多个读操作共用的读锁,但会排斥所有写操作。
Lock writeLock()//得到一个写锁,排斥所有其他的读操作和写操作。
ReentrantReadWriteLock允许多个读线程同时访问,但不许写线程和读线程,写线程和写线程同事访问。
ReentrantReadWriteLock支持以下功能:
1)支持公平和非公平的获取锁的方式;
2)支持可重入。读线程在获取了读锁后还可以获取读锁;写线程在获取了写锁之后既可以再次获取写锁又可以获取读锁;
3)还允许从写入锁降级为读取锁,其实现方式是:先获取写入锁,然后获取读取锁,最后释放写入锁。但是,从读取锁升级到写入锁是不允许的;
4)读取锁和写入锁都支持锁获取期间的中断;
5)Condition支持。仅写入锁提供了一个 Conditon 实现;读取锁不支持 Conditon ,readLock().newCondition() 会抛出 UnsupportedOperationException
这篇关于JavaConcurrency- Synchronization(同步)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!