标签(空格分隔): java
java 5新增机制--->同步锁(Lock)
从java5开始,java提供了一种更为强大的线程同步机制,就是同步所对象来实现同步,同步锁对象有Lock
对象充当。
Lock
提供了比synchronized
方法和sysnchronized
代码块更为广泛的锁定操作,Lock
允许实现更为灵活的结构,可以具有很大差别的属性,并支持多个相关的Condition
对象。
Lock是控制多个线程对共享资源进行访问的工具。通常锁提供了对共享资源的单独访问,没次只能有一个线程对Lock对象进行加锁,线程开始访问共享资源之前,先获得Lock对象。
某些锁可能允许对共享资源的并发访问,如:ReadWriteLock(读写锁)
。
Lock
,ReadWritedLock
是java5提供的两个根接口
同时为Lock
接口提供了ReentrantLock(可重入锁实现)
实现类,为ReadWriteLock
提供了ReentrantReadWriteLock
实现类。
Java 8
新增了新型的StampedLock
类,在大多数场景中可以替代传统的ReetrantReadWriteLock
,ReetrantReadWriteLock
为读写操作提供了三种锁模式,Writing
,ReadingOptimistic
,Reading
1、ReentrantLock锁(可重用锁)
使用很简单,获取ReentrantLock
对象,注意在同步的时候一个定要保证ReentrantLock
对象时同一个对象
ReentrantLock lock = new ReentrantLock();
加锁
lock.lock();
由于Lock机制是使用java实现的,synchronized
是底层jvm实现,因此使用Lock
必须手动释放锁,为了保证锁一定被释放,我们使用try{}finally{}
来实现锁的释放
finally{
//最后一步,无论怎样都要释放锁
lock.unlock();
}
private static final ReentrantLock lock = new ReentrantLock();
public void func(){
//加锁
lock.lock();
try{
要同步的代码区域
} finally{
//最后一步,无论怎样都要释放锁
lock.unlock();
}
}
ReentrantLock
锁,它是一种可重用锁。(什么是可重用锁,一个线程可以对已经加锁的Reentrantlock再次加锁,ReentrantLock
对象会维持一个计数器,来追踪lock()
方法的嵌套调用,线程在每次调用lock()
加锁后,必须显示的调用unlock()
来释放锁。所以一段被锁保护的代码,可以调用另一个被相同锁锁定的方法)。
2、ReentrantReadWriteLock(读写锁)
ReentrantLock'实现了标准的互斥操作,也就是一次只能有一个线程持有锁,也即所谓独占锁的概念。也就是说
读/写,
读/读,
写/写`操作不能同时发生,在一定程度上面减低了吞吐量。需要强调的一个概念是,锁是有一定的开销的,当并发比较大的时候,锁的开销就比较客观了。所以如果可能的话就尽量少用锁,非要用锁的话就尝试看能否改造为读写锁。
ReadWriteLock
一个资源能够被多个读线程访问,或者被一个写线程访问,但是不能同时存在读写线程也就是说读写锁使用的场合是一个共享资源被大量读取操作,而只有少量的写操作(修改数据)。
public interface ReadWriteLock {
/**
* Returns the lock used for reading.
*
* @return the lock used for reading
*/
Lock readLock();
/**
* Returns the lock used for writing.
*
* @return the lock used for writing
*/
Lock writeLock();
}
这里需要说明的是ReadWriteLock
并不是Lock
的子接口,只不过ReadWriteLock
借助Lock
来实现读写两个视角,在ReadWriteLock
中每次读取共享数据就需要读取锁,当需要修改共享数据时就需要写入锁,看起来好像是两个锁,但其实不尽然
ReentrantReadWriteLock的几个特性:
公平性:
- 非公平锁: 这个和独占锁的非公平性一样,所以读取操作没有公平性和非公平性,写操作时,由于写操作可能立即获取到锁,所以会推迟一个或多个读操作或者写操作,因此非公平锁的吞吐量要高于公平锁.
- 公平锁 : 利用AQS的CLH队列,释放当前保持的锁(读锁或者写锁)时,优先为等待时间最长的那个写线程分配写入锁,当前前提是写线程的等待时间要比所有读线程的等待时间要长。同样一个线程持有写入锁或者有一个写线程已经在等待了,那么试图获取公平锁的(非重入)所有线程(包括读写线程)都将被阻塞,直到最先的写线程释放锁。如果读线程的等待时间比写线程的等待时间还有长,那么一旦上一个写线程释放锁,这一组读线程将获取锁
重入性:
- 读写锁允许读线程和写线程按照请求锁的顺序重新获取读取锁或者写入锁。当然了只有写线程释放了锁,读线程才能获取重入锁。
- 写线程获取写入锁后可以再次获取读取锁,但是读线程获取读取锁后却不能获取写入锁。
- 另外读写锁最多支持65535个递归写入锁和65535个递归读取锁。
锁降级:
- 写线程获取写入锁后可以获取读取锁,然后释放写入锁,这样就从写入锁变成了读取锁,从而实现锁降级的特性。
想知道更多特性可以参考[ReentrantReadWriteLock深入分析][1]
如何使用?
public class Databean {
private int mDataStr = 123;
private ReentrantReadWriteLock mLock = new ReentrantReadWriteLock();
public void getData(){
mLock.readLock().lock();//上读读锁其他线程不能写
System.out.println(Thread.currentThread().getName()+"可以读取数据");
System.out.println(Thread.currentThread().getName()+"读取到数据--->"+mDataStr);
try {
Thread.sleep((long) (Math.random()*1000));
} catch (InterruptedException e) {
e.printStackTrace();
}finally{
mLock.readLock().unlock();//释放读取锁
}
}
public void writeData(int data ){
mLock.writeLock().lock();//上写入锁,同一个时间内只有一个线程可以写入数据,其他线程都必须等待
System.out.println(Thread.currentThread().getName()+"可以写入数据");
try {
Thread.sleep((long) (Math.random()*1000));
System.out.println(Thread.currentThread().getName()+"写入数据-------->"+data);
mDataStr = data;
} catch (InterruptedException e) {
e.printStackTrace();
}finally{
mLock.writeLock().unlock();//释放写入锁
}
}
}
线程通信
1、等待-唤醒机制(生产者-消费者机制)
当我们在开发工程中,需要这样一个需求:生产方不停的生产物品,消费方不停的消费物品。
由于计算机的线程调度具有随机性,这样就会出现以下问题:
1.生产线程的物品还没有生产,消费线程不停的消费,这样会导致消费线程工作无用,浪费系统资源
2.生产线程的物品还没有完全生产完毕,消费线程就开始消费了该物品,造成数据发生错误。
为了解决上面的问题,我们做如下设想:
当生产线程完全生产了一个物品之后,自己停止生产,然后告诉消费线程,开始消费,当消费线程消费完毕之后,自己停止消费,告诉生产线程,开始生产,这样就完成了一个良性的循环,既没有避免的系统资源的无效开销,同时也可以保证,数据的正确性。
为了实现这样的功能,java的Object
类提供了wait()
、notify()
、notifyAll()
三个方法,这三个方法比不属于Thread
类,而是属于Object
,这个三个方法必须由同步监视器对象来调用,这可以分成以下两种情况。
- 对于使用
synchronized
修饰的同步方法,因为该类的默认实例(this)就是同步监视器,所以可以在同步方法中直接调用者三个方法。- 对于使用
synchronized
修饰的同步代码块,同步监视器是synchronized
后括号里的对象,因此必须使用该对象调用者三个方法。
关于这三个方法有如下解释:
wait()
是导致当前线程等待,直到其他线程调用该同步监视器对象的notify()
方法或者notifyAll()
方法来唤醒该线程,wait()
分别有三个重载方法,分别是传入等待的时间(等待指定的时间后自动苏醒),wait()
和sleep()
方法的不同是:a.调用者不同,wait的调用者必须是同步监视器对象,sleep的调用者是Thread类,是一个静态的方法;b.wait方法执行后该线程会释放锁,sleep方法不会释放锁。
notify()
方法此同步监视器上等待的单个线程,如果所有的线程都在此同步监视器上等待,则会唤醒其中的一个线程,唤醒具有任意性。只有当前线程放弃对同步监视器上的锁定之后(使用wait()方法),才可以执行被唤醒的线程。
notifyAll()
唤醒此同步监视器上的所有等待的线程,只有当前线程放弃对同步监视器上的锁定之后,才可以执行被唤醒的线程
示例:
public class Goods {
private int mNumber;
private boolean flag = false;
/**
* 商品的生产方法
*/
public void product2() {
synchronized (this) {
if (isProduct) { //如果flag = true 这里是所有生产线程都将等待
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (!isProduct) { //二次判断,保证不会重复生产
mNumber++;
System.out.println(Thread.currentThread().getName() + "------------生产--" + mNumber );
}
isProduct = true;
this.notifyAll();
}
}
/**
* 商品的消费方法
*/
public void consume2() {
synchronized (this) {
if (!isProduct) {// 如果flag = false 所有消费线程都将等待
try {
System.out.println(Thread.currentThread().getName() + "++等待--");
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (isProduct){//二次判断保证不会重复消费
System.out.println(Thread.currentThread().getName() + "---消费--" + mNumber);
}
isProduct = false;
this.notifyAll();
}
}
/**
* 生产者
*/
class Producer implements Runnable{
private Goods mGoods=null;
public Producer(Goods mGoods) {
this.mGoods = mGoods;
}
@Override
public void run() {
while(true)
mGoods.product();
}
}
/**
* 消费者
*/
class Consumer implements Runnable{
private Goods mGoods=null;
public Consumer(Goods mGoods) {
this.mGoods = mGoods;
}
@Override
public void run() {
while(true)
mGoods.consume();
}
}
/**
*测试
*/
public class ThreadTest {
public static void main(String[] args) {
Goods goods = new Goods();
Thread producer1 = new Thread(new Producer(goods),"生产线程1");
Thread producer2 = new Thread(new Producer(goods),"生产线程2");
Thread consumer1 = new Thread(new Consumer(goods) ,"消费线程1");
Thread consumer2 = new Thread(new Consumer(goods) ,"消费线程2");
producer1.start();
// producer2.start();
consumer1.start();
// consumer2.start();
}
}
上面的程序测试发现,在一个上产线程,一个消费线程的条件下运行一切正常,但是当开启多个生产线程,多个消费线程的时候,问题就来了,因为notifyAll
和notify
唤醒的线程是任意的,很可能出现生产线程又将生产线程唤醒。出现多次生产的情况,或者消费线程唤醒了消费线程,出现了多次消费的情况。
如何解决这个问题,我们在代码中加上二次判断,保证了生产和消费的唯一性。
2、使用Condition控制线程通信
如果程序不使用synchronized
来保证线程同步,而是直接使用Lock
对象来保证线程同步,则系统不存在隐式的同步监视器。也就不能使用wait()
、notify()
、notifyAll()
方法进行线程通信了。
当使用lock对象
来保证线程同步时,java提供了Condition
类来保持协调,使用Condition
可以让那些已经得到Lock
对象却无法继续执行的线程释放Lock
对象,Condition
也可以唤醒其他处于等待的线程。
Condition
将同步监视器的方法(wait
,notify
,notifAll
)分解成截然不同的对象,以便通过这些对象鱼Lock
对象组合使用,为每一个对象提供多个等待集(wait-set)。在这种情况下,Lock
取代了同步方法和同步代码块,Condition
取代了同步监视器。
Condition
实例被绑定到一个Lock
对象上。要获取特定Lock
实例的Condition
实例,调用Lock
对象的newCondition()
方法即可。Condition
类提供了如下三个方法。
await()
:类似于隐示同步监视器的wait()
,导致当前线程等待,直到其他线程调用该Condition
的signal()
或者signalAll()
方法来唤醒该线程,该await()
方法有更多的变体,如:long awaitNanos(long nanosTimeout)
、void awaitUninterruptibly()
、awaitUntil(Date deadline)
等,可以完成更丰富的操作。signal()
方法,唤醒此Lock
对象上等待的单个线程。如果所有的线程都在Lock
上等待,则会唤醒其中的一个,唤醒具有任意性。只用当前线程放弃对该Lock
对象的锁定后(使用await()方法
),才可以执行唤醒操作。
signalAll()
:唤醒此Lock
对象上等待的所有线程,只用当前线程放弃对该Lock
对象的锁定后(使用await()方法
),才可以执行唤醒操作。
示例:
private final Lock mLock = new ReentrantLock();
private final Condition mCon_pro = mLock.newCondition();
private final Condition mCon_con = mLock.newCondition();
/**
* 商品的生产方法
*/
public void product() {
mLock.lock();
try {
while (isProduct) { //如果flag = true 这里是所有生产线程都将等待
try {
mCon_pro.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (!isProduct) {
mNumber++;
System.out.println(Thread.currentThread().getName() + "------------生产--" +mNumber);
}
isProduct = true;
mCon_con.signalAll();
} finally {
mLock.unlock();
}
}
/**
* 商品的消费方法
*/
public void consume() {
mLock.lock();
try {
while (!isProduct) {// 如果flag = false 所有消费线程都将等待
try {
mCon_con.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (isProduct) {
System.out.println(Thread.currentThread().getName() + "---消费--" + mNumber);
}
isProduct = false;
mCon_pro.signalAll();
} finally {
mLock.unlock();
}
}
3、线程的虚假唤醒
对于上面的例子,这里我要提一个小问题,也是我在测试中遇到的问题,我挑一段其中的代码:
try {
while (!isProduct) {// 如果flag = false 所有消费线程都将等待
try {
mCon_con.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
我想问的是既然isProduct
是判断是否有商品生产过了,那么我们完全可以使用if
来代替啊,为什么要使用while
呢,而且官方API的例子也是while
。我当时就感觉可以,于是我就是用if
来代替,结果出现问题了,处于等待的线程有的时候会突然被唤醒?这是什么情况,当时我就蒙逼了,API的例子使用的是while
,原来它也发现了这个问题才这样写的。于是我仔细阅读了一下await()
方法被唤醒的条件,有一个第四条spurious wakeup
什么意思呢?中文解释是虚假唤醒
,就是说不是被signal
、和signalAll
唤醒的,通俗一点就是莫名的醒了,所以我们使用while
来代替if
,即使他醒了,我们也要让他再次await
。