多线程基础体系知识清单(下)

简介: 多线程基础体系知识清单(下)

notify和notifyAll的区别


notify


notify可以唤醒一个处于等待状态的线程,上代码:


public class Main{
    public static void main(String[] args) {
        Object lock = new Object();
        Thread threadA = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lock) {
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                    print();
                }
            }
        });
        Thread threadB = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lock) {
                    print();
                    lock.notify();
                }
            }
        });
        threadA.setName("threadA");
        threadB.setName("threadB");
        threadA.start();
        threadB.start();
    }
    public static void print() {
            System.out.println("当前线程:"+Thread.currentThread().getName()+"执行print");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            System.out.println("当前线程:"+Thread.currentThread().getName()+"执行完毕");
    }
}


执行结果:


image.png


代码解释:


线程A在开始执行时立即调用wait进入无限等待状态,如果没有别的线程来唤醒它,它将一直等待下去,所以此时B持有锁开始执行,并且在执行完毕时调用了notify方法,该方法可以唤醒wait状态的A线程,于是A线程苏醒,开始执行剩下的代码。


notifyAll


notifyAll可以用于唤醒所有等待的线程,使所有处于等待状态的线程都变为ready状态,去重新争夺锁。


public class Main{
    public static void main(String[] args) {
        Object lock = new Object();
        Thread threadA = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lock) {
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                    print();
                }
            }
        });
        Thread threadB = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lock) {
                    print();
                    lock.notifyAll();
                }
            }
        });
        threadA.setName("threadA");
        threadB.setName("threadB");
        threadA.start();
        threadB.start();
    }
    public static void print() {
            System.out.println("当前线程:"+Thread.currentThread().getName()+"执行print");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            System.out.println("当前线程:"+Thread.currentThread().getName()+"执行完毕");
    }
}


执行结果:


image.png


要唤醒前一个例子中的线程A,不光notify方法可以做到,调用notifyAll方法同样也可以做到,那么两者有什么区别呢?


区别


要说清楚他们的区别,首先要简单的说一下Java synchronized的一些原理,在openjdk中查看java的源码可以看到,java对象中存在monitor锁,monitor对象中包含锁池和等待池。


锁池


假设有多个对象进入synchronized块争夺锁,而此时已经有一个对象获取到了锁,那么剩余争夺锁的对象将直接进入锁池中。


等待池


假设某个线程调用了对象的wait方法,那么这个线程将直接进入等待池,而等待池中的对象不会去争夺锁,而是等待被唤醒。


下面可以说notify和notifyAll的区别了:


notifyAll会让所有处于等待池中的线程全部进入锁池去争夺锁,而notify只会随机让其中一个线程去争夺锁。



yield方法


概念


/**
     * A hint to the scheduler that the current thread is willing to yield
     * its current use of a processor. The scheduler is free to ignore this
     * hint.
     *
     * <p> Yield is a heuristic attempt to improve relative progression
     * between threads that would otherwise over-utilise a CPU. Its use
     * should be combined with detailed profiling and benchmarking to
     * ensure that it actually has the desired effect.
     *
     * <p> It is rarely appropriate to use this method. It may be useful
     * for debugging or testing purposes, where it may help to reproduce
     * bugs due to race conditions. It may also be useful when designing
     * concurrency control constructs such as the ones in the
     * {@link java.util.concurrent.locks} package.
     */
    public static native void yield();


yield源码上有一段长长的注释,其大意是说:当前线程调用yield方法时,会给当前线程调度器一个暗示,当前线程愿意让出CPU的使用,但是它的作用应结合详细的分析和测试来确保已经达到了预期的效果,因为调度器可能会无视这个暗示,使用这个方法是不那么合适的,或许在测试环境中使用它会比较好。


测试:


public class Main{
    public static void main(String[] args) {
        Thread threadA = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("ThreadA正在执行yield");
                Thread.yield();
                System.out.println("ThreadA执行yield方法完成");
            }
        });
        Thread threadB = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("ThreadB正在执行yield");
                Thread.yield();
                System.out.println("ThreadB执行yield方法完成");
            }
        });
        threadA.setName("threadA");
        threadB.setName("threadB");
        threadA.start();
        threadB.start();
    }


测试结果:


image.png


可以看出,存在不同的测试结果,这里选出两张。


第一种结果:线程A执行完yield方法,让出cpu给线程B执行。然后两个线程继续执行剩下的代码。


第二种结果:线程A执行yield方法,让出cpu给线程B执行,但是线程B执行yield方法后并没有让出cpu,而是继续往下执行,此时就是系统无视了这个暗示。


interrupt方法


中止线程


interrupt函数可以中断一个线程,在interrupt之前,通常使用stop方法来终止一个线程,但是stop方法过于暴力,它的特点是,不论被中断的线程之前处于一个什么样的状态,都无条件中断,这会导致被中断的线程后续的一些清理工作无法顺利完成,引发一些不必要的异常和隐患,还有可能引发数据不同步的问题。


温柔的interrupt方法


interrupt方法的原理与stop方法相比就显得温柔的多,当调用interrupt方法去终止一个线程时,它并不会暴力地强制终止线程,而是通知这个线程应该要被中断了,和yield一样,这也是一种暗示,至于是否应该中断,由被中断的线程自己去决定。当对一个线程调用interrupt方法时:


  1. 如果该线程处于被阻塞状态,则立即退出阻塞状态,抛出InterruptedException异常。
  2. 如果该线程处于running状态,则将该线程的中断标志位设置为true,被设置的线程继续运行,不受影响,当运行结束时由线程决定是否被中断。

线程池


线程池的引入是用来解决在日常开发的多线程开发中,如果开发者需要使用到非常多的线程,那么这些线程在被频繁的创建和销毁时,会对系统造成一定的影响,有可能系统在创建和销毁这些线程所耗费的时间会比完成实际需求的时间还要长。


另外,在线程很多的状况下,对线程的管理就形成了一个很大的问题,开发者通常要将注意力从功能上转移到对杂乱无章的线程进行管理上,这项动作实际上是非常耗费精力的。


利用Executors创建不同的线程池满足不同场景的需求


newFixThreadPool(int nThreads)


指定工作线程数量的线程池。


newCachedThreadPool()


处理大量中断事件工作任务的线程池,


  1. 试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程。
  2. 如果线程闲置的时间超过阈值,则会被终止并移出缓存。
  3. 系统长时间闲置的时候,不会消耗什么资源。


newSingleThreadExecutor()


创建唯一的工作线程来执行任务,如果线程异常结束,会有另一个线程取代它。可保证顺序执行任务。


newSingleThreadScheduledExecutor()与newScheduledThreadPool(int corePoolSize)


定时或周期性工作调度,两者的区别在于前者是单一工作线程,后者是多线程

newWorkStealingPool()


内部构建ForkJoinPool,利用working-stealing算法,并行地处理任务,不保证处理顺序。


Fork/Join框架:把大任务分割称若干个小任务并行执行,最终汇总每个小任务后得到大任务结果的框架。


为什么要使用线程池


线程是稀缺资源,如果无限制地创建线程,会消耗系统资源,而线程池可以代替开发者管理线程,一个线程在结束运行后,不会销毁线程,而是将线程归还线程池,由线程池再进行管理,这样就可以对线程进行复用。


所以线程池不但可以降低资源的消耗,还可以提高线程的可管理性。


使用线程池启动线程


public class Main{
    public static void main(String[] args) {
        ExecutorService newFixThreadPool = Executors.newFixedThreadPool(10);
        newFixThreadPool.execute(new Runnable() {
            @Override
            public void run() {
                // TODO Auto-generated method stub
                System.out.println("通过线程池启动线程成功");
            }
        });
        newFixThreadPool.shutdown();
    }
}


新任务execute执行后的判断


要知道这个点首先要先说说ThreadPoolExecutor的构造函数,其中有几个参数:


  1. corePoolSize:核心线程数量。
  2. maximumPoolSize:线程不够用时能创建的最大线程数。
  3. workQueue:等待队列。


那么新任务提交后会执行下列判断:


  1. 如果运行的线程少于corePoolSize,则创建新线程来处理任务,即时线程池中的其它线程是空闲的。
  2. 如果线程池中的数量大于等于corePoolSize且小于maximumPoolSize,则只有当workQueue满时,才创建新的线程去处理任务。
  3. 如果设置的corePoolSize和maximumPoolSize相同,则创建的线程池大小是固定的,如果此时有新任务提交,若workQueue未满,则放入workQueue,等待被处理。
  4. 如果运行的线程数大于等于maximumPoolSize,maximumPoolSize,这时如果workQueue已经满了,则通过handler所指定的策略来处理任务。


handler 线程池饱和策略


  • AbortPolicy:直接抛出异常,默认。
  • CallerRunsPolicy:用调用者所在的线程来执行任务。
  • DiscardOldestPolicy:丢弃队列中靠最前的任务,并执行当前任务。
  • DiscardPolicy:直接丢弃任务
  • 自定义。


线程池的大小如何选定


这个问题并不是什么秘密,在网上各大技术网站均有文章说明,我就拿一个最受认可的写上吧


  • CPU密集型:线程数 = 核心数或者核心数+1
  • IO密集型:线程数 = CPU核数*(1+平均等待时间/平均工作时间)


当然这个也不能完全依赖这个公式,更多的是要依赖平时的经验来操作,这个公式也只是仅供参考而已。


结语


本文提供了一些Java多线程和并发方面最最基础的知识,适合初学者了解Java多线程的一些基本知识,如果想了解更多的关于并发方面的内容可以看:


https://juejin.im/post/5d8da403f265da5b5d203bf4



相关文章
|
4天前
|
人工智能 JavaScript 测试技术
Qwen3-Coder入门教程|10分钟搞定安装配置
Qwen3-Coder 挑战赛简介:无论你是编程小白还是办公达人,都能通过本教程快速上手 Qwen-Code CLI,利用 AI 轻松实现代码编写、文档处理等任务。内容涵盖 API 配置、CLI 安装及多种实用案例,助你提升效率,体验智能编码的乐趣。
354 105
|
5天前
|
JSON fastjson Java
FastJson 完全学习指南(初学者从零入门)
摘要:本文是FastJson的入门学习指南,主要内容包括: JSON基础:介绍JSON格式特点、键值对规则、数组和对象格式,以及嵌套结构的访问方式。FastJson是阿里巴巴开源的高性能JSON解析库,具有速度快、功能全、使用简单等优势,并介绍如何引入依赖,如何替换Springboot默认的JackJson。 核心API: 序列化:将Java对象转换为JSON字符串,演示对象、List和Map的序列化方法; 反序列化:将JSON字符串转回Java对象,展示基本对象转换方法;
|
5天前
|
缓存 JavaScript 前端开发
JavaScript 的三种引入方法详解
在网页开发中,JavaScript 可通过内联、内部脚本和外部脚本三种方式引入 HTML 文件,各具适用场景。本文详解其用法并附完整示例代码,帮助开发者根据项目需求选择合适的方式,提升代码维护性与开发效率。
201 110
|
6天前
|
Android开发 开发者 Windows
这是我设计的一种不关机,然后改造操作系统的软件设计思路2.0版本
本文介绍了在不重启系统的情况下实现操作系统改造的两种方案。第一种方案通过SLFM Recovery模式,在独立于操作系统的最高权限环境下完成系统更新与改造,并支持断电恢复与失败回滚。第二种方案采用多分区机制,通过SLFM套件在独立分区中完成系统改造,适用于可中断与不可中断服务场景,确保系统更新过程的安全与稳定。
232 132