Day126.JUC:Lock、可重入锁、公平锁、限时等待上锁、读写锁

目录

一、JUC概述及回顾

二、Lock 锁  ★★★

可重入锁 (递归锁) ReentrantLock

公平锁

限时等待上锁 tryLock (解决死锁)  

三、读写锁  (ReentrantReadWriteLock)

锁降级 (了解)


一、JUC概述及回顾

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

Int i = 0, i ++; 非原子性,分三步;需要用到atomic 原子性容器

线程和进程

每一个程序都有一个进程,操作系统动态执行的基本单元;

一个进程中可以包含若干个线程;线程是资源调度的最小单位

常识:一个进程中通常有3000 - 5000个线程

生活实例:

使用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括号里配置的对象

所有的静态同步方法用的是同一把锁—— 类对象本身

项目中用synchronized还是Lock?项目中其他模块用的什么锁,保持一致。先去模仿再去创新

二、Lock 锁  ★★★

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(); //解锁
        }
    }
}

可重入锁 (递归锁) ReentrantLock

可重入锁又名递归锁(锁可以传递),是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁。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 (解决死锁)  

tryLock 方法可以选择传入指定的等待时间,无参则表示立即返回锁申请的结果true表示获取锁成功,false表示获取锁失败。我们可以将这种方法用来解决死锁问题

 booleantryLock()
          仅在调用时锁未被另一个线程保持的情况下,才获取该锁。
 booleantryLock(long timeout, TimeUnit unit)
          如果锁在给定等待时间内没有被另一个线程保持,且当前线程未被中断,则获取该锁。
        boolean flag = false;
        try {
            //限时等待上锁
            flag = lock.tryLock(3,TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

三、读写锁  (ReentrantReadWriteLock)

读写锁允许同一时刻被多个读线程访问写线程访问时,读线程和其他的写线程都会被阻塞

同一时刻允许多个线程对共享资源进行读操作;同一时刻只允许一个线程对共享资源进行写操作;

生活案例:开会投影仪、直播、红蜘蛛

  • 写写不可并发
  • 读写不可并发 (脏读)
  • 读读可以并发

避免死锁:锁的轻一点,范围小一些

1. 创建实例 没有锁的情况

class MyCache{
    //  volatile : 在多线程中, 被它修饰的变量是可见的!(防止指令重排)
    private volatile Map<String,String> 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<String,String> 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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值