JUC-day02

news2024/10/5 17:10:43

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 小结(重要)

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

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/363340.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Java集合(二)---Map

1.什么是Hash算法哈希算法是指把任意长度的二进制映射为固定长度的较小的二进制值&#xff0c;这个较小的二进制值叫做哈希值static final int hash(Object key) {int h;return (key null) ? 0 : (h key.hashCode()) ^ (h >>> 16);}以上是HashMap中的hash算法代码2…

三天吃透Spring面试八股文(最新整理)

本文已经收录到Github仓库&#xff0c;该仓库包含计算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享等核心知识点&#xff0c;欢迎star~ Github地址&#xff1a;https://github.com/…

【音视频处理】为什么MP3不是无损音乐?音频参数详解,码率、采样率、音频帧、位深度、声道、编码格式的关系

大家好&#xff0c;欢迎来到停止重构的频道。上期我们讨论了视频的相关概念&#xff0c;本期我们讨论音频的相关概念。包括采样率、码率、单双声道、音频帧、编码格式等概念。这里先抛出一个关于无损音频的问题。为什么48KHz采样率的.mp3不是无损音乐 &#xff0c;而48KHz采样率…

95后刚毕业2、3年就年薪50W,才发现,打败我们的不是年龄····

一刷朋友圈&#xff0c;一读公众号&#xff0c;一打开微博&#xff0c;甚至是一和朋友聊天&#xff0c;这些让人焦虑的话题总会铺天盖地的袭来&#xff1a; Ta刚毕业半年&#xff0c;就升职加薪当上了测试主管 &#xff08;同样是一天24小时&#xff0c;为什么同龄人正在抛弃…

【Linux】编辑器——vim(最小集+指令集+自动化配置)

目录 1.vim最小集 1.1 vim的三种模式 1.2 vim的基本操作 2.vim指令集 2.1 命令模式指令集 移动光标 删除文字 复制 替换 撤销上一次操作 更改 跳至指定的行 2.2 底行模式指令集 列出行号 跳到文件中的某一行 查找字符 保存文件 多文件操作 3.如何配置vim 配…

网络编程之TCP 的介绍

TCP 的介绍学习目标能够说出TCP 的特点1. 网络应用程序之间的通信流程之前我们学习了 IP 地址和端口号&#xff0c;通过 IP 地址能够找到对应的设备&#xff0c;然后再通过端口号找到对应的端口&#xff0c;再通过端口把数据传输给应用程序&#xff0c;这里要注意&#xff0c;数…

科技 “新贵”ChatGPT 缘何 “昙花一现” ,仅低代码风靡至今

恍惚之间&#xff0c;ChatGPT红遍全网&#xff0c;元宇宙沉入深海…… 在科技圈&#xff0c;见证了太多“昙花一现”&#xff0c;“新贵” ChatGPT 的爆火几乎复制了元宇宙的路径&#xff0c;它会步元宇宙的后尘&#xff0c;成为下一个沉入深海的工具吗&#xff1f; 不可否认的…

小程序开发注意点

1.组件样式隔离注意点 2.methods方法 3.自定义组件的properties参数 4.自定义组件的事件监听 5.纯数据字段 6.插槽 单个插槽 启用多插槽 使用多个插槽 7.属性绑定实现父传子功能 例如在这里有一个组件为<one></one>&#xff0c;那么可以在组件当中传入参数 &l…

iOS上架及证书最新创建流程

目前使用uniapp框架开发app&#xff0c;大大节省了我们兼容多端应用的工作量和人手&#xff0c;所以目前非常缺乏ios上架和证书创建流程流程的文档假如你没有任何的打包或上架经验&#xff0c;参考本文有很大的收益。通常申请ios证书和上架ipa应用&#xff0c;是需要MAC电脑的&…

干货复试详细教程——从联系导师→自我介绍的复试教程

文章目录联系导师联系之前的准备联系导师注意自我介绍教育技术领域通用的复试准备其他补充联系导师 确定出分和自己能进复试以后联系。 分两类 科研技能型 低调&#xff0c;如实介绍&#xff0c;不吹不水。就算你很牛啥都会手握核心期刊论文也不太狂 学霸高分型 不要自卑&…

审计syslog设备活动

从交换机到路由器&#xff0c;几乎所有网络设备都会生成syslog。因为您的网络中有大量生成syslog的设备&#xff0c;所以审计过程&#xff08;包括跟踪、监控和分析所有syslog&#xff09;需要花费大量时间和精力。但是&#xff0c;无论这些任务需要多少精力去完成&#xff0c;…

Java:Java与Python — 编码大战

Java和Python是目前市场上最热门的两种编程语言&#xff0c;因为它们具有通用性、高效性和自动化能力。两种语言都有各自的优点和缺点&#xff0c;但主要区别在于Java 是静态类型的&#xff0c;Python是动态类型的。它们有相似之处&#xff0c;因为它们都采用了“一切都是对象”…

3、Maven安装

前言&#xff1a;工具下载地址阿里云盘&#xff1a;Maven&#xff1a;https://www.aliyundrive.com/s/SgHKjQ5doSp提取码: ml40一、什么是maven?Apache Maven是个项目管理和自动构建工具&#xff0c;基于项目对象模型&#xff08;POM&#xff09;的概念。作用&#xff1a;完成…

小白都能看懂的C语言入门教程

文章目录C语言入门教程1. 第一个C语言程序HelloWorld2. C语言的数据类型3. 常量变量的使用4. 自定义标识符#define5. 枚举的使用6. 字符串和转义字符7. 判断和循环8. 函数9. 数组的使用10. 操作符的使用11. 结构体12. 指针的简单使用C语言入门教程 1. 第一个C语言程序HelloWor…

Could not find resource jdbc.properties问题的解决

以如下开头的内容&#xff1a; Exception in thread "main" org.apache.ibatis.exceptions.PersistenceException: ### Error building SqlSession. ### The error may exist in SQL Mapper Configuration 出现以上问题是没有在src/main/resources下创建jdbc.prop…

IDEA怎么自动生成serialVersionUID

序列化和反序列化Java是面向对象的语言&#xff0c;与其他语言进行交互&#xff08;比如与前端js进行http通信&#xff09;&#xff0c;需要把对象转化成一种通用的格式比如json&#xff08;前端显然不认识Java对象&#xff09;&#xff0c;从对象到json字符串的转换&#xff0…

CF707C Pythagorean Triples 题解

CF707C Pythagorean Triples 题解题目链接字面描述题面翻译题目描述输入格式输出格式样例 #1样例输入 #1样例输出 #1样例 #2样例输入 #2样例输出 #2样例 #3样例输入 #3样例输出 #3样例 #4样例输入 #4样例输出 #4样例 #5样例输入 #5样例输出 #5提示思路代码实现题目 链接 http…

深度卷积对抗神经网络 进阶 第一部分 GANs 在数据增强和隐私领域的应用 GANs for Data Augmentation and Privacy

深度卷积对抗神经网络 进阶 第一部分 GANs 在数据增强和隐私领域的应用 GANs for Data Augmentation and Privacy GANs可以创造性地生成数据&#xff0c;这样就可以用在数据增强领域&#xff0c;在某些缺乏数据的行业认为地添加数据。此外&#xff0c;生成的数据如果用于神经网…

365天深度学习训练营-第J3周:DenseNet算法实战与解析

目录 一、前言 二、论文解读 1、DenseNet的优势 2、设计理念 3、网络结构 4、与其他算法进行对比 三、代码复现 1、使用Pytorch实现DenseNet 2、使用Tensorflow实现DenseNet网络 四、分析总结 一、前言 &#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习…

如何使用 ESP-PROG 板的 Program 接口为 ESP32-S3-WROOM-1 系列的模组烧录固件?

ESP-PROG 是一款乐鑫推出的开发调试工具&#xff0c;具有自动下载固件、串口通信、JTAG 在线调试等功能。具体使用说明参见&#xff1a;ESP-Prog 下载与调试板介绍 。 ESP-Prog 采用 FTDI 公司的 FT2232HL 为 USB Bridge Controller 芯片&#xff0c;可通过配置将 USB 2.0 接口…