区别:
this指代 当前类对象 。Java中this关键字的用法如下:
参考链接:Java基础知识(超详细解析,排版清晰!):this关键字
注意: 在使用this关键字时,可能会遇到这个报错:“Call to ‘this()’must be first statement in constructor body”,原因是:this()和super()为构造方法,作用是在JVM堆中构建出一个对象。因为避免多次创建对象,所以 一个方法只能调用一次this()或super()。this()和super()的调用只能写在第一行,避免操作对象时对象还未构建成功。而且**this()和super()不能同时出现**。
java线程中有两种线程,一种是用户线程,一种是守护线程。
守护线程是一种特殊的线程,它具有陪伴的含义。当**进程中不存在非守护线程了,则守护线程自动销毁。** 典型的守护线程就是垃圾回收线程。当进程中没有用户线程了,则垃圾回收线程没有存在的必要,自动销毁。任何一个守护线程都是整个JVM中所有非守护线程的保姆。只要当前JVM中有非守护线程没有结束,守护线程就在工作。只有当最后一个非守护线程不工作的时候,守护进程才随着JVM一同结束工作。Dammon(守护进程)的作用是为非守护进程的运行提供服务 。守护进程最典型的应用是GC(垃圾回收器),它是很称职的守护者。
从操作系统的角度来看,线程是CPU分配的最小单位。
synchronized void method() {
// 业务代码
}
synchronized void static method() {
// 业务代码
}
synchronized(this) {
// 业务代码
}
总结:
Synchronized的语义底层是通过一个 monitor(监视器锁) 的对象来完成。每个对象有一个监视器锁(monitor)。每个Synchronized修饰过的代码,当它的monitor被占用时就会处于锁定状态并且尝试获取monitor的所有权。
过程:
volitile关键字的作用是可以 使内存中的数据对象线程可见。主内存对线程是不可见的,添加 volitile 关键字后,主内存对线程可见。(每次读取数据都是直接从主内存中读取,而不是从本地内存处读取)
在JDK1.2之前,Java的内存模型实现总是从主存(即共享内存)读取变量,是不需要进行特别的注意的。而在当前的Java内存模型下,线程可以把变量保存本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。
要解决这个问题,就需要把变量声明为volatile ,这就 指示JVM,这个变量是共享且不稳定的, 每次使用它都到主存中进行读取。 所以,volatile关键字除了**①防止 JVM 的指令重排 ,还有一个重要的作用就是②保证变量的可见性。**
原子性: 原子性指的是一个操作是不可分割、不可中断的,要么全部执行并且执行的过程不会被任何因素打断,要么就全不执行。
可见性: 可见性指的是一个线程修改了某一个共享变量的值时,其它线程能够立即知道这个修改。
有序性: 有序性指的是对于一个线程的执行代码,从前往后依次执行,单线程下可以认为程序是有序的,但是并发时有可能会发生指令重排。
原子性: JMM只能保证基本的原子性,如果要保证一个代码块的原子性,需要使用 synchronized 。
可见性: Java是利用 volatile 关键字来保证可见性的,除此之外,final和synchronized也能保证可见性。
有序性: synchronized或者volatile 都可以保证多线程之间操作的有序性。
在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。重排序分3种类型:
volatile有两个作用,保证 可见性 和 有序性。
volatile怎么保证可见性的呢?
相比synchronized的加锁方式来解决共享变量的内存可见性问题,volatile就是更轻量的选择,它没有上下文切换的额外开销成本。
volatile可以确保对某个变量的更新对其他线程马上可见,一个变量被声明为volatile 时,线程在写入变量时不会把值缓存在寄存器或者其他地方,而是会把值刷新回主内存,当其它线程读取该共享变量,会从主内存重新获取最新值,而不是使用当前线程的本地内存中的值。
volatile怎么保证有序性的呢?
重排序可以分为编译器重排序和处理器重排序,valatile保证有序性,就是通过分别限制这两种类型的重排序。
synchronized关键字和volatile关键字是两个互补的存在,而不是对立的存在!
volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好。但是 volatile关键字只能用于变量,而synchronized关键字可以修饰方法以及代码块。
volatile关键字 能保证数据的可见性(√),但不能保证数据的原子性(×)。synchronized关键字 两者都能保证。
volatile关键字主要用于 解决变量在多个线程之间的可见性,而synchronized 关键字解决的是 多个线程之间访问资源的同步性 。
即synchronized是线程安全的,而volatile是非线程安全的。
公平锁: 线程同步时,多个线程排队时,顺序执行。
非公平锁: 线程同步时,多个线程排队时,可以插队。
CountDownLatch减法计数器: 允许一个或多个线程等待其他线程完成操作。
可以用来倒计时,当两个线程同时执行时,如果要确保一个线程先执行,可以使用计数器,当计数器清零的时候,再让另一个线程执行。
适用场景: ①协调子线程结束动作:等待所有子线程运行结束,如王者开黑得等待所有人上线才能开始打;②协调子线程开始动作:统一各线程动作开始的时机。
CountDownLatch的核心方法有:
· await():等待latch降为0 。
· boolean await(long timeout, TimeUnit unit):等待latch降为0,但是可以设置超时时间。比如有玩家超时未确认,那就重新匹配,总不能为了某个玩家等到天荒地老。
· countDown():latch数量减1 。
· getCount():获取当前的latch数量。
CyclicBarrier加法计数器: 同步屏障,可循环使用(Cyclic)的屏障(Barrier)。
它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。
可以用来计时,当执行的次数达到CyclicBarrier设置的值时,就会执行CyclicBarrier参数中的接口中实现的代码,达到一次条件就会执行一次CyclicBarrier中的接口方法。
semaphore计数信号量: 信号量。用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源。
实际开发时可以使用它来完成限流操作(流量控制),限制可以访问某些资源的线程数量,如数据库连接。
都可以协调多线程的结束动作,在它们结束后都可以执行特定动作。
二者的区别在于: ① CountDownLatch的执行是一次性的,无法重复利用;CyclicBarrier可以重复使用。② CountDownLatch中的各个子线程不可以等待其他线程,只能完成自己的任务;而CyclicBarrier中的各个线程可以等待其他线程。
Exchanger(交换者)是一个用于线程间协作的工具类。Exchanger用于 进行线程间的数据交换 。它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据。
假如两个线程有一个没有执行exchange()方法,则会一直等待,如果担心有特殊情况发生,避免一直等待,可以使用exchange(V x, long timeOut, TimeUnit unit) 设置最大等待时长。
corePoolSize 核心池大小,初始化的线程数量。
maximumPoolSize 线程池最大线程数,它决定了线程池容量的上限。
corePoolSize 就是线程池的大小,maximumPoolSize是一种补救措施,任务量突然增大的时候的一种补救措施。
keepAliveTime=线程对象的存活时间(在没有任务可执行的情况下),必须是线程池中的数量大于corePoolSize,才会生效。
unit 线程对象存活时间单位。
workQueue 等待队列,存储等待执行的任务。
threadFactory 线程工厂,用来创建线程对象。
handler 拒绝策略。
拒绝策略有4种:
线程池的顶级接口是 Executors,创建线程池是通过创建 ThreadPoolExecutors 对象来完成的。
① 降低资源消耗。
通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
② 提高响应速度。
当任务到达时,任务可以不需要的等到线程创建就能立即执行。
③ 提高线程的可管理性。
线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
execute用于 提交不需要返回值的任务。
submit()方法用于 提交需要返回值的任务 。线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值。
可以通过调用线程池的 shutdown 或 shutdownNow 方法来关闭线程池。它们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。
shutdown()将线程池状态置为shutdown,并不会立即停止(将队列中的任务全部执行完之后再停止):
线程池的状态有以下5种:
线程池提供了几个setter方法来设置线程池的参数。
这里主要有两个思路:
修饰类,表示类不可以被继承。
修饰方法,表示方法不可以被子类覆盖,但是可以重载。
修饰变量,表示变量一旦被赋值就不可以更改它的值。(不可变指的是变量的引用不可变,不是引用指向的内容的不可变。)
1. 修饰成员变量
如果final修饰的是类变量(静态变量),只能在静态初始块中指定初始值或者声明该类变量时指定初始值。
如果final修饰的是成员变量,可以在非静态初始化块声明该变量或者构造器中执行初始值。
2. 修饰局部变量
系统不会为局部变量进行初始化,局部变量必须由程序员显示初始化。因此,final修饰局部变量时,可以在定义时指定默认值(后面的代码中无法再对变量赋值),也可以不指定默认值,而是在后面的代码中对final变量赋初值(仅能赋值一次)
3. 修饰基本数据类型和引用数据类型
如果是基本数据类型的变量,则其值一旦在初始化后便不能再更改。
如果是引用类型的变量,则在对其初始化后便不能再让其指向另一个对象,但是**引用的值是可变的**。
1. 互斥条件: 该资源任意一个时刻只由一个线程占用。
2. 请求与保持条件: 一个线程因请求资源而阻塞时,对已获得的资源保持不放。
3. 不剥夺条件: 线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
4. 循环等待条件: 若干线程之间形成一种头尾相接的循环等待资源关系。
- 如何预防死锁? 破坏死锁的产生的必要条件即可:
①破坏请求与保持条件:一次性申请所有的资源。
②破坏不剥夺条件:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
③破坏循环等待条件:靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。
- 如何避免死锁?
避免死锁就是在资源分配时,借助于算法(比如 银行家算法 )对资源分配进行计算评估,使其进入安全状态。
安全状态 指的是系统能够按照某种线程推进顺序(P1、P2、P3…Pn)来为每个线程分配所需资源,直到满足每个线程对资源的最大需求,使每个线程都可顺利完成。称
可以使用jdk自带的命令行工具排查:
线程类的构造方法、静态块是被new这个线程类所在的线程所调用的,而run方法里面的代码才是被线程自身所调用的(线程自身只执行run方法里的代码)。
如上面的代码所示,程序中包含两个线程,main主线程和t线程,由于t线程是在main主线程内创建,因此t线程的构造方法、静态块代码均由main线程调用执行,t线程内的run方法中的代码由t线程自身调用执行,由于是两个线程在运行,势必存在争抢CPU资源,所有两种输出结果均有可能。
ThreadLocal叫做 线程本地变量,意思是ThreadLocal中填充的变量属于 当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量 。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量,多个线程操作这个变量的时候,实际是操作自己本地内存里面的变量,从而起到线程隔离的作用,避免了线程安全问题。
ThreadLoal变量,线程局部变量,同一个ThreadLocal所包含的对象,在不同的Thread中有不同的副本。这里有几点需要注意:
ThreadLocal 其实是与线程绑定的一个变量。ThreadLocal和Synchonized都用于 解决多线程并发访问 。
但是ThreadLocal与synchronized有本质的 区别:
① Synchronized用于线程间的 数据共享 ,而ThreadLocal则用于线程间的 数据隔离 。
② Synchronized是利用 锁的机制 ,使变量或代码块在某一时该只能被一个线程访问。而ThreadLocal为每一个线程都 提供了变量的副本 ,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。
而Synchronized却正好相反,它用于在多个线程间通信时能够获得数据共享。
一句话理解ThreadLocal,threadlocal是作为当前线程中属性ThreadLocalMap集合中的某一个Entry的key值Entry(threadlocal,value),虽然不同的线程之间threadlocal这个key值是一样,但是不同的线程所拥有的ThreadLocalMap是独一无二的,也就是不同的线程间同一个ThreadLocal(key)对应存储的值(value)不一样,从而到达了线程间变量隔离的目的,但是在同一个线程中这个value变量地址是一样的。
① 每个线程需要有自己单独的实例。
② 实例需要在多个方法中共享,但不希望被多线程共享。
对于第一点,每个线程拥有自己实例,实现它的方式很多。例如可以在线程内部构建一个单独的实例。ThreadLocal可以以非常方便的形式满足该需求。
对于第二点,可以在满足第一点(每个线程有自己的实例)的条件下,通过方法间引用传递的形式实现。ThreadLocal使得代码耦合度更低,且实现更优雅。
举例: 我们的系统应用是一个典型的MVC架构,登录后的用户每次访问接口,都会在请求头中携带一个token,在控制层可以根据这个token,解析出用户的基本信息。那么问题来了,假如在服务层和持久层都要用到用户信息,比如rpc调用、更新用户获取等等,那应该怎么办呢?
一种办法是显式定义用户相关的参数,比如账号、用户名……这样一来,我们可能需要大面积地修改代码,多少有点瓜皮,那该怎么办呢?
这时候我们就可以用到ThreadLocal,在控制层拦截请求把用户信息存入ThreadLocal,这样我们在任何一个地方,都可以取出ThreadLocal中存的用户数据。
很多其它场景的 cookie、session等等数据隔离 也都可以通过ThreadLocal去实现。
我们常用的 数据库连接池 也用到了ThreadLocal:数据库连接池的连接交给ThreadLocal进行管理,保证当前线程的操作都是同一个Connnection。
Entry将 ThreadLocal作为Key,值作为value保存,它继承自WeakReference(弱引用),注意构造函数里的第一行代码super(k),这意味着ThreadLocal对象是一个「弱引用」,如下:
主要两个原因:
① 没有手动删除这个Entry。
② CurrentThread当前线程依然运行。
第一点很好理解,只要在使用完ThreadLocal后,调用其 remove方法 删除对应的Entry,就能避免内存泄漏。
第二点稍微复杂一点,由于ThreadLocalMap是Thread的一个属性,被当前线程所引用,所以ThreadLocalMap的生命周期跟Thread一样长。如果threadlocal变量被回收,那么当前线程的threadlocal 变量副本指向的就是key=null, 也即entry(null,value),那这个entry对应的value永远无法访问到。实际使用ThreadLocal场景都是采用线程池,而线程池中的线程都是复用的,这样就可能导致非常多的entry(null,value)出现,从而导致内存泄露。
综上,ThreadLocal内存泄漏的根源是: threadLocal中的键是弱引用,弱引用很容易被回收,如果ThreadLocal(ThreadLocalMap的Key)被垃圾回收器回收了,但是ThreadLocalMap的生命周期跟 Thread一样长,对于重复利用的线程来说,如果没有手动删除(remove()方法)对应key就会导致entry(null,value)的对象越来越多,从而导致内存泄漏。
ThreadLocalMap的key为弱引用,value为强引用,GC发生时,该key的ThreadLocal对象被回收,此时该Entry的key为null,value为强引用,也就是说此时value无法通过key访问到并且也不会被回收,从而导致内存泄漏。
那么为什么ThreadLocalMap的key要设计成弱引用呢? 其实很简单,如果key设计成强引用且没有手动remove(),那么key会和value一样伴随线程的整个生命周期。
1、假设在业务代码中使用完ThreadLocal,ThreadLocal引用被回收了,但是 因为threadLocalMap的Entry强引用了threadLocal(key就是threadLocal), 造成ThreadLocal无法被回收。 在没有手动删除Entry以及CurrentThread(当前线程)依然运行的前提下, 始终有强引用链CurrentThread Ref → CurrentThread →Map(ThreadLocalMap)-> entry, Entry就不会被回收( Entry中包括了ThreadLocal实例和value), 导致Entry内存泄漏,也就是说:ThreadLocalMap中的key即使使用了强引用, 也无法完全避免内存泄漏。
事实上,在ThreadLocalMap中的set/getEntry方法中,会对key为null(即使ThreadLocal为null)进行判断,如果为null的话,那么会把value置为null的。这就意味着使用threadLocal , CurrentThread依然运行的前提下,就算忘记调用remove方法,弱引用比强引用可以多一层保障:弱引用的ThreadLocal会被回收。对应value在下一次ThreadLocaI调用get()/set()/remove()中的任一方法的时候会被清除,从而避免内存泄漏。
① 将ThreadLocal变量定义成private static 的,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后remove它,防止内存泄露。
② 每次~~使用完ThreadLocal,都调用它的remove()方法~~ ,清除数据。
参考链接:
史上最全ThreadLocal 详解(一)、史上最全ThreadLocal 详解(二)
父线程能用ThreadLocal来给子线程传值吗?毫无疑问,不能。那该怎么办?
这时候可以用到另外一个类——InheritableThreadLocal 。
使用起来很简单,在主线程的InheritableThreadLocal实例设置值,在子线程中就可以拿到了。
那原理是什么呢?
原理很简单,在Thread类里还有另外一个变量:
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
在 Thread.init 的时候,如果父线程的inheritableThreadLocals不为空,就把它赋给当前线程(子线程)的inheritableThreadLocals 。
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
出现内存泄漏、上下文切换、线程安全、死锁。
注意:
线程在生命周期中会经历 新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和终止(Terminated) 5种状态。
新建状态: 使用 new 关键字创建了一个线程之后,该线程就处于新建状态,此时仅由JVM为其分配内存,并初始化其成员变量的值。
就绪状态: 当线程对象 调用了 start() 方法之后 ,该线程处于就绪状态。Java虚拟机会为其创建方法调用栈和程序计数器,等待调度运行。
运行状态: 如果处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,则该线程处于运行状态。
阻塞状态: 阻塞状态是指线程因为某种原因放弃了cpu使用权,也即让出了cpu时间片,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得cpu时间片转到运行(running)状态。阻塞的情况分3种:
①等待阻塞(wait -> 等待队列)
运行(running)的线程执行wait()方法,JVM会把该线程放入等待队列中。
②同步阻塞(lock -> 锁池)
运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
③其他阻塞(sleep/join/yield) 运行(running)的线程执行 Thread.sleep(long ms)或 join() 或 yield() 方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。
终止状态: 线程运行完毕或因异常导致线程终止运行。
1. 线程正常运行结束。
2. 使用退出标志退出线程。
3. 调用interrupt方法结束线程。
①线程处于阻塞状态: 如使用了sleep,同步锁的wait,socket中的receiver,accept等方法时,会使线程处于阻塞状态。当 调用线程的interrupt()方法时,会抛出InterruptException异常。 阻塞中的那个方法抛出这个异常,通过代码捕获该异常,然后break跳出循环状态,从而让我们有机会结束这个线程的执行。通常很多人认为只要调用 interrupt 方法线程就会结束,实际上是错的,一定要先捕获 InterruptedException 异常之后通过 break 来跳出循环,才能正常结束 run 方法。(线程处于阻塞状态,只有捕获了InterruptedException 异常之后,才能正常终止线程)
②线程未处于阻塞状态: 使用 isInterrupted() 判断线程的中断标志来退出循环。当使用 interrupt()方法时,中断标志就会置 true,线程可以终止。
4. 调用stop方法终止线程(线程不安全)。
线程上下文切换就是 保存当前线程的运行条件和状态(上下文),加载其他线程的上下文信息 。
在两个或者多个并发进程中,如果每个进程持有某种资源而又等待其它进程释放它或它们现在保持着的资源,在未改变这种状态之前都不能向前推进,称这一组进程产生了死锁。
通俗的讲就是 两个或多个进程无限期的阻塞、相互等待的一种状态 。
如何处理死锁问题
常用的处理死锁的方法有:死锁预防、死锁避免、死锁检测、死锁解除、鸵鸟策略(忽略)。
阻塞 是由于 资源不足引起的排队等待 的现象。
CAS(Compare And Swap/Set)比较并交换,无锁优化,乐观锁机制,锁自旋,主要用来保证操作的原子性。位于java.util.concurrent.atomic.xxx 包下,属于原子操作。
CAS算法过程: 它包含 3 个参数 CAS(V,E,N)。 V(variate)表示要更新的变量(内存值),E(expect)表示预期值(旧的),N(new)表示新值。当且仅当 V 值等于 E 值时,才会将 V 的值设为 N,如果 V 值和 E 值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。最后,CAS 返回当前 V 的真实值。(看要更新的值是否等于期望值)
CAS 操作是基于乐观锁机制,认为操作可以成功完成。当多个线程同时使用 CAS 操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。 基于这样的原理, CAS 操作即使没有锁,也可以发现其他线程对当前线程的干扰,并进行恰当的处理。
JDK1.5 的原子包:java.util.concurrent.atomic 这个包里面提供了一组原子类。其基本的特性就是在多线程环境下,当有多个线程同时执行这些类的实例包含的方法时,具有排他性,即当某个线程进入方法,执行其中的指令时,不会被其他线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完成,才由 JVM 从等待队列中选择一个线程进入 ,这只是一种逻辑上的理解。相对于 synchronized 这种阻塞算法,CAS 是 非阻塞算法 的一种常见实现。
CAS 会导致“ABA问题”。CAS 算法实现一个重要前提需要取出内存中某时刻的数据,而在下一时刻比较并替换,那么这个时间差中可能会存在数据变化 。比如说一个线程 one 从内存位置 V 中取出 A,这时候另一个线程 two 也从内存中取出 A,并且 two 进行了一些操作变成了 B,然后 two 又将 V 位置的数据变成 A,这时候线程 one 进行 CAS 操作发现内存中仍然是 A,然后 one 操作成功。尽管线程 one 的 CAS 操作成功,但是不代表这个过程就是没有问题的。 部分乐观锁的实现是 通过版本号(version)的方式来解决 ABA 问题 ,乐观锁每次在执行数据的修改操作时,都会带上一个版本号,一旦版本号和数据的版本号一致就可以执行修改操作并对版本号执行 +1 操作,否则就执行失败。因为每次操作的版本号都会随之增加,所以不会出现 ABA 问题,因为版本号只会增加不会减少。
怎么解决ABA问题?加版本号。
每次修改变量,都在这个变量的版本号上加1,这样,刚刚A->B->A,虽然A的值没变,但是它的版本号已经变了,再判断版本号就会发现此时的A已经被改过了。Java提供了AtomicStampReference类,它的compareAndSet方法首先检查当前的对象引用值是否等于预期引用,并且当前印戳(Stamp)标志是否等于预期标志,如果全部相等,则以原子方式将引用值和印戳标志的值更新为给定的更新值。
怎么解决循环性能开销问题?
在Java中,很多使用自旋CAS的地方,会有一个自旋次数的限制,超过一定次数,就停止自旋。
怎么解决只能保证一个变量的原子操作问题?
可以考虑改用 锁 来保证操作的原子性。
可以考虑合并多个变量,将多个变量封装成一个对象,通过AtomicReference来保证原子性。
AbstractQueuedSynchronizer ,抽象的队列式的同步器,AQS 定义了一套多线程访问共享资源的 同步器框架 ,许多同步类实现都依赖于它,如常用的 ReentrantLock/Semaphore/CountDownLatch。
维护一个 volatile int state共享资源 和一个 FIFO线程等待队列(多线程争抢资源时会进入该阻塞队列等待资源)。state 的访问方式有三种: getState()、setState()、compareAndSetState()。
1. Exclusive 独占资源-ReentrantLock
Exclusive(独占,只有一个线程能执行,如 ReentrantLock)
2. Share 共享资源-Semaphore/CountDownLatch
Share(共享,多个线程可同时执行,如 Semaphore/CountDownLatch)。
AQS 只是一个框架,具体资源的获取/释放方式交由自定义同步器去实现 ,AQS 这里只定义了一个接口,具体资源的获取交由自定义同步器去实现了(通过 state 的 get/set/CAS)之所以没有定义成 abstract,是因为独占模式下只用实现tryAcquire-tryRelease,而共享模式下只用实现 tryAcquireShared-tryReleaseShared。如果都定义成abstract,那么每个模式也要去实现另一模式下的接口。不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/ 唤醒出队等),AQS 已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法:
同步器的实现是 ABS 核心,以 ReentrantLock 为例,state 初始化为 0,表示未锁定状态。A 线程 lock()时,会调用 tryAcquire()独占该锁并将 state+1。此后,其他线程再 tryAcquire()时就会失败,直到 A 线程 unlock()到 state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A 线程自己是可以重复获取此锁的(state 会累加),这就是可重入的概念。但要注意, 获取多少次就要释放多么次,这样才能保证 state 是能回到零态的。 以 CountDownLatch 以例,任务分为 N 个子线程去执行,state 也初始化为 N(注意 N 要与线程个数一致)。这 N 个子线程是并行执行的,每个子线程执行完后 countDown()一次,state 会 CAS 减 1。等到所有子线程都执行完后(即 state=0),会 unpark()主调用线程,然后主调用线程就会从 await()函数返回,继续后面动作。ReentrantReadWriteLock 实现独占和共享两种方式。
一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也
只需要实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared 中的一种即可。但 AQS 也支持自定义同步器 同时实现独占和共享两种方式,如 ReentrantReadWriteLock 。
Fork/Join框架是Java7提供的一个 用于并行执行任务的框架,是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。
要想掌握Fork/Join框架,首先需要理解两个点,分而治之 和 工作窃取算法 。
分而治之
Fork/Join框架体现了分治思想:将一个规模为N的问题分解为K个规模较小的子问题,这些子问题相互独立且与原问题性质相同。求出子问题的解,就可得到原问题的解。
工作窃取算法
大任务拆成了若干个小任务,把这些小任务放到不同的队列里,各自创建单独线程来执行队列里的任务。
那么问题来了,有的线程干活块,有的线程干活慢。干完活的线程不能让它空下来,得让它去帮没干完活的线程干活。它去其它线程的队列里窃取一个任务来执行,这就是所谓的 工作窃取 。
工作窃取发生的时候,它们会访问同一个队列,为了减少窃取任务线程和被窃取任务线程之间的竞争,通常任务会使用双端队列,被窃取任务线程永远从双端队列的头部拿,而窃取任务的线程永远从双端队列的尾部拿任务执行。