一、锁对象与条件对象
1.锁对象(Lock):锁对象可以保证任一时刻只有一个线程进入临界区。一旦某个线程获得了锁对象,其他所有线程都无法从lock方法返回,进入阻塞状态,直到拥有锁对象的线程释放锁。
2.条件对象(Condition):条件对象用来管理已经获得了锁(进入临界区)但因逻辑条件无法满足而不能继续工作的线程集。
3.用法:
lock.lock();
try{
while(!(ok to proceed))
condition.await();
...
condition.signalAll();
}finally{
lock.unlock();
}
4.说明:
-
锁对象可以拥有一个或多个条件对象。每个条件对象管理一个独立的线程集。
-
当某个线程在condition上调用await方法时,它将放弃已获得的锁,进入等待状态,成为condition等待集上的一员。直到发生下面三个条件才有可能被唤醒。
-
a. 当某个线程在condition上调用signalAll方法时,condition等待集上的所有线程将解除等待状态,并试图重新进入锁,一旦某个线程获得了锁,该获得锁的线程将从await方法返回。
-
b. 当某个线程在condition上调用signal方法时,将随机在condition等待集上选择一个线程解除等待状态。此方法使用有风险,因为一旦被选择线程发现自己仍不满足继续工作的条件,再次进入await方法,将不再有线程将其从等待状态唤醒,所有线程进入死锁状态。
-
c. 当其他线程中断某个处于await方法调用中的(等待状态)线程时,该线程将结束等待状态。
-
如果整个程序只有await,没有signal或signalAll,程序将进入死锁。
-
unlock方法一定要在finally块中被调用
-
锁测试与超时:线程在调用lock方法获取锁时,很可能发生堵塞。可以使用tryLock方法申请锁,如果成功则获得锁返回true,如果失败,则立即返回false,该线程可以立即离开去做处理其他事情。tryLock方法还可以传一个超时参数,超过这个时间未获得锁则返回false。
三、synchronized、wait、notify/notifyAll 与对象锁
1. 对象锁:java中每一个对象都有一个内部锁(monitor/监视器),可以用关键字synchronized获取对象的内部锁
2. synchronized的用法
-
public synchronized void method(){...}
-
synchronized(obj){...}
3. wait、notify/notifyAll方法:在某个对象上调用wait方法的线程必须拥有此对象的监视器。wait方法被调用后,此线程释放该对象的监视器,并在此对象监视器上等待。直到其他线程在此对象上调用notify或notifyAll方法唤醒在此对象上等待的线程,该线程才有机会结束等待状态,继续执行。
synchronized (obj){
while ()
obj.wait();
... // Perform action appropriate to condition
obj.notifyAll();
}
4.说明:
-
内部锁机制(synchronized机制)相比“锁对象+条件对象“有以下劣势: (1)试图获得内部锁时不能设置超时 (2)每个内部锁只能有单一条件,可能是不够的 (3)不能中断一个正在试图获得内部锁的线程
-
Thread.sleep(...)与Object.wati()的主要区别:sleep方法没有释放监视器,当在synchronized代码块中调用sleep,该对象的监视器仍被该线程所拥有。而wait会释放监视器。
5.关于同步方式的选择:
-
最好既不用synchronized机制也不用"锁对象+条件对象"机制,而是尽量采用java.util.concurrent包下提供的其他机制来处理加锁,如阻塞队列机制。
-
只有特别需要Lock+Condition提供的特性时,才使用该机制。否则,尽量使用synchronized机制。
四、volatile关键字
volatile修饰的成员变量在每次被线程访问时,都强迫从共享内存中重读该成员变量的值。而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存。这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。
Java语言规范中指出:为了获得最佳速度,允许线程保存共享成员变量的私有拷贝,而且只当线程进入或者离开同步代码块时才与共享成员变量的原始值对比。这样当多个线程同时与某个对象交互时,就必须要注意到要让线程及时的得到共享成员变量的变化。而volatile关键字就是提示VM:对于这个成员变量不能保存它的私有拷贝,而应直接与共享成员变量交互。
注意:volatile关键字用于声明简单类型变量,如int、float、boolean等数据类型。对volatile变量的操作不是原子级别的,换言之,如果对某个volatile变量的操作结果依赖于其之前的值,如 n+=1 ,则此操作无法保证原子性。
使用建议:在两个或者更多的线程访问的成员变量上使用volatile。当要访问的变量已在synchronized代码块中,或者为常量时,不必使用。由于使用volatile屏蔽掉了VM中必要的代码优化,所以在效率上比较低,因此一定在必要时才使用此关键字。
五、读写锁ReentrantReadWriteLock
适用场合:多个线程从共享的数据结构中读取数据,较少线程对数据进行修改。
-
读锁:由lock.readLock()获得,可以被多个读操作共用,但会排斥所有写操作。即如果有一个线程获得了读锁,则所有尝试获取写锁的线程阻塞。
-
写锁:由lock.writeLock()获得,排斥所有其他的读操作和写操作。即如果有一个线程获得了写锁,则所有尝试获得写锁和读锁的线程阻塞。
六、阻塞队列BlockingQueue
使用阻塞队列,生产者线程向阻塞队列插入数据,当队列满时,生产者线程在此队列上阻塞。消费者线程从阻塞队列取出数据,当队列为空时,消费者线程在此队列上阻塞。
七、同步器
java.util.concurrent包包含了几个能帮助人们同步并发线程的类。如果并发的线程集满足以下条件,则应该优先使用适合的库类而不是自己用锁或条件去管理线程的同步
类
|
功能
|
适用场景
|
CyclicBarrier
|
允许一组线程相互等待,直到其中预定数目
的线程到达一个公共障栅(barrier),然后可以选择一个处理障栅的动作
|
大量线程需要在它们的结果可用之前完成
|
CountDownLatch
|
允许线程集等待直到计数器减为0
|
一个或多个线程需要等待直到指定数目的事件发生
|
Exchanger
|
允许两个线程在要交换的对象准备好时交换对象
|
当两个线程工作在同一个数据结构的两个实例
上的时候,一个向实例添加数据而另一个从实
例清除数据
|
Semaphore
|
允许线程集等待直到被允许继续执行为止
|
限制访问资源的线程总数,如果许可数是1,
常常阻塞线程直到另一个线程给出许可为止
|
SynchronousQueue
|
允许一个线程把对象交给另一个线程
|
在没有显示同步的情况下,当两个线程准备好
将一个对象从一个线程传递到另一个线程时
|
1.障栅
CyclicBarrier类实现了一个 集结点 ,称为 障栅 。考虑一次运算分解成不同部分运行分散到大量线程中运行的情形,当所有部分都准备好时,需要把结果都组合到一起。当一个线程完成了它的那部分任务之后,我们让它运行到障栅处。一旦所有的线程都达到了这个障栅,障栅就撤销,线程可以继续运行。
barrier 在释放所有等待线程后可以被重用,所以称它为循环 的 barrier。这一点有别于倒计时门闩,倒计时门闩只能使用一次。
当在障栅上等待的线程离开了障栅(等待超过了指定的时限,或者被中断),那么障栅就被破坏了,所有其他在此障栅上await的线程在await方法上抛出BrokenBarrierException异常。
可以提供一个可选的障栅动作,当所有线程到达障栅时就会执行这个动作。
用法:
(1)构造一个障栅,并给出参与的线程数
CyclicBarrier barrier = new CyclicBarrier(threadCcount)
另外,构造时还能指定障栅动作(所有线程都到达障栅时才会执行)
CyclicBarrier barrier = new CyclicBarrier(threadCcount,new Runnalbe(){public void run(){...}});
(2)每个线程做一些工作,然后再障栅上等待(可指定超时参数)
public void run(){
doWork();
barrier.await();
...
}
2.倒计时门栓
CountDownLatch让一个线程集等待直到计数减为0。倒计时门栓是一次性的,一旦计数为0,它就不能再重用了。
countDown方法递减锁存器的计数,如果计数到达零,则释放所有等待的线程。
举例,假定所有工作线程需要由另一些线程提供数据,工作线程被启动并在门外等候(await),其他线程准备数据,当数据准备好的时候,准备线程调用countDown,当计数值减为0时,所有的工作线程就可以继续运行了。
一个有用的特例是计数值为1的门栓,它是一个简单的开关锁存器。实现一个只能通过一次的门,线程在门外等候直到另一个线程将计数值置为0。
3.交换器
当两个线程在同一个数据缓冲区的两个实例上工作的时候,就可以使用交换器。典型情况是,一个线程向缓冲区填入数据,另一个线程消耗数据,当它们都完成后,相互交换缓冲区。
4.信号量
信号量Semaphore管理许多 许可证 ,线程向信号量申请到许可证时才能继续执行。实际上,并不存在许可证这种对象,信号量内部维护的是一个计数。注意,许可证可以由它的持有者以外的人释放,当被释放的许可多于最大可用许可数时,信号量被设置成最大可用许可数。关于信号量机制,可以参考操作系统的教材。
5.同步队列
同步队列是一种将生产者与消费者线程配对的机制,当一个线程调用SynchronousQueued的put方法时,它会阻塞一直到另一个线程调用take方法为止,反之亦然。与Exchanger不同,数据仅仅往一个方向传递,从生产者到消费者。
阅读(525) | 评论(0) | 转发(0) |