Java Thread 知识点总结.md
本文由weitangzhu_2008参考了大量线程相关文章,加上自己的理解提取整理而成,并且提供了目录和各部分返回目录的链接,目的是方便学习查阅线程相关知识点。为了尊重本篇所引用的文章作者,每部分都注明了引用的文章地址,且发布时选择类别为转载
。
文章目录
Java 线程
(返回目录↺)
参考列表:
https://blog.youkuaiyun.com/shimiso/article/details/10005983
https://www.ibm.com/developerworks/cn/java/j-lo-processthread/
http://www.cnblogs.com/dolphin0520/p/3920357.html
https://www.cnblogs.com/lixuan1998/p/6937986.html
https://www.cnblogs.com/GarfieldEr007/p/5746362.html
http://www.cnblogs.com/dolphin0520/p/3613043.html
线程与进程
现在的操作系统是多任务操作系统,多线程是实现多任务的一种方式。
- 进程:是指一个内存中运行的应用程序,每个进程都有自己独立的一块内存空间,一个进程中可以启动多个线程。比如在Windows系统中,一个运行的exe就是一个进程。
- 线程:是指进程中的一个执行流程,一个进程中可以运行多个线程。比如java.exe进程中可以运行很多线程。线程总是属于某个进程,进程中的多个线程共享进程的内存。
线程的创建和启动
创建线程最重要的是提供线程函数(回调函数),该函数作为新创建线程的入口函数,实现自己想要的功能。Java 提供了两种方法来创建一个线程:
- 继承 Thread 类
class MyThread extends Thread{
public void run() {
System.out.println("My thread is started.");
}
}
实现该继承类的 run 方法,然后就可以创建这个子类的对象,调用 start 方法即可创建一个新的线程:
MyThread myThread = new MyThread();
myThread.start();
- 实现 Runnable 接口
class MyRunnable implements Runnable{
public void run() {
System.out.println("My runnable is invoked.");
}
}
实现 Runnable 接口的类的对象可以作为一个参数传递到创建的 Thread 对象中,同样调用 Thread#start 方法就可以在一个新的线程中运行 run 方法中的代码了:
Thread myThread = new Thread(new MyRunnable());
myThread.start();
可以看到,不管是用哪种方法,实际上都是要实现一个 run 方法的, 该方法本质是上一个回调方法,由 start 方法新创建的线程会调用这个方法从而执行需要的代码。
一个 Java 线程的创建根本上就对应了一个本地线程(native thread)的创建,两者是一一对应的。
首先 , Java 线程的 start 方法会创建一个本地线程(通过调用 JVM_StartThread),该线程的线程函数是定义在 jvm.cpp 中的 thread_entry,由其再进一步调用 run 方法。 run 方法和普通方法其实没有本质区别,直接调用 run 方法不会报错,但是却是在当前线程执行,而不会创建一个新的线程。
实现Runnable接口比继承Thread类所具有的优势:
- 可以避免java中的单继承的限制
- 适合多个相同的程序代码的线程去处理同一个资源
- 代码可以被多个线程共享
线程状态转换
(返回目录↺)
一般来说,线程包括以下这几个状态:创建(new)、就绪(runnable)、运行(running)、阻塞(blocked)、消亡(dead)。如下图所示:
- 新建状态(New):新创建了一个线程对象。
- 就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
- 运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
- 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
- 等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)
- 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
- 其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(sleep不会释放持有的锁)
- 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
线程优先级调度
(返回目录↺)
参考列表:
https://www.cnblogs.com/ixenos/p/6216301.html
https://www.cnblogs.com/GarfieldEr007/p/5746362.html
调度策略:
-
在任意时刻,当有多个线程处于可运行状态时,运行系统总是挑选一个优先级最高的线程执行,只有当线程停止、退出或者由于某些原因不执行的时候,低优先级的线程才可能被执行
-
两个优先级相同的线程同时等待执行时,那么运行系统会以round-robin(轮询调度)的方式选择一个线程执行(Java的优先级策略是抢占式调度!)
-
被选中的线程可因为以下原因退出,而给其他线程执行的机会:
-
一个更高优先级的线程处于可运行状态(Runnable)
-
线程主动退出(yield),或它的run方法结束
-
在支持分时方式的系统上,分配给该线程的时间片结束
-
Java线程的优先级用整数表示,取值范围是1~10,Thread类有以下三个静态常量:
static int MAX_PRIORITY // 线程可以具有的最高优先级,取值为10。
static int MIN_PRIORITY // 线程可以具有的最低优先级,取值为1。
static int NORM_PRIORITY // 分配给线程的默认优先级,取值为5。
Thread类的setPriority()和getPriority()方法分别用来设置和获取线程的优先级。
每个线程都有默认的优先级,主线程的默认优先级为Thread.NORM_PRIORITY。
线程的优先级有继承关系,比如A线程中创建了B线程,那么B将和A具有相同的优先级。
线程内存
(返回目录↺)
参考列表:
http://www.cnblogs.com/dolphin0520/p/3613043.html
在整个程序执行过程中,JVM会用一段空间来存储程序执行期间需要用到的数据和相关信息,这段空间一般被称作为Runtime Data Area(运行时数据区),也就是我们常说的JVM内存。
根据《Java虚拟机规范》的规定,运行时数据区通常包括这几个部分:程序计数器(Program Counter Register)、Java栈(VM Stack)、本地方法栈(Native Method Stack)、方法区(Method Area)、堆(Heap)。如下图所示:
- 程序计数器:
在JVM中,多线程是通过线程轮流切换来获得CPU执行时间的,因此,在任一具体时刻,一个CPU的内核只会执行一条线程中的指令,因此,为了能够使得每个线程都在线程切换后能够恢复在切换之前的程序执行位置,每个线程都需要有自己独立的程序计数器,并且不能互相被干扰,否则就会影响到程序的正常执行次序。因此,可以这么说,程序计数器是每个线程所私有的
。 - Java栈:
Java栈中存放的是一个个的栈帧,每个栈帧对应一个被调用的方法,在栈帧中包括局部变量表(Local Variables)、操作数栈(Operand Stack)、指向当前方法所属的类的运行时常量池(运行时常量池的概念在方法区部分会谈到)的引用(Reference to runtime constant pool)、方法返回地址(Return Address)和一些额外的附加信息。当线程执行一个方法时,就会随之创建一个对应的栈帧,并将建立的栈帧压栈。当方法执行完毕之后,便会将栈帧出栈。由于每个线程正在执行的方法可能不同,因此每个线程都会有一个自己的Java栈
,互不干扰。如下图所示:
- 本地方法栈:
本地方法栈与Java栈的作用和原理非常相似。区别只不过是Java栈是为执行Java方法服务的,而本地方法栈则是为执行本地方法(Native Method)服务的。在JVM规范中,并没有对本地方发展的具体实现方法以及数据结构作强制规定,虚拟机可以自由实现它。在HotSopt虚拟机中直接就把本地方法栈和Java栈合二为一。 - 堆:
Java中的堆是用来存储对象本身的以及数组(当然,数组引用是存放在Java栈中的)。只不过和C语言中的不同,在Java中,程序员基本不用去关心空间释放的问题,Java的垃圾回收机制会自动进行处理。因此这部分空间也是Java垃圾收集器管理的主要区域。另外,堆是被所有线程共享的,在JVM中只有一个堆
。 - 方法区:
方法区与堆一样是被线程共享的区域
。在方法区中,存储了每个类的信息(包括类的名称、方法信息、字段信息)、静态变量、常量以及编译器编译后的代码等。在方法区中有一个非常重要的部分就是运行时常量池,它是每一个类或接口的常量池的运行时表示形式,在类和接口被加载到JVM后,对应的运行时常量池就被创建出来。当然并非Class文件常量池中的内容才能进入运行时常量池,在运行期间也可将新的常量放入运行时常量池中,比如String的intern方法。
守护线程与非守护线程
(返回目录↺)
参考列表:
https://www.cnblogs.com/lixuan1998/p/6937986.html
Java分为两种线程:用户线程和守护线程
守护线程是指在程序运行的时候在后台提供一种通用服务的线程,比如垃圾回收线程就是一个很称职的守护者,并且这种线程并不属于程序中不可或缺的部分。因 此,当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程
。反过来说,只要任何非守护线程还在运行,程序就不会终止
。
守护线程和用户线程唯一的不同之处就在于虚拟机的离开:如果用户线程已经全部退出运行了,只剩下守护线程存在了,虚拟机也就退出了。 因为没有了被守护者,守护线程也就没有工作可做了,也就没有继续运行程序的必要了。
将线程转换为守护线程可以通过调用Thread对象的setDaemon(true)
方法来实现。在使用守护线程时需要注意一下几点:
-
thread.setDaemon(true) 必须在thread.start()之前设置,否则会跑出一个IllegalThreadStateException异常。你不能把正在运行的常规线程设置为守护线程。
-
在Daemon线程中产生的新线程也是Daemon的。
-
守护线程应该永远不去访问固有资源,如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断。
以下代码验证了只要有用户线程存在,守护线程就不会终结,当所有用户线程结束时,守护线程才结束,并非某些人说创建守护线程的父线程结束时守护线程也结束:
public static void main(String[] args) {
TestDaemon.start();
System.out.println("main over");
}
public class TestDaemon {
public static void start() {
startTh1();
startDaemon();
}
public static void startTh1() {
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("thread1 step: " + i);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
public static void startDaemon() {
Thread dm = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
System.out.println("thread daemon");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
dm.setDaemon(true);
dm.start();
}
}
结果如下:
thread1 step: 0
main over
thread daemon
thread daemon
thread1 step: 1
thread daemon
thread daemon
thread1 step: 2
thread daemon
thread daemon
thread1 step: 3
thread daemon
thread daemon
thread1 step: 4
thread daemon
thread daemon
Process finished with exit code 0
一个Java程序至少启动几个线程
(返回目录↺)
参考列表:
http://www.tianshouzhi.com/api/tutorials/mutithread/239
每当使用java命令执行一个类时,实际上都会启动一个JVM,每一个JVM实际上是操作系统中启动的一个进程,java本身具备了垃圾回收机制,所以每个运行时至少启动两个线程,一个是main线程,另一个是垃圾回收线程。
上面说的是至少有几个,要想弄明白到底有几个线程会被启动,最佳的方法是自己动手实践。如下代码的作用是打印出当前JVM中运行的所有线程信息,不同版本的JDK运行的结果可能会不同:
public class ThreadNumDemo {
public static void main(String[] args) {
ThreadMXBean threadMXBean =ManagementFactory.getThreadMXBean();
ThreadInfo[] threadInfos=threadMXBean.dumpAllThreads(false,false);
for (ThreadInfo threadInfo : threadInfos) {
System.out.println(threadInfo.getThreadId()+"-"+threadInfo.getThreadName());
}
}
}
结果如下:
10-Monitor Ctrl-Break
5-Attach Listener
4-Signal Dispatcher
3-Finalizer // 调用对象的finalize方法的线程,就是垃圾回收的线程
2-Reference Handler // 清除reference的线程
1-main // 主线程
Process finished with exit code 0
Thread.sleep
(返回目录↺)
参考列表:
https://www.journaldev.com/1020/thread-sleep-java
https://www.e-learn.cn/content/wangluowenzhang/279284
http://www.importnew.com/7219.html
Thread.sleep(long millis) 方法可用于暂停当前线程的执行状态,进入阻塞状态,并持续指定的时间长度,时间单位是毫秒。参数millis不能为负数,否则会抛出IllegalArgumentException。
Thread.sleep(long millis, int nanos) 方法支持同时指定毫秒和钠秒,其中纳秒nanos范围在0到999999之间。
Thread.sleep 几个要点
- sleep只能暂停
当前的
线程; - sleep
不会释放
当前线程已经获取到的monitor锁
; - 在被唤醒和执行之前,
实际休眠的时间取决于系统计时器和线程调度程序
;对于一个空闲的系统,实际休眠时间会接近于
指定的时间长度;而对于一个繁忙的系统,实际休眠时间会稍长一些
。 - 任何其他线程都可以中断当前处于sleep中的线程,同时会抛出异常
InterruptedException
。 - 线程休眠到期后会自动返回到
可运行状态
,然后等待线程调度器给予CPU控制权后再执行,而不是直接到运行状态。
Thread.currentThread().sleep(x) vs. Thread.sleep(x)
在IDE中编辑使用Thread.currentThread().sleep(x)时,会提示使用静态的Thread.sleep(x),为什么呢?
- sleep方法是一个
静态方法
,不需要实例化就可以直接调用; - 另外更危险的是,
Thread.currentThread().sleep(x)
和someOtherThread.sleep(x)
这种写法很容易让人误以为可以在当前线程中让其他线程休眠
,但实际上还是让本线程休眠了。
优先使用TimeUnit类中的sleep()
Thread.sleep()可以接收长整型毫秒和长整型的纳秒参数,这样对程序员造成的一个问题就是很难知道到底当前线程是睡眠了多少秒、分、小时或者天。
TimeUnit是java.util.concurrent包下面的一个类,TimeUnit提供了可读性更好的线程暂停操作,通过指定DAYS、HOURS、MINUTES、SECONDS、MILLISECONDS和NANOSECONDS等枚举实例可以很方便地设定时间。例如:
TimeUnit.MINUTES.sleep(4); // sleeping for 4 minutes
Thread.yield
(返回目录↺)
参考列表:
http://www.importnew.com/14958.html
https://blog.youkuaiyun.com/dabing69221/article/details/17426953
yield 英文意思是“屈服,投降,放弃”;
一个调用Thread.yield()方法的线程,是在告诉虚拟机线程调度器,它乐意让出对cpu的占用权利,但这仅仅是暗示
,调度器可能会直接忽略掉此暗示。
如果调度器真的按Thread.yield()方法暗示的去做了,也只是使当前线程从执行状态(运行状态)
变为可执行状态(就绪状态)
,而并不会进入阻塞状态
,然后调度器会从可执行状态线程中选择一个来执行,刚刚主动暗示放弃的那个线程还是有可能被再次选中
进入运行状态的,并不是说一定会执行其他线程。
Thread.yield()的定义如下,它是一个静态的native方法:
/**
* A hint to the scheduler that the current thread is willing to yield
* its current use of a processor. The scheduler is free to ignore this
* hint.
*
* <p> Yield is a heuristic attempt to improve relative progression
* between threads that would otherwise over-utilise a CPU. Its use
* should be combined with detailed profiling and benchmarking to
* ensure that it actually has the desired effect.
*
* <p> It is rarely appropriate to use this method. It may be useful
* for debugging or testing purposes, where it may help to reproduce
* bugs due to race conditions. It may also be useful when designing
* concurrency control constructs such as the ones in the
* {@link java.util.concurrent.locks} package.
*/
public static native void yield();
定义说明了yield要适当使用,它的使用应该结合详细的剖析和基准测试,以确保它有预期的效果。对于以调试或测试为目的,重现由于竞争条件下导致的bug可能会很有用。
Thread.join
(返回目录↺)
参考列表:
https://www.cnblogs.com/lonelywolfmoutain/p/5103821.html
https://segmentfault.com/q/1010000005337306/a-1020000005337860
http://www.blogjava.net/vincent/archive/2008/08/23/223912.html
join方法作用:
假如一个Thread实例对象为t,则t.join()方法的作用是使所属线程对象t正常执行完run方法,而对当前线程无限期阻塞,直到所属线程销毁后再执行当前线程的逻辑。
join方法使用
join方法有如下三种形式:
public final void join(long millis) throws InterruptedException
public final void join(long millis, int nanos) throws InterruptedException
public final void join() throws InterruptedException
join使用示例:
public static void main(String[] args) throws Exception {
Runnable r = new ThreadTest();
Thread t = new Thread(r);
t.start();
t.join();// t执行完了后,才会走到主线程的下面一句打印
System.out.println("this is in main thread");
}
join方法实现原理
join方法是通过Object.wait
来实现的,如下:
public final synchronized void join(long millis) throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
当前线程调用t.join时候,当前线程会获得线程对象t的锁(wait 意味着已经拿到了该对象的锁),调用该对象的wait(等待时间),直到该对象唤醒当前线程。
但是何时唤醒当前线程呢?答案是子线程死的时候会调用自己的notifyAll方法
,从而唤醒当前线程。
JVM底层实际是使用OS层提供的API来支持线程的,比如UNIX-Like的OS中一般使用pthread。在Thread执行start的方法时,就会调用native方法的start0,start0底层实际经过很多层的封装,最终会调用createJavaThread的方法,createJavaThread就会调pthread_create创建一个线程并执行。过程大致是这样的:Thread.start() -> start0() -> … -> createJavaCreate() -> pthread_create() => threadStart() => attachThread() -> 执行Thread的run() -> detachThread() “这个方法最后会调用Object.notifyAll”。
Thread.interrupt
(返回目录↺)
参考列表:
https://blog.youkuaiyun.com/javazejian/article/details/72828483
interrupt 相关方法
在Java中,提供了以下3个有关线程中断的方法:
//中断线程(实例方法)
public void Thread.interrupt();
//判断线程是否被中断(实例方法)
public boolean Thread.isInterrupted();
//判断是否被中断并清除当前中断状态(静态方法)
public static boolean Thread.interrupted();
interrupt 可以中断的阻塞类型
(返回目录↺)
当一个线程处于被阻塞状态
或者试图执行一个阻塞操作
时,使用Thread.interrupt()方式中断该线程,注意此时将会抛出一个InterruptedException的异常,同时中断状态将会被复位(由中断状态改为非中断状态)。
可以中断
:因调用了Thread.sleep、Thread.join、Object.wait而处于阻塞状态的线程;不可以中断
:因等待获取锁对象的synchronized方法或者代码块而处于阻塞状态的线程;
例1:
// https://blog.youkuaiyun.com/javazejian/article/details/72828483
public class InterruputSleepThread3 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread() {
@Override
public void run() {
//while在try中,通过异常中断就可以退出run循环
try {
while (true) {
//当前线程处于阻塞状态,异常必须捕捉处理,无法往外抛出
TimeUnit.SECONDS.sleep(2);
}
} catch (InterruptedException e) {
System.out.println("Interruted When Sleep");
boolean interrupt = this.isInterrupted();
//中断状态被复位
System.out.println("interrupt:"+interrupt);
}
}
};
t1.start();
TimeUnit.SECONDS.sleep(2);
//中断处于阻塞状态的线程
t1.interrupt();
/**
* 输出结果:
Interruted When Sleep
interrupt:false
*/
}
}
如上述代码所示,我们创建一个线程,并在线程中调用了sleep方法从而使用线程进入阻塞状态,启动线程后,调用线程实例对象的interrupt方法中断阻塞异常,并抛出InterruptedException异常,此时中断状态也将被复位。
例2:
package com.example;
import java.util.concurrent.TimeUnit;
/**
* Created by WTZ on 2018/4/24.
* 此部分基于看到别人对sleep的验证后扩展想到对join验证
*/
public class SynchronizedRun implements Runnable {
Thread t;
public SynchronizedRun(Thread t) {
this.t = t;
}
public synchronized void f() {
System.out.println("sync into f");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("to interrupt external t");
t.interrupt();
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("sync leave f");
}
public void run() {
f();
}
public static void main(String[] args) throws InterruptedException {
testSynchronizedBlocked();
}
public static void testSynchronizedBlocked() throws InterruptedException {
SynchronizedRun sync = new SynchronizedRun(Thread.currentThread());
Thread t = new Thread(sync);
t.start();
try {
t.join();
} catch (InterruptedException e) {
System.out.println("main thread join interrupted");
}
System.out.println("main thread run over");
}
}
上述代码执行结果:
sync into f
to interrupt external t
main thread join interrupted
main thread run over
sync leave f
如上述代码所示,我们在主线程中启动了一个子线程,并对子线程调用了join方法,按常理会阻塞等待子线程执行完毕后才回到主线程执行,但我们在子线程执行完成之前中断了主线程的等待。
interrupt 无法直接中断非阻塞状态
(返回目录↺)
除了阻塞中断的情景,我们还可能会遇到处于运行期且非阻塞的状态
的线程,这种情况下,直接调用Thread.interrupt()中断非阻塞状态下线程是不会得到任响应的
,例如:
// https://blog.youkuaiyun.com/javazejian/article/details/72828483
public class InterruputThread {
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(){
@Override
public void run(){
while(true){
// 处于运行期且非阻塞的状态
System.out.println("未被中断");
}
}
};
t1.start();
TimeUnit.SECONDS.sleep(2);
t1.interrupt();
/**
* 输出结果(无限执行):
未被中断
未被中断
未被中断
......
*/
}
}
上述代码中虽然我们调用了interrupt方法,但线程t1并未被中断,因为处于非阻塞状态的线程需要我们手动进行中断检测并结束程序,改进后代码如下:
// https://blog.youkuaiyun.com/javazejian/article/details/72828483
public class InterruputThread {
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(){
@Override
public void run(){
while(true){
//判断当前线程是否被中断
if (this.isInterrupted()){
System.out.println("线程中断");
break;
}
}
System.out.println("已跳出循环,线程中断!");
}
};
t1.start();
TimeUnit.SECONDS.sleep(2);
t1.interrupt();
/**
* 输出结果:
线程中断
已跳出循环,线程中断!
*/
}
}
为了兼顾以上两种情况,那么就可以如下编写:
public void run(){
try {
//判断当前线程是否已中断,注意interrupted方法是静态的,执行后会对中断状态进行复位
while (!Thread.interrupted()) {
TimeUnit.SECONDS.sleep(2);
}
} catch (InterruptedException e) {
}
}
ThreadLocal
(返回目录↺)
参考列表:
https://www.jianshu.com/p/40d4c7aebd66
ThreadLocal 用处
当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本
,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。常用于用户登录控制,如记录session信息。
ThreadLocal 实现原理
每个线程Thread内部有一个ThreadLocal.ThreadLocalMap
类型的成员变量threadLocals
,这个threadLocals就是用来存储多个ThreadLocal<T>
中的T类型变量副本的,键值key为当前ThreadLocal变量,value为T类型变量副本。通过ThreadLocal中的set(T value) 和 get() 可以分别存放和获取T类型变量,如下是源码:
public class ThreadLocal<T> {
...
public ThreadLocal() {
}
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
return setInitialValue();
}
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
...
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal k, Object v) {
super(k);
value = v;
}
}
...
private Entry[] table;
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);
}
...
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();
}
...
}
}
使用示意图如下:
线程池
(返回目录↺)
参考列表:
http://www.cnblogs.com/dolphin0520/p/3932921.html
https://www.cnblogs.com/dongguacai/p/6030187.html
线程池作用
- 线程是稀缺资源,大多数时候线程都是执行一个时间很短的任务就结束了,频繁创建和销毁线程会大大降低系统的效率,使用线程池可以减少创建和销毁线程的次数,每个工作线程都可以重复使用。
- 可以根据系统的承受能力,调整线程池中工作线程的数量,防止因为消耗过多内存导致服务器崩溃。
线程池创建工具Executors
通常我们会使用Executors
类中提供的几个静态方法来创建线程池:
Executors.newCachedThreadPool(); //创建一个缓冲池,缓冲池容量大小为Integer.MAX_VALUE
Executors.newSingleThreadExecutor(); //创建容量为1的缓冲池
Executors.newFixedThreadPool(int); //创建固定容量大小的缓冲池
这几个静态方法实际是调用了ThreadPoolExecutor
来创建线程池:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
而ThreadPoolExecutor继承了AbstractExecutorService类,并提供了四个构造器:
public class ThreadPoolExecutor extends AbstractExecutorService {
.....
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
BlockingQueue<Runnable> workQueue);
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory);
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
BlockingQueue<Runnable> workQueue,RejectedExecutionHandler handler);
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler);
...
}
构造器中各个参数的含义:
-
corePoolSize:核心池的大小,在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了prestartAllCoreThreads()或者prestartCoreThread()方法,从这2个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建corePoolSize个线程或者一个线程。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中;
-
maximumPoolSize:线程池最大线程数,它表示在线程池中最多能创建多少个线程;
-
keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止。当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0;
-
unit:参数keepAliveTime的时间单位
-
workQueue:一个阻塞队列,用来存储等待执行的任务,这个参数的选择也很重要,会对线程池的运行过程产生重大影响,一般来说,这里的阻塞队列有以下几种选择:
- ArrayBlockingQueue:基于数组的先进先出队列,此队列创建时必须指定大小;
- LinkedBlockingQueue:基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为Integer.MAX_VALUE;
- SynchronousQueue:这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务。
ArrayBlockingQueue和PriorityBlockingQueue使用较少,一般使用LinkedBlockingQueue和Synchronous。
-
threadFactory:线程工厂,主要用来创建线程;
-
handler:表示当拒绝处理任务时的策略,有以下四种取值:
- ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
- ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
- ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
- ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
另外,抽象类AbstractExecutorService实现了ExecutorService接口,基本实现了ExecutorService中声明的所有方法;ExecutorService接口继承了Executor接口,并声明了一些方法:submit、invokeAll、invokeAny以及shutDown等;Executor是一个顶层接口,在它里面只声明了一个方法execute(Runnable),返回值为void,参数为Runnable类型,从字面意思可以理解,就是用来执行传进去的任务的;以上几个类之间的关系如下图:
线程池状态
(返回目录↺)
在ThreadPoolExecutor中定义了一个volatile变量用来保证线程之间的可见性,另外定义了几个static final变量表示线程池的各个状态:
volatile int runState;
static final int RUNNING = 0;
static final int SHUTDOWN = 1;
static final int STOP = 2;
static final int TERMINATED = 3;
各个状态之间的切换如下图所示:
-
当创建线程池后,初始时,线程池处于RUNNING状态;
-
如果调用了shutdown()方法,则线程池处于SHUTDOWN状态,此时线程池不能够接受新的任务,它会等待所有任务执行完毕;
-
如果调用了shutdownNow()方法,则线程池处于STOP状态,此时线程池不能接受新的任务,并且会去尝试终止正在执行的任务;
-
当线程池处于SHUTDOWN或STOP状态,并且所有工作线程已经销毁,任务缓存队列已经清空或执行结束后,线程池被设置为TERMINATED状态。
线程池中的线程初始化
(返回目录↺)
默认情况下,创建线程池之后,线程池中是没有线程的,需要提交任务之后才会创建线程。
在实际中如果需要线程池创建之后立即创建线程,可以通过以下两个方法办到:
- prestartCoreThread():初始化一个核心线程;
- prestartAllCoreThreads():初始化所有核心线程
下面是这2个方法的实现:
public boolean prestartCoreThread() {
return addIfUnderCorePoolSize(null); //注意传进去的参数是null
}
public int prestartAllCoreThreads() {
int n = 0;
while (addIfUnderCorePoolSize(null))//注意传进去的参数是null
++n;
return n;
}
注意上面传进去的参数是null,执行线程会阻塞在getTask方法中:
r = workQueue.take();
即等待任务队列中有任务。
线程池执行任务流程
(返回目录↺)
在ThreadPoolExecutor类中,最核心的任务提交方法是execute()方法,虽然通过submit也可以提交任务,但是实际上submit方法里面最终调用的还是execute()方法,所以我们只需要研究execute()方法的实现原理即可:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
// 如果线程池中当前线程数小于核心池大小,则创建工作线程来执行任务
if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) {
// 如果线程池中当前线程数大于核心池大小或者创建工作线程失败,则把任务加入缓冲队列
if (runState == RUNNING && workQueue.offer(command)) {
// 如果其他线程突然调用shutdown或者shutdownNow方法关闭了线程池,则调用如下方法保证添加到任务缓存队列中的任务得到处理
if (runState != RUNNING || poolSize == 0)
ensureQueuedTaskHandled(command);
}
// 如果当前线程池不处于RUNNING状态或者任务加入缓冲队列失败,则新建线程来执行任务
else if (!addIfUnderMaximumPoolSize(command))
reject(command); // 如果线程总数大于最大线程池限制,就采取拒绝策略
}
}
如上述代码所述,具体执行任务流程概括如下:
- 如果当前线程池中的线程数目小于corePoolSize,则每来一个任务,就会创建一个线程去执行这个任务;
- 如果当前线程池中的线程数目>=corePoolSize,则每来一个任务,会尝试将其添加到任务缓存队列当中,若添加成功,则该任务会等待空闲线程将其取出去执行;若添加失败(一般来说是任务缓存队列已满),则会尝试创建新的线程去执行这个任务;
- 如果当前线程池中的线程数目达到maximumPoolSize,则会采取任务拒绝策略进行处理;
- 如果线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止,直至线程池中的线程数目不大于corePoolSize;如果允许为核心池中的线程设置存活时间,那么核心池中的线程空闲时间超过keepAliveTime,线程也会被终止。
以上流程可以用如下图来表示:
从上边流程可以看出:当 corePoolSize < maximumPoolSize
时,且缓冲队列有界已满时,后加入的任务会直接被执行,也就是先于还在缓冲队列里等待的任务执行。而我们常用的Executors.newFixedThreadPool(int nThreads)
方法,使用的是未设置容量的无界队列LinkedBlockingQueue
,且设置了 corePoolSize = maximumPoolSize
,因此不会出现后来的任务先于队列中的任务执行的情况。
线程池容量的动态调整
(返回目录↺)
ThreadPoolExecutor提供了动态调整线程池容量大小的方法:setCorePoolSize()和setMaximumPoolSize(),
- setCorePoolSize:设置核心池大小
- setMaximumPoolSize:设置线程池最大能创建的线程数目大小
当上述参数从小变大时,ThreadPoolExecutor进行线程赋值,还可能立即创建新的线程来执行任务。
如何合理配置线程池的大小
一般需要根据任务的类型来配置线程池大小:
-
如果是CPU密集型任务,就需要尽量压榨CPU,参考值可以设为 NCPU+1
-
如果是IO密集型任务,参考值可以设置为2*NCPU
Callable、Future和FutureTask
我们知道创建线程的2种方式,一种是直接继承Thread,另外一种就是实现Runnable接口。这两种方式都有一个缺陷,就是在执行完任务之后无法直接获取执行结果。如果需要获取执行结果,就必须通过共享变量或者使用线程通信的方式来达到效果,这样使用起来就比较麻烦。
通过在线程池中使用Callable和Future,可以在任务执行完毕之后得到任务执行结果。在ExecutorService接口中声明了若干个submit方法的重载版本如下:
<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);
类似于java.lang.Runnable,Callable 也是一个接口,在它里面也只声明了一个方法,只不过这个方法叫做call():
public interface Callable<V> {
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result
* @throws Exception if unable to compute a result
*/
V call() throws Exception;
}
Future就是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过get方法获取执行结果,该方法会阻塞直到任务返回结果。Future也是一个接口:
public interface Future<V> {
boolean cancel(boolean mayInterruptIfRunning); // 用来取消任务,如果取消任务成功则返回true,否则返回false。参数mayInterruptIfRunning表示是否允许取消正在执行却没有执行完毕的任务,如果设置true,则表示可以取消正在执行过程中的任务。如果任务已经完成,此方法肯定返回false;如果任务还没有执行,此方法肯定返回true;若任务正在执行,且mayInterruptIfRunning设置为true,则返回true
boolean isCancelled(); // 表示任务是否被取消成功
boolean isDone(); // 表示任务是否已经完成
V get() throws InterruptedException, ExecutionException; // 用来获取执行结果,这个方法会产生阻塞,会一直等到任务执行完毕才返回;
V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException; // 用来获取执行结果,如果在指定时间内,还没获取到结果,就抛出超时异常
}
FutureTask类实现了RunnableFuture接口,间接实现了Future接口,我们看一下RunnableFuture接口的实现:
public interface RunnableFuture<V> extends Runnable, Future<V> {
void run();
}
public class FutureTask<V> implements RunnableFuture<V>
可以看出RunnableFuture继承了Runnable接口和Future接口,而FutureTask实现了RunnableFuture接口。所以它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值。
FutureTask提供了2个构造器:
public FutureTask(Callable<V> callable) {}
public FutureTask(Runnable runnable, V result) {}
示例:
public class Test {
public static void main(String[] args) {
ExecutorService executor = Executors.newCachedThreadPool();
Task task = new Task();
//第1种方式
Future<Integer> result = executor.submit(task);
//第2种方式
/*FutureTask<Integer> futureTask = new FutureTask<Integer>(task);
executor.submit(futureTask);*/
executor.shutdown();
//第3种方式,注意这种方式和第2种方式效果是类似的,只不过一个使用的是ExecutorService,一个使用的是Thread
/*Thread thread = new Thread(futureTask);
thread.start();*/
try {
Thread.sleep(1000);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
System.out.println("主线程在执行任务");
try {
// 对应方式1的结果获取:
System.out.println("task运行结果"+result.get());
// 对应方式2和3的结果获取:
//System.out.println("task运行结果"+futureTask.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
System.out.println("所有任务执行完毕");
}
}
class Task implements Callable<Integer>{
@Override
public Integer call() throws Exception {
System.out.println("子线程在进行计算");
Thread.sleep(3000);
int sum = 0;
for(int i=0;i<100;i++)
sum += i;
return sum;
}
}
线程并发
(返回目录↺)
参考列表:
http://www.cnblogs.com/dolphin0520/p/3920373.html
内存模型
计算机在执行程序时,每条指令都是在CPU中执行的,而执行指令过程中,势必涉及到数据的读取和写入。由于CPU执行速度比从物理内存读写速度快很多,为了提高效率,在CPU里面就有了高速缓存
。
当程序在运行过程中,会将运算需要的数据从主存(物理内存)
复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。比如下面的这段代码:
i = i + 1;
当线程执行这个语句时,会先从主存当中读取i的值,然后复制一份到高速缓存当中,然后CPU执行指令对i进行加1操作,然后将数据写入高速缓存,最后将高速缓存中i最新的值刷新到主存当中。
这个代码在单线程中运行是没有任何问题的,但是在多线程中运行就会有问题了。在多核CPU中,每条线程可能运行于不同的CPU中,因此每个线程运行时有自己的高速缓存(对单核CPU来说,其实也会出现这种问题,只不过是以线程调度的形式来分别执行的)。
比如同时有2个线程执行这段代码,假如初始时i的值为0,那么我们希望两个线程执行完之后i的值变为2。但是事实会是这样吗?
可能存在下面一种情况:初始时,两个线程分别读取i的值存入各自所在的CPU的高速缓存当中,然后线程1进行加1操作,然后把i的最新值1写入到内存。此时线程2的高速缓存当中i的值还是0,进行加1操作之后,i的值为1,然后线程2把i的值写入内存。最终结果i的值是1,而不是2。这就是著名的缓存一致性问题
,通常称这种被多个线程访问的变量为共享变量
。
为了解决缓存不一致性问题,硬件层面上通常有以下2种解决方法:
-
通过在总线加LOCK#锁的方式;
-
通过缓存一致性协议;
但是上面的方式会有一个问题,由于在锁住总线期间,其他CPU无法访问内存,导致效率低下。所以就出现了缓存一致性协议。最出名的就是Intel 的MESI协议,MESI协议保证了每个缓存中使用的共享变量的副本是一致的。它核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。
在JVM内存模型中,也会存在缓存一致性问题和指令重排序的问题。JVM内存模型规定所有的变量都是存在主存
当中(类似于前面说的物理内存),每个线程都有自己的工作内存
(类似于前面的高速缓存)。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。并且每个线程不能访问其他线程的工作内存。
原子性问题
(返回目录↺)
原子性:即一个操作或者多个操作要么全部执行
并且执行的过程不会被任何因素打断
,要么就都不执行
。
一个很经典的例子就是银行账户转账问题:
比如从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元,往账户B加上1000元。试想一下,如果这2个操作不具备原子性,会造成什么样的后果。
当已经从主存读取了B账户余额到缓存,在增加1000元并写回主存之前,突然另一个取款线程从B账户取出500元,作减法后在把减后剩余的钱写回主存前,又切换回转账线程把1000元写入主存,最后取款线程再把减后剩余的钱写回主存,这样就覆盖了转账的1000元,相当于B没收到A已经扣掉的钱。
在java中,请分析以下哪些操作是原子性操作:
x = 10; //语句1
y = x; //语句2
x++; //语句3
x = x + 1; //语句4
其实只有语句1是原子性操作,其他三个语句都不是原子性操作。
语句1是直接将数值10赋值给x,也就是说线程执行这个语句的会直接将数值10写入到工作内存中。
语句2实际上包含2个操作,它先要去读取x的值,再将x的值写入工作内存,虽然读取x的值以及 将x的值写入工作内存 这2个操作都是原子性操作,但是合起来就不是原子性操作了。
同样的,x++和 x = x+1包括3个操作:读取x的值,进行加1操作,写入新的值。
也就是说,只有简单的读取、赋值
(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作
。
不过这里有一点需要注意:在32位平台下,对64位数据的读取和赋值是需要通过两个操作来完成的,不能保证其原子性
。但是好像在最新的JDK中,JVM已经保证对64位数据的读取和赋值也是原子性操作了。
java原子性问题可以通过synchronized和Lock来解决。因为synchronized
和Lock
能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。
可见性问题
(返回目录↺)
可见性:是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
举个简单的例子,看下面这段代码:
//线程1执行的代码
int i = 0;
i = 10;
//线程2执行的代码
j = i;
假若执行线程1的是CPU1,执行线程2的是CPU2。由上面的分析可知,当线程1执行 i =10这句时,会先把i的初始值加载到CPU1的高速缓存中,然后赋值为10,那么在CPU1的高速缓存当中i的值变为10了,却没有立即写入到主存当中。
此时线程2执行 j = i,它会先去主存读取i的值并加载到CPU2的缓存当中,注意此时内存当中i的值还是0,那么就会使得j的值为0,而不是10.
这就是可见性问题,线程1对变量i修改了之后,线程2没有立即看到线程1修改的值。
对于可见性问题,Java提供了volatile
关键字来保证可见性。当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存
,当有其他线程需要读取时,它会去内存中读取新值。
另外,通过synchronized和Lock也能够保证可见性
,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中
,因此可以保证可见性。
有序性问题
(返回目录↺)
有序性:即程序执行的顺序按照代码的先后顺序执行。举个简单的例子,看下面这段代码:
int i = 0;
boolean flag = false;
i = 1; //语句1
flag = true; //语句2
上面代码定义了一个int型变量,定义了一个boolean类型变量,然后分别对两个变量进行赋值操作。从代码顺序上看,语句1是在语句2前面的,那么JVM在真正执行这段代码的时候会保证语句1一定会在语句2前面执行吗?不一定,为什么呢?这里可能会发生指令重排序
(Instruction Reorder)。
一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致
,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的
。
因为处理器在进行重排序时是会考虑指令之间的数据依赖性
,如果一个指令Instruction 2必须用到Instruction 1的结果,那么处理器会保证Instruction 1会在Instruction 2之前执行。
虽然重排序不会影响单个线程
内程序执行的结果,但是多线程呢?下面看一个例子:
//线程1:
context = loadContext(); //语句1
inited = true; //语句2
//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
上面代码中,由于语句1和语句2没有数据依赖性,因此可能会被重排序。假如发生了重排序,在线程1执行过程中先执行语句2,而此是线程2会以为初始化工作已经完成,那么就会跳出while循环,去执行doSomethingwithconfig(context)方法,而此时context并没有被初始化,就会导致程序出错。
从上面可以看出,指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性
。
在Java里面,可以通过volatile
关键字来保证一定的“有序性”,它能确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面。
volatile
(返回目录↺)
参考列表:
http://www.cnblogs.com/dolphin0520/p/3920373.html
volatile 能保证可见性和有序性
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
-
保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
-
禁止进行指令重排序:
- 当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
- 在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。举个简单的例子如下:
//x、y为非volatile变量
//flag为volatile变量
x = 2; //语句1
y = 0; //语句2
flag = true; //语句3
x = 4; //语句4
y = -1; //语句5
由于flag变量为volatile变量,那么在进行指令重排序的过程的时候,不会将语句3放到语句1、语句2前面,也不会讲语句3放到语句4、语句5后面。但是要注意语句1和语句2的顺序、语句4和语句5的顺序是不作任何保证的。
并且volatile关键字能保证,执行到语句3时,语句1和语句2必定是执行完毕了的,且语句1和语句2的执行结果对语句3、语句4、语句5是可见的。
volatile 不能保证原子性
(返回目录↺)
下边是一个不能保证原子性的验证例子:
public class Test {
public volatile int inc = 0;
public void increase() {
inc++;
}
public static void main(String[] args) {
final Test test = new Test();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<1000;j++)
test.increase();
};
}.start();
}
while(Thread.activeCount()>1) //保证前面的线程都执行完
Thread.yield();
System.out.println(test.inc);
}
}
对于这段程序的输出结果也许有些朋友认为是10000,但是事实上运行它会发现每次运行结果都不一致,都是一个小于10000的数字。
原因是上面对变量inc进行自增操作,而自增操作是不具备原子性的,它包括读取变量的原始值、进行加1操作、写入工作内存
。那么就是说自增操作的三个子操作可能会分割开执行,就有可能导致下面这种情况出现:
- 假如某个时刻变量inc的值为10,线程1对变量进行自增操作,线程1先读取了变量inc的原始值,然后线程1被阻塞了。
- 然后线程2也去读取变量inc的原始值,由于线程1只是对变量inc进行读取操作,而没有对变量进行修改操作,所以不会导致线程2的工作内存中缓存变量inc的缓存行无效,所以线程2会直接去主存读取inc的值,发现inc的值时10,然后进行加1操作,并把11写入工作内存,最后写入主存。
- 然后线程1接着要进行加1操作,由于已经读取了inc的值,此时在线程1的工作内存中inc的值仍然为10,所以线程1对inc进行加1操作后inc的值为11,然后将11写入工作内存,最后写入主存。
- 最后两个线程分别进行了一次自增操作后,inc只增加了1。
问题根源就在于自增操作不是原子性操作,而且volatile也无法保证对变量的任何操作都是原子性的。可以用以下几种方式来解决原子性问题:
- 采用synchronized:
public int inc = 0;
public synchronized void increase() {
inc++;
}
- 采用Lock:
public int inc = 0;
Lock lock = new ReentrantLock();
public void increase() {
lock.lock();
try {
inc++;
} finally{
lock.unlock();
}
}
- 采用AtomicInteger:
public AtomicInteger inc = new AtomicInteger();
public void increase() {
inc.getAndIncrement();
}
在java 1.5的java.util.concurrent.atomic包下提供了一些原子操作类,即对基本数据类型的 自增(加1操作),自减(减1操作)、以及加法操作(加一个数),减法操作(减一个数)进行了封装,保证这些操作是原子性操作。atomic是利用CAS来实现原子性操作的(Compare And Swap),CAS实际上是利用处理器提供的CMPXCHG指令实现的,而处理器执行CMPXCHG指令是一个原子性操作。
volatile 实现机制
(返回目录↺)
下面这段话摘自《深入理解Java虚拟机》:
“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”
lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
- 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
- 它会强制将对缓存的修改操作立即写入主存;
- 如果是写操作,它会导致其他CPU中对应的缓存行无效。
volatile 使用场景
(返回目录↺)
synchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized,但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。
volatile 使用场景是:保证操作本身是原子性操作前提下,同时又需要解决并发可见性和有序性问题的场景。
例1:状态标记量
volatile boolean inited = false;
//线程1:
context = loadContext();
inited = true;
//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
例2:双重检查模式
参考:http://www.iteye.com/topic/652440
class Singleton{
private volatile static Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
if(instance==null) {
synchronized (Singleton.class) {//1
if(instance==null)//2
instance = new Singleton();//3
}
}
return instance;
}
}
双重检测同步延迟加载是为处理原版非延迟加载方式瓶颈问题,我们需要对 instance 进行第二次检查,目的是避开过多的同步(因为这里的同步只需在第一次创建实例时才同步,一旦创建成功,以后获取实例时就不需要同获取锁了),但在Java中行不通,因为Java 平台内存模型允许所谓的“无序写入”,同步块外面的if (instance == null)
可能看到已存在但不完整的
实例。JDK5.0以后版本若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 对象的构造函数并将引用返回给它,来完成对该对象的初始化。
为展示此事件的发生情况,假设代码行 instance =new Singleton(); 执行了下列伪代码:
mem = allocate(); //为单例对象分配内存空间.
instance = mem; //注意,instance 引用现在是非空,但还未初始化
ctorSingleton(instance); //为单例对象通过instance调用构造函数
这段伪代码不仅是可能的,而且是一些 JIT 编译器上真实发生的。执行的顺序是颠倒的,但鉴于当前的内存模型,这也是允许发生的。
锁
(返回目录↺)
参考列表:
https://www.cnblogs.com/Mainz/p/3546347.html
锁的代价
-
锁是用来做并发最简单的方式,当然其代价也是最高的。内核态的锁的时候需要操作系统进行一次上下文切换,加锁、释放锁会导致比较多的上下文切换和调度延时,等待锁的线程会被挂起直至锁释放。
-
Java在JDK1.5之前都是靠synchronized关键字保证同步的,这种通过使用一致的锁定协议来协调对共享状态的访问,可以确保无论哪个线程持有守护变量的锁,都采用独占的方式来访问这些变量,如果出现多个线程同时访问锁,那第一些线线程将被挂起,当线程恢复执行时,必须等待其它线程执行完他们的时间片以后才能被调度执行,在挂起和恢复执行过程中存在着很大的开销。
乐观锁与悲观锁
-
独占锁是一种悲观锁,synchronized就是一种独占锁,它假设最坏的情况,并且只有在确保其它线程不会造成干扰的情况下执行,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。
-
乐观锁,就是每次不加锁而假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。比如CAS(比较并交换)是CPU指令级的操作,只有一步原子操作,所以非常快;而且CAS避免了请求操作系统来裁定锁的问题,不用麻烦操作系统,直接在CPU内部就搞定了。
CAS无锁算法
要实现无锁(lock-free)的非阻塞算法有多种实现方法,其中CAS(比较与交换,Compare and swap)是一种有名的无锁算法。
- CAS的语义是“我认为V的值应该为A,如果是,那么将V的值更新为B,否则不修改并告诉V的值实际为多少”。
- CAS是项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。
- CAS有3个操作数,内存地址V,旧的预期值A,要修改的新值B。当且仅当旧的预期值A和内存地址V中当前的值相同时,说明共享数据没有被修改,于是就将内存地址V中的值修改为B;如果不相等,说明共享数据已经被修改,放弃已经所做的操作,然后重新执行刚才的操作。
JVM对CAS的支持
-
在JDK1.5之前,如果不编写明确的代码就无法执行CAS操作,在JDK1.5中引入了底层的支持,在int、long和对象的引用等类型上都公开了CAS的操作,并且JVM把它们编译为底层硬件提供的最有效的方法,在运行CAS的平台上,运行时把它们编译为相应的机器指令,如果处理器/CPU不支持CAS指令,那么JVM将使用自旋锁。因此,值得注意的是,CAS解决方案与平台/编译器紧密相关。
-
在原子类变量中,如java.util.concurrent.atomic中的AtomicXXX,都使用了这些底层的JVM支持为数字类型的引用类型提供一种高效的CAS操作,而在java.util.concurrent中的大多数类在实现时都直接或间接的使用了这些原子变量类。
AQS(AbstractQueuedSynchronizer)
(返回目录↺)
参考列表:
http://www.idouba.net/sync-implementation-by-aqs
https://blog.youkuaiyun.com/qq_17250009/article/details/79012528
http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/6-b14/java/util/concurrent/locks/AbstractQueuedSynchronizer.java
JDK在java/util/concurrent提供了很多常用的并发类及并发容器类。并发类基本是通过CAS/AQS实现,并发容器基本是通过synchronized和CAS/AQS实现的。
AQS(AbstractQueuedSynchronizer)是 java.util.concurrent的基础。J.U.C中宣传的封装良好的好用的同步工具类Semaphore、CountDownLatch、ReentrantLock、ReentrantReadWriteLock、FutureTask等虽然各自都有不同特征,但是简单看一下源码,每个类内部都包含一个如下的内部类定义:
static class Sync extends AbstractQueuedSynchronizer
同时每个类内部都包含有这样一个属性,连属性名都一样!注释已经暗示了,几种同步类提供的功能其实都是委托这个AQS的子类sync来完成,有些是部分功能,有些则是全部功能。
/** All mechanics via AbstractQueuedSynchronizer subclass */
private final Sync sync;
AQS中有以下三个重要属性:
private transient volatile Node head;
private transient volatile Node tail;
private volatile int state;
可以看出AQS中维护着一个volatile修饰的属性“state”和一个具有Node类型head和tail的FIFO的wait queue。
在使用上可以把AQS作为基类,重写如下几个方法,同时可以通过 getState()
, setState(int)
and/orcompareAndSetState(int,int)
来修改synchronization state :
tryAcquire(int)
tryRelease(int)
tryAcquireShared(int)
tryReleaseShared(int)
isHeldExclusively()
以上方法中带有shared后缀的一组表示共享锁的获取和释放,而另外一组没有后缀的表示排他锁(独占锁)的获取和释放。
在AQS的设计中,在父类AQS中实现了对等待队列的默认实现,无论是对共享锁还是对排他锁。子类中几乎不用修改该部分功能,而state在子类中根据需要被赋予了不同的意义,子类通过对state的不同操作来提供不同的同步器功能,进而对封装的工具类提供不同的功能。
通过使用sun.misc.Unsafe类中的CAS方法,对“state”属性进行一系列操作来实现独占锁和共享锁,独占锁和共享锁又分为公平锁和非公平锁。
- 独占锁:同一时刻只有一个线程持有同一锁,其余线程在链表中排队。
- 共享锁:同一时刻可以多个线程持有同一锁。
- 公平锁:锁被线程持有后,其余线程排队执行。锁按照FIFO放入链表。
- 非公平锁:锁被线程持有后,其余线程排队执行。锁按照FIFO放入链表。但是在刚释放锁的之后,如果有新线程竞争锁,那么新线程将和链表中下个即将被唤醒的线程竞争锁。
几个使用AQS的工具类对比如下:
工具类 | 工具类作用 | state的作用 | 锁类型 |
---|---|---|---|
Semaphore | 控制同时访问某个特定资源的操作数量 | 表示初始化的许可数 | 共享锁 |
CountDownLatch | 把一组线程全部关在外面,在某个状态时候放开。一种同步机制来保证一个或多个线程等待其他线程完成。 | 维护一个计数器 | 共享锁 |
ReentrantLock | 标准的互斥操作,也就是一次只能有一个线程持有锁 | 表示获得锁的线程对锁的重入次数 | 排他锁 |
ReentrantReadWriteLock | 读写锁。允许多个读线程同时持有锁,但是只有一个写线程可以持有锁。写线程获取写入锁后可以再次获取读取锁,但是读线程获取读取锁后却不能获取写入锁 | 高16位表示共享锁的数量,低16位表示独占锁的重入次数 | 读锁:共享; 写锁:排他 |
FutureTask | 封装一个执行任务交给其他线程去执行,开始执行后可以被取消,可以查看执行结果,如果执行结果未完成则阻塞。 | 用来存储执行状态RUNNING、RUN、CANCELLED | 共享锁 |
同步锁与并发锁
synchronized同步锁
synchronized 作用和语法
(返回目录↺)
参考列表:
http://tutorials.jenkov.com/java-concurrency/synchronized.html
https://blog.youkuaiyun.com/javazejian/article/details/72828483
https://www.cnblogs.com/gxjz/p/5729624.html
http://www.cnblogs.com/dolphin0520/p/3920373.html
在 Java 中,关键字 synchronized
可以保证在同一个时刻
,只有一个线程
可以执行被其修饰的方法或代码块(主要是对共享数据的操作),从而保证了操作原子性
,另外由于在释放锁之前会将对变量的修改刷新到主存当中,所以还可以保证一个线程的变化(主要是共享数据的变化)能被其他线程所看到,即保证了可见性
,完全可以替代Volatile功能。
The synchronized keyword can be used to mark four different types of blocks:
- Instance methods (锁存在于所属的
实例对象
object中) - Static methods (锁存在于所属的
类对象
X.class中) - Code blocks inside instance methods (锁存在于
synchronized (object)
所指定的object中,常使用this
,也可以是其他对象) - Code blocks inside static methods (锁存在于
synchronized (X.class)
所指定的X.class中,常使用当前类的Class对象
)
注:什么是类对象?
其实从某种意义上说,在java中有两种对象:实例对象和Class对象。
实例对象就是我们平常定义的一个类的实例,而Class对象是没办法用new关键字得到的,因为它是jvm生成用来保存对应类的信息的,换句话说,当我们定义好一个类文件并编译成.class字节码后,编译器同时为我们创建了一个Class对象并将它保存.class文件中。
Class对象有以下三种方式:
Class c = a.getClass(); // a为类A的一个实例对象 Class c = Class.forName("A"); Class c = A.class;
synchronized 使用示例如下:
public class MyClass {
// sychronized的对象最好选择引用不会变化的对象(例如被标记为final或初始化后永远不会变)
private final Object mutex = new Object();
public synchronized void log1(String msg1, String msg2){
log.writeln(msg1);
log.writeln(msg2);
}
public void log2(String msg1, String msg2){
synchronized(this){
log.writeln(msg1);
log.writeln(msg2);
}
}
public void log3(String msg1, String msg2){
synchronized(mutex){
log.writeln(msg1);
log.writeln(msg2);
}
}
public static synchronized void log4(String msg1, String msg2){
log.writeln(msg1);
log.writeln(msg2);
}
public static void log5(String msg1, String msg2){
synchronized(MyClass.class){
log.writeln(msg1);
log.writeln(msg2);
}
}
}
当一个线程访问object中的一个synchronized(this)修饰的方法或代码块时,其他线程对object中所有
synchronized(this)修饰的方法或代码块(包括相同和不相同
的方法及代码块)的访问都将被阻塞
,但仍然可以访问该object中未被synchronized(this)修饰的方法或代码块。
synchronized 实现原理
(返回目录↺)
参考列表:
https://blog.youkuaiyun.com/javazejian/article/details/72828483
http://www.cnblogs.com/javaminer/p/3889023.html
http://www.importnew.com/23511.html
https://blog.youkuaiyun.com/u012465296/article/details/53022317
任何线程
要执行被synchronized
修饰的方法或代码块
时,都必须先获取该方法或代码块所属对象
对应的Monitor锁
。每个java对象头
里存储着指向synchronized使用的monitor锁的起始地址。
synchronized 字节码表示
(返回目录↺)
在java语言中存在两种内建的synchronized语法:1、synchronized语句;2、synchronized方法。
对于synchronized语句当Java源代码被javac编译成bytecode的时候,会在同步块的入口位置和退出位置分别插入monitorenter和monitorexit
字节码指令。
而synchronized方法则会被翻译成普通的方法调用和返回指令(如invokevirtual、areturn指令),在VM字节码层面并没有任何特别的指令来实现被synchronized修饰的方法,而是在Class文件的方法表中将该方法的access_flags字段中的synchronized标志位置1
,表示该方法是同步方法并使用调用该方法的对象或该方法所属的Class在JVM的内部对象表示Klass做为锁对象。
Java 对象头
(返回目录↺)
在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。其中java对象头里存储着指向synchronized使用的monitor锁的起始地址。
-
实例变量:存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。
-
填充数据:由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐,这点了解即可。
-
对象头:jvm中采用2个字来存储对象头(如果对象是数组则会分配3个字,多出来的1个字记录的是数组长度),其主要结构是由Mark Word 和 Class Metadata Address 组成,其结构说明如下表:
虚拟机位数 | 头对象结构 | 说明 |
---|---|---|
32/64bit | Mark Word | 存储对象的hashCode、锁信息或分代年龄或GC标志等信息 |
32/64bit | Class Metadata Address | 类型指针指向对象的类元数据,JVM通过这个指针确定该对象是哪个类的实例 |
Mark Word 被设计成为一个非固定的数据结构,以便存储更多有效的数据,它会根据对象本身的状态复用自己的存储空间,如32位JVM下,除了上述列出的Mark Word默认存储结构外,还有如下可能变化的结构:
Monitor 锁
(返回目录↺)
每个java对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系存在多种实现方式,例如monitor可以与对象一起创建销毁,或者当线程试图获取对象锁时自动生成。
当一个 monitor 被某个线程持有后,它便处于锁定状态,对象头的MarkWord中的LockWord指向monitor的起始地址。在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的):
ObjectMonitor() {
_header = NULL;
_count = 0; // 记录个数
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL; // 持有锁的线程标识
_WaitSet = NULL; // 处于wait状态的线程,会被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; // 处于等待锁block状态的线程,会被加入到该列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
ObjectMonitor中有两个队列,_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中的原因。
synchronized 可重入性
(返回目录↺)
从互斥锁的设计上来说,当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源
时,这种情况属于重入锁
,请求将会成功。
在java中,synchronized是基于原子性的内部锁机制,是可重入的,因此在一个线程调用synchronized方法的同时在其方法体内部调用该对象另一个synchronized方法,也就是说一个线程得到一个对象锁后再次请求该对象锁,是允许的,这就是synchronized的可重入性
。
注意,由于synchronized是基于monitor实现的,因此每次重入,monitor中的计数器仍会加1。
synchronized 优化
(返回目录↺)
在Java早期版本中,synchronized属于重量级锁
,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的Mutex Lock来实现的,而操作系统实现线程之间的切换
时需要从用户态转换到核心态
,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的synchronized效率低
的原因。
庆幸的是在Java 6之后引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态
,他们会随着竞争的激烈而逐渐升级
。注意锁可以升级不可降级
,这种策略是为了提高获得锁和释放锁的效率。
-
偏向锁
- 引入背景:
在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得
,因此为了减少同一线程获取锁(会涉及到一些CAS操作、耗时)的代价而引入偏向锁。 - 核心思想:当一个对象锁处于无锁状态时,如果一个线程获得了该锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求该锁时,无需再做任何同步操作。
- 优缺点:偏向锁有很好的优化效果,毕竟极有可能连续多次是同一个线程申请相同的锁,避免在锁获取过程中执行不必要的CAS原子指令,因为CAS原子指令虽然相对于重量级锁来说开销比较小但还是存在非常可观的本地延迟。但是对于锁竞争比较激烈的场合,偏向锁就失效了。
- 引入背景:
-
轻量级锁
- 引入背景:倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段,此时Mark Word 的结构也变为轻量级锁的结构。轻量级锁所适应的场景是:
虽然存在多线程会获取锁,但大多数情况下这些线程是交替执行同步块的
。 - 核心思想:在无锁竞争的情况下完全可以避免调用操作系统层面的重量级互斥锁,取而代之的是在monitorenter和monitorexit中只需要依靠一条CAS原子指令就可以完成锁的获取及释放。
- 优缺点:在调用操作系统的重量级的互斥锁(mutex/semaphore)之前,竞争线程不会直接进入阻塞状态,而是会先
自旋
一定的次数,当达到一定的次数时如果仍然没有成功获得锁,才开始准备进入阻塞状态,最后会导致轻量级锁膨胀为重量级锁。自旋,就是让该线程做几个空循环等待一段时间,不会被立即挂起,看持有锁的线程是否会很快释放锁。
虽然自旋可以避免线程切换带来的开销,但是它占用了处理器的时间。如果持有锁的线程很快就释放了锁,那么自旋的效率就非常好,反之,自旋的线程就会白白消耗掉处理的资源,它不会做任何有意义的工作。
- 引入背景:倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段,此时Mark Word 的结构也变为轻量级锁的结构。轻量级锁所适应的场景是:
不同状态锁的比较如下:
锁 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
偏向锁 | 加锁和解锁,不需要额外的消耗。和执行非同步方法相比,仅存在纳秒级的差距。 | 如果线程间存在锁竞争,会带来额外的锁撤销的消耗。 | 适用于只有一个线程访问同步块场景。 |
轻量级锁 | 竞争的线程不会阻塞,提高了程序的响应速度。 | 如果始终得不到锁,竞争的线程,使用自旋会消耗cpu。 | 追求响应时间,同步块执行速度非常快。 |
重量级锁 | 线程竞争不使用自旋,不会消耗cpu。 | 线程阻塞,响应时间缓慢。 | 追求吞吐量。同步块执行速度较长。 |
-
锁消除
-
Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间。
-
例如,StringBuffer的append是一个同步方法,但是在下述代码中,add方法中的StringBuffer属于一个局部变量,并且不会被其他线程所使用,因此StringBuffer不可能存在共享资源竞争的情景,JVM会自动将其锁消除。
-
/**
* Created by zejian on 2017/6/4.
* Blog : http://blog.youkuaiyun.com/javazejian [原文地址,请尊重原创]
* 消除StringBuffer同步锁
*/
public class StringBufferRemoveSync {
public void add(String str1, String str2) {
//StringBuffer是线程安全,由于sb只会在append方法中使用,不可能被其他线程引用
//因此sb属于不可能共享的资源,JVM会自动消除内部的锁
StringBuffer sb = new StringBuffer();
sb.append(str1).append(str2);
}
public static void main(String[] args) {
StringBufferRemoveSync rmsync = new StringBufferRemoveSync();
for (int i = 0; i < 10000000; i++) {
rmsync.add("abc", "123");
}
}
}
synchronized 与 Thread.interrupt
(返回目录↺)
参考列表:
https://blog.youkuaiyun.com/javazejian/article/details/72828483
线程的中断操作对于正在等待获取的锁对象的synchronized方法或者代码块并不起作用,也就是对于synchronized来说,如果一个线程在等待锁,那么结果只有两种,要么它获得这把锁继续执行,要么它就保持等待,即使调用中断线程的方法,也不会生效。演示代码如下:
/**
* Created by zejian on 2017/6/2.
* Blog : http://blog.youkuaiyun.com/javazejian [原文地址,请尊重原创]
*/
public class SynchronizedBlocked implements Runnable{
public synchronized void f() {
System.out.println("Trying to call f()");
while(true) // Never releases lock
Thread.yield();
}
/**
* 在构造器中创建新线程并启动获取对象锁
*/
public SynchronizedBlocked() {
//该线程已持有当前实例锁
new Thread() {
public void run() {
f(); // Lock acquired by this thread
}
}.start();
}
public void run() {
//中断判断
while (true) {
if (Thread.interrupted()) {
System.out.println("中断线程!!");
break;
} else {
f();
}
}
}
public static void main(String[] args) throws InterruptedException {
SynchronizedBlocked sync = new SynchronizedBlocked();
Thread t = new Thread(sync);
//启动后调用f()方法,无法获取当前实例锁处于等待状态
t.start();
TimeUnit.SECONDS.sleep(1);
//中断线程,无法生效
t.interrupt();
}
}
Object.wait/notify同步锁线程交互
(返回目录↺)
参考列表:
https://www.cnblogs.com/x_wukong/p/4009709.html
https://blog.youkuaiyun.com/javazejian/article/details/72828483
wait/notify功能与使用
Object.wait(), Object.nofity(), Object.nofityAll()必须要与synchronized(Obj)一起使用,也就是wait与notify是针对已经获取了Obj锁进行操作,调用这几个方法前必须拿到当前对象的监视器monitor对象,否则就会抛出IllegalMonitorStateException异常。
- 从语法角度来说,Obj.wait(),Obj.notify必须在synchronized(Obj){…}语句块内。
- 从功能上来说,线程在获取对象锁后,通过调用wait可以主动释放对象锁,同时本线程休眠,直到有其它线程调用对象的notify()或nofityAll()唤醒该线程,才能继续获取对象锁,并继续执行。
- Thread.sleep()与Object.wait()二者都可以暂停当前线程,释放CPU控制权,主要的区别在于Object.wait()在释放CPU同时,释放了对象锁的控制,而Thread.sleep()方法并不会释放锁。
- 有一点需要注意的是notify()调用后,并不是马上就释放对象锁的,而是在相应的synchronized(){}语句块执行结束,自动释放锁后,JVM会在wait()对象锁的线程中随机选取一线程,赋予其对象锁,唤醒线程,继续执行。
wait/notify例一
(返回目录↺)
对Object.wait(),Object.notify()的应用最经典的例子,应该是三线程打印ABC的问题,题目要求如下:
题1:建立三个线程,A线程打印10次A,B线程打印10次B,C线程打印10次C,要求线程同时运行,交替打印10次ABC。
该问题为三线程间的同步唤醒操作,主要的目的就是ThreadA->ThreadB->ThreadC->ThreadA循环执行三个线程。为了控制线程执行的顺序,那么就必须要确定唤醒、等待的顺序,所以每一个线程必须同时持有两个对象锁,才能继续执行。一个对象锁是prev,就是前一个线程所持有的对象锁,还有一个就是自身对象锁。代码如下:
public class MyWaitNotifyThread implements Runnable {
private String name;
private Object prev;
private Object self;
public MyWaitNotifyThread(String name, Object prev, Object self) {
this.name = name;
this.prev = prev;
this.self = self;
}
@Override
public void run() {
int count = 10;
while (count > 0) {
synchronized (prev) {
synchronized (self) {
System.out.print(name);
count--;
self.notify();// 唤醒等待获取self锁的线程,即唤醒下一个线程
}
if (count == 0) {
return;
}
try {
prev.wait();// 释放prev锁,同时本线程释放CPU进入阻塞状态,等待通过prev锁唤醒自己,也就是等上一个线程唤醒自己;由于这里没有调用notify,也就是说没有去唤醒等待prev锁的线程,现在大家都等着prev锁了
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
private static void testWaitNotify() {
Object a = new Object();
Object b = new Object();
Object c = new Object();
MyWaitNotifyThread pa = new MyWaitNotifyThread("A", c, a);
MyWaitNotifyThread pb = new MyWaitNotifyThread("B", a, b);
MyWaitNotifyThread pc = new MyWaitNotifyThread("C", b, c);
new Thread(pa).start();
//Thread.sleep(10);
new Thread(pb).start();
//Thread.sleep(10);
new Thread(pc).start();
//Thread.sleep(10);
}
上述(没有增加Thread.sleep(10)时)看起来似乎没什么问题,但如果你仔细想一下,就会发现有问题,就是初始条件,三个线程按照ABC的顺序来启动,按照前面的思考,A唤醒B,B唤醒C,C再唤醒A。但是这种假设依赖于JVM中线程调度、执行的顺序。考虑一种情况如下:
- 如果主线程在启动A后,执行A,A还没有释放任何锁时又切回主线程,启动了ThreadB、ThreadC,由于A线程尚未释放self.notify,也就是B需要在synchronized(prev)处等待,而这时C却调用synchronized(prev)获取了对b的对象锁;
- 接着在A调用完分别释放了self锁(a锁)、prev锁(c锁)后,ThreadB获取了prev也就是a的对象锁,但还差self锁(b锁),Thread C获取到self锁也就是c锁时执行条件就已经满足了,会打印C;
- 之后Thread C执行完成再释放c及b的对象锁,这时ThreadB也具备了运行条件,会打印B,于是循环变成了ACBACB了;
- 为了严格按照ABC顺序执行,可以在每启动一个线程后通过Thread.sleep(N)让主线程也睡眠,以保证刚启动的子线程能完全走完一轮,因此这个N取决于走完一轮所需要的时间。
wait/notify例二
(返回目录↺)
题2:用synchronized、notify、wait实现一个火车票卖票程序,要求生成者不断的往队列中插入唯一的票通知消费者取票,消费者取走这唯一的票再通知生产者放票,如此循环。
来源:http://blog.youkuaiyun.com/new_abc/article/details/28642189
代码如下,Ticket.java:
public class Ticket {
ArrayDeque ticket = new ArrayDeque();
long ticketNum = 1;
/**
* 买票
*/
public synchronized void buy() {
try {
System.out.println("当前票数量:" + ticket.size());
if (ticket.size() > 0) {
// 取票
System.out.println(Thread.currentThread().getName() +
" 取走第:" + ticket.pop() + "张票.");
notify();
} else {
// 无票
System.out.println(Thread.currentThread().getName() +
"等待售票员放票.");
wait();//等待放票
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 售票
*/
public synchronized void sell() {
try {
System.out.println("当前票数量:" + ticket.size());
if (ticket.size() > 0) {
System.out.println(Thread.currentThread().getName() +
"等待乘客取票.");
wait();// 等待取票
} else {
ticket.push(ticketNum);// 放票
System.out.println(Thread.currentThread().getName() +
"放入第:" + ticketNum + "张票.");
ticketNum++;
notify(); // 通知取票
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
SellTicketThread.java
public class SellTicketThread extends Thread {
private Ticket ticket;
public SellTicketThread(String name, Ticket ticket) {
super(name);
this.ticket = ticket;
}
public void run() {
while (true)//循环放票
{
ticket.sell();
}
}
}
BuyTicketThread.java
public class BuyTicketThread extends Thread {
private Ticket ticket;
public BuyTicketThread(String name, Ticket ticket) {
super(name);
this.ticket = ticket;
}
public void run() {
while (true) {//循环取票
ticket.buy();
}
}
}
测试主程序TestMain.java
public class TestMain {
public static void main(String[] args) throws InterruptedException {
Ticket ticket = new Ticket();
new SellTicketThread("售票员 1", ticket).start();
//new SellTicketThread("售票员 2", ticket).start();
new BuyTicketThread("张三", ticket).start();
//new BuyTicketThread("李四", ticket).start();
//new BuyTicketThread("王五", ticket).start();
}
}
如果有多个售票员或者买票员,需要将Ticket.java中的notify改为notifyAll,否则可能导致程序运行达不到预期的效果,例如有两个售票员和一个买票员,假如其中的一个售票员和买票员都在wait,这个时候另外一个售票员放完票后调用notify唤醒的是在等待的售票员,这个时候由于有票,所以它也会wait,而买票员没有notify将其唤醒了。
Lock并发锁
(返回目录↺)
参考列表:
http://www.cnblogs.com/dolphin0520/p/3923167.html
http://www.idouba.net/sync-implementation-by-aqs/
Lock定义与实现
Lock是java.util.concurrent.locks包中一个接口:
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
ReentrantLock是唯一实现了Lock接口的类,并且ReentrantLock提供了更多的方法。
Lock使用方法
(返回目录↺)
首先lock()方法是平常使用得最多的一个方法,就是用来获取锁,如果锁已被其他线程获取,则进行等待。
采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此一般来说,使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。
使用方法如下:
// 在类中声明和创建一个全局变量Lock
private Lock lock = new ReentrantLock();
// 在要上锁的方法中添加如下代码
lock.lock();
try{
//处理任务
}catch(Exception ex){
}finally{
lock.unlock(); //释放锁
}
tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。
tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。
所以,一般情况下通过tryLock来获取锁时是这样使用的:
// 在类中声明和创建一个全局变量Lock
private Lock lock = new ReentrantLock();
if(lock.tryLock()) {
try{
//处理任务
}catch(Exception ex){
}finally{
lock.unlock(); //释放锁
}
}else {
//如果不能获取锁,则直接做其他事情
}
lockInterruptibly()方法比较特殊,当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就使说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。
由于lockInterruptibly()的声明中抛出了异常,所以lock.lockInterruptibly()必须放在try块中或者在调用lockInterruptibly()的方法外声明抛出InterruptedException。因此lockInterruptibly()一般的使用形式如下:
public void method() throws InterruptedException {
lock.lockInterruptibly();
try {
//.....
}
finally {
lock.unlock();
}
}
ReadWriteLock定义与实现
(返回目录↺)
ReadWriteLock也是一个接口,在它里面只定义了两个方法:
public interface ReadWriteLock {
/**
* Returns the lock used for reading.
*
* @return the lock used for reading.
*/
Lock readLock();
/**
* Returns the lock used for writing.
*
* @return the lock used for writing.
*/
Lock writeLock();
}
一个用来获取读锁,一个用来获取写锁。也就是说将文件的读写操作分开,分成2个锁来分配给线程,从而使得多个线程可以同时进行读操作。
读写锁允许多个读线程同时持有锁,但是只有一个写线程可以持有锁。写线程获取写入锁后可以再次获取读取锁,但是读线程获取读取锁后却不能获取写入锁。
ReentrantReadWriteLock实现了ReadWriteLock接口。
ReadWriteLock使用方法
(返回目录↺)
ReadWriteLock与Lock使用方法基本一致,如下:
// 在类中声明和创建一个全局变量rwl
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
// 在要上锁的方法中添加如下代码
rwl.readLock().lock();
try{
//处理任务
}catch(Exception ex){
}finally{
rwl.readLock().unlock(); //释放锁
}
如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁。
如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。
Lock与synchronized区别
(返回目录↺)
- Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
- synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
- 通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
- Lock可以让等待锁的线程响应中断,lockInterruptibly()的用法时已经体现了Lock的可中断性,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断。
- 读写锁ReadWriteLock可以使得多个线程之间的读操作不会发生冲突,提高多个线程进行读操作的效率。
- synchronized是非公平锁,它无法保证等待的线程获取锁的顺序;
而对于ReentrantLock和ReentrantReadWriteLock,默认情况下是非公平锁,但是可以设置为公平锁,比如new ReentrantLock(true)通过传参数为true来设置为公平锁。
注1:公平锁与非公平锁
公平锁即尽量以请求锁的顺序来获取锁。比如同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该所,这种就是公平锁。
非公平锁即无法保证锁的获取是按照请求锁的顺序进行的。这样就可能导致某个或者一些线程永远获取不到锁。
注2:可重入性
如果锁具备可重入性,则称作为可重入锁。像synchronized和ReentrantLock都是可重入锁,可重入性实际上表明了锁的分配机制:基于线程的分配,而不是基于方法调用的分配。举个简单的例子,当一个线程执行到某个synchronized方法时,比如说method1,而在method1中会调用另外一个synchronized方法method2,此时线程不必重新去申请锁,而是可以直接执行方法method2。
Condition并发锁线程交互
(返回目录↺)
参考列表:
http://www.cnblogs.com/dolphin0520/p/3920385.html
Condition功能
Condition是在java 1.5中才出现的,它用来替代传统的Object的wait()、notify()实现线程间的协作:
- Condition是个接口,基本的方法就是await()和signal()方法;
- await()对应Object的wait();
- signal()对应Object的notify();
- signalAll()对应Object的notifyAll();
- Condition依赖于Lock接口,生成一个Condition的基本代码是lock.newCondition() ;
- 调用Condition的await()和signal()方法,都必须在lock保护之内,就是说必须在lock.lock()和lock.unlock之间才可以使用。
Condition使用示例
public class Test {
private int queueSize = 10;
private PriorityQueue<Integer> queue = new PriorityQueue<Integer>(queueSize);
private Lock lock = new ReentrantLock();
private Condition notFull = lock.newCondition();
private Condition notEmpty = lock.newCondition();
public static void main(String[] args) {
Test test = new Test();
Producer producer = test.new Producer();
Consumer consumer = test.new Consumer();
producer.start();
consumer.start();
}
class Consumer extends Thread{
@Override
public void run() {
consume();
}
private void consume() {
while(true){
lock.lock();
try {
while(queue.size() == 0){
try {
System.out.println("队列空,等待数据");
notEmpty.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
queue.poll(); //每次移走队首元素
notFull.signal();
System.out.println("从队列取走一个元素,队列剩余"+queue.size()+"个元素");
} finally{
lock.unlock();
}
}
}
}
class Producer extends Thread{
@Override
public void run() {
produce();
}
private void produce() {
while(true){
lock.lock();
try {
while(queue.size() == queueSize){
try {
System.out.println("队列满,等待有空余空间");
notFull.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
queue.offer(1); //每次插入一个元素
notEmpty.signal();
System.out.println("向队列取中插入一个元素,队列剩余空间:"+(queueSize-queue.size()));
} finally{
lock.unlock();
}
}
}
}
}
同步容器与并发容器
(返回目录↺)
参考列表:
http://www.cnblogs.com/dolphin0520/p/3933404.html
http://www.cnblogs.com/dolphin0520/p/3933551.html
http://www.cnblogs.com/dolphin0520/p/3932905.html
http://www.cnblogs.com/dolphin0520/p/3938914.html
在Java的集合容器框架中,主要有四大类别:List、Set、Queue、Map。
List、Set、Queue接口分别继承了Collection接口,Map本身是一个接口。
ArrayList、LinkedList实现了List接口,HashSet实现了Set接口,PriorityQueue实现了Queue接口,HashMap实现了Map接口。另外LinkedList(实际上是双向链表)还实现了Deque接口(继承自Queue接口,双向队列,允许在队首、队尾进行入队和出队操作)。
上述这些容器都是非线程安全的,如果有多个线程并发地访问这些容器时,就会出现问题。
ConcurrentModificationException
参考:http://www.cnblogs.com/dolphin0520/p/3933551.html
在对Vector等容器并发地进行迭代修改时,会报ConcurrentModificationException异常,但是在并发容器中不会出现这个问题。
其实,在单线程下迭代操作时也会报ConcurrentModificationException,例如:
public class Test {
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<Integer>();
list.add(2);
Iterator<Integer> iterator = list.iterator();
while(iterator.hasNext()){
Integer integer = iterator.next();
if(integer==2)
list.remove(integer);
}
}
}
结果如下:
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
at java.util.ArrayList$Itr.next(ArrayList.java:851)
可以发现,异常出现在checkForComodification()方法中,源码如下:
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
原因是modCount != expectedModCount。那为何会不相等呢?
在ArrayList的源码中并没有iterator()这个方法,在其父类AbstractList中找到了iterator()方法的具体实现,代码如下:
public Iterator<E> iterator() {
return new Itr();
}
接着看Itr的具体实现,它是AbstractList的一个成员内部类。我们发现modCount是AbstractList类中的一个成员变量,表示对List的修改次数,每次调用add()方法或者remove()方法就会对modCount进行加1操作;expectedModCount是Itr中的成员变量,表示对ArrayList修改次数的期望值,它的初始值为modCount。具体类图关系如下图所示:
从测试代码可以看到list大小为1,通过ArrayList.remove方法删除元素最终是调用的fastRemove()方法,在fastRemove()方法中,首先对modCount进行加1操作(因为对集合修改了一次),然后接下来就是删除元素的操作,最后将size进行减1操作变为0。
删除后继续while循环,调用hasNext方法()判断:
public boolean hasNext() {
return cursor != size();
}
由于此时cursor为1,而size为0,那么返回true,所以继续执行while循环,然后继续调用iterator的next()方法:
public E next() {
checkForComodification();
try {
E next = get(cursor);
lastRet = cursor++;
return next;
} catch (IndexOutOfBoundsException e) {
checkForComodification();
throw new NoSuchElementException();
}
}
注意,此时要注意next()方法中的第一句:checkForComodification(),此时modCount为1,而expectedModCount为0,因此程序就抛出了ConcurrentModificationException异常。像使用for-each进行迭代实际上也会出现这种问题。
那么如何解决呢?
在Itr类中也给出了一个remove()方法:
public void remove() {
if (lastRet == -1)
throw new IllegalStateException();
checkForComodification();
try {
AbstractList.this.remove(lastRet);
if (lastRet < cursor)
cursor--;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException e) {
throw new ConcurrentModificationException();
}
}
在这个方法中,删除元素实际上调用的就是list.remove()方法,但是它多了一个操作:
expectedModCount = modCount;
因此,在迭代器中如果要删除元素的话,需要调用Itr类的remove方法,就不会因expectedModCount与modCount不等而报错了。
但此方法在单线程环境下适用,在多线程下并不适用,例如:
public class Test {
static ArrayList<Integer> list = new ArrayList<Integer>();
public static void main(String[] args) {
list.add(1);
list.add(2);
list.add(3);
list.add(4);
list.add(5);
Thread thread1 = new Thread(){
public void run() {
Iterator<Integer> iterator = list.iterator();
while(iterator.hasNext()){
Integer integer = iterator.next();
System.out.println(integer);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
};
Thread thread2 = new Thread(){
public void run() {
Iterator<Integer> iterator = list.iterator();
while(iterator.hasNext()){
Integer integer = iterator.next();
if(integer==2)
iterator.remove();
}
};
};
thread1.start();
thread2.start();
}
}
上述例子还是会报错:ConcurrentModificationException
有人说ArrayList是非线程安全的容器,换成同步容器Vector就没问题了,实际上换成Vector还是会出现这种错误。
原因在于,虽然Vector的方法采用了synchronized进行了同步,但是实际上通过Iterator访问的情况下,每个线程里面返回的是不同的iterator
,也即是说expectedModCount是每个线程私有。线程1在进行遍历,线程2在进行修改,那么很有可能导致线程2修改后导致Vector中的modCount自增了,线程2的expectedModCount也自增了,但是线程1的expectedModCount没有自增,此时线程1遍历时就会出现expectedModCount不等于modCount的情况了。
上述问题在多线程下一般有2种解决办法:
1)在使用iterator迭代的时候使用synchronized或者Lock进行同步上述run方法中的整个代码块;
2)使用并发容器CopyOnWriteArrayList代替ArrayList和Vector。
同步容器
(返回目录↺)
参考:http://www.cnblogs.com/dolphin0520/p/3933404.html
为了解决并发问题,Java提供了同步容器,主要包括2类:
-
Vector、Stack、HashTable
Vector实现了List接口,实际上就是一个数组,和ArrayList类似,但是Vector中的方法都是synchronized方法,即进行了同步措施。Stack也用synchronized进行了同步,它继承于Vector类。
HashTable实现了Map接口,进行了同步处理。
-
Collections类中提供的静态工厂方法创建的类
Collections类是一个工具提供类,注意,它和Collection不同,Collection是一个顶层的接口。在Collections类中提供了大量的方法,比如对集合或者容器进行排序、查找等操作。最重要的是,在它里面提供了几个静态工厂方法来创建同步容器类。
同步容器的缺陷
-
性能问题
同步容器中的方法采用了synchronized进行了同步,这必然会影响到执行性能;同步容器将所有对容器状态的访问都串行化了,代价就是严重降低了并发性,当多个线程竞争容器时,吞吐量严重降低。 -
不完全线程安全
同步容器能保证多线程对容器的单次读写操作是安全的,但不能做到每个线程连续多次的迭代操作是安全的。比如下边两个线程启动后,会报异常:java.lang.ArrayIndexOutOfBoundsExceptionThread thread1 = new Thread(){ public void run() { for(int i=0;i<vector.size();i++) vector.remove(i); }; }; Thread thread2 = new Thread(){ public void run() { for(int i=0;i<vector.size();i++) vector.get(i); }; };
因为可能出现,thread2刚通过vector.size()获取到大小为10,然后切到线程thread1执行vector.remove(i)删除了下标为9的元素,再切换到thread2去vector.get(9)就报错了。因此为了保证线程安全,必须在for循环外加同步措施,保证这一连续操作执行完毕。
并发容器
(返回目录↺)
参考:http://www.cnblogs.com/dolphin0520/p/3932905.html
Java5.0开始util.concurrent中提供了并发性能较好的并发容器,主要解决了两个问题:
1)根据具体场景进行设计,尽量避免synchronized,提供并发性。
2)定义了一些并发安全的复合操作,并且保证并发环境下的迭代操作不会出错。
并发容器在迭代时,可以不封装在synchronized中,可以保证不抛异常,但是未必每次看到的都是"最新的、当前的"数据。
并发容器列举如下:
- CopyOnWriteArrayList和CopyOnWriteArraySet分别代替List和Set,主要是在遍历操作为主的情况下来代替同步的List和同步的Set,这也就是上面所述的思路:
迭代过程要保证不出错,除了加锁,另外一种方法就是"克隆"容器对象
。 - ConcurrentHashMap代替同步的Map,HashMap是根据散列值分段存储的,同步Map在同步的时候锁住了所有的段,而ConcurrentHashMap加锁的时候根据散列值锁住了散列值锁对应的那段,因此提高了并发性能。ConcurrentHashMap也增加了对常用复合操作的支持,比如:putIfAbsent()、replace(),这2个操作都是原子操作。
- ConcurrentSkipListMap可以在并发中替代SortedMap(例如用Collections.synchronizedSortedMap包装的TreeMap)。
- ConcurrentSkipListSet可以在并发中替代SortedSet(例如用Collections.synchronizedSortedSet包装的TreeSet)。
- ConcurrentLinkedQueue是一个线程安全的先进先出的
非阻塞
队列,可以用来替代使用阻塞算法(入队和出队用同一把锁或两个锁)实现的线程安全队列。
ConcurrentHashMap
(返回目录↺)
参考:http://www.cnblogs.com/dolphin0520/p/3932905.html
ConcurrentHashMap可以做到读取数据不加锁,并且其内部的结构可以让其在进行写操作的时候能够将锁的粒度保持地尽量地小,不用对整个ConcurrentHashMap加锁。内部结构如下所示:
从上图可以看到,ConcurrentHashMap 在内部采用了一个叫做Segment的结构,一个Segment其实就是一个类Hash Table的结构,Segment内部维护了一个链表数组。
Segment的数据结构如下:
static final class Segment<K,V> extends ReentrantLock implements Serializable {
transient volatile int count;// Segment中元素的数量
transient int modCount;// 对table的大小造成影响的操作的数量(比如put或者remove操作)
transient int threshold;// 阈值,Segment里面元素的数量超过这个值就会对Segment进行扩容
transient volatile HashEntry<K,V>[] table;// 链表数组,数组中的每一个元素代表了一个链表的头部
final float loadFactor;// 负载因子,用于确定threshold
}
ConcurrentHashMap 定位一个元素的过程需要进行两次Hash操作
,第一次Hash定位到Segment,第二次Hash定位到元素所在的链表的头部,因此,这一种结构的带来的副作用是Hash的过程要比普通的HashMap要长
,但是带来的好处是写操作的时候可以只对元素所在的Segment进行加锁即可,不会影响到其他的Segment
,这样在最理想的情况下,ConcurrentHashMap可以最高同时支持Segment数量大小的写操作。
CopyOnWrite容器
(返回目录↺)
参考:http://ifeve.com/java-copy-on-write/
CopyOnWrite容器即写时复制
的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
例如CopyOnWriteArrayList中add方法的实现:
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
public E get(int index) {
return get(getArray(), index);
}
可以发现在添加的时候是需要加锁的,否则多线程写的时候会Copy出N个副本出来。 读的时候不需要加锁,如果读的时候有多个线程正在向CopyOnWriteArrayList添加数据,读还是会读到旧的数据,因为写的时候不会锁住旧的CopyOnWriteArrayList。
CopyOnWrite并发容器用于读多写少
的并发场景。比如白名单,黑名单,商品类目的访问和更新场景,假如我们有一个搜索网站,用户在这个网站的搜索框中,输入关键字搜索内容,但是某些关键字不允许被搜索。这些不能被搜索的关键字会被放在一个黑名单当中,黑名单每天晚上更新一次。
/**
* 黑名单服务
*
* @author fangtengfei
*
*/
public class BlackListServiceImpl {
private static CopyOnWriteMap<String, Boolean> blackListMap = new CopyOnWriteMap<String, Boolean>(1000);
public static boolean isBlackList(String id) {
return blackListMap.get(id) == null ? false : true;
}
public static void addBlackList(String id) {
blackListMap.put(id, Boolean.TRUE);
}
/**
* 批量添加黑名单
*
* @param ids
*/
public static void addBlackList(Map<String,Boolean> ids) {
blackListMap.putAll(ids);
}
}
代码很简单,但是使用CopyOnWriteMap需要注意两件事情:
- 减少扩容开销。
根据实际需要,初始化CopyOnWriteMap的大小,避免写时CopyOnWriteMap扩容的开销。 - 使用批量添加。
因为每次添加,容器每次都会进行复制,所以减少添加次数,可以减少容器的复制次数。如使用上面代码里的addBlackList方法。
CopyOnWrite容器存在两个问题 :
- 内存占用问题。
因为CopyOnWrite的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象(注意:在复制的时候只是复制容器里的引用,只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存)。如果这些对象占用的内存比较大,比如说200M左右,那么再写入100M数据进去,内存就会占用300M,那么这个时候很有可能造成频繁的Yong GC和Full GC。 - 数据一致性问题。
CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。
阻塞队列
(返回目录↺)
参考:http://www.cnblogs.com/dolphin0520/p/3932906.html
阻塞队列好处
使用非阻塞队列的时候有一个很大问题就是:它不会对当前线程产生阻塞,那么在面对类似消费者-生产者的模型时,就必须额外地实现同步策略以及线程间唤醒策略,这个实现起来就非常麻烦。
但是有了阻塞队列就不一样了,它会对当前线程产生阻塞,比如一个线程从一个空的阻塞队列中取元素,此时线程会被阻塞直到阻塞队列中有了元素。当队列中有元素后,被阻塞的线程会自动被唤醒(不需要我们编写代码去唤醒)。这样提供了极大的方便性。
阻塞队列的实现原理与我们用Object.wait()、Object.notify()和非阻塞队列实现生产者-消费者的思路类似,只不过它把这些工作一起集成到了阻塞队列中实现。
常用的阻塞队列
(返回目录↺)
在java.util.concurrent包下提供了若干个阻塞队列,主要有以下几个:
- ArrayBlockingQueue:基于数组实现的一个有界阻塞队列,在创建ArrayBlockingQueue对象时必须制定容量大小。并且可以指定公平性与非公平性,默认情况下为非公平的,即不保证等待时间最长的队列最优先能够访问队列。
- LinkedBlockingQueue:基于链表实现的一个阻塞队列,在创建LinkedBlockingQueue对象时如果不指定容量大小,则默认大小为Integer.MAX_VALUE。
- PriorityBlockingQueue:以上2种队列都是先进先出队列,而PriorityBlockingQueue却不是,它会按照元素的优先级对元素进行排序,按照优先级顺序出队,每次出队的元素都是优先级最高的元素。注意,此阻塞队列为无界阻塞队列,即容量没有上限(通过源码就可以知道,它没有容器满的信号标志),前面2种都是有界队列。
- DelayQueue:基于PriorityQueue,一种延时阻塞队列,DelayQueue中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。DelayQueue也是一个无界队列,因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞。
阻塞vs非阻塞队列中的方法
(返回目录↺)
非阻塞队列中的几个主要方法:
- add(E e):将元素e插入到队列末尾,若插入成功,则返回true;若插入失败(即队列已满),则会抛出异常;
- remove():移除队首元素,若移除成功,则返回true;如果移除失败(队列为空),则会抛出异常;
- offer(E e):将元素e插入到队列末尾,若插入成功,则返回true;若插入失败(即队列已满),则返回false;
- poll():移除并获取队首元素,若成功,则返回队首元素;否则返回null;
- peek():获取队首元素,若成功,则返回队首元素;否则返回null
对于非阻塞队列,一般情况下建议使用offer、poll和peek三个方法,不建议使用add和remove方法。因为使用offer、poll和peek三个方法可以通过返回值判断操作成功与否。
阻塞队列中的几个主要方法:
阻塞队列包括了非阻塞队列中的大部分方法,上面列举的5个方法在阻塞队列中都存在,但是要注意这5个方法在阻塞队列中都进行了同步措施。除此之外,阻塞队列提供了另外4个非常有用的方法:
- put(E e):用来向队尾存入元素,如果队列满,则等待;
- take():用来从队首取元素,如果队列为空,则等待;
- offer(E e,long timeout, TimeUnit unit):用来向队尾存入元素,如果队列满,则等待一定的时间,当时间期限达到时,如果还没有插入成功,则返回false;否则返回true;
- poll(long timeout, TimeUnit unit):用来从队首取元素,如果队列空,则等待一定的时间,当时间期限达到时,如果取到,则返回null;否则返回取得的元素;
阻塞vs非阻塞队列:实现生产者-消费者模式
(返回目录↺)
下面先使用Object.wait()和Object.notify()、非阻塞队列实现生产者-消费者模式:
public class Test {
private int queueSize = 10;
private PriorityQueue<Integer> queue = new PriorityQueue<Integer>(queueSize);
public static void main(String[] args) {
Test test = new Test();
Producer producer = test.new Producer();
Consumer consumer = test.new Consumer();
producer.start();
consumer.start();
}
class Consumer extends Thread{
@Override
public void run() {
consume();
}
private void consume() {
while(true){
synchronized (queue) {
while(queue.size() == 0){
try {
System.out.println("队列空,等待数据");
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
queue.notify();
}
}
queue.poll(); //每次移走队首元素
queue.notify();
System.out.println("从队列取走一个元素,队列剩余"+queue.size()+"个元素");
}
}
}
}
class Producer extends Thread{
@Override
public void run() {
produce();
}
private void produce() {
while(true){
synchronized (queue) {
while(queue.size() == queueSize){
try {
System.out.println("队列满,等待有空余空间");
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
queue.notify();
}
}
queue.offer(1); //每次插入一个元素
queue.notify();
System.out.println("向队列取中插入一个元素,队列剩余空间:"+(queueSize-queue.size()));
}
}
}
}
}
下面是使用阻塞队列实现的生产者-消费者模式:
public class Test {
private int queueSize = 10;
private ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue<Integer>(queueSize);
public static void main(String[] args) {
Test test = new Test();
Producer producer = test.new Producer();
Consumer consumer = test.new Consumer();
producer.start();
consumer.start();
}
class Consumer extends Thread{
@Override
public void run() {
consume();
}
private void consume() {
while(true){
try {
queue.take();
System.out.println("从队列取走一个元素,队列剩余"+queue.size()+"个元素");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Producer extends Thread{
@Override
public void run() {
produce();
}
private void produce() {
while(true){
try {
queue.put(1);
System.out.println("向队列取中插入一个元素,队列剩余空间:"+(queueSize-queue.size()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
J.U.C Tools
(返回目录↺)
参考列表:http://www.cnblogs.com/dolphin0520/p/3920397.html
在java 1.5中java.util.concurrent包下,提供了一些非常有用的辅助类来帮助我们进行并发编程,比如CountDownLatch,CyclicBarrier和Semaphore。
CountDownLatch
CountDownLatch可以实现类似计数器的功能。比如有一个任务A,它要等待其他4个任务执行完毕之后才能执行,此时就可以利用CountDownLatch来实现这种功能了。
CountDownLatch类只提供了一个构造器:
public CountDownLatch(int count) { }; //参数count为计数值
下面这3个方法是CountDownLatch类中最重要的方法:
public void await() throws InterruptedException { }; //调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行
public boolean await(long timeout, TimeUnit unit) throws InterruptedException { }; //和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行
public void countDown() { }; //将count值减1
示例:
private static void testCountDownLatch() {
final CountDownLatch latch = new CountDownLatch(2);
new Thread(){
public void run() {
runCountDown(latch);
};
}.start();
new Thread(){
public void run() {
runCountDown(latch);
};
}.start();
try {
System.out.println("开始等待2个子线程执行完毕...");
latch.await();
System.out.println("2个子线程已经执行完毕");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private static void runCountDown(CountDownLatch latch) {
try {
System.out.println("子线程"+Thread.currentThread().getName()+"正在执行");
Thread.sleep(3000);
System.out.println("子线程"+Thread.currentThread().getName()+"执行完毕");
latch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
结果如下:
线程Thread-0正在执行
线程Thread-1正在执行
开始等待2个子线程执行完毕...
线程Thread-0执行完毕
线程Thread-1执行完毕
2个子线程已经执行完毕
CyclicBarrier
(返回目录↺)
CyclicBarrier字面意思回环栅栏,通过它可以实现让一组线程等待至某个状态之后再全部同时执行。叫做回环是因为当所有等待线程都被释放以后,CyclicBarrier可以被重用。我们暂且把这个状态就叫做barrier,当调用await()方法之后,线程就处于barrier了。
CyclicBarrier提供2个构造器:
public CyclicBarrier(int parties, Runnable barrierAction) {}
public CyclicBarrier(int parties) {}
参数parties指让多少个线程或者任务等待至barrier状态;参数barrierAction为当这些线程都达到barrier状态时会执行的内容。
CyclicBarrier中最重要的方法就是await方法,它有2个重载版本:
public int await() throws InterruptedException, BrokenBarrierException { };
public int await(long timeout, TimeUnit unit)throws InterruptedException,BrokenBarrierException,TimeoutException { };
第一个版本比较常用,用来挂起当前线程,直至所有线程都到达barrier状态再同时执行后续任务;
第二个版本是让这些线程等待至一定的时间,如果还有线程没有到达barrier状态就直接让到达barrier的线程执行后续任务。
示例如下:
public class Test {
public static void main(String[] args) {
int N = 4;
CyclicBarrier barrier = new CyclicBarrier(N,new Runnable() {
@Override
public void run() {
System.out.println("全部处理完毕后进入回调中,当前线程"+Thread.currentThread().getName());
}
});
for(int i=0;i<N;i++)
new Writer(barrier).start();
}
static class Writer extends Thread{
private CyclicBarrier cyclicBarrier;
public Writer(CyclicBarrier cyclicBarrier) {
this.cyclicBarrier = cyclicBarrier;
}
@Override
public void run() {
System.out.println("线程"+Thread.currentThread().getName()+"正在写入数据...");
try {
Thread.sleep(5000); //以睡眠来模拟写入数据操作
System.out.println("线程"+Thread.currentThread().getName()+"写入数据完毕,等待其他线程写入完毕");
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
}catch(BrokenBarrierException e){
e.printStackTrace();
}
System.out.println("所有线程写入完毕,继续处理其他任务...");
}
}
}
结果如下:
线程Thread-0正在写入数据...
线程Thread-1正在写入数据...
线程Thread-2正在写入数据...
线程Thread-3正在写入数据...
线程Thread-0写入数据完毕,等待其他线程写入完毕
线程Thread-1写入数据完毕,等待其他线程写入完毕
线程Thread-2写入数据完毕,等待其他线程写入完毕
线程Thread-3写入数据完毕,等待其他线程写入完毕
全部处理完毕后进入回调中,当前线程Thread-3
所有线程写入完毕,继续处理其他任务...
所有线程写入完毕,继续处理其他任务...
所有线程写入完毕,继续处理其他任务...
所有线程写入完毕,继续处理其他任务...
从结果可以看出,当四个线程都到达barrier状态后,会从四个线程中选择一个线程去执行Runnable。
Semaphore
(返回目录↺)
Semaphore翻译成字面意思为 信号量,Semaphore可以控同时访问的线程个数,通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。
Semaphore 提供了2个构造器:
public Semaphore(int permits) { //参数permits表示许可数目,即同时可以允许多少线程进行访问
sync = new NonfairSync(permits);
}
public Semaphore(int permits, boolean fair) { //这个多了一个参数fair表示是否是公平的,即等待时间越久的越先获取许可
sync = (fair)? new FairSync(permits) : new NonfairSync(permits);
}
Semaphore类中比较重要的几个方法,首先是acquire()、release()方法:
public void acquire() throws InterruptedException { }//阻塞等待获取一个许可
public void acquire(int permits) throws InterruptedException { } //阻塞等待获取permits个许可
public void release() { } //释放一个许可
public void release(int permits) { } //释放permits个许可
// 一个线程调用release()方法前不是必须得调用过acquire()方法,但是正确的用法应该由应用程序去建立
上边这4个方法都会被阻塞,如果想立即得到执行结果,可以使用下面几个方法:
public boolean tryAcquire() { }; //尝试获取一个许可,若获取成功,则立即返回true,若获取失败,则立即返回false
public boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException { }; //尝试获取一个许可,若在指定的时间内获取成功,则立即返回true,否则则立即返回false
public boolean tryAcquire(int permits) { }; //尝试获取permits个许可,若获取成功,则立即返回true,若获取失败,则立即返回false
public boolean tryAcquire(int permits, long timeout, TimeUnit unit) throws InterruptedException { }; //尝试获取permits个许可,若在指定的时间内获取成功,则立即返回true,否则则立即返回false
示例:假若一个工厂有5台机器,但是有8个工人,一台机器同时只能被一个工人使用,只有使用完了,其他工人才能继续使用。
private static void testSemaphore() {
int N = 8; //工人数
Semaphore semaphore = new Semaphore(5); //机器数目
for (int i = 0; i < N; i++)
new Worker(i, semaphore).start();
}
static class Worker extends Thread {
private int num;
private Semaphore semaphore;
public Worker(int num, Semaphore semaphore) {
this.num = num;
this.semaphore = semaphore;
}
@Override
public void run() {
try {
semaphore.acquire();
System.out.println("worker " + this.num + " get a machine...");
Thread.sleep(2000);
System.out.println("worker " + this.num + " release a machine");
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
结果如下:
worker 0 get a machine...
worker 3 get a machine...
worker 4 get a machine...
worker 7 get a machine...
worker 1 get a machine...
worker 0 release a machine
worker 2 get a machine...
worker 4 release a machine
worker 7 release a machine
worker 5 get a machine...
worker 1 release a machine
worker 6 get a machine...
worker 3 release a machine
worker 2 release a machine
worker 5 release a machine
worker 6 release a machine
J.U.C Atomic
(返回目录↺)
参考列表:
http://ifeve.com/java-atomic/
https://www.cnblogs.com/my376908915/p/6758415.html
Java从JDK1.5开始提供了java.util.concurrent.atomic包,方便程序员在多线程环境下,无锁的进行原子操作。原子变量的底层使用了处理器提供的原子指令,但是不同的CPU架构可能提供的原子指令不一样,也有可能需要某种形式的内部锁,所以该方法不能绝对保证线程不被阻塞。
在Atomic包里一共有12个类,四种原子更新方式,分别是原子更新基本类型,原子更新数组,原子更新引用和原子更新字段。Atomic包里的类基本都是使用Unsafe实现的包装类。
基本类型类原子化
用于通过原子的方式更新基本类型,Atomic包提供了以下三个类:
- AtomicBoolean:原子更新布尔类型。
- AtomicInteger:原子更新整型。
- AtomicLong:原子更新长整型。
AtomicInteger的常用方法如下:
int addAndGet(int delta) // 以原子方式将输入的数值与实例中的值(AtomicInteger里的value)相加,并返回结果
int getAndIncrement() // 以原子方式将当前值加1,注意:这里返回的是自增前的值。
boolean compareAndSet(int expect, int update) // 如果输入的数值等于预期值,则以原子方式将该值设置为输入的值。
void lazySet(int newValue) // 最终会设置成newValue,使用lazySet设置值后,可能导致其他线程在之后的一小段时间内还是可以读到旧的值。关于该方法的更多信息可以参考并发网翻译的一篇文章《AtomicLong.lazySet是如何工作的?》
int getAndSet(int newValue) // 以原子方式设置为newValue的值,并返回旧值。
以AtomicInteger为例,看看它是怎么实现的:
如果是读取值,很简单,将value声明为volatile的,就可以保证在没有锁的情况下,数据是线程可见的:
private volatile int value;
public final int get() {
return value;
}
涉及到值变更的操作呢?以++i为例:
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
可以看到采用了CAS操作,每次从内存中读取数据然后将此数据和+1后的结果进行CAS操作,如果成功就返回结果,否则重试直到成功为止。
Atomic包提供了三种基本类型的原子更新,但是Java的基本类型里还有char,float和double等。那么问题来了,如何原子的更新其他的基本类型呢?Atomic包里的类基本都是使用Unsafe实现的,看下Unsafe的源码,发现Unsafe只提供了三种CAS方法,compareAndSwapObject,compareAndSwapInt和compareAndSwapLong,再看AtomicBoolean源码,发现其是先把Boolean转换成整型,再使用compareAndSwapInt进行CAS,所以原子更新double也可以用类似的思路来实现。
public class AtomicBoolean implements java.io.Serializable {
private volatile int value;
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicBoolean.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
public final boolean compareAndSet(boolean expect, boolean update) {
int e = expect ? 1 : 0;
int u = update ? 1 : 0;
return unsafe.compareAndSwapInt(this, valueOffset, e, u);
}
}
引用类型原子化
(返回目录↺)
Atomic包提供了以下三个引用类型原子化类:
- AtomicReference:原子更新引用类型。
- AtomicMarkableReference:原子更新带有标记位的引用类型。可以原子的更新一个布尔类型的标记位和引用类型。构造方法是AtomicMarkableReference(V initialRef, boolean initialMark)。该类将布尔值与引用关联起来,可以解决使用CAS进行原子更新时,可能出现的
ABA问题
。 - AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于原子的更数据和数据的版本号,可以解决使用CAS进行原子更新时,可能出现的
ABA问题
。
AtomicReference的使用示例如下:
User user = new User("conan", 15);
atomicUserRef.set(user);
User updateUser = new User("Shinichi", 17);
atomicUserRef.compareAndSet(user, updateUser);
System.out.println(atomicUserRef.get().getName());
System.out.println(atomicUserRef.get().getOld());
结果如下:
Shinichi
17
ABA问题
参考列表:
http://www.jb51.net/article/129690.htm
http://blog.hesey.net/2011/09/resolve-aba-by-atomicstampedreference.html
https://blog.youkuaiyun.com/zhaozhirongfree1111/article/details/72781758
在运用CAS做Lock-Free操作中有一个经典的ABA问题:
线程1准备用CAS将变量的值由A替换为B,在此之前,线程2将变量的值由A替换为C,又由C替换为A,然后线程1执行CAS时发现变量的值仍然为A,所以CAS成功。但实际上这时的现场已经和最初不同了,尽管CAS成功,但可能存在潜藏的问题。
假设有两个线程T1和T2,这两个线程对同一个栈进行出栈和入栈的操作。我们使用AtomicReference定义的tail来保存栈顶位置:
AtomicReference<T> tail;
如上图1所示,假设T1线程准备出栈,对于出栈操作我们只需要将栈顶位置由sp通过CAS操作更新为newSP即可。但是可以出现下述情况:
- 在T1线程执行tail.compareAndSet(sp,newSP)之前系统进行了线程调度,T2线程开始执行。
- T2执行了三个操作,A出栈,B出栈,然后又将A入栈。
- 此时系统又开始调度,T1线程继续执行出栈操作,但是在T1线程看来,栈顶元素仍然为A,即T1仍然认为B还是栈顶A的下一个元素,而实际上的情况如图2所示。
- T1会认为栈没有发生变化,所以tail.compareAndSet(sp,newSP)执行成功,栈顶指针被指向了B节点。而实际上B已经不存在于堆栈中,T1将A出栈后的结果如图3所示,这显然不是正确的结果。
使用AtomicMarkableReference,AtomicStampedReference都可以解决ABA问题。他们在实现compareAndSet指令的时候除了要比较当对象的前值和预期值以外,还要比较当前(操作的)戳值和预期(操作的)戳值,当全部相同时,compareAndSet方法才能成功。 两者区别如下:
- AtomicStampedReference是使用pair的int stamp作为计数器使用,AtomicMarkableReference的pair使用的是boolean mark;
- AtomicStampedReference可能关心的是动过几次,AtomicMarkableReference关心的是有没有被人动过。
数组原子化
(返回目录↺)
Atomic包提供了以下三个数组原子化类:
- AtomicIntegerArray:原子更新整型数组里的元素。
- AtomicLongArray:原子更新长整型数组里的元素。
- AtomicReferenceArray:原子更新引用类型数组里的元素。
AtomicIntegerArray类主要是提供原子的方式更新数组里的整型元素
,并不是对整个数组对象
实现原子化,其常用方法如下:
int addAndGet(int i, int delta) // 以原子方式将输入值与数组中索引i的元素相加。
boolean compareAndSet(int i, int expect, int update) // 如果当前值等于预期值,则以原子方式将数组位置i的元素设置成update值。
使用示例:
public class AtomicIntegerArrayTest {
static int[] value = new int[] { 1, 2 };
static AtomicIntegerArray ai = new AtomicIntegerArray(value);
public static void main(String[] args) {
ai.getAndSet(0, 3);
System.out.println(ai.get(0));
System.out.println(value[0]);
}
}
结果如下:
3
1
需要注意的是,数组value通过构造方法传递进去,然后AtomicIntegerArray会将当前数组复制一份,所以当AtomicIntegerArray对内部的数组元素进行修改时,不会影响到传入的数组。
Field更新原子化
(返回目录↺)
如果我们只需要某个类里的某个字段,Atomic包提供了以下三个类来只更新类中的Filed:
- AtomicIntegerFieldUpdater:原子更新整型的字段的更新器。
- AtomicLongFieldUpdater:原子更新长整型字段的更新器。
- AtomicReferenceFieldUpdater:原子更新引用类型里的字段。
原子更新字段类都是抽象类,每次使用都时候必须使用静态方法newUpdater创建一个更新器。原子更新类的字段的必须使用public volatile修饰符。AtomicIntegerFieldUpdater的例子代码如下:
private static AtomicIntegerFieldUpdater<User> updater = AtomicIntegerFieldUpdater
.newUpdater(User.class, "old");
public static void main(String[] args) {
User conan = new User("conan", 10);
System.out.println(updater.getAndIncrement(conan));
System.out.println(updater.get(conan));
}
结果如下:
10
11