JUC学习

1.什么是JUC

JUC即 java.util.concurrent 工具包的简称,用来进行基于多线程的开发,在实现多线程中使用JUC可以帮助我们更简单地实现更多功能。

2.JUC的功能介绍

2.1 从基础的等待唤醒机制到JUC

基础的等待唤醒机制使用同步代码块或同步方法加锁,通过锁对象调用wait使线程进入等待状态,通过锁对象调用notufy唤醒等待的线程。而在JUC中,加锁使用Lock手动加锁,通过使用Condition对象调用await使线程进入等待状态,通过使用Condition对象调用signal唤醒等待的线程,也就是:

java 复制代码
public class Example {
    //创建锁对象
    Lock lock=new ReentrantLock();
    //创建Condition监视器
    Condition condition=lock.newCondition();
    public void method(){
        //加锁
        lock.lock();
        try {
            if(等待条件)
              condition.await();
            else{
                业务代码;
                condition.signalAll();
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            lock.unlock();
        }
    }
}

2.2 Condition

为什么要用condition监视器来调用await或signalAll,而不使用锁对象调用wait或notifyAll呢?

先来看一个案例:有四个线程A、B、C、D,让这四个线程按照顺序依次执行两次。

首先使用基础的等待唤醒机制来实现:

java 复制代码
//资源类
public class Example1 {
    //标志位,初始值设为1,让A先执行
    private int flag=1;

    public synchronized void A(){
        for(int i=0;i<2;i++) {
                //当标志位为1时A执行
                //使用循环是为了防止在等待的过程中其他线程对flag进行了修改
                //在被唤醒时重新判断等待条件,防止虚假唤醒
                while (flag != 1) {
                    this.wait();
                }
                System.out.println("线程A正在运行中...");
                flag = 2;
                this.notifyAll();
        }
    }

    public synchronized void B(){
        for(int i=0;i<2;i++) {
            //当标志位为2时B执行
            while (flag!=2)
                this.wait();
            System.out.println("线程B正在运行中...");
            flag=3;
            this.notifyAll();
        }
    }

    public synchronized void C(){
        for(int i=0;i<2;i++) {
            //标志位为3时C执行
            while (flag!=3)
                this.wait();
            System.out.println("线程C正在运行中...");
            flag=4;
            this.notifyAll();
        }
    }

    public synchronized void D(){
        for(int i=0;i<2;i++) {
                //标志位为4时D执行
                while (flag != 4)
                    this.wait();
                System.out.println("线程D正在运行中...");
                flag = 1;
                this.notifyAll();
        }
    }
}

//实现
public class Main {
    public static void main(String[] args) {
        //创建资源类对象
        Example1 example1=new Example1();

        //创建A、B、C、D四个线程
        Thread ta=new Thread(()->{
                example1.A();
        });
        Thread tb=new Thread(()->{
                example1.B();
        });
        Thread tc=new Thread(()->{
                example1.C();
        });
        Thread td=new Thread(()->{
                example1.D();
        });

        //开启四个线程
        ta.start();
        tb.start();
        tc.start();
        td.start();
    }
}

然后使用JUC来实现:

java 复制代码
//资源类
public class Example {
    //标志
    private int flag=1;
    //创建锁对象
    Lock lock=new ReentrantLock();
    //创建Condition对象
    Condition condition1=lock.newCondition();
    Condition condition2=lock.newCondition();
    Condition condition3=lock.newCondition();
    Condition condition4=lock.newCondition();

    public void A(){
        for(int i=0;i<2;i++) {
            //加锁
            lock.lock();
            try {
                while (flag != 1)
                    condition1.await();
                System.out.println("线程A正在运行中...");
                flag = 2;
                condition2.signalAll();
            } catch (Exception e) {
                throw new RuntimeException(e);
            } finally {
                lock.unlock();
            }
        }
    }
    public void B(){
        for(int i=0;i<2;i++) {
            //加锁
            lock.lock();
            try {
                while (flag != 2)
                    condition2.await();
                System.out.println("线程B正在运行中...");
                flag = 3;
                condition3.signalAll();
            } catch (Exception e) {
                throw new RuntimeException(e);
            } finally {
                lock.unlock();
            }
        }
    }
    public void C(){
        for(int i=0;i<2;i++) {
            //加锁
            lock.lock();
            try {
                while (flag != 3)
                    condition3.await();
                System.out.println("线程C正在运行中...");
                flag = 4;
                condition4.signalAll();
            } catch (Exception e) {
                throw new RuntimeException(e);
            } finally {
                lock.unlock();
            }
        }
    }
    public void D() {
        for (int i = 0; i < 2; i++) {
            //加锁
            lock.lock();
            try {
                while (flag != 4)
                    condition4.await();
                System.out.println("线程D正在运行中...");
                flag = 1;
                condition1.signalAll();
            } catch (Exception e) {
                throw new RuntimeException(e);
            } finally {
                lock.unlock();
            }
        }
    }
}

//实现
public class Main {
    public static void main(String[] args) throws IOException {

        //创建资源类对象
        Example example=new Example();

        //创建A、B、C、D四个线程
        Thread ta=new Thread(()->{
                example.A();
        });
        Thread tb=new Thread(()->{
                example.B();
        });
        Thread tc=new Thread(()->{
                example.C();
        });
        Thread td=new Thread(()->{
                example.D();
        });

        //启动四个线程
        ta.start();
        tb.start();
        tc.start();
        td.start();
    }
}

运行结果都是:

那既然都能实现同样的效果,为什么还要用JUC来实现呢?

使用Condition可以更高效的实现规定各个线程的执行顺序。如果使用锁对象调用notifyAll方法,那么每次都会唤醒所有的线程,这些线程都会重新竞争锁,如果我们所需要唤醒的线程没有在第一时间获得锁,那么这个线程就必须一直等待,直到获取到锁;而通过使用Condition对象可以实现在每次唤醒时能够精准唤醒所需要的线程,此时只有这一个线程竞争锁,省去了等待时间,所以使用Condition的效率更高。

2.3 TimeUnit

TimeUnit可以用来设置时间的单位,比如TimeUnit.DAYS表示单位天,其他单位如下:

除此之外,还可以通过TimeUnit实现休眠方法,调用格式为TimeUnit.单位.sleep(时间的值),比如Thread.sleep(1000)就可以替换为:

TimeUnit.SECONDS.sleep(1)或者TimeUnit.MILLISECONDS.sleep(1000)。

从功能上来看,两者都可以实现线程的暂停;从参数的类型上看,两者的参数类型都是long。但从可读性和维护性的角度来看,TimeUnit 提供了更清晰和直观的方式来表示时间单位,尤其是在进行复杂的时间计算时;并且从灵活性的角度来看,TimeUnit 提供了多种时间单位的选择,而 Thread.sleep 只能接受毫秒为单位的时间。所以更推荐使用TimeUnit来调用sleep。

2.4 在多线程中使用List、Set以及Map

2.4.1 List

ArrayList是动态数组,可以动态地扩容数组的长度,在单线程中比较常用,但如果是多线程的项目则会出现问题。下面是案例:

java 复制代码
public class Main {
    public static void main(String[] args) {

        ArrayList<Integer> arrayList=new ArrayList<>();

        //创建100个线程,每个线程都执行向数组添加数据以及输出当前数组所有元素的操作
        for(int i=0;i<100;i++){
            new Thread(()->{
                arrayList.add(new Random().nextInt(10));
                System.out.println(arrayList);
            }).start();
        }
    }
}

运行结果:

这里在运行时抛出了ConcurrentModificationException异常,可见ArrayList是非线程安全的。这个异常是并发修改异常,对于ArrayList,它的所有方法都是普通方法并没有加锁,当一个线程正在读取数据时其他线程对其进行了修改,或者有多个线程同时对其进行了修改都会抛出并发修改异常。

更深层次的原因是,当使用标准输出来输出ArrayList的所有元素时,会先调用它的toString方法,而toString方法内部使用的是ArrayList的迭代器来遍历。ArrayList的迭代器是快速失败的,快速失败迭代器的原理是,迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个modCount变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()或者next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,如果是,则继续遍历;否则抛出并发修改异常,终止遍历。下面是toString的源码:

解决办法:

  • 最直接的,就是将修改和读取的操作加上synchronized关键字:
java 复制代码
public class Main {
    public static void main(String[] args) {

        List<Integer> list=new ArrayList<>();
        for(int i=0;i<100;i++){
            new Thread(()->{
                synchronized (list) {
                    list.add(new Random().nextInt(10));
                    System.out.println(list);
                }
            }).start();
        }
    }
}
  • 使用Vector容器

Vector容器对在多线程中所有可能出现线程安全问题的公共方法都加上了锁(将普通方法变成同步方法,锁对象为this),所以可以保证对同一个Vector对象不能同时修改或者同时修改和读(因为对于同一个Vector对象,其所有的同步方法使用的锁对象都一样,就能保证在同一时间只能有一个线程进行修改,并且读和写不能同时执行)。

用法:

java 复制代码
public class Main {
    public static void main(String[] args) {

        List<Integer> list=new Vector<>();
        for(int i=0;i<100;i++){
            new Thread(()->{
                    list.add(new Random().nextInt(10));
                    System.out.println(list);
            }).start();
        }
    }
}
  • 使用Collections.synchronizedList(new ArrayList<>())

Collections.synchronizedList的原理是,在对原本的ArrayList进行操作时,都会加上关键字Synchronized加锁来保证线程安全;因为是对同一个ArrayList进行操作,所以将这些操作变为同步方法后其锁对象都是这个ArrayList,保证了锁的唯一性。

用法:

java 复制代码
public class Main {
    public static void main(String[] args) {

        List<Integer> list=Collections.synchronizedList(new ArrayList<>());
        for(int i=0;i<100;i++){
            new Thread(()->{
                    list.add(new Random().nextInt(10));
                    System.out.println(list);
            }).start();
        }
    }
}
  • JUC的CopyOnWriteArrayList

CopyonWrite是写时复制机制,也就是在对数组进行修改操作时,不会直接在原数组上修改,而是会先复制一份作为副本,在这个副本上进行修改,修改完成后再将内部数组引用指向新的数组。由于写操作期间原数组不会被修改,因此读取操作可以安全地在原数组上进行,不会受到写操作的影响。

既然同时进行读写操作不会受到影响,并且同时读也不会互相干扰,那么在执行读取操作时就不需要获取锁了,因此读取操作通常是非常高效的。然而,写操作的开销相对较大,不仅要加锁(这里使用的是lock锁)保证同一时间只有一个线程执行修改操作,而且需要复制整个数组,相较于原始的ArrayList增加了内存上的开销;因此,CopyOnWriteArrayList适合于读操作远多于写操作的场景。

以add方法的源码帮助理解:

用法:

java 复制代码
public class Main {
    public static void main(String[] args) {

        List<Integer> list=new CopyOnWriteArrayList<>();
        for(int i=0;i<100;i++){
            new Thread(()->{
                    list.add(new Random().nextInt(10));
                    System.out.println(list);
            }).start();
        }
    }
}
2.4.2 Set

Set相比于上面的List而言,没有了Vector容器,但和List类似,也有Collections和JUC两种方式:

  • Collections.synchronizedSet

原理和synchronizedList一样,都是在执行操作之前加上关键字Synchronized。用法为:

java 复制代码
Set<Integer> set= Collections.synchronizedSet(new HashSet<>());

补充:HashSet其实就是HashMap,并且在使用add向HashSet中添加数据时实际上是将数据添加到HashMap的Key部分了,由于Key不能重复,所以Set中不能存储重复的值。下面是源码:

  • CopyOnWriteArraySet

原理和CopyOnWriteArrayList一样,都是利用写时复制的机制,读操作不会加锁,写操作开销较大。用法为:

java 复制代码
Set<Integer> set= new CopyOnWriteArraySet<>();
2.4.3 Map

Map同样既有Collections的实现方式,也有JUC的实现方式:

  • Collections.synchronizedMap

用法为:

java 复制代码
Map<Integer,String> map=Collections.synchronizedMap(new HashMap<>());
  • ConcurrentHashMap

在 Java 8 之前,ConcurrentHashMap 主要使用了分段锁(Segmentation)来实现并发控制。它将整个映射表分成多个段(Segment),每个段都有自己的锁,这样多个线程在访问不同分段时需要的锁不同,使得它们可以同时访问不同的段,并且减少了锁竞争。

在 Java 8 中,ConcurrentHashMap 的实现进行了改进,引入了红黑树和 CAS(Compare-and-Swap)操作来优化性能。当链表的长度超过一定阈值(默认为 8)时,链表会转换为红黑树,以提高搜索性能。当进行修改操作时,使用 CAS 操作来无锁地进行修改。

CAS是一种乐观锁,采用无限循环的方式执行修改操作。在修改之前先记录原来的值,执行完修改操作后先将修改后的结果暂存,并再次记录当前的值,随后与原值比较;如果两个值相同说明在修改期间没有其他线程对其进行修改,则提交修改的结果并退出循环;如果两个值不同,说明在修改期间有其他线程已经修改了值,如果提交修改的结果则会覆盖其他线程的结果,所以不能提交,需要进入下一次循环重新尝试修改。

用法为:

java 复制代码
ConcurrentHashMap<Integer,String> map= new ConcurrentHashMap<>();
相关推荐
世事如云有卷舒29 分钟前
《Rust权威指南》学习笔记(二)
笔记·学习·rust
开心的一只小青蛙43 分钟前
DCMM数据能力成熟度评估模型--学习笔记(2)
笔记·学习·dcmm·数据管理能力成熟度评估模型
CSJK-1 小时前
模式识别与机器学习 | 第十章 半监督学习
人工智能·学习·机器学习
宇寒风暖1 小时前
软件工程大复习(七)面向对象设计
笔记·学习·软件工程·uml
跳跳的向阳花1 小时前
01、Docker学习,第一天:简单入门与安装
学习·docker
找了一圈尾巴2 小时前
Wend看源码-Java-Arrays 工具集学习
java·开发语言·学习
打工人你好3 小时前
iOS 逆向学习 - iOS Security Features:硬件与软件多重防护体系
学习·ios
半路程序员3 小时前
kubernetes学习-StatefulSet(五)
学习·容器·kubernetes
JoneMaster4 小时前
[读书日志]从零开始学习Chisel 第三篇:Scala面向对象编程——类和对象(敏捷硬件开发语言Chisel与数字系统设计)
开发语言·嵌入式硬件·学习·硬件架构·scala