1.使用重入锁完成的“生产-消费”模型:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class Account {
private static int commonIndex = 5;
private static Lock indexLock = new ReentrantLock();
private static Condition canPull = indexLock.newCondition();
private static Condition needPush = indexLock.newCondition();
// 多线程索取
public int pull() {
int index = 0;
indexLock.lock();
try {
while (commonIndex < 1) {// 判断数据是否符合条件,没有则继续等待并通知补充!
System.out.println("数量小于1,不够了,请补充...");
needPush.signalAll();// 数量不够了需要放入,发出需要补充信号
canPull.await();//等待可以拉取的通知
System.out.println("收到拉取通知,继续拉取...");
}
index = commonIndex;
Thread.sleep(200); // 用于测试是否会出现取出相同的数字来,(结果:加锁后不会有线程问题)
commonIndex--;
} catch (Exception e) {} finally {
indexLock.unlock();
}
return index;
}
// 单线成维护
public void push() {
indexLock.lock();
try {
while (commonIndex <= 0) {
commonIndex += 3;//以此补充3个
canPull.signalAll();//通知:可以拉取了...
System.out.println("报告补充完毕!继续等待补充通知...");
needPush.await();//等待
}
} catch (Exception e) {} finally {
indexLock.unlock();
}
}
}
public class Test {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newFixedThreadPool(100);
// 索取
for (int i = 0; i < 10; i++) {
threadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println(new Account().pull());
}
});
}
// 维护,这里也可以用多个线程,但是同步判断条件commonIndex <= 0,
// 会使得多线程无意义,即只有第一个线程会补充,后续补充线程发现 >0 后不再补充
threadPool.execute(new Runnable() {
@Override
public void run() {
new Account().push();
}
});
// 关闭线程池
threadPool.shutdown();
}
}
运行结果:
5
4
3
2
1
数量小于1,不够了,请补充...
数量小于1,不够了,请补充...
数量小于1,不够了,请补充...
数量小于1,不够了,请补充...
数量小于1,不够了,请补充...
报告补充完毕!继续等待补充通知...
收到拉取通知,继续拉取...
3
收到拉取通知,继续拉取...
2
收到拉取通知,继续拉取...
1
收到拉取通知,继续拉取...
数量小于1,不够了,请补充...
收到拉取通知,继续拉取...
数量小于1,不够了,请补充...
报告补充完毕!继续等待补充通知...
收到拉取通知,继续拉取...
3
收到拉取通知,继续拉取...
2
本文为了说明了ReentrantLock和Lock.newCondition()用法,其实上述的 “生产-消费”模型使用 阻塞队列 实现会简单很多。
2.重入读写锁,允许并发读,提高性能,下面是混合读写的锁控制模型:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.ReentrantReadWriteLock;
class Account {
int data = 0;
final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
void processCachedData() throws Exception {
rwl.readLock().lock();
log("is reading ...");
if (isOk()) {
rwl.readLock().unlock();//写锁与读锁互斥,必需释放rwl的读锁后才能上rwl的写锁
rwl.writeLock().lock();
try {
if (isOk()) {// 得到写锁后再判断,防止被其它线程抢先更改
log("is writing ...");
data += 3;// 进行写操作
//Thread.sleep(200000);//可以看到,加上写锁将不可再读
Thread.sleep(2);
} else {
log("发现已被抢先更新,无需再更新。");
}
} finally {
log("write over.");
rwl.readLock().lock();// 在释放写锁之前最好先先获取读锁,以防止使用之前被其他写线程改变了数据状态
rwl.writeLock().unlock(); // 释放写锁,但上面一句依旧保留了读锁,可防止写锁介入
}
}
try {
log("读取并使用数据:" + data--);
Thread.sleep(1000);
} finally {
log("read over.");
rwl.readLock().unlock();
}
}
boolean isOk() {
return data == 0;
}
void log(String msg) {
System.out.println(Thread.currentThread().getName() + " " + msg);
}
}
public class Test {
public static void main(String[] args) {
final Account account = new Account();
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++) {
executorService.execute(new Runnable() {
@Override
public void run() {
try {
account.processCachedData();
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
executorService.shutdown();
}
}
输出:
pool-1-thread-1 is reading ...
pool-1-thread-1 is writing ...
pool-1-thread-1 write over.
pool-1-thread-1 读取并使用数据:3
pool-1-thread-2 is reading ...
pool-1-thread-2 读取并使用数据:2
pool-1-thread-3 is reading ...
pool-1-thread-3 读取并使用数据:1
pool-1-thread-4 is reading ...
pool-1-thread-5 is reading ...
pool-1-thread-1 read over.
pool-1-thread-2 read over.
pool-1-thread-3 read over.
pool-1-thread-4 is writing ...
pool-1-thread-4 write over.
pool-1-thread-4 读取并使用数据:3
pool-1-thread-4 read over.
pool-1-thread-5 发现已被抢先更新,无需再更新。
pool-1-thread-5 write over.
pool-1-thread-5 读取并使用数据:2
pool-1-thread-5 read over.
可以看出:
a.读写锁的优势在于允许同时加读锁以提供性能
b.读锁释放前不能加写锁
c.写锁释放前不能加读锁或写锁
3.一种更加常用的读写同步控制方式
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* get 和 allKeys 是读操作
* put 和 clear 是写操作
* 读操作可以并发进行,但有写锁时会等写锁释放后再加读锁
* 写操作不允许同时读或写,故必需先释放所有的读锁和写锁
*/
public class RWDictionary {
private final Map<String, String> m = new TreeMap<String, String>();
private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private final Lock r = rwl.readLock();
private final Lock w = rwl.writeLock();
public String get(String key) {
r.lock();
try {
return m.get(key);
} finally {
r.unlock();
}
}
public Object[] allKeys() {
r.lock();
try {
return m.keySet().toArray();
} finally {
r.unlock();
}
}
public String put(String key, String value) {
w.lock();
try {
return m.put(key, value);
} finally {
w.unlock();
}
}
public void clear() {
w.lock();
try { m.clear(); } finally { w.unlock(); }
}
}
上面的例子中,有多个写操作和多个读操作,它们同样遵循原则:并发读、互斥写
上面的例子对非线程安全的集合进行了读写锁控制,比直接使用同步的集合(同步所有访问)效率更高!