1. Java并发编程-线程的各种创建方式
方法一:继承Thread类,作为线程对象存在(继承Thread对象)
public class CreatThreadDemo1 extends Thread{
/**
* 构造方法: 继承父类方法的Thread(String name);方法
* @param name
*/
public CreatThreadDemo1(String name){
super(name);
}
@Override
public void run() {
while (!interrupted()){
System.out.println(getName()+"线程执行了...");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
CreatThreadDemo1 d1 = new CreatThreadDemo1("first");
CreatThreadDemo1 d2 = new CreatThreadDemo1("second");
d1.start();
d2.start();
d1.interrupt(); //中断第一个线程
}
}
常规方法,不多做介绍了,interrupted方法,是来判断该线程是否被中断。(终止线程不允许用stop方法,该方法不会施放占用的资源。所以我们在设计程序的时候,要按照中断线程的思维去设计,就像上面的代码一样)。
让线程等待的方法
sleep不会释放锁、wait会释放锁然后进入等待队列。
- Thread.sleep(200); //线程休息2ms
- Object.wait(); //让线程进入等待,直到调用Object的notify或者notifyAll时,线程停止休眠
方法二:实现runnable接口,作为线程任务存在
Thread类也是Runnable的实现类。start方法最终会调用Runnable的run方法。
public class CreatThreadDemo2 implements Runnable {
@Override
public void run() {
while (true){
System.out.println("线程执行了...");
}
}
public static void main(String[] args) {
//将线程任务传给线程对象
Thread thread = new Thread(new CreatThreadDemo2());
//启动线程
thread.start();
}
}
Runnable 只是来修饰线程所执行的任务,它不是一个线程对象。想要启动Runnable对象,必须将它放到一个线程对象里。
方法三:匿名内部类创建线程对象
用lambda写更好。
public class CreatThreadDemo3 extends Thread{
public static void main(String[] args) {
//创建无参线程对象
new Thread(){
@Override
public void run() {
System.out.println("线程执行了...");
}
}.start();
//创建带线程任务的线程对象
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程执行了...");
}
}).start();
//创建带线程任务并且重写run方法的线程对象
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("runnable run 线程执行了...");
}
}){
@Override
public void run() {
System.out.println("override run 线程执行了...");
}
}.start();
}
}
创建带线程任务并且重写run方法的线程对象中,为什么只运行了Thread的run方法。我们看看Thread类的源码,
,我们可以看到Thread实现了Runnable接口,而Runnable接口里有一个run方法。
所以,我们最终调用的重写的方法应该是Thread类的run方法。而不是Runnable接口的run方法。
方法四:创建带返回值的线程
public class CreatThreadDemo4 implements Callable {
public static void main(String[] args) throws ExecutionException, InterruptedException {
CreatThreadDemo4 demo4 = new CreatThreadDemo4();
FutureTask<Integer> task = new FutureTask<Integer>(demo4); //FutureTask最终实现的是runnable接口
Thread thread = new Thread(task);
thread.start();
System.out.println("我可以在这里做点别的业务逻辑...因为FutureTask是提前完成任务");
//拿出线程执行的返回值
Integer result = task.get();
System.out.println("线程中运算的结果为:"+result);
}
//重写Callable接口的call方法
@Override
public Object call() throws Exception {
int result = 1;
System.out.println("业务逻辑计算中...");
Thread.sleep(3000);
return result;
}
}
Callable接口介绍:
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;
}
返回指定泛型的call方法。然后调用FutureTask对象的get方法得道call方法的返回值。
方法五:定时器Timer
public class CreatThreadDemo5 {
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("定时器线程执行了...");
}
},0,1000); //延迟0,周期1s
}
}
方法六:线程池创建线程
public class CreatThreadDemo6 {
public static void main(String[] args) {
//创建一个具有10个线程的线程池
ExecutorService threadPool = Executors.newFixedThreadPool(10);
long threadpoolUseTime = System.currentTimeMillis();
for (int i = 0;i<10;i++){
threadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"线程执行了...");
}
});
}
long threadpoolUseTime1 = System.currentTimeMillis();
System.out.println("多线程用时"+(threadpoolUseTime1-threadpoolUseTime));
//销毁线程池
threadPool.shutdown();
threadpoolUseTime = System.currentTimeMillis();
}
}
方法七:利用java8新特性 stream 实现并发
lambda表达式不懂的,可以看看我的java8新特性文章:
java8-lambda:https://www.jianshu.com/p/3a08dc78a05f
java8-stream:https://www.jianshu.com/p/ea16d6712a00
public class CreatThreadDemo7 {
public static void main(String[] args) {
List<Integer> values = Arrays.asList(10,20,30,40);
//parallel 平行的,并行的
int result = values.parallelStream().mapToInt(p -> p*2).sum();
System.out.println(result);
//证明它是并发处理
values.parallelStream().forEach(p-> System.out.println( Thread.currentThread().getName() +" "+p));
}
}
200
ForkJoinPool.commonPool-worker-3 20
ForkJoinPool.commonPool-worker-1 40
ForkJoinPool.commonPool-worker-2 10
main 30
2. Java并发编程-wait、notify使用入门
在前面讲解synchronize的文章中,有提到wait和notify,大概描述了它的使用,这里我将根据官方api详细的教你如何使用。
所属对象
wait,notify,notifyAll 是定义在Object类的实例方法,用于控制线程状态。
使用总结:
- wait() 和 notify() 必须由对象持有者去调用,也就是说必须进入monitor监控,有三种方式:
- 执行该对象的synchronized实例方法
- 执行synchronized代码块
- 执行该类的synchronized静态方法
- 当想要调用wait( )进行线程等待时,必须要取得这个锁对象的控制权(对象监视器),一般是放到synchronized(obj)代码中。
- 在while循环里用wait操作性能更好(比if判断)
- 调用obj.wait()方法会释放obj的锁,并将锁放入锁池队列。(锁池队列标识在对象头)
- obj.notify( )方法只会通知等待队列中的第一个相关线程去锁池拿obj锁, 然后进入就绪状态(不会通知优先级比较高的线程)
- notifyAll( )通知所有等待该竞争资源的线程(也不会按照线程的优先级来执行)
实例
1. 通过调用对象的wait和notify实现
/** 调用对象的 wait 和 notify 实例
* Created by Fant.J.
*/
public class Demo {
private boolean flag = false;
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
public static void main(String[] args) {
Demo demo = new Demo();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("修改flag线程执行");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
demo.setFlag(true);
notify();
System.out.println("修改flag并释放锁成功");
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
while (demo.isFlag() != true){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("flag为true时线程执行");
}
}).start();
}
}
修改flag线程执行
Exception in thread "Thread-1" java.lang.IllegalMonitorStateException
at java.lang.Object.wait(Native Method)
at java.lang.Object.wait(Object.java:502)
at com.thread.waitNotify.Demo$2.run(Demo.java:41)
at java.lang.Thread.run(Thread.java:748)
Exception in thread "Thread-0" java.lang.IllegalMonitorStateException
at java.lang.Object.notify(Native Method)
at com.thread.waitNotify.Demo$1.run(Demo.java:31)
at java.lang.Thread.run(Thread.java:748)
从运行结果可以看出,它报错IllegalMonitorStateException,我们上面有给出报该异常的原因,是因为没有没有获取到对象的监视器控制权,我们new了两个线程,一个调用了wait 一个调用了notify,jvm认为wait是一个线程下的wait,notify是另一个线程下的notify,事实上,我们想实现的是针对Demo对象的锁的wait和notify,所以,我们需要调用Demo对象的wait和notify方法。
修改后的代码:
/** 调用对象的 wait 和 notify 实例
* Created by Fant.J.
*/
public class Demo {
private boolean flag = false;
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
public static void main(String[] args) {
Demo demo = new Demo();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (demo) {
System.out.println("修改flag线程执行");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
demo.setFlag(true);
demo.notify();
System.out.println("修改flag并释放锁成功");
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (demo) {
while (demo.isFlag() != true) {
try {
demo.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("flag为true时线程执行");
}
}
}).start();
}
}
修改flag线程执行
修改flag并释放锁成功
flag为true时线程执行
修改了两处,一处是加了synchronized代码块,一处是添加了wait和notify的调用对象。
2. 通过synchronized修饰方法来实现
package com.thread.waitNotify_1;
/** 通过synchronized方法实现 wait notify
* Created by Fant.J.
*/
public class Demo2 {
private volatile boolean flag = false;
public synchronized boolean getFlag() {
System.out.println(Thread.currentThread().getName()+"开始执行...");
if (this.flag != true){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+"执行结束...");
return flag;
}
public synchronized void setFlag(boolean flag) {
this.flag = flag;
notify();
}
public static void main(String[] args) {
Demo2 demo2 = new Demo2();
Runnable target1 = new Runnable() {
@Override
public void run() {
demo2.getFlag();
}
};
Runnable target2 = new Runnable() {
@Override
public void run() {
demo2.setFlag(true);
}
};
new Thread(target1).start();
new Thread(target1).start();
new Thread(target1).start();
new Thread(target1).start();
}
}
Thread-0开始执行...
Thread-1开始执行...
Thread-2开始执行...
Thread-3开始执行...
为什么四个线程都执行了呢?synchronized不是锁定线程了吗?我在上面8点中也有说明,wait()操作后,会暂时释放synchronized的同步锁,等notify()触发后,又会重拾起该锁,保证线程同步。
然后我们条用target2来释放一个线程:
new Thread(target1).start();
new Thread(target1).start();
new Thread(target1).start();
new Thread(target1).start();
new Thread(target2).start();
Thread-0开始执行...
Thread-1开始执行...
Thread-2开始执行...
Thread-3开始执行...
Thread-0执行结束...
可以看到只释放了一个线程,并且是第一个线程,如果有优先级,他也是释放第一个线程。
如果把notify改成notifyAll。
Thread-0开始执行...
Thread-2开始执行...
Thread-1开始执行...
Thread-3开始执行...
Thread-3执行结束...
Thread-1执行结束...
Thread-2执行结束...
Thread-0执行结束...
如何证明,每次notify后会拿到synchronized锁呢,我在执行notify后添加一些时间戳捕获帮助我们查看
public synchronized void setFlag(boolean flag) {
this.flag = flag;
// notify();
notifyAll();
System.out.println("测试notify触发后会不会等2s"+System.currentTimeMillis());
try {
Thread.sleep(2000);
System.out.println("测试notify触发后会不会等2s"+System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Thread-0开始执行...
Thread-1开始执行...
Thread-2开始执行...
Thread-3开始执行...
测试notify触发后会不会等2s1529817196847
测试notify触发后会不会等2s1529817198847
Thread-3执行结束...
Thread-2执行结束...
Thread-1执行结束...
Thread-0执行结束...
可以看到的确是notify重拾了synchronized的同步锁,执行完该方法后才会释放锁。
3. Java并发编程-线程安全、优先级设定
优先级设定
一个合理的优先级可以在一定条件下避免一些活跃性问题,比如死锁、饥饿等
public class Task implements Runnable{
@Override
public void run() {
while (true) {
System.out.println(Thread.currentThread().getName() + "线程执行了...");
}
}
}
public class PriorityTest {
public static void main(String[] args) {
Thread t1 = new Thread(new Task());
Thread t2 = new Thread(new Task());
/**
* 设置优先级
* MAX_PRIORITY=10
* MIN_PRIORITY=1
* NORM_PRIORITY=5
*/
t1.setPriority(Thread.NORM_PRIORITY);
t2.setPriority(Thread.MAX_PRIORITY);
t1.start();
t2.start();
}
}
线程安全问题解决
当多个线程在非原子处理下操作相同的资源时,难免出现资源的混乱。
我在这里举个value++的例子。
无线程安全时
public class Task{
public int value = 0;
// 没有处理线程安全
public int getValue() {
return value++;
}
public static void main(String[] args) {
Task task = new Task();
new Thread(){
@Override
public void run() {
while (true) {
System.out.println(Thread.currentThread().getName() + " " + task.getValue());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
new Thread(){
@Override
public void run() {
while (true) {
System.out.println(Thread.currentThread().getName() + " " + task.getValue());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
}
}
我们会发现2和5有重复。这是jvm底层的问题,我在后文有分析。
处理线程安全
如果我们给线程加锁,将其变成原子性操作,就会解决该问题。
public synchronized int getValue() {
return value++;
}
既在修饰符后加上synchronized关键字。
但是又有一个问题,这样的话,其实原理上是串行处理的, 性能比较低, 那我们该如何更好的解决这个问题呢。下一章分析。
4. Java并发编程-深入剖析volatile关键字
1.volatile关键字的两层语义
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
- 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
- 禁止进行指令重排序。
我们来看一段代码:
假如线程1先执行,线程2后执行:
//线程1
boolean stop = false;
while(!stop){
//doSomething();
}
//线程2
stop = true;
事实上,这段代码会真的先执行线程1,然后再执行2吗,答案是肯定的:不是。两个线程各干各的事情,没有绝对的先后问题,所以会出现两种答案(一个线程用stop=false跑线程1,一个线程用stop=true跑线程二,互不相关), 那如何保证stop在两个线程中变化可见呢?将该变量用volatile修饰。
volatile修饰使得变量多线程可见原理:
这个涉及到jvm的内存模型, 这个内存模型也就是下面三点描述的:
- 使用volatile关键字会强制将修改的值立即写入主存;
- 使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);
- 由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。
所以不管多少个线程跑的时候,stop的值是各线程保持同步的。这是利用了volatile的线程可见性原理。
2.volatile保证原子性吗?
从上面知道volatile关键字保证了操作的可见性,但是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);
}
}
经过上面的介绍volatile的可见性,我想大家能很快的得出这段代码的答案10000。然而事实上打印出来的数字总比10000小。这就涉及到了volatile与原子性操作的联系。
原因:自增操作不是原子性操作,而且volatile也无法保证对变量的任何操作都是原子性的。
- 自增操作是不具备原子性的,它包括读取变量的原始值、进行加1操作、写入工作内存。那么就是说自增操作的三个子操作可能会分割开执行
- 假如线程1从住内存中获取到变量值,在执行自增的时候是阻塞性质的,这时候线程2也拿到一个相同的值,然后也进行自增,那么这两个线程最终写入的值是一样的。
那如何保证原子性呢?有三种方式
1. 给自增方法加上同步锁synchronized
public class Test {
public volatile int inc = 0;
public synchronized 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);
}
}
2. 采用Lock
public class Test {
public volatile int inc = 0;
Lock lock = new ReentrantLock();
public void increase() {
lock.lock();
try {
inc++;
} finally{
lock.unlock();
}
}
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);
}
}
3. 采用AtomicInteger
在java 1.5的java.util.concurrent.atomic包下提供了一些原子操作类,即对基本数据类型的 自增(加1操作),自减(减1操作)、以及加法操作(加一个数),减法操作(减一个数)进行了封装,保证这些操作是原子性操作。atomic是利用CAS来实现原子性操作的(Compare And Swap),CAS实际上是利用处理器提供的CMPXCHG指令实现的,而处理器执行CMPXCHG指令是一个原子性操作。
public class Test {
public AtomicInteger inc = new AtomicInteger();
public void increase() {
inc.getAndIncrement();
}
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);
}
}
3.volatile有序性
volatile保证有序性通过两点:也就是Happen-Before原则:
- 当程序执行到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也能保证执行顺序与前面编译顺序一样。
应该保证有序性而没有使用volatile会怎样呢
//线程1:
context = loadContext(); //语句1
inited = true; //语句2
//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
前面举这个例子的时候,提到有可能语句2会在语句1之前执行,那么久可能导致context还没被初始化,而线程2中就使用未初始化的context去进行操作,导致程序报空指针异常。如果用volatile关键字对inited变量进行修饰,就不会出现这种问题了。
4.volatile的原理和实现机制
前面讲述了源于volatile关键字的一些使用,下面我们来探讨一下volatile到底如何保证可见性和禁止指令重排序的。参考:《深入理解Java虚拟机》:
观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令.
lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
- 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
- 它会强制将对缓存的修改操作立即写入主存;
- 如果是写操作,它会导致其他CPU中对应的缓存行无效。
5. Java并发编程-synchronized保证线程安全的原理
什么时候会发生线程安全问题:
线程安全是并发编程中的重要关注点,应该注意到的是,造成线程安全问题的主要诱因有两点:
- 存在共享数据(也称临界资源)。
- 存在多条线程共同操作共享数据。
那如何保证线程安全呢?java提供了一些线程安全的工具, 这里主要介绍synchronized锁。
synchronized的三种使用方式
synchronized关键字最主要有以下3种使用方式,下面分别介绍
- 修饰实例方法。作用于当前实例加锁,进入同步代码前要获得当前实例的锁
- 修饰静态方法。作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
- 修饰代码块。指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
synchronized与this锁(对象锁)、class锁(类锁)的关系
- 如果修饰的是静态方法,则拿到的是类锁。因为此时对象实例还没有生成。如果修饰普通方法,则拿到的是对象锁,同一个实例会拦截。
- 只要采用类锁
synchronized(xxx.class)
, 就会拦截所有线程,同时只能有一个线程访问。 - 只要采用对象锁
sychronized(this)
, 如果是同一个实例就会拦截, 如果是不同实例, 则可以同时访问。
synchronized作用于实例方法
所谓的实例对象锁就是用synchronized修饰实例对象中的实例方法,注意是实例方法不包括静态方法,如下
public class AccountingSync implements Runnable{
//共享资源(临界资源)
static int i=0;
/**
* synchronized 修饰实例方法
*/
public synchronized void increase(){
i++;
}
@Override
public void run() {
for(int j=0;j<1000000;j++){
increase();
}
}
public static void main(String[] args) throws InterruptedException {
AccountingSync instance=new AccountingSync();
Thread t1=new Thread(instance);
Thread t2=new Thread(instance);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
/**
* 输出结果:
* 2000000
*/
}
上述代码中,我们开启两个线程操作同一个共享资源即变量i,由于i++;操作并不具备原子性,该操作是先读取值,然后写回一个新值,相当于原来的值加上1,分两步完成,如果第二个线程在第一个线程读取旧值和写回新值期间读取i的域值,那么第二个线程就会与第一个线程一起看到同一个值,并执行相同值的加1操作,这也就造成了线程安全失败,因此对于increase方法必须使用synchronized修饰,以便保证线程安全。此时我们应该注意到synchronized修饰的是实例方法increase,在这样的情况下,当前线程的锁便是实例对象instance,注意Java中的线程同步锁可以是任意对象。从代码执行结果来看确实是正确的,倘若我们没有使用synchronized关键字,其最终输出结果就很可能小于2000000,这便是synchronized关键字的作用。这里我们还需要意识到,当一个线程正在访问一个对象的 synchronized 实例方法,那么其他线程不能访问该对象的其他 synchronized 方法,毕竟一个对象只有一把锁,当一个线程获取了该对象的锁之后,其他线程无法获取该对象的锁,所以无法访问该对象的其他synchronized实例方法,但是其他线程还是可以访问该实例对象的其他非synchronized方法,当然如果是一个线程 A 需要访问实例对象 obj1 的 synchronized 方法 f1(当前对象锁是obj1),另一个线程 B 需要访问实例对象 obj2 的 synchronized 方法 f2(当前对象锁是obj2),这样是允许的,因为两个实例对象锁并不同相同,此时如果两个线程操作数据并非共享的,线程安全是有保障的,遗憾的是如果两个线程操作的是共享数据,那么线程安全就有可能无法保证了,如下代码将演示出该现象
public class AccountingSyncBad implements Runnable{
static int i=0;
public synchronized void increase(){
i++;
}
@Override
public void run() {
for(int j=0;j<1000000;j++){
increase();
}
}
public static void main(String[] args) throws InterruptedException {
//new新实例
Thread t1=new Thread(new AccountingSyncBad());
//new新实例
Thread t2=new Thread(new AccountingSyncBad());
t1.start();
t2.start();
//join含义:当前线程A等待thread线程终止之后才能从thread.join()返回
t1.join();
t2.join();
System.out.println(i);
}
}
控制台结果:1452317
上述代码与前面不同的是我们同时创建了两个新实例AccountingSyncBad,然后启动两个不同的线程对共享变量i进行操作,但很遗憾操作结果是1452317而不是期望结果2000000,因为上述代码犯了严重的错误,虽然我们使用synchronized修饰了increase方法,但却new了两个不同的实例对象,这也就意味着存在着两个不同的实例对象锁,因此t1和t2都会进入各自的对象锁,也就是说t1和t2线程使用的是不同的锁,因此线程安全是无法保证的。解决这种困境的的方式是将synchronized作用于静态的increase方法,这样的话,对象锁就当前类对象,由于无论创建多少个实例对象,但对于的类对象拥有只有一个,所有在这样的情况下对象锁就是唯一的。下面我们看看如何使用将synchronized作用于静态的increase方法。
synchronized作用于静态方法
当synchronized作用于静态方法时,其锁就是当前类的class对象锁。由于静态成员不专属于任何一个实例对象,是类成员,因此通过class对象锁可以控制静态 成员的并发操作。需要注意的是如果一个线程A调用一个实例对象的非static synchronized方法,而线程B需要调用这个实例对象所属类的静态 synchronized方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的class对象,而访问非静态 synchronized 方法占用的锁是当前实例对象锁,看如下代码
public class AccountingSyncClass implements Runnable{
static int i=0;
/**
* 作用于静态方法,锁是当前class对象,也就是
* AccountingSyncClass类对应的class对象
*/
public static synchronized void increase(){
i++;
}
/**
* 非静态,访问时锁不一样不会发生互斥
*/
public synchronized void increase4Obj(){
i++;
}
@Override
public void run() {
for(int j=0;j<1000000;j++){
increase();
}
}
public static void main(String[] args) throws InterruptedException {
//new新实例
Thread t1=new Thread(new AccountingSyncClass());
//new心事了
Thread t2=new Thread(new AccountingSyncClass());
//启动线程
t1.start();t2.start();
t1.join();t2.join();
System.out.println(i);
}
}
由于synchronized关键字修饰的是静态increase方法,与修饰实例方法不同的是,其锁对象是当前类的class对象。注意代码中的increase4Obj方法是实例方法,其对象锁是当前实例对象,如果别的线程调用该方法,将不会产生互斥现象,毕竟锁对象不同,但我们应该意识到这种情况下可能会发现线程安全问题(操作了共享静态变量i)。
synchronized同步代码块
除了使用关键字修饰实例方法和静态方法外,还可以使用同步代码块,在某些情况下,我们编写的方法体可能比较大,同时存在一些比较耗时的操作,而需要同步的代码又只有一小部分,如果直接对整个方法进行同步操作,可能会得不偿失,此时我们可以使用同步代码块的方式对需要同步的代码进行包裹,这样就无需对整个方法进行同步操作了,同步代码块的使用示例如下:
public class AccountingSync implements Runnable{
static AccountingSync instance=new AccountingSync();
static int i=0;
@Override
public void run() {
//省略其他耗时操作....
//使用同步代码块对变量i进行同步操作,锁对象为instance
synchronized(instance){
for(int j=0;j<1000000;j++){
i++;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(instance);
Thread t2=new Thread(instance);
t1.start();t2.start();
t1.join();t2.join();
System.out.println(i);
}
}
从代码看出,将synchronized作用于一个给定的实例对象instance,即当前实例对象就是锁对象,每次当线程进入synchronized包裹的代码块时就会要求当前线程持有instance实例对象锁,如果当前有其他线程正持有该对象锁,那么新到的线程就必须等待,这样也就保证了每次只有一个线程执行i++;操作。当然除了instance作为对象外,我们还可以使用this对象(代表当前实例)或者当前类的class对象作为锁,如下代码:
//this,当前实例对象锁
synchronized(this){
for(int j=0;j<1000000;j++){
i++;
}
}
//class对象锁
synchronized(AccountingSync.class){
for(int j=0;j<1000000;j++){
i++;
}
}
了解完synchronized的基本含义及其使用方式后,下面我们将进一步深入理解synchronized的底层实现原理。
synchronized底层语义原理
Java 虚拟机中的同步(Synchronization)基于进入和退出管程(Monitor)对象实现, 无论是显式同步(有明确的 monitorenter 和 monitorexit 指令,即同步代码块)还是隐式同步都是如此。在 Java 语言中,同步用的最多的地方可能是被 synchronized 修饰的同步方法。同步方法 并不是由 monitorenter 和 monitorexit 指令来实现同步的,而是由方法调用指令读取运行时常量池中方法的 ACC_SYNCHRONIZED 标志来隐式实现的,关于这点,稍后详细分析。下面先来了解一个概念Java对象头,这对深入理解synchronized实现原理非常关键。
如果对上面的执行结果还有疑问,也先不用急,我们先来了解Synchronized的原理,再回头上面的问题就一目了然了。我们先通过反编译下面的代码来看看Synchronized是如何实现对代码块进行同步的:
public class SynchronizedDemo {
public void method() {
synchronized (this) {
System.out.println("Method 1 start");
}
}
}
反编译结果:
关于这两条指令的作用,我们直接参考JVM规范中描述:
monitorenter :
每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
-
如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
-
如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
-
如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
monitorexit:
执行monitorexit的线程必须是objectref所对应的monitor的所有者。
指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。
通过这两段描述,我们应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。
我们再来看一下同步方法的反编译结果:
源代码:
public class SynchronizedMethod {
public synchronized void method() {
System.out.println("Hello World!");
}
}
反编译结果:
从反编译的结果来看,方法的同步并没有通过指令monitorenter和monitorexit来完成(理论上其实也可以通过这两条指令来实现),不过相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标示符。JVM就是根据该标示符来实现方法的同步的:当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。 其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。
关于synchronized 可能需要了解的关键点
synchronized的可重入性
从互斥锁的设计上来说,当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁,请求将会成功,在java中synchronized是基于原子性的内部锁机制,是可重入的,因此在一个线程调用synchronized方法的同时在其方法体内部调用该对象另一个synchronized方法,也就是说一个线程得到一个对象锁后再次请求该对象锁,是允许的,这就是synchronized的可重入性。如下:
public class AccountingSync implements Runnable{
static AccountingSync instance=new AccountingSync();
static int i=0;
static int j=0;
@Override
public void run() {
for(int j=0;j<1000000;j++){
//this,当前实例对象锁
synchronized(this){
i++;
increase();//synchronized的可重入性
}
}
}
public synchronized void increase(){
j++;
}
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(instance);
Thread t2=new Thread(instance);
t1.start();t2.start();
t1.join();t2.join();
System.out.println(i);
}
}
正如代码所演示的,在获取当前实例对象锁后进入synchronized代码块执行同步代码,并在代码块中调用了当前实例对象的另外一个synchronized方法,再次请求当前实例锁时,将被允许,进而执行方法体代码,这就是重入锁最直接的体现,需要特别注意另外一种情况,当子类继承父类时,子类也是可以通过可重入锁调用父类的同步方法。注意由于synchronized是基于monitor实现的,因此每次重入,monitor中的计数器仍会加1。
线程中断与synchronized
线程中断
正如中断二字所表达的意义,在线程运行(run方法)中间打断它,在Java中,提供了以下3个有关线程中断的方法
//中断线程(实例方法)
public void Thread.interrupt();
//判断线程是否被中断(实例方法)
public boolean Thread.isInterrupted();
//判断是否被中断并清除当前中断状态(静态方法)
public static boolean Thread.interrupted();
当一个线程处于被阻塞状态或者试图执行一个阻塞操作时,使用Thread.interrupt()方式中断该线程,注意此时将会抛出一个InterruptedException的异常,同时中断状态将会被复位(由中断状态改为非中断状态),如下代码将演示该过程:
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异常,此时中断状态也将被复位。这里有些人可能会诧异,为什么不用Thread.sleep(2000);而是用TimeUnit.SECONDS.sleep(2);其实原因很简单,前者使用时并没有明确的单位说明,而后者非常明确表达秒的单位,事实上后者的内部实现最终还是调用了Thread.sleep(2000);,但为了编写的代码语义更清晰,建议使用TimeUnit.SECONDS.sleep(2);的方式,注意TimeUnit是个枚举类型。ok~,除了阻塞中断的情景,我们还可能会遇到处于运行期且非阻塞的状态的线程,这种情况下,直接调用Thread.interrupt()中断线程是不会得到任响应的,如下代码,将无法中断非阻塞状态下的线程:
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并未被中断,因为处于非阻塞状态的线程需要我们手动进行中断检测并结束程序,改进后代码如下:
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();
/**
* 输出结果:
线程中断
已跳出循环,线程中断!
*/
}
}
是的,我们在代码中使用了实例方法isInterrupted判断线程是否已被中断,如果被中断将跳出循环以此结束线程。综合所述,可以简单总结一下中断两种情况,一种是当线程处于阻塞状态或者试图执行一个阻塞操作时,我们可以使用实例方法interrupt()进行线程中断,执行中断操作后将会抛出interruptException异常(该异常必须捕捉无法向外抛出)并将中断状态复位,另外一种是当线程处于运行状态时,我们也可调用实例方法interrupt()进行线程中断,但同时必须手动判断中断状态,并编写中断线程的代码(其实就是结束run方法体的代码)。有时我们在编码时可能需要兼顾以上两种情况,那么就可以如下编写:
public void run(){
try {
//判断当前线程是否已中断,注意interrupted方法是静态的,执行后会对中断状态进行复位
while (!Thread.interrupted()) {
TimeUnit.SECONDS.sleep(2);
}
} catch (InterruptedException e) {
}
}
中断与synchronized
事实上线程的中断操作对于正在等待获取的锁对象的synchronized方法或者代码块并不起作用,也就是对于synchronized来说,如果一个线程在等待锁,那么结果只有两种,要么它获得这把锁继续执行,要么它就保存等待,即使调用中断线程的方法,也不会生效。演示代码如下
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();
}
}
我们在SynchronizedBlocked构造函数中创建一个新线程并启动获取调用f()获取到当前实例锁,由于SynchronizedBlocked自身也是线程,启动后在其run方法中也调用了f(),但由于对象锁被其他线程占用,导致t线程只能等到锁,此时我们调用了t.interrupt();
但并不能中断线程。
等待唤醒机制与synchronized
所谓等待唤醒机制本篇主要指的是notify/notifyAll和wait方法,在使用这3个方法时,必须处于synchronized代码块或者synchronized方法中,否则就会抛出IllegalMonitorStateException异常,这是因为调用这几个方法前必须拿到当前对象的监视器monitor对象,也就是说notify/notifyAll和wait方法依赖于monitor对象,在前面的分析中,我们知道monitor 存在于对象头的Mark Word 中(存储monitor引用指针),而synchronized关键字可以获取 monitor ,这也就是为什么notify/notifyAll和wait方法必须在synchronized代码块或者synchronized方法调用的原因。
synchronized (obj) {
obj.wait();
obj.notify();
obj.notifyAll();
}
需要特别理解的一点是,与sleep方法不同的是wait方法调用完成后,线程将被暂停,但wait方法将会释放当前持有的监视器锁(monitor),直到有线程调用notify/notifyAll方法后方能继续执行,而sleep方法只让线程休眠并不释放锁。同时notify/notifyAll方法调用后,并不会马上释放监视器锁,而是在相应的synchronized(){}/synchronized方法执行结束后才自动释放锁。
Java虚拟机对synchronized的优化
**锁主要存在四中状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。**他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。
锁粗化
就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。
如上面实例:vector每次add的时候都需要加锁操作,JVM检测到对同一个对象(vector)连续加锁、解锁操作,会合并一个更大范围的加锁、解锁操作,即加锁解锁操作会移到for循环之外。
锁消除
如果JVM不可能存在资源竞争,变量不可能逃逸到方法外,则将锁消除。
自旋锁
对象锁的锁状态只会持续很短一段时间**,**为了这一段很短的时间频繁地阻塞和唤醒线程是非常不值得的。
偏向锁
为了在非多线程的情况下减少线程间锁的交换消耗资源(CAS操作)。偏向锁只能被动释放噢,等锁有了竞争
轻量级锁
当多个线程竞争偏向锁,会升级为轻量级锁。(利用CAS操作更新对象的Mark Word,如果失败则升级为重量级锁)
重量级锁
重量级锁通过对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。
6. Java并发编程-join方法
thread.join把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。比如在线程A中调用了线程B的Join()方法,直到线程B执行完毕后,才会继续执行线程A。
例子:
在线程A中调用了线程B的Join()方法,直到线程B执行完毕后,才会继续执行线程A。
public class Demo {
public void a(Thread join){
System.out.println("方法a执行了");
join.start();
try {
join.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("a方法执行完毕");
}
public void b(){
System.out.println("加塞线程执行。。");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("加塞线程执行完毕。。");
}
public static void main(String[] args) {
Demo demo = new Demo();
Thread t1 = new Thread(() -> demo.b());
new Thread(() -> demo.a(t1)).start();
}
}
方法a执行了
加塞线程执行。。
加塞线程执行完毕。。
a方法执行完毕
源码分析
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操作挂起 调用join方法的线程锁
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
其中参数long millis
是来规范join执行的时间,默认为0.
isAlive():
A thread is alive if it has been started and has not yet died
如果当前线程活着,就将其挂起(wait)。每当线程执行完毕后,默认会调用notifyAll方法。
7. Java并发编程-本地线程ThreadLocal
ThreadLocal一般称为线程本地变量,它是一种特殊的线程绑定机制,将变量与线程绑定在一起,为每一个线程维护一个独立的变量副本。通过ThreadLocal可以将对象的可见范围限制在同一个线程内。
Demo
public class Demo {
private ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
public int getNext(){
Integer integer = threadLocal.get();
integer++;
threadLocal.set(integer);
return integer;
}
public static void main(String[] args) {
Demo demo = new Demo();
new Thread(() -> {
while (true){
System.out.println(Thread.currentThread().getName()+" :"+demo.getNext());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(() -> {
while (true){
System.out.println(Thread.currentThread().getName()+" :"+demo.getNext());
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
Thread-0 :1
Thread-1 :1
Thread-1 :2
Thread-1 :3
Thread-1 :4
Thread-1 :5
Thread-1 :6
Thread-1 :7
Thread-1 :8
Thread-1 :9
Thread-1 :10
Thread-0 :2
......
我们可以看到,线程0和线程1是相互不干扰的
源码分析
demo
中,我们用到ThreadLocal
的get()
方法和set()
方法,我们看看它底层是怎么实现的,当然,还有个remove()
方法。这三个是最核心的API。
1. get()
先获取到当前线程, 然后根据当前线程获取ThreadLocalMap, 然后返回其value值。
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
//如果map不为空,则拿出他的Entry,进而拿出entry的value
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
return setInitialValue();
中的setInitialValue()
:
1.1 setInitialValue()
private T setInitialValue() {
//调用下面的initialValue方法
T value = initialValue();
//获取当前线程
Thread t = Thread.currentThread();
//通过当前线程来获取map
ThreadLocalMap map = getMap(t);
//如果map不为空,则set,如果为空,new一个ThreadLocalMap(createMap方法)
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
protected T initialValue() {
return null;
}
1.2 createMap
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
1.3 getMap
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
ThreadLocal.ThreadLocalMap threadLocals = null;
1.4 Entry
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
注意这里继承了WeakReference, 标明了它是一个软引用。
为什么用软引用呢?
假如每个key都强引用指向threadlocal,也就是上图虚线那里是个强引用,那么这个threadlocal就会因为和entry存在强引用无法被回收!造成内存泄漏 ,除非线程结束,线程被回收了,map也跟着回收。
1.5 ThreadLocal和ThreadLocalMap对应关系
2. set(T value)
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
set()
方法和上文的setInitialValue()
方法很相似。不过一个是面向业务编程,一个是对象初始化(无参)。
2.1 remove
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
m.remove(this) 的 remove() 方法
2.2 ThreadLocalMap.remove
/**
* Remove the entry for key.
*/
private void remove(ThreadLocal<?> key) {
//获取所有的Entry
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
//遍历查找 key,如果存在,clear对应的Entry
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
3. 总结
我们可以看到,ThreadLocal
和Thread
对象进行绑定,然后将Thread
做key
(实际上key并不是ThreadLocal本身,而是它的一个弱引用),Object
做value
存放在map
中。也就是说每个线程有一个自己的ThreadLocalMap。调用get
方法时,获取到当前对象然后获取value
进而进行操作,在往某个ThreadLocal
里塞值的时候,都会往自己的ThreadLocalMap
里存,读也是以某个ThreadLocal
作为引用,在自己的map
里找对应的key
,从而实现了线程隔离。
8. Java并发编程-ThreadLocal内存泄漏问题
ThreadLocal
我们知道ThreadLocal可以用来做线程隔离, 但如果使用不当也会产生内存泄漏的问题。这篇文章主要通过内存泄漏来讲述。
内存泄漏原因
ThreadLocal内存泄露,最主要的原因在于它的内部类ThreadLocalMap中的Entry的设计。Entry继承了WeakReference<ThreadLocal<?>>,即Entry的key是弱引用:
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
...
}
为什么要设计为软连接
假如每个key都强引用指向ThreadLocal,也就是上图虚线那里是个强引用,那么这个ThreadLocal就会因为和entry存在强引用无法被回收!造成内存泄漏 ,除非线程结束,线程被回收了,map也跟着回收。
为什么已经是软连接了还是会内存泄漏
因为我们都用线程池来提高性能,Thread并没有像预期那样执行完就销毁,所以key’会在垃圾回收的时候被回收掉, 而key对应的value则不会被回收, 这样会导致一种现象:key为null,value有值。key为空的话value是无效数据,久而久之,value累加就会导致内存泄漏。
怎么解决这个内存泄漏问题
每次使用完ThreadLocal都调用它的remove()方法清除数据。
JDK开发者是如何避免内存泄漏的
ThreadLocal提供的get()方法中,调用了ThreadLocalMap#getEntry()方法,对key进行了校验和对null key进行擦除。
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);
}
如果key为null, 则会调用getEntryAfterMiss()方法,在这个方法中,如果k == null , 则调用expungeStaleEntry(i);方法。
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
expungeStaleEntry(i)方法完成了对key=null 的key所对应的value进行赋空, 释放了空间避免内存泄漏。
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
Entry e;
int i;
// 遍历下一个key为空的entry, 并将value指向null
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
同理, set()方法最终也是调用该方法(expungeStaleEntry), 调用路径: set(T value)->map.set(this, value)->rehash()->expungeStaleEntries()
remove方法remove()->ThreadLocalMap.remove(this)->expungeStaleEntry(i)
这样做, 也只能说尽可能避免内存泄漏, 但并不会完全解决内存泄漏这个问题。比如极端情况下我们只创建ThreadLocal但不调用set、get、remove方法等。所以最能解决问题的办法就是用完ThreadLocal后手动调用remove().
手动remove?你怎么去设计/实现?
如果是spring项目, 可以借助于bean的声明周期, 在拦截器的afterCompletion阶段进行调用。
弱引用导致内存泄漏,那为什么key不设置为强引用
如果key设置为强引用, 当threadLocal实例释放后, threadLocal=null, 但是threadLocal会有强引用指向threadLocalMap,threadLocalMap.Entry又强引用threadLocal, 这样会导致threadLocal不能正常被GC回收。
弱引用虽然会引起内存泄漏, 但是也有set、get、remove方法操作对null key进行擦除的补救措施, 方案上略胜一筹。
线程执行结束后会不会清空value
事实上,当currentThread执行结束后, threadLocalMap变得不可达从而被回收,Entry等也就都被回收了,但这个环境就要求不对Thread进行复用,但是我们项目中经常会复用线程来提高性能, 所以currentThread一般不会处于终止状态。
Thread和ThreadLocal有什么联系呢
Thread和ThreadLocal是绑定的, ThreadLocal依赖于Thread去执行, Thread将需要隔离的数据存放到ThreadLocal(准确的讲是ThreadLocalMap)中, 来实现多线程处理。
相关问题扩展
spring如何处理bean多线程下的并发问题
ThreadLocal天生为解决相同变量的访问冲突问题, 所以这个对于spring的默认单例bean的多线程访问是一个完美的解决方案。spring也确实是用了ThreadLocal来处理多线程下相同变量并发的线程安全问题。
spring 如何保证数据库事务在同一个连接下执行的
要想实现jdbc事务, 就必须是在同一个连接对象中操作, 多个连接下事务就会不可控, 需要借助分布式事务完成。那spring 如何保证数据库事务在同一个连接下执行的呢?
DataSourceTransactionManager 是spring的数据源事务管理器, 它会在你调用getConnection()的时候从数据库连接池中获取一个connection, 然后将其与ThreadLocal绑定, 事务完成后解除绑定。 这样就保证了事务在同一连接下完成。
详细源码:
- 事务开始阶段:org.springframework.jdbc.datasource.DataSourceTransactionManager#doBegin->TransactionSynchronizationManager#bindResource->org.springframework.transaction.support.TransactionSynchronizationManager#bindResource
-
事务结束阶段:
org.springframework.jdbc.datasource.DataSourceTransactionManager#doCleanupAfterCompletion->TransactionSynchronizationManager#unbindResource->org.springframework.transaction.support.TransactionSynchronizationManager#unbindResource->TransactionSynchronizationManager#doUnbindResource
9. Java并发编程-Atomic包
Java从JDK1.5开始提供了java.util.concurrent.atomic包,方便程序员在多线程环境下,无锁的进行原子操作。原子变量的底层使用了处理器提供的原子指令,但是不同的CPU架构可能提供的原子指令不一样,也有可能需要某种形式的内部锁,所以该方法不能绝对保证线程不被阻塞。
Atomic包介绍
一个小型工具包,支持单变量上的无锁线程安全编程。
它里面有17个java类。
原子更新基本类型类
用于通过原子的方式更新基本类型,Atomic包提供了以下三个类:
- AtomicBoolean:原子更新布尔类型。
- AtomicInteger:原子更新整型。
- AtomicLong:原子更新长整型。
AtomicInteger的常用方法如下:
- int addAndGet(int delta) :以原子方式将输入的数值与实例中的值(AtomicInteger里的value)相加,并返回结果
- boolean compareAndSet(int expect, int update) :如果输入的数值等于预期值,则以原子方式将该值设置为输入的值。
- int getAndIncrement():以原子方式将当前值加1,注意:这里返回的是自增前的值。
- void lazySet(int newValue):最终会设置成newValue,使用lazySet设置值后,可能导致其他线程在之后的一小段时间内还是可以读到旧的值。关于该方法的更多信息可以参考并发网翻译的一篇文章《AtomicLong.lazySet是如何工作的?》
- int getAndSet(int newValue):以原子方式设置为newValue的值,并返回旧值。
AtomicInteger实例
package com.thread.atomic;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Created by Fant.J.
* 2018/2/28 17:21
*/
public class AtomicIntegerTest {
static AtomicInteger value = new AtomicInteger(0);
public static int getValue(){
return value.getAndIncrement();//value++;
}
public static void main(String[] args) {
/* Runnable r1 = ()->{
for (;;){
System.out.println(Thread.currentThread().getName()+" :"+getValue());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
r1.run();*/
new Thread(()->{
for (;;){
System.out.println(Thread.currentThread().getName()+" :"+getValue());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(()->{
for (;;){
System.out.println(Thread.currentThread().getName()+" :"+getValue());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
用的lambda表达式, 没有学过的可以先看看这些文章:https://www.jianshu.com/p/3a08dc78a05f
那如何更新double类型的数据呢
Atomic包提供了三种基本类型的原子更新,但是Java的基本类型里还有char,float和double等。那么问题来了,如何原子的更新其他的基本类型呢?Atomic包里的类基本都是使用Unsafe实现的,让我们一起看下Unsafe的源码,发现Unsafe只提供了三种CAS方法,compareAndSwapObject,compareAndSwapInt和compareAndSwapLong,再看AtomicBoolean源码,发现其是先把Boolean转换成整型,再使用compareAndSwapInt进行CAS,所以原子更新double也可以用类似的思路来实现。
原子更新数组类
通过原子的方式更新数组里的某个元素,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));
}
}
原子更新引用类型
原子更新基本类型的AtomicInteger,只能更新一个变量,如果要原子的更新多个变量,就需要使用这个原子更新引用类型提供的类。Atomic包提供了以下三个类:
AtomicReference:原子更新引用类型。
AtomicReferenceFieldUpdater:原子更新引用类型里的字段。
AtomicMarkableReference:原子更新带有标记位的引用类型。可以原子的更新一个布尔类型的标记位和引用类型。构造方法是AtomicMarkableReference(V initialRef, boolean initialMark)
AtomicReference的使用例子代码如下:
package com.thread.atomic;
import java.util.concurrent.atomic.AtomicReference;
/**
* Created by Fant.J.
* 2018/2/28 18:40
*/
public class AtomicReferenceTest {
static AtomicReference<User> ar = new AtomicReference<>();
public static void main(String[] args) {
User user = new User("fantj",20);
User updateUser = new User("dalao",20);
ar.set(user);
ar.compareAndSet(user,updateUser); //public final boolean compareAndSet(User expect, User update)
System.out.println(ar.get().getName() + " :"+ar.get().getAge());
}
static class User{
private String name;
private Integer age;
public User(String name, int i) {
this.name = name;
this.age=i;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
}
}
原子更新字段类
如果我们只需要某个类里的某个字段,那么就需要使用原子更新字段类,Atomic包提供了以下三个类:
- AtomicIntegerFieldUpdater:原子更新整型的字段的更新器。
- AtomicLongFieldUpdater:原子更新长整型字段的更新器。
- AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于原子的更数据和数据的版本号,可以解决使用CAS进行原子更新时,可能出现的ABA问题。
原子更新字段类都是抽象类,每次使用都时候必须使用静态方法newUpdater创建一个更新器。原子更新类的字段的必须使用public volatile修饰符。
AtomicIntegerFieldUpdater的例子代码如下:
package com.thread.atomic;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
/**
* 如果我们只需要某个类里的某个字段,那么就需要使用原子更新字段类
* 原子更新类的字段的必须使用public volatile修饰符。
* Created by Fant.J.
* 2018/2/28 18:50
*/
public class AtomicIntegerFieldUpdaterTest {
//假设我们需要User里的age属性,并给age进行原子更新
static AtomicIntegerFieldUpdater<User> aif = AtomicIntegerFieldUpdater.newUpdater(User.class,"age");
public static void main(String[] args) {
User user = new User("fantj",20);
//将User对象传给 原子更新字段类对象
aif.getAndIncrement(user);
System.out.println(aif.get(user));
}
static class User{
private String name;
public volatile int age; //注意这里要加 volatile
public User(String name, int i) {
this.name = name;
this.age=i;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
}
}
那最后,聊一聊CAS
什么是CAS
CAS,Compare and Swap即比较并交换。 java.util.concurrent包借助CAS实现了区别于synchronized同步锁的一种乐观锁。乐观锁就是每次去取数据的时候都乐观的认为数据不会被修改,所以不会上锁,但是在更新的时候会判断一下在此期间数据有没有更新。CAS有3个操作数:内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。CAS的关键点在于,系统在硬件层面保证了比较并交换操作的原子性,处理器使用基于对缓存加锁或总线加锁的方式来实现多处理器之间的原子操作。
CAS的优缺点
-
CAS由于是在硬件层面保证的原子性,不会锁住当前线程,它的效率是很高的。
-
CAS虽然很高效的实现了原子操作,但是它依然存在三个问题。
-
ABA问题。CAS在操作值的时候检查值是否已经变化,没有变化的情况下才会进行更新。但是如果一个值原来是A,变成B,又变成A,那么CAS进行检查时会认为这个值没有变化,但是实际上却变化了。ABA问题的解决方法是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就变成1A-2B-3A。从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。
-
并发越高,失败的次数会越多,CAS如果长时间不成功,会极大的增加CPU的开销。因此CAS不适合竞争十分频繁的场景。
-
只能保证一个共享变量的原子操作。当对多个共享变量操作时,CAS就无法保证操作的原子性,这时就可以用锁,或者把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象的原子性,你可以把多个变量放在一个对象里来进行CAS操作。
-
以上只介绍了常用的atomic类,其它的类如果你感兴趣可以自己研究一下。官方文档
本文参考文献:
https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/atomic/package-summary.html
http://ifeve.com/java-atomic/
10. Java并发编程-AQS
AbstractQueuedSynchronizer是为实现依赖于先进先出 (FIFO) 等待队列的阻塞锁和相关同步器(信号量、事件,等等)提供的一个框架。
1. 构造方法
/**
* Creates a new {@code AbstractQueuedSynchronizer} instance
* with initial synchronization state of zero.
*/
protected AbstractQueuedSynchronizer() { }
创建具有初始同步状态 0 的新 AbstractQueuedSynchronizer 实例。
2. 方法详细信息
方法有很多,因为aqs怎么是个框架,需要大概了解每个方法,不然谈不到使用。
主要有三大类:状态(state)、获得锁(acquire)、释放锁(release)、附加一个独占线程(ExclusiveOwnerThread)
这四个概念一定要非常清楚。不然很难学会。当然,队列和链表基础也得扎实。
好了,我基于官方文档和源码整理了以下方法(过目理解):
getState
protected final int getState() {
return state;
}
返回同步状态的当前值。此操作具有 volatile 读的内存语义。
setState
protected final int getState() {
return state;
}
设置同步状态的值。此操作具有 volatile 写的内存语义。
参数:
newState - 新的状态值
compareAndSetState
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
compareAndSwap即CAS,详细可查找Java并发编程 – Atomic包文章。
如果当前状态值等于预期值,则以原子方式将同步状态设置为给定的更新值。此操作具有 volatile 读和写的内存语义。
如果成功,则返回 true。返回 false 指示实际值与预期值不相等。
tryAcquire
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
试图在独占模式下获取对象状态。此方法应该查询是否允许它在独占模式下获取对象状态,如果允许,则获取它。
此方法总是由执行 acquire 的线程来调用。如果此方法报告失败,则 acquire 方法可以将线程加入队列(如果还没有将它加入队列),直到获得其他某个线程释放了该线程的信号。可以用此方法来实现 lock.tryLock()方法。默认实现将抛出UnsupportedOperationException。
如果成功,则返回 true。在成功的时候,此对象已经被获取。
tryRelease
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
试图设置状态来反映独占模式下的一个释放。
此方法总是由正在执行释放的线程调用。
默认实现将抛出 UnsupportedOperationException。
tryAcquireShared
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
试图在共享模式下获取对象状态。此方法应该查询是否允许它在共享模式下获取对象状态,如果允许,则获取它。
此方法总是由执行 acquire 线程来调用。如果此方法报告失败,则 acquire 方法可以将线程加入队列(如果还没有将它加入队列),直到获得其他某个线程释放了该线程的信号。
默认实现将抛出 UnsupportedOperationException。
tryReleaseShared
protected boolean tryReleaseShared(int arg) {
throw new UnsupportedOperationException();
}
试图设置状态来反映共享模式下的一个释放。
此方法总是由正在执行释放的线程调用。
默认实现将抛出 UnsupportedOperationException。
参数:
arg - release 参数。该值总是传递给 release 方法的那个值,或者是因某个条件等待而保存在条目上的当前状态值。该值是不间断的,并且可以表示任何内容。
返回:
如果此对象现在处于完全释放状态,从而使正在等待的线程都可以试图获得此对象,则返回 true;否则返回 false
抛出:
IllegalMonitorStateException - 如果正在进行的释放操作将在非法状态下放置此同步器。必须以一致的方式抛出此异常,以便同步正确运行
UnsupportedOperationException - 如果不支持共享模式
isHeldExclusively
protected boolean isHeldExclusively() {
throw new UnsupportedOperationException();
}
如果对于当前(正调用的)线程,同步是以独占方式进行的,则返回 true。此方法是在每次调用非等待 AbstractQueuedSynchronizer.ConditionObject 方法时调用的。(等待方法则调用 release(int)。)
默认实现将抛出 UnsupportedOperationException。此方法只是 AbstractQueuedSynchronizer.ConditionObject 方法内进行内部调用,因此,如果不使用条件,则不需要定义它。
返回:
如果同步是以独占方式进行的,则返回 true;其他情况则返回 false
抛出:
UnsupportedOperationException - 如果不支持这些条件
acquire
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
以独占模式获取对象,忽略中断。通过至少调用一次 tryAcquire(int) 来实现此方法,并在成功时返回。否则在成功之前,一直调用 tryAcquire(int) 将线程加入队列,线程可能重复被阻塞或不被阻塞。可以使用此方法来实现 Lock.lock() 方法。
参数:
arg - acquire 参数。此值被传送给 tryAcquire(int),但它是不间断的,并且可以表示任何内容。
acquireInterruptibly
public final void acquireInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
以独占模式获取对象,如果被中断则中止。通过先检查中断状态,然后至少调用一次 tryAcquire(int) 来实现此方法,并在成功时返回。否则在成功之前,或者线程被中断之前,一直调用 tryAcquire(int) 将线程加入队列,线程可能重复被阻塞或不被阻塞。可以使用此方法来实现 Lock.lockInterruptibly() 方法。
参数:
arg - acquire 参数。此值被传送给 tryAcquire(int),但它是不间断的,并且可以表示任何内容。
抛出:
InterruptedException - 如果当前线程被中断
tryAcquireNanos
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout);
}
试图以独占模式获取对象,如果被中断则中止,如果到了给定超时时间,则会失败。通过先检查中断状态,然后至少调用一次 tryAcquire(int) 来实现此方法,并在成功时返回。否则,在成功之前、线程被中断之前或者到达超时时间之前,一直调用 tryAcquire(int) 将线程加入队列,线程可能重复被阻塞或不被阻塞。可以用此方法来实现 Lock.tryLock(long, TimeUnit) 方法。
参数:
arg - acquire 参数。此值被传送给 tryAcquire(int),但它是不间断的,并且可以表示任何内容。
nanosTimeout - 等待的最长时间,以毫微秒为单位
返回:
如果获取对象,则返回 true,如果超时,则返回 false
抛出:
InterruptedException - 如果当前线程被中断
release
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
以独占模式释放对象。如果 tryRelease(int) 返回 true,则通过消除一个或多个线程的阻塞来实现此方法。可以使用此方法来实现 Lock.unlock() 方法
参数:
arg - release 参数。此值被传送给 tryRelease(int),但它是不间断的,并且可以表示任何内容。
返回:
从 tryRelease(int) 返回的值
acquireShared
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
以共享模式获取对象,忽略中断。通过至少先调用一次 tryAcquireShared(int) 来实现此方法,并在成功时返回。否则在成功之前,一直调用 tryAcquireShared(int) 将线程加入队列,线程可能重复被阻塞或不被阻塞。
参数:
arg - acquire 参数。此值被传送给 tryAcquireShared(int),但它是不间断的,并且可以表示任何内容。
acquireSharedInterruptibly
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
以共享模式获取对象,如果被中断则中止。通过先检查中断状态,然后至少调用一次 tryAcquireShared(int) 来实现此方法,并在成功时返回。否则在成功或线程被中断之前,一直调用 tryAcquireShared(int) 将线程加入队列,线程可能重复被阻塞或不被阻塞。
参数:
arg - acquire 参数。此值被传送给 tryAcquireShared(int),但它是不间断的,并且可以表示任何内容。
抛出:
InterruptedException - 如果当前线程被中断
tryAcquireSharedNanos
public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquireShared(arg) >= 0 ||
doAcquireSharedNanos(arg, nanosTimeout);
}
试图以共享模式获取对象,如果被中断则中止,如果到了给定超时时间,则会失败。通过先检查中断状态,然后至少调用一次 tryAcquireShared(int) 来实现此方法,并在成功时返回。否则在成功之前、线程被中断之前或者到达超时时间之前,一直调用 tryAcquireShared(int) 将线程加入队列,线程可能重复被阻塞或不被阻塞。
参数:
arg - acquire 参数。此值被传送给 tryAcquireShared(int),但它是不间断的,并且可以表示任何内容。
nanosTimeout - 等待的最长时间,以毫微秒为单位
返回:
如果获取对象,则返回 true,如果超时,则返回 false
抛出:
InterruptedException - 如果当前线程被中断
releaseShared
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
以共享模式释放对象。如果 tryReleaseShared(int) 返回 true,则通过消除一个或多个线程的阻塞来实现该方法。
参数:
arg - release 参数。此值被传送给 tryReleaseShared(int),但它是不间断的,并且可以表示任何内容。
返回:
从 tryReleaseShared(int) 中返回的值
hasQueuedThreads
public final boolean hasQueuedThreads() {
return head != tail;
}
查询是否有正在等待获取的任何线程。注意,随时可能因为中断和超时而导致取消操作,返回 true 并不能保证其他任何线程都将获取对象。
在此实现中,该操作是以固定时间返回的。
返回:
如果可能有其他线程正在等待获取锁,则返回 true。
hasContended
public final boolean hasContended() {
return head != null;
}
查询是否其他线程也曾争着获取此同步器;也就是说,是否某个 acquire 方法已经阻塞。
在此实现中,该操作是以固定时间返回的。
返回:
如果曾经出现争用,则返回 true
getFirstQueuedThread
private Thread fullGetFirstQueuedThread() {
/*
* The first node is normally head.next. Try to get its
* thread field, ensuring consistent reads: If thread
* field is nulled out or s.prev is no longer head, then
* some other thread(s) concurrently performed setHead in
* between some of our reads. We try this twice before
* resorting to traversal.
*/
Node h, s;
Thread st;
if (((h = head) != null && (s = h.next) != null &&
s.prev == head && (st = s.thread) != null) ||
((h = head) != null && (s = h.next) != null &&
s.prev == head && (st = s.thread) != null))
return st;
/*
* Head's next field might not have been set yet, or may have
* been unset after setHead. So we must check to see if tail
* is actually first node. If not, we continue on, safely
* traversing from tail back to head to find first,
* guaranteeing termination.
*/
Node t = tail;
Thread firstThread = null;
while (t != null && t != head) {
Thread tt = t.thread;
if (tt != null)
firstThread = tt;
t = t.prev;
}
return firstThread;
}
返回队列中第一个(等待时间最长的)线程,如果目前没有将任何线程加入队列,则返回 null.
在此实现中,该操作是以固定时间返回的,但是,如果其他线程目前正在并发修改该队列,则可能出现循环争用。
返回:
队列中第一个(等待时间最长的)线程,如果目前没有将任何线程加入队列,则返回 null
isQueued
public final boolean isQueued(Thread thread) {
if (thread == null)
throw new NullPointerException();
for (Node p = tail; p != null; p = p.prev)
if (p.thread == thread)
return true;
return false;
}
如果给定线程的当前已加入队列,则返回 true。
该实现将遍历队列,以确定给定线程是否存在。
参数:
thread - 线程
返回:
如果给定线程在队列中,则返回 true
抛出:
NullPointerException - 如果 thread 为 null
getQueueLength
public final int getQueueLength()
返回等待获取的线程数估计值。该值只能是一个估计值,因为在此方法遍历内部数据结构时,线程的数量可能发生大的变化。该方法是为用来监视系统状态而设计的,不是为同步控制设计的。
返回:
正等待获取的线程估计数
getQueuedThreads
public final Collection<Thread> getQueuedThreads() {
ArrayList<Thread> list = new ArrayList<Thread>();
for (Node p = tail; p != null; p = p.prev) {
Thread t = p.thread;
if (t != null)
list.add(t);
}
return list;
}
返回包含可能正在等待获取的线程 collection。因为在构造该结果时,实际线程 set 可能发生大的变化,所以返回的 collection 只是尽最大努力获得的一个估计值。返回 collection 的元素并不是以特定顺序排列的。此方法是为促进子类的构造而设计的,这些子类提供了大量的监视设施。
返回:
线程的 collection
getExclusiveQueuedThreads
public final Collection<Thread> getExclusiveQueuedThreads()
返回包含可能正以独占模式等待获取的线程 collection。此方法具有与 getQueuedThreads() 相同的属性,除了它只返回那些因独占获取而等待的线程。
返回:
线程的 collection
getSharedQueuedThreads
public final Collection<Thread> getExclusiveQueuedThreads() {
ArrayList<Thread> list = new ArrayList<Thread>();
for (Node p = tail; p != null; p = p.prev) {
if (!p.isShared()) {
Thread t = p.thread;
if (t != null)
list.add(t);
}
}
return list;
}
返回包含可能正以共享模式等待获取的线程 collection。此方法具有与 getQueuedThreads() 相同的属性,除了它只返回那些因共享获取而等待的线程。
返回:
线程的 collection
toString
public String toString() {
int s = getState();
String q = hasQueuedThreads() ? "non" : "";
return super.toString() +
"[State = " + s + ", " + q + "empty queue]";
}
返回标识此同步器及其状态的字符串。此状态被括号括起来,它包括字符串 “State =”,后面是 getState() 的当前值,再后面是 “nonempty” 或 “empty”,这取决于队列是否为空。
覆盖:
类 Object 中的 toString
返回:
标识此同步器及其状态的字符串
owns
public final boolean owns(ConditionObject condition) {
return condition.isOwnedBy(this);
}
查询给定的 ConditionObject 是否使用了此同步器作为其锁。
参数:
condition - 条件
返回:
如果具备此条件,则返回 true
抛出:
NullPointerException - 如果 condition 为 null
hasWaiters
public final boolean hasWaiters(ConditionObject condition) {
if (!owns(condition))
throw new IllegalArgumentException("Not owner");
return condition.hasWaiters();
}
查询是否有线程正在等待给定的、与此同步器相关的条件。注意,因为随时可能发生超时和中断,所以返回 true 并不能保证将来某个 signal 将唤醒任何线程。此方法主要是为了监视系统状态而设计的。
参数:
condition - 条件
返回:
如果有正在等待的线程,则返回 true
抛出:
IllegalMonitorStateException - 如果不进行独占同步
IllegalArgumentException - 如果给定的 condition 与此同步器无关
NullPointerException - 如果 condition 为 null
getWaitQueueLength
public final int getWaitQueueLength(ConditionObject condition) {
if (!owns(condition))
throw new IllegalArgumentException("Not owner");
return condition.getWaitQueueLength();
}
返回正在等待与此同步器有关的给定条件的线程数估计值。注意,因为随时可能发生超时和中断,所以估计值只是实际等待线程的数量上限。此方法是为监视系统状态而设计的,不是为同步控制设计的。
参数:
condition - 条件
返回:
等待线程的估计数
抛出:
IllegalMonitorStateException - 如果不进行独占同步
IllegalArgumentException - 如果给定的 condition 与此同步器无关
NullPointerException - 如果 condition 为 null
getWaitingThreads
public final Collection<Thread> getWaitingThreads(ConditionObject condition) {
if (!owns(condition))
throw new IllegalArgumentException("Not owner");
return condition.getWaitingThreads();
}
返回一个 collection,其中包含可能正在等待与此同步器有关的给定条件的那些线程。因为在构造该结果时,实际线程 set 可能发生大的变化,所以返回的 collection 只是尽最大努力获得的一个估计值。返回 collection 的元素并不是以特定顺序排列的。
参数:
condition - 条件
返回:
线程的 collection
抛出:
IllegalMonitorStateException - 如果不进行独占同步
IllegalArgumentException - 如果给定的 condition 与此同步器无关
NullPointerException - 如果 condition 为 null
3. 源码详解
为什么把这些单独拿出来呢,个人认为它是上面方法实现的底层,它提现了对队列和链表的详细处理,也是上面方法的核心处理代码。我只拿了几个最核心的部分,看懂都好。想详细查看请自己查找源码。
为什么说它需要队列和链表的基础呢?
我们来看下AQS的数据结构Node结点的相关代码
addWaiter方法
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// 尝试快速方式直接放到队尾。
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
为当前线程创建并排队节点,其实就是增长链表。详细请参考Java AbstractQueuedSynchronizer源码阅读2-addWaiter()
enq方法
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
将节点插入队列,如有必要进行初始化。
因为上面两个方法都调用了compareAndSetTail方法,所以都有在链表后加结点的操作。
unparkSuccessor方法
private void unparkSuccessor(Node node) {
//这里,node一般为当前线程所在的结点。
int ws = node.waitStatus;
if (ws < 0)//置零当前线程所在的结点状态,允许失败。
compareAndSetWaitStatus(node, ws, 0);
//找到下一个需要唤醒的结点s
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)//从这里可以看出,<=0的结点,都是还有效的结点。
s = t;
}
if (s != null)
//唤醒
LockSupport.unpark(s.thread);
}
如果结点后还有结点的话,唤醒节点的后继者。
cancelAcquire方法
private void cancelAcquire(Node node) {
// 如果节点不存在
if (node == null)
return;
node.thread = null;
// 获取到前一个结点
Node pred = node.prev;
//一直获取到队列中的最后一个结点
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
// predNext is the apparent node to unsplice. CASes below will
// fail if not, in which case, we lost race vs another cancel
// or signal, so no further action is necessary.
Node predNext = pred.next;
// Can use unconditional write instead of CAS here.
// After this atomic step, other Nodes can skip past us.
// Before, we are free of interference from other threads.
node.waitStatus = Node.CANCELLED;
// If we are the tail, remove ourselves.
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);
} else {
// If successor needs signal, try to set pred's next-link
// so it will get one. Otherwise wake it up to propagate.
int ws;
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
Node next = node.next;
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next);
} else {
unparkSuccessor(node);
}
node.next = node; // help GC
}
}
取消获取锁。emmm,自己静下心去分析。它判断了所有的结点可能出现的情况,来对链表的最后一个结点进行操作。详细请参考文章:Java AbstractQueuedSynchronizer源码阅读3-cancelAcquire()
11. Java并发编程-AQS入门&实现可重入锁
Java并发编程 – AQS可能会看的一脸懵逼,今天实战一个项目练手AQS
MyAQSLock.java
/**
* Created by Fant.J.
*/
public class MyAQSLock implements Lock {
private Helper helper = new Helper();
private class Helper extends AbstractQueuedSynchronizer {
@Override
protected boolean tryAcquire(int arg) {
// 如果第一个线程进来,可以拿到锁,因此我们可以返回true
// 如果第二个线程进来,则拿不到锁,返回false。有种特例,如果当前进来的线程和当前保存的线程是同一个线程,则可以拿到锁,但是有代价,要更新状态值
// 如何判断是第一个线程进来还是其他线程进来?
//获取状态值
int state = getState();
Thread t = Thread.currentThread();
//如果状态=0,那就是第一个线程
if (state == 0) {
if (compareAndSetState(0, arg)) {
//设置当前线程为独占锁线程
setExclusiveOwnerThread(t);
return true;
}
} else if (getExclusiveOwnerThread() == t) {
setState(state + 1);
return true;
}
return false;
}
@Override
protected boolean tryRelease(int arg) {
// 锁的获取和释放肯定是一一对应的,那么调用此方法的线程一定是当前线程
//获取当前线程,如果不等于独占锁的线程
if (Thread.currentThread() != getExclusiveOwnerThread()) {
throw new RuntimeException();
}
int state = getState() - arg;
boolean flag = false;
if (state == 0) {
setExclusiveOwnerThread(null);
flag = true;
}
setState(state);
return flag;
}
Condition newCondition() {
return new ConditionObject();
}
}
@Override
public void lock() {
helper.acquire(1);
}
@Override
public void lockInterruptibly() throws InterruptedException {
helper.acquireInterruptibly(1);
}
@Override
public boolean tryLock() {
return helper.tryAcquire(1);
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return helper.tryAcquireNanos(1, unit.toNanos(time));
}
@Override
public void unlock() {
helper.release(1);
}
@Override
public Condition newCondition() {
return helper.newCondition();
}
}
从上往下分析,首先继承Lock接口,然后定义一个子类为非公共内部帮助器类Helper类,Helper类继承AQS,重写它的tryAcquire和tryRelease方法。作为锁的获取和释放。然后填充Lock的子类实现。(为什么Lock子类方法里传值都是1呢,因为AQS源码就是这样,1一路传到底),注释还算详细,就不在这多说了。
/**
* Created by Fant.J.
*/
class TestAQS {
private int value;
private MyAQSLock myAQSLock = new MyAQSLock();
public int next(){
myAQSLock.lock();
try {
Thread.sleep(300);
return value++;
} catch (InterruptedException e) {
throw new RuntimeException();
}finally {
myAQSLock.unlock();
}
}
public void a() {
myAQSLock.lock();
System.out.println("a");
b();
myAQSLock.unlock();
}
public void b() {
myAQSLock.lock();
System.out.println("b");
myAQSLock.unlock();
}
public static void main(String[] args) {
TestAQS test = new TestAQS();
new Thread(new Runnable() {
@Override
public void run() {
test.a();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
while (true){
System.out.println(Thread.currentThread().getName()+" "+test.next());
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
while (true){
System.out.println(Thread.currentThread().getName()+" "+test.next());
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
while (true){
System.out.println(Thread.currentThread().getName()+" "+test.next());
}
}
}).start();
}
}
开了四个线程,一个线程去跑a方法,a方法中调用b方法,a、b方法调用之前都有加锁,之后有解锁,这个线程用来做可重入锁的测试,其他三个线程是测试线程安全。
结果图
12. Java并发编程-Condition
因为wait()、notify()是和synchronized配合使用的,因此如果使用了显示锁Lock,就不能用了。所以显示锁要提供自己的等待/通知机制,Condition应运而生。
Condition中的
await()
方法相当于Object的wait()
方法,Condition中的signal()
方法相当于Object的notify()
方法,Condition中的signalAll()
相当于Object的notifyAll()
方法。不同的是,Object中的wait(),notify(),notifyAll()
方法是和"同步锁"
(synchronized关键字)捆绑使用的;而Condition是需要与"互斥锁"/"共享锁"
捆绑使用的。
1. 函数列表
-
void await() throws InterruptedException
当前线程进入等待状态,直到被通知(signal)或者被中断时,当前线程进入运行状态,从await()返回; -
void awaitUninterruptibly()
当前线程进入等待状态,直到被通知,对中断不做响应; -
long awaitNanos(long nanosTimeout) throws InterruptedException
在接口1的返回条件基础上增加了超时响应,返回值表示当前剩余的时间,如果在nanosTimeout之前被唤醒,返回值 = nanosTimeout - 实际消耗的时间,返回值 <= 0表示超时; -
boolean await(long time, TimeUnit unit) throws InterruptedException
同样是在接口1的返回条件基础上增加了超时响应,与接口3不同的是:
可以自定义超时时间单位;
返回值返回true/false,在time之前被唤醒,返回true,超时返回false。 -
boolean awaitUntil(Date deadline) throws InterruptedException
当前线程进入等待状态直到将来的指定时间被通知,如果没有到指定时间被通知返回true,否则,到达指定时间,返回false; -
void signal()
唤醒一个等待在Condition上的线程; -
void signalAll()
唤醒等待在Condition上所有的线程。
2. 具体实现
看到电脑上当初有帮人做过一道题,就拿它做实例演示:
编写一个Java应用程序,要求有三个进程:student1,student2,teacher,其中线程student1准备“睡”1分钟后再开始上课,线程student2准备“睡”5分钟后再开始上课。Teacher在输出4句“上课”后,“唤醒”了休眠的线程student1;线程student1被“唤醒”后,负责再“唤醒”休眠的线程student2.
2.1 实现一:
基于Object和synchronized实现。
package com.fantJ.bigdata;
/**
* Created by Fant.J.
* 2018/7/2 16:36
*/
public class Ten {
static class Student1{
private boolean student1Flag = false;
public synchronized boolean isStudent1Flag() {
System.out.println("学生1开始睡觉1min");
if (!this.student1Flag){
try {
System.out.println("学生1睡着了");
wait(1*1000*60);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("学生1被唤醒");
return student1Flag;
}
public synchronized void setStudent1Flag(boolean student1Flag) {
this.student1Flag = student1Flag;
notify();
}
}
static class Student2{
private boolean student2Flag = false;
public synchronized boolean isStudent2Flag() {
System.out.println("学生2开始睡觉5min");
if (!this.student2Flag){
try {
System.out.println("学生2睡着了");
wait(5*1000*60);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("学生2被唤醒");
return student2Flag;
}
public synchronized void setStudent2Flag(boolean student2Flag) {
notify();
this.student2Flag = student2Flag;
}
}
static class Teacher{
private boolean teacherFlag = true;
public synchronized boolean isTeacherFlag() {
if (!this.teacherFlag){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("老师准备吼着要上课");
return teacherFlag;
}
public synchronized void setTeacherFlag(boolean teacherFlag) {
this.teacherFlag = teacherFlag;
notify();
}
}
public static void main(String[] args) {
Student1 student1 = new Student1();
Student2 student2 = new Student2();
Teacher teacher = new Teacher();
Thread t = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0;i<4;i++){
System.out.println("上课");
}
teacher.isTeacherFlag();
System.out.println("学生1被吵醒了,1s后反应过来");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
student1.setStudent1Flag(true);
}
});
Thread s1 = new Thread(new Runnable() {
@Override
public void run() {
student1.isStudent1Flag();
System.out.println("准备唤醒学生2,唤醒需要1s");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
student2.setStudent2Flag(true);
}
});
Thread s2 = new Thread(new Runnable() {
@Override
public void run() {
student2.isStudent2Flag();
}
});
s1.start();
s2.start();
t.start();
}
}
当然,用notifyAll
可能会用更少的代码,这种实现方式虽然复杂,单性能上会比使用notifyAll()
要强很多,因为没有锁争夺导致的资源浪费。但是可以看到,代码很复杂,实例与实例之间也需要保证很好的隔离。
2.2 实现二:
基于Condition、ReentrantLock实现。
public class xxx{
private int signal = 0;
public Lock lock = new ReentrantLock();
Condition teacher = lock.newCondition();
Condition student1 = lock.newCondition();
Condition student2 = lock.newCondition();
public void teacher(){
lock.lock();
while (signal != 0){
try {
teacher.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("老师叫上课");
signal++;
student1.signal();
lock.unlock();
}
public void student1(){
lock.lock();
while (signal != 1){
try {
student1.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("学生1醒了,准备叫醒学生2");
signal++;
student2.signal();
lock.unlock();
}
public void student2(){
lock.lock();
while (signal != 2){
try {
student2.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("学生2醒了");
signal=0;
teacher.signal();
lock.unlock();
}
public static void main(String[] args) {
ThreadCommunicate2 ten = new ThreadCommunicate2();
new Thread(() -> ten.teacher()).start();
new Thread(() -> ten.student1()).start();
new Thread(() -> ten.student2()).start();
}
}
Condition
依赖于Lock
接口,生成一个Condition的基本代码是lock.newCondition()
调用Condition
的await()
和signal()
方法,都必须在lock
保护之内,就是说必须在lock.lock()
和lock.unlock
之间才可以使用。
可以观察到,我取消了synchronized
方法关键字,在每个加锁的方法前后分别加了lock.lock(); lock.unlock();
来获取/施放锁,并且在释放锁之前施放想要施放的Condition
对象。同样的,我们使用signal
来完成线程间的通信。
3. Condition实现有界队列
为什么要用它来实现有界队列呢,因为我们可以利用Condition来实现阻塞(当队列空或者满的时候)。这就为我们减少了很多的麻烦。
public class MyQueue<E> {
private Object[] objects;
private Lock lock = new ReentrantLock();
private Condition addCDT = lock.newCondition();
private Condition rmCDT = lock.newCondition();
private int addIndex;
private int rmIndex;
private int queueSize;
MyQueue(int size){
objects = new Object[size];
}
public void add(E e){
lock.lock();
while (queueSize == objects.length){
try {
addCDT.await();
} catch (InterruptedException e1) {
e1.printStackTrace();
}
}
objects[addIndex] = e;
System.out.println("添加了数据"+"Objects["+addIndex+"] = "+e);
if (++addIndex == objects.length){
addIndex = 0;
}
queueSize++;
rmCDT.signal();
lock.unlock();
}
public Object remove(){
lock.lock();
while (queueSize == 0){
try {
System.out.println("队列为空");
rmCDT.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Object temp = objects[rmIndex];
objects[rmIndex] = null;
System.out.println("移除了数据"+"Objects["+rmIndex+"] = null");
if (++rmIndex == objects.length){
rmIndex = 0;
}
queueSize--;
addCDT.signal();
lock.unlock();
return temp;
}
public void foreach(E e){
if (e instanceof String){
Arrays.stream(objects).map(obj->{
if (obj == null){
obj = " ";
}
return obj;
}).map(Object::toString).forEach(System.out::println);
}
if (e instanceof Integer){
Arrays.stream(objects).map(obj -> {
if (obj == null ){
obj = 0;
}
return obj;
}).map(object -> Integer.valueOf(object.toString())).forEach(System.out::println);
}
}
}
add
方法就是往队列中添加数据。
remove
是从队列中按FIFO移除数据。
foreach
方法是一个观察队列内容的工具方法,很容易看出,它是用来遍历的。
public static void main(String[] args) {
MyQueue<Integer> myQueue = new MyQueue<>(5);
myQueue.add(5);
myQueue.add(4);
myQueue.add(3);
// myQueue.add(2);
// myQueue.add(1);
myQueue.remove();
myQueue.foreach(5);
}
添加了数据Objects[0] = 5
添加了数据Objects[1] = 4
添加了数据Objects[2] = 3
移除了数据Objects[0] = null
0
4
3
0
0
4. 源码分析
ReentrantLock.class
public Condition newCondition() {
return sync.newCondition();
}
sync溯源:
private final Sync sync;
Sync类中有一个newCondition()方法:
final ConditionObject newCondition() {
return new ConditionObject();
}
public class ConditionObject implements Condition, java.io.Serializable {
/** First node of condition queue. */
private transient Node firstWaiter;
/** Last node of condition queue. */
private transient Node lastWaiter;
}
public interface Condition {
void await() throws InterruptedException;
void awaitUninterruptibly();
long awaitNanos(long nanosTimeout) throws InterruptedException;
boolean await(long time, TimeUnit unit) throws InterruptedException;
boolean awaitUntil(Date deadline) throws InterruptedException;
void signal();
void signalAll();
await源码:
public final void await() throws InterruptedException {
// 1.如果当前线程被中断,则抛出中断异常
if (Thread.interrupted())
throw new InterruptedException();
// 2.将节点加入到Condition队列中去,这里如果lastWaiter是cancel状态,那么会把它踢出Condition队列。
Node node = addConditionWaiter();
// 3.调用tryRelease,释放当前线程的锁
long savedState = fullyRelease(node);
int interruptMode = 0;
// 4.为什么会有在AQS的等待队列的判断?
// 解答:signal操作会将Node从Condition队列中拿出并且放入到等待队列中去,在不在AQS等待队列就看signal是否执行了
// 如果不在AQS等待队列中,就park当前线程,如果在,就退出循环,这个时候如果被中断,那么就退出循环
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// 5.这个时候线程已经被signal()或者signalAll()操作给唤醒了,退出了4中的while循环
// 自旋等待尝试再次获取锁,调用acquireQueued方法
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null)
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
-
将当前线程加入
Condition
锁队列。特别说明的是,这里不同于AQS
的队列,这里进入的是Condition
的FIFO
队列。 -
释放锁。这里可以看到将锁释放了,否则别的线程就无法拿到锁而发生死锁。
-
自旋
(while
)挂起,直到被唤醒(signal
把他重新放回到AQS的等待队列)或者超时或者CACELLED等。 -
获取锁
(acquireQueued
)。并将自己从Condition
的FIFO
队列中释放,表明自己不再需要锁(我已经拿到锁了)。
signal()源码
public final void signal() {
if (!isHeldExclusively())
//如果同步状态不是被当前线程独占,直接抛出异常。从这里也能看出来,Condition只能配合独占类同步组件使用。
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
//通知等待队列队首的节点。
doSignal(first);
}
private void doSignal(Node first) {
do {
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) && //transferForSignal方法尝试唤醒当前节点,如果唤醒失败,则继续尝试唤醒当前节点的后继节点。
(first = firstWaiter) != null);
}
final boolean transferForSignal(Node node) {
//如果当前节点状态为CONDITION,则将状态改为0准备加入同步队列;如果当前状态不为CONDITION,说明该节点等待已被中断,则该方法返回false,doSignal()方法会继续尝试唤醒当前节点的后继节点
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
/*
* Splice onto queue and try to set waitStatus of predecessor to
* indicate that thread is (probably) waiting. If cancelled or
* attempt to set waitStatus fails, wake up to resync (in which
* case the waitStatus can be transiently and harmlessly wrong).
*/
Node p = enq(node); //将节点加入同步队列,返回的p是节点在同步队列中的先驱节点
int ws = p.waitStatus;
//如果先驱节点的状态为CANCELLED(>0) 或设置先驱节点的状态为SIGNAL失败,那么就立即唤醒当前节点对应的线程,线程被唤醒后会执行acquireQueued方法,该方法会重新尝试将节点的先驱状态设为SIGNAL并再次park线程;如果当前设置前驱节点状态为SIGNAL成功,那么就不需要马上唤醒线程了,当它的前驱节点成为同步队列的首节点且释放同步状态后,会自动唤醒它。
//其实笔者认为这里不加这个判断条件应该也是可以的。只是对于CAS修改前驱节点状态为SIGNAL成功这种情况来说,如果不加这个判断条件,提前唤醒了线程,等进入acquireQueued方法了节点发现自己的前驱不是首节点,还要再阻塞,等到其前驱节点成为首节点并释放锁时再唤醒一次;而如果加了这个条件,线程被唤醒的时候它的前驱节点肯定是首节点了,线程就有机会直接获取同步状态从而避免二次阻塞,节省了硬件资源。
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
signal
就是唤醒Condition
队列中的第一个非CANCELLED节点线程,而signalAll就是唤醒所有非CANCELLED
节点线程,本质是将节点从Condition
队列中取出来一个还是所有节点放到AQS
的等待队列。尽管所有Node
可能都被唤醒,但是要知道的是仍然只有一个线程能够拿到锁,其它没有拿到锁的线程仍然需要自旋等待,就上上面提到的第4步(acquireQueued
)。
实现过程概述
我们知道
Lock的本质是AQS
,AQS
自己维护的队列是当前等待资源的队列,AQS
会在资源被释放后,依次唤醒队列中从前到后的所有节点,使他们对应的线程恢复执行,直到队列为空。而Condition
自己也维护了一个队列,该队列的作用是维护一个等待signal
信号的队列。但是,两个队列的作用不同的,事实上,每个线程也仅仅会同时存在以上两个队列中的一个,流程是这样的:
- 线程1调用
reentrantLock.lock
时,尝试获取锁。如果成功,则返回,从AQS
的队列中移除线程;否则阻塞,保持在AQS
的等待队列中。 - 线程1调用
await
方法被调用时,对应操作是被加入到Condition
的等待队列中,等待signal信号;同时释放锁。 - 锁被释放后,会唤醒AQS队列中的头结点,所以线程2会获取到锁。
- 线程2调用
signal
方法,这个时候Condition
的等待队列中只有线程1一个节点,于是它被取出来,并被加入到AQS的等待队列中。注意,这个时候,线程1 并没有被唤醒,只是被加入AQS等待队列。 signal
方法执行完毕,线程2调用unLock()
方法,释放锁。这个时候因为AQS中只有线程1,于是,线程1被唤醒,线程1恢复执行。
所以:
发送signal
信号只是将Condition
队列中的线程加到AQS
的等待队列中。只有到发送signal
信号的线程调用reentrantLock.unlock()
释放锁后,这些线程才会被唤醒。
可以看到,整个协作过程是靠结点在AQS
的等待队列和Condition
的等待队列中来回移动实现的,Condition
作为一个条件类,很好的自己维护了一个等待信号的队列,并在适时的时候将结点加入到AQS
的等待队列中来实现的唤醒操作。
Condition等待通知的本质请参考:https://www.cnblogs.com/sheeva/p/6484224.html
13. Java多线程-互斥锁-共享锁-读写锁-快速入门
什么是互斥锁?
在访问共享资源之前对进行加锁操作,在访问完成之后进行解锁操作。 加锁后,任何其他试图再次加锁的线程会被阻塞,直到当前进程解锁。
如果解锁时有一个以上的线程阻塞,那么所有该锁上的线程都被编程就绪状态, 第一个变为就绪状态的线程又执行加锁操作,那么其他的线程又会进入等待。 在这种方式下,只有一个线程能够访问被互斥锁保护的资源。
什么是共享锁?
互斥锁要求只能有一个线程访问被保护的资源,共享锁从字面来看也即是允许多个线程共同访问资源。
什么是读写锁?
读写锁既是互斥锁,又是共享锁,read模式是共享,write是互斥(排它锁)的。
读写锁有三种状态:读加锁状态、写加锁状态和不加锁状态
一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁。
用ReentrantLock手动实现一个简单的读写锁。
MyReadWriteLock.java
/**
* Created by Fant.J.
*/
public class MyReadWriteLock {
private Map<String,Object> map = new HashMap<>();
private ReadWriteLock rwl = new ReentrantReadWriteLock();
private Lock r = rwl.readLock();
private Lock w = rwl.writeLock();
public Object get(String key){
try {
r.lock();
System.out.println(Thread.currentThread().getName()+"read 操作执行");
Thread.sleep(500);
return map.get(key);
} catch (InterruptedException e) {
e.printStackTrace();
return null;
} finally {
r.unlock();
System.out.println(Thread.currentThread().getName()+"read 操作结束");
}
}
public void put(String key,Object value){
try {
w.lock();
System.out.println(Thread.currentThread().getName()+"write 操作执行");
Thread.sleep(500);
map.put(key,value);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
w.unlock();
System.out.println(Thread.currentThread().getName()+"write 操作结束");
}
}
}
测试读读共享(不互斥)
/**
* Created by Fant.J.
*/
public class Test {
public static void main(String[] args) {
MyReadWriteLock myReadWriteLock = new MyReadWriteLock();
myReadWriteLock.put("a","fantj_a");
//读读不互斥(共享)
//读写互斥
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(myReadWriteLock.get("a"));
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(myReadWriteLock.get("a"));
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(myReadWriteLock.get("a"));
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(myReadWriteLock.get("a"));
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(myReadWriteLock.get("a"));
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(myReadWriteLock.get("a"));
}
}).start();
}
}
mainwrite 操作执行
mainwrite 操作结束
Thread-1read 操作执行
Thread-2read 操作执行
Thread-0read 操作执行
Thread-3read 操作执行
Thread-4read 操作执行
Thread-5read 操作执行
Thread-1read 操作结束
Thread-0read 操作结束
Thread-2read 操作结束
Thread-3read 操作结束
fantj_a
fantj_a
fantj_a
fantj_a
Thread-4read 操作结束
fantj_a
Thread-5read 操作结束
fantj_a
可以看出,中间有很多read操作是并发进行的。
那么我们再看下写写是否有互斥性:
/**
* 测试 写-写 模式 是互斥的
* Created by Fant.J.
*/
public class TestWriteWrite {
public static void main(String[] args) {
MyReadWriteLock myReadWriteLock = new MyReadWriteLock();
new Thread(new Runnable() {
@Override
public void run() {
myReadWriteLock.put("b","fantj_b");
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
myReadWriteLock.put("b","fantj_b");
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
myReadWriteLock.put("b","fantj_b");
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
myReadWriteLock.put("b","fantj_b");
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
myReadWriteLock.put("b","fantj_b");
}
}).start();
}
}
Thread-0write 操作执行
Thread-1write 操作执行
Thread-0write 操作结束
Thread-1write 操作结束
Thread-2write 操作执行
Thread-2write 操作结束
Thread-3write 操作执行
Thread-3write 操作结束
Thread-4write 操作执行
Thread-4write 操作结束
控制台能明显感觉到线程休息的时间。所以它的写-写操作肯定是互斥的。
最后再看看,写-读 操作是否互斥。
写-读互斥 测试
/**
* 测试 写-读模式 互斥
* Created by Fant.J.
*/
public class TestWriteRead {
public static void main(String[] args) {
MyReadWriteLock myReadWriteLock = new MyReadWriteLock();
//读读不互斥(共享)
//读写互斥
new Thread(new Runnable() {
@Override
public void run() {
myReadWriteLock.put("a","fantj_a");
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(myReadWriteLock.get("a"));
}
}).start();
}
}
Thread-0write 操作执行
Thread-1read 操作执行
Thread-0write 操作结束
Thread-1read 操作结束
fantj_a
控制台可以看到write操作执行后线程被阻塞。直到写释放了锁。
问题分析
问题1:仔细想了想,如果有一种场景,就是用户一直再读,写获取不到锁,那么不就造成脏读吗?
上一章我介绍了公平锁/非公平锁,资源的抢占不就是非公平锁造成的吗,那我们用公平锁把它包装下不就能避免了吗,我做了个简单的实现:(不知道公平锁的可以翻我上章教程)
/**
* 测试 读写锁 的公平锁 实现
* Created by Fant.J.
*/
public class TestReadWriteRead {
public static void main(String[] args) {
ReentrantLock fairLock = new ReentrantLock(true);
MyReadWriteLock myReadWriteLock = new MyReadWriteLock();
myReadWriteLock.put("a","fantj_a");
new Thread(new Runnable() {
@Override
public void run() {
fairLock.lock();
System.out.println(myReadWriteLock.get("a"));
System.out.println("fair队列长度"+fairLock.getQueueLength());
fairLock.unlock();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
fairLock.lock();
System.out.println(myReadWriteLock.get("a"));
System.out.println("fair队列长度"+fairLock.getQueueLength());
fairLock.unlock();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
fairLock.lock();
System.out.println(myReadWriteLock.get("a"));
System.out.println("fair队列长度"+fairLock.getQueueLength());
fairLock.unlock();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
fairLock.lock();
myReadWriteLock.put("a","fantj_a_update");
System.out.println("fair队列长度"+fairLock.getQueueLength());
fairLock.unlock();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
fairLock.lock();
System.out.println(myReadWriteLock.get("a"));
System.out.println("fair队列长度"+fairLock.getQueueLength());
fairLock.unlock();
}
}).start();
}
}
mainwrite 操作执行
mainwrite 操作结束
Thread-0read 操作执行
Thread-0read 操作结束
fantj_a
fair队列长度4
Thread-1read 操作执行
Thread-1read 操作结束
fantj_a
fair队列长度3
Thread-2read 操作执行
Thread-2read 操作结束
fantj_a
fair队列长度2
Thread-3write 操作执行
Thread-3write 操作结束
fair队列长度1
Thread-4read 操作执行
Thread-4read 操作结束
fantj_a_update
fair队列长度0
14. Java多线程-锁降级
锁降级指当前线程把持住写锁再获取到读锁,随后释放先前拥有的写锁的过程。
概念网上有很多,随便一查一大堆,我就不多浪费大家时间。
为什么要锁降级?
主要原因:读锁是共享锁。写锁是排它锁,如果在写锁施放之前施放了写锁,会造成别的线程很快又拿到了写锁,然后阻塞了读锁,造成数据的不可控性(感知不到数据的变化),也造成了不必要的cpu资源浪费,写只需要一个线程来进行,然后共享锁,不需要多线程都去获取这个写锁,如果先施放写锁再获取读锁,后果就是如此。
案例
package com.thread.demotionLock;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* 锁降级
* Created by Fant.J.
*/
public class MyDemotionLock {
private Map<String,Object> map = new HashMap<>();
private ReadWriteLock rwl = new ReentrantReadWriteLock();
private Lock r = rwl.readLock();
private Lock w = rwl.writeLock();
int value = 100;
public void readAndWrite(){
//获取读锁
r.lock();
Object object = map.get("a");
if (object == null){
System.out.println("获取到了空值");
//缓存是空,模拟从新从数据库中获取
//关闭读锁,获取写锁
r.unlock();
w.lock();
map.put("a","Fant.J"+value);
try {
Thread.sleep(1500);
} catch (InterruptedException e) {
e.printStackTrace();
}
//完成写操作后,应该在写锁释放之前获取到读锁
w.unlock();
System.out.println("我要施放锁啦");
r.lock();
}
System.out.println("线程"+Thread.currentThread().getName()+map.get("a"));
r.unlock();
}
public static void main(String[] args) {
MyDemotionLock lock = new MyDemotionLock();
Runnable runnable = () -> {
System.out.println(Thread.currentThread().getName()+"启动");
lock.readAndWrite();
};
Thread[] threadArray = new Thread[10];
for (int i=0; i<10; i++) {
threadArray[i] = new Thread(runnable);
}
for (int i=0; i<10; i++) {
threadArray[i].start();
}
}
}
Thread-0启动
Thread-1启动
Thread-2启动
获取到了空值
获取到了空值
获取到了空值
Thread-3启动
Thread-4启动
Thread-5启动
Thread-6启动
Thread-7启动
Thread-8启动
Thread-9启动
我要施放锁啦
我要施放锁啦
我要施放锁啦
线程Thread-3Fant.J100
线程Thread-6Fant.J100
线程Thread-8Fant.J100
线程Thread-7Fant.J100
线程Thread-5Fant.J100
线程Thread-4Fant.J100
线程Thread-2Fant.J100
线程Thread-0Fant.J100
线程Thread-1Fant.J100
线程Thread-9Fant.J100
下面是把持住写锁并获取读锁后再施放写锁:
r.lock();
System.out.println("我要施放锁啦");
w.unlock();
Thread-0启动
Thread-1启动
Thread-2启动
获取到了空值
获取到了空值
Thread-3启动
Thread-4启动
Thread-5启动
Thread-6启动
Thread-7启动
Thread-8启动
Thread-9启动
我要施放锁啦
线程Thread-2Fant.J100
我要施放锁啦
线程Thread-1Fant.J100
线程Thread-0Fant.J100
线程Thread-3Fant.J100
线程Thread-6Fant.J100
线程Thread-8Fant.J100
线程Thread-5Fant.J100
线程Thread-4Fant.J100
线程Thread-9Fant.J100
线程Thread-7Fant.J100
比较这两种结果,其实他们大概看起来差异不大,都没有影响到读值,但是仔细观察你会发现他们执行的步骤不一样。
代码块一:很容易看出来写锁释放后,读锁全部都在争取资源(从控制台打印顺序可以看到)。
代码块二:我要施放锁啦 -> 线程Thread-2Fant.J100 -> 我要施放锁啦,可以看出打印第一个线程Thread-2Fant.J100
是没有读锁争抢的
质疑?
就算我不使用锁降级,程序的运行结果也是正确的(这是因为锁的机制和volatile关键字相似)。
肯定有人问,看着没啥大的区别啊!
因为我当时就是这样质疑的,后来看了很多个大牛的分析,基本上分为两派。
一派(主流)的描述:没有感知到数据变化的读锁冲进来会继续占用写锁,阻塞已完成写操作的线程去继续获取读锁。
二派的描述:为了性能,因为读锁的抢占必将引起资源分配和判断等操作,降级锁减少了线程的阻塞唤醒,实时连续性更强。
其实我觉得技术没有谁对谁错,只有出发点和自己擅长的领域的不同。
有精力的可以自己追踪源码,分析字节码去深入,以后我会把这部分的研究贴出来。
小小性能的提升,可能对吞吐量就是小几百甚至上千的提升。
15. Java并发编程-公平锁和非公平锁的一些思考
在java的锁机制中,公平和非公平的参考物是什么,个人而言觉得是相对产生的结果而立,简单的来说,如果一个线程组里,能保证每个线程都能拿到锁,那么这个锁就是公平锁。相反,如果保证不了每个线程都能拿到锁,也就是存在有线程饿死,那么这个锁就是非公平锁。本文围绕ReenTrantLock来讲。
实现原理
那如何能保证每个线程都能拿到锁呢,队列FIFO是一个完美的解决方案,也就是先进先出,java的ReenTrantLock也就是用队列实现的公平锁和非公平锁。
在公平的锁中,如果有另一个线程持有锁或者有其他线程在等待队列中等待这个所,那么新发出的请求的线程将被放入到队列中。而非公平锁上,只有当锁被某个线程持有时,新发出请求的线程才会被放入队列中(此时和公平锁是一样的)。所以,它们的差别在于非公平锁会有更多的机会去抢占锁。
公平锁:
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
#hasQueuedPredecessors的实现
public final boolean hasQueuedPredecessors() {
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
非公平锁:
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
示例
公平锁:
package com.thread.fair;
import java.util.concurrent.locks.ReentrantLock;
/**
* Created by Fant.J.
*/
public class MyFairLock {
/**
* true 表示 ReentrantLock 的公平锁
*/
private ReentrantLock lock = new ReentrantLock(true);
public void testFail(){
try {
lock.lock();
System.out.println(Thread.currentThread().getName() +"获得了锁");
}finally {
lock.unlock();
}
}
public static void main(String[] args) {
MyFairLock fairLock = new MyFairLock();
Runnable runnable = () -> {
System.out.println(Thread.currentThread().getName()+"启动");
fairLock.testFail();
};
Thread[] threadArray = new Thread[10];
for (int i=0; i<10; i++) {
threadArray[i] = new Thread(runnable);
}
for (int i=0; i<10; i++) {
threadArray[i].start();
}
}
}
Thread-0启动
Thread-0获得了锁
Thread-1启动
Thread-1获得了锁
Thread-2启动
Thread-2获得了锁
Thread-3启动
Thread-3获得了锁
Thread-4启动
Thread-4获得了锁
Thread-5启动
Thread-5获得了锁
Thread-6启动
Thread-6获得了锁
Thread-8启动
Thread-8获得了锁
Thread-7启动
Thread-7获得了锁
Thread-9启动
Thread-9获得了锁
可以看到,获取锁的线程顺序正是线程启动的顺序。
非公平锁:
/**
* Created by Fant.J.
*/
public class MyNonfairLock {
/**
* false 表示 ReentrantLock 的非公平锁
*/
private ReentrantLock lock = new ReentrantLock(false);
public void testFail(){
try {
lock.lock();
System.out.println(Thread.currentThread().getName() +"获得了锁");
}finally {
lock.unlock();
}
}
public static void main(String[] args) {
MyNonfairLock nonfairLock = new MyNonfairLock();
Runnable runnable = () -> {
System.out.println(Thread.currentThread().getName()+"启动");
nonfairLock.testFail();
};
Thread[] threadArray = new Thread[10];
for (int i=0; i<10; i++) {
threadArray[i] = new Thread(runnable);
}
for (int i=0; i<10; i++) {
threadArray[i].start();
}
}
}
Thread-1启动
Thread-0启动
Thread-0获得了锁
Thread-1获得了锁
Thread-8启动
Thread-8获得了锁
Thread-3启动
Thread-3获得了锁
Thread-4启动
Thread-4获得了锁
Thread-5启动
Thread-2启动
Thread-9启动
Thread-5获得了锁
Thread-2获得了锁
Thread-9获得了锁
Thread-6启动
Thread-7启动
Thread-6获得了锁
Thread-7获得了锁
可以看出非公平锁对锁的获取是乱序的,即有一个抢占锁的过程。
最后
那非公平锁和公平锁适合什么场合使用呢,他们的优缺点又是什么呢?
优缺点:
非公平锁性能高于公平锁性能。首先,在恢复一个被挂起的线程与该线程真正运行之间存在着严重的延迟。而且,非公平锁能更充分的利用cpu的时间片,尽量的减少cpu空闲的状态时间。
使用场景
使用场景的话呢,其实还是和他们的属性一一相关,举个栗子:如果业务中线程占用(处理)时间要远长于线程等待,那用非公平锁其实效率并不明显,但是用公平锁会给业务增强很多的可控制性。
16. Java并发编程-手动实现可重入Lock
Lock就像synchronized块一样是一个线程同步机制。 然而,Lock定比synchronized更灵活、更复杂。
Lock和synchronized块 的区别
- 同步块不保证等待输入它的线程被授予访问权限的顺序。
- 不能将任何参数传递给同步块的条目。
- 同步块必须完全包含在单个方法中。 一个Lock可以在不同的方法中调用lock()和unlock()。
简单例子
Lock lock = new ReentrantLock();
lock.lock();
//要保证线程安全的代码
lock.unlock();
其中,你应该能够猜到,lock() 方法是加锁,unlock()方法是解锁。
Lock接口含有的方法
- lock()
- lockInterruptibly()
- tryLock()
- tryLock(long timeout, TimeUnit timeUnit)
- unlock()
lock()方法锁定Lock实例。 如果锁定实例已被锁定,则线程调用锁定()将被锁定,直到解锁锁定。
lockInterruptibly()方法锁定Lock,除非调用该方法的线程已被中断。如果一个线程被阻塞,等待通过此方法锁定Lock,该线程将被中断,并退出此方法调用。(获取锁的时候可以被中断)
tryLock()方法立即尝试锁定Lock实例。 如果锁定成功则返回true;如果Lock已经被锁定,则返回false。 这个方法永远不会阻塞
tryLock(long timeout,TimeUnit timeUnit)的工作方式与tryLock()方法相似,只是它对超时时间有所规定。
unlock()方法解锁Lock实例。 通常,Lock实现将只允许已锁定Lock的线程调用此方法。 调用此方法的其他线程可能会导致未经检查的异常(RuntimeException)。
ReentrantLock实例
ReentrantLock 可重入锁,是Lock的一个子类。我们这里来使用它实现线程安全编程。
package com.lock;
import com.thread.security.Task;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 重入锁
* Created by Fant.J.
* 2018/3/6 20:09
*/
public class ReentrantLockTest {
public int value = 0;
//实例化重入锁锁
Lock lock = new ReentrantLock();
public int getValue() {
//加锁
lock.lock();
int a = value++;
//消除锁
lock.unlock();
return a;
}
public static void main(String[] args) {
ReentrantLockTest task = new ReentrantLockTest();
new Thread(){
@Override
public void run() {
while (true) {
System.out.println(Thread.currentThread().getName() + " " + task.getValue());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
new Thread(){
@Override
public void run() {
while (true) {
System.out.println(Thread.currentThread().getName() + " " + task.getValue());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
}
}
手写自己的Lock实现类
如果有特殊业务需求,我们也可以重写Lock接口,来打造一个自己的lock锁。
package com.lock;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
/**
* Created by Fant.J.
* 2018/3/6 20:12
*/
public class MyLock implements Lock {
//声明一个判断锁的布尔值
private boolean isLocked = false;
/**
* 必须声明 synchronized 原自行操作,不然jvm不会识别是哪个线程的wait方法,notify也一样
*/
@Override
public synchronized void lock() {
while (isLocked){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
isLocked = true;
}
@Override
public synchronized void unlock() {
isLocked = false;
notify();
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public boolean tryLock() {
return false;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return false;
}
@Override
public Condition newCondition() {
return null;
}
}
然后我们做测试
package com.lock;
/**
* Created by Fant.J.
* 2018/3/6 20:24
*/
public class MyLockTest {
public int value = 0;
MyLock myLock = new MyLock();
public int getValue(){
myLock.lock();
value++;
myLock.unlock();
return value;
}
public static void main(String[] args) {
MyLockTest task = new MyLockTest();
new Thread(){
@Override
public void run() {
while (true) {
System.out.println(Thread.currentThread().getName() + " " + task.getValue());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
new Thread(){
@Override
public void run() {
while (true) {
System.out.println(Thread.currentThread().getName() + " " + task.getValue());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
}
}
结果没有出现线程安全问题,这里不做截图了,自己可以试试。但是我们写的方法还有一定的问题,就是MyLock这个类不支持 可重入锁,意思就是如果有两个锁嵌套,如果相同的线程先调用a方法,再调用带锁的b方法,则就会进入自旋锁。
测试方法源码
package com.lock;
/**
* Created by Fant.J.
* 2018/3/6 20:24
*/
public class MyLockTest2 {
public int value = 0;
MyLock myLock = new MyLock();
public void a(){
myLock.lock();
System.out.println("a");
b();
myLock.unlock();
}
public void b(){
myLock.lock();
System.out.println("b");
myLock.unlock();
}
public static void main(String[] args) {
MyLockTest2 task = new MyLockTest2();
new Thread(){
@Override
public void run() {
task.a();
}
}.start();
new Thread(){
@Override
public void run() {
task.a();
}
}.start();
}
}
执行该方法后,我们会发现,线程停止在打印出"a"后,一直在等待。这就是因为该锁不是可重入锁。
可重入锁的设计
我在这里只贴和上面代码不同的部分。
public class MyLock implements Lock {
//声明一个判断锁的布尔值
private boolean isLocked = false;
Thread lockBy = null;
int lockCount = 0;
@Override
public synchronized void lock() {
Thread currentThread = Thread.currentThread(); //获取到当前线程
while (isLocked && currentThread != lockBy){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
isLocked = true;
lockBy = currentThread; //将currentThread线程指向 lockBy线程
lockCount++;//计数器自增
}
@Override
public synchronized void unlock() {
if (lockBy == Thread.currentThread()){
lockCount--;
if (lockCount ==0 ){
notify();
isLocked = false;
}
}
}
}
第一个线程执行a()方法,得到了锁,使lockedBy等于当前线程,也就是说,执行的这个方法的线程获得了这个锁,执行add()方法时,同样要先获得锁,因不满足while循环的条件,也就是不等待,继续进行,将此时的lockedCount变量,也就是当前获得锁的数量加一,当释放了所有的锁,才执行notify()。如果在执行这个方法时,有第二个线程想要执行这个方法,因为lockedBy不等于第二个线程,导致这个线程进入了循环,也就是等待,不断执行wait()方法。只有当第一个线程释放了所有的锁,执行了notify()方法,第二个线程才得以跳出循环,继续执行。
17. Java并发编程-单例模式线程安全问题
单例模式是指对一个对象进行一次实例化,然后全局都可以调用该实例化对象来完成项目的开发。
在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。这些应用都或多或少具有资源管理器的功能。每台计算机可以有若干个打印机,但只能有一个Printer Spooler,以避免两个打印作业同时输出到打印机中。每台计算机可以有若干通信端口,系统应当集中管理这些通信端口,以避免一个通信端口同时被两个请求同时调用。总之,选择单例模式就是为了避免不一致状态,避免政出多头。
实现单例的不同方式
饿汉式单例
饿汉式单例是指在方法调用前,实例就已经创建好了。下面是实现代码:
package com.thread.singleton;
/**
* 单例模式-- 饿汉式
* Created by Fant.J.
* 2018/2/25 19:24
*/
public class Singleton1 {
/** 私有化构造方法,在外部不能实例化对象 */
private Singleton1(){}
/** 在这里实例化 静态对象 (优点:不存在线程安全问题。 缺点:每次调用都实例化,占用空间) */
private static Singleton1 singleton1 = new Singleton1();
public static Singleton1 getInstance(){
return singleton1;
}
}
优点:不存在线程安全问题。 缺点:每次调用都实例化,占用空间
懒汉式单例
懒汉式单例是指在方法调用获取实例时才创建实例,因为相对饿汉式显得“不急迫”,所以被叫做“懒汉模式”。下面是实现代码:
package com.thread.singleton;
/**
* 单例模式 -- 懒汉式
* Created by Fant.J.
* 2018/2/25 19:30
*/
public class Singleton2 {
private Singleton2(){}
private static Singleton2 instance;
public synchronized static Singleton2 getInstance() {
/* 下面这段代码 不是原子性操作 会出现线程安全问题 。**/
if (instance == null) {
instance = new Singleton2();
}
return instance;
}
}
在这段代码中,在if语句里面,就可能跑有多个线程同步判断和同步new。会产生线程安全问题。
解决方法:
- 给方法加上synchronized(变成单线程,影响性能)
- 给代码块加synchronized(双重检查加锁)
虽然2方法解决了性能问题, 但是还会有问题 。
问题来自 jvm 的优化:指令重排序(有兴趣了解)
我们可以在对象中添加volatile 关键字来 不让jvm对该 对象做优化
完善后的代码如下:
package com.thread.singleton;
/**
* 单例模式 -- 懒汉式
* Created by Fant.J.
* 2018/2/25 19:30
*/
public class Singleton2 {
private Singleton2(){}
private static Singleton2 instance;
public synchronized static Singleton2 getInstance() {
/* 下面这段代码 不是原子性操作 会出现线程安全问题 。
解决方法:1.给方法加上synchronized(变成单线程,影响性能)
2.给代码块加synchronized(双重检查加锁)
虽然2方法解决了性能问题, 但是还会有问题 。
问题来自 jvm 的优化:指令重排序(有兴趣了解)
我们可以在对象中添加volatile 关键字来 不让jvm对该 对象做优化
**/
if (instance == null) {
synchronized (Singleton2.class){
if (instance == null){
instance = new Singleton2();
}
}
}
return instance;
}
}
ThreadPoolExector源码分析
线程池源码也是面试经常被提问到的点,我会将全局源码做一分析,然后告诉你面试考啥,怎么答。
为什么要用线程池?
简洁的答两点就行。
- 降低系统资源消耗。
- 提高线程可控性。
如何创建使用线程池?
JDK8提供了五种创建线程池的方法:
- 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
- (JDK8新增)会根据所需的并发数来动态创建和关闭线程。能够合理的使用CPU进行对任务进行并发操作,所以适合使用在很耗时的任务。
注意返回的是ForkJoinPool对象。
public static ExecutorService newWorkStealingPool(int parallelism) {
return new ForkJoinPool
(parallelism,
ForkJoinPool.defaultForkJoinWorkerThreadFactory,
null, true);
}
什么是ForkJoinPool:
public ForkJoinPool(int parallelism,
ForkJoinWorkerThreadFactory factory,
UncaughtExceptionHandler handler,
boolean asyncMode) {
this(checkParallelism(parallelism),
checkFactory(factory),
handler,
asyncMode ? FIFO_QUEUE : LIFO_QUEUE,
"ForkJoinPool-" + nextPoolId() + "-worker-");
checkPermission();
}
使用一个无限队列来保存需要执行的任务,可以传入线程的数量,
不传入,则默认使用当前计算机中可用的cpu数量,
使用分治法来解决问题,使用fork()和join()来进行调用
- 创建一个可缓存的线程池,可灵活回收空闲线程,若无可回收,则新建线程。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
- 创建一个单线程的线程池。
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
- 创建一个定长线程池,支持定时及周期性任务执行。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
上层源码结构分析
Executor结构:
Executor
一个运行新任务的简单接口
public interface Executor {
void execute(Runnable command);
}
ExecutorService
扩展了Executor接口。添加了一些用来管理执行器生命周期和任务生命周期的方法
AbstractExecutorService
对ExecutorService接口的抽象类实现。不是我们分析的重点。
ThreadPoolExecutor
Java线程池的核心实现。
ThreadPoolExecutor源码分析
属性解释
// AtomicInteger是原子类 ctlOf()返回值为RUNNING;
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
// 高3位表示线程状态
private static final int COUNT_BITS = Integer.SIZE - 3;
// 低29位表示workerCount容量
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
// runState is stored in the high-order bits
// 能接收任务且能处理阻塞队列中的任务
private static final int RUNNING = -1 << COUNT_BITS;
// 不能接收新任务,但可以处理队列中的任务。
private static final int SHUTDOWN = 0 << COUNT_BITS;
// 不接收新任务,不处理队列任务。
private static final int STOP = 1 << COUNT_BITS;
// 所有任务都终止
private static final int TIDYING = 2 << COUNT_BITS;
// 什么都不做
private static final int TERMINATED = 3 << COUNT_BITS;
// 存放任务的阻塞队列
private final BlockingQueue<Runnable> workQueue;
值的注意的是状态值越大线程越不活跃。
线程池状态的转换模型:
构造器
public ThreadPoolExecutor(int corePoolSize,//线程池初始启动时线程的数量
int maximumPoolSize,//最大线程数量
long keepAliveTime,//空闲线程多久关闭?
TimeUnit unit,// 计时单位
BlockingQueue<Runnable> workQueue,//放任务的阻塞队列
ThreadFactory threadFactory,//线程工厂
RejectedExecutionHandler handler// 拒绝策略) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
在向线程池提交任务时,会通过两个方法:execute和submit。
本文着重讲解execute方法。submit方法放在下次和Future、Callable一起分析。
execute方法:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
// clt记录着runState和workerCount
int c = ctl.get();
//workerCountOf方法取出低29位的值,表示当前活动的线程数
//然后拿线程数和 核心线程数做比较
if (workerCountOf(c) < corePoolSize) {
// 如果活动线程数<核心线程数
// 添加到
//addWorker中的第二个参数表示限制添加线程的数量是根据corePoolSize来判断还是maximumPoolSize来判断
if (addWorker(command, true))
// 如果成功则返回
return;
// 如果失败则重新获取 runState和 workerCount
c = ctl.get();
}
// 如果当前线程池是运行状态并且任务添加到队列成功
if (isRunning(c) && workQueue.offer(command)) {
// 重新获取 runState和 workerCount
int recheck = ctl.get();
// 如果不是运行状态并且
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
//第一个参数为null,表示在线程池中创建一个线程,但不去启动
// 第二个参数为false,将线程池的有限线程数量的上限设置为maximumPoolSize
addWorker(null, false);
}
//再次调用addWorker方法,但第二个参数传入为false,将线程池的有限线程数量的上限设置为maximumPoolSize
else if (!addWorker(command, false))
//如果失败则拒绝该任务
reject(command);
}
总结一下它的工作流程:
-
当
workerCount < corePoolSize
,创建线程执行任务。 -
当
workerCount >= corePoolSize
&&阻塞队列workQueue
未满,把新的任务放入阻塞队列。 -
当
workQueue
已满,并且workerCount >= corePoolSize
,并且workerCount < maximumPoolSize
,创建线程执行任务。 -
当workQueue已满,
workerCount >= maximumPoolSize
,采取拒绝策略,默认拒绝策略是直接抛异常。
通过上面的execute方法可以看到,最主要的逻辑还是在addWorker方法中实现的,那我们就看下这个方法:
addWorker方法
主要工作是在线程池中创建一个新的线程并执行
参数定义:
firstTask
the task the new thread should run first (or null if none). (指定新增线程执行的第一个任务或者不执行任务)core
if true use corePoolSize as bound, else maximumPoolSize.(core如果为true则使用corePoolSize绑定,否则为maximumPoolSize。 (此处使用布尔指示符而不是值,以确保在检查其他状态后读取新值)。)
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {
int c = ctl.get();
// 获取运行状态
int rs = runStateOf(c);
// Check if queue empty only if necessary.
// 如果状态值 >= SHUTDOWN (不接新任务&不处理队列任务)
// 并且 如果 !(rs为SHUTDOWN 且 firsTask为空 且 阻塞队列不为空)
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
// 返回false
return false;
for (;;) {
//获取线程数wc
int wc = workerCountOf(c);
// 如果wc大与容量 || core如果为true表示根据corePoolSize来比较,否则为maximumPoolSize
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
// 增加workerCount(原子操作)
if (compareAndIncrementWorkerCount(c))
// 如果增加成功,则跳出
break retry;
// wc增加失败,则再次获取runState
c = ctl.get(); // Re-read ctl
// 如果当前的运行状态不等于rs,说明状态已被改变,返回重新执行
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
// 根据firstTask来创建Worker对象
w = new Worker(firstTask);
// 根据worker创建一个线程
final Thread t = w.thread;
if (t != null) {
// new一个锁
final ReentrantLock mainLock = this.mainLock;
// 加锁
mainLock.lock();
try {
// Recheck while holding lock.
// Back out on ThreadFactory failure or if
// shut down before lock acquired.
// 获取runState
int rs = runStateOf(ctl.get());
// 如果rs小于SHUTDOWN(处于运行)或者(rs=SHUTDOWN && firstTask == null)
// firstTask == null证明只新建线程而不执行任务
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
// 如果t活着就抛异常
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
// 否则加入worker(HashSet)
//workers包含池中的所有工作线程。仅在持有mainLock时访问。
workers.add(w);
// 获取工作线程数量
int s = workers.size();
//largestPoolSize记录着线程池中出现过的最大线程数量
if (s > largestPoolSize)
// 如果 s比它还要大,则将s赋值给它
largestPoolSize = s;
// worker的添加工作状态改为true
workerAdded = true;
}
} finally {
mainLock.unlock();
}
// 如果worker的添加工作完成
if (workerAdded) {
// 启动线程
t.start();
// 修改线程启动状态
workerStarted = true;
}
}
} finally {
if (! workerStarted)
addWorkerFailed(w);
}
// 返回线启动状态
return workerStarted;
为什么需要持有mainLock?
因为workers是HashSet类型的,不能保证线程安全。
那w = new Worker(firstTask);
如何理解呢
Worker.java
private final class Worker
extends AbstractQueuedSynchronizer
implements Runnable
可以看到它继承了AQS并发框架还实现了Runnable。证明它还是一个线程任务类。那我们调用t.start()事实上就是调用了该类重写的run方法.
Worker为什么不使用ReentrantLock来实现呢?
tryAcquire方法它是不允许重入的,而ReentrantLock是允许重入的。对于线程来说,如果线程正在执行是不允许其它锁重入进来的。
线程只需要两个状态,一个是独占锁,表明正在执行任务;一个是不加锁,表明是空闲状态。
public void run() {
runWorker(this);
}
run方法又调用了runWorker方法:
final void runWorker(Worker w) {
// 拿到当前线程
Thread wt = Thread.currentThread();
// 拿到当前任务
Runnable task = w.firstTask;
// 将Worker.firstTask置空 并且释放锁
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
// 如果task或者getTask不为空,则一直循环
while (task != null || (task = getTask()) != null) {
// 加锁
w.lock();
// If pool is stopping, ensure thread is interrupted;
// if not, ensure thread is not interrupted. This
// requires a recheck in second case to deal with
// shutdownNow race while clearing interrupt
// return ctl.get() >= stop
// 如果线程池状态>=STOP 或者 (线程中断且线程池状态>=STOP)且当前线程没有中断
// 其实就是保证两点:
// 1. 线程池没有停止
// 2. 保证线程没有中断
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
// 中断当前线程
wt.interrupt();
try {
// 空方法
beforeExecute(wt, task);
Throwable thrown = null;
try {
// 执行run方法(Runable对象)
task.run();
} catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
afterExecute(task, thrown);
}
} finally {
// 执行完后, 将task置空, 完成任务++, 释放锁
task = null;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
// 退出工作
processWorkerExit(w, completedAbruptly);
}
总结一下runWorker方法的执行过程:
- while循环中,不断地通过getTask()方法从workerQueue中获取任务
- 如果线程池正在停止,则中断线程。否则调用3.
- 调用task.run()执行任务;
- 如果task为null则跳出循环,执行processWorkerExit()方法,销毁线程
workers.remove(w);
这个流程图非常经典:
除此之外,ThreadPoolExector
还提供了tryAcquire
、tryRelease
、shutdown
、shutdownNow
、tryTerminate
、等涉及的一系列线程状态更改的方法有兴趣可以自己研究。大体思路是一样的,这里不做介绍。
Worker为什么不使用ReentrantLock来实现呢?
tryAcquire方法它是不允许重入的,而ReentrantLock是允许重入的。对于线程来说,如果线程正在执行是不允许其它锁重入进来的。
线程只需要两个状态,一个是独占锁,表明正在执行任务;一个是不加锁,表明是空闲状态。
在runWorker方法中,为什么要在执行任务的时候对每个工作线程都加锁呢?
shutdown方法与getTask方法存在竞态条件.(这里不做深入,建议自己深入研究,对它比较熟悉的面试官一般会问)
高频考点
- 创建线程池的五个方法。
- 线程池的五个状态
- execute执行过程。
- runWorker执行过程。(把两个流程图记下,理解后说个大该就行。)
- 比较深入的问题就是我在文中插入的问题。
wait()与sleep()方法区别
本质上的区别: sleep是对线程状态的控制, wait是线程间通讯。一个是Thread类, 一个是Object类。Thread不会影响锁的行为,锁相关的方法都定义在Object类中
- sleep是Thread的方法, wait是Object的方法。
- sleep不会释放锁, wait会施放锁并将锁添加到一个等待队列。
- sleep不依赖monitor, wait依赖monitor。
- sleep不需要唤醒、wait需要。
注意:锁池是对象头中的一个标记,用于存放等待锁的线程。调用wait()方法, 意味着当前线程需要释放自己的所有锁放入锁池, 然后进入该对象的等待队列。当调用notify()时, 就通知等待队列中的一个线程出列, 然后进入锁池去拿到锁。
并发工具类CountDownLatch、CyclicBarrier、Semaphore实践及源码分析
带你快速了解这几个同步信号工具。
1. CountDownLatch
用给定的计数 初始化 CountDownLatch。由于调用了 countDown() 方法,所以在当前计数到达零之前,await 方法会一直受阻塞。之后,会释放所有等待的线程,await 的所有后续调用都将立即返回。
案例:项目经理改个需求,当小王、小李、小赵都相继改完代码后,项目经理才进行审查。
1.1 Boss.java
public class Boss implements Runnable{
private CountDownLatch countDownLatch;
public Boss(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
System.out.println("Boss: 需求改动!速度更新!");
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Boss: 都改完了是吗,我看看阿!");
}
}
1.2 Worker.java
public class Worker implements Runnable{
private CountDownLatch countDownLatch;
private String name;
public Worker(CountDownLatch countDownLatch, String name) {
this.countDownLatch = countDownLatch;
this.name = name;
}
@Override
public void run() {
System.out.println(name + ": 开始改代码");
try {
Thread.sleep((long) (Math.random()*10000));
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name + ": 代码改完了!");
countDownLatch.countDown();
}
}
1.3 Main
public class Main {
public static void main(String[] args) {
// 当调用三次countDown时释放所有等待线程
CountDownLatch countDownLatch = new CountDownLatch(3);
ExecutorService executorService = Executors.newCachedThreadPool();
Boss boss = new Boss(countDownLatch);
Worker worker1 = new Worker(countDownLatch, "小王");
Worker worker2 = new Worker(countDownLatch, "小李");
Worker worker3 = new Worker(countDownLatch, "小赵");
executorService.execute(boss);
executorService.execute(worker1);
executorService.execute(worker2);
executorService.execute(worker3);
executorService.shutdown();
}
}
结果:
Boss: 需求改动!速度更新!
小王: 开始改代码
小李: 开始改代码
小赵: 开始改代码
小王: 代码改完了!
小李: 代码改完了!
小赵: 代码改完了!
Boss: 都改完了是吗,我看看阿!
2. CyclicBarrier
一个同步辅助类,它允许一组线程互相等待,直到到达某个公共屏障点 (common barrier point)。
案例:老板叫开会,等所有人都来了后就开始开会,先来的人先wait。
2.1 Boss
public class Boss implements Runnable{
private CyclicBarrier cyclicBarrier;
Boss(CyclicBarrier cyclicBarrier) {
this.cyclicBarrier = cyclicBarrier;
}
@Override
public void run() {
System.out.println("Boss: 开会!");
}
}
2.2 Worker
public class Worker implements Runnable{
private CyclicBarrier cyclicBarrier;
private String name;
public Worker(CyclicBarrier cyclicBarrier, String name) {
this.cyclicBarrier = cyclicBarrier;
this.name = name;
}
@Override
public void run() {
try {
System.out.println(name + ": 我正在路上");
Thread.sleep((long) (Math.random()*10000));
System.out.println(name + ": 我到了!");
cyclicBarrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}
}
2.3 Main
public class Main {
public static void main(String[] args) {
// 当三个线程处于barrier(wait)状态的时候,同时开始执行后续任务。
CyclicBarrier cyclicBarrier = new CyclicBarrier(3, () ->System.out.println("Boss: 都到了吗?那我来说两句...balabala..."));
ExecutorService executorService = Executors.newFixedThreadPool(4);
Boss boss = new Boss(cyclicBarrier);
Worker worker1 = new Worker(cyclicBarrier, "小王");
Worker worker2 = new Worker(cyclicBarrier, "小李");
Worker worker3 = new Worker(cyclicBarrier, "小赵");
executorService.execute(boss);
executorService.execute(worker1);
executorService.execute(worker2);
executorService.execute(worker3);
executorService.shutdown();
}
}
我设置当三个线程处于barrier(wait)状态的时候,同时开始执行后续任务。
Boss: 开会!
小王: 我正在路上
小李: 我正在路上
小赵: 我正在路上
小王: 我到了!
小李: 我到了!
小赵: 我到了!
Boss: 都到了吗?那我来说两句...balabala...
3. Semaphore
一个计数信号量。从概念上讲,信号量维护了一个许可集。如有必要,在许可可用前会阻塞每一个 acquire(),然后再获取该许可。每个 release() 添加一个许可,从而可能释放一个正在阻塞的获取者。但是,不使用实际的许可对象,Semaphore 只对可用许可的号码进行计数,并采取相应的行动。
通常用于限制可以访问某些资源(物理或逻辑的)的线程数目。(类似网络带宽)
案例:老板发红包,办公室只能站下三个人,员工排队去领。
3.1 Boss
public class Boss implements Runnable{
@Override
public void run() {
System.out.println("排队领红包!我身边最多围三个人!");
}
}
3.2 Worker
public class Worker implements Runnable{
private Semaphore semaphore;
private String name;
Worker(Semaphore semaphore, String name) {
this.semaphore = semaphore;
this.name = name;
}
@Override
public void run() {
try {
semaphore.acquire();
System.out.println(name + ": 我正在老板面前等红包");
Thread.sleep((long) (Math.random()*10000));
System.out.println(name +": 我拿到红包了");
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
3.3 Main
public class Main {
public static void main(String[] args) {
// 设置三个信号量,只有拿到信号才能执行后续任务
Semaphore semaphore = new Semaphore(3);
ExecutorService executorService = Executors.newCachedThreadPool();
Worker worker1 = new Worker(semaphore, "小王");
Worker worker2 = new Worker(semaphore, "小Li");
Worker worker3 = new Worker(semaphore, "小赵");
Worker worker4 = new Worker(semaphore, "小孙");
Worker worker5 = new Worker(semaphore, "小周");
Boss boss = new Boss();
executorService.execute(boss);
executorService.execute(worker1);
executorService.execute(worker2);
executorService.execute(worker3);
executorService.execute(worker4);
executorService.execute(worker5);
executorService.shutdown();
}
}
排队领红包!我身边最多围三个人!
小王: 我正在老板面前等红包
小Li: 我正在老板面前等红包
小孙: 我正在老板面前等红包
小Li: 我拿到红包了
小赵: 我正在老板面前等红包
小孙: 我拿到红包了
小周: 我正在老板面前等红包
小周: 我拿到红包了
小王: 我拿到红包了
小赵: 我拿到红包了