目录
三、读写锁 (ReentrantReadWriteLock)
在 Java 5.0 提供了 java.util.concurrent(简称JUC)包,在此包中增加了在并发编程中很常用的工具类。此包包括了几个小的、已标准化的可扩展框架,并提供一些功能实用的类,没有这些类,一些功能会很难实现或实现起来冗长乏味。

Int i = 0, i ++; 非原子性,分三步;需要用到atomic 原子性容器
线程和进程
每一个程序都有一个进程,操作系统动态执行的基本单元;
一个进程中可以包含若干个线程;
生活实例:
使用QQ,查看进程一定有一个QQ.exe的进程,我可以用qq和A文字聊天,和B视频聊天,给C传文件,给D发一段语言,这就是线程。QQ支持录入信息的搜索。
大四的时候写论文,用word写论文,同时用QQ音乐放音乐,同时用QQ聊天,多个进程。
word如没有保存,停电关机,再通电后打开word可以恢复之前未保存的文档,word也会检查你的拼写,两个线程:容灾备份,语法检查
Nginx 多个进程
并行和并发
并行:同一时间多个线程在执行,同一时刻多个线程在访问同一个资源, (烧水泡面)
并发:同一时间多个线程在做同一件事 (秒杀、春运抢票)
wait/sleep的区别
wait 释放锁,sleep 不释放锁
wait是Object的方法,sleep是Thread
wait 一般搭配 notify 使用
相同点:在哪睡的在哪醒
创建线程的方式:
继承Thread抽象类;
实现Runnable接口:
实现JUC 的Callable接口
实现线程池 (常用)
lambda表达式
Lambda 是一个匿名函数,使代码更简洁、更灵活
左侧:指定了 Lambda 表达式需要的所有参数
右侧:指定了 Lambda 体,即 Lambda 表达式要执行的功能
复制小括号(参数),写死右箭头(->),落地大括号{}
前提:Lambda,省略了方法名。要求必须是函数式接口 @FunctionalInterface
//Foo foo = (int x,int y) -> {return x+y;}; Foo foo = (x,y) -> x+y; System.out.println(foo.add(10,20));
函数式接口扩展
1.8之后 接口可以实现default(缺省方法)方法、静态方法
@FunctionalInterface interface Foo{ public int add(int x,int y); default int sub(int x,int y){ return x - y; } public static int div(int x,int y){ return x/y; } }
synchronized的8锁问题
多线程编程模板上:高内聚,低耦合;线程 操作 资源类
// 资源类 class Phone{ // 发送短信 public synchronized void sendMsg() throws InterruptedException { System.out.println("----发送短信方法...."); } // 发送邮件 public synchronized void sendEmail(){ System.out.println("----发送邮件方法...."); } // 打电话 public void getHello(){ System.out.println("----打电话 hello word!...."); } } //1. 标准访问,先打印短信还是邮件 //2. 停4秒在短信方法内,先打印短信还是邮件 //3. 普通的hello方法,是先打短信还是hello //4. 现在有两部手机,先打印短信还是邮件 //5. 两个静态同步方法,1部手机,先打印短信还是邮件 //6. 两个静态同步方法,2部手机,先打印短信还是邮件 //7. 1个静态同步方法,1个普通同步方法,1部手机,先打印短信还是邮件 //8. 1个静态同步方法,1个普通同步方法,2部手机,先打印短信还是邮件 public class Lock_8 { public static void main(String[] args) { // 线程操作资源类 Phone phone = new Phone(); // Phone phone2 = new Phone(); // 定义一个线程: new Thread(()->{ try { phone.sendMsg(); } catch (InterruptedException e) { e.printStackTrace(); } },"AA").start(); // 定义一个线程: new Thread(()->{ phone.sendEmail(); // phone2.sendEmail(); // phone.getHello(); },"BB").start(); } } *答案 //1. 标准访问,先打印短信还是邮件(短信) //2. 停4秒在短信方法内,先打印短信还是邮件(短信) //3. 普通的hello方法,是先打短信还是hello(hello) //4. 现在有两部手机,先打印短信还是邮件(邮件) //5. 两个静态同步方法,1部手机,先打印短信还是邮件(短信) //6. 两个静态同步方法,2部手机,先打印短信还是邮件(短信) //7. 1个静态同步方法,1个普通同步方法,1部手机,先打印短信还是邮件(邮件) //8. 1个静态同步方法,1个普通同步方法,2部手机,先打印短信还是邮件(邮件)普通同步方法,锁是当前实例对象 this。
静态同步方法,锁是当前类的Class对象。
同步方法块,锁是Synchonized括号里配置的对象
所有的静态同步方法用的是同一把锁—— 类对象本身。
Lock 实现提供了比使用同步方法和语句可以获得的更广泛的锁操作。它们允许更灵活的结构,可能具有非常不同的属性,并且可能支持多个关联的条件对象。 (公平锁,非公平锁,共享锁,独占锁...)
Lock 是一个接口,这里主要有三个实现:ReentrantLock(可重复的)、ReentrantReadWriteLock.ReadLock、ReentrantReadWriteLock.WriteLock (可重复的读写锁)
参考API:
注意:synchronized和Lock不能同时使用
- class X {
- private final ReentrantLock lock = new ReentrantLock();
- // ...
-
- public void m() {
- lock.lock(); // 上锁
- try {
- // ... method body
- } finally {
- lock.unlock() // 解锁
- }
- }
- }
改造后的售票方法
- //资源类
- class Ticket{
-
- private Integer number = 20;
- private Lock lock = new ReentrantLock();
-
- public void sale(){
- lock.lock(); //上锁
- try {
- //判断有没有票
- if(number<=0){
- System.out.println("票已售空");
- return;
- }
- Thread.sleep(200);
- number--;
- System.out.println(Thread.currentThread().getName()+"卖票成功,剩余票数"+number);
- } catch (InterruptedException e) {
- e.printStackTrace();
- } finally {
- lock.unlock(); //解锁
- }
- }
- }
可重入锁又名递归锁(锁可以传递),是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁。Java中ReentrantLock 和 synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。
- class A{
- public synchronized void aa{
- ......
- bb();
- ......
- }
- public synchronized void bb{
- ......
- }
- }
- A a = new A();
- a.aa();
ReentrantLock和synchronized区别
二者都是独占锁
(1) synchronized加锁和解锁的过程自动进行,ReentrantLock 上锁解锁需要手动进行,比较灵活,
(2) synchronized可重入,因为加锁和解锁自动进行,不必担心最后是否释放锁;ReentrantLock也可重入,但加锁和解锁需要手动进行,且次数需一样,否则其他线程无法获得锁。
(3) synchronized不可响应中断,一个线程获取不到锁就一直等着;ReentrantLock可以响应中断。
所谓公平锁,也就是在锁上等待时间最长的线程将获得锁的使用权。通俗的理解就是谁排队时间最长谁先执行获取锁。
在创建ReentrantLock锁时,加入Boolean参数,默认非公平
注意:底层多维护了一个队列,效率会降低
private Lock lock = new ReentrantLock(true);
ReentrantLock()创建一个 ReentrantLock 的实例。 |
ReentrantLock(boolean fair)创建一个具有给定公平策略的 ReentrantLock(公平锁)。 |
tryLock 方法可以选择传入指定的等待时间,无参则表示立即返回锁申请的结果:true表示获取锁成功,false表示获取锁失败。我们可以将这种方法用来解决死锁问题。
boolean | tryLock()仅在调用时锁未被另一个线程保持的情况下,才获取该锁。 |
boolean | tryLock(long timeout, TimeUnit unit)如果锁在给定等待时间内没有被另一个线程保持,且当前线程未被中断,则获取该锁。 |
- boolean flag = false;
- try {
- //限时等待上锁
- flag = lock.tryLock(3,TimeUnit.SECONDS);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
读写锁允许同一时刻被多个读线程访问,写线程访问时,读线程和其他的写线程都会被阻塞。
同一时刻允许多个线程对共享资源进行读操作;同一时刻只允许一个线程对共享资源进行写操作;
生活案例:开会投影仪、直播、红蜘蛛
避免死锁:锁的轻一点,范围小一些

1. 创建实例 没有锁的情况
- class MyCache{
- // volatile : 在多线程中, 被它修饰的变量是可见的!(防止指令重排)
- private volatile Map
map = new HashMap<>(); -
- //写数据
- public void pot(String key,String value){
- try {
- System.out.println(Thread.currentThread().getName()+"开始写入-----");
- Thread.sleep(200);
- map.put(key, value);
- System.out.println(Thread.currentThread().getName()+"写入完成-----");
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
-
- //写数据
- public void get(String key){
- try {
- System.out.println(Thread.currentThread().getName()+"开始读取-----");
- Thread.sleep(200);
- String msg = map.get(key);
- System.out.println(Thread.currentThread().getName()+"---读取完成......."+msg);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
-
- public class ReentrantReadWriteLockDemo {
- public static void main(String[] args) {
- MyCache myCache = new MyCache();
- //创建5个写线程
- for (int i = 0; i < 5; i++) {
- String num = String.valueOf(i);
- new Thread(()->{
- myCache.pot(num,num);
- },num).start();
- }
-
- //创建5个写线程
- for (int i = 0; i < 5; i++) {
- String num = String.valueOf(i);
- new Thread(()->{
- myCache.get(num);
- },num).start();
- }
- }
- }
2. 参考API
- class CachedData {
- Object data;
- volatile boolean cacheValid;
- ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
-
- void processCachedData() {
- rwl.readLock().lock();
- if (!cacheValid) {
- // Must release read lock before acquiring write lock
- rwl.readLock().unlock();
- rwl.writeLock().lock();
- // Recheck state because another thread might have acquired
- // write lock and changed state before we did.
- if (!cacheValid) {
- data = ...
- cacheValid = true;
- }
- // Downgrade by acquiring read lock before releasing write lock
- rwl.readLock().lock();
- rwl.writeLock().unlock(); // Unlock write, still hold read
- }
-
- use(data);
- rwl.readLock().unlock();
- }
- }
3. 加入读写锁
- class MyCache{
- // volatile : 在多线程中, 被它修饰的变量是可见的!(防止指令重排)
- private volatile Map
map = new HashMap<>(); - //读写锁
- private final ReentrantReadWriteLock rwlock = new ReentrantReadWriteLock();
-
- //写数据
- public void pot(String key,String value){
- rwlock.writeLock().lock(); //上写锁 (排他锁)
- try {
- System.out.println(Thread.currentThread().getName()+"开始写入-----");
- Thread.sleep(200);
- map.put(key, value);
- System.out.println(Thread.currentThread().getName()+"写入完成-----");
- } catch (InterruptedException e) {
- e.printStackTrace();
- }finally {
- rwlock.writeLock().unlock(); //解开写锁
- }
- }
-
- //写数据
- public void get(String key){
- rwlock.readLock().lock(); //读写锁 (共享锁)
- try {
- System.out.println(Thread.currentThread().getName()+"开始读取-----");
- Thread.sleep(200);
- String msg = map.get(key);
- System.out.println(Thread.currentThread().getName()+"---读取完成......."+msg);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }finally {
- rwlock.readLock().unlock(); //解开写锁
- }
- }
- }

独占改为共享可以(降级),共享改为独占不可(升级)。
什么是锁降级,锁降级就是从写锁降级成为读锁。在当前线程拥有写锁的情况下,再次获取到读锁,随后释放写锁的过程就是锁降级。这里可以举个例子:
- // 锁的降级:从写锁降级成为读锁,随后释放写锁的过程就是锁降级!
- public void c(){
- rwl.writeLock().lock();
- System.out.println("写锁--------------");
- //写锁还没有释放之前,获取读锁
- rwl.readLock().lock();
- System.out.println("读锁===============");
- rwl.writeLock().unlock();
- System.out.println("释放写锁+++++++++++");
- rwl.readLock().unlock();
- System.out.println("释放读锁###########");
- }
读写锁总结
1. 支持公平/非公平策略
2. 支持可重入
- 同一读线程在获取了读锁后还可以获取读锁
- 同一写线程在获取了写锁之后既可以再次获取写锁又可以获取读锁3. 支持锁降级,不支持锁升级
4. 读写锁如果使用不当,很容易产生“饥饿”问题:
- 在读线程非常多,写线程很少的情况下,容易导致写线程“饥饿”,虽然使用“公平”策略可以一定程度上缓解这个问题,但“公平”策略会使系统吞吐量降低。 (底层排队,维护队列效率降低)5. Condition 条件支持 (钥匙)
- 写锁可以通过newCondition()方法获取Condition对象。但是读锁是没法获取Condition对象,读锁调用newCondition()方法会直接抛出UnsupportedOperationException。