重入锁(ReentrantLock)、重入读写锁(ReentrantReadWriteLock)、线程通信(Condition)


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(); }
    }
}
上面的例子中,有多个写操作和多个读操作,它们同样遵循原则:并发读、互斥写

上面的例子对非线程安全的集合进行了读写锁控制,比直接使用同步的集合(同步所有访问)效率更高!

阅读更多

没有更多推荐了,返回首页