线程是进程中的一个实体,线程本身是不会独立存在的。进程是代码在数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,线程则是进程的一个执行路径,一个进程中至少有一个线程,进程中多个线程共享进程的资源。
操作系统在分配资源时是把资源分配给进程的, 但是 CPU 资源比较特殊。它是被分配到线程的,因为真正要占用CPU运行的是线程,所以也说线程是 CPU 分配的基本单位。
java
中多线程用到的两种线程调度模型:分时调度模型和抢占式调度模型;
分时调度模型是指让所有的线程轮流获取CPU时间片,并且平均分配每个线程占用的CPU时间片
Java虚拟机采用的是抢占式调度模型,是指优先让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU时间片。
什么是原子操作:指不可被中断的操作,要么执行,要么不执行。(基本数据类型的变量的赋值和读取都是原子性操作)
自增操作不具有原子性
- x = 10; //语句1
- y = x; //语句2
- x++; //语句3
- x = x + 1; //语句4
语句1 是原子操作,语句2 不是。(读取x的数据, x的数据写给y),语句3不是(读取x的值,操作x的值,写入新的值)
语句4:不是(读取x的值,操作x的值,写入新的值)
可见性:是指形成之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的结果,另一个线程马上就能看到。
当一个共享变量被 volatile
修饰时,他会保证修改的值立即被更新到内存中,所以对其他线程是可见的,当有其他线程需要读取时,他会去内存中读取新值。而普通的共享变量不能保证可见性,因为普通的共享变量被修改后,什么时候写入内存不确定。当有其他线程去读取时,此时内存中可能还是原来的旧值。因此无法保证可见性。
在 java
的内存模型中,允许编译器和处理器对指令进行重排序。但是重排序过程不影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
多线程并发执行下可以通过volatile
关键字来保证一定的有序性。另外可以通过 synchronize
或 lock
来保证有序性。
解决方案:
JDK Atomic
原子类,synchronized
、lock
解决原子性问题
synchronized
、volitile
、lock
解决可见性问题
Happens-before
规则 解决有序性问题
因为
jvm
会对代码进行编译优化,指令会出现重排序的情况,为了避免编译优化对并发编程安全性的影响,需要happens-before
规则定义一些禁止编译优化的场景,保证并发编程的正确性。
实现线程同步的方式:
synchronized
关键字;Lock
锁实现同步;
New
):新建了一个线程对象Runnable
):线程对象创建成功,之后,调用了该对象的start()
方法,该线程就处于就绪状态(可运行状态)。当该线程成功抢夺到CPU时间片时,线程就处于运行状态(Running
);Blocked
):线程由于某种原因放弃了CPU使用权,暂停运行。WAITING
):线程进入等待状态(通知或中断方法)TIME_WAITING
):不同于WAITING
,他是可以在指定的时间自行返回的。TERMINATED
):线程执行完毕;Java将操作系统中的运行和就绪两个状态合并称为运行状态。
阻塞状态是线程阻塞在进入
synchronized
关键字修饰的方法或代码块(获取锁)时的状态,但是阻塞在Lock
接口的线程状态却是等待状态
,因为Lock
接口对于阻塞的实现使用的是LockSupport
类的相关方法;
wait()
方法,该线程会释放占用的所有资源,JVM
会把该线程放入“等待池”中。进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify()
或notifyAll()
方法才能被唤醒。JVM
会把该线程放入“锁池”中。sleep()
或join()
方法,或者发出了I/O
请求时,JVM
会把该线程置为阻塞状态。当sleep()
状态超时、join()
等待线程终止或者超时、或者I/O
处理完毕时,线程重新转入就绪状态。需要注意的是:
wait()
方式会释放占用的资源,其中包括锁资源;
Java中有三种创建线程的方式,分别是继承Thread
类并重写run()
,实现Runnable
接口的run()
,或者使用FutureTask(实现Callable接口)
方式;
Thread
类package com.wddong.Thread;
public class MyThreadDemo {
public static void main(String[] args) throws InterruptedException {
MyThread myThread = new MyThread();
//启动线程
myThread.start();
//当前线程(main-thread)等待,myThread线程执行完成;
myThread.join();
}
}
class MyThread extends Thread{
@Override
public void run() {
System.out.println("MyThread running");
}
}
Runnable
接口package com.wddong.Thread;
public class MyRunnableDemo {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new MyRunnable());
//启动线程
thread.start();
//当前线程(main-thread)等待,myThread线程执行完成;
thread.join();
}
}
class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("MyRunnable running");
}
}
Future(Callable接口)
方式package com.wddong.Thread;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class MyFutureDemo {
public static void main(String[] args) throws InterruptedException, ExecutionException {
FutureTask<String> future = new FutureTask<>(new MyCallable());
Thread thread = new Thread(future);
//启动线程
thread.start();
//获取返回值
String returnString = future.get();
//打印返回值
System.out.println(returnString);
//当前线程(main-thread)等待,myThread线程执行完成;
thread.join();
}
}
/**
* 实现Callable接口和实现Runnable接口的差异就是Callable接口具有返回值;
* 泛型:返回值类型为String;
*/
class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("MyFurure running");
return "callable";
}
}
继承Thread
:
run()
获取当前线程直接使用 this
就可以了,无需使用 Thread. currentThread()
方法Java
不支持多继承,如果继承了 Thread
类,那么就不能再继承其他类。实现Runnable
:
Runnable
是接口,可以多实现;Future
:
优:
wait()
函数当一个线程调用一个共享变量wait()
方法时, 该调用线程会被阻塞挂起,直到发生下面几件事情中之一才返回:
线程调用了该共享对象notify()
或者notifyAll()
方法
其他线程调用了该线程interrupt()
方法使该线程抛出InterruptedException
异常返回。
另外需要注意的是,如果调用
wait()
,线程没有事先获取该对象的监视器锁,则调用wait()
方法时调用线程会抛出IllegalMonitorStateException
异常。
wait
系列方法:
wait(long timeout)
:不同之处在于,如果一个线程调用共享对象的该方法挂起后,没有在指定的
timeout ms
内被其它线程调用该共享变量的notify()
或者notifyAll()
方法唤醒,那么该函数还是会因为超时而返回。如果将timeout
设置为0则和wait()
方法效果一样,因为在wait()
方法内部就是调用了wait(0)
需要注意的是,如果在调用该函数时,传递了一个负的timeout
会抛出IllegumentException
异常;
wait(long timeout, int nanos)
:在其内部调用的是
wait(long timeout)
函数;public final void wait(long timeout,int nanos) throws InterruptedException{ if (timeout < 0){ throw new IllegalArgumentException("timeout value is negative"); } if(nanos < 0 || nanos > 999999) { throw new IllegalArgumentException("nanosecond timeout value out of range"); } if (nacos > 0) { timeout ++; } wait(timeout); }
上面提到,线程没有事先获取该对象的监视器锁,则调用wait()
方法时调用线程会抛出 IllegalMonitorStateException
异常。那么
一个线程如何获取共享变量的监视器锁呢?
执行synchronized
同步代码块,使用该共享变量作为参数
synchronized (共享变量){ //doSomething }
调用了该共享变量的方法,并且该方法被synchronized
关键字修饰
synchronized void add nt a , int b) { //doSomething }
notify()
/notifyAll()
notify()
一个线程调用共享对象的notify()
方法后,会唤醒一个在该共享变量上调用wait()
系列方法后被挂起的线程。一个共享变量上可能会有多个线程在等待,具体唤醒哪个等待的线程是随机的。此外,被唤醒的线程不能马上从wait()
方法返回并继续执行,它必须在获取了共享对象的监视器锁后才可以返回。也就是唤醒它的线程释放了共享变量上的监视器锁后,被唤醒的线程也不一定会获取到共享对象的监视器锁,这是因为该线程还需要和其他线程一起竞争该锁。 只有该线程竞争到了共享变量的监视器锁后才可以继续执行。
类似wait()
系列方法,只有当前线程获取到了共享变量的监视器锁后,才可以调用共享变量的notify()
方法,否则会抛出IllegalMonitorStateException
异常。
notifyAll()
在共享变量上调用notify()
函数会唤醒被阻塞到该共享变量上某一个随机线程,notifyAll()
方法则会唤醒所有在该共享变量上由于调用wait
系列方法而被挂起的线程。
package com.wddong.Thread;
public class WaitAndNotifyDemo {
private static volatile Object shareResourceA = new Object();
public static void main(String[] args) throws InterruptedException {
Thread threadA = new Thread(() -> {
//获取shareResourceA共享资源的监视器锁
synchronized (shareResourceA) {
System.out.println("threadA get shareResourceA lock");
try {
System.out.println("threadA begin wait");
shareResourceA.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("threadA end wait");
}
}, "threadA");
Thread threadB = new Thread(() -> {
//获取shareResourceA共享资源的监视器锁
synchronized (shareResourceA) {
System.out.println("threadB get shareResourceA lock");
try {
System.out.println("threadB begin wait");
shareResourceA.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("threadB end wait");
}
}, "threadB");
Thread threadC = new Thread(() -> {
synchronized (shareResourceA) {
System.out.println("threadC begin notify ");
shareResourceA.notify();
}
}, "threadC");
//启动线程
threadA.start();
threadB.start();
Thread.sleep(1000);
threadC.start();
//等待线程结束
threadA.join();
threadB.join();
threadC.join();
}
}
执行结果:
threadA get shareResourceA lock
threadA begin wait
threadB get shareResourceA lock
threadB begin wait
threadC begin notify
threadA end wait
从代码运行的结果中,可以看出。notify
只会唤醒共享资源下的随机一个线程。我们将threadC
中的notify()
换成notifyAll()
查看运行结果
Thread threadC = new Thread(() -> {
synchronized (shareResourceA) {
System.out.println("threadC begin notify ");
shareResourceA.notifyAll();
}
}, "threadC");
执行结果:
threadA get shareResourceA lock
threadA begin wait
threadB get shareResourceA lock
threadB begin wait
threadC begin notify
threadB end wait
threadA end wait
可以看到notifyAll()
唤醒的是共享资源下所有的线程;
await(),signal(),signalAll()
)LOCK 对比 Synchronize
的优势在于 LOCK 能够精确唤醒。
LOCK 中的睡眠和唤醒
await()
signal(),signalAll()
#精确的通知和唤醒线程
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* A -> B -> C -> A
* @description:
*/
public class ConditionDemo {
public static void main(String[] args) {
Data3 data3 = new Data3();
new Thread(()->{
for (int i = 0; i < 10; i++) {
data3.printA();
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
data3.printB();
}
},"B").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
data3.printC();
}
},"C").start();
}
}
class Data3{
private Lock lock = new ReentrantLock();
private Condition condition1 = lock.newCondition();
private Condition condition2 = lock.newCondition();
private Condition condition3 = lock.newCondition();
private int num = 1; // 1A 2B 3C
public void printA(){
lock.lock();
try {
while (num != 1){
condition1.await();
}
System.out.println(Thread.currentThread().getName()+" Im A ");
num = 2;
//唤醒2
condition2.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printB(){
lock.lock();
try {
while (num != 2){
condition2.await();
}
System.out.println(Thread.currentThread().getName()+" Im B ");
num = 3;
//唤醒3
condition3.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printC(){
lock.lock();
try {
while (num != 3){
condition3.await();
}
System.out.println(Thread.currentThread().getName()+" Im C ");
num = 1;
// 唤醒1
condition1.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
package com.wddong.Thread.ProdAndCustom;
//奶箱类
public class Box {
//定义奶箱中牛奶的数量
private int milkCount = 0;
//奶箱中最大存放的牛奶数
private volatile static int MAX_MILK_COUNT = 3;
//放入牛奶
public synchronized void putMilk() throws InterruptedException {
//存放牛奶到达上限。等待消费
if (milkCount == MAX_MILK_COUNT){
System.out.println("牛奶过多,等待消费");
wait();
}
System.out.println(String.format("奶箱中共有%d瓶牛奶,放入一瓶,还剩%d瓶",milkCount,++milkCount));
//通知消费
notify();
}
//取出牛奶
public synchronized void getMilk() throws InterruptedException {
//如果没有牛奶,等待
if (milkCount==0) {
System.out.println("牛奶过少,等待放入");
wait();
}
//有牛奶直接消费
System.out.println(String.format("奶箱中共有%d瓶牛奶,取出一瓶,还剩%d瓶",milkCount,--milkCount));
//消费完,通知生产
notify();
}
}
package com.wddong.Thread.ProdAndCustom;
//生产者、消费者
public class ProdAndCustomDemo {
public static void main(String[] args) throws InterruptedException {
Box box = new Box();
//生产者线程
Thread prod = new Thread(() -> {
for (int i = 0; i <10 ; i++) {
try {
box.putMilk();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "prod");
//消费者线程
Thread custom = new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
box.getMilk();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "custom");
prod.start();
Thread.sleep(1000);
custom.start();
prod.join();
custom.join();
System.out.println("main thread run end");
}
}
//执行结果:
奶箱中共有0瓶牛奶,放入一瓶,还剩1瓶
奶箱中共有1瓶牛奶,放入一瓶,还剩2瓶
奶箱中共有2瓶牛奶,放入一瓶,还剩3瓶
牛奶过多,等待消费
奶箱中共有3瓶牛奶,取出一瓶,还剩2瓶
奶箱中共有2瓶牛奶,取出一瓶,还剩1瓶
奶箱中共有1瓶牛奶,取出一瓶,还剩0瓶
牛奶过少,等待放入
奶箱中共有0瓶牛奶,放入一瓶,还剩1瓶
奶箱中共有1瓶牛奶,放入一瓶,还剩2瓶
奶箱中共有2瓶牛奶,放入一瓶,还剩3瓶
牛奶过多,等待消费
奶箱中共有3瓶牛奶,取出一瓶,还剩2瓶
奶箱中共有2瓶牛奶,取出一瓶,还剩1瓶
奶箱中共有1瓶牛奶,取出一瓶,还剩0瓶
牛奶过少,等待放入
奶箱中共有0瓶牛奶,放入一瓶,还剩1瓶
奶箱中共有1瓶牛奶,放入一瓶,还剩2瓶
奶箱中共有2瓶牛奶,放入一瓶,还剩3瓶
牛奶过多,等待消费
奶箱中共有3瓶牛奶,取出一瓶,还剩2瓶
奶箱中共有2瓶牛奶,取出一瓶,还剩1瓶
奶箱中共有1瓶牛奶,取出一瓶,还剩0瓶
牛奶过少,等待放入
奶箱中共有0瓶牛奶,放入一瓶,还剩1瓶
奶箱中共有1瓶牛奶,取出一瓶,还剩0瓶
main thread run end
虚假唤醒问题:一个线程从挂起状态变为可运行状态,但是该线程并未被其他线程调用notify()、notifyAll()
进行通知,或者被中断,或者等待超时,这就是所谓的虚假唤醒;
/**
* 使用了四个线程来模拟虚假唤醒问题
* A,C 线程是增加
* B,D线程是减少
* wait()方法会释放锁
* A 线程 调用wait()会释放锁。 所以C 线程也可以拿到锁 进入代码块,此时A,C 中的 num = 0;notifyAll()唤醒后,
* 会同时进行 num++ 会造成数量未按要求变化 num = 2
* 解决虚假唤醒问题:需要将 if 判断修改为 while 判断即可。
*/
//生产者、消费者
public class ProdAndCustomDemo {
public static void main(String[] args) throws InterruptedException {
Box box = new Box();
//生产者线程
Thread A = new Thread(() -> {
for (int i = 0; i <100 ; i++) {
try {
box.putMilk();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "A");
//消费者线程
Thread B = new Thread(() -> {
for (int i = 0; i < 100; i++) {
try {
box.getMilk();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "B");
//生产者线程
Thread C = new Thread(() -> {
for (int i = 0; i <100 ; i++) {
try {
box.putMilk();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "C");
//消费者线程
Thread D = new Thread(() -> {
for (int i = 0; i < 100; i++) {
try {
box.getMilk();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "D");
A.start();
Thread.sleep(1000);
B.start();
C.start();
Thread.sleep(1000);
D.start();
A.join();
B.join();
C.join();
D.join();
System.out.println("main thread run end");
}
}
执行结果:
..........
奶箱中共有14瓶牛奶,取出一瓶,还剩13瓶
奶箱中共有13瓶牛奶,取出一瓶,还剩12瓶
奶箱中共有12瓶牛奶,取出一瓶,还剩11瓶
.....
奶箱中共有2瓶牛奶,取出一瓶,还剩1瓶
奶箱中共有1瓶牛奶,取出一瓶,还剩0瓶
main thread run end
可以看到,我们在之前的Box
设置了奶瓶上限最多为3瓶,但是由于虚假唤醒导致,输出的结果有问题。解决方案就是将Box
类 if
判断修改为 while
判断即可。
//奶箱类
public class Box {
//定义奶箱中牛奶的数量
private int milkCount = 0;
//奶箱中最大存放的牛奶数
private volatile static int MAX_MILK_COUNT = 3;
//放入牛奶
public synchronized void putMilk() throws InterruptedException {
//存放牛奶到达上限。等待消费
while (milkCount == MAX_MILK_COUNT){
System.out.println("牛奶过多,等待消费");
wait();
}
System.out.println(String.format("奶箱中共有%d瓶牛奶,放入一瓶,还剩%d瓶",milkCount,++milkCount));
//通知消费
notify();
}
//取出牛奶
public synchronized void getMilk() throws InterruptedException {
//如果没有牛奶,等待
while (milkCount==0) {
System.out.println("牛奶过少,等待放入");
wait();
}
//有牛奶直接消费
System.out.println(String.format("奶箱中共有%d瓶牛奶,取出一瓶,还剩%d瓶",milkCount,--milkCount));
//消费完,通知生产
notify();
}
}
join()
public class JoinDemo {
public static void main(String[] args) throws InterruptedException {
Thread threadOne = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("threadOne over");
}
});
threadOne.start();
System.out.println("wait threadOne over");
//下方的代码是 在main线程中执行的 threadOne.join(); 所以是 main 线程等待 threadOne线程执行完毕;
threadOne.join();
}
}
Sleep()
Thread
类中有静态的sleep
方法,当一个执行中的线程调用了Thread.sleep()
方法后,调用线程会暂时让出指定时间的执行权,也就是在这期间不参与 CPU
的调度,但是该线程所拥有的监视器资源,比如锁还是持有不让出的。指定的睡眠时间到了后,该函数会正常返回,线程处于就绪状态。参与CPU
的调度。当重新获取到CPU
时间片资源后就可以继续运行了;
package com.wddong.Thread;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
//线程Sleep期间不释放监视器资源
public class SleepDemo {
private static final Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
//创建线程A
Thread threadA = new Thread(() -> {
lock.lock();
try {
System.out.println("child threadA is in sleep");
Thread.sleep(10000);
System.out.println("child threadA is in awaked");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "threadA");
//创建线程A
Thread threadB = new Thread(() -> {
lock.lock();
try {
System.out.println("child threadB is in sleep");
Thread.sleep(10000);
System.out.println("child threadB is in awaked");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "threadB");
//启动线程
threadA.start();
threadB.start();
threadA.join();
threadB.join();
}
}
程序运行结果:
child threadA is in sleep
child threadA is in awaked
child threadB is in sleep
child threadB is in awaked
从程序运行结果中可以看出,线程在
sleep()
期间是不释放锁资源的;
yield()
Thread
类中的静态方法 yield()
,当一个线程调用yield()
方法时,就是告诉线程调度器当前线程可以让出自己的CPU
时间片。
我们知道操作系统是为每个线程分配一个时间片来占有CPU
的,正常情况下当一个线程将分配给自己的时间片使用完后,线程调度器才会进行下一轮的线程调度,而当一个线程调用了Thread
类的静态方法 yield()
时,是在告诉线程调度器自己占有的时间片中还没有使用完的部分自己不想使用了,这暗示线程调度器现在就可以进行下一轮的线程调度。
当一 线程调用 yield()
方法时, 当前线程会让出CPU
使用权,然后处于就绪状态,线程调度器会从线程就绪队列里面获取一个线程优先级最高的线程,当然也有可能会调度到刚刚让出CPU
的那个线程来获取CPU
行权。
public class YieldDemo {
public static void main(String[] args) throws InterruptedException {
Thread threadOne = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
//告知线程调度器,让出CPU时间片;threadlOne 线程中执行的,所以是 threadOne线程让出;
Thread.yield();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("threadOne over");
}
});
threadOne.start();
System.out.println("wait threadOne over");
}
}
Daemon()
Java
中的线程分为两类,分别为daemon线程(守护线程)
和 user 线程(用户线程)
。JVM
启动会调用main
函数,main
数所在的线程就是一个用户线程,其实在 JVM
内部同时还启动了好多守护线程,比如垃圾回收线程。
那么守护线程和用户线程有什么区别呢?区别之一是当最后一个非守护线程结束时,JVM
正常退出,而不管当前是否还有守护线程,也就是说守护线程是否结束并不影响JVM
退出。
public class DaemonDemo {
public static void main(String[] args) {
Thread daemonThread = new Thread(() -> {
System.out.println("daemon thread ");
});
//设置为守护线程
daemonThread.setDaemon(true);
daemonThread.start();
}
}
wait()
和 sleep()
的比较wait()
是Object
类的方法, sleep()
是线程类 Thread
的静态方法wait()
不需要捕获异常 ,sleep()
需要捕获异常wait()
会释放锁,sleep()
不会释放锁。wait()
方法需要notify(),notifyAll()
唤醒wait()
的调用位置只能在同步代码块中,sleep()
可以在任何地方进行调用Java
中的线程中断是一种线程间的协作模式,通过设置线程的中断标志并不能直接终止线程,而是被中断的线程通过判断中断状态自行处理;
void interrupt()
:设置线程的中断状态为true
并直接返回;
当
线程A
运行时,线程B
可以通过调用线程A
的interrupt()
方法来设置线程A
的中断标志为true
并直接返回。此时,仅仅只是设置了线程A
的中断标志为true
,线程A
并没有中断,会继续向下执行。如果
线程A
因为调用了wait()
、join()
、sleep()
等方法而被阻塞挂起的情况下,这个时候如果线程B
中调用了线程A
的interrupt()
方法,线程A
会在调用这些方法的地方抛出InterruptedException
异常而返回;
boolean isInterrupted()
:检测当前线程是否被中断,是
返回true
,否
返回false
;
boolean interrupted()
:检测当前线程是否被中断,是
返回true
,否
返回false
。与isInterrupted()
方法不同的是,该方法如果发现当前线程被中断,则会清楚中断标志。且当前线程为 static
方法;
public boolean isInterrupted() { //传递false ,说明不清除中断标志 return this.isInterrupted(false); } public static boolean interrupted() { //清除中断标志 return currentThread().isInterrupted(true); }
static
静态方法意味着interrupted()
是获取当前调用线程的中断标志而不是调用interrupted()
方法的实例对象中的中断标志;public class InterruptedDemo { public static void main(String[] args) { Thread thread = new Thread(() -> { }); //中断标志 thread.interrupted(); Thread.interrupted(); } }
如上,虽然第一次调用
interrupted()
的对象是thread
,第二次调用的对象是main
线程,这两种方式调用 的对象虽然不同。但是其实获取的都是main
线程的状态。
共享资源:被多个线程访问的变量称为共享资源;
多钱程访问同一个共享变量时特别容易出现并发问题,特别是在多个线程需要对共享变量进行写入时,为了保证线程安全,一般使用者在访问共享变量时需要进行适当的同步,同步的措施 般是加锁。或者采用ThreadLocal
servlet
是线程安全的吗?
servlet
不是线程安全的。servlet
是单实例多线程的。当多个线程同时访问同一个方法,不能保证共享变量的线程安全性
Struts2
的action
是多实例多线程的,是线程安全的。每次请求都会new
一个新的action
分配给这个请求,请求完成后销毁
死锁是指两个或两个以上的线程在争夺共享资源时,出现相互等待的情况,在无外力的情况下,会一直无法运行下去;
线程A
持有资源2
,线程B
持有资源1
,线程A
等待资源1
,线程B
等待资源2
,进入了死锁状态;
死锁产生的四个条件:
{T0, T1 T2 ,…, Tn}
中T0
正在等待一T1
占用的资源T1
正在等待T2
用的资源,……Tn
在等待己被T0
占用的资源。package com.wddong.Thread;
public class DeadLockDemo {
private static Object resouceA = new Object();
private static Object resouceB = new Object();
public static void main(String[] args) throws InterruptedException {
Thread threadA = new Thread(() -> {
synchronized (resouceA){
System.out.println("threadA get resouceA");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("threadA waiting resouceB");
synchronized (resouceB){
System.out.println("threadA get resouceB");
}
}
}, "threadA");
Thread threadB = new Thread(() -> {
synchronized (resouceB){
System.out.println("threadB get resouceB");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("threadB waiting resouceA");
synchronized (resouceA){
System.out.println("threadB get resouceA");
}
}
}, "threadB");
threadA.start();
threadB.start();
threadA.join();
threadB.join();
}
}
使用 jps -l
定位进程号
D:\ideaworkspace\testtest\wddong\target\classes\com\wddongtt\wddong\java8>jps -l
11492 org.jetbrains.idea.maven.server.RemoteMavenServer
14164 org.jetbrains.jps.cmdline.Launcher
4884 sun.tools.jps.Jps
14680 com.wddongtt.wddong.thread.cas.DeadLockDemo
16268 org.jetbrains.kotlin.daemon.KotlinCompileDaemon
7020
使用 jstack 进程号
找到死锁问题
D:\ideaworkspace\testtest\wddong\target\classes\com\wddongtt\wddong\java8>jstack 14680
2022-02-13 17:39:02
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.131-b11 mixed mode):
"DestroyJavaVM" #14 prio=5 os_prio=0 tid=0x0000000003583000 nid=0x3f84 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"T2" #13 prio=5 os_prio=0 tid=0x000000001f23e800 nid=0x1338 waiting for monitor entry [0x000000001ff3e000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.wddongtt.wddong.thread.cas.MyThread.run(DeadLockDemo.java:36)
- waiting to lock <0x000000076bbd7d20> (a java.lang.String)
- locked <0x000000076bbd7d58> (a java.lang.String)
at java.lang.Thread.run(Thread.java:748)
"T1" #12 prio=5 os_prio=0 tid=0x000000001f23e000 nid=0x3ef0 waiting for monitor entry [0x000000001fe3f000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.wddongtt.wddong.thread.cas.MyThread.run(DeadLockDemo.java:36)
- waiting to lock <0x000000076bbd7d58> (a java.lang.String)
- locked <0x000000076bbd7d20> (a java.lang.String)
at java.lang.Thread.run(Thread.java:748)
要想避免死锁,只需要破坏掉至少一个构造死锁的必要条件即可。目前只有请求并持有和环路等待条件是可以被破坏的;
上面的案例,我们就可通过环路等待条件就可以避免死锁;更改申请资源的顺序。使用资源申请的有序性原则就可以避免死锁;
修正线程A
和线程B
申请资源的顺序都为 resourceA
=>resouceB
;
Thread threadB = new Thread(() -> {
synchronized (resouceA){
System.out.println("threadB get resouceA");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("threadB waiting resouceB");
synchronized (resouceB){
System.out.println("threadB get resouceB");
}
}
}, "threadB");
java的内存模型:
java 的内存模型分为主内存和线程自己独有的工作内存。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主内存进行操作。 每个线程的工作内存是私有的。
JMM:Java内存模型
,Java 线程之间的通信由Java内存模型(JMM)
控制,JMM
决定一个线程对共享变量的写入何时对另一个线程可见;
JMM的抽象示意图
编译器和处理器为了优化程序执行性能会对指令序列进行重排序。
JMM
对于编译器重排序和处理器重排序制定了一些规则,保证一致的内存可见性保证;
编译器重排序:禁止特定类型的编译器重排序
处理器重排序:要求Java编译器生成指令序列时,插入指定类型的内存屏障;
从Java源代码到最终实际执行的指令序列,会分别经历下面三个重排序:
源代码==>
1. 编译器优化重排序==>
2. 指令级并行重排序==>
3. 内存系统重排序==>
最终执行的指令序列
在JMM
的设计过程中,包含了两个角色程序员
,编译器和处理器
,对于程序员
他们希望内存模型易于理解,易于编程。对于编译器和处理器
而言,希望JMM
的束缚越少越好,以便于更好的做优化。
基于双方角色提出的如此的需求:
JMM
对于程序员
做出了如下的保障,程序员
不需要管编译器和处理器
如何去实现,只需要知道编译器和处理器
实现完之后会满足Happens-before
规则;
JMM
对于编译器和处理器
的要求:在不改变(单线程和正确同步的多线程程序
)的执行结果条件下,编译器和处理器怎么优化都可以;
《
JSR-133:Java Memory Model and Thread Specification
》定义了如下happens-before
规则:
- 程序顺序规则:一个线程中的每个操作,
happens-before
于该线程中的任意后续操作- 监视器锁规则:对一个锁的解锁,
happens-before
于随后对这个锁的加锁volatile
变量规则:对一个volatile
域的写,happens-before
于任意后续对这个volatile
域的读;- 传递性:如果
A happens-before B
,且B happens-before C
,那么A happens-before C
;start()
规则:如果线程A
执行操作ThreadB.start()
,那么A线程
的ThreadB.start()
操作happens-before
于线程B
中的任意操作join()
规则:如果线程A
执行操作ThreadB.join()
并成功返回,那么线程B
中的任意操作happens-before
于线程A
从ThreadB.join()
操作成功返回。
如果
A happens-before B
,JMM
并不要求A一定要在B之前执行。JMM仅仅要求前一个操作(执行结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前;
as-if-serial
语义:不管怎么重排序,单线程程序的执行结果不能被改变(编译器,处理器,runtime
都必须遵守 as-if-serial
语义);
as-if-serial
和 happens-before
的比较:
as-if-serial
保证单线程内程序的执行结果不被改变,
as-if-serial
语义给编写单线程程序的程序员创造了一个环境:单线程程序是按程序的顺序来执行的。
happens-before
关系保证正确同步的多线程程序的执行结果不被改变,都是在不改变程序执行结果的前提下,尽可能的提高程序执行的并发度;
happens-before
关系给编写正确同步的多线程程序的程序员创造了一个环境:正确同步的多线程程序是按happens-before
指定的顺序来执行的
synchronized
public class SynchronizedDemo {
public static void main(String[] args) {
SynchronizedCase synchronizedCase = new SynchronizedCase();
// 可以任意的去指定是锁Class模板还是具体对象
// synchronized (SynchronizedCase.class){
synchronized (synchronizedCase){
}
}
}
public class SynchronizedCase{
//synchronized修饰静态方法,锁的是Class
public synchronized static void method1(){
}
//synchronized修饰实例方法,锁的是方法的调用对象
public synchronized void method2(){
}
}
private Lock lock = new ReentrantLock();
lock.lock();
try{}finally{
lock.unlock();
}
Synchronized
是内置的 java
内置关键字,Lock
是一个Java
类synchronized
可以给类,方法,代码块加锁,而Lock
只能给代码块加锁Synchronized
可以自动释放锁,Lock
需要手动释放(不释放会造成死锁)Java虚拟机中的提供的轻量级的同步机制
作用:
通过JIT编译器
生成的汇编指令查看 volatile
进行写操作时,会多出一行Lock
前缀的指令;
Lock
前缀的指令在多核处理器下会引发两件事情:
如何使其他CPU中的该内存地址数据无效?
在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置为无效状态;当处理器需要对数据进行操作时,会重新从系统内存中获取;
查询
Java
代码的汇编指令需要设置JVM
允许参数:-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -Xcomp;
如果你的jdk
版本小于等于8还要在jdk
里面添加Hsdis
插件,将该插件目录里面的两个文件(hsdis-amd64.dll,hsdis-i386.dll
)复制到%JAVA_HOME%\jre\bin\server
下,然后运行你的Java
程序,就可以看到控制台里面一堆的汇编指令代码输出了。public class Singleton { private volatile static Singleton myinstance; public static Singleton getInstance() { if (myinstance == null) { synchronized (Singleton.class) { if (myinstance == null) { myinstance = new Singleton();//对象创建过程,本质可以分文三步 } } } return myinstance; } public static void main(String[] args) { Singleton.getInstance(); } }
不加
volatile
关键字时在控制台输出指令搜索myinstance
可以看到如下两行0x00000000038064dd: mov %r10d,0x68(%rsi) 0x00000000038064e1: shr $0x9,%rsi 0x00000000038064e5: movabs $0xf1d8000,%rax 0x00000000038064ef: movb $0x0,(%rsi,%rax,1) ;*putstatic myinstance ; - com.it.edu.jmm.Singleton::getInstance@24 (line 22)
加了
volatile
关键字后,变成下面这样了:0x0000000003cd6edd: mov %r10d,0x68(%rsi) 0x0000000003cd6ee1: shr $0x9,%rsi 0x0000000003cd6ee5: movabs $0xf698000,%rax 0x0000000003cd6eef: movb $0x0,(%rsi,%rax,1) 0x0000000003cd6ef3: lock addl $0x0,(%rsp) ;*putstatic myinstance ; - com.it.edu.jmm.Singleton::getInstance@24 (line 22)
public class Test {
//线程1
static boolean isFlag = false;
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
while(!isFlag){
doSomething();
}
},"A").start();
TimeUnit.SECONDS.sleep(1);
System.out.println("sleep ending");
//main 等待线程A启动完成
isFlag = true;
System.out.println("main run ending");
}
public static void doSomething(){
System.out.println("1");
}
/**
*
*
* 当执行 doSomething 方法时,不执行打印语句时,由于 isFlag 变量的可见性,程序将会变为死循环。
* 需要添加 volatile 关键字,保证isFlag字段的可见性
*
* public void println(String x) {
* synchronized (this) {
* print(x);
* newLine();
* }
* }
* 当执行 doSomething 方法时,执行打印语句时
* 当执行dosomething方法打印语句时,无volatile关键字,while循环将在执行一段时间后停止。
*/
}
线程1
运行时,会将变量的值拷贝到自己的工作内存中去, 当线程2
更改了 stop
变量的值之后,但是没有来得及写入内存。就会造成问题。
public class Test1 {
private static volatile int num = 0;
//使用JUC下的原子包,解决volatile 不能解决原子性的问题
private static volatile AtomicInteger numAtomic = new AtomicInteger(0);
public static void main(String[] args) {
for (int i = 0; i < 10000; i++) {
new Thread(()->{
for (int j = 0; j < 100; j++) {
doSomething();
}
}).start();
}
//保证上方创建的线程执行完全
while (Thread.activeCount()>2){
Thread.yield();
}
System.out.println("num:"+num);
}
public static void doSomething(){
num++;
}
// public static void doSomething(){
// numAtomic.incrementAndGet();
// }
}
Volatile 可以通过内存屏障,在volatile 修饰的变量操作前后,添加相应的屏障。
volatile
的 System.out.println
问题volatile
的 System.out.println
问题
乐观锁,悲观锁,公平锁,非公平锁,独占锁,共享锁,可重入锁,自旋锁;
乐观锁和悲观锁是在数据库中引入的名词,但是在并发包锁里面也引入了类似的思想。
悲观锁指对数据被外界修改持保守态度,认为数据很容易就会被其他线程修改。所以在数据被处理前先对数据进行加锁,并且在整个数据处理的过程中。使数据处于锁定状态;
synchronized
就是悲观锁。乐观锁的实现方式
使用版本号来确定独到的数据与提交的数据是否一致,提交后修改版本号,不一致时可以采取丢弃和再次尝试的策略
update table1 set name = #{name},age = #{age} where id = #{id} and version = # {version};
CAS
(乐观锁是一种思想。CAS
是这种思想的一种实现方式)
CAS
是乐观锁的一种实现方式之一
根据线程获取锁的抢占机制,锁可以分为公平和非公平锁;
公平锁: 非常公平, 不能够插队,必须先来后到!
非公平锁:非常不公平,可以插队 (默认都是非公平)
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
根据锁只能被单个线程持有还是能被多个线程共同持有,锁可以分为独占锁和共享锁;
读写锁(
ReentrantReadWriteLock
): 相对于Lock锁更加细粒度的控制
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCacheLock myCache = new MyCacheLock();
// 写入
for (int i = 1; i <= 10; i++) {
final int temp = i;
new Thread(() -> {
myCache.put(temp, temp);
}, String.valueOf(i)).start();
}
// 读取
for (int i = 1; i <= 10; i++) {
final int temp = i;
new Thread(() -> {
myCache.get(temp);
}, String.valueOf(i)).start();
}
}
}
/**
* 自定义缓存
* 没有进行加锁操作,在多线程写入的时候,很容易被其他的写入线程插入写入操作
* 通过读(共享锁)写(独占锁)锁解决插入时被其他线程插队的情况
*/
class MyCache {
private volatile Map<Integer, Integer> map = new HashMap<>();
public void put(Integer key, Integer value) {
System.out.println(Thread.currentThread().getName() + "写入" + key);
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "写入OK");
}
public void get(Integer key) {
System.out.println(Thread.currentThread().getName() + "读取" + key);
Object o = map.get(key);
System.out.println(Thread.currentThread().getName() + "读取OK");
}
}
/**
* 加锁的,线程安全的
*/
class MyCacheLock {
private volatile Map<Integer, Integer> map = new HashMap<>();
// 读写锁: 相对于Lock锁更加细粒度的控制
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
public void put(Integer key, Integer value) {
//写锁
readWriteLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "写入" + key);
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "写入OK");
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.writeLock().unlock();
}
}
public void get(Integer key) {
//读锁
readWriteLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "读取" + key);
Object o = map.get(key);
System.out.println(Thread.currentThread().getName() + "读取OK");
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.readLock().unlock();
}
}
}
可重入锁:synchronized/ReentrantLock
都是可重入锁
可重入锁(也叫递归锁:递归传递同一把锁)
import java.util.concurrent.atomic.AtomicReference;
/**
* 自旋锁
*/
public class SpinlockDemo {
// int 0
// Thread null
AtomicReference<Thread> atomicReference = new AtomicReference<>();
// 加锁
public void myLock() {
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName() + "==> mylock");
// 自旋锁
while (!atomicReference.compareAndSet(null, thread)) {
}
}
// 解锁
public void myUnLock() {
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName() + "==> myUnlock");
atomicReference.compareAndSet(thread, null);
}
}
CAS(compare and swap)
是乐观锁的一种实现方式,JUC
中的很多工具类都是基于CAS
实现的。
CAS
: 比较当前工作内存中的值和主内存中的值,如果这个值是期望的,那么则执行操作!如果不是就 一直循环!
CAS
中的三个操作数:
- 内存位置( V )
- 预期原值( A )
- 新值( B )
CAS
是通过无限循环来获取数据的,如果在第一轮循环中,a 线程
获取地址里面的值被b线程
修改了,那么a 线程
需要自旋,到下次循环才有可能机会执行。
public final class Unsafe {
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
//var2 偏移量 offset
//var4 = 1;
do {
//获取内存地址中的值
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
// var1 + var2 = var5 时,执行 var5 + var4
return var5;
}
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
}
CAS
的缺点ABA
问题对于只能保证一个共享变量的原子操作:从
JDK1.5
开始,JDK
提供了AtomicReference
类来保证引用对象之间的原子性,就可以把多个变量放在一个对象中来进行CAS
操作;
ABA
问题ABA
问题什么是ABA问题?
两个线程,线程1
和线程2
。 线程1
从内存位置V
取出 值A
,线程2
从内存位置V
取出值A
。线程2
修改A值
变成 B 值
。 线程2
修改B 值
又变成A 值
。 线程1
进行CAS
操作发现 内存中仍然是 A 值
。线程1
操作成功。
ABA
问题如何解决Java1.5
开始 JDK
中 atomic
包中的 atomicStampedReference
类 来解决ABA
问题
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicStampedReference;
public class CASDemo1 {
//AtomicStampedReference 注意,如果泛型是一个包装类,注意对象的引用问题
static AtomicStampedReference<Integer> atomicStampedReference = new
AtomicStampedReference<>(1, 1);
// CAS compareAndSet : 比较并交换!
public static void main(String[] args) {
new Thread(() -> {
int stamp = atomicStampedReference.getStamp(); // 获得版本号
System.out.println("a1=>" + stamp);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(atomicStampedReference.compareAndSet(1, 2,
stamp,
stamp + 1));
stamp = atomicStampedReference.getStamp();
System.out.println("a2=>" + atomicStampedReference.getStamp());
System.out.println(atomicStampedReference.compareAndSet(2, 1,
stamp,
stamp + 1));
System.out.println("a3=>" + atomicStampedReference.getStamp());
}, "a").start();
// 乐观锁的原理相同!
new Thread(() -> {
int stamp = atomicStampedReference.getStamp(); // 获得版本号
System.out.println("b1=>" + stamp);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(atomicStampedReference.compareAndSet(1, 6,
stamp, stamp + 1));
System.out.println("b2=>" + atomicStampedReference.getStamp());
}, "b").start();
}
}
执行结果:
a1=>1
b1=>1
true
a2=>2
true
a3=>3
false
b2=>3
JDK
的rt.jar
包中 Unsafe
类提供了硬件级别的原子性操作,Unsafe
类中的方法都是 native
方法,它们使用JNI
的方式访问 C++
实现库。
为了解决计算机系统中主内存和 CPU
之间运行速度差问题,会在CPU
与主内存之间添加一级或多级高速缓冲存储器(Cache)。这个 Cache 一般被集成到 CPU 内部,所以也叫 CPU cache。如下为两级 Cache 结构。
在 Cache 内部是按行存储的,其中每一行称为一个 Cache 行,Cache 行是 Cache 与主内存进行数据交换的单位,Cache 行的大小一般为 2 的幂次数字节;
当 CPU 访问某个变量时,会先去 CPU Cache 中查询变量是否存在,存在直接过去,当 CPU Cache 中不存在时,则从主内存中获取变量,由于上面我们介绍了是以Cache行
为交换单位的,所以可能会把多个变量存放到一个Cache行
中。当多个线程同时修改一个Cache行
中的多个变量时,由于同时只能有一个线程操作缓存行,所以相比将每个变量放到缓存行,性能会有所下降,这个就是伪共享;
产生的原因就是交换单位为一个 Cache行
,一个 Cache行
中可能同时包含多个变量。当多个线程同时操作这个 Cache行
上的变量时,就会造成 伪共享;
在JDK8
之前一般通过字节填充的方式避免伪共享的问题,也就是创建一个变量时使用填充字段 填充 该变量所在的缓存行。如下代码
public final static class FilledLong{
public volatile long value = 0L;
public long p1,p2,p3,p4,p5,p6;
}
// 假设一个缓存行大小为 64 字节,如上的代码一个long类型为 8 字节,我们填充了6个long类型;
// FilledLong 类对象对象头占 8 个字节,value字段占8个字节,填充了6个long类型48字节
// 刚好 8 + 8 + 48 = 一个`cache行` 64字节;
JDK8
提供了一个 sun.misc.Contended
注解,用来解决伪共享问题;
// Thread类中就使用到了 @sun.misc.Contended 注解;
public class Thread implements Runnable {
@sun.misc.Contended("tlr")
long threadLocalRandomSeed;
/** Probe hash value; nonzero if threadLocalRandomSeed initialized */
@sun.misc.Contended("tlr")
int threadLocalRandomProbe;
/** Secondary seed isolated from public ThreadLocalRandom sequence */
@sun.misc.Contended("tlr")
int threadLocalRandomSecondarySeed;
}
需要注意的是,在默认情况下,@Contended 注解只用于 Java 核心类,比如 rt 包下的类,如果用户类路径下的类需要使用这个注解,则需要添加
JVM参数
:-XX:-RestrictContended
填充的宽度默认为128,若自定义宽度则可以设置-XX:ContendedPaddingWidth
参数;
JUC
Fork/Join
Fork/Join
框架是Java7
提供的一个用于并行执行任务的框架,是一个把大任务拆分为若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架;
并行执行:提高效率
大数据量:小数据量完全没有使用
Fork/Join
的必要;
- 步骤一分割任务
- 步骤二执行任务并合并结果
任务分割 fork
,结果合并join
。这点在代码编写时也有所体现;
工作窃取算法:是指某个线程从其他队列里窃取任务来执行。
一个很大的任务,分割成为了多个互不依赖的子任务,为了减少线程间的竞争,把子任务分别放到不同的队列里,并为每个队列创建一个单独的线程来执行队列里的任务,线程和队列一一对应;
如上图,
线程1
窃取线程2
中的任务执行,由于他们会访问同一个队列,所以为了减少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列,被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务的线程永远从双端队列的尾部拿任务。
**优点:**充分利用线程进行并行计算,减少线程间竞争
**缺点:**某些时候存在竞争,比如双端队列中只有一个任务时。
Fork/Join
的使用要使用Fork/Join
框架,必须先创建一个Fork/Join
任务(ForkJoinTask
),通过RecursiveAction
,RecursiveTask
类实现。通过ForkJoinPool
执行ForkJoinTask
;
RecursiveAction
和RecursiveTask
都是ForkJoinTask
的子类;
RecursiveAction
:没有返回结果的任务
RecursiveTask
:有返回结果的任务
Fork/join
的好处累加 1~10_0000_0000之间所有的数,下面的案例使用三个方法,测试三种方法的效率。分别是
普通累加
、Fork/Join
、Stream并行流
;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.RecursiveTask;
import java.util.stream.LongStream;
/**
* 求和计算的任务!
*
* 如何使用 forkjoin
* 1、forkjoinPool 通过它来执行
* 2、计算任务 forkjoinPool.execute(ForkJoinTask task)
* 3. 计算类要继承 ForkJoinTask
*
*public class ForkJoinPool extends AbstractExecutorService {
* public void execute(ForkJoinTask> task) {
* if (task == null)
* throw new NullPointerException();
* externalPush(task);
* }
* }
*
* public abstract class RecursiveTask extends ForkJoinTask {}
*/
public class ForkJoinDemo extends RecursiveTask<Long> {
private Long start;
private Long end;
// 临界值
private Long temp = 10000000L;
public ForkJoinDemo(Long start, Long end) {
this.start = start;
this.end = end;
}
// 计算方法
@Override
protected Long compute() {
if ((end - start) < temp) {
Long sum = 0L;
for (Long i = start; i <= end; i++) {
sum += i;
}
return sum;
} else { // forkjoin 递归
long middle = (start + end) / 2; // 中间值
ForkJoinDemo task1 = new ForkJoinDemo(start, middle);
task1.fork(); // 拆分任务,把任务压入线程队列
ForkJoinDemo task2 = new ForkJoinDemo(middle + 1, end);
task2.fork(); // 拆分任务,把任务压入线程队列
return task1.join() + task2.join();
}
}
}
class Test {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// test1(); // 12224 sum=500000000500000000 时间:5267
test2(); // 10038 sum=500000000500000000 时间:3240
// test3(); // 153 sum=500000000500000000时间:190
}
// 普通程序员
public static void test1() {
Long sum = 0L;
long start = System.currentTimeMillis();
for (Long i = 1L; i <= 10_0000_0000; i++) {
sum += i;
}
long end = System.currentTimeMillis();
System.out.println("sum=" + sum + " 时间:" + (end - start));
}
// 会使用ForkJoin
public static void test2() throws ExecutionException, InterruptedException {
long start = System.currentTimeMillis();
ForkJoinPool forkJoinPool = new ForkJoinPool();
ForkJoinTask<Long> task = new ForkJoinDemo(0L, 10_0000_0000L);
ForkJoinTask<Long> submit = forkJoinPool.submit(task);// 提交任务
Long sum = submit.get();
long end = System.currentTimeMillis();
System.out.println("sum=" + sum + " 时间:" + (end - start));
}
// Stream并行流
public static void test3() {
long start = System.currentTimeMillis();
// .range(): ()
// .rangeClose: (]
long sum = LongStream.rangeClosed(0L, 10_0000_0000L).parallel().reduce(0, Long::sum);
long end = System.currentTimeMillis();
System.out.println("sum=" +sum + "时间:" + (end - start));
}
}
AtomicLong
,Atomic
原子包下的类public class AtomicDemo {
private static AtomicInteger atomicInteger = new AtomicInteger(0);
private static AtomicLong atomicLong = new AtomicLong(0);
private static int count = 0;
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
new Thread(()->{
for (int j = 0; j < 1000; j++) {
atomicInteger.incrementAndGet();
atomicLong.incrementAndGet();
count++;
}
}).start();
}
//等待线程执行完毕
while (Thread.activeCount()>2){}
System.out.println("atomicInteger:"+atomicInteger);
System.out.println("atomicLong:"+atomicLong);
System.out.println("count:"+count);
}
}
执行结果:
atomicInteger:20000
atomicLong:20000
count:19207
public class AtomicLong extends Number implements java.io.Serializable { private static final Unsafe unsafe = Unsafe.getUnsafe(); //调用unsafe方法,原子性设置value值为原始值+1,返回值为递增后的值 public final long incrementAndGet() { return unsafe.getAndAddLong(this, valueOffset, 1L) + 1L; } } public final class Unsafe{ public final long getAndAddLong(Object var1, long var2, long var4) { long var6; do { var6 = this.getLongVolatile(var1, var2); //CAS操作保证安全(代码A) } while(!this.compareAndSwapLong(var1, var2, var6, var6 + var4)); return var6; } }
在没有原子类的情况下,实现累加需要使用一定的同步措施,比如synchronized
等。但是这些都阻塞算法,对性能有一定的消耗,本章介绍的Atomic
原子操作类都是CAS
非阻塞算法,性能更好。但是在高并发情况下AtomicLong
还会存在性能问题,JDK8
提供了一个高并发下性能更好的LongAdder
类。
LongAdder
AtomicLong
通过CAS
提供了非阻塞的原子性操作,相比使用过阻塞算法的同步器性能已经提升很多了。但是在使用AtomicLong
时存在一个问题,在高并发下大量线程会同时竞争更新同一个原子变量(上面代码块的代码A
处),由于同时只有一个线程CAS
会成功,这就造成大量线程竞争失败后,通过自旋导致白白浪费CPU
资源;
在这种背景下,JDK8
新增了LongAdder
来克服高并发下AtomicLong
的缺点;
LongAdder
的实现思路:把一个变量分解为多个变量,让同样多的线程去竞争多个资源;
LongAdder
内部维护多个Cell
变量,同等并发量情况下,减少了争夺共享资源的并发量。多个线程在争夺同一个Cell
原子变量时如果失败,它不是在当前Cell
一直自旋CAS
重试,而是尝试其他Cell
,增加了当前线程CAS
的成功性;最后在获取LongAdder
当前值时,是把所有Cell
变量的value
值累加再加上base
返回的。
JUC
下的并发List
(CopyOnWriteArrayList
)CopyOnWriteArrayList
LockSupport
JDK
中的rt.jar
包里的LockSupport
是一个工具类,主要作用是挂起和唤醒线程,该工具类是创建锁和其他同步类的基础;
LockSupport
使用Unsafe
类实现;
介绍LockSupport
之前,需要特别提到许可证
,LockSupport
类与每个使用它的线程都会关联一个许可证,默认情况下调用LockSupport
类的方法的线程不持有许可证。那么LockSupport
和许可证
的关系,我们从下面几个LockSupport
中的函数讲解;
void park()
如果调用park
方法的线程已经拿到了LockSupport
关联的许可证,则调用LockSupport.park()
时会直接返回,否则禁止参与线程调度,直接阻塞挂起
public static void park() {
UNSAFE.park(false, 0L);
}
public class LockSupportDemo {
public static void main(String[] args) {
System.out.println("begin park");
LockSupport.park();
System.out.println("end park");
}
}
执行结果:
begin park
由于未拿到许可证,导致线程被阻塞挂起;
在其他线程调用unpark(Thread thread)
方法并且将当前线程做为参数时,调用park
方法而被阻塞的线程会被返回。另外如果其他线程调用了阻塞线程的interrupt()
方法,设置了中断标志或者线程被虚假唤醒,则阻塞线程也返回;
因调用park方法而被阻塞的线程被其他线程中断而返回时不会抛出
InterruptedException
异常;
void unpark(Thread thread)
当一个线程调用unpark()
时,
thread
线程没有持有与LockSupport
类关联的许可证,那么就让其thread
线程持有;thread
之前因调用park
方法而被挂起,那么该线程将被唤醒;thread
之前没有调用park
方法,那么调用unpark
方法之后再调用park
方法,将会直接返回。public class LockSupportDemo {
public static void main(String[] args) {
System.out.println("begin park");
//使当前线程获取到许可证
LockSupport.unpark(Thread.currentThread());
LockSupport.park();
System.out.println("end park");
}
}
执行结果:
begin park
end park
public class LockSupportDemo2 {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
System.out.println("child thread begin park!");
LockSupport.park();
System.out.println("child thread unpark");
}, "thread");
//启动子线程
thread.start();
//main 线程等待1s
Thread.sleep(1000);
System.out.println("main thread begin unpark");
//调用unpark使thread线程持有许可证,使park方法返回
LockSupport.unpark(thread);
}
}
执行结果:
child thread begin park!
main thread begin unpark
child thread unpark
同样,前文我们也介绍了另外如果其他线程调用了阻塞线程的interrupt()
方法,设置了中断标志或者线程被虚假唤醒,则阻塞线程也返回;这里面提到了两个关键字中断和虚假唤醒,我们看下面的案例
public class LockSupportDemo3 {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
System.out.println("child thread begin park!");
//调用park方法挂起,只有被中断才会被退出循环
//这里使用while而不是使用if进行判断就是防止虚假唤醒的情况发生;
while (!Thread.currentThread().isInterrupted()){
System.out.println("child thread ready park!");
LockSupport.park();
}
System.out.println("child thread unpark");
}, "thread");
//启动子线程
thread.start();
//main 线程等待1s
Thread.sleep(1000);
System.out.println("main thread begin unpark");
//中断thread线程
thread.interrupt();
}
}
执行结果:
child thread begin park!
child thread ready park!
main thread begin unpark
child thread unpark
如上代码,只有中断子线程才会运行结束,即使你调用unpark
子线程也不会结束;
condition
本节需要注意的点,同步队列和等待队列;
关于
condition
的介绍,在前面的Lock
锁精确唤醒已经初步有介绍;
Condition
对象是由Lock
对象(调用Lock
对象的newCondition()
方法)创建出来的,Condition
依赖Lock
对象。
Condition
的使用方式比较简单,需要注意的是在调用Condition
方法前要先获取到锁,否则会抛出IllegalMonitorStateException
异常;
public class ConditionDemo {
private ReentrantLock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
private Condition condition2 = lock.newCondition();
public void conditionWait() throws InterruptedException {
lock.lock();
try {
condition.await();
}finally {
lock.unlock();
}
}
public void conditionSignal() throws InterruptedException {
lock.lock();
try {
condition.signal();
}finally {
lock.unlock();
}
}
}
condition
的实现分析 public Condition newCondition() {
return sync.newCondition();
}
final ConditionObject newCondition() {
return new ConditionObject();
}
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer{
public class ConditionObject implements Condition{
}
}
ConditionObject
是同步器AbstractQueuedSynchronizer
的内部类,每个Condition
对象都包含着一个队列(等待队列),该队列是Condition
对象实现等待/通知
功能的关键。
一个Lock锁(更确切的说法是同步器)拥有一个同步队列和多个等待队列
下面我们看下Condition
的await
、signal
系列方法;
await
的实现分析如果一个线程调用了Condition.await()
,那么该线程将会释放锁,构造成节点加入等待队列并进入等待状态。
构造成节点:节点的定义复用了同步器中节点的定义,也就是说,同步队列和等待队列中节点类型都是同步器的静态内部类
AbstractQueuedSynchronizer.Node
;关于
Node
节点的详细信息,在后续的AQS
篇幅会具体讲解。
public class ConditionObject implements Condition{
/** First node of condition queue. */
private transient Node firstWaiter;
/** Last node of condition queue. */
private transient Node lastWaiter;
}
一个Condition
包含一个等待队列,Condition
拥有首节点firstWaiter
和尾结点lastWaiter
。当await
方法调用时,会以当前线程构造节点,并将节点从尾部加入等待队列。
新增节点只需要将原有的尾结点
nextWaiter
指向新增的节点,并且更新尾结点即可;
从队列的角度看
await
方法相当于同步队列的首节点(获取了锁的节点)移动到Condition
的等待队列中;
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
//当前线程加入等待队列
Node node = addConditionWaiter();
//释放同步状态(释放锁)
int savedState = fullyRelease(node);
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
能够成功调用await
方法,也就是意味着成功获取到了锁,也就是同步队列
中的首节点,也就是当前线程构造成节点并加入等待队列
,然后释放同步状态,唤醒同步队列
中的后继节点,然后当前线程会进入等待状态;
signal
的实现分析如果一个线程调用了Condition.siganl()
,将会唤醒在等待队列
中等待时间最长的节点(首节点),在唤醒节点之前,会将节点移动到同步队列;
public final void signal() {
if (!isHeldExclusively())
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) &&
(first = firstWaiter) != null);
}
final boolean transferForSignal(Node node) {
/*
* If cannot change waitStatus, the node has been cancelled.
*/
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);
int ws = p.waitStatus;
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
通过调用同步器的enq(Node node)
方法,等待队列中的头节点线程安全的移动到同步队列。当节点移动到同步队列之后,当前线程再通过LockSupport
唤醒该节点的线程;
被唤醒后的线程,将从await方法中的while循环中退出
isOnSyncQueue(Node node)
方法,返回true
,节点已经在同步队列中,进而调用同步器的acquireQueued(final Node node, int arg)
方法加入到获取同步状态的竞争中。
Condition
的signalAll
方法相当于对等待队列中的每一个节点均执行一次signal
方法,效果就是将等待队列中所有节点全部移动到同步队列中,并唤醒每个节点的线程;
AQS
原理AQS原理
ConcurrentLinkedQueue
BlockingQueue
)JDK 7
提供了7个阻塞队列,如下。
ArrayBlockingQueue
:一个由数组结构组成的有界阻塞队列。
LinkedBlockingQueue
:一个由链表结构组成的有界阻塞队列。
PriorityBlockingQueue
:一个支持优先级排序的无界阻塞队列。
DelayQueue
:一个使用优先级队列实现的无界阻塞队列。
DelayQueue
是一个支持延时获取元素的无界阻塞队列。队列使用PriorityQueue
来实现。队 列中的元素必须实现Delayed
接口,在创建元素时可以指定多久才能从队列中获取当前元素。 只有在延迟期满时才能从队列中提取元素。
SynchronousQueue
:一个不存储元素的阻塞队列。
同步队列和其他的
BlockingQueue
不一样,SynchronousQueue
不存储元素,put
了一个元素,必须从里面先take
取出来,否则不能再put
进去值!(没有容量, 进去一个元素,必须等待取出来之后,才能再往里面放一个元素!)
LinkedTransferQueue
:一个由链表结构组成的无界阻塞队列。
LinkedBlockingDeque
:一个由链表结构组成的双向阻塞队列。
双向阻塞队列,可以从队列的两端插入和移出元素。多了
addFirst
、addLast
、offerFirst
、offerLast
、peekFirst
和peekLast
等方法
BlockingQueue
)四组API
方式 | 抛出异常 | 有返回值,不抛出异常 | 阻塞等待 | 超时等待 |
---|---|---|---|---|
添加 | add(e) |
offer(e) |
put(e) |
offer(e,time,unit) |
移除 | remove() |
poll() |
take() |
poll(time,unit) |
检测队首元素 | element() |
peek() |
- | - |
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.TimeUnit;
public class Test {
// 队列的大小 2
static ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(2);
public static void main(String[] args) {
test1();
}
/**
* 抛出异常
*/
public static void test1() {
System.out.println(blockingQueue.add("a"));
System.out.println(blockingQueue.add("b"));
// IllegalStateException: Queue full 抛出异常!(超出队列大小抛出异常)
System.out.println(blockingQueue.add("c"));
System.out.println("===========");
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
// java.util.NoSuchElementException 抛出异常!
System.out.println(blockingQueue.remove());
}
/**
* 有返回值,没有异常
*/
public static void test2(){
System.out.println(blockingQueue.offer("a"));
System.out.println(blockingQueue.offer("b"));
System.out.println(blockingQueue.offer("c")); // 返回 false 不抛出异常!
System.out.println("============================");
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll()); // 返回 null 不抛出异常!
}
/**
* 等待,阻塞(一直阻塞)
*/
public static void test3() throws InterruptedException {
// 一直阻塞
blockingQueue.put("a");
blockingQueue.put("b");
blockingQueue.put("c"); // 队列没有位置了,一直阻塞
System.out.println(blockingQueue.take());
System.out.println(blockingQueue.take());
System.out.println(blockingQueue.take()); // 没有这个元素,一直阻塞
}
/**
* 等待,阻塞(等待超时)
*/
public static void test4() throws InterruptedException {
blockingQueue.offer("a");
blockingQueue.offer("b");
blockingQueue.offer("c",2, TimeUnit.SECONDS); // 等待超过2秒就退出
System.out.println("===============");
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
blockingQueue.poll(2,TimeUnit.SECONDS); // 等待超过2秒就退出
}
}
如果队列是空的,消费者会一直等待。当生产者添加元素时,消费者是如何知道当前队列有元素的呢?JDK
采用的是通知模式实现的。
通知模式:当生产者往满的队列里添加元素时会阻塞住生产者,当消费者消费了一个队列中的元素后,会通知生产者当前队列可用。
我们通过查看ArrayBlockingQueue
的阻塞方法put
,take
源码。
public class ArrayBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == items.length)
notFull.await();
enqueue(e);
} finally {
lock.unlock();
}
}
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
notEmpty.await();
return dequeue();
} finally {
lock.unlock();
}
}
private void enqueue(E x) {
// assert lock.getHoldCount() == 1;
// assert items[putIndex] == null;
final Object[] items = this.items;
items[putIndex] = x;
if (++putIndex == items.length)
putIndex = 0;
count++;
notEmpty.signal();
}
private E dequeue() {
// assert lock.getHoldCount() == 1;
// assert items[takeIndex] != null;
final Object[] items = this.items;
@SuppressWarnings("unchecked")
E x = (E) items[takeIndex];
items[takeIndex] = null;
if (++takeIndex == items.length)
takeIndex = 0;
count--;
if (itrs != null)
itrs.elementDequeued();
notFull.signal();
return x;
}
}
通过源码我们可以发现ArrayBlockingQueue
使用了Condition
来实现。当向队列中插入元素,队列不可用时,通过await
方法。之前我们学过的Condition
章节,也知道了Condition
的await
也就最终调用了LockSupport.park(this)
来实现;
import java.util.Random;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
public class DelayQueueDemo {
static class DelayedEle implements Delayed {
private final long delayTime; //延迟时间
private final long expire; //到期时间
private String taskName;//任务名称
public DelayedEle(long delay, String taskName) {
delayTime = delay;
this.taskName = taskName;
expire = System.currentTimeMillis() + delay;
}
/**
* 剩余时间=到期时间 当前时间
*/
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(this.expire - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
}
/**
* 优先级队列里面的优先级规则
*/
@Override
public int compareTo(Delayed o) {
return (int) (this.getDelay(TimeUnit.MILLISECONDS) -
getDelay(TimeUnit.MILLISECONDS));
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder(" DelayedEle { ");
sb.append(" delay=").append(delayTime);
sb.append(", expire=").append(expire);
sb.append(", taskName='").append(taskName).append('\'');
sb.append('}');
return sb.toString();
}
}
/**
* 创建了一个延迟队列,然后使用随机数生成器生成了 10 个延迟任
* 务,最后通过循环依次获取延迟任务,并打印 。
*/
public static void main(String[] args) {
//创建delay队列
DelayQueue<DelayedEle> delayQueue = new DelayQueue<DelayedEle>();
//创建延迟任务
Random random = new Random();
for (int i = 0; i < 10; ++i) {
DelayedEle element = new DelayedEle(random.nextInt(500), " task :" + i);
delayQueue.offer(element);
//依次取出任务并打印
DelayedEle ele = null;
try {
//循环,如采想逃免虚假唤醒,则不能把全部元素都打印出来
for (; ; ) {
//获取过期任务并打印
while ((ele = delayQueue.take()) != null) {
System.out.println(ele.toString());
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
创建延迟任务 DelayedEle
其中 delayTime
表示当前任务需要延迟多少 ms
时间过期, expire
是当前时间的 ms
值加上 delayTime
的值 。另 外,实现了 Delayed
接口,实现了 long getDelay(TimeUnit unit)
方法用来获取当前元素还剩下多少时间过期,实现了 int compareTo(Delayed o)
方法用来决定优先级队列元素的比较规则;
CountDownLatch
(减法计数器)//构造方法
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
//设置减法计数器
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 0; i < 5; i++) {
final String temp = i+"";
new Thread(()->{
System.out.println(temp);
countDownLatch.countDown(); //计数器减一
}).start();
}
//等待计数器归零,再往下执行
// countDownLatch.await();
// public boolean await(long timeout, TimeUnit unit);
countDownLatch.await(10, TimeUnit.SECONDS);//等待超时时间
System.out.println("CountDownLatchDemo ending ");
}
}
CyclicBarrier
(加法计数器)/**
* 加法计数器
* 集齐7颗龙珠召唤神龙
*/
public class CyclicBarrierDemo {
public static void main(String[] args) {
// 召唤龙珠的线程(计数器达到7),计数器达到设定值,执行Runnable类的run();
//CyclicBarrier 的两种构造法方法
//public CyclicBarrier(int parties, Runnable barrierAction);
//public CyclicBarrier(int parties);
CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> {
System.out.println("召唤神龙成功!");
});
for (int i = 1; i <= 7; i++) {
final int temp = i;
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "收集" + temp + " 个龙珠");
try {
cyclicBarrier.await(); // 等待,计数器加一
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}
CountDownLatch
,CyclicBarrier
比较调用CountDownLatch
的countDown
方法后,当前线程并不会阻塞,会继续往下执行;而调用CyclicBarrier
的await
方法,会阻塞当前线程,直到CyclicBarrier
指定的线程全部都到达了指定点的时候,才能继续往下执行;
CountDownLatch
是不能复用的,而CyclicLatch
是可以复用的。
CountDownLatch
一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行;而CyclicBarrier
一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;CountDownLatch
强调一个线程等多个线程完成某件事情。CyclicBarrier
是多个线程互等,等大家都完成,再携手共进。
Semaphore
(信号量)作用: 限制并发数
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
/**
* Semaphore 的两种构造方法
* public Semaphore(int permits, boolean fair);
* public Semaphore(int permits);
*/
public class SemaphoreDemo {
public static void main(String[] args) {
// 线程数量:停车位! 限流!
Semaphore semaphore = new Semaphore(3);//限定信号量的最大值
for (int i = 1; i <= 6; i++) {
new Thread(() -> {
try {
semaphore.acquire(); //获取信号量(假设信号量已满,等待,一直等到信号量被释放)
System.out.println(Thread.currentThread().getName() + "得到了車位");
if (Thread.currentThread().getName().equals("1")){
TimeUnit.SECONDS.sleep(3);
}else TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName() + "離開了車位");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release(); // release() 释放信号量(唤醒其他等待的线程)
}
}, String.valueOf(i)).start();
}
}
}
Exchanger(交换者)
是一个用于县城管间协作的工具类。Exchanger
用于线程间的数据交换。它提供一个同步点,在这个同步点,两个线程可以彼此交换数据。
如果第一个线程先执行
exchange()
,它会一直等待第二个线程也执行exchange()
方法,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方;
import java.util.concurrent.Exchanger;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ExchangerDemo {
public static void main(String[] args) {
Exchanger<String> exgr = new Exchanger<>();
ExecutorService threadPool = Executors.newFixedThreadPool(2);
threadPool.execute(()->{
String A = "待交换数据A";
try {
String exchangeB = exgr.exchange(A);
System.out.println("A这边拿到的交换数据是:"+exchangeB);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
threadPool.execute(()->{
String B = "待交换数据B";
try {
String exchangeA = exgr.exchange(B);
System.out.println("B这边拿到的交换数据是:"+exchangeA);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
threadPool.shutdown();
}
}
执行结果:
A这边拿到的交换数据是:待交换数据B
B这边拿到的交换数据是:待交换数据A
线程池的好处(线程复用,控制最大并发数,管理线程)
newFixThreadPool
//创建固定线程数的线程池newCachedThreadPool
//大小无界的线程池,适用执行很多的短期异步任务的小程序。newScheduledThreadPool
//固定个数的线程池。适用于需要多个后台线程执行周期任务,为了满足资源管理的需求而需要限制后台线程的数量的应用场景;newSingleThreadExecutor
//创建单个线程的线程池newThreadPoolExecuted
(自定义线程池)为什么阿里巴巴开发手册中强制不建议使用Executors创建线程池,使用Executors创建的线程池有什么弊端?
public class Demo {
public static void main(String[] args) {
Executors.newCachedThreadPool();
Executors.newFixedThreadPool(1);
Executors.newSingleThreadExecutor();
Executors.newScheduledThreadPool(1);
}
}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler);
//1. newCachedThreadPool
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
//2. newScheduledThreadPool(ScheduledThreadPoolExecutor中DelayedWorkQueue是一个无界队列,所以设置maximumPoolSize的大小没有什么效果)
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
//3. newFixedThreadPool(keepAliveTime设置为0,表示多余的空闲线程会被立即终止。)
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
//4. newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
newCachedThreadPool
,newScheduledThreadPool
允许的线程(非核心线程)数量为 Integer.MAX_VALUE
,会创建大量的线程,可能会导致OOM
newFixedThreadPool
,newSingleThreadExecutor
允许创建的请求队列长度为 Integer.MAX_VALUE
,会堆积大量的请求,可能会导致OOM
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler);
#参数说明
corePoolSize:核心线程数量
maximumPoolSize:最大线程数
keepAliveTime:存活时间,(除了核心线程之外,其他的线程超时了没有人调用就会释放的时间)
unit:存活单位
workQueue:阻塞队列(前面章节介绍的。)
handler:拒绝策略
## 拒绝策略:默认是直接抛出异常
1. AbortPolicy:直接抛出异常
2. CallRunsPolicy: 只用调用者所在的线程处理任务
3. DiscurdOldestPlicy:队列满了,丢弃队列中最近的一个任务,来执行最新的任务(尝试和最早的任务做一个竞争)
4. DisCardPolicy:队列满了,不处理直接丢弃
5. 也可以根据应用场景需要来实现RejectedExecutionHandler接口自定义策略;
IO密集型
: 判断你程序中十分耗IO
的数量(假设你的程序中有15 个大型任务,IO
十分占用资源 。设置最大线程数 > 15 就好)CPU 密集型
: 查看自己的CPU
是几核的。可以保持CPU
效率最高 (程序中获取线程数: Runtime.getRuntime().availableProcessores()
) public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
/*
* Proceed in 3 steps:
*
* 1. If fewer than corePoolSize threads are running, try to
* start a new thread with the given command as its first
* task. The call to addWorker atomically checks runState and
* workerCount, and so prevents false alarms that would add
* threads when it shouldn't, by returning false.
*
* 2. If a task can be successfully queued, then we still need
* to double-check whether we should have added a thread
* (because existing ones died since last checking) or that
* the pool shut down since entry into this method. So we
* recheck state and if necessary roll back the enqueuing if
* stopped, or start a new thread if there are none.
*
* 3. If we cannot queue task, then we try to add a new
* thread. If it fails, we know we are shut down or saturated
* and so reject the task.
*/
/**
* 1. 如果运行的线程少于corePoolSize,则尝试
* 用给定的命令作为第一个命令启动一个新线程
* 的任务。调用addWorker会自动检查runState和
* workerCount,这样可以防止误报
* 线程,当它不应该返回false。
* 2. 如果一个任务可以成功排队,那么我们仍然需要
* 再次检查我们是否应该添加一个线程
* (因为现有的已经在上次检查后死亡)或其他
* 进入此方法后池关闭。所以我们
* 如果需要,请重新检查状态并回滚排队
* 停止,或者启动一个新的线程(如果没有)。
* 3.如果不能对任务进行排队,则尝试添加一个新的
* 线程。如果它失败了,我们知道我们已经关闭或饱和了
* 所以拒绝这个任务。
*/
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
else if (!addWorker(command, false))
reject(command);
}
先执行线程(核心+最大线程)中的任务,最后再执行任务队列中的任务
submit()
,execute()
方法有什么区别?两个方法均可以向线程池提交任务。
sumbit()
有返回值,而execute()
没有submit
方便exception
处理添加(addWorker)
/执行(execute)
源码分析public class ThreadPoolExecutor extends AbstractExecutorService {
//用来记录线程池状态和线程池中线程个数(高3位表示线程池状态,后29位表示线程池中线程个数)
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
//
private static final int COUNT_BITS = Integer.SIZE - 3;
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;
// Packing and unpacking ctl
private static int runStateOf(int c) { return c & ~CAPACITY; }
private static int workerCountOf(int c) { return c & CAPACITY; }
private static int ctlOf(int rs, int wc) { return rs | wc; }
}
线程池状态含义如下。
RUNNING
接受新任务并且处理阻塞队列里的任务SHUTDOWN
:拒绝新任务但是处理阻塞队列里的任务STOP
:拒绝新任务并且抛弃阻塞队列的任务,同时会中断正在处理的任务。TIDYING
:所有任务都执行完(包含阻塞队列里面的任务)后当前线程池活动线程数为0, 将要调用terminated
方法TERMINATED
:终止状态 。terminated
方法调用完成以后的状态
线程池状态转换列举如下
RUNNING
->SHUTDOWN
: 显式调用shutdown()
方法,或者隐式调用了finalize()
方法里面的shutdown()
方法RUNNING或 SHUTDOWN
->STOP
显式调用shutdownNow()
方法SHUTDOWN
->TIDYING
:当线程池和任务队列都为空时 。STOP
->TIDYING
:当线程池为空时 。TIDYING
->TERMINATED
:当terminated() hook
方法执行完成时。
execute()
public void execute(Runnable command) {
//1. 如果任务为null,则抛出NPE异常
if (command == null)
throw new NullPointerException();
//2. 获取当前线程池的状态+线程个数变量的组合值
int c = ctl.get();
//3. 当前线程池中线程数量是否小于corePoolSize,小于则开启新线程运行
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
//4. 如果线程池处于RUNNING状态,则添加任务到阻塞队列
if (isRunning(c) && workQueue.offer(command)) {
//4.1 二次检查
int recheck = ctl.get();
//4.2 如果当前线程池状态不是RUNNING则从队列中删除任务,并执行拒绝策略
if (! isRunning(recheck) && remove(command))
reject(command);
//4.3 否则如果当前线程池为空,则添加一个线程
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
//5. 如果队列满,则新增线程,新增失败则执行拒绝策略
else if (!addWorker(command, false))
reject(command);
}
代码4处
:线程处于RUNNING
状态则添加当前任务到任务队列。这里需要判断线程池状态是因为有可能线程池己经处于非RUNNING
状态 ,而在非RUNNING
状态下是要抛弃新任务;如果向任务队列添加任务成功,则
代码4.2
对线程池状态进行二次校验,这是因为添加任务到 任务队列后 ,执行代码4.2
前有可能线程池的状态己经发生改变了。这里进行二次校验,如果当前线程池状态不是RUNNING
了则把任务从任务队列移除,移除后执行拒绝策略 ;如果二次校验通过,则执行代码4.3
重新判断当前线程池里面是否还有线程,如果没有则新增一个线程。
addWorker()
//主要分为两部分:
//第一部分双重循环的目的是通过CAS操作增加线程数;
//第二部分主要是把并发安全的任务添加到workers里面,并且启动任务执行;
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// Check if queue empty only if necessary.
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
for (;;) {
int wc = workerCountOf(c);
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); // Re-read ctl
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 {
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// Recheck while holding lock.
// Back out on ThreadFactory failure or if
// shut down before lock acquired.
int rs = runStateOf(ctl.get());
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
workers.add(w);
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock();
}
if (workerAdded) {
t.start();
workerStarted = true;
}
}
} finally {
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
《Java并发编程之美》____翟陆续
《Java并发编程的艺术》____方腾飞