校招准备:(四):java多线程

1.多线程:

1.1进程和线程的区别

进程和线程的主要区别(总结)

进程和线程都是一个时间段的描述,是CPU工作时间段的描述。

进程是程序向操作系统申请资源(如内存空间,文件句柄)的基本单位。,线程是CPU调度的最小单位

每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;

同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。

在操作系统中能同时运行多个进程(程序);而在同一个进程(程序)中有多个线程同时执行(通过CPU调度,在每个时间片中只有一个线程执行)

系统在运行的时候会为每个进程分配不同的内存空间;而对线程而言,除了CPU外,系统不会为线程分配内存(线程所使用的资源来自其所属进程的资源),线程组之间只能共享资源。

1.2Java线程状态

线程的生命周期:
6种状态:
1.new:已创建而未启动(未调用.start())
2.RUNNABLE,两个子状态:READY和RUNNING。
3.BLOCKED
4.WAITING
5.TIMED_WAITING
6.TERMINATED。run方法正常执行结束或者抛出异并发是一个人同时做几个事,并行是几个人同时做几个事。
thread.start()方法启动相应的线程,即请求java虚拟机雨你行相应的线程,这个线程何时运行由线程调度器(操作系统的一个部分)来决定。

推荐阅读:全面理解Java内存模型(JMM)及volatile关键字

在Window系统和Linux系统上,Java线程的实现是基于一对一的线程模型,所谓的一对一模型,实际上就是通过语言级别层面程序去间接调用系统内核的线程模型,即我们在使用Java线程时,Java虚拟机内部是转而调用当前操作系统的内核线程来完成当前任务。

JMM中的happens-before 原则:

倘若在程序开发中,仅靠sychronized和volatile关键字来保证原子性、可见性以及有序性,那么编写并发程序可能会显得十分麻烦,幸运的是,在Java内存模型中,还提供了happens-before 原则来辅助保证程序执行的原子性、可见性以及有序性的问题,它是判断数据是否存在竞争、线程是否安全的依据,happens-before 原则内容如下

  • 程序顺序原则,即在一个线程内必须保证语义串行性,也就是说按照代码顺序执行。
  • 锁规则 解锁(unlock)操作必然发生在后续的同一个锁的加锁(lock)之前,也就是说,如果对于一个锁解锁后,再加锁,那么加锁的动作必须在解锁动作之后(同一个锁)。
  • volatile规则 volatile变量的写,先发生于读,这保证了volatile变量的可见性,简单的理解就是,volatile变量在每次被线程访问时,都强迫从主内存中读该变量的值,而当该变量发生变化时,又会强迫将最新的值刷新到主内存,任何时刻,不同的线程总是能够看到该变量的最新值。
  • 线程启动规则 线程的start()方法先于它的每一个动作,即如果线程A在执行线程B的start方法之前修改了共享变量的值,那么当线程B执行start方法时,线程A对共享变量的修改对线程B可见
  • 传递性 A先于B ,B先于C 那么A必然先于C
  • 线程终止规则 线程的所有操作先于线程的终结,Thread.join()方法的作用是等待当前执行的线程终止。假设在线程B终止之前,修改了共享变量,线程A从线程B的join方法成功返回后,线程B对共享变量的修改将对线程A可见。
  • 线程中断规则 对线程 interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测线程是否中断。
  • 对象终结规则 对象的构造函数执行,结束先于finalize()方法

1.3其他基础知识

创建Thread时,java虚拟机会为每个线程分配调用栈(Call Stack)所需的内存空间,调用栈用于跟踪java代码(方法)之间的调用关系以及java代码对本地代码(Native Code)的调用。

java线程优先级(0-10越高越大)只是给线程调度器一个提示,不能保证运行顺序。

线程是否会阻止java虚拟机正常停止分为守护线程和用户线程。

java平台本身就是一个多线程平台。java虚拟机启动的是否创建一个main线程,该线程负责执行java程序的入口方法(main方法)。
java虚拟机垃圾回收器也是通过专门的线程,java虚拟机中的JIT(Just In Time)编译器会动态的将java字节码(Byte Code)编译为机器码,也是在java虚拟机创建的专门线程中。

1.4Thread类的常用方法及区别:

static void sleep(long millis) 从Running状态放弃处理器进入Block状态
注意:当一个线程处于睡眠阻塞时,若被其他线程调用.interrupt方法中断,则sleep方法会抛出InterruptedException异常

void interrupt()  中断线程。

//判断是否被中断并清除当前中断状态(静态方法)
public static boolean Thread.interrupted();

boolean isInterrupted() 
测试这个线程是否被中断。 (1.8继续讲解线程中断)

void join()  等待该线程终止。比如显示线程要等待下载图片线程执行完毕再显示。

void run()     

void start()  

从Object类继承来的方法  void notify()         void wait()

synchronized(someObject){
    while(保护条件不成立){
        someObject.wait();
    }
    //已成立,则继续执行其他操作
    do();
}

synchronized(someObject){
    //更新等待线程的保护条件
    update();
    //已成立,则继续执行其他操作
    someObject.notify();
}

java虚拟机为每个对象维护一个入口集(entry set)用于存储申请该对象someObject内部锁的线程,申请失败的线程会被暂停,存入相应锁的入口集,等待再次申请锁的机会。当锁被持有线程释放时,会从入口集中任意唤醒一个等待线程,再次获得申请锁的机会。

还会维护一个等待集的队列(wait set),存储该对象上的等待线程。someObject.wait()将当前线程暂停并释放相应内部锁的同时,会将当前线程的引用存入该方法所属对象someObject的等待集中。但调用并不会返回,其他线程在该线程需要的条件满足时,会执行此对象someObject.notify方法,其对象someObject再次申请所在线程对象someObject的内部锁,获取成功后,执行wait的剩余指令,将自己移出等待集。
在该线程执行一个对象的notify()会使该对象等待集中的任意一个线程唤醒,但此对象仍会停留在等待集。直到该线程获取到相应的内部锁,wait方法执行结束,将其移出等待集。
使用while是因为等待线程在被唤醒、继续运行、获取相应对象的内部锁时有可能其他线程抢占修改了状态变量,所以一般放到while中。

等待唤醒机制本篇主要指的是notify/notifyAll和wait方法,在使用这3个方法时,必须处于synchronized代码块或者synchronized方法中,否则就会抛出IllegalMonitorStateException异常,这是因为调用这几个方法前必须拿到当前对象的监视器monitor对象,也就是说notify/notifyAll和wait方法依赖于monitor对象

wait/notify可能出现的问题:

1.过早唤醒:因为使用了notifyall()使的全部等待线程唤醒,其中一部分不满足条件。

2.信号丢失:使用notify()为随机选择线程,会遇到满足条件的线程没有收到通知。

3.上下文切换会导致时间消耗、

1.7仍有详细讲解。

sleep方法和wait的区别:

wait方法调用完成后,线程将被暂停,但wait方法将会释放当前持有的监视器锁(monitor),直到有线程调用notify/notifyAll方法后方能继续执行。
而sleep方法只让线程休眠并不释放锁。

同时notify/notifyAll方法调用后,并不会马上释放监视器锁,而是在相应的synchronized(){}/synchronized方法执行结束后才自动释放锁。
 

1.5线程安全,线程安全的三个特性,线程活性故障

-当多个线程访问某个类时,不管运行环境采用何种调度方式或者这些进程将如何交替执行,并且在主调代码中不需要任何额外的协同或者同步,这个类都能表现出正确的行为,那么这个类是线程安全的。

竞态:计算的正确性依赖于相对时间顺序或者线程的交错。

a++操作相当于三个指令(伪代码):
1.load(a,r1)//将变量a的值从内存读到寄存器。
2.increment(r1)//将寄存器r1的值增加1
3.store(1,r1)//将r1的内容那个写入a对应的内存空间。

竞态的两种模式:1.read-modify-write读改写,例如a++,2.check-then-act检测而后行动例如;if-else语句

线程安全问题分为:

原子性:访问(读、写)某个共享变量的操作从其他执行线程来看要么尚未发生要么已经结束。实现原子性:1使用锁,2.利用处理器提供的CAS(Compare-and-Swap)指令。java中除了long和double以外的其他类型变量的写操作都是原子操作,由java虚拟机具体实现。而低于volatile关键字修饰的long/double型变量的写操作具有原子性。

可见性;一个线程对共享变量进行更新,后续访问该变量的线程可以读取打更新的结果。导致的原因:编译器优化,计算机存储系统(寄存器,Cache(cpu不是直接访问内存(RAM)而是通过Cache、寄存器(Register)高速缓存(Cache)写缓冲器(store Buffer)无效化队列(Invalidate Queue)))cpu可以通过缓存一致性协议将其他处理器的cache数据更新到自己cache上(缓存同步)。一致性的保障是通过更新共享变量的处理器执行冲刷处理器缓存的动作,读取共享变量的处理器执行刷新处理器缓存的动作实现。java可以通过volatile关键字来保证可见性。

有序性,重排序:指令重排序(编译器,jit,处理器),存储子系统重排序(高速缓存,写缓存器),
对于a=new A();可以分为三个操作:
1.objef=allocate(A.class)分配A实例所需的内存空间,并获得指向该空间的引用。
2.invokeConstructor(objef)//调用A类的构造器初始化objref引用指向的A实例
3a=objref,将引用赋值给实例变量a
禁止重排序是通过调用处理器提供的指令(内存屏障),volatile和synchronized都能实现有序性。
线程上下文切换:一个线程暂停,另一个线程被操作系统线程调度器选中开始或继续运行。单处理器通过时间片分配的方式实现多线程。对java来说,从RUNNABLE状态到其他状态之间的切换过程就是上下文切换过程。

线程活性故障:
死锁:x线程持有a等待b,y线程持有b等待a。产生条件:资源互斥、资源不可抢夺、占用并等待资源、循环等待资源。
锁死:线程等待a,可是a永远不会够,或者说是没有a
活锁:一直处于runnable状态,但没有进展。
饥饿:无法抢到资源。

static保障线程读取到相应字段的初始值。
final保证对象被final修饰的字段初始化完毕,就算此变量是对象。
对象溢出:发布的对象可能是未初始化完成的对象。常见的几种操作:在构造器中将this赋值给共享变量,在构造器中将this作为方法参数传递到其他方法,在构造器中启动基于匿名类的线程。由于对象还没有初始化完成,发布到其他线程可能出现错误。

1.6内存屏障、volatile关键字

同一时间内,处于running状态的子线程越多,称之为高并发。

内存屏障:是一类仅针对内存读、写操作指令的跨处理器架构的比较底层的抽象。通过插入到两个指令之间使用,禁止编译器处理器进行重排序并进行缓存刷新。可以分为加载屏障(刷新处理器缓存),存储屏障(冲刷处理器缓存)。java虚拟机在释放锁对应的字节码指令后插入一个存储凭证,在申请锁对应的机器码之后的临界区开始处加入加载屏障。也可以分为获取屏障(读操作后插入,禁止读操作与之后的操作重排序)和释放屏障(写操作之前插入,禁止与之前的操作重排序)

锁可以禁止临界区和外面发生重排序。

volatile关键字:通过内存屏障来完成。保障可见性,有序性和long/double读写的原子性(java对long和double型以外的任何类型的变量的写操作都是原子操作)。他修饰的变量表示易变化,对此变量的读写操作必须从高速缓存中获取来读取相对新值。变量不会被分配到寄存器存储,不会引起上下文切换。

1.7锁,synchronized原理,lock原理

深入理解Java并发之synchronized实现原理

锁可以保障原子性,可见性,有序性。
条件:多个线程访问同一组数据必须使用同一个锁,仅仅是读数据也要持有锁。

synchronized为非公平锁,lock则都支持。

  • 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁

  • 修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁

  • 修饰代码块(static和非static),指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

synchronized(锁句柄){}锁句柄是一个对象的引用或者是能返回对象的表达式。
同步实例方法(类中的方法)相当于在方法是方法中加了synchronized(this){}。
同步static方法相当于在方法中synchronized(类.class){}java类本身也是一个对象。
锁句柄变量的值一旦改变,相当于不同的锁。

Java 虚拟机中的同步(Synchronization)基于进入和退出管程(Monitor)对象实现, 无论是显式同步(有明确的 monitorenter 和 monitorexit 指令,即同步代码块)还是隐式同步都是如此。在 Java 语言中,同步用的最多的地方可能是被 synchronized 修饰的同步方法。同步方法 并不是由 monitorenter 和 monitorexit 指令来实现同步的,而是由方法调用指令读取运行时常量池中方法的 ACC_SYNCHRONIZED 标志隐式实现的。

对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。

  • 实例变量:存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。

  • 填充数据:由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐,这点了解即可。

对于顶部,则是Java头对象,它实现synchronized的锁对象的基础,一般而言,synchronized使用的锁对象是存储在Java对象头里的,jvm中采用2个字来存储对象头(如果对象是数组则会分配3个字,多出来的1个字记录的是数组长度),其主要结构是由Mark Word 和 Class Metadata Address 组成。

虚拟机位数    头对象结构    说明
32/64bit    Mark Word    存储对象的hashCode、锁信息或分代年龄或GC标志等信息
32/64bit    Class Metadata Address    类型指针指向对象的类元数据,JVM通过这个指针确定该对象是哪个类的实例。

synchronized的对象锁,锁标识位为10,其中指针指向的是monitor对象(也称为管程或监视器锁)的起始地址。每个对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式,如monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态。在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的)

bjectMonitor中有两个队列,_WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象),_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时,首先会进入 _EntryList 集合,当线程获取到对象的monitor 后进入 _Owner 区域并把monitor中的owner变量设置为当前线程,同时monitor中的计数器count加1,若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSe t集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。如下图所示

由此看来,monitor对象存在于每个Java对象的对象头中(存储的指针的指向),synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因,同时也是notify/notifyAll/wait等方法存在于顶级对象Object中的原因

同步语句块的实现使用的是monitorenter 和 monitorexit 指令,其中monitorenter指令指向同步代码块的开始位置,monitorexit指令则指明同步代码块的结束位置。
当执行monitorenter指令时,当前线程将试图获取 objectref(即对象锁) 所对应的 monitor 的持有权,当 objectref 的 monitor 的进入计数器为 0,那线程可以成功取得 monitor,并将计数器值设置为 1,取锁成功。
如果当前线程已经拥有 objectref 的 monitor 的持有权,那它可以重入这个 monitor (关于重入性稍后会分析),重入时计数器的值也会加 1。
倘若其他线程已经拥有 objectref 的 monitor 的所有权,那当前线程将被阻塞,直到正在执行线程执行完毕,即monitorexit指令被执行,执行线程将释放 monitor(锁)并设置计数器值为0 ,其他线程将有机会持有 monitor 。
值得注意的是编译器将会确保无论方法通过何种方式完成,方法中调用过的每条 monitorenter 指令都有执行其对应 monitorexit 指令,而无论这个方法是正常结束还是异常结束。为了保证在方法异常完成时 monitorenter 和 monitorexit 指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,它的目的就是用来执行 monitorexit 指令。从字节码中也可以看出多了一个monitorexit指令,它就是异常结束时被执行的释放monitor 的指令。

如果一个同步方法执行期间抛 出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的monitor将在异常抛到同步方法之外时自动释放。

Java早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的,而操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的synchronized效率低的原因。
Java 6之后,为了减少获得锁和释放锁所带来的性能消耗,引入了轻量级锁和偏向锁:
锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁,但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级,重量级锁就是mutex lock(互斥锁)。

Synchronized的原理及自旋锁,偏向锁,轻量级锁,重量级锁的区别

1.自旋锁 
如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。 
但是线程自旋是需要消耗cup的,说白了就是让cup在做无用功,如果一直获取不到锁,那线程也不能一直占用cup自旋做无用功,所以需要设定一个自旋等待的最大时间。 
如果持有锁的线程执行的时间超过自旋等待的最大时间仍没有释放锁,就会导致其它争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。 
自旋锁的优缺点: 
自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗,这些操作会导致线程发生两次上下文切换! 
但是如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适合使用自旋锁了,因为自旋锁在获取锁前一直都是占用cpu做无用功,占着XX不XX,同时有大量线程在竞争一个锁,会导致获取锁的时间很长,线程自旋的消耗大于线程阻塞挂起操作的消耗,其它需要cup的线程又不能获取到cpu,造成cpu的浪费。所以这种情况下我们要关闭自旋锁 
2、偏向锁 
偏向锁,顾名思义,它会偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,减少加锁/解锁的一些CAS操作(比如等待队列的一些CAS操作)这种情况下,就会给线程加一个偏向锁。 
如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。 
偏向锁的适用场景: 
始终只有一个线程在执行同步块,在它没有执行完释放锁之前,没有其它线程去执行同步块,在锁无竞争的情况下使用,一旦有了竞争就升级为轻量级锁,升级为轻量级锁的时候需要撤销偏向锁,撤销偏向锁的时候会导致stop the word操作; 
在有锁的竞争时,偏向锁会多做很多额外操作,尤其是撤销偏向所的时候会导致进入安全点,安全点会导致stw(stop the world),导致性能下降,这种情况下应当禁用; 

3.轻量级锁是由偏向所升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁;

多个线程竞争锁,轻量级锁就要膨胀为重量级锁后面等待锁的线程也要进入阻塞状态。 而当前线程便尝试使用自旋来获取锁,自旋就是为了不让线程阻塞,而采用循环去获取锁的过程。

 内部包含cas操作和自旋锁,相较于重量锁,少了一些容器。

4、重量级锁Synchronized 
Synchronized可以把任意一个非NULL的对象当作锁。 
1、作用于方法时,锁住的是对象的实例(this); 
2、当作用于静态方法时,锁住的是Class实例,又因为Class的相关数据存储在永久带PermGen(jdk1.8则是metaspace),永久带是全局共享的,因此静态方法锁相当于类的一个全局锁,会锁所有调用该方法的线程; 
3、作用于一个对象实例时,锁住的是所有以该对象为锁的代码块。 
它有多个队列,当多个线程一起访问某个对象监视器的时候,对象监视器会将这些线程存储在不同的容器中。

Synchronized是非公平锁。 Synchronized在线程进入ContentionList时,等待的线程会先尝试自旋获取锁,如果获取不到就进入ContentionList,这明显对于已经进入队列的线程是不公平的,还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占OnDeck线程的锁资源。

lock原理:

lock接口:

public interface Lock {
    //加锁
    void lock();

    //解锁,注意unlock()操作必须在finally代码块中,这样可以确保即使临界区执行抛出异常,线程最终也能正常释放锁。
    void unlock();

    //可中断获取锁,与lock()不同之处在于可响应中断操作,即在获
    //取锁的过程中可中断,注意synchronized在获取锁时是不可中断的
    void lockInterruptibly() throws InterruptedException;

    //尝试非阻塞获取锁,调用该方法后立即返回结果,如果能够获取则返回true,否则返回false
    boolean tryLock();

    //根据传入的时间段获取锁,在指定时间内没有获取锁则返回false,如果在指定时间内当前线程未被中并断获取到锁则返回true
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

    //获取等待通知组件,该组件与当前锁绑定,当前线程只有获得了锁
    //才能调用该组件的wait()方法,而调用后,当前线程将释放锁。
    Condition newCondition();

ReetrantLock支持重进入,同时也支持公平锁与非公平锁。

基本原理:

AbstractQueuedSynchronizer又称为队列同步器(后面简称AQS),它是用来构建锁或其他同步组件的基础框架,内部通过一个int类型的成员变量state来控制同步状态,当线程调用lock方法进行加锁后,若此时state=0时,则说明没有任何线程占有共享资源的锁,当前线程可以获取到锁,同时将state设置为1,表示获取成功。当state=1时,则说明有线程目前正在使用共享变量,
那么当前执行线程将被封装为Node结点加入同步队列等待,AQS内部通过内部类Node构成FIFO的同步队列来完成线程获取锁的排队工作,同步队列采用的是双向链表的结构这样可方便队列进行结点增删操作。同时利用内部类ConditionObject构建等待队列,当Condition调用wait()方法后,线程将会加入等待队列中,而当Condition调用signal()方法后,线程将从等待队列转移动同步队列中进行锁竞争。
注意这里涉及到两种队列,一种的同步队列,当线程请求锁而等待的后将加入同步队列等待,而另一种则是等待队列(可有多个),通过Condition调用await()方法释放锁后,将加入等待队列。

Node包含了需要同步的线程本身以及线程的状态,如是否被阻塞,是否等待唤醒,是否已经被取消等。每个Node结点内部关联其前继结点prev和后继结点next,这样可以方便线程释放锁后快速唤醒下一个在等待的线程,

共享模式和独占模式:所谓共享模式是一个锁允许多条线程同时操作,如信号量Semaphore采用的就是基于AQS的共享模式实现的,而独占模式则是同一个时间段只能有一个线程对共享资源进行操作,多余的请求线程需要排队等待,如ReentranLock。

变量waitStatus则表示当前被封装成Node结点的等待状态,共有4种取值CANCELLED、SIGNAL、CONDITION、PROPAGATE。

CANCELLED:值为1,在同步队列中等待的线程等待超时或被中断,需要从同步队列中取消该Node的结点,其结点的waitStatus为CANCELLED,即结束状态,进入该状态后的结点将不会再变化。

SIGNAL:值为-1,被标识为该等待唤醒状态的后继结点,当其前继结点的线程释放了同步锁或被取消,将会通知该后继结点的线程执行。说白了,就是处于唤醒状态,只要前继结点释放锁,就会通知标识为SIGNAL状态的后继结点的线程执行。

CONDITION:值为-2,与Condition相关,该标识的结点处于等待队列中,结点的线程等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。

PROPAGATE:值为-3,与共享模式相关,在共享模式中,该状态标识结点的线程处于可运行状态。

0状态:值为0,代表初始化状态。
pre和next,分别指向当前Node结点的前驱结点和后继结点,thread变量存储的请求锁的线程。nextWaiter,与Condition相关,代表等待队列中的后继结点

AQS采用的模板模式的方式构建的,其内部除了提供并发操作核心方法以及同步队列操作外,还提供了一些模板方法让子类自己实现,如加锁操作以及解锁操作。
为什么这么做?
这是因为AQS作为基础组件,封装的是核心并发操作,但是实现上分为两种模式,即共享模式与独占模式,而这两种模式的加锁与解锁实现方式是不一样的,但AQS只关注内部公共方法实现并不关心外部不同模式的实现,所以提供了模板方法给子类使用,也就是说实现独占锁,如ReentrantLock需要自己实现tryAcquire()方法和tryRelease()方法,而实现共享模式的Semaphore,则需要实现tryAcquireShared()方法和tryReleaseShared()方法,这样做的好处是显而易见的,无论是共享模式还是独占模式,其基础的实现都是同一套组件(AQS),只不过是加锁解锁的逻辑不同罢了,更重要的是如果我们需要自定义锁的话,也变得非常简单,只需要选择不同的模式实现不同的加锁和解锁的模板方法即可,

Condition接口

在并发编程中,每个Java对象都存在一组监视器方法,如wait()notify()以及notifyAll()方法,通过这些方法,我们可以实现线程间通信与协作(也称为等待唤醒机制),如生产者-消费者模式,而且这些方法必须配合着synchronized关键字使用。见上文分析。
与synchronized的等待唤醒机制相比Condition具有更多的灵活性以及精确性,这是因为notify()在唤醒线程时是随机(同一个锁),而Condition则可通过多个Condition实例对象建立更加精细的线程控制,也就带来了更多灵活性了,我们可以简单理解为以下两点

通过Condition能够精细的控制多线程的休眠与唤醒。await()singal()

对于一个锁,我们可以为多个线程间建立不同的Condition。

Condition的具体实现类是AQS的内部类ConditionObject,前面我们分析过AQS中存在两种队列,一种是同步队列,一种是等待队列,而等待队列就相对于Condition而言的。注意在使用Condition前必须获得锁,同时在Condition的等待队列上的结点与前面同步队列的结点是同一个类即Node,其结点的waitStatus的值为CONDITION。在实现类ConditionObject中有两个结点分别是firstWaiter和lastWaiter,firstWaiter代表等待队列第一个等待结点,lastWaiter代表等待队列最后一个等待结点,

每个Condition都对应着一个等待队列,也就是说如果一个锁上创建了多个Condition对象,那么也就存在多个等待队列。等待队列是一个FIFO的队列,在队列中每一个节点都包含了一个线程的引用,而该线程就是Condition对象上等待的线程。当一个线程调用了await()相关的方法,那么该线程将会释放锁,并构建一个Node节点封装当前线程的相关信息加入到等待队列中进行等待,直到被唤醒、中断、超时才从队列中移出。Condition中的等待队列模型如下

你对ReentrantLock的理解?
同一个线程没进入一次,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。
Synchronized是依赖于JVM实现的,而ReenTrantLock是JDK实现
synchronize由编译器生成对应字节码去保证锁的加锁和释放,而ReenTrantLock需要手工声明来加锁和释放锁,为了避免忘记手工释放锁造成死锁,所以最好在finally中声明释放锁。
  ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。
2.      ReenTrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,选择性通知:一个lock创建多个condition实例,将线程对象注册到指定的condition中,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。
3.      ReenTrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。
ReenTrantLock的实现是一种自旋锁,通过循环调用CAS操作来实现加锁。
AQS(AbstractQueuedSynchronizer)
使用了模板设计模式,使用者继承aqs,并重写指定的方法。然后将aqs组合在自定义同步组件的实现中,并调用其模板方法。,由具体实现控制加锁具体的应用:比如 CountdownLatch 、CyclicBarrier、Semaphore信号量等
AQS,队列同步器,在juc包中的工具类都是依赖于AQS来实现同步控制。抽象类,本身没有实现任何的同步接口,仅仅定义了同步状态的获取以及释放的方法来提供自定义的同步组件。分为独占锁和共享锁(读写锁的读模式)。
内部有先进先出的双向队列,如果当前线程竞争锁失败,那么aqs会把当前线程以及等待状态信息构成一个node加入到同步等待队列,设置prev节点和前一个节点的next,再通过cas将tail指向自己,同时再阻塞该线程。head节点表示获取锁成功的节点,当获取锁的线程释放锁以后,会从队列中唤醒一个阻塞节点(线程)。获取成功则把自己设置为头节点,这一步不需要cas。
AQS可以被当做是一个同步监视器的实现,并且具有排队功能。
其实就是利用CAS尝试设置AQS的state为1。如果设置成功,表示获取锁成功;如果设置失败,表示state之前已经是>=1,已经被别的线程占用了AQS的锁,所示无法设置state为1,稍后会把线程加入到等待队列。

可重入性

锁的可重入性:从互斥锁的设计上来说,当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁,请求将会成功。

synchronized和ReetrantLock是可重入的。
synchronized:在获取当前实例对象锁后进入synchronized代码块执行同步代码,并在代码块中调用了当前实例对象的另外一个synchronized方法,再次请求当前实例锁时,将被允许,进而执行方法体代码,这就是重入锁最直接的体现,需要特别注意另外一种情况,当子类继承父类时,子类也是可以通过可重入锁调用父类的同步方法。注意由于synchronized是基于monitor实现的,因此每次重入,monitor中的计数器仍会加1。

两者比较:

在JDK 1.6之后,虚拟机对于synchronized关键字进行整体优化后,在性能上synchronized与ReentrantLock已没有明显差距,因此在使用选择上,需要根据场景而定,大部分情况下我们依然建议是synchronized关键字,原因之一是使用方便语义清晰,二是性能上虚拟机已为我们自动优化。而ReentrantLock提供了多样化的同步特性,如超时获取锁、可以被中断获取锁(synchronized的同步是不能中断的)、等待唤醒机制的多个条件变量(Condition)等,因此当我们确实需要使用到这些功能是,可以选择ReentrantLock。

synchronized属于隐式锁,即锁的持有与释放都是隐式的,我们无需干预。
显式锁,即锁的持有和释放都必须由我们手动编写,可以在一个方法中获得锁,在另外一个方法中释放。也可以通过tryLock来判断是否获取到了锁。

synchronize和lock锁如何选择

1,等待可中断

等待可中断是指当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情,可中断特性对处理执行时间非常长的同步块很有帮助。

参看这篇博客lock应用之可中断

2,可实现公平锁

公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁;而非公平锁则不保证这一点,在锁被释放时,任何一个等待锁的线程都有机会获得锁。

synchronized中的锁是非公平的,ReentrantLock默认情况下也是非公平的,但可以通过带布尔值的构造函数要求使用公平锁。

在创建lock时添加true参数即可实现公平锁。Lock lock = new ReentrantLock(true);

一般来说,用到的都是非公平锁,非公平锁的性能比公平锁的性能快5—10倍,因为公平锁单独维护了一个队列,如果当前线程不是队列第一个元素则获取不到锁,增加线程切换的次数。

公平锁:多线执行的顺序维度。

3,锁可以绑定多个条件

锁绑定多个条件是指一个ReentrantLock对象可以同时绑定多个Condition对象,而在synchronized中,锁对象的wait()和notify()或notifyAll()方法可以实现一个隐含的条

件,如果要和多于一个的条件关联的时候,就不得不额外地添加一个锁,而ReentrantLock则无须这样做,只需要多次调用newCondition()方法即可。

4,lock锁可以实现读写锁

详情可参看这篇博客 java lock锁之读写锁 

当我们需要这些扩展的功能的时候推荐使用lock锁,只需要简单的实现互斥代码推荐使用synchronize锁

1.8线程中断:

在线程运行(run方法)中间打断它,在Java中,提供了以下3个有关线程中断的方法:

//中断线程(实例方法)
public void Thread.interrupt();

//判断线程是否被中断(实例方法)
public boolean Thread.isInterrupted();

//判断是否被中断并清除当前中断状态(静态方法)
public static boolean Thread.interrupted();

当一个线程处于被阻塞状态或者试图执行一个阻塞操作时,使用Thread.interrupt()方式中断该线程,注意此时将会抛出一个InterruptedException的异常,同时中断状态将会被复位(由中断状态改为非中断状态)

一种是当线程处于阻塞状态或者试图执行一个阻塞操作时,我们可以使用实例方法interrupt()进行线程中断,执行中断操作后将会抛出interruptException异常(该异常必须捕捉无法向外抛出)并将中断状态复位,另外一种是当线程处于运行状态时,我们也可调用实例方法interrupt()进行线程中断,但同时必须手动判断中断状态,并编写中断线程的代码(其实就是结束run方法体的代码)。有时我们在编码时可能需要兼顾以上两种情况,那么就可以如下编写:
 

public void run(){
    try {
    //判断当前线程是否已中断,注意interrupted方法是静态的,执行后会对中断状态进行复位
    while (!Thread.interrupted()) {
        TimeUnit.SECONDS.sleep(2);
    }
    } catch (InterruptedException e) {

    }
}

线程的中断操作对于正在等待获取的锁对象的synchronized方法或者代码块并不起作用,也就是对于synchronized来说,如果一个线程在等待锁,那么结果只有两种,要么它获得这把锁继续执行,要么它就保存等待,即使调用中断线程的方法,也不会生效。

而使用lock则可以进行中断响应。
 

1.8单例模式的优缺点和使用场景三种实现延迟加载的单例模式:

1.在单例模式中,活动的单例只有一个实例,对单例类的所有实例化得到的都是相同的一个实例。这样就 防止其它对象对自己的实例化,确保所有的对象都访问一个实例

  4.由于在系统内存中只存在一个对象,因此可以 节约系统资源,当 需要频繁创建和销毁的对象时单例模式无疑可以提高系统的性能。

缺点: 
    1.不适用于变化的对象,如果同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态

 1.资源共享的情况下,避免由于资源操作时导致的性能或损耗等。如上述中的日志文件,应用配置。 
    2.控制资源的情况下,方便资源之间的互相通信。如线程池等。 

//1.双重询问
class Single{
    private static volatile Single instance;
    public static Single getInstance(){
        if(instance==null){
            synchronize(Single.this){
                if(instance==null){
                    instance=new Single();
                }
            }
        }
        return instance;
    }
}

//2、基于静态内部类的单例模式:
class Single{
    private static class InstanceHolder{'
        final static Single instance=new Single();
    }
    public static Single getInstance(){
        return InstancHolder.instance;
    }
}

//3、通过枚举类型:
enum Single{
    INSTANCE;
    public void doSomeThing(){}
}

静态内部类也是延迟加载,见校招准备:(二):java高级知识5.3关于静态内部类的问题。

1.9CAS

Java并发编程-无锁CAS与Unsafe类及其并发包Atomic

CAS(compare and swap):一种处理器指令的称呼,是一条CPU的原子指令,不会造成所谓的数据不一致问题。。属于乐观派,它总认为自己可以成功完成操作,当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。
通过CAS实现线程安全:

boolen compareAndSwap(Variable v,Object a,Object b ){
    if(a=v.get()){//如果没有修改过则进行修改
        v.set(b);
        return true;
    }
    return false;
}
对于其的使用:
public void increment() {
    long oldValue;
    long newValue;
    do {
        oldValue = count;// 读取共享变量当前值
        newValue = oldValue + 1;// 计算共享变量的新值
    } while (/* 调用CAS来更新共享变量的值 */!compareAndSwap(oldValue, newValue));
}


但是这种方式不好处理aba,应该加上变量对应的修订号或者时间戳来判断是否真的改变过。

Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,单从名称看来就可以知道该类是非安全的,毕竟Unsafe拥有着类似于C的指针操作,因此总是不应该首先使用Unsafe类。

在Java中无锁操作CAS基于以下3个方法实现,在稍后讲解Atomic系列内部方法是基于下述方法的实现的。

//第一个参数o为给定对象,offset为对象内存的偏移量,通过这个偏移量迅速定位字段并设置或获取该字段的值,
//expected表示期望值,x表示要设置的值,下面3个方法都通过CAS原子指令执行操作。
public final native boolean compareAndSwapObject(Object o, long offset,Object expected, Object x);                                                                                                  

public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x);

public final native boolean compareAndSwapLong(Object o, long offset,long expected,long x);

//这是一个CAS操作过程,直到设置成功方能退出循环,返回旧值
 public final int getAndAddInt(Object o, long offset, int delta) {
     int v;
     do {
         //获取内存中最新值
         v = getIntVolatile(o, offset);
       //通过CAS操作
     } while (!compareAndSwapInt(o, offset, v, v + delta));
     return v;
 }

挂起与恢复
将一个线程进行挂起是通过park方法实现的,调用 park后,线程将一直阻塞直到超时或者中断等条件出现。unpark可以终止一个挂起的线程,使其恢复正常。Java对线程的挂起操作被封装在 LockSupport类中,LockSupport类中有各种版本pack方法,其底层实现最终还是使用Unsafe.park()方法和Unsafe.unpark()方法

内存屏障

这里主要包括了loadFence、storeFence、fullFence等方法,这些方法是在Java 8新引入的,用于定义内存屏障,避免代码重排序,与Java内存模型相关。

并发包中的原子操作类(Atomic系列)

通过前面的分析我们已基本理解了无锁CAS的原理并对Java中的指针类Unsafe类有了比较全面的认识,下面进一步分析CAS在Java中的应用,即并发包中的原子操作类(Atomic系列),从JDK 1.5开始提供了java.util.concurrent.atomic包,在该包中提供了许多基于CAS实现的原子操作类,用法方便,性能高效,主要分以下4种类型。

1.原子更新基本类型

原子更新基本类型主要包括3个类:

  • AtomicBoolean:原子更新布尔类型
  • AtomicInteger:原子更新整型
  • AtomicLong:原子更新长整型

2.原子更新引用

原子更新引用类型可以同时更新引用类型,比如AtomicReference原子类

3.原子更新数组
原子更新数组指的是通过原子的方式更新数组里的某个元素,主要有以下3个类

AtomicIntegerArray:原子更新整数数组里的元素
AtomicLongArray:原子更新长整数数组里的元素
AtomicReferenceArray:原子更新引用类型数组里的元素

4.原子更新属性
如果我们只需要某个类里的某个字段,也就是说让普通的变量也享受原子操作,可以使用原子更新字段类,如在某些时候由于项目前期考虑不周全,项目需求又发生变化,使得某个类中的变量需要执行多线程操作,由于该变量多处使用,改动起来比较麻烦,而且原来使用的地方无需使用线程安全,只要求新场景需要使用时,可以借助原子更新器处理这种场景,Atomic并发包提供了以下三个类:

AtomicIntegerFieldUpdater:原子更新整型的字段的更新器。
AtomicLongFieldUpdater:原子更新长整型字段的更新器。
AtomicReferenceFieldUpdater:原子更新引用类型里的字段。

请注意原子更新器的使用存在比较苛刻的条件如下

操作的字段不能是static类型。

操作的字段不能是final类型的,因为final根本没法修改。

字段必须是volatile修饰的,也就是数据本身是读一致的。

属性必须对当前的Updater所在的区域是可见的,如果不是当前类内部进行原子更新器操作不能使用private,protected子类操作父类时修饰符必须是protect权限及以上,如果在同一个package下则必须是default权限及以上,也就是说无论何时都应该保证操作类与被操作类间的可见性。

1.10线程池:

JAVA线程池学习以及队列拒绝策略

什么情况下使用线程池?
1、单个任务处理时间比较短
2、处理任务数量大

使用线程池:
降低资源消耗:重复利用已经创建的线程降低线程创建和销毁造成的损失,提高响应速度:不需要创建线程,提高线程的可管理性。

ThreadPoolExecutor类

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler); 
参数名、说明: 
* corePoolSize  线程池维护线程的最少数量 
* maximumPoolSize   线程池维护线程的最大数量 
* keepAliveTime 线程池维护线程所允许的空闲时间 
* workQueue 任务队列,用来存放我们所定义的任务处理线程 
* threadFactory 线程创建工厂 
* handler   线程池对拒绝任务的处理策略 

ThreadPoolExecutor将根据corePoolSize和maximumPoolSize设置的边界自动调整池大小。当新任务在方法execute(Runnable) 中提交时, 如果运行的线程少于corePoolSize,则创建新线程来处理请求。 

1.当线程池中的线程数没有达到coreSize时,提交一个任务,不论此时有没有线程处于空闲状态,则会创建一个新的线程!

2.当线程数达到coseSize时,此时在提交任务,如果有线程空闲,则执行任务,如果没有空闲,则放入taskQueue中等待被执行。

3.当taskQueue队列中放满任务时,如果此时所有的线程都处于执行任务期间,再次提交任务,则会创建一个新线程,用于执行队列中的任务,当线程数达到maxSize的时候,如果再次提交任务,如果任务队列已满,并且所有的线程处于活动期间,则提交任务失败,抛出RejectExecutionException.

总之,如果没有设置quitCore=true,核心线程不会被销毁,在线程数达到coreSize之前,每次提交任务都会创建新线程。
线程数达到coreSize后,提交任务会使用空闲线程执行,如果没有空闲线程,则会放到任务队列taskQueue中,等待被执行!
如果队列满了,所有coreSize线程都处于executing task的状态,则会创建一个新线程用于执行队列中的任务。

如果正在运行的线程等于corePoolSize时,ThreadPoolExecutor优先往队列中添加任务,直到队列满了,并且没有空闲线程时才创建新的线程。如果设置的corePoolSize 和 maximumPoolSize 相同,则创建了固定大小的线程池。 

keepAliveTime:当线程数达到maximumPoolSize时,经过某段时间,发现多出的线程出于空闲状态,就进行线程的回收。keepAliveTime就是线程池内最大的空闲时间。 
workQueue:当核心线程不能都在处理任务时,新进任务被放在Queue里。

线程池中任务有三种排队策略
直接提交。直接提交策略表示线程池不对任务进行缓存。新进任务直接提交给线程池,当线程池中没有空闲线程时,创建一个新的线程处理此任务。这种策略需要线程池具有无限增长的可能性。实现为:SynchronousQueue

有界队列。当线程池中线程达到corePoolSize时,新进任务被放在队列里排队等待处理。有界队列(如ArrayBlockingQueue)有助于防止资源耗尽,但是可能较难调整和控制。队列大小和最大池大小可能需要相互折衷:使用大型队列和小型池可以最大限度地降低 CPU 使用率、操作系统资源和上下文切换开销,但是可能导致人工降低吞吐量。如果任务频繁阻塞(例如,如果它们是 I/O 边界),则系统可能为超过您许可的更多线程安排时间。使用小型队列通常要求较大的池大小,CPU 使用率较高,但是可能遇到不可接受的调度开销,这样也会降低吞吐量。

无界队列。使用无界队列(例如,不具有预定义容量的 LinkedBlockingQueue)将导致在所有 corePoolSize 线程都忙时新任务在队列中等待。这样,创建的线程就不会超过 corePoolSize。(因此,maximumPoolSize 的值也就无效了。)当每个任务完全独立于其他任务,即任务执行互不影响时,适合于使用无界队列;例如,在 Web 页服务器中。这种排队可用于处理瞬态突发请求,当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性。

拒绝策略:Java线程池的拒绝策略

è¿éåå¾çæè¿°

ThreadPoolExecutor是Executors类的实现,Executors类里面提供了一些静态工厂,生成一些常用的线程池

FixedThreadPool(

          特点:1每一个线程都是核心线程,不设置quitCore=true时,每个线程都不会被销毁):

CachedThreadPool (

                特点:1.没有核心线程,所有线程空闲60秒后销毁

                           2.无边界,不管提交多少任务,都有线程去立刻执行,充分利用了SynchronousQueue容量为0的特点)

SingleThreadPool(

                     特点:只有一个核心线程,即按照任务提交的顺序,串行执行任务

ScheduledThreadPool:

          特点是:核心线程数固定,无最大边界,非核心线程空闲即销毁,可延时执行任务

1.11Threadlocal

这才是Thread Local的正确原理与适用场景

  • 因为每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用。这是也是 ThreadLocal 命名的由来

  • 既然每个 Thread 有自己的实例副本,且其它 Thread 不可访问,那就不存在多线程间共享的问

Map 由 ThreadLocal 类的静态内部类 ThreadLocalMap 提供。该类的实例维护某个 ThreadLocal 与具体实例的映射。与 HashMap 不同的是,ThreadLocalMap 的每个 Entry 都是一个对 键 的弱引用,这一点从 super(k) 可看出。另外,每个 Entry 都包含了一个对 值 的强引用。
 

如上文所述,ThreadLocal 适用于如下两种场景

  • 每个线程需要有自己单独的实例

  • 实例需要在多个方法中共享,但不希望被多线程共享

ThreadLocalMap 的 set 方法中,通过 replaceStaleEntry 方法将所有键为 null 的 Entry 的值设置为 null,从而使得该值可被回收。另外,会在 rehash 方法中通过 expungeStaleEntry 方法将键和值为 null 的 Entry 设置为 null 从而使得该 Entry 可被回收。通过这种方式,ThreadLocal 可防止内存泄漏。
 

 

 

 

基于数据的分割实现并发化
基于任务的分割实现并发化。
多线程实现生产者消费者模式;
1、阻塞队列。BlockQueue
2、流量控制和信号量(Semaphore)
使用无界队列不会导致生产者线程阻塞,但是可能会积压,需要信号量来进行流量控制限速。semaphore用来控制同一时间内对虚拟资源的访问次数,先申请配额acquire(),资源访问结束后返还配额release()。acquire在成功获取到一个配额后会立即返回,如果不够用则会使线程暂停。内部维持一个等待队列来存储暂停的线程。acquire返回前使配额-1,release使当前配额+1并唤醒等待队列中的一个线程。
3、通过管道pipedOutputStream生产者,PipedInputStream消费者。适合单生产者,单消费者
4、双缓存Exchanger生产者和消费者各自创建一个缓存区,消费者线程调用Exchanger.exchange(v)时,v为空的或消费过的,生产者则相反。只有两个线程都调用了,才会进行返回。

java线程终止:比如用户取消任务,服务或者系统关闭,错误处理(一个线程出错,其他线程没必要继续运行)。通过t.interrupt()为当前线程向t线程发起中断请求。而t线程有可能做出反应,也有可能不会处于阻塞状态。有些方法对中断的响应为抛出interruptedException异常,用户可以继续不捕获直接throw将异常抛出去,或者捕获一下做中间处理再throw,或者通过t.interrupt()保留中断标志。
要优雅的线程停止,最好将所有的任务做完再停止。

一个类的同一个实例变量被多个线程共享是否使线程处于共享状态。
无状态对象不包括任何实例变量或可更新的静态变量

runnable和callable:
实现类都可以被threadpoolexecutor执行,但是callable接口可以返回结果。excute用于提交不需要返回值的任务,submit用于提交需要返回值的任务,线程池会返回一个future类型的对象。
通过构造方法创建线程池threadPoolExecutor,也可以通过executors工具类创建三种类型的:fixedThreadPool(固定数量),SingleThreadExecutor(一个线程),CachedThreadPool(根据实际情况调整大小)
原子类有四种类型:基本类型,数组类型,引用类型,对象的属性修改类型。内部主要是利用cas+volatile保证原子操作

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值