您的位置:首页 > 编程语言 > Java开发

Java并发---synchronized、Lock、Condition的区别

2017-12-12 18:16 435 查看
        synchronized是Java关键字, Lock是Java接口。

      在Android开发中大多数情况下都使用synchronized,例如常用的StringBuffer、HashTable等线程安全的数据结构;但后台开发更多的用Lock。 

1、 synchronized保证任何时刻最多只有一个线程执行被修饰的函数/代码块,不用显示申请锁、释放锁; 缺点是所有等待线程是线性逐个运行的。具体用法参考Java生产者消费者模式

2、 Lock更加灵活, 支持申请锁、释放锁等。

ReentrantLock类实现了Lock接口, 提供了申请锁、释放锁等基本功能;

ReentrantReadWriteLock类实现了ReadWriteLock接口, 提供了读写分离功能,即无写时支持多线程读,只能支持一个线程写。

StampedLock是升级版的读写锁, 写的时候也支持读, 但读时可判断当前是否在写并重读。



     

     下面看Lock接口的定义, 包含申请锁、释放锁等功能。 在申请锁后必须要释放锁, 否则会出现死锁问题。

public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
       API文档里介绍了Lock的用法, 先申请锁l.lock(), 得到锁后执行代码逻辑, 最后在finally里释放锁。

lock函数是无返回值的阻塞函数, 如果拿不到锁则阻塞等待, 直到获取到锁后继续向下执行。

Lock l = ...;
l.lock();
try {
// access the resource protected by this lock
}finally {
l.unlock();
}


tryLock是有返回值的非阻塞函数(能立即返回), 成功拿到锁后返回true, 失败时返回false。

Lock lock = ...;
if (lock.tryLock()) {
try {
// manipulate protected state
}finally {
lock.unlock();
}
} else {
// perform alternative actions
}}


tryLock(long time, TimeUnit unit)方法和tryLock()方法类似, 区别是如果拿不到锁会等待一段时间, 超时返回false。

void testLock() throws InterruptedException {
Lock lock = new ReentrantLock();
if (lock.tryLock(3, TimeUnit.SECONDS)) {  //最多等待3秒
try {
// manipulate protected state
} finally {
lock.unlock();
}
} else {
// perform alternative actions
}
}


lockInteruptibly函数跟lock函数的区别是在其它线程调用当前线程的interupt方法后, 当前线程如果处于阻塞状态能够继续向下执行(效果类似于拿到了锁)。

Lock lock = new ReentrantLock();
//如果当前线程阻塞在这里,在其它线程调用当前线程的interupt方法后当前线程继续执行
lock.lockInterruptibly();
try {
//.....
}
finally {
lock.unlock();
}
     ReentrantLock类支持了基本的线程互斥操作, 即任何时刻只有一个线程能拿到锁, 缺点是最多一个线程在运行。

 Java为了支持高并发的读写操作, 又提供了读写所ReadWriteLock, 支持多读单写, 即无写时可以多读, 写时不能读,最多一个线程写。

public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
      一个是读锁,一个是写锁。  
public static class Reader implements Runnable {
private ReentrantReadWriteLock rwl;

public Reader(ReentrantReadWriteLock lock) {
this.rwl = lock;
}

@Override
public void run() {
while (true) {
try {
Thread.sleep(100);
} catch (Exception ex) {

}
Lock lock = rwl.readLock();
lock.lock();
try {
System.out.println(System.currentTimeMillis() + Thread.currentThread().getName() + "正在进行读操作");
Thread.sleep(500);
} catch (Exception ex) {

} finally {
lock.unlock();
}
}
}
}

public static class Writer implements Runnable {
private ReentrantReadWriteLock rwl;

public Writer(ReentrantReadWriteLock lock) {
this.rwl = lock;
}

@Override
public void run() {
while (true) {
try {
Thread.sleep(100);
} catch (Exception ex) {

}
Lock lock = rwl.writeLock();
lock.lock();
try {
System.out.println(System.currentTimeMillis() + Thread.currentThread().getName() + "正在进行写操作");
Thread.sleep(500);
} catch (Exception ex) {

} finally {
lock.unlock();
}
}
}

}

public static void main(String[] args) {
ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
new Thread(new Writer(rwl), "写线程A").start();
new Thread(new Writer(rwl), "写线程B").start();
new Thread(new Writer(rwl), "写线程C").start();
new Thread(new Writer(rwl), "写线程D").start();

new Thread(new Reader(rwl), "读线程1").start();
new Thread(new Reader(rwl), "读线程2").start();
new Thread(new Reader(rwl), "读线程3").start();
new Thread(new Reader(rwl), "读线程4").start();
}
结果:

1513073055136写线程A正在进行写操作
1513073055639写线程B正在进行写操作
1513073056139写线程C正在进行写操作
1513073056641写线程D正在进行写操作
1513073057141读线程1正在进行读操作
1513073057142读线程2正在进行读操作
1513073057142读线程3正在进行读操作
1513073057142读线程4正在进行读操作
1513073057647写线程A正在进行写操作
1513073058149写线程B正在进行写操作
1513073058651写线程C正在进行写操作
      可以看出任意时刻只有1个线程可以写, 但是可以有多个线程同时读。

Java8对读写锁进行了改进,新增StampedLock类支持乐观锁, 实现读不阻塞写, 在读的时候如果发生了写操作则重新读。下面代码摘自源码注释,区别是在读函数里调用validate方法判断释放有写操作发生并重新获取读锁。

class Point {
private double x, y;
private final StampedLock sl = new StampedLock();

void move(double deltaX, double deltaY) { // an exclusively locked method
long stamp = sl.writeLock();
try {
x += deltaX;
y += deltaY;
finally {
sl.unlockWrite(stamp);
}
}

double distanceFromOrigin() { // A read-only method
long stamp = sl.tryOptimisticRead();
double currentX = x, currentY = y;
if (!sl.validate(stamp)) {
stamp = sl.readLock();
try {
currentX = x;
currentY = y;
} finally {
sl.unlockRead(stamp);
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}

void moveIfAtOrigin(double newX, double newY) { // upgrade
// Could instead start with optimistic, not read mode
long stamp = sl.readLock();
try {
while (x == 0.0 && y == 0.0) {
long ws = sl.tryConvertToWriteLock(stamp);
if (ws != 0L) {
stamp = ws;
x = newX;
y = newY;
break;
}
else {
sl.unlockRead(stamp);
stamp = sl.writeLock();
}
}
} finally {
sl.unlock(stamp);
}
}
}}


Condition可以替换Object的wait/notify/notifyAll实现线程间通讯,await替换wait,signal替换notify,signalAll替换notifyAll。

PS: Conditon也是继承于Object类的, 但Object类的wait、notify和notifyAll都是用final修饰的, 所以Condition要新增函数。



JDK的示例代码是单个生产者单个消费者模式, 比起Object的wait/notify/notifyAll(随机唤醒读线程或写线程)的优势在于创建了读Condition和写Condition, 从而可以只唤醒读线程或写线程

     改造一下JDK示例代码使用signalAll(唤醒所有await线程但必须拿到唯一的锁后才能继续运行),将仓库容量给为3个, 使用2个写线程和2个读线程实现生产者消费者模式。

public class Main {
static class BoundedBuffer {
final Lock lock = new ReentrantLock(); //锁对象
final Condition notFull = lock.newCondition(); //写线程锁,仓库满时等待
final Condition notEmpty = lock.newCondition(); //读线程锁,仓库空时等待

final Object[] items = new Object[3];//数据仓库,最多容纳3个商品
int putptr; //写索引
int takeptr; //读索引
int count; //items记录数

//写操作,每次生产一个商品
public void put(Object x, String name) throws InterruptedException {
lock.lock(); //锁定
try {
System.out.println(System.currentTimeMillis() + "--- 写线程" + name+"进入. 剩余商品" + count + "个");
// 如果仓库满则等待读锁
while (count == items.length) {
System.out.println(System.currentTimeMillis() + "--- 写线程"+name+"等待");
notFull.await(); //释放锁并等待signal
System.out.println(System.currentTimeMillis() + "--- 写线程"+name+"继续执行");
}
//创建商品
items[putptr] = x;
if (++putptr == items.length) putptr = 0;
++count; //创建了一个商品

System.out.println(System.currentTimeMillis() + "--- 写线程" + name+"退出,并signal读线程. 剩余商品" + count + "个");
//如果读线程等待则唤醒读线程
notEmpty.signalAll();
} finally {
lock.unlock();//释放锁
}
}

//读操作,每次消费一个商品
public Object take(String name) throws InterruptedException {
lock.lock(); //获取锁
try {
System.out.println(System.currentTimeMillis() + "--- 读线程" + name+"进入. 剩余商品" + count + "个");
//如果仓库空则等待写线程
while (count == 0) {
System.out.println(System.currentTimeMillis() + "--- 读线程"+name+"等待");
notEmpty.await(); //释放锁并等待signal
System.out.println(System.currentTimeMillis() + "--- 读线程"+name+"继续执行");
}

//读取队列,并更新读索引
Object x = items[takeptr];
if (++takeptr == items.length) takeptr = 0;
--count; //消费了一个商品

System.out.println(System.currentTimeMillis() + "--- 读线程"+name+"退出,并signal写线程. 剩余商品"+ count + "个");
//如果写线程等待则唤醒写线程
notFull.signalAll();
return x;
} finally {
lock.unlock();//解除锁定
}
}
}

public static void main(String[] args) {
final BoundedBuffer obj = new BoundedBuffer();

//写线程A
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
obj.put(new Object(), "A");

Thread.sleep(100);
} catch (Exception ex) {

}
}
}
}).start();

//写线程B
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
obj.put(new Object(), "B");

Thread.sleep(50);
} catch (Exception ex) {

}
}
}
}).start();

//读线程1
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
obj.take("1");
Thread.sleep(100);
} catch (Exception ex) {

}
}
}
}).start();

//读线程2
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
obj.take("2");
Thread.sleep(80);
} catch (Exception ex) {

}
}
}
}).start();
}
}运行程序, 任意时刻只能有1个线程拿到锁, 但通过awati/signal可以唤醒读线程或写线程。 即写线程等待时肯定是读线程拿到锁, 读线程等待时肯定是写线程拿到锁。
1513132664275--- 读线程1进入. 剩余商品3个
1513132664275--- 读线程1退出,并signal写线程. 剩余商品2个
1513132664275--- 写线程A继续执行
1513132664275--- 写线程A退出,并signal读线程. 剩余商品3个
1513132664280--- 读线程2进入. 剩余商品3个
1513132664280--- 读线程2退出,并signal写线程. 剩余商品2个
1513132664284--- 写线程B进入. 剩余商品2个
1513132664284--- 写线程B退出,并signal读线程. 剩余商品3个
1513132664337--- 写线程B进入. 剩余商品3个
1513132664337--- 写线程B等待
1513132664364--- 读线程2进入. 剩余商品3个
1513132664364--- 读线程2退出,并signal写线程. 剩余商品2个
1513132664365--- 写线程B继续执行
1513132664365--- 写线程B退出,并signal读线程. 剩余商品3个
1513132664376--- 读线程1进入. 剩余商品3个
1513132664376--- 读线程1退出,并signal写线程. 剩余商品2个
1513132664376--- 写线程A进入. 剩余商品2个
1513132664376--- 写线程A退出,并signal读线程. 剩余商品3个
1513132664419--- 写线程B进入. 剩余商品3个
1513132664419--- 写线程B等待
1513132664448--- 读线程2进入. 剩余商品3个
1513132664448--- 读线程2退出,并signal写线程. 剩余商品2个
1513132664448--- 写线程B继续执行
1513132664448--- 写线程B退出,并signal读线程. 剩余商品3个
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: