多线程(学习笔记)

其他文章链接
Java基础
Java集合
多线程
JVM
MySQL
Redis
docker
计算机网络
操作系统



1.线程和进程

1.1 进程

进程是程序的⼀次执⾏过程,是系统运⾏程序的基本单位,因此进程是动态的。系统运⾏⼀个程序即是⼀个进程从创建,运⾏到消亡的过程。

  在Java中,当我们启动main函数时其实就是启动了⼀个JVM的进程,⽽main函数所在的线程就是这个进程中的⼀个线程,也称主线程。
  在windows中通过查看任务管理器的⽅式,可以看到window当前运⾏的进程。

1.2 线程

线程与进程相似,但线程是⼀个⽐进程更⼩的执⾏单位。⼀个进程在其执⾏的过程中可以产⽣多个线程。与进程不同的是同类的多个线程共享进程的堆和⽅法区资源,但每个线程有⾃⼰的程序计数器、虚拟机栈和本地⽅法栈,所以系统在产⽣⼀个线程,或是在各个线程之间作切换⼯作时,负担要⽐进程⼩得多,也正因为如此,线程也被称为轻量级进程。

  Java程序天⽣就是多线程程序,⼀个Java程序的运⾏是main线程和多个其他线程同时运⾏。

2.线程与进程的关系(JVM角度)

2.1 进程和线程的关系

在这里插入图片描述
从上图可以看出:⼀个进程中可以有多个线程,多个线程共享进程的堆和⽅法区(JDK1.8之后的元空间)资源,但是每个线程有⾃⼰的程序计数器、虚拟机栈和本地⽅法栈
线程是进程划分成的更⼩的运⾏单位。线程和进程最⼤的不同在于基本上各进程是独⽴的,⽽各线程则不⼀定,因为同⼀进程中的线程极有可能会相互影响。线程执⾏开销⼩,但不利于资源的管理和保护;⽽进程正相反。

2.2 程序计数器为什么是私有的

程序计数器主要有下⾯两个作⽤:

  1. 字节码解释器通过改变程序计数器来依次读取指令,从⽽实现代码的流程控制,如:顺序执⾏、选择、循环、异常处理。
  2. 在多线程的情况下,程序计数器⽤于记录当前线程执⾏的位置,从⽽当线程被切换回来的时候能够知道该线程上次运⾏到哪⼉了。

  需要注意的是,如果执⾏的是native⽅法,那么程序计数器记录的是undefined地址,只有执⾏的是Java代码时程序计数器记录的才是下⼀条指令的地址。

所以,程序计数器私有主要是为了线程切换后能恢复到正确的执⾏位置

2.3 虚拟机栈和本地⽅法栈为什么是私有的

  • 虚拟机栈:每个Java⽅法在执⾏的同时会创建⼀个栈帧⽤于存储局部变量表、操作数栈、常量池引⽤等信息。从⽅法调⽤直⾄执⾏完成的过程,就对应着⼀个栈帧在Java虚拟机栈中⼊栈和出栈的过程。
  • 本地⽅法栈:和虚拟机栈所发挥的作⽤⾮常相似,区别是:虚拟机栈为虚拟机执⾏Java⽅法(也就是字节码)服务,⽽本地⽅法栈则为虚拟机使⽤到的Native⽅法服务。在HotSpot虚拟机中和Java虚拟机栈合⼆为⼀。

所以,为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地⽅法栈是线程私有的。

2.4 简单了解堆和方法区

  堆和⽅法区是所有线程共享的资源,其中堆是进程中最⼤的⼀块内存,主要⽤于存放新创建的对象(所有对象都在这⾥分配内存),⽅法区主要⽤于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

3.并发与并行

  • 并发:同⼀时间段,多个任务都在执⾏(单位时间内不⼀定同时执⾏);
  • 并⾏:单位时间内,多个任务同时执⾏。

4.使⽤多线程的原因

  • 从计算机底层来说:线程可以⽐作是轻量级的进程,是程序执⾏的最⼩单位,线程间的切换和调度的成本远远⼩于进程。另外,多核CPU时代意味着多个线程可以同时运⾏,这减少了线程上下⽂切换的开销。
  • 从当代互联⽹发展趋势来说:现在的系统动不动就要求百万级甚⾄千万级的并发量,⽽多线程并发编程正是开发⾼并发系统的基础,利⽤好多线程机制可以⼤⼤提⾼系统整体的并发能⼒以及性能。

深⼊到计算机底层来探讨:

  • 单核时代:在单核时代多线程主要是为了提⾼CPU和IO设备的综合利⽤率。举个例⼦:当只有⼀个线程的时候会导致CPU计算时,IO设备空闲;进⾏IO操作时,CPU空闲。我们可以简单地说这两者的利⽤率⽬前都是50%左右。但是当有两个线程的时候就不⼀样了,当⼀个线程执⾏CPU计算时,另外⼀个线程可以进⾏IO操作,这样两个的利⽤率就可以在理想情况下达到100%了。
  • 多核时代:多核时代多线程主要是为了提⾼CPU利⽤率。举个例⼦:假如我们要计算⼀个复杂的任务,我们只⽤⼀个线程的话,CPU只会⼀个CPU核⼼被利⽤到,⽽创建多个线程就可以让多个CPU核⼼被利⽤到,这样就提⾼了CPU的利⽤率。

5.使⽤多线程可能带来的问题

并发编程的⽬的就是为了能提⾼程序的执⾏效率提⾼程序运⾏速度,但是并发编程并不总是能提⾼程序运⾏速度的,⽽且并发编程可能会遇到很多问题,⽐如:内存泄漏、上下⽂切换、死锁

6.线程有哪些基本状态 ?

  • NEW:初始状态,线程被构建,但是还没有调用start()方法。
  • RUNNABLE:运行状态,Java线程将操作系统中的就绪 REDAY 和运行 RUNNING 两种状态笼统地称作“运行中”。
  • BLOCKED:阻塞状态,表示线程阻塞于锁。
  • WAITING:等待状态,表示线程进人等待状态,进入该状态表示当前线程需要等待其他线程做出一些特定动作(通知或中断)。
  • TIME_WAITING:超时等待状态,该状态不同于WAITING,它是可以在指定的时间自行返回的。
  • TERMINATED:终止状态,表示当前线程已经执行完毕。
  • 线程创建之后它将处于NEW(新建)状态。
  • 调⽤start()⽅法后开始运⾏,线程这时候处于READY(可运⾏)状态。可运⾏状态的线程获得了cpu时间⽚(timeslice)后就处于RUNNING(运⾏)状态。
  • 当线程执⾏wait()⽅法之后,线程进⼊WAITING(等待)状态。进⼊等待状态的线程需要依靠其他线程的通知才能够返回到运⾏状态。
  • TIME_WAITING(超时等待)状态相当于在等待状态的基础上增加了超时限制,⽐如通过sleep(long millis)方法或wait(long millis)⽅法可以将Java线程置于TIMEDWAITING状态。当超时时间到达后Java线程将会返回到RUNNABLE状态。
  • 当线程调⽤同步⽅法时,在没有获取到锁的情况下,线程将会进⼊到BLOCKED(阻塞)状态。
  • 线程在执⾏Runnable的run()⽅法之后将会进⼊到TERMINATED(终⽌)状态。

7.上下⽂切换

多线程编程中⼀般线程的个数都⼤于CPU核⼼的个数,⽽⼀个CPU核⼼在任意时刻只能被⼀个线程使⽤,为了让这些线程都能得到有效执⾏,CPU采取的策略是为每个线程分配时间⽚并轮转的形式。当⼀个线程的时间⽚⽤完的时候就会重新处于就绪状态让给其他线程使⽤,这个过程就属于⼀次上下⽂切换。

概括来说就是:当前任务在执⾏完CPU时间⽚切换到另⼀个任务之前会先保存⾃⼰的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是⼀次上下⽂切换。

上下⽂切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒⼏⼗上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下⽂切换对系统来说意味着消耗⼤量的CPU时间,事实上,可能是操作系统中时间消耗最⼤的操作。
Linux相⽐与其他操作系统(包括其他类Unix系统)有很多的优点,其中有⼀项就是,其上下⽂切换和模式切换的时间消耗⾮常少。

8.线程死锁

8.1 什么是线程死锁

多个线程同时被阻塞,它们中的⼀个或者全部都在等待某个资源被释放。由于线程被⽆限期地阻塞,因此程序不可能正常终⽌。

举例:线程A持有资源2,线程B持有资源1,他们同时都想申请对⽅的资源,所以这两个线程就会互相等待⽽进⼊死锁状态。

产⽣死锁必须具备以下四个条件:

  1. 互斥条件:该资源任意⼀个时刻只由⼀个线程占⽤。
  2. 请求与保持条件:⼀个进程因请求资源⽽阻塞时,对已获得的资源保持不放。
  3. 不剥夺条件:线程已获得的资源在末使⽤完之前不能被其他线程强⾏剥夺,只有⾃⼰使⽤完毕后才释放资源。
  4. 循环等待条件:若⼲进程之间形成⼀种头尾相接的循环等待资源关系。

8.2 如何避免线程死锁

  为了避免死锁,我们只要破坏产⽣死锁的四个条件中的其中⼀个就可以了。

  1. 破坏互斥条件:这个条件我们没有办法破坏,因为我们⽤锁本来就是想让他们互斥的(临界资源需要互斥访问)。
  2. 破坏请求与保持条件:⼀次性申请所有的资源。
  3. 破坏不剥夺条件:占⽤部分资源的线程进⼀步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
  4. 破坏循环等待条件:靠按序申请资源来预防。按某⼀顺序申请资源,释放资源则反序释放。破坏循环等待条件。

9.sleep() ⽅法和 wait() ⽅法

  • 区别:sleep()⽅法没有释放锁,⽽wait()⽅法释放了锁。
  • 相同:两者都可以暂停线程的执⾏。

wait()通常被⽤于线程间交互/通信,sleep()通常被⽤于暂停执⾏。wait()⽅法被调⽤后,线程不会⾃动苏醒,需要别的线程调⽤同⼀个对象上的notify()或者notifyAll()⽅法。sleep()⽅法执⾏完成后,线程会⾃动苏醒。或者可以使⽤wait(longtimeout)超时后线程会⾃动苏醒。

10. 为什么我们调⽤start()⽅法时会执⾏run()⽅法,为什么我们不能直接调⽤run()⽅法?

new⼀个Thread,线程进⼊了新建状态。调⽤start()⽅法,会启动⼀个线程并使线程进⼊了就绪状态,当分配到时间⽚后就可以开始运⾏了。start()会执⾏线程的相应准备⼯作,然后⾃动执⾏run()⽅法的内容,这是真正的多线程⼯作。但是,直接执⾏run()⽅法,会把run()⽅法当成⼀个main线程下的普通⽅法去执⾏,并不会在某个线程中执⾏它,所以这并不是多线程⼯作。

总结:调⽤start()⽅法⽅可启动线程并使线程进⼊就绪状态,直接执⾏run()⽅法的话不会以多线程的⽅式执⾏。

11.synchronized

synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的⽅法或者代码块在任意时刻只能有⼀个线程执⾏。

在 Java 早期版本中, synchronized 属于重量级锁,效率低下。
因为监视器锁(monitor)是依赖于底层的操作系统的MutexLock来实现的,Java的线程是映射到操作系统的原⽣线程之上的。如果要挂起或者唤醒⼀个线程,都需要操作系统帮忙完成,⽽操作系统实现线程之间的切换时需要从⽤户态转换到内核态,这个状态之间的转换需要相对⽐较⻓的时间,时间成本相对较⾼。
在Java6之后Java官⽅对从JVM层⾯对synchronized较⼤优化,所以现在的synchronized锁效率也优化得很不错了。JDK1.6对锁的实现引⼊了⼤量的优化,如⾃旋锁、适应性⾃旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
所以,⽬前不论是各种开源框架还是JDK源码都⼤量使⽤了synchronized关键字。

12.使⽤ synchronized

  1. 修饰实例⽅法:作⽤于当前对象实例加锁,进⼊同步代码前要获得当前对象实例的锁
	sunchronized void method() {
		//业务代码
	}
  1. 修饰静态⽅法:也就是给当前类加锁,会作⽤于类的所有对象实例,进⼊同步代码前要获得当前class的锁。因为静态成员不属于任何⼀个实例对象,是类成员(static表明这是该类的⼀个静态资源,不管new了多少个对象,只有⼀份)。所以,如果⼀个线程A调⽤⼀个实例对象的⾮静态synchronized⽅法,⽽线程B需要调⽤这个实例对象所属类的静态synchronized⽅法,是允许的,不会发⽣互斥现象,因为访问静态synchronized⽅法占⽤的锁是当前类的锁,⽽访问⾮静态synchronized⽅法占⽤的锁是当前实例对象锁。
	sunchronized void static method() {
		//业务代码
	}
  1. 修饰代码块:指定加锁对象,对给定对象/类加锁。synchronized(this|object)表示进⼊同步代码库前要获得给定对象的锁。synchronized(类.class)表示进⼊同步代码前要获得当前class的锁
	sunchronized(this) {
		//业务代码
	}

总结:

  • synchronized 关键字加到 static 静态⽅法和 synchronized(class) 代码块上都是是给 Class 类上锁。
  • synchronized 关键字加到实例⽅法上是给对象实例上锁。
  • 尽量不要使⽤ synchronized(String a) 因为 JVM 中,字符串常量池具有缓存功能。

13.构造⽅法可以使⽤ synchronized 关键字修饰么

构造⽅法不能使⽤ synchronized 关键字修饰。

构造⽅法本身就属于线程安全的,不存在同步的构造⽅法⼀说。

14.synchronized 关键字的底层原理

synchronized 关键字底层原理属于 JVM 层⾯。

14.1 synchronized 同步语句块

  synchronized 同步语句块的实现使⽤的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置, monitorexit 指令则指明同步代码块的结束位置。
  当执⾏ monitorenter 指令时,线程试图获取锁也就是获取对象监视器 monitor 的持有权

在 Java 虚拟机(HotSpot)中,Monitor 是基于 C++实现的,由ObjectMonitor实现的。每个对 象中都内置了⼀个 ObjectMonitor对象。 另外, wait/notify等⽅法也依赖于 monitor对象,这就是为什么只有在同步的块或者⽅法 中才能调⽤ wait/notify等⽅法,否则会抛出 java.lang.IllegalMonitorStateException的异常的原因。

  在执⾏monitorenter时,会尝试获取对象的锁,如果锁的计数器为0则表示锁可以被获取,获取后将锁计数器设为1也就是加1。
  在执⾏monitorexit指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外⼀个线程释放为⽌。

14.2 synchronized 修饰⽅法的的情况

  synchronized 修饰的⽅法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该⽅法是⼀个同步⽅法。JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别⼀个⽅法是否声明为同步⽅法,从⽽执⾏相应的同步调⽤。

14.3 总结

  synchronized 同步语句块的实现使⽤的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置, monitorexit 指令则指明同步代码块的结束位 置。
  synchronized 修饰的⽅法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该⽅法是⼀个同步⽅法。
  不过两者的本质都是对对象监视器 monitor 的获取。

15.为什么要弄⼀个CPU⾼速缓存

  类⽐我们开发⽹站后台系统使⽤的缓存(⽐如Redis)是为了解决程序处理速度和访问常规关系型数据库速度不对等的问题。CPU缓存则是为了解决CPU处理速度和内存处理速度不对等的问题。
  我们甚⾄可以把内存可以看作外存的⾼速缓存,程序运⾏的时候我们把外存的数据复制到内存,由于内存的处理速度远远⾼于外存,这样提⾼了处理速度。
  总结:CPU Cache缓存的是内存数据⽤于解决CPU处理速度和内存不匹配的问题,内存缓存的是硬盘数据⽤于解决硬盘访问速度过慢的问题。

16.JMM(Java 内存模型)

  在JDK1.2之前,Java的内存模型实现总是从主存(即共享内存)读取变量,是不需要进⾏特别的注意的。⽽在当前的Java内存模型下,线程可以把变量保存本地内存(⽐如机器的寄存器)中,⽽不是直接在主存中进⾏读写。这就可能造成⼀个线程在主存中修改了⼀个变量的值,⽽另外⼀个线程还继续使⽤它在寄存器中的变量值的拷⻉,造成数据的不⼀致。

  要解决这个问题,就需要把变量声明为volatile,这就指示JVM,这个变量是共享且不稳定的,每次使⽤它都到主存中进⾏读取。
所以,volatile关键字除了防⽌JVM的指令重排,还有⼀个重要的作⽤就是保证变量的可⻅性。
在这里插入图片描述

17.synchronized 和 volatile

synchronized关键字和volatile关键字是两个互补的存在,⽽不是对⽴的存在。

  • volatile关键字是线程同步的轻量级实现,所以volatile性能⽐synchronized关键字好。但是volatile关键字只能⽤于变量,⽽synchronized关键字可以修饰⽅法以及代码块。
  • volatile关键字能保证数据的可⻅性,但不能保证数据的原⼦性。synchronized关键字两者都能保证。
  • volatile关键字主要⽤于解决变量在多个线程之间的可⻅性,⽽synchronized关键字解决的是多个线程之间访问资源的同步性。

18.ThreadLocal

通常情况下,我们创建的变量是可以被任何⼀个线程访问并修改的。如果想实现每⼀个线程都有⾃⼰的专属本地变量就要使用到 JDK 中提供的 ThreadLocal类。ThreadLocal类主要解决的就是让每个线程绑定⾃⼰的值,可以将ThreadLocal类形象的⽐喻成存放数据的盒⼦,盒⼦中可以存储每个线程的私有数据。

  如果创建了⼀个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是ThreadLocal变量名的由来。他们可以使⽤get()和set()⽅法来获取默认值或将其值更改为当前线程所存的副本的值,从⽽避免了线程安全问题。

19.线程池

19.1 线程池简述与优点

线程池提供了⼀种限制和管理资源(包括执⾏⼀个任务)。每个线程池还维护⼀些基本统计信息,例如已完成任务的数量。

使⽤线程池的好处:

  • 降低资源消耗。通过重复利⽤已创建的线程降低线程创建和销毁造成的消耗。
  • 提⾼响应速度。当任务到达时,任务可以不需要的等到线程创建就能⽴即执⾏。
  • 提⾼线程的可管理性。线程是稀缺资源,如果⽆限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使⽤线程池可以进⾏统⼀的分配,调优和监控。

19.2 如何创建线程池

⽅式⼀:通过构造⽅法实现
在这里插入图片描述⽅式⼆:通过Executor框架的⼯具类Executors来实现
我们可以创建三种类型的ThreadPoolExecutor:

  • FixedThreadPool:该⽅法返回⼀个固定线程数量的线程池。该线程池中的线程数量始终不变。当有⼀个新的任务提交时,线程池中若有空闲线程,则⽴即执⾏。若没有,则新的任务会被暂存在⼀个任务队列中,待有线程空闲时,便处理在任务队列中的任务。
  • SingleThreadExecutor:⽅法返回⼀个只有⼀个线程的线程池。若多余⼀个任务被提交到该线程池,任务会被保存在⼀个任务队列中,待线程空闲,按先⼊先出的顺序执⾏队列中的任务。
  • CachedThreadPool:该⽅法返回⼀个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复⽤,则会优先使⽤可复⽤的线程。若所有线程均在⼯作,⼜有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执⾏完毕后,将返回线程池进⾏复⽤。

在这里插入图片描述

注意:《阿⾥巴巴Java开发⼿册》中强制线程池不允许使⽤Executors去创建,⽽是通过ThreadPoolExecutor的⽅式,这样的处理⽅式让写的同学更加明确线程池的运⾏规则,规避资源耗尽的⻛险
Executors 返回线程池对象的弊端如下:

  • FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列⻓度为Integer.MAX_VALUE ,可能堆积⼤量的请求,从⽽导致 OOM。
  • CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建⼤量线程,从⽽导致 OOM。

19.3 ThreadPoolExecutor 类分析

19.3.1 ThreadPoolExecutor构造函数重要参数分析

ThreadPoolExecutor3个最重要的参数:

  • corePoolSize:核⼼线程数线程数定义了最⼩可以同时运⾏的线程数量。
  • maximumPoolSize:当队列中存放的任务达到队列容量的时候,当前可以同时运⾏的线程数量变为最⼤线程数。
  • workQueue:当新任务来的时候会先判断当前运⾏的线程数量是否达到核⼼线程数,如果达到的话,新任务就会被存放在队列中。

ThreadPoolExecutor其他常⻅参数:

  1. keepAliveTime:当线程池中的线程数量⼤于corePoolSize的时候,如果这时没有新的任务提交,核⼼线程外的线程不会⽴即销毁,⽽是会等待,直到等待的时间超过了keepAliveTime才会被回收销毁;
  2. unit:keepAliveTime参数的时间单位。
  3. threadFactory:executor创建新线程的时候会⽤到。
  4. handler:饱和策略。

19.3.2 ThreadPoolExecutor饱和策略

  如果当前同时运⾏的线程数量达到最⼤线程数量并且队列也已经被放满了任 时, ThreadPoolTaskExecutor 定义⼀些策略:

  • ThreadPoolExecutor.AbortPolicy :抛出 RejectedExecutionException来拒绝新任务的处理。
  • ThreadPoolExecutor.CallerRunsPolicy :调⽤执⾏⾃⼰的线程运⾏任务。您不会任务请求。 但是这种策略会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略喜欢增加队列容量。如果您的应⽤程序可以承受此延迟并且你不能任务丢弃任何⼀个任务请求的话, 你可以选择这个策略。
  • ThreadPoolExecutor.DiscardPolicy : 不处理新任务,直接丢弃掉。
  • ThreadPoolExecutor.DiscardOldestPolicy : 此策略将丢弃最早的未处理的任务请求。

举个例⼦: Spring 通过 ThreadPoolTaskExecutor 或者我们直接通过 ThreadPoolExecutor 的构造函数创建线程池的时候,当我们不指定 RejectedExecutionHandler 饱和策略的话来配置线程池的时候默认使⽤的是 ThreadPoolExecutor.AbortPolicy 。在默认情况下, ThreadPoolExecutor 将抛出 RejectedExecutionException 来拒绝新来的任务 ,这代表你将丢失对这个任务的处理。 对于可伸缩的应⽤程序,建议使⽤ ThreadPoolExecutor.CallerRunsPolicy 。当最⼤池被填满时,此策略为我们提供可伸缩队列。

19.3.3 线程池原理

在这里插入图片描述

20.Runnable 接⼝和 Callable 接⼝

Runnable接⼝不会返回结果或抛出检查异常,但是Callable接⼝可以。
所以,如果任务不需要返回结果或抛出异常推荐使⽤Runnable接⼝,这样代码看起来会更加简洁。

⼯具类 Executors 可以实现 Runnable 对象和 Callable 对象之间的相互转换。

21.execute()⽅法和 submit()⽅法

  1. execute()⽅法⽤于提交不需要返回值的任务,所以⽆法判断任务是否被线程池执⾏成功与否;
  2. submit()⽅法⽤于提交需要返回值的任务。线程池会返回⼀个Future类型的对象,通过这个Future对象可以判断任务是否执⾏成功,并且可以通过Future的get()⽅法来获取返回值,get()⽅法会阻塞当前线程直到任务完成,⽽使⽤get(longtimeout, TimeUnitunit)⽅法则会阻塞当前线程⼀段时间后⽴即返回,这时候有可能任务没有执⾏完。

22.Atomic 原⼦类

Atomic是指⼀个操作是不可中断的。即使是在多个线程⼀起执⾏的时候,⼀个操作⼀旦开始,就不会被其他线程⼲扰。

所以,所谓原⼦类说简单点就是具有原⼦/原⼦操作特征的类。

23.JUC包中的原⼦类

基本类型
使⽤原⼦的⽅式更新基本类型

  • AtomicInteger :整形原⼦类
  • AtomicLong :⻓整型原⼦类
  • AtomicBoolean :布尔型原⼦类
    数组类型
    使⽤原⼦的⽅式更新数组⾥的某个元素
  • AtomicIntegerArray :整形数组原⼦类
  • AtomicLongArray :⻓整形数组原⼦类
  • AtomicReferenceArray :引⽤类型数组原⼦类
    引⽤类型
  • AtomicReference :引⽤类型原⼦类
  • AtomicStampedReference :原⼦更新带有版本号的引⽤类型。该类将整数值与引⽤关联起 来,可⽤于解决原⼦的更新数据和数据的版本号,可以解决使⽤ CAS 进⾏原⼦更新时可能 出现的 ABA 问题。
  • AtomicMarkableReference :原⼦更新带有标记位的引⽤类型
    对象的属性修改类型
  • AtomicIntegerFieldUpdater :原⼦更新整形字段的更新器
  • AtomicLongFieldUpdater :原⼦更新⻓整形字段的更新器
  • AtomicReferenceFieldUpdater :原⼦更新引⽤类型字段的更新器

24.AQS

AQS 的全称为 AbstractQueuedSynchronizer,这个类在 java.util.concurrent.locks包下⾯。
AQS 是⼀个⽤来构建锁和同步器的框架,使⽤ AQS 能简单且⾼效地构造出应⽤⼴泛的⼤量的同步器,⽐如ReentrantLock ,Semaphore ,其他的诸如 ReentrantReadWriteLock , SynchronousQueue , FutureTask 等等皆是基于 AQS 的。我们也能利⽤ AQS构造出符合我们⾃⼰需求的同步器。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值