多线程详解

文章目录

线程、程序、进程的基本概念

  • 线程:与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
  • 程序:是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码。
  • 进程:是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如 CPU 时间,内存空间,文件,文件,输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。 线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。从另一角度来说,进程属于操作系统的范畴,主要是同一段时间内,可以同时执行一个以上的程序,而线程则是在同一程序内几乎同时执行一个以上的程序段。

线程的基本状态

  1. 新建状态(New):新创建了一个线程对象。

  2. 就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于
    “可运行线程池”中,变得可运行,只等待获取CPU的使用权。即在就绪状态的进程除CPU之外,其
    它的运行所需资源都已全部获得。

  3. 运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。

  4. 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入
    就绪状态,才有机会转到运行状态。
    阻塞的情况分三种:

  5. 等待阻塞:运行的线程执行wait()方法,该线程会释放占用的所有资源,JVM会把该线程放入

“等待池”中。进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify()或
notifyAll()方法才能被唤醒,
  1. 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线
程放入“锁池”中。
  1. 其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为
阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新
转入就绪状态。
  1. 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态。线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。Java 线程状态变迁如下图所示:

image-20220321163552828

线程生命周期

当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生
命周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡
(Dead)5 种状态。尤其是当线程启动以后,它不可能一直"霸占"着 CPU 独自运行,所以 CPU 需要在多
条线程之间切换,于是线程状态也会多次在运行、阻塞之间切换

  1. 新建状态(NEW)
    当程序使用 new 关键字创建了一个线程之后,该线程就处于新建状态,此时仅由 JVM 为其分配内存,
    并初始化其成员变量的值
  2. 就绪状态(RUNNABLE):
    当线程对象调用了 start()方法之后,该线程处于就绪状态。Java 虚拟机会为其创建方法调用栈和程序计
    数器,等待调度运行。
  3. 阻塞状态(BLOCKED):
    阻塞状态是指线程因为某种原因放弃了 cpu 使用权,也即让出了 cpu timeslice,暂时停止运行。直到线
    程进入可运行(runnable)状态,才有机会再次获得 cpu timeslice 转到运行(running)状
    态。阻塞的情况分三种:
    1. 等待阻塞(o.wait->等待对列):
      运行(running)的线程执行 o.wait()方法,JVM 会把该线程放入等待队列(waitting queue)中。
    2. 同步阻塞(lock->锁池)
      运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线程放入锁池
      (lock pool)中。
    3. 其他阻塞(sleep/join)
      运行(running)的线程执行 Thread.sleep(long ms)或 t.join()方法,或者发出了 I/O 请求时,JVM 会把该
      线程置为阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O处理完毕时,线程重新
      转入可运行(runnable)状态。
  4. 线程死亡(DEAD)
    线程会以下面三种方式结束,结束后就是死亡状态。
    1. 正常结束:run()或call()方法执行完成,线程正常结束
    2. 异常结束:线程抛出一个未捕获的异常或错误
    3. 调用stop:直接调用线程的stop()方法来结束这一线程,此方法容易导致死锁,不推荐使用

终止线程的四种方式

正常运行结束

程序运行结束,线程自动结束。

使用退出标志退出线程

一般 run()方法执行完,线程就会正常结束,然而,常常有些线程需要长时间的运行,只有在外部某些条件满足的情况下,才能关闭这些线程。使用一个变量来控制循环,例如:最直接的方法就是设一个 boolean 类型的标志,并通过设置这个标志为 true 或 false 来控制 while循环是否退出,代码示例:

public class ThreadSafe extends Thread {
    public volatile boolean exit = false;
        public void run() { while (!exit){
        //do something
        }
    }
}

定义了一个退出标志 exit,当 exit 为 true 时,while 循环退出,exit 的默认值为 false.在定义 exit时,使用了一个 Java 关键字 volatile,这个关键字的目的是使 exit 同步,也就是说在同一时刻只能由一个线程来修改 exit 的值。

Interrupt() 方法结束线程

使用 interrupt()方法来中断线程有两种情况:

  1. 线程处于阻塞状态:如使用了 sleep,同步锁的 wait,socket 中的 receiver,accept 等方法时,会使线程处于阻塞状态。当调用线程的 interrupt()方法时,会抛出 InterruptException 异常。阻塞中的那个方法抛出这个异常,通过代码捕获该异常,然后 break 跳出循环状态,从而让我们有机会结束这个线程的执行。通常很多人认为只要调用 interrupt 方法线程就会结束,实际上是错的, 一定要先捕获 InterruptedException 异常之后通过 break 来跳出循环,才能正常结束 run 方法。
  2. 线程未处于阻塞状态:使用 isInterrupted()判断线程的中断标志来退出循环。当使用interrupt()方法时,中断标志就会置 true,和使用自定义的标志来控制循环是一样的道理。
public class ThreadSafe extends Thread {
    public void run() {
        while (!isInterrupted()){ //非阻塞过程中通过判断中断标志来退出
            try{
            	Thread.sleep(5*1000);//阻塞过程捕获中断异常来退出
            }catch(InterruptedException e){
                e.printStackTrace();
                break;//捕获到异常之后,执行 break 跳出循环
            }
        }
    }
}

stop 方法终止线程(线程不安全)

程序中可以直接使用 thread.stop()来强行终止线程,但是 stop 方法是很危险的,就象突然关闭计算机电源,而不是按正常程序关机一样,可能会产生不可预料的结果,不安全主要是:thread.stop()调用之后,创建子线程的线程就会抛出 ThreadDeatherror 的错误,并且会释放子线程所持有的所有锁。一般任何进行加锁的代码块,都是为了保护数据的一致性,如果在调用thread.stop()后导致了该线程所持有的所有锁的突然释放(不可控制),那么被保护数据就有可能呈现不一致性,其他线程在使用这些被破坏的数据时,有可能导致一些很奇怪的应用程序错误。因此,并不推荐使用 stop 方法来终止线程。

wait()和notify()

简介

  1. wait( ),notify( ),notifyAll( )都不属于Thread类,而是属于Object基础类,也就是每个对象都有wait( ),notify( ),notifyAll( ) 的功能,因为每个对象都有锁,锁是每个对象的基础,当然操作锁的方法也是最基础了。
  2. 当需要调用以上的方法的时候,一定要对竞争资源进行加锁,如果不加锁的话,则会报IllegalMonitorStateException 异常
  3. 当想要调用wait( )进行线程等待时,必须要取得这个锁对象的控制权(对象监视器),一般是放到
    synchronized(obj)代码中。
  4. 在while循环里而不是if语句下使用wait,这样,会在线程暂停恢复后都检查wait的条件,并在条件实际上并未改变的情况下处理唤醒通知
  5. 调用obj.wait( )释放了obj的锁,否则其他线程也无法获得obj的锁,也就无法在synchronized(obj){obj.notify() } 代码段内唤醒A。
  6. notify( )方法只会通知等待队列中的第一个相关线程(不会通知优先级比较高的线程)
  7. notifyAll( )通知所有等待该竞争资源的线程(也不会按照线程的优先级来执行)
  8. 假设有三个线程执行了obj.wait( ),那么obj.notifyAll( )则能全部唤醒tread1,thread2,thread3,但是要继续执行obj.wait()的下一条语句,必须获得obj锁,因此,tread1,thread2,thread3只有一个有机会获得锁继续执行,例如tread1,其余的需要等待thread1释放obj锁之后才能继续执行。
  9. 当调用obj.notify/notifyAll后,调用线程依旧持有obj锁,因此,thread1,thread2,thread3虽被唤醒,但是仍无法获得obj锁。直到调用线程退出synchronized块,释放obj锁后,thread1,thread2,thread3中的一个才有机会获得锁继续执行。

示例

public class WaitNotifyTest {
    // 在多线程间共享的对象上使用wait
    private String[] shareObj = {"true"};

    public static void main(String[] args) {
        WaitNotifyTest test = new WaitNotifyTest();
        ThreadWait threadWait1 = test.new ThreadWait("wait thread1");
        threadWait1.setPriority(2);
        ThreadWait threadWait2 = test.new ThreadWait("wait thread2");
        threadWait2.setPriority(3);
        ThreadWait threadWait3 = test.new ThreadWait("wait thread3");
        threadWait3.setPriority(4);
        ThreadNotify threadNotify = test.new ThreadNotify("notify thread");
        threadNotify.start();
        threadWait1.start();
        threadWait2.start();
        threadWait3.start();
    }

    class ThreadWait extends Thread {
        public ThreadWait(String name) {
            super(name);
        }

        public void run() {
            synchronized (shareObj) {
                while ("true".equals(shareObj[0])) {
                    System.out.println("线程" + this.getName() + "开始等待");
                    long startTime = System.currentTimeMillis();
                    try {
                        shareObj.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    long endTime = System.currentTimeMillis();
                    System.out.println("线程" + this.getName()
                            + "等待时间为:" + (endTime - startTime));
                }
            }
            System.out.println("线程" + getName() + "等待结束");
        }
    }

    class ThreadNotify extends Thread {
        public ThreadNotify(String name) {
            super(name);
        }

        public void run() {
            try {
// 给等待线程等待时间
                sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (shareObj) {
                System.out.println("线程" + this.getName() + "开始准备通知");
                shareObj[0] = "false";
                shareObj.notifyAll();
                System.out.println("线程" + this.getName() + "通知结束");
            }
            System.out.println("线程" + this.getName() + "运行结束");
        }
    }
}

输出结果:

线程wait thread1开始等待
线程wait thread3开始等待
线程wait thread2开始等待
线程notify thread开始准备通知
线程notify thread通知结束
线程notify thread运行结束
线程wait thread2等待时间为:3014
线程wait thread2等待结束
线程wait thread3等待时间为:3014
线程wait thread3等待结束
线程wait thread1等待时间为:3014
线程wait thread1等待结束

sleep()和wait()

  1. 对于 sleep()方法,我们首先要知道该方法是属于 Thread 类中的。而 wait()方法,则是属于 Object类中的。
  2. sleep()方法导致了程序暂停执行指定的时间,让出 cpu 该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。
  3. 在调用 sleep()方法的过程中,线程不会释放对象锁。而当调用 wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用 notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。

start()和run()

  1. start()方法来启动线程,真正实现了多线程运行。这时无需等待 run 方法体代码执行完毕,可
    以直接继续执行下面的代码。
  2. 通过调用 Thread 类的 start()方法来启动一个线程, 这时此线程是处于就绪状态, 并没有运行。
  3. 方法 run()称为线程体,它包含了要执行的这个线程的内容,线程就进入了运行状态,开始运行
    run 函数当中的代码。 Run 方法运行结束, 此线程终止。然后 CPU 再调度其它线程。

Linux进程间通信方式

概述

通信方法无法介于内核态与用户态的原因
管道(不包括命名管道)局限于父子进程间的通信
消息队列在硬、软中断中无法阻塞地接受数据
信号量无法介于内核态和用户态使用
内存共享需要信号量辅助,而信号量有无法使用
套接字在硬、软中断中无法阻塞地接受数据

信号

信号又称软终端,通知程序发生异步事件,程序执行中随时被各种信号中断,进程可以忽略该信号,也可以中断当前程序转而去处理信号,引起信号原因:
1).程序中执行错误码;
2).其他进程发送来的;
3).用户通过控制终端发送来;
4).子进程结束时向父进程发送SIGCLD;
5).定时器生产的SIGALRM;

管道

​ 管道的优点是不需要加锁,缺点是默认缓冲区太小,只有4K,同时只适合父子进程间通信,而且一个管道只适合单向通信,如果要双向通信需要建立两个。而且不适合多个子进程,因为消息会乱,它的发送接收机制是用read/write这种适用流的,缺点是数据本身没有边界,需要应用程序自己解释,而一般消息大多是一个固定长的消息头,和一个变长的消息体,一个子进程从管道read到消息头后,消息体可能被别的子进程接收到。

单向,一段输入,另一端输出,先进先出FIFO。管道也是文件。管道大小4096字节。

特点:管道满时,写阻塞;空时,读阻塞。

分类:普通管道(仅父子进程间通信)位于内存;命名管道位于文件系统,没有亲缘关系管道只要知道管道名也可以通讯。

​ 管道是由内核管理的一个缓冲区(buffer),相当于我们放入内存中的一个纸条。管道的一端连接一个进程的输出。这个进程会向管道中放入信息。管道的另一端连接一个进程的输入,这个进程取出被放入管道的信息。一个缓冲区不需要很大,它被设计成为环形的数据结构,以便管道可以被循环利用。当管道中没有信息的话,从管道中读取的进程会等待,直到另一端的进程放入信息。当管道被放满信息的时候,尝试放入信息的进程会等待,直到另一端的进程取出信息。当两个进程都终结的时候,管道也自动消失。

消息队列

​ 消息队列也不要加锁,默认缓冲区和单消息上限都要大一些,是64K,它并不局限于父子进程间通信,只要一个相同的key,就可以让不同的进程定位到同一个消息队列上,它也可以用来给双向通信,不过稍微加个标识,可以通过消息中的type进行区分,比如一个任务分派进程,创建了若干个执行子进程,不管是父进程发送分派任务的消息,还是子进程发送任务执行的消息,都将type设置为目标进程的pid,因为msgrcv可以指定只接收消息类型为type的消息,这样就实现了子进程只接收自己的任务,父进程只接收任务结果。

消息队列是先进先出FIFO原则
消息结构模板

struct msgbuf
{
    long int mtype;//消息类型
    char mtext[1];//消息内容
}

共享内存

共享内存的几乎可以认为没有上限,它也是不局限与父子进程,采用跟消息队列类似的定位方式,因为内存是共享的,不存在任何单向的限制,最大的问题就是需要应用程序自己做互斥,有如下几种方案:

  1. 只适用两个进程共享,在内存中放一个标志位,一定要声明为volatile,大家基于标志位来互斥,例如为0时第一个可以写,第二个就等待,为1时第一个等待,第二个可以写/读
  2. 也只适用两个进程,是用信号,大家等待不同的信号,第一个写完了发送信号2,等待信号1,第二个等待信号2,收到后读取/写入完,发送信号1,它不是用更多进程是因为虽然父进程可以向不同子进程分别发送信号,但是子进程收到信号会同时访问共享内存,产生不同子进程间的竞态条件,如果用多块共享内存,又存在子进程发送结果通知信号时,父进程收到信号后,不知道是谁发送,也意味着不知道该访问哪块共享内存,即使子进程发送不同的结果通知信号,因为等待信号的一定是阻塞的,如果某个子进程意外终止,父进程将永远阻塞下去,而不能超时处理
  3. 采用信号量或者msgctl自己的加锁、解锁功能,不过后者只适用于linux

信号量

信号量是一种用于提供不同进程间或一个进程间的不同线程间线程同步手段的原语,systemV信号量在内核中维护
二值信号量 : 其值只有0、1 两种选择,0表示资源被锁,1表示资源可用;
计数信号量:其值在0 和某个限定值之间,不限定资源数只在0 1 之间;
计数信号量集 :多个信号量的集合组成信号量集

总结

管道是最弱的,只适合有限场景;

消息队列能适合大部分场景,缺点是默认缓冲也比较小,不过这个可以调整,前提是你有管理员权限;

共享内存是最强大的,只是要做互斥;

线程池的原理及创建方式

原理

线程池的优点

  1. 线程是稀缺资源,使用线程池可以减少创建和销毁线程的次数,每个工作线程都可以重复使用
  2. 可以根据系统的承受能力,调整线程中工作线程的数量,防止因为消耗过多内存导致服务器崩溃
  3. 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。

线程池的创建

public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,
                            long keepAliveTime,
                            TimeUnit unit,
                            BlockingQueue<Runnable> workQueue,
                            RejectedExecutionHandler handler)
  • corePoolSize:线程池核心线程数,在没有设置 allowCoreThreadTimeOut 为 true 的情况下,核心线程会在线
    程池中一直存活,即使处于闲置状态。
  • maximumPoolSize:线程池最大线程数量当活动线程(核心线程+非核心线程)达到这个数值后,后续任务将
    会根据 RejectedExecutionHandler 来进行拒绝策略处理。
  • keepAliverTime:当活跃线程数大于核心线程数时,空闲的多余线程最大存活时间,超过该时长,非核心线程就会被回收。若线程池通设置核心线程也允许 timeOut,即 allowCoreThreadTimeOut 为 true,则该时长同样会作用于核心线程,在超过aliveTime 时,核心线程也会被回收,AsyncTask 配置的线程池就是这样设置的
  • unit:存活时间的单位
  • workQueue:存放任务的队列,通过线程池的 execute() 方法提交的 Runnable 对象会存储在该队列中
  • handler:超出线程范围和队列容量的任务的处理程序

任务阻塞队列BlockingQueue

任务队列 workQueue 是用于存放不能被及时处理掉的任务的一个队列,它是 一个 BlockingQueue 类型。

关于 BlockingQueue,虽然它是 Queue 的子接口,但是它的主要作用并不是容器,而是作为线程同步的工具,他有一个特征,当生产者试图向 BlockingQueue 放入(put)元素,如果队列已满,则该线程被阻塞;当消费者试图从 BlockingQueue 取出(take)元素,如果队列已空,则该线程被阻塞。

常用的阻塞队列具体类

  1. ArrayBlockingQueue
  2. LinkedBlockingQueue
  3. PriorityBlockingQueue
  4. LinkedBlockingDeque

无界队列

队列大小无限制,常用的为无界的LinkedBlockingQueue,使用该队列做为阻塞队列时要尤其当心,当任务耗时较长时可能会导致大量新任务在队列中堆积最终导致OOM。

有界队列

常用的有两类,一类是遵循FIFO原则的队列如ArrayBlockingQueue与有界的LinkedBlockingQueue,另一类是优先级队列如PriorityBlockingQueue。PriorityBlockingQueue中的优先级由任务的Comparator决定。
使用有界队列时队列大小需和线程池大小互相配合,线程池较小有界队列较大时可减少内存消耗,降低cpu使用率和上下文切换,但是可能会限制系统吞吐量。

同步移交

如果不希望任务在队列中等待而是希望将任务直接移交给工作线程,可使用SynchronousQueue作为等待队列。SynchronousQueue不是一个真正的队列,而是一种线程之间移交的机制。要将一个元素放入
SynchronousQueue中,必须有另一个线程正在等待接收这个元素。只有在使用无界线程池或者有饱和策略时才建议使用该队列。

BlockingQueue的添加和删除方法

添加方法:

  1. add:添加元素到队列里,添加成功返回true,由于容量满了添加失败会抛出IllegalStateException异常
  2. offer:添加元素到队列里,添加成功返回true,添加失败返回false
  3. put:添加元素到队列里,如果容量满了会阻塞直到容量不满

删除方法:

  1. poll:删除队列头部元素,如果队列为空,返回null。否则返回元素。
  2. remove:基于对象找到对应的元素,并删除。删除成功返回true,否则返回false
  3. take:删除队列头部元素,如果队列为空,一直阻塞到队列有元素并删除

ArrayBlockingQueue底层实现

ArrayBlockingQueue的原理就是使用一个可重入锁和这个锁生成的两个条件对象进行并发控制(classictwo-condition algorithm)。
ArrayBlockingQueue是一个带有长度的阻塞队列,初始化的时候必须要指定队列长度,且指定长度之后不允许进行修改。

ArrayBlockingQueue带有的属性
//存储队列元素的数组,是个循环数组
final Object[] items;

//拿数据的索引,用于take,poll,peek,remove方法
int takeIndex;

// 放数据的索引,用于put,offer,add方法
int putIndex;

// 元素个数
int count;

// 可重入锁
final ReentrantLock lock;

// notEmpty条件对象,由lock创建
private final Condition notEmpty;

// notFull条件对象,由lock创建
private final Condition notFull;
ArrayBlockingQueue数据的添加(add,offer,put)

add方法内部调用offer方法,如果队列满了,抛出IllegalStateException异常,否则返回true

offer方法如果队列满了,返回false,否则返回true

add方法和offer方法不会阻塞线程,put方法如果队列满了会阻塞线程,直到有线程消费了队列里的数据才有可能被唤醒。

这3个方法内部都会使用可重入锁保证原子性。

add方法:

public boolean add(E e) {
    if (offer(e))
    	return true;
    else
    	throw new IllegalStateException("Queue full");
}

offer方法:

public boolean offer(E e) {
    checkNotNull(e); // 不允许元素为空
    final ReentrantLock lock = this.lock;
    lock.lock(); // 加锁,保证调用offer方法的时候只有1个线程
    try {
        if (count == items.length) // 如果队列已满
        	return false; // 直接返回false,添加失败
        else {
            insert(e); // 数组没满的话调用insert方法
            return true; // 返回true,添加成功
        }
    } finally {
    	lock.unlock(); // 释放锁,让其他线程可以调用offer方法
    }
}
private void insert(E x) {
    items[putIndex] = x; // 元素添加到数组里
    putIndex = inc(putIndex); // 放数据索引+1,当索引满了变成0
    ++count; // 元素个数+1
    notEmpty.signal(); // 使用条件对象notEmpty通知,比如使用take方法的时候队列里没有数据,被阻塞。这个时候队列insert了一条数据,需要调用signal进行通知
}

put方法:

public void put(E e) throws InterruptedException {
    checkNotNull(e); // 不允许元素为空
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly(); // 加锁,保证调用put方法的时候只有1个线程
    try {
        while (count == items.length) // 如果队列满了,阻塞当前线程,并加入到条件对象
        notFull的等待队列里
        notFull.await(); // 线程阻塞并被挂起,同时释放锁
        insert(e); // 调用insert方法
    } finally {
    	lock.unlock(); // 释放锁,让其他线程可以调用put方法
    }
}
ArrayBlockingQueue数据的删除(poll、take、remove)

poll方法对于队列为空的情况,返回null,否则返回队列头部元素。

remove方法取的元素是基于对象的下标值,删除成功返回true,否则返回false。

poll方法和remove方法不会阻塞线程。

take方法对于队列为空的情况,会阻塞并挂起当前线程,直到有数据加入到队列中。

这3个方法内部都会调用notFull.signal方法通知正在等待队列满情况下的阻塞线程。

poll方法:

public E poll() {
    final ReentrantLock lock = this.lock;
    lock.lock(); // 加锁,保证调用poll方法的时候只有1个线程
    try {
        return (count == 0) ? null : extract(); // 如果队列里没元素了,返回null,否则调用extract方法
    } finally {
    	lock.unlock(); // 释放锁,让其他线程可以调用poll方法
    }
}
private E extract() {
    final Object[] items = this.items;
    E x = this.<E>cast(items[takeIndex]); // 得到取索引位置上的元素
    items[takeIndex] = null; // 对应取索引上的数据清空
    takeIndex = inc(takeIndex); // 取数据索引+1,当索引满了变成0
    --count; // 元素个数-1
    notFull.signal(); // 使用条件对象notFull通知,比如使用put方法放数据的时候队列已满,被阻塞。这个时候消费了一条数据,队列没满了,就需要调用signal进行通知
    return x; // 返回元素
}

take方法:

public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly(); // 加锁,保证调用take方法的时候只有1个线程
    try {
        while (count == 0) // 如果队列空,阻塞当前线程,并加入到条件对象notEmpty的等待队
        列里
        notEmpty.await(); // 线程阻塞并被挂起,同时释放锁
        return extract(); // 调用extract方法
    } finally {
    	lock.unlock(); // 释放锁,让其他线程可以调用take方法
    }
}

remove方法:

public boolean remove(Object o) {
    if (o == null) return false;
    final Object[] items = this.items;
    final ReentrantLock lock = this.lock;
    lock.lock();// 加锁,保证调用remove方法的时候只有1个线程
    try {
        //队列不为空时进行移除
        if (count > 0) {
             //引用下次元素入列位置的索引
            final int putIndex = this.putIndex;
            //引用下次元素出列位置的索引
            int i = takeIndex;
            /**从出列位置开始循环查找数组中的元素,
                直到找到了要删除的元素的索引,执行
                removeAt(i)根据元素索引删除元素的方法,
                最后返回true表示删除成功,否则返回false删除失败*/
            do {// 遍历元素
                if (o.equals(items[i])) {// 两个对象相等的话
                    removeAt(i);// 调用removeAt方法
                    return true;// 删除成功,返回true
                }
                if (++i == items.length)
                    i = 0;
            } while (i != putIndex);
        }
        return false;// 删除失败,返回false
    } finally {
        lock.unlock();// 释放锁,让其他线程可以调用remove方法
    }
}
//根据索引删除队列中的元素
void removeAt(final int removeIndex) {
    final Object[] items = this.items;
     //如果要删除的元素就是队列中的第一个元素,直接移除,并且将出列索引+1,队列长度-1
    if (removeIndex == takeIndex) {
        // removing front item; just advance
        items[takeIndex] = null;
        if (++takeIndex == items.length)
            takeIndex = 0;
        count--;
        if (itrs != null)
            itrs.elementDequeued();
    } else {
         //引用下次元素入列位置的索引
        final int putIndex = this.putIndex;
         /**遍历数组,删除元素,并将删除的元素留下的空位填充满*/
        for (int i = removeIndex;;) {
            //索引归零
            int next = i + 1;
            if (next == items.length)
                next = 0;
              //中间的元素
            if (next != putIndex) {
                items[i] = items[next];
                i = next;
            } else {//遍历到了最后一个元素
                items[i] = null;
                //下次入列的索引重置,并跳出循环
                this.putIndex = i;
                break;
            }
        }
        count--;
        if (itrs != null)
            itrs.removedAt(removeIndex);
    }
    notFull.signal();
}

LinkedBlockingQueue底层实现

LinkedBlockingQueue是一个使用链表完成队列操作的阻塞队列。链表是单向链表,而不是双向链表。内部使用放锁和拿锁,这两个锁实现阻塞。

LinkedBlockingQueue带有的属性
// 容量大小
private final int capacity;
// 元素个数,因为有2个锁,存在竞态条件,使用AtomicInteger
private final AtomicInteger count = new AtomicInteger(0);
// 头结点
private transient Node<E> head;
// 尾节点
private transient Node<E> last;
// 拿锁
private final ReentrantLock takeLock = new ReentrantLock();
// 拿锁的条件对象
private final Condition notEmpty = takeLock.newCondition();
// 放锁
private final ReentrantLock putLock = new ReentrantLock();
// 放锁的条件对象
private final Condition notFull = putLock.newCondition();

ArrayBlockingQueue只有1个锁,添加数据和删除数据的时候只能有1个被执行,不允许并行执行。

而LinkedBlockingQueue有2个锁,放锁和拿锁,添加数据和删除数据是可以并行进行的,当然添加数据和删除数据的时候只能有1个线程各自执行。

LinkedBlockingQueue数据的添加(add、offer、put)

add方法内部调用offer方法:

public boolean offer(E e) {
    if (e == null) throw new NullPointerException(); // 不允许空元素
    final AtomicInteger count = this.count;
    if (count.get() == capacity) // 如果容量满了,返回false
    	return false;
    int c = -1;
    Node<E> node = new Node(e); // 容量没满,以新元素构造节点
    final ReentrantLock putLock = this.putLock;
    putLock.lock(); // 放锁加锁,保证调用offer方法的时候只有1个线程
    try {
        if (count.get() < capacity) { 
        	enqueue(node); // 节点添加到链表尾部
        	c = count.getAndIncrement(); // 元素个数+1
        	if (c + 1 < capacity) // 如果容量还没满
            // 在放锁的条件对象notFull上唤醒正在等待的线程,表示可以再次往队列里面加数据了,队列还没满
        		notFull.signal(); 
        }
    } finally {
    	putLock.unlock(); // 释放放锁,让其他线程可以调用offer方法
    }
// 由于存在放锁和拿锁,这里可能拿锁一直在消费数据,count会变化。这里的if条件表示如果队列中还有1条数据
    if (c == 0) 
        // 在拿锁的条件对象notEmpty上唤醒正在等待的1个线程,表示队列里还有1条数据,可以进行消费
    	signalNotEmpty(); 
    return c >= 0; // 添加成功返回true,否则返回false
}

put方法:

public void put(E e) throws InterruptedException {
    if (e == null) throw new NullPointerException(); // 不允许空元素
    int c = -1;
    Node<E> node = new Node(e); // 以新元素构造节点
    final ReentrantLock putLock = this.putLock;
    final AtomicInteger count = this.count;
    putLock.lockInterruptibly(); // 放锁加锁,保证调用put方法的时候只有1个线程
    try {
        while (count.get() == capacity) { // 如果容量满了
            notFull.await(); // 阻塞并挂起当前线程
        }
        enqueue(node); // 节点添加到链表尾部
        c = count.getAndIncrement(); // 元素个数+1
        if (c + 1 < capacity) // 如果容量还没满
            notFull.signal(); // 在放锁的条件对象notFull上唤醒正在等待的线程,表示可以再次往队列里面加数据了,队列还没满
    } finally {
        putLock.unlock(); // 释放放锁,让其他线程可以调用put方法
    }
    if (c == 0) // 由于存在放锁和拿锁,这里可能拿锁一直在消费数据,count会变化。这里的if条件 表示如果队列中还有1条数据
        // 在拿锁的条件对象notEmpty上唤醒正在等待的1个线程,表示队列里还有1条数据,可以进行消费
        signalNotEmpty();
}

​ LinkedBlockingQueue的添加数据方法add,put,offer跟ArrayBlockingQueue一样,不同的是它们的底层实现不一样。
​ ArrayBlockingQueue中放入数据阻塞的时候,需要消费数据才能唤醒。而LinkedBlockingQueue中放入数据阻塞的时候,因为它内部有2个锁,可以并行执行放入数据和消费数据,不仅在消费数据的时候进行唤醒插入阻塞的线程,同时在插入的时候如果容量还没满,也会唤醒插入阻塞的线程。

LinkedBlockingQueue数据的删除(poll、take、remove)

poll方法:

 public E poll() {
     final AtomicInteger count = this.count;
     if (count.get() == 0) // 如果元素个数为0
         return null; // 返回null
     E x = null;
     int c = -1;
     final ReentrantLock takeLock = this.takeLock;
     takeLock.lock(); // 拿锁加锁,保证调用poll方法的时候只有1个线程
     try {
         if (count.get() > 0) { // 判断队列里是否还有数据
             x = dequeue(); // 删除头结点
             c = count.getAndDecrement(); // 元素个数-1
             if (c > 1) // 如果队列里还有元素
                 notEmpty.signal(); // 在拿锁的条件对象notEmpty上唤醒正在等待的线程,表示队列里还有数据,可以再次消费
         }
     } finally {
         takeLock.unlock(); // 释放拿锁,让其他线程可以调用poll方法
     }
     if (c == capacity) // 由于存在放锁和拿锁,这里可能放锁一直在添加数据,count会变化。这里 的if条件表示如果队列中还可以再插入数据
         signalNotFull(); // 在放锁的条件对象notFull上唤醒正在等待的1个线程,表示队列里还能再次添加数据
     return x;
 }

take方法:

public E take() throws InterruptedException {
    E x;
    int c = -1;
    final AtomicInteger count = this.count;
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lockInterruptibly(); // 拿锁加锁,保证调用take方法的时候只有1个线程
    try {
        while (count.get() == 0) { // 如果队列里已经没有元素了
            notEmpty.await(); // 阻塞并挂起当前线程
        }
        x = dequeue(); // 删除头结点
        c = count.getAndDecrement(); // 元素个数-1
        if (c > 1) // 如果队列里还有元素
            notEmpty.signal(); // 在拿锁的条件对象notEmpty上唤醒正在等待的线程,表示队列里还有数据,可以再次消费
    } finally {
        takeLock.unlock(); // 释放拿锁,让其他线程可以调用take方法
    }
    if (c == capacity) // 由于存在放锁和拿锁,这里可能放锁一直在添加数据,count会变化。这里 的if条件表示如果队列中还可以再插入数据
        signalNotFull(); // 在放锁的条件对象notFull上唤醒正在等待的1个线程,表示队列里还能再次添加数据
    return x;
}

remove方法:

public boolean remove(Object o) {
    if (o == null) return false;
    fullyLock(); // remove操作要移动的位置不固定,2个锁都需要加锁
    try {
        for (Node<E> trail = head, p = trail.next; // 从链表头结点开始遍历
             p != null;
             trail = p, p = p.next) {
            if (o.equals(p.item)) { // 判断是否找到对象
                unlink(p, trail); // 修改节点的链接信息,同时调用notFull的signal方法
                return true;
            }
        }
        return false;
    } finally {
        fullyUnlock(); // 2个锁解锁
    }
}

LinkedBlockingQueue的take方法对于没数据的情况下会阻塞,poll方法删除链表头结点,remove方法删除指定的对象。

需要注意的是remove方法由于要删除的数据的位置不确定,需要2个锁同时加锁。

任务拒绝类型

ThreadPoolExecutor.AbortPolicy:(抛出异常)

当线程池中的数量等于最大线程数时抛 java.util.concurrent.RejectedExecutionException 异常,涉及到该异常的任务也不会被执行,线程池默认的拒绝策略就是该策略。

public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    throw new RejectedExecutionException("Task " + r.toString() +" rejected from " +
    										e.toString());
}

ThreadPoolExecutor.DiscardPolicy():(默默丢弃)

当线程池中的数量等于最大线程数时,默默丢弃不能执行的新加任务,不报任何异常。

public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
}

ThreadPoolExecutor.CallerRunsPolicy():(重试机制)

当线程池中的数量等于最大线程数时,重试添加当前的任务;它会自动重复调用execute()方法。换言之将任务回退给调用者来直接运行

public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    if (!e.isShutdown()) {
   		 r.run();
    }
}

ThreadPoolExecutor.DiscardOldestPolicy():(抛弃老任务)

当线程池中的数量等于最大线程数时,抛弃线程池中工作队列头部的任务(即等待时间最久的任务),并执行新传入的任务。

public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    if (!e.isShutdown()) {
        e.getQueue().poll();
        e.execute(r);
    }
}

Executor框架的两级调度模型

image-20220321170220124

​ JAVA线程既是工作单元,也是执行机制。而在Executor框架中,我们将工作单元与执行机制分离开来。Runnable和Callable是工作单元(也就是俗称的任务),而执行机制由Executor来提供。这样一来Executor是基于生产者消费者模式的,提交任务的操作相当于生成者,执行任务的线程相当于消费者。

1.Executor是异步任务执行框架的基础,该框架能够支持多种不同类型的任务执行策略,Executor只提供了一个执行方法,任务时Runnable类型,不支持Callable类型

public interface Executor{
    void execute(Runnable command);
}

2.ExecutorService接口实现了Executor接口,主要提供了关闭线程池和submit方法

public interface ExecutorService extends Executor{
    List<Runnable> shutdownNow();
    boolean isTerminated();
    <T> Future<T> SUBMIT(Callable<T> task);
}

另外ExecutorService接口有两个重要的实现类:

ThreadPoolExecutor与ScheduledThreadPoolExecutor。

  • ThreadPoolExecutor是线程池的核心实现类,用来执行被提交的任务
  • ScheduledThreadPoolExecutor是一个实现类,可以在给定的延迟后运行任务,或者定期执行命令

Executors工厂

推荐使用Exectuors工厂方法来创建线程池,Executors可以创建3种类型ThreadPoolExecutorSingleThreadExecutor、FixedThreadExecutor和CachedThreadPool。

newSingleThreadExecutor:单线程线程池

ExecutorService threadPool = Executors.newSingleThreadExecutor();
public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1, 1,
        0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>()));
}

从源码来看可以知道,单线程线程池的创建也是通过ThreadPoolExecutor,里面的核心线程数和线程数都是1,并且工作队列使用的是无界队列。由于是单线程工作,每次只能处理一个任务,所以后面所有的任务都被阻塞在工作队列中,只能一个个任务执行

newScheduledThreadPool:定长线程池,支持定时及周期性任务执行

ExecutorService threadPool = Executors.newScheduledThreadPool(5);
public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
          new DelayedWorkQueue());
}

ScheduledThreadPoolExecutor的父类即ThreadPoolExecutor,因此这里各参数含义和上面一样。值得关心的是DelayedWorkQueue这个阻塞对列,在上面没有介绍,它作为静态内部类就在ScheduledThreadPoolExecutor中进行了实现。

DelayedWorkQueue是一个无界队列,它能按一定的顺序对工作队列中的元素进行排列。因此这里
设置的最大线程数 Integer.MAX_VALUE没有任何意义。

newFixedThreadExecutor:固定大小线程池

ExecutorService threadPool = Executors.newFixedThreadPool(5);
public static ExecutorService newFixedThreadPool(int nThreads) {
	return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,new 												LinkedBlockingQueue<Runnable>());
}

这个与单线程类似,只是创建了固定大小的线程数量。

newCachedThreadPool:无界线程池

ExecutorService threadPool = Executors.newCachedThreadPool();
public static ExecutorService newCachedThreadPool () {
            return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                    60L, TimeUnit.SECONDS,
                    new SynchronousQueue<Runnable>());
        }

无界线程池意味着没有工作队列,任务进来就执行,线程数量不够就创建,与前面两个的区别是:空闲的线程会被回收掉,空闲的时间是60s。这个适用于执行很多短期异步的小程序或者负载较轻的服务器。

newWorkStealingPool:拥有多个任务队列(以便减少连接数)的线程池

ExecutorService executorService = Executors.newWorkStealingPool();
public static ExecutorService newWorkStealingPool() {
    return new ForkJoinPool(Runtime.getRuntime().availableProcessors(),
						    ForkJoinPool.defaultForkJoinWorkerThreadFactory,
						    null, true);
}

如果不主动设置它的并发数,那么这个方法就会以当前机器的CPU处理器个数为线程个数,这个线程池会并行处理任务,不能够保证任务执行的顺序。

自带线程池的坑

  1. Executors.newFixedThreadPool(10);
    固定大小的线程池:
    它的实现new ThreadPoolExecutor(10, 10, 0L, TimeUnit.MILLISECONDS,newLinkedBlockingQueue());
    初始化一个指定线程数的线程池,其中corePoolSize == maximumPoolSize,使用LinkedBlockingQuene作为阻塞队列,当线程池没有可执行任务时,也不会释放线程。由于LinkedBlockingQuene的特性,这个队列是无界的,若消费不过来,会导致内存被任务队列占满,最终oom;
  2. Executors.newCachedThreadPool();
    缓存线程池:
    它的实现new ThreadPoolExecutor(0,Integer.MAX_VALUE,60L, TimeUnit.SECONDS,newSynchronousQueue());
    初始化一个可以缓存线程的线程池,默认缓存60s,线程池的线程数可达到Integer.MAX_VALUE,即2147483647,内部使用SynchronousQueue作为阻塞队列;和newFixedThreadPool创建的线程池不同,newCachedThreadPool在没有任务执行时,当线程的空闲时间超过keepAliveTime,会自动释放线程资源,当提交新任务时,如果没有空闲线程,则创建新线程执行任务,会导致一定的系统开销,因为线程池的最大值了Integer.MAX_VALUE,会导致无限创建线程;所以,使用该线程池时,一定要注意控制并发的任务数,否则创建大量的线程会导致严重的性能问题;
  3. Executors.newSingleThreadExecutor()
    单线程线程池:
    同newFixedThreadPool线程池一样,队列用的是LinkedBlockingQueue无界队列,可以无限的往里面添加任务,直到内存溢出;

Callable、Future、FutureTash详解

Callable与Future是在JAVA的后续版本中引入进来的,Callable类似于Runnable接口,实现Callable接口的类与实现Runnable的类都是可以被线程执行的任务。

三者之间的关系

Callable是Runnable封装的异步运算任务。
Future用来保存Callable异步运算的结果
FutureTask封装Future的实体类

Callable与Runnable的区别

  1. Callable定义的方法是call,而Runnable定义的方法是run。
  2. call方法有返回值,而run方法是没有返回值的。
  3. call方法可以抛出异常,而run方法不能抛出异常。

Future

Future表示异步计算的结果,提供了以下方法,主要是判断任务是否完成、中断任务、获取任务执行结果

public interface Future<V> {

    boolean cancel(boolean mayInterruptIfRunning);

    boolean isCancelled();

    boolean isDone();

    V get() throws InterruptedException, ExecutionException;

    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

FutureTask实现了Future接口

可取消的异步计算,此类提供了对Future的基本实现,仅在计算完成时才能获取结果,如果计算尚未完成,则阻塞get方法。

public class FutureTask<V> implements RunnableFuture<V>
public interface RunnableFuture<V> extends Runnable, Future<V>

FutureTask不仅实现了Future接口,还实现了Runnable接口,所以不仅可以将FutureTask当成一个任务交给Executor来执行,还可以通过Thread来创建一个线程。

Callable与FutureTask

通过调用FutureTask的状态设置的方法,演示了状态的变迁

public class MyCallableTask implements Callable<Integer>
{
    @Override
     public Integer call() throws Exception
     {
 		System.out.println("callable do somothing");
 		Thread.sleep(5000);
 		return new Random().nextInt(100);
 		}
}

public class CallableTest
 {
     public static void main(String[] args) throws Exception
     {
         Callable<Integer> callable = new MyCallableTask();
         FutureTask<Integer> future = new FutureTask<Integer>(callable);
         Thread thread = new Thread(future);
         thread.start();
         Thread.sleep(100);
         //尝试取消对此任务的执行,尝试取消对任务的执行,该方法如果由于任务已完成、已取消则返回false,如果能够取消还未完成的任务,则返回true,该DEMO中由于任务还在休眠状态,所以可以取消成功。
         future.cancel(true);
         //判断任务取消是否成功:如果在任务正常完成前将其取消,则返回true
         System.out.println("future is cancel:" + future.isCancelled());
         if(!future.isCancelled())
         {
         	System.out.println("future is cancelled");
         }
         //判断任务是否完成:如果任务完成,则返回true,以下几种情况都属于任务完成:正常终止、异常或者取消而完成。我们的DEMO中,任务是由于取消而导致完成
         System.out.println("future is done:" + future.isDone());
         if(!future.isDone())
         {
             //获取异步线程执行的结果,我这个DEMO中没有执行到这里,需要注意的是,future.get方法会阻塞当前线程, 直到任务执行完成返回结果为止
         	System.out.println("future get=" + future.get());
         }else{
             //任务已完成
             System.out.println("task is done");
         }
     }
 }

执行结果:

callable do somothing
future is cancel:true
future is done:true
task is done

Callable与Future

public class CallableThread  implements Callable<String>{
    @Override
    public String call() throws Exception{
        System.out.println("进入Call方法,开始休眠,休眠时间为:" +
                System.currentTimeMillis());
        Thread.sleep(10000);
        return "今天停电";
    }
    public static void main(String[] args) throws Exception{
        ExecutorService es = Executors.newSingleThreadExecutor();
        Callable<String> call = new CallableThread();
        Future<String> fu = es.submit(call);
        es.shutdown();
        Thread.sleep(5000);
        System.out.println("主线程休眠5秒,当前时间" + System.currentTimeMillis());
        String str = fu.get();
        System.out.println("Future已拿到数据,str=" + str + ";当前时间为:" +
                System.currentTimeMillis());
    }

}

执行结果:

进入Call方法,开始休眠,休眠时间为:1647855113947
主线程休眠5秒,当前时间1647855118948
Future已拿到数据,str=今天停电;当前时间为:1647855123948

这里的future是直接扔到线程池里面去执行的。由于要打印任务的执行结果,所以从执行结果来看,主线程虽然休眠了5s,但是从Call方法执行到拿到任务的结果,这中间的时间差正好是10s,说明get方法会阻塞当前线程直到任务完成。

通过FutureTask也可以达到同样效果

public class CallableThread implements Callable<String> {
    @Override
    public String call() throws Exception {
        System.out.println("进入Call方法,开始休眠,休眠时间为:" +
                System.currentTimeMillis());
        Thread.sleep(10000);
        return "今天停电";
    }

    public static void main(String[] args) throws Exception {
        ExecutorService es = Executors.newSingleThreadExecutor();
        Callable<String> call = new CallableThread();
        FutureTask<String> task = new FutureTask<String>(call);
        es.submit(task);
        es.shutdown();
        Thread.sleep(5000);
        System.out.println("主线程等待5秒,当前时间为:" + System.currentTimeMillis());
        String str = task.get();
        System.out.println("Future已拿到数据,str=" + str + ";当前时间为:" +
                System.currentTimeMillis());
    }
}

以上的组合可以给我们带来这样的一些变化:
如有一种场景中,方法A返回一个数据需要10s,A方法后面的代码运行需要20s,但是这20s的执行过程中,只有后面10s依赖于方法A执行的结果。如果与以往一样采用同步的方式,势必会有10s的时间被浪费,如果采用前面两种组合,则效率会提高:
1、先把A方法的内容放到Callable实现类的call()方法中
2、在主线程中通过线程池执行A任务
3、执行后面方法中10秒不依赖方法A运行结果的代码
4、获取方法A的运行结果,执行后面方法中10秒依赖方法A运行结果的代码
这样代码执行效率一下子就提高了,程序不必卡在A方法处。

java多线程安全机制

线程安全的定义就是:如果线程执行过程中不会产生共享资源的冲突就是线程安全的。

互斥同步锁(悲观锁)

1)Synchorized,由语言级别实现的互斥同步锁
2)ReentrantLock,是API层面的互斥同步锁,需要程序自己打开并在finally中关闭锁

ReentrantLock你synchronized更加灵活体现在三个方面,等待可中断,公平锁以及绑定多个条件

​ 互斥同步锁就是以互斥的手段达到顺序访问的目的。操作系统提供了很多互斥机制比如信号量,互斥量,临界区资源等来控制在某一个时刻只能有一个或者一组线程访问同一个资源。互斥同步锁都是可重入锁,好处是可以保证不会死锁。但是因为涉及到核心态和用户态的切换,因此比较消耗性能。

非阻塞同步锁

原子类(CAS):
非阻塞同步锁也叫乐观锁,相比悲观锁来说,它会先进行资源在工作内存中的更新,然后根据与主存中旧值的对比来确定在此期间是否有其他线程对共享资源进行了更新,如果旧值与期望值相同,就认为没有更新,可以把新值写回内存,否则就一直重试直到成功。它的实现方式依赖于处理器的机器指令:CAS(Compare And Swap)

非阻塞锁是不可重入的,否则会造成死锁。

无同步方案

1)可重入代码
在执行的任何时刻都可以中断-重入执行而不会产生冲突。特点就是不会依赖堆上的共享资源
2)ThreadLocal/Volaitile
线程本地的变量,每个线程获取一份共享变量的拷贝,单独进行处理。

3)线程本地存储
如果一个共享资源一定要被多线程共享,可以尽量让一个线程完成所有的处理操作,比如生产者消费者模式中,一般会让一个消费者完成对队列上资源的消费。典型的应用是基于请求-应答模式的web服务器的设计

Volatile原理及使用场景

Volatile原理

volatile变量进行写操作时,JVM 会向处理器发送一条 Lock 前缀的指令,将这个变量所在缓存行的数据写会到系统内存。
Lock 前缀指令实际上相当于一个内存屏障(也成内存栅栏),它确保指令重排序时不会把其 后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内 存屏障这句指令时,在它前面的操作已经全部完成。

image-20220321194359566

Volatile适用场景

状态标志

​ 用于指示发生了一个重要的一次性事件,例如完成初始化或请求停机。线程1执行doWork()的过程中,可能有另外的线程2调用了shutdown,所以boolean变量必须是volatile

`volatile` boolean shutdownRequested;

...

 public void shutdown() {
    shutdownRequested = true;
 }

 public void doWork() {
     while (!shutdownRequested) {
    // do stuff
     }
}

​ 这种类型的状态标记的一个公共特性是:通常只有一种状态转换; shutdownRequested 标志从 false转换为 true ,然后程序停止。这种模式可以扩展到来回转换的状态标志,但是只有在转换周期不被察觉的情况下才能扩展(从 false 到 true ,再转换到 false )。此外,还需要某些原子状态转换机制,例如原子变量。

一次性安全发布(one-time safe publication)

​ 在缺乏同步的情况下,可能会遇到某个对象引用的更新值(由另一个线程写入)和该对象状态的旧值同时存在。
​ 这就是造成著名的双重检查锁定(double-checked-locking)问题的根源,其中对象引用在没有同步的情况下进行读操作,产生的问题是您可能会看到一个更新的引用,但是仍然会通过该引用看到不完全构造的对象。

//注意`volatile`!!!!!!!!!!!!!!!!!
private `volatile` static Singleton instace;

public static Singleton getInstance(){
        //第一次null检查
        if(instance == null){
             `synchronized`(Singleton.class) { //1
                //第二次null检查
                 if(instance == null){ //2
                 	instance = new Singleton();//3
         		}
     		}
        }
    return instance;
}

如果不用volatile,则因为内存模型允许所谓的“无序写入”,可能导致失败。——某个线程可能会获得一个未完全初始化的实例。

考察上述代码中的 //3 行。此行代码创建了一个 Singleton 对象并初始化变量 instance 来引用此对象。这行代码的问题是:在Singleton 构造函数体执行之前,变量instance 可能成为非 null 的!

假设上述代码执行以下事件序列:

  1. 线程 1 进入 getInstance() 方法。
  2. 由于 instance 为 null,线程 1 在 //1 处进入synchronized 块。
  3. 线程 1 前进到 //3 处,但在构造函数执行之前,使实例成为非null。
  4. 线程 1 被线程 2 预占。
  5. 线程 2 检查实例是否为 null。因为实例不为 null,线程 2 将instance 引用返回,返回一个构造完
    整但部分初始化了的Singleton 对象。
  6. 线程 2 被线程 1 预占。
  7. 线程 1 通过运行 Singleton 对象的构造函数并将引用返回给它,来完成对该对象的初始化。

独立观察(independent observation)

​ 安全使用 volatile 的另一种简单模式是:定期 “发布” 观察结果供程序内部使用。【例如】假设有一种环境传感器能够感觉环境温度。一个后台线程可能会每隔几秒读取一次该传感器,并更新包含当前文档的volatile 变量。然后,其他线程可以读取这个变量,从而随时能够看到最新的温度值。
​ 使用该模式的另一种应用程序就是收集程序的统计信息。【例】如下代码展示了身份验证机制如何记忆最近一次登录的用户的名字。将反复使用 lastUser 引用来发布值,以供程序的其他部分使用。

public class UserManager {
    public `volatile` String lastUser; //发布的信息

    public boolean authenticate(String user, String password) {
         boolean valid = passwordIsValid(user, password);
         if (valid) {
             User u = new User();
             activeUsers.add(u);
             lastUser = user;
         }
        return valid;
    }
}

“volatile bean” 模式

volatile bean 模式的基本原理是:很多框架为易变数据的持有者(例如 HttpSession )提供了容器,但是放入这些容器中的对象必须是线程安全的。

​ 在 volatile bean 模式中,JavaBean 的所有数据成员都是 volatile 类型的,并且 getter 和 setter 方法必须非常普通——即不包含约束!

public class Person {
    private `volatile` String firstname;
    private `volatile` String lastname;
    private `volatile` int age;

    public String getFirstname() {
        return firstname;
    }

    public void setFirstname(String firstname) {
        this.firstname = firstname;
    }

    public String getLastname() {
        return lastname;
    }

    public void setLastname(String lastname) {
        this.lastname = lastname;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

开销较低的“读-写锁”策略

如果读操作远远超过写操作,您可以结合使用内部锁和 volatile 变量来减少公共代码路径的开销。

如下显示的线程安全的计数器,使用 synchronized 确保增量操作是原子的,并使用 volatile 保证当前结果的可见性。如果更新不频繁的话,该方法可实现更好的性能,因为读路径的开销仅仅涉及volatile 读操作,这通常要优于一个无竞争的锁获取的开销。

public class CheesyCounter {

    @GuardedBy("this") private `volatile` int value;

    //读操作,没有`synchronized`,提高性能
    public int getValue(){
        return value;
    }
    //写操作,必须`synchronized`,因为value++不是原子操作
    public `synchronized` int increment(){
        return value++;
    }
}

使用锁进行所有变化的操作,使用 volatile 进行只读操作。
其中,锁一次只允许一个线程访问值,volatile 允许多个线程执行读操作

ThreadLocal原理及源码分析

ThreadLocal原理

ThreadLocal是用来维护本线程的变量的,并不能解决共享变量的并发问题。ThreadLocal是 各线程将值存入该线程的map中,以ThreadLocal自身作为key,需要用时获得的是该线程之前 存入的值。如果存入的是共享变量,那取出的也是共享变量,并发问题还是存在的。

ThreadLocal的适用场景
场景:数据库连接、Session管理

image-20220322130718255

ThreadLocal出现OOM

ThreadLocal变量是维护在Thread内部的,这样的话只要我们的线程不退出,对象的引用就会 一直存在。当线程退出时,Thread类会进行一些清理工作,其中就包含ThreadLocalMapThread调用exit方法如下:

/**
* This method is called by the system to give a Thread
* a chance to clean up before it actually exits.
*/
private void exit() {
    if (group != null) {
        group.threadTerminated(this);
        group = null;
    }
    /* Aggressively null out all reference fields: see bug 4006245 */
    target = null;
    /* Speed the release of some of these resources */
    threadLocals = null;
    inheritableThreadLocals = null;
    inheritedAccessControlContext = null;
    blocker = null;
    uncaughtExceptionHandler = null;
}

ThreadLocal在没有线程池使用的情况下,正常情况下不会存在内存泄露,但是如果使用了线程 池的话,就依赖于线程池的实现,如果线程池不销毁线程的话,那么就会存在内存泄露。

ThreadLocal并不是一个Thread,而是Thread的一个局部变量。

ThreadLocal 的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。

ThreadLocal全部方法和内部类

image-20220322132705708

ThreadLocal公有方法只有四个:get,set,remove,withInitial

image-20220322132751289

ThreadLocal源码分析

线程局部变量在Thread中的位置

既然是线程局部变量,那么理所当然就应该存储在自己的线程对象中,我们可以从 Thread 的源码中找到线程局部变量存储的地方:

publicclass Thread implements Runnable {
    /* Make sure registerNatives is the first thing <clinit> does. */
    private static native void registerNatives();
    static {
        registerNatives();
    }
     /* 与此线程相关的线程本地值。此映射由 ThreadLocal 类维护。 */
    ThreadLocal.ThreadLocalMap threadLocals = null;
    /*
     * 可继承线程与此线程相关的本地值。此映射由 InheritableThreadLocal 类维护。
     */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}

我们可以看到线程局部变量是存储在Thread对象的 threadLocals 属性中,而 threadLocals 属性是一个 ThreadLocal.ThreadLocalMap 对象。ThreadLocalMap为ThreadLocal的静态内部类:

image-20220322134019043

Thread与ThreadLocalMap的关系

Thread和ThreadLocalMap的关系,先看下边这个简单的图,可以看出Thread中的 threadLocals 就是ThreadLocal中的ThreadLocalMap:

image-20220322134321496

到这里应该大致能够感受到上述三者之间微妙的关系,再看一个复杂点的图:

image-20220322134805446

​ 可以看出每个 thread 实例都有一个 ThreadLocalMap 。在上图中的一个Thread的这个ThreadLocalMap中分别存放了3个Entry,默认一个ThreadLocalMap初始化了16个Entry,每一个Entry对象存放的是一个ThreadLocal变量对象。

​ 再简单一点的说就是:一个Thread中只有一个ThreadLocalMap,一个ThreadLocalMap中可以有多个ThreadLocal对象,其中一个ThreadLocal对象对应一个ThreadLocalMap中的一个Entry(也就是说:一个Thread可以依附有多个ThreadLocal对象)。

ThreadLocalMap与WeakReference

ThreadLocalMap 从字面上就可以看出这是一个保存 ThreadLocal 对象的map(其实是以ThreadLocal 对象为Key),不过是经过了两层包装的ThreadLocal对象:

  1. 第一层包装是使用 WeakReference<ThreadLocal<?>> 将 ThreadLocal 对象变成一个弱引用的对象;
  2. 第二层包装是定义了一个专门的类 Entry 来扩展 WeakReference<ThreadLocal<?>> :
static class ThreadLocalMap {

        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
}

类 Entry 很显然是一个保存map键值对的实体, ThreadLocal<?> 为key, 要保存的线程局部变量的值为

value 。 super(k) 调用的 WeakReference 的构造函数,表示将 ThreadLocal<?> 对象转换成弱引用对象,用做key

ThreadLocalMap的构造函数

public class ThreadLocal<T> {
    ...
    /**
             * Construct a new map initially containing (firstKey, firstValue).
             * ThreadLocalMaps are constructed lazily, so we only create
             * one when we have at least one entry to put in it.
             */
    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        table = new Entry[INITIAL_CAPACITY];
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        table[i] = new Entry(firstKey, firstValue);
        size = 1;
        setThreshold(INITIAL_CAPACITY);
    }
    ...
}

可以看出,ThreadLocalMap这个map的实现是使用一个数组 private Entry[] table 来保存键值对的实体,初始大小为16, ThreadLocalMap 自己实现了如何从 key 到 value 的映射:

int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);

使用一个 static 的原子属性 AtomicInteger nextHashCode ,通过每次增加 HASH_INCREMENT =0x61c88647 ,然后 & (INITIAL_CAPACITY - 1) 取得在数组 private Entry[] table 中的索引。

public class ThreadLocal<T> {
    /**
     * ThreadLocals rely on per-thread linear-probe hash maps attached
     * to each thread (Thread.threadLocals and
     * inheritableThreadLocals).  The ThreadLocal objects act as keys,
     * searched via threadLocalHashCode.  This is a custom hash code
     * (useful only within ThreadLocalMaps) that eliminates collisions
     * in the common case where consecutively constructed ThreadLocals
     * are used by the same threads, while remaining well-behaved in
     * less common cases.
     */
    private final int threadLocalHashCode = nextHashCode();
    /**
     * The next hash code to be given out. Updated atomically. Starts at
     * zero.
     */
    private static AtomicInteger nextHashCode =
        new AtomicInteger();

    /**
     * The difference between successively generated hash codes - turns
     * implicit sequential thread-local IDs into near-optimally spread
     * multiplicative hash values for power-of-two-sized tables.
     */
    private static final int HASH_INCREMENT = 0x61c88647;

    /**
     * Returns the next hash code.
     */
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
    ...
}

ThreadLocalMap是一个类似HashMap的集合,只不过自己实现了寻址,也没有HashMap中的put方法,而是set方法。

ThreadLocal中的set方法

 /**
     * Sets the current thread's copy of this thread-local variable
     * to the specified value.  Most subclasses will have no need to
     * override this method, relying solely on the {@link #initialValue}
     * method to set the values of thread-locals.
     *
     * @param value the value to be stored in the current thread's copy of
     *        this thread-local.
     */
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

由于每个thread实例都有一个ThreadLocalMap,所以在进行set的时候,首先根据Thread.currentThread()获取当前线程,然后根据当前线程t,调用getMap(t)获取ThreadLocalMap对象,如果是第一次设置值,ThreadLocalMap对象是空值,所以会进行初始化操作,即调用createMap(t,value)方法

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    table = new Entry[INITIAL_CAPACITY];
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}
 private void setThreshold(int len) {
     threshold = len * 2 / 3;
 }

即是调用上述的构造方法进行构造,这里仅仅是初始化了16个元素的引用数组,并没有初始化16个Entry 对象。而是一个线程中有多少个线程局部对象要保存,那么就初始化多少个 Entry 对象来保存它们。

为什么要用ThreadLocalMap来保存线程局部对象

原因是一个线程拥有的的局部对象可能有很多,这样实现的话,那么不管你一个线程拥有多少个局部变量,都是使用同一个 ThreadLocalMap 来保存的,ThreadLocalMap 中 private Entry[] table 的初始大小是16。超过容量的2/3时,会扩容。

/**
     * Sets the current thread's copy of this thread-local variable
     * to the specified value.  Most subclasses will have no need to
     * override this method, relying solely on the {@link #initialValue}
     * method to set the values of thread-locals.
     *
     * @param value the value to be stored in the current thread's copy of
     *        this thread-local.
     */
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

如果map不为空的情况,会调用 map.set(this, value); 方法,我们看到是以当前 thread的引用为 key, 获得ThreadLocalMap ,然后调用 map.set(this, value); 保存进 private Entry[] table :

/**
         * Set the value associated with key.
         *
         * @param key the thread local object
         * @param value the value to be set
         */
private void set(ThreadLocal<?> key, Object value) {

    // We don't use a fast path as with get() because it is at
    // least as common to use set() to create new entries as
    // it is to replace existing ones, in which case, a fast
    // path would fail more often than not.

    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);

    for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();
        if (k == key) {
            e.value = value;
            return;
        }
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

set(T value) 方法为每个Thread对象都创建了一个ThreadLocalMap,并且将value放入ThreadLocalMap中,ThreadLocalMap作为Thread对象的成员变量保存。那么可以用下图来表示ThreadLocal在存储value时的关系。

image-20220322141302353

ThreadLocal的get方法

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}
private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        return e;
    else
        return getEntryAfterMiss(key, i, e);
}

首先获取ThreadLocalMap对象,由于ThreadLocalMap使用的当前的ThreadLocal作为key,所以传入的参数为this,然后调用getEntry() 方法,通过这个key构造索引,根据索引去table(Entry数组)中去查找线程本地变量,根据下标找到Entry对象,然后判断Entry对象e不为空并且e的引用与传入的key一样则直接返回,如果找不到则调用getEntryAfterMiss() 方法。调用 getEntryAfterMiss 表示直接散列到的位置没找到,那么顺着hash表递增(循环)地往下找,从i开始,一直往下找,直到出现空的槽为止。

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;
    while (e != null) {
        ThreadLocal<?> k = e.get();
        if (k == key)
            return e;
        if (k == null)
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

ThreadLocal的内存回收

ThreadLocal涉及到的两个层面的内存自动回收

在ThreadLocal层面的内存回收

​ 当线程死亡时,那么所有的保存在的线程局部变量就会被回收,其实这里是指线程Thread对象中的
ThreadLocal.ThreadLocalMap threadLocals 会被回收,这是显然的。

ThreadLocalMap 层面的内存回收

​ 如果线程可以活很长的时间,并且该线程保存的线程局部变量有很多(也就是 Entry 对象很多),那么就涉及到在线程的生命期内如何回收 ThreadLocalMap 的内存了,不然的话,Entry对象越多,那么ThreadLocalMap 就会越来越大,占用的内存就会越来越多,所以对于已经不需要了的线程局部变量,就应该清理掉其对应的Entry对象。

​ 使用的方式是,Entry对象的key是WeakReference 的包装,当ThreadLocalMap 的 private Entry[]table ,已经被占用达到了三分之二时 threshold = 2/3 (也就是线程拥有的局部变量超过了10个) ,就会尝试回收 Entry 对象,我们可以看到 ThreadLocalMap.set() 方法中有下面的代码:

if (!cleanSomeSlots(i, sz) && sz >= threshold)
	rehash();

cleanSomeSlots 就是进行回收内存:

 private boolean cleanSomeSlots(int i, int n) {
     boolean removed = false;
     Entry[] tab = table;
     int len = tab.length;
     do {
         i = nextIndex(i, len);
         Entry e = tab[i];
         if (e != null && e.get() == null) {
             n = len;
             removed = true;
             i = expungeStaleEntry(i);
         }
     } while ( (n >>>= 1) != 0);
     return removed;
 }

总结

​ 通过源代码可以看到每个线程都可以独立修改属于自己的副本而不会互相影响,从而隔离了线程.避免了线程访问实例变量发生安全问题. 同时我们也能得出下面的结论:

  1. ThreadLocal只是操作Thread中的ThreadLocalMap对象的集合;
  2. ThreadLocalMap变量属于线程的内部属性,不同的线程拥有完全不同的ThreadLocalMap变量;
  3. 线程中的ThreadLocalMap变量的值是在ThreadLocal对象进行set或者get操作时创建的;
  4. 使用当前线程的ThreadLocalMap的关键在于使用当前的ThreadLocal的实例作为key来存储value值;
  5. ThreadLocal模式至少从两个方面完成了数据访问隔离,即纵向隔离(线程与线程之间的ThreadLocalMap不同)和横向隔离(不同的ThreadLocal实例之间的互相隔离);
  6. 一个线程中的所有的局部变量其实存储在该线程自己的同一个map属性中;
  7. 线程死亡时,线程局部变量会自动回收内存;
  8. 线程局部变量是通过一个 Entry 保存在map中,该Entry 的key是一个 WeakReference包装的ThreadLocal, value为线程局部变量,key 到 value 的映射是通过:ThreadLocal.threadLocalHashCode & (INITIAL_CAPACITY - 1) 来完成的;
  9. 当线程拥有的局部变量超过了容量的2/3(没有扩大容量时是10个),会涉及到ThreadLocalMap中Entry的回收;

对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

Synchronized与Volatile对比

Synchronized与Volatile的区别

  1. volatile主要应用在多个线程对实例变量更改的场合,刷新主内存共享变量的值从而使得各个线程可以获得最新的值,线程读取变量的值需要从主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。另外,synchronized还会创建一个内存屏障,内存屏障指令保证了所有CPU操作结果都会直接刷到主存中(即释放锁前),从而保证了操作的内存可见性,同时也使得先获得这个锁的线程的所有操作
  2. volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的。
  3. volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞,比如多个线程争抢synchronized锁对象时,会出现阻塞。
  4. volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性,因为线程获得锁才能进入临界区,从而保证临界区中的所有语句全部得到执行。
  5. volatile标记的变量不会被编译器优化,可以禁止进行指令重排;synchronized标记的变量可以被编译器优化。

Synchronized与Volatile三大性质总结

synchronized:具有原子性,有序性和可见性
粒度:对象锁、类锁

原子性

原子性是指一个操作是不可中断的,要么全部执行成功要么全部执行失败,有着“同生共死”的感觉。即使在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程所干扰。

int a = 10; //1
a++; //2
int b=a; //3
a = a+1; //4

上面这四个语句中只有第1个语句是原子操作,将10赋值给线程工作内存的变量a,而语句2(a++),实际上包含了三个操作:1. 读取变量a的值;2:对a进行加一的操作;3.将计算后的值再赋值给变量a,而这三个操作无法构成原子操作。对语句3,4的分析同理可得这两条语句不具备原子性。

java内存模型中定义了8种操作都是原子的,不可再分的。

  1. lock(锁定):作用于主内存中的变量,它把一个变量标识为一个线程独占的状态;
  2. unlock(解锁):作用于主内存中的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
  3. read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便后面的load动作使用;
  4. load(载入):作用于工作内存中的变量,它把read操作从主内存中得到的变量值放入工作内存中的变量副本
  5. use(使用):作用于工作内存中的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作;
  6. assign(赋值):作用于工作内存中的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作;
  7. store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送给主内存中以便随后的write操作使用;
  8. write(操作):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。

synchronized

上面一共有八条原子操作,其中六条可以满足基本数据类型的访问读写具备原子性,还剩下lock和unlock两条原子操作。如果我们需要更大范围的原子性操作就可以使用lock和unlock原子操作。尽管jvm没有把lock和unlock开放给我们使用,但jvm以更高层次的指令monitorenter和monitorexit指令开放给我们使用,反应到java代码中就是—synchronized关键字,也就是说synchronized满足原子性。

volatile

public class VolatileExample {
    private static `volatile` int counter = 0;

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 10000; i++)
                        counter++;
                }
            });
            thread.start();
        }
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(counter);
    }
}

开启10个线程,每个线程都自加10000次,如果不出现线程安全的问题最终的结果应该就是:10*10000= 100000;可是运行多次都是小于100000的结果,问题在于 volatile并不能保证原子性,在前面说过counter++这并不是一个原子操作,包含了三个步骤:1.读取变量counter的值;2.对counter加一;3.将新值赋值给变量counter。如果线程A读取counter到工作内存后,其他线程对这个值已经做了自增操作后,那么线程A的这个值自然而然就是一个过期的值,因此,总结果必然会是小于100000的。

如果让volatile保证原子性,必须符合以下两条规则:

  1. 运算结果并不依赖于变量的当前值,或者能够确保只有一个线程修改变量的值;
  2. 变量不需要与其他的状态变量共同参与不变约束

有序性

synchronized

synchronized语义表示锁在同一时刻只能由一个线程进行获取,当锁被占用后,其他线程只能等待。因此,synchronized语义就要求线程在访问读写共享变量时只能“串行”执行,因此synchronized具有有序性。

volatile

如果在本线程内观察,所有的操作都是有序的;如果在一个线程观察另一个线程,所有的操作都是无序的。在单例模式的实现上有一种双重检验锁定的方式(Double-checkedLocking)。代码如下:

public class Singleton {
    private Singleton() {
    }

    private `volatile` static Singleton instance;

    public Singleton getInstance() {
        if (instance == null) {
            `synchronized` (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

我们先来分析一下不加volatile的情况,有问题的语句是这条:

instance = new Singleton();

这条语句实际上包含了三个操作:1.分配对象的内存空间;2.初始化对象;3.设置instance指向刚分配的
内存地址。但由于存在重排序的问题,可能有以下的执行顺序:

image-20220321192329291

​ 如果2和3进行了重排序的话,线程B进行判断if(instance==null)时就会为true,而实际上这个instance并没有初始化成功,显而易见对线程B来说之后的操作就会是错得。而用volatile修饰的话就可以禁止2和3操作重排序,从而避免这种情况。volatile包含禁止指令重排序的语义,其具有有序性

可见性

​ 可见性是指当一个线程修改了共享变量后,其他线程能够立即得知这个修改。通过之前对synchronzed内存语义进行了分析,当线程获取锁时会从主内存中获取共享变量的最新值,释放锁的时候会将共享变量同步到主内存中。从而,synchronized具有可见性。同样的在volatile分析中,会通过在指令中添加lock指令,以实现内存可见性。因此, volatile具有可见性

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Black_Me_Bo

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值