当前位置: 首页>后端>正文

【Java核心基础知识】06 - 多线程并发(5)

多线程知识点目录

多线程并发(1)- https://www.jianshu.com/p/8fcfcac74033
多线程并发(2)-https://www.jianshu.com/p/a0c5095ad103
多线程并发(3)-https://www.jianshu.com/p/c5c3bbd42c35
多线程并发(4)-https://www.jianshu.com/p/e45807a9853e
多线程并发(5)-https://www.jianshu.com/p/5217588d82ba
多线程并发(6)-https://www.jianshu.com/p/d7c888a9c03c

十四、ThreadLocal作用(线程本地存储)

ThreadLocal也常被叫做线程本地变量线程本地存储,ThreadLocal的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或组件之间一些公共变量的传递的复杂性。

ThreadLocalMap(线程的一个属性)

  1. 每个线程中都有一个自己的ThreadLocalMap类对象,可以将线程自己的对象保持到其中各管各的,线程可以正确的访问到自己的对象。
  2. 将一个公用的ThreadLocal静态实例作为key,将不同对象的引用保存到不同线程的ThreadLocalMap中,然后再线程执行的各处通过这个静态ThreadLocal实例的get()方法取得自己线程保存的那个对象,避免了将这个对象作为参数传递的麻烦。
  3. ThreadLocalMap其实就是线程里面的一个属性,它在Thread类中定义:
    ThreadLocal.ThreadLocalMap threadLocals = null;

使用场景

  1. 数据库连接:在Web应用中,每个用户请求通常会分配一个独立的线程来处理。为了避免数据库连接的竞争,可以在每个线程中使用ThreadLocal来存储独立的数据库连接,确保每个线程都有自己的数据库连接,避免数据库连接的争用和超时等问题。
  2. 会话管理:在Web应用中,每个用户请求需要独立的管理会话数据,包括用户信息、购物车等。可以使用ThreadLocal来存储每个线程的用户会话数据,确保每个线程都有自己的会话数据副本,避免多个线程之间共享会话数据导致的数据竞争和数据不一致问题。
  3. 线程局部变量:在一些需要为每个线程维护独立的状态或数据的场景中,可以使用ThreadLocal来创建线程局部变量。例如,在多线程计算中,每个线程可能需要独立的状态数据或计算结果,使用ThreadLocal可以方便地为每个线程维护独立的数据副本,避免多个线程之间共享数据导致的数据竞争和数据不一致问题。
  4. 线程局部工具类:有些工具类是线程不安全的,不能被多个线程共享。在这种情况下,可以使用ThreadLocal为每个线程创建独立的工具类实例,确保每个线程都有自己的工具类实例,避免多个线程之间共享工具类导致的数据竞争和错误。

使用实例

在这个示例中,我们创建了一个ThreadLocal变量localVariable,并在两个线程中分别设置它的值为100和200。每个线程都有其自己独立的localVariable副本,因此它们不会互相干扰。在每个线程的print方法中,我们打印当前线程本地内存中localVariable的值,并在调用remove方法后打印其值,以验证localVariable已被清除。

public class MyThread {
    // 创建ThreadLocal变量
    public static final ThreadLocal<Integer> localVariable = new ThreadLocal<Integer>();

    static void print(String str) {
        // 打印当前线程本地内存中localVariable变量的值
        System.out.println(str + ": " + localVariable.get());
        // 清除当前线程本地内存中的localVariable变量
        localVariable.remove();
    }

    public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() {
            public void run() {
                // 设置线程1中本地变量的值
                localVariable.set(100);
                print(Thread.currentThread().getName() );
                System.out.println(Thread.currentThread().getName() + "   ---   After remove: " + localVariable.get());
            }
        });

        Thread t2 = new Thread(new Runnable() {
            public void run() {
                // 设置线程2中本地变量的值
                localVariable.set(200);
                print(Thread.currentThread().getName() );
                System.out.println(Thread.currentThread().getName() + "   ---   After remove: " + localVariable.get());
            }
        });

        t1.start();
        t2.start();
    }
}

十五、synchronized 和 ReentrantLock 的区别

  1. 锁的获取方式:synchronized是Java关键字,通过在方法或代码块前加上synchronized关键字来标识同步区域,锁的获取是隐式的,即通过进入同步方法或代码块来获取锁。而ReentrantLock是显式地通过调用lock()方法来获取锁。
  2. 锁的释放方式:synchronized锁的释放是自动的,当线程离开同步方法或代码块时,锁会自动释放。而ReentrantLock需要显式地调用unlock()方法来释放锁。
  3. 中断等待和阻塞:synchronized不支持中断等待和阻塞,一旦线程进入同步方法或代码块,其他线程必须等待该线程执行完成才能继续执行。而ReentrantLock支持中断等待和阻塞,可以通过调用lockInterruptibly()方法来中断等待锁的线程。
  4. 可重入性:synchronized是可重入的,即一个线程可以多次进入同步方法或代码块,不会产生死锁。而ReentrantLock也是可重入的,但需要显式地调用lock()和unlock()方法来实现。
  5. 公平性:synchronized默认情况下是非公平的,即不能保证等待时间最长的线程优先获取锁。而ReentrantLock可以设置为公平锁或非公平锁,公平锁按照等待时间分配锁的获取顺序,非公平锁则没有这个限制。
  6. 性能:在性能方面,ReentrantLock相对于synchronized来说更加灵活,可以使用更底层的机制来实现锁的获取和释放,因此性能上可能更加优秀。但是,对于简单的同步场景来说,使用synchronized可能更加简单和方便。
  7. 发生异常时:synchronized会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要再finally块中释放锁。

十六、ConcurrentHashMap并发

16.1 减小锁粒度

减小锁粒度是指缩小锁定对象的范围,从而减小锁冲突的可能性,从而提高系统的并发能力。减小锁粒度是一种削弱多线程锁竞争的有效手段,这种技术典型的应用是ConcurrentHashMap(高性能的HashMap)类的实现。
对于HashMap而言,最重要的两个方法是get()与set()方法,如果我们队整个HashMap加锁,可以得到线程安全的对象,但是加锁粒度太大。Segment的大小也被称为ConcurrentHashMap的并发度。

16.2 ConcurrentHashMap分段锁

ConcurrentHashMap内部细分了若干个小的HashMap,称之为段(Segment)。默认情况下一个ConcurrentHashMap被进一步细分为16个段。
如果需要在ConcurrentHashMap中添加一个新的表项,并不是将整个HashMap加锁,而是首先根据HashCode得到该表项应该存放在那个段中,然后对该段加锁,并完成put操作。在多线程环境中,如果多个线程同时进行put操作,只要被加入的表项不存放在同一个段中,则线程间可以做到真正的并行。

ConcurrentHashMap 是由Segment数组结构和HashEntry数组结构组成
ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一种可重入锁ReenTrantLock,在ConcureentHashMap里扮演锁的角色,HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构,一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素,每个Segment守护一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。

【Java核心基础知识】06 - 多线程并发(5),第1张
ConcureentHashMap数据结构组成

十七、Java中用到的线程调度

【Java核心基础知识】06 - 多线程并发(5),第2张
Java中用到的线程调度

17.1 抢占式调度

抢占式调度指的是每条线程执行的时间、线程的切换都由系统控制,系统控制指的是在系统某种运行机制下,可能没跳线程都分同样的执行时间片,也可能是某些线程执行的时间片较长,甚至某些线程得不到执行的时间片。在这种机制下,一个线程的堵塞不会导致整个进程堵塞。

17.2 协同式调度

协同式调度指某一线程执行完后主动通知系统切换到另一线程上执行,这种模式就像接力赛一样,一个人跑完自己的线程就把接力棒交给下一个人,下个人继续往下跑。线程的执行时间由线程本身控制,线程切换可以预知,不存在多线程同步问题,但它有一个致命弱点:如果一个线程编写有问题,运行到一半就一直堵塞,那么可能导致整个系统崩溃。

17.3 JVM的线程调度实现(抢占式调度)

Java使用的是一种称为"抢占式调度"的策略,这意味着CPU时间片会根据线程的优先级分配给各个线程。

然而,这并不意味着优先级高的线程会独占执行时间片。在Java中,线程调度器采用的是一种称为"时间片轮转调度"的方法,它为每个线程分配一个固定长度的时间片,然后按照各自的优先级循环调度这些线程。当一个线程的时间片用完后,调度器会暂停这个线程的执行,并将它放入就绪队列,等待下一次被调度。

同时,需要注意的是,虽然高优先级的线程会得到更多的执行时间片,但是这并不意味着低优先级的线程得不到执行时间。只是相对于高优先级的线程,它们得到的时间片会少一些。因此,在Java中,即使线程的优先级不同,也不会出现某些线程完全得不到执行的情况。

此外,Java中的线程调度是依赖于操作系统的。不同的操作系统可能会有不同的线程调度策略和算法。但是,Java虚拟机规范定义了Java线程调度的一些基本行为和要求,以确保Java程序在不同平台上具有可移植性和一致性。

17.4 线程让出CPU的情况

在Java中,线程可以让出CPU的情况有以下几种:

  1. 线程执行完毕:当一个线程完成了它的任务,它就会退出并释放CPU。
  2. 线程被阻塞:当一个线程试图获取一个锁但无法立即获得时,它会被阻塞并让出CPU,直到锁被释放。
  3. 线程在等待I/O操作:当一个线程正在等待一个I/O操作完成时,例如等待一个网络请求返回或等待磁盘操作完成,它会进入等待状态并让出CPU。
  4. 线程被中断:当一个线程被中断时,它会被强制让出CPU,以便其他线程可以获得执行机会。
  5. 线程的优先级改变:如果一个线程的优先级被改变,那么它可能会让出CPU以便其他线程可以获得更多的执行时间。
  6. 线程正在休眠:如果一个线程正在休眠,它会让出CPU,直到休眠时间结束。
  7. 线程正在进行垃圾回收:在进行垃圾回收时,一些线程可能需要让出CPU以便垃圾回收器可以执行其工作。

需要注意的是,这些情况并非一定会导致线程让出CPU。例如,即使线程正在等待I/O操作,它也可能仍然占用CPU。此外,Java虚拟机规范并没有规定具体的线程调度策略和算法,因此不同的JVM实现可能会有不同的行为。

十八、进程调度算法

18.1 优先调度算法

1. 先来先服务调度算法(FCFS)

当在作业调度中采用该算法时,每次调度都是从后备作业队列中选择一个或多个最先进入该队列的作业,将它们调入内存,为它们分配资源、创建进程,然后放入就绪队列。在进程调度中采用FCFS算法时,则每次调度是从就绪队列中选择一个最先进入该队列的进程,为之分配处理机,使之投入运行。该进程一直运行到完成或发生某事件而阻塞后才放弃处理机,特点是:算法比较简单,可以实现基本上的公平。

2. 短作业(进程)优先调度算法

短作业优先(Shortest Job First,SJF)和最短进程优先(Shortest Process First,SPF)都是基于进程或作业执行时间的调度算法。它们的主要目标是最小化平均等待时间和平均转环时间,从而提高系统效率和响应速度。

2.1 短作业优先(Shortest Job First,SJF)
SJF是一种非抢占式的调度算法,适用于批处理系统。在SJF调度中,当一个作业到达时,系统会检查所有在内存中的进程,选择执行时间最短的进程开始执行。这样做的目的是尽量减少等待时间,因为等待时间对于用户来说是最没有价值的。
SJF算法的主要问题是它可能会导致饥饿现象。也就是说,如果一个长作业持续到来,那么它可能会长时间地占用CPU,使得其他短作业无法得到执行。

2.2 最短进程优先(Shortest Process First,SPF)
SPF是一种抢占式的调度算法,适用于分时系统。在SPF调度中,当一个进程就绪时,系统会检查所有在就绪队列中的进程,选择执行时间最短的进程开始执行。这种策略照顾了紧迫型作业,即那些需要在最短时间内得到响应的作业。
与SJF算法相比,SPF算法可以更好地平衡等待时间和运行时间。然而,它仍然存在一些问题,例如可能会导致忙等待现象,即一个进程可能会不断地检查自己的优先级并尝试获取CPU,这会浪费CPU资源。

18.2 高优先权优先调度算法

高优先权优先调度算法(FPF,Priority Scheduling Algorithm)是一种常见的进程调度算法,它赋予紧迫性作业最高优先权,使其在进入系统后便获得优先处理。这种算法常被用于批处理系统中作为作业调度算法,也作为多种操作系统中的进程调度算法,还可用于实时系统中。

在FPF调度中,进程的优先级可以是静态的或动态的。
静态优先级一般用一个整数表示,在进程创建时确定,进程的整个运行期间保持不变。
动态优先级则是在进程创建时赋予优先级,随着进程的推进或者等待时间的增加而改变。
例如,当等待时间与服务时间之和就是系统的响应时间时,优先权 = (等待时间 + 要求服务时间)/ 要求服务时间。可以看出随着进程等待时间增长,优先权线性增长,等待足够长时间是一定能获得处理机的;要求服务时间短,优先权线性增长,有利于短作业。

在FPF调度中,有两种方式进行进程调度:非抢占式优先级算法和抢占式优先级算法。
非抢占式优先级算法下,系统一旦把处理机分配给就绪队列中优先级最高的进程后,该进程就能一直执行下去,直至完成;或因等待某事件的发生使该进程不得不放弃处理机时,系统才能将处理机分配给另一个优先级高的就绪进程。
抢占式优先级调度算法下,进程调度程序把处理机分配给当时优先级最高的就绪进程,使之执行。一旦出现了另一个优先级更高的就绪进程时,进程调度程序就停止正在执行的进程,将处理机分配给新出现的优先级最高的就绪进程。

18.3 基于时间片的轮转调度算法

3.1 时间片轮转法

基于时间片的轮转调度算法是一种常见的进程调度算法,也称为时间片轮转调度算法(Round Robin Scheduling Algorithm)。

这种算法的核心思想是将系统中的所有进程按照到达时间的先后顺序排列,并按照顺序依次执行。每个进程在执行期间都会被分配一个固定长度的时间片(Quantum),进程在执行完一个时间片后,如果还没有完成,将被放回就绪队列的末尾,等待下一次的调度。

然而,时间片轮转调度算法也存在一些缺点。例如,如果时间片的选择不合适,可能会导致系统性能的下降。如果时间片过长,会导致进程的等待时间过长,降低了系统的效率;如果时间片过短,则会导致进程频繁地切换,增加了系统的开销。

此外,时间片轮转调度算法对于一些需要长时间运行的进程可能不够友好,因为它们可能需要等待较长时间才能获得执行机会。

总之,基于时间片的轮转调度算法是一种常见的进程调度算法,具有公平性和简洁性的优点,但也需要注意时间片的长度和选择是否合适,以保证系统的性能和效率。

3.2 多级反馈队列调度算法

多级反馈队列调度算法是一种在操作系统中应用的进程调度算法。这种算法根据先来先服务原则,给就绪队列排序,并为每个队列赋予不同的优先级。每个队列中的进程都会被赋予不同的时间片,优先级越高,时间片越短。进程在等待时,首先进入优先级最高的队列等待,如果高优先级队列中没有进程等待,才会考虑次优先级的队列。

当一个新进程进入内存后,首先将它放入第一队列的末尾,按 FCFS 原则排队等待调度。当轮到该进程执行时,如它能在该时间片内完成,便可准备撤离系统;如果它在一个时间片结束时尚未完成,调度程序便将该进程转入第二队列的末尾,再同样地按 FCFS 原则等待调度执行;如果它在第二队列中运行一个时间片后仍未完成,再依次将它放入第三队列,……,如此下去,当一个长作业(进程)从第一队列依次降到第 n 队列后,在第 n 队列便采取按时间片轮转的方式运行。

在多级反馈队列调度算法中,如果规定第一个队列的时间片略大于多数人机交互所需之处理时间时,便能够较好的满足各种类型用户的需要

这种算法的优点在于,它结合了非抢占式和抢占式调度的优点,可以更好地平衡系统负载和响应时间。同时,多级反馈队列调度算法还可以根据实际需要调整时间片的长度,以适应不同的作业需求。

然而,这种算法也存在一些缺点。例如,如果优先级相同或优先级设置不合理,可能会导致某些进程得不到足够的执行时间。此外,该算法的实现和管理也相对复杂,需要操作系统提供更多的支持和维护。

总之,多级反馈队列调度算法是一种有效的进程调度算法,可以更好地平衡系统负载和响应时间,但需要注意优先级的设置和调整,以保证系统的性能和效率。


https://www.xamrdz.com/backend/3a61944777.html

相关文章: