JUC-day02

JUC-day02

  • 集合的线程安全
  • callable和future
  • JUC三个工具类(练习)
  • 读写锁: 共享锁 独占锁(练习)
  • AQS: 实现原理(核心方法)
  • CAS: 原理–>可见性关键字

1 集合的线程安全(重点)

1.1 集合操作Demo

NotSafeDemo

    public static void main(String[] args) {
        List list = new ArrayList();

        for (int i = 0; i < 10; i++) {
            new Thread(() ->{
                list.add(UUID.randomUUID().toString());
                System.out.println(list);
            }, "线程" + i).start();
        }
    }

异常内容

java.util.ConcurrentModificationException

问题: 为什么会出现并发修改异常?

查看ArrayList的add方法源码

/**
 * Appends the specified element to the end of this list.
 *
 * @param e element to be appended to this list
 * @return <tt>true</tt> (as specified by {@link Collection#add})
 */
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

那么我们如何去解决List类型的线程安全问题?

1.2 Vector

Vector 是矢量队列,它是JDK1.0版本添加的类。继承于AbstractList,实现了List, RandomAccess, Cloneable这些接口。
Vector 继承了AbstractList,实现了List;所以,它是一个队列,支持相关的添加、删除、修改、遍历等功能
Vector 实现了RandmoAccess接口,即提供了随机访问功能。RandmoAccess是java中用来被List实现,为List提供快速访问功能的。在Vector中,我们即可以通过元素的序号快速获取元素对象;这就是快速随机访问。
Vector 实现了Cloneable接口,即实现clone()函数。它能被克隆。

和ArrayList不同,Vector中的操作是线程安全的。

NotSafeDemo代码修改

  // Vector没有线程安全问题,因为方法中用synchronized同步关键字修饰 所有没有并发异常
    public static void main(String[] args) {
        Vector vector = new Vector();
        for (int i = 0;i < 100;i++){
            new Thread(() -> {
                vector.add(UUID.randomUUID().toString());
                System.out.println(vector);
            }, "线程" + i).start();
        }
    }

现在没有运行出现并发异常,为什么?

查看Vector的add方法

/**
 * Appends the specified element to the end of this Vector.
 *
 * @param e element to be appended to this Vector
 * @return {@code true} (as specified by {@link Collection#add})
 * @since 1.2
 */
public synchronized boolean add(E e) {
    modCount++;
    ensureCapacityHelper(elementCount + 1);
    elementData[elementCount++] = e;
    return true;
}

add方法被synchronized同步修辞,线程安全!因此没有并发异常

1.3 Collections

Collections提供了方法synchronizedList保证list是同步线程安全的

NotSafeDemo代码修改

	// Collections提供了方法synchronizedList保证list是同步线程安全的
    public static void main(String[] args) {
        List list = Collections.synchronizedList(new ArrayList<>());
        for (int i = 0; i < 100;i++){
            new Thread(() -> {
                list.add(UUID.randomUUID().toString());
                System.out.println(list);
            }).start();
        }
    }

没有并发修改异常

查看方法源码

/**
 * Returns a synchronized (thread-safe) list backed by the specified
 * list.  In order to guarantee serial access, it is critical that
 * <strong>all</strong> access to the backing list is accomplished
 * through the returned list.<p>
 *
 * It is imperative that the user manually synchronize on the returned
 * list when iterating over it:
 * <pre>
 *  List list = Collections.synchronizedList(new ArrayList());
 *      ...
 *  synchronized (list) {
 *      Iterator i = list.iterator(); // Must be in synchronized block
 *      while (i.hasNext())
 *          foo(i.next());
 *  }
 * </pre>
 * Failure to follow this advice may result in non-deterministic behavior.
 *
 * <p>The returned list will be serializable if the specified list is
 * serializable.
 *
 * @param  <T> the class of the objects in the list
 * @param  list the list to be "wrapped" in a synchronized list.
 * @return a synchronized view of the specified list.
 */
public static <T> List<T> synchronizedList(List<T> list) {
    return (list instanceof RandomAccess ?
            new SynchronizedRandomAccessList<>(list) :
            new SynchronizedList<>(list));
}

1.4 CopyOnWriteArrayList(重点)

首先我们对CopyOnWriteArrayList进行学习,其特点如下:

它相当于线程安全的ArrayList。和ArrayList一样,它是个可变数组;但是和ArrayList不同的时,它具有以下特性:

  1. 它最适合于具有以下特征的应用程序:List 大小通常保持很小,只读操作远多于可变操作,需要在遍历期间防止线程间的冲突。
  2. 它是线程安全的。
  3. 因为通常需要复制整个基础数组,所以可变操作(add()、set() 和 remove() 等等)的开销很大。
  4. 迭代器支持hasNext(), next()等不可变操作,但不支持可变 remove()等操作。
  5. 使用迭代器进行遍历的速度很快,并且不会与其他线程发生冲突。在构造迭代器时,迭代器依赖于不变的数组快照。

1. 独占锁效率低:采用读写分离思想解决

2. 写线程获取到锁,其他写线程阻塞

3. 复制思想:

当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行 Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。

这时候会抛出来一个新的问题,也就是数据不一致的问题。如果写线程还没来得及写会内存,其他的线程就会读到了脏数据。

这就是CopyOnWriteArrayList 的思想和原理。就是拷贝一份。

NotSafeDemo代码修改

 	// CopyOnWriteArrayList线程安全,写时先复制一个副本,在副本中操作完,使用副本替换原数组 不影响其他线程读取原数组的数组
    public static void main(String[] args) {
        List list = new CopyOnWriteArrayList();
        for (int i = 0;i < 100;i++){
            new Thread(() -> {
                list.add(UUID.randomUUID().toString());
                System.out.println(list);
            }).start();
        }
    }

没有线程安全问题

方法源码

    /**
     * Appends the specified element to the end of this list.
     *
     * @param e element to be appended to this list
     * @return {@code true} (as specified by {@link Collection#add})
     */
    public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }

原因分析(重点):动态数组与线程安全

下面从“动态数组”和“线程安全”两个方面进一步对CopyOnWriteArrayList的原理进行说明。

  • “动态数组”机制

    • 它内部有个“volatile数组”(array)来保持数据。在“添加/修改/删除”数据时,都会新建一个数组,并将更新后的数据拷贝到新建的数组中,最后再将该数组赋值给“volatile数组”, 这就是它叫做CopyOnWriteArrayList的原因
    • 由于它在“添加/修改/删除”数据时,都会新建数组,所以涉及到修改数据的操作,CopyOnWriteArrayList效率很低;但是单单只是进行遍历查找的话,效率比较高。
  • “线程安全”机制

    • 通过volatile和互斥锁来实现的。
    • 通过“volatile数组”来保存数据的。一个线程读取volatile数组时,总能看到其它线程对该volatile变量最后的写入;就这样,通过volatile提供了“读取到的数据总是最新的”这个机制的保证。
    • 通过互斥锁来保护数据。在“添加/修改/删除”数据时,会先“获取互斥锁”,再修改完毕之后,先将数据更新到“volatile数组”中,然后再“释放互斥锁”,就达到了保护数据的目的。

1.5 小结(重点)

1.线程安全与线程不安全集合

集合类型中存在线程安全与线程不安全的两种,常见例如:

ArrayList ----- Vector

HashMap -----HashTable

但是以上都是通过synchronized关键字实现,效率较低

2.Collections构建的线程安全集合
在这里插入图片描述

3.java.util.concurrent并发包下

CopyOnWriteArrayList CopyOnWriteArraySet类型,通过动态数组与线程安全个方面保证线程安全

2 Callable&Future接口

2.1 Callable接口

目前我们学习了有两种创建线程的方法-一种是通过创建Thread类,另一种是通过使用Runnable创建线程。但是,Runnable缺少的一项功能是,当线程终止时(即run()完成时),我们无法使线程返回结果。为了支持此功能,Java中提供了Callable接口。

Callable接口的特点如下(重点)

  • 为了实现Runnable,需要实现不返回任何内容的run()方法,而对于Callable,需要实现在完成时返回结果的call()方法。
  • call()方法可以引发异常,而run()则不能。
  • 为实现Callable而必须重写call方法
  • 不能直接替换runnable,因为Thread类的构造方法根本没有Callable
创建新类MyThread实现runnable接口
class MyThread implements Runnable{
 @Override
 public void run() {
 
 }
}
新类MyThread2实现callable接口
class MyThread2 implements Callable<Integer>{
 @Override
 public Integer call() throws Exception {
  return 200;
 } 
}

2.2 Future接口

当call()方法完成时,结果必须存储在主线程已知的对象中,以便主线程可以知道该线程返回的结果。为此,可以使用Future对象。

将Future视为保存结果的对象–它可能暂时不保存结果,但将来会保存(一旦Callable返回)。Future基本上是主线程可以跟踪进度以及其他线程的结果的一种方式。要实现此接口,必须重写5种方法,这里列出了重要的方法,如下:

  • **public boolean cancel(boolean mayInterrupt):**用于停止任务。

    如果尚未启动,它将停止任务。如果已启动,则仅在mayInterrupt为true时才会中断任务。

  • **public Object get()抛出InterruptedException,ExecutionException:**用于获取任务的结果。

    如果任务完成,它将立即返回结果,否则将等待任务完成,然后返回结果。

  • **public boolean isDone():**如果任务完成,则返回true,否则返回false

可以看到Callable和Future做两件事-Callable与Runnable类似,因为它封装了要在另一个线程上运行的任务,而Future用于存储从另一个线程获得的结果。实际上,future也可以与Runnable一起使用。

要创建线程,需要Runnable。为了获得结果,需要future。

2.4 FutureTask

Java库具有具体的FutureTask类型,该类型实现Runnable和Future,并方便地将两种功能组合在一起。
可以通过为其构造函数提供Callable来创建FutureTask。然后,将FutureTask对象提供给Thread的构造函数以创建Thread对象。因此,间接地使用Callable创建线程。

核心原理:(重点)

  • 在主线程中需要执行比较耗时的操作时,但又不想阻塞主线程时,可以把这些作业交给Future对象在后台完成
  • 当主线程将来需要时,就可以通过Future对象获得后台作业的计算结果或者执行状态
  • 一般FutureTask多用于耗时的计算,主线程可以在完成自己的任务后,再去获取结果。
  • 仅在计算完成时才能检索结果;如果计算尚未完成,则阻塞 get 方法
  • 一旦计算完成,就不能再重新开始或取消计算
  • get方法而获取结果只有在计算完成时获取,否则会一直阻塞直到任务转入完成状态,然后会返回结果或者抛出异常
  • get只计算一次,因此get方法放到最后

2.5 使用Callable和Future

CallableDemo案例

 public static void main(String[] args) throws Exception{
        System.out.println(Thread.currentThread().getName() + "开始干活");
        FutureTask<String > futureTask1 = new FutureTask<>(() -> {
            System.out.println(Thread.currentThread().getName() + "执行了runable");
        }, "abc");

//        FutureTask<String> futureTask = new FutureTask<String>(() -> {
//            System.out.println(Thread.currentThread().getName() + ",子任务开始干活");
//            Thread.sleep(5000);
//            return "123";
//        });

        // 子线程是由主线程创建的
        new Thread(futureTask1).start();;
        // 阻塞调用线程 必须等获取到返回值才能往下执行
        System.out.println(futureTask1.get());

        System.out.println(Thread.currentThread().getName() + "干活结束");
    }

2.6 小结(重点)

  • 在主线程中需要执行比较耗时的操作时,但又不想阻塞主线程时,可以把这些作业交给Future对象在后台完成, 当主线程将来需要时,就可以通过Future对象获得后台作业的计算结果或者执行状态

  • 一般FutureTask多用于耗时的计算,主线程可以在完成自己的任务后,再去获取结果

  • 仅在计算完成时才能检索结果;如果计算尚未完成,则阻塞 get 方法。一旦计算完成,就不能再重新开始或取消计算。get方法而获取结果只有在计算完成时获取,否则会一直阻塞直到任务转入完成状态,然后会返回结果或者抛出异常。

  • 只计算一次

3 JUC三大辅助类

JUC中提供了三种常用的辅助类,通过这些辅助类可以很好的解决线程数量过多时Lock锁的频繁操作。这三种辅助类为:

  • CountDownLatch: 减少计数
  • CyclicBarrier: 循环栅栏
  • Semaphore: 信号灯

下面我们分别进行详细的介绍和学习

3.1 减少计数CountDownLatch

CountDownLatch类可以设置一个计数器,然后通过countDown方法来进行减1的操作,使用await方法等待计数器不大于0,然后继续执行await方法之后的语句。

  • CountDownLatch主要有两个方法,当一个或多个线程调用await方法时,这些线程会阻塞
  • 其它线程调用countDown方法会将计数器减1(调用countDown方法的线程不会阻塞)
  • 当计数器的值变为0时,因await方法阻塞的线程会被唤醒,继续执行

场景: 6个同学陆续离开教室后值班同学才可以关门。

CountDownLatchDemo

  /**
     * CountDownLatch:减少计数
     * 6个同学陆续离开教室后值班同学才可以关门
     * @param args
     */
    public static void main(String[] args) throws InterruptedException {
        // 定义一个数值为6的计数器
        CountDownLatch countDownLatch = new CountDownLatch(6);

        // 创建6个同学
        for (int i = 1;i <= 6;i++){
            new Thread(() -> {
                try {
                    if (Thread.currentThread().getName().equals("同学6")){
                        Thread.sleep(2000);
                    }
                    System.out.println(Thread.currentThread().getName() + "离开了");
                    // 计数器减1 不会阻塞
                    countDownLatch.countDown();

                }catch (Exception e){
                    e.printStackTrace();
                }
            }, "同学" + i).start();
        }

        // 主线程await休息
        System.out.println("主线程睡觉");
        // 等全部同学离开关门
        countDownLatch.await();
        // 全部离开后自动唤醒主线程
        System.out.println("全部离开了,现在的计数器为:" +countDownLatch.getCount());

    }

3.2 循环栅栏CyclicBarrier

CyclicBarrier看英文单词可以看出大概就是循环阻塞的意思,在使用中CyclicBarrier的构造方法第一个参数是目标障碍数,每次执行CyclicBarrier一次障碍数会加一,如果达到了目标障碍数,才会执行cyclicBarrier.await()之后的语句。可以将CyclicBarrier理解为加1操作

场景: 集齐7颗龙珠就可以召唤神龙

CyclicBarrierDemo

    /**
     * CyclicBarrier:循环栅栏
     * 场景:机器7颗龙珠召唤神龙
     * @param args
     */
    public static void main(String[] args) {
        // 定义神龙召唤需要的龙珠总数
        CyclicBarrier cyclicBarrier = new CyclicBarrier(7);
        // 定义7个线程分别去收集龙珠
        for (int i = 1;i <= 7;i++){
            new Thread(() -> {
                try {
                    if (Thread.currentThread().getName().equals("龙珠3号")){
                        System.out.println("龙珠3号抢夺战开始,孙悟空开启超级赛亚人模式!!!");
                        Thread.sleep(5000);
                        System.out.println("龙珠3号抢夺战结束,孙悟空打赢了,拿到了龙珠3号");
                    }else{
                        System.out.println(Thread.currentThread().getName() + "收集到了!!!");
                    }
                    // 每收集到1个 栅栏+1
                    cyclicBarrier.await();
                }catch (Exception e){
                    e.printStackTrace();
                }

            }, "龙珠" + i + "号").start();
        }
    }

3.3 信号灯Semaphore

Semaphore的构造方法中传入的第一个参数是最大信号量(可以看成最大线程池),每个信号量初始化为一个最多只能分发一个许可证。使用acquire方法获得许可证,release方法释放许可

场景: 抢车位, 10部汽车3个停车位

SemaphoreDemo

    /**
     * 信号灯:Semaphore
     * 场景:抢车位:6部汽车3个停车位
     * @param args
     */
    public static void main(String[] args) throws InterruptedException {
        Semaphore semaphore = new Semaphore(3);
        // 模拟10辆汽车停车
        for (int i = 1;i <= 10;i++){
            Thread.sleep(100);
            // 停车
            new Thread( () -> {
                try {
                    System.out.println(Thread.currentThread().getName() + "找车位ing");
                    // 抢车位
                    semaphore.acquire();
                    // 所有抢到车位的都停3秒
                    System.out.println(Thread.currentThread().getName() + "停车成功!!");
                    Thread.sleep(3000);
                }catch (Exception e){
                    e.printStackTrace();
                }finally {
                    System.out.println(Thread.currentThread().getName() + "溜了溜了");
                    // 释放车位
                    semaphore.release();
                }
            }, "汽车" + i).start();
        }

    }

4 读写锁(重点重点重点)

4.1 读写锁介绍

现实中有这样一种场景:对共享资源有读和写的操作,且写操作没有读操作那么频繁。在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以应该允许多个线程同时读取共享资源;但是如果一个线程想去写这些共享资源,就不应该允许其他线程对该资源进行读和写的操作了。

针对这种场景,JAVA的并发包提供了读写锁ReentrantReadWriteLock,它表示两个锁,一个是读操作相关的锁,称为共享锁;一个是写相关的锁,称为排他锁

  1. 线程进入读锁的前提条件:
  • 没有其他线程的写锁

  • 没有写请求, 或者有写请求,但调用线程和持有锁的线程是同一个(可重入锁)。

  1. 线程进入写锁的前提条件:
  • 没有其他线程的读锁

  • 没有其他线程的写锁

而读写锁有以下三个重要的特性:

(1)公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平。

(2)重进入:读锁和写锁都支持线程重进入。

(3)锁降级:遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁。

4.2 ReentrantReadWriteLock

ReentrantReadWriteLock 类的整体结构

public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {

    /** 读锁 */
    private final ReentrantReadWriteLock.ReadLock readerLock;

    /** 写锁 */
    private final ReentrantReadWriteLock.WriteLock writerLock;

    final Sync sync;
    
    /** 使用默认(非公平)的排序属性创建一个新的 ReentrantReadWriteLock */
    public ReentrantReadWriteLock() {
        this(false);
    }

    /** 使用给定的公平策略创建一个新的 ReentrantReadWriteLock */
    public ReentrantReadWriteLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
        readerLock = new ReadLock(this);
        writerLock = new WriteLock(this);
    }

    /** 返回用于写入操作的锁 */
    public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
    
    /** 返回用于读取操作的锁 */
    public ReentrantReadWriteLock.ReadLock  readLock()  { return readerLock; }


    abstract static class Sync extends AbstractQueuedSynchronizer {}

    static final class NonfairSync extends Sync {}

    static final class FairSync extends Sync {}

    public static class ReadLock implements Lock, java.io.Serializable {}

    public static class WriteLock implements Lock, java.io.Serializable {}
}

可以看到,ReentrantReadWriteLock实现了ReadWriteLock接口,ReadWriteLock接口定义了获取读锁和写锁的规范,具体需要实现类去实现;同时其还实现了Serializable接口,表示可以进行序列化,在源代码中可以看到ReentrantReadWriteLock实现了自己的序列化逻辑。

ReentrantReadWriteLock有五个内部类,五个内部类之间也是相互关联的。内部类的关系如下图所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mW9iXf2x-1677056773217)(assets/image-20230217204330949.png)]

  • Sync继承自AQS、NonfairSync继承自Sync类、FairSync继承自Sync类
  • ReadLock实现了Lock接口、WriteLock也实现了Lock接口

4.3 入门案例

场景: 使用ReentrantReadWriteLock 对一个hashmap进行读和写操作

思考:

  • 场景一: 多个线程同时获取读锁,结果如何?
  • 场景二: 多线程同时获取写锁, 结果如何?
  • 场景三:同一个线程先获取读锁后再去获取写锁,结果如何
  • 场景四:同一个线程先获取写锁后再去获取读锁,结果如何
  • 场景五: 一个线程先获取读锁后其他线程获取写锁,结果如何?
  • 场景六: 一个线程获取写锁后其他线程获取读锁,结果如何?
  • 场景七: 同一个线程获取读锁后再去获取写锁,结果如何?
  • 场景八: 同一个线程获取写锁后再去获取读锁,结果如何?

接下来我们一一进行实战验证

4.3.1 场景一 多个线程之间同时加读锁—> 成功—> 共享锁!

package com.atguigu;

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class Test {
    // 定义一把读写锁
    ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    public void read(){
        // 获取读锁
        ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
        try {
            // 加锁
            readLock.lock();
            // 删除
            System.out.println(Thread.currentThread().getName() + "加读锁成功!");
            Thread.sleep(3000);
        }catch (Exception e){

        }finally {
            System.out.println("读锁释放");
            // 释放锁
            readLock.unlock();
        }
    }


    /**
     * 场景一:多个线程之间同时加读锁---> 成功! ---> 共享锁!
     */
    public static void main(String[] args) {
        // 初始化对象
        Test test = new Test();
        new Thread(() ->{
            test.read();
        }, "读线程1").start();
        new Thread(() ->{
            test.read();
        }, "读线程2").start();
    }
}

结论: 多个线程可以同时获取读锁

4.3.2 场景二 多个线程之间同时加写锁—>排他—>独占锁

package com.atguigu;

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class Test1 {

    // 定义一把读写锁
    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    public void write(){
        // 获取写锁
        ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
        try {
            // 加锁
            writeLock.lock();
            //输出
            System.out.println(Thread.currentThread().getName() + "加写锁成功");
            Thread.sleep(3000);
        }catch (Exception e){

        }finally {
            System.out.println(Thread.currentThread().getName() + "释放");
            // 释放锁
            writeLock.unlock();
        }
    }

    /**
     * 场景二:多个线程之间同时加写锁--->排他--->独占锁
     * @param args
     */
    public static void main(String[] args) {
        // 初始化对象
        Test1 test1 = new Test1();
        new Thread(() -> {
            test1.write();
        }, "写线程1").start();
        new Thread(() -> {
            test1.write();
        }, "写线程2").start();
    }

}

结论: 多个线程同时获取写锁,同一时间只有一个线程能获取到写锁

4.3.3 场景三 同一个线程先读后写—>失败–>加了读锁不饿能加写锁 进程会卡住

package com.atguigu;

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class Test2 {

    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    /**
     * 先读后写
     */
    public void readAndWrite(){
        // 获取读锁
        ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
        // 获取写锁
        ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
        try {
            // 加读锁
            readLock.lock();
            // 输出
            System.out.println(Thread.currentThread().getName() + "加读锁成功");
            // 加写锁
            writeLock.lock();
            // 输出
            System.out.println(Thread.currentThread().getName() + "加写锁成功");
            Thread.sleep(3000);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            // 释放锁
            readLock.unlock();
            System.out.println("读锁释放");
            writeLock.unlock();
            System.out.println("写锁释放");
        }
    }

    /**
     * 场景三:同一个线程先读后写--->失败-->加了读锁不饿能加写锁 进程会卡住
     * @param args
     */
    public static void main(String[] args) {
        // 创建对象
        Test2 test2 = new Test2();
        new Thread(() -> {
            test2.readAndWrite();
        }).start();
    }
}

结论: 当对象具有读锁时,不能再获取写锁,进程会卡住

4.3.4 场景四 同一个线程先写后读---->成功—>加了写锁可以再加读锁

package com.atguigu;

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class Test3 {
    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    /**
     * 先写后读
     */
    public void WriteAndRead(){
        // 获取写锁
        ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
        // 获取读锁
        ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
        try {
            // 加写锁
            writeLock.lock();
            // 输出
            System.out.println(Thread.currentThread().getName() + "加写锁成功");
            // 加读锁
            readLock.lock();
            // 输出
            System.out.println(Thread.currentThread().getName() + "加读锁成功");
            Thread.sleep(3000);
        }catch (Exception e){

        }finally {
            // 释放锁
            writeLock.unlock();
            System.out.println("写锁释放");
            readLock.unlock();
            System.out.println("读锁释放");

        }
    }

    /**
     * 场景四:同一个线程先写后读---->成功--->加了写锁可以再加读锁
     * @param args
     */
    public static void main(String[] args) {
        // 初始化对象
        Test3 test3 = new Test3();
        new Thread(() -> {
            test3.WriteAndRead();
        },"线程A").start();
    }
}

4.3.5 场景五:不同线程,先读后写—>失败—>不同的线程之间读锁和写锁是互斥的

package com.atguigu;

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class Test4 {
    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    public void readAndWrite(){
        // 获取读锁
        ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
        // 获取写锁
        ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
        try {
            // 加读锁
            readLock.lock();
            // 输出
            System.out.println(Thread.currentThread().getName() + "加读锁成功");
            // 加写锁
            writeLock.lock();
            // 输出
            System.out.println(Thread.currentThread().getName() + "加写锁成功");
            Thread.sleep(3000);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            // 释放读锁
            readLock.unlock();
            System.out.println("释放读锁");
            // 释放写锁
            writeLock.unlock();
            System.out.println("释放写锁");
        }
    }

    /**
     * 场景五:不同线程,先读后写--->失败--->不同的线程之间读锁和写锁是互斥的
     * @param args
     */
    public static void main(String[] args) {
        // 初始化对象
        Test4 test4 = new Test4();
        new Thread(() -> {
            test4.readAndWrite();
        }, "线程A").start();
        new Thread(() -> {
            test4.readAndWrite();
        }, "线程B").start();
    }
}

4.3.6 场景六:不同线程,先写后读—>失败—>不同的线程之间读锁和写锁是互斥的

package com.atguigu;

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class Test5 {

    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    public void writeAndRead(){
        // 获取写锁
        ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
        // 获取读锁
        ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
        try{
            // 加写锁
            writeLock.lock();
            // 输出
            System.out.println(Thread.currentThread().getName() + "加写锁");
            // 加读锁
            readLock.lock();
            // 输出
            System.out.println(Thread.currentThread().getName() + "加读锁");
            Thread.sleep(3000);
        }catch (Exception e){

        }finally {
            // 释放写锁
            writeLock.unlock();
            System.out.println("释放写锁");
            // 释放读锁
            readLock.unlock();
            System.out.println("释放读锁");
        }
    }

    /**
     * 场景六:不同线程,先写后读--->失败--->不同的线程之间读锁和写锁是互斥的
     * @param args
     */
    public static void main(String[] args) {
        // 初始化对象
        Test5 test5 = new Test5();
        new Thread(() -> {
            test5.writeAndRead();
        }, "线程A").start();

        new Thread(() -> {
            test5.writeAndRead();
        }, "线程B").start();
    }
}

4.3.7 场景七:同一个线程先读再读—>成功—>读锁可重入

package com.atguigu;

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class Test6 {
    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    /**
     * 先读后读
     */
    public void readAndRead(){
        // 获取读锁
        ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
        try {
            // 加锁一次
            readLock.lock();
            System.out.println("第一次成功!");
            // 加锁两次
            readLock.lock();
            System.out.println("第二次成功!");
            // 输出
            System.out.println(Thread.currentThread().getName() + "加读锁成功!");
            Thread.sleep(3000);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            readLock.unlock();
            System.out.println("读锁释放一次");
            readLock.unlock();
            System.out.println("读锁释放两次");

        }
    }

    /**
     * 场景七:同一个线程先读再读--->成功--->读锁可重入
     * @param args
     */
    public static void main(String[] args) {
        // 初始化对象
        Test6 test6 = new Test6();
        new Thread(() -> {
            test6.readAndRead();
        }, "读线程").start();
    }
}

场景八:同一个线程先写再写—>成功—>写锁可重入

package com.atguigu;

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class Test7 {
    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    /**
     * 先写后写
     */
    public void writeAndWrite(){
        // 获取写锁
        ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
        try {
            // 加锁一次
            writeLock.lock();
            // 输出
            System.out.println("加写锁一次");
            // 加锁两次
            writeLock.lock();
            // 输出
            System.out.println("加写锁两次");
            System.out.println(Thread.currentThread().getName() + "加锁成功");
            Thread.sleep(3000);
        }catch (Exception e){

        }finally {
            // 释放锁
            writeLock.unlock();
            System.out.println("释放写锁一次");
            writeLock.unlock();
            System.out.println("释放写锁两次");
        }
    }

    /**
     * 场景八:同一个线程先写再写--->成功--->写锁可重入
     * @param args
     */
    public static void main(String[] args) {
        // 初始化对象
        Test7 test7 = new Test7();
        new Thread(() -> {
            test7.writeAndWrite();
        }, "写线程").start();
    }
}

4.4 AQS(重点)

4.4.1 AQS简介

AbstractQueuedSynchronizer抽象的队列式的同步器, AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它

它维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。这里volatile是核心关键词,具体volatile的语义,在此不述。state的访问方式有三种:

  • getState()
  • setState()
  • compareAndSetState()

AQS定义两种资源共享方式:Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)。

4.4.2 AQS核心方法介绍

不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法:

  • isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
  • tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
  • tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
  • tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
  • tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。

4.4.3 基于AQS的实际案列

以ReentrantLock为例:

  • state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。
  • 其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。
  • 释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。

再以CountDownLatch以例:

  • 任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。
  • 这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS减1。
  • 等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。

一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。

4.5 CAS

CAS是英文单词Compare And Swap的缩写,翻译过来就是比较并替换。

CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。

更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。

CAS是英文单词Compare And Swap的缩写,翻译过来就是比较并替换。

CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。

更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。

1.在内存地址V当中,存储着值为10的变量
在这里插入图片描述

2.此时线程1想要把变量的值增加1。对线程1来说,旧的预期值A=10,要修改的新值B=11
在这里插入图片描述

3.在线程1要提交更新之前,另一个线程2抢先一步,把内存地址V中的变量值率先更新成了11
在这里插入图片描述

4.线程1开始提交更新,首先进行A和地址V的实际值比较(Compare),发现A不等于V的实际值,提交失败
在这里插入图片描述

5.线程1重新获取内存地址V的当前值,并重新计算想要修改的新值。此时对线程1来说,A=11,B=12。这个重新尝试的过程被称为自旋

在这里插入图片描述

6.这一次比较幸运,没有其他线程改变地址V的值。线程1进行Compare,发现A和地址V的实际值是相等的
在这里插入图片描述

7.线程1进行SWAP,把地址V的值替换为B,也就是12
在这里插入图片描述

缺点:

1.CPU开销较大

2.不能保证代码块的原子性

应用场景

  • ConcurrentHashMap的put中key或者头节点不存在时用到
    在这里插入图片描述

    • AQS的实现类ReentrantReadWriteLock 中加锁释放锁中用到
      在这里插入图片描述
  • Unsafe中的getAndAddInt方法中有用到
    在这里插入图片描述

4.6 小结(重要)

  • 在线程持有读锁的情况下,该线程不能取得写锁(因为获取写锁的时候,如果发现当前的读锁被占用,就马上获取失败,不管读锁是不是被当前线程持有)。

  • 在线程持有写锁的情况下,该线程可以继续获取读锁(获取读锁时如果发现写锁被占用,只有写锁没有被当前线程占用的情况才会获取失败)。

原因: 当线程获取读锁的时候,可能有其他线程同时也在持有读锁,因此不能把获取读锁的线程“升级”为写锁;而对于获得写锁的线程,它一定独占了读写锁,因此可以继续让它获取读锁,当它同时获取了写锁和读锁后,还可以先释放写锁继续持有读锁,这样一个写锁就“降级”为了读锁。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值