文章目录
1、ForkJoin
其本质思想就是大任务拆分成小任务,类似分治法
ForkJoin 特点:工作窃取
原理就是利用双端队列,当某个线程执行完自己的任务后会去窃取其他线程的任务来执行
使用
步骤:
1、创建任务类继承RecursiveTask抽象类,重写compute()方法
2、在compute()中采用分治与递归的思想编写业务代码
3、创建ForkJoin池实例new ForkJoinPool()
4、将你的任务类实例传入实例池的submit()方法pool.submit(new MyForkJoinTask(1,10001));
,获得一个ForkJoinTask的实例
5、通过ForkJoinTask实例的get()方法获得结果
测试(1-10001相加):
/**
* 这是一个简单的Join/Fork计算过程,将1—10001数字相加
*/
public class ForkJoinPoolTest {
// 数字个数在200个以下时进行计算
private static final Integer MAX = 200;
static class MyForkJoinTask extends RecursiveTask<Integer> {
// 子任务开始计算的值
private Integer startValue;
// 子任务结束计算的值
private Integer endValue;
public MyForkJoinTask(Integer startValue , Integer endValue) {
this.startValue = startValue;
this.endValue = endValue;
}
@Override
protected Integer compute() {
// 如果条件成立,说明这个任务所需要计算的数值分为足够小了
// 可以正式进行累加计算了
if(endValue - startValue < MAX) {
System.out.println("开始计算的部分:startValue = " + startValue + ";endValue = " + endValue);
Integer totalValue = 0;
for(int index = this.startValue ; index <= this.endValue ; index++) {
totalValue += index;
}
return totalValue;
}
// 否则再进行任务拆分,拆分成两个任务
else {
MyForkJoinTask subTask1 = new MyForkJoinTask(startValue, (startValue + endValue) / 2);
subTask1.fork();
MyForkJoinTask subTask2 = new MyForkJoinTask((startValue + endValue) / 2 + 1 , endValue);
subTask2.fork();
return subTask1.join() + subTask2.join();
}
}
}
public static void main(String[] args) {
// 这是Fork/Join框架的线程池
ForkJoinPool pool = new ForkJoinPool();
ForkJoinTask<Integer> taskFuture = pool.submit(new MyForkJoinTask(1,10001));
try {
Integer result = taskFuture.get();
System.out.println("result = " + result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace(System.out);
}
}
}
执行结果:
2、JMM(Java内存模型)
JMM(Java Memery Moudle):Java内存模型
如图,当线程要对a进行操作的时候要分成几步
1、将主内存中的a读取到本线程的工作内存
2、对a进行操作修改
3、将更改后的a刷新回主内存
这时如果没有线程同步的话,就很容易出现问题,也就是当线程1还未将更改后的a刷回主内存的时候,线程2要修改a,但是它取的依然是未经更改的主内存中的a,这样就会出现问题
JMM的八种交互操作(原子性)
lock (锁定):作用于主内存的变量,把一个变量标识为线程独占状态
unlock (解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
read (读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用
load (载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中
use (使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令
assign (赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中
store (存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用
write (写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中
JMM八种指令的规则
1、不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write
2、不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存
3、不允许一个线程将没有assign的数据从工作内存同步回主内存
一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是怼变量实施use、store操作之前,必须经过assign和load操作
4、一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁
如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值
5、如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量
6、对一个变量进行unlock操作之前,必须把此变量同步回主内存
3.Volatile
一个关键字
1、保证了可见性
2、禁止指令重排(有序性)
3、不保证原子性
3.1、可见性
测试(不加volatile):
public class Demo01 {
public static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
while (flag){
System.out.print("flag==>"+flag);
// System.out.println();
// 此处不能用这句打印,因为此语句中包含了Synchronized
}
},"A").start();
// 为了让线程A先进入while循环
Thread.sleep(3000);
flag = false;
System.out.println(flag);
}
}
执行结果:
这里线程投资是我强制停止的,否则会无限打印
原理其实很简单,就是当线程A进入while循环读取flag的时候,将flag从主内存读取到线程A的工作内存中进行使用,当主线程更改flag值的时候线程A并不知道,于是一直在使用工作内存中的flag
加了volatile:
public class Demo01 {
public static volatile boolean flag = true;
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
while (flag){
System.out.print("flag==>"+flag);
// System.out.println(); // 此语句中包含了Synchronized
}
},"A").start();
// 为了让线程A先进入while循环
Thread.sleep(3000);
flag = false;
System.out.println(flag);
}
}
执行结果:
虽然依然输出许多值,但它是自己停止的
加了volatile修饰后的flag,每当它的值更改的时候,都会去提醒其他线程,这个flag已经被更改了
3.2、禁止指令重排(有序性)
指令重排:JVM在执行我们写的代码的时候会进行优化,也就是当两句代码之间没有关系的时候,它们的执行顺序可能会不一致,例如如下代码,a和b之间没有关系,JVM可能会先执行int b = 2
的操作
int a = 1;
int b = 2;
内存也会重排:例如Integer a = new Integer(3);
,这句指令大致有3个步骤:
1、实例化:在堆中开辟一片空间
2、初始化:赋值
3、指向引用地址
但是JVM可能会改变它们的执行顺序,会先指向引用地址,之后再进行赋值
而volatile就禁止JVM进行指令重排
3.3、原子性
原子性:不可分割
线程A执行任务的时候要么全部成功,要么全部失败
测试:
/**
* volatile 原子性测试
*/
public class AtomicTest {
public static volatile int num = 0;
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 20; i++) {
new Thread(()->{
for (int j = 0; j < 10000; j++) {
num++;
}
}).start();
}
// 此处是为了让线程已经启动
Thread.sleep(1000);
while(true) {
// 此处 == 2 是因为IDEA除了主线程外还有一个监控线程
if (Thread.activeCount() == 2){
System.out.println("num = " + num);
break;
}
}
}
}
执行结果:
理论上是200000,但是这里只加到了182399,原因就是volatile不能保证原子性
原因:这里num++一共有3个步骤:
1、从主内存中读取值
2、执行自增操作
3、将值刷入主内存
也就是说,当线程A读取num,还没自增,此时线程B读取num,执行自增,两个线程自增完同时写入的话,值相当于只加了一次
此处要想正确执行的话,可以加synchronized、Lock或者AtomicInteger
4、CAS
CAS(Compare And Swap):比较并交换
当我们要进行num++
的操作的时候,利用CAS,它在刷入主存的时候会和主存的值进行比较,如果主存中的num没有改变的话,它才会写进主存,若是主存中的值改变了,它会继续获取主存的num值再次执行num++的操作,再次比较,这就是自旋
缺点:
- 循环会耗时
- 一次只能保证一个共享变量的原子性
- ABA问题
ABA问题
也就是当进行CAS比较的时候,num的值可能看似没有变化,但实际上已经来了两个线程对num进行了修改,只是刚好将num改回了原始值,但是在进行CAS比较的时候不知道,这就是ABA问题
解决方法也很简单,加版本号
在num上加上一个版本号,每次对这个num进行更改的时候,都会更新改变版本号,这样CAS的时候就知道有没有其他线程来操作过这个对象了
AtomicReference的简单使用:
/**
* 测试 AtomicReference
* 解决ABA问题
*/
public class AtomicReferenceDemo01 {
public static void main(String[] args) {
// AtomicStampedReference<Integer>(初始值,初始版本号)
// 注意,这里初始值不能设置过大,因为它会进行自动装箱(Integer),在-128~127这个范围之外可能导致错误
AtomicStampedReference<Integer> atomicInteger = new AtomicStampedReference<Integer>(50,1);
new Thread(()->{
System.out.println("A线程版本号1===>"+atomicInteger.getStamp());
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 将2000设置为2022
System.out.println(atomicInteger.compareAndSet(50, 60,
1, atomicInteger.getStamp() + 1));
System.out.println(atomicInteger.getReference());
}).start();
}
}
执行结果:
5、锁
5.1、公平锁与非公平锁
公平锁:非常公平,线程获取锁不可以插队
非公平锁:非常不公平,线程获取锁可以插队
synchronized是非公平锁
Lock可以设置是公平还是非公平锁,默认是非公平锁
Lock lock = new ReentrantLock(); // 非公平锁
Lock lock = new ReentrantLock(true); // 公平锁
5.2、可重入锁
可重入锁(递归锁):当获得了锁之后,再次获取相同的锁不会出现死锁
例:
/**
* 可重入锁
* Synchronized
* 拿到外面的锁之后会默认拿到里面的锁
* 例:在sms方法里拿到了锁之后,默认会拿到call里的锁
*/
public class Demo01 {
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(()->{
try {
phone.sms();
} catch (InterruptedException e) {
e.printStackTrace();
}
},"A").start();
new Thread(()->{
phone.call();
},"B").start();
}
}
class Phone{
public synchronized void sms() throws InterruptedException {
System.out.println(Thread.currentThread().getName()+"==>sms");
TimeUnit.SECONDS.sleep(3);
call();
}
public synchronized void call(){
System.out.println(Thread.currentThread().getName()+"==>call");
}
}
执行结果:
按理说,当A执行sms()方法会获得一把锁,但是当A执行call()方法的时候按理说上一把锁还没有释放,A应该会等待自己去释放锁,也就是造成死锁,但是这里没有造成死锁现象,因为synchronized是可重入锁,A两次获得的是同一把锁,所以无需重复获取
5.3、怎么排除死锁
死锁:两个以上的线程都需要获取对方的锁,都在等待对方释放锁
模拟死锁,死锁的实现
/**
* 死锁的实现
*/
public class DeadLockTest {
public static void main(String[] args) {
// 创建两个资源
Resource resourceA = new Resource("A");
Resource resourceB = new Resource("B");
// 将两个资源分别传给两个任务对象
MyThread run1 = new MyThread(resourceA, resourceB);
MyThread run2 = new MyThread(resourceB, resourceA);
new Thread(run1).start();
new Thread(run2).start();
}
}
// 任务对象
class MyThread implements Runnable {
// 两个资源 A B
private Resource lockA;
private Resource lockB;
public MyThread(Resource lockA, Resource lockB) {
this.lockA = lockA;
this.lockB = lockB;
}
@SneakyThrows
@Override
public void run() {
synchronized (lockA) {
System.out.println(Thread.currentThread().getName() +
"获取到了资源" + lockA.toString());
// 这里是为了让另一个线程先占有另一个资源
Thread.sleep(1000);
synchronized (lockB) {
System.out.println(Thread.currentThread().getName() +
"获取到了资源" + lockB.toString());
}
}
}
}
// 资源对象
class Resource {
private String name;
public Resource(String name) {
this.name = name;
}
@Override
public String toString() {
return name;
}
}
执行结果:
可以看到,两个线程都阻塞了,出现死锁
排查死锁
(1)可以查看日志
(2)使用指令查看信息
使用的图片来自狂神的视频
步骤:
1、使用 jps -l
定位进程号
2、使用jstack 进程号
找到死锁问题