当前位置: 首页 > article >正文

JUC学习

1.什么是JUC

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

2.JUC的功能介绍

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

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

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,让这四个线程按照顺序依次执行两次。

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

//资源类
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来实现:

//资源类
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是动态数组,可以动态地扩容数组的长度,在单线程中比较常用,但如果是多线程的项目则会出现问题。下面是案例:

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关键字:
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对象,其所有的同步方法使用的锁对象都一样,就能保证在同一时间只能有一个线程进行修改,并且读和写不能同时执行)。

        用法:

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,保证了锁的唯一性。

        用法:

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方法的源码帮助理解:

        用法:

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。用法为:

Set<Integer> set= Collections.synchronizedSet(new HashSet<>());

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

  • CopyOnWriteArraySet

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

Set<Integer> set= new CopyOnWriteArraySet<>();
                2.4.3 Map

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

  • Collections.synchronizedMap

        用法为:

Map<Integer,String> map=Collections.synchronizedMap(new HashMap<>());
  • ConcurrentHashMap

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

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

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

        用法为:

ConcurrentHashMap<Integer,String> map= new ConcurrentHashMap<>();


http://www.kler.cn/a/464831.html

相关文章:

  • mysql报错2059
  • IT运维的365天--024 闲置路由器关闭了dhcp,如何知道它的IP是啥
  • 【Infineon AURIX】AURIX缓存(CACHE)变量访问指南
  • 大数据组件(二)快速入门数据集成平台SeaTunnel
  • 建造者模式 Builder Pattern
  • docker下载redis,zookeeper,kafka超时time out
  • 软件工程大复习(五) 需求工程与需求分析
  • Linux系统安装es详细教程
  • 【IEEE冠名会议】2025年IEEE第二届深度学习与计算机视觉国际会议(DLCV 2025)
  • Go recover的执行时机
  • 剪映--关键帧教程:制作视频文字说明,文字动态划线,透明文字,虚拟触控,画面旋转缩小退出
  • TCP IP 网络协议基础入门 1
  • 加速开发体验:为 Android Studio 设置国内镜像源
  • VSCode函数调用关系图插件开发(d3-graphviz)
  • Git核心概念总结
  • 2022浙江大学信号与系统笔记
  • 小程序租赁系统的优势与应用探索
  • Android笔试面试题AI答之Android基础(11)
  • BFS中的双向广搜和A-star
  • 深入理解 PHP 构造函数和析构函数:附示例代码