java多线程基础
下面说一下线程的7种状态
下面我重点来说一下阻塞状态
阻塞状态是可以分很多种的:
下面用另外一张图来说明这种状态
简单说一下线程的启动原理
下面说一下java中的线程
java线程的异步请求方式
上面就会先把main执行出来,等阻塞结束之后把run()方法里面的come in执行出来,这个是一个异步的操作
从线程中取得一个返回值
1.用一个类去实现Callable接口
上面可以返回一个结果
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
// 实现Callable接口,指定返回值类型为String
public class CallableDemo implements Callable<String> {
// 实现call方法,该方法内为具体的任务逻辑
@Override
public String call() throws Exception {
System.out.println("come in"); // 输出提示信息
Thread.sleep(10000); // 休眠10秒,模拟耗时操作
return "SUCCESS"; // 返回任务执行结果
}
// 主函数
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 创建一个固定大小为1的线程池
ExecutorService executorService = Executors.newFixedThreadPool(1);
// 创建CallableDemo实例
CallableDemo callableDemo = new CallableDemo();
// 提交任务给线程池,获得一个Future对象
Future<String> future = executorService.submit(callableDemo);
System.out.println(future.get()); // 阻塞,等待任务执行完毕并获取结果
executorService.shutdown(); // 关闭线程池
}
}
我们把上面的线程交给线程池去执行,然后返回一个Future对象接收返回值,里面说一下future.get()方法是一个阻塞方法,比如上面线程阻塞了10秒钟,我们get()这个方法是拿不到结果的
interrupt()的作用
上面的运行情况下面分析一下:
在某些情况下,在线程中断之前,可能不会打印消息“Test:1”。这是因为线程调度程序决定何时在线程之间切换,并且新线程可能在有机会打印消息之前就被中断了。start()方法和interrupt()方法之间的执行顺序没有保证,因此中断可能在线程有机会开始执行其run()方法之前发生。
下面我们来看另外一段代码
package com.pxx.interrupt;
public class InterruptDemo2 implements Runnable {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
try {
Thread.sleep(200);//这里睡了0.2秒
System.out.println("interrupt线程运行了");
} catch (InterruptedException e) {//这里会有复位的作用,又把isInterrupted变为false
System.out.println("异常被捕获");
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new InterruptDemo2());
t1.start();
Thread.sleep(1000);
t1.interrupt();//把isInterrupted()这个方法变为true
System.out.println(Thread.currentThread().getName() + "执行了");
}
}
我们来看其中的一个运行结果:
那么我们来分析一下运行结果
我先来说一下InterruptedException这个异常会在什么时候去触发,它的触发条件是
!Thread.currentThread().isInterrupted()这个位置变为true的时候,它就会去触发这个异常
并且又把这个异常复位为false
上面就是main主线程调用了start之后,就被t1线程抢走了并开始执行,然后中间main线程抢回了执行的时间片,把isInterrupted变为了true,这个时候t1线程又抢走了时间片,发现iisInterrupted()这个方法变为了true,于是触发了异常,并且复位了isInterrupted()变为了false,然后开始执行catch里面的代码,执行完一句之后,又被main线程抢走了,这个时候main执行完最后一条语句结束,然后t1去执行打印异常栈追踪信息,又开始一直循环执行里面的代码,因为没有在让isInterrupted()继续中断的条件
好了我们可以改动一下上面的代码让它继续中断
说一下并发与并行
并发是并行的假象,看似程序在同时执行多个操作,而并发只是要求程序假装同时执行多个操作,也就是每个时间片执行一个操作,多个操作快速切换执行而已
大体说一下并发编程的三大特性
可见性
当一个线程修改了共享变量的值,其他线程能够看到修改的值。
具体原理分析如下:
当我们在Java程序中定义了一个变量,例如 int x = 5;
,这个变量实际上在计算机内存中有两个位置保存着它的值:一个是主内存(主存,RAM),另一个是线程的工作内存(缓存,CPU缓存)。在不同的线程中,可能会有各自的工作内存。
当一个线程修改了这个变量的值,例如 x = 10;
,这个修改首先会发生在线程的工作内存中,而不是直接在主内存中。其他线程如果要读取这个变量的值,通常会从主内存中读取。问题在于,由于工作内存的存在,不同的线程可能在各自的工作内存中保存了不同的变量值。
为了确保多个线程之间对共享变量的修改能够正确地被其他线程看到,Java 内存模型使用了“主内存同步”的机制。当一个线程修改了变量的值后,会将新值同步回主内存,而其他线程在读取这个变量的值之前,会先从主内存中刷新(获取)变量的最新值,而不是直接从自己的工作内存中读取。
下面说的直白一点就是:
有序性
简单说一下什么叫指令重排
指令重排是指在程序执行过程中,CPU或者编译器为了提高性能,可能会对指令的执行顺序进行优化,使得程序在逻辑上的执行顺序与实际的指令执行顺序不一致。
在Java中,由于Java代码最终会被编译成字节码,然后由JVM执行,JVM为了提高程序执行的效率,也可能会进行指令重排。这就意味着,即使程序的源代码中顺序是有序的,JVM在执行时可能会重新排列指令的执行顺序。
指令重排可能会导致多线程程序出现问题。例如,如果一个线程在初始化一个对象时,先为对象分配内存空间,然后初始化对象的属性,最后将对象的引用赋值给某个变量。如果发生了指令重排,可能会导致另一个线程在获取到对象的引用之后,访问到的对象并未完成初始化,从而引发错误。
原子性
一个或多个操作,要么全部执行且在执行过程中不被任何因素打断,要么全部不执行
来说一下JMM内存模型
Java内存模型(Java Memory Model,JMM)
首先要明白JMM内存模型他不是一个具体硬件存在,它是一个虚拟的概念。
了解了上面这个概念之后,我们必须去了解一下计算机内部的重要组成部分以及jvm到底占据计算机什么位置和内存是如何划分的
直接用一张图来说明一下
好了,在回过头来说JMM,它是一个概念,一个数据的处理规则对吧,下面我们就来剖析一下这个规则
JMM规范了Java虚拟机与计算机内存是如何协同工作的:
规定了一个线程如何和何时可以看到由其他线程修改过后的共享变量的值,以及在必须时如何同步的访问共享变量。它是围绕原子性,有序性,可见性展开的
单纯这样来看,我们能看懂个屁啊,既然是围绕可见性展开的,那我们就贴一段会有可见性问题的代码来说一下
package com.pxx.visibility;
public class VisibilityTest {
private boolean flag = true;
public void refresh() {
flag = false;
System.out.println(Thread.currentThread().getName() + "修改flag");
}
public void load() {
System.out.println(Thread.currentThread().getName() + "开始执行...");
int i = 0;
//进行循环,flag = true
while (flag) {
i++;
}
System.out.println(Thread.currentThread().getName() + "跳出循环 i=" + i);
}
public static void main(String[] args) throws InterruptedException {
VisibilityTest test = new VisibilityTest();
//线程threadA模拟数据加载场景
Thread threadA = new Thread(() -> test.load(), "threadA");
threadA.start();
//让threadA执行
Thread.sleep(1000);
Thread threadB = new Thread(() -> test.refresh(), "threadB");
threadB.start();
}
}
运行结果:
分析可见性:
下面在用一个图来讲解一下JMM概念
上面还提到了主内存与工作内存的具体的交互操作:
下面我来说一个问题,我刚刚提到,只要本地内存共享变量一直存在,那么程序就会从本地缓存里面取数据,那么什么时候本地内存会不存在呢?
第一,栈空间是有限的,到了一定的限度之后,会把变量给清理掉
第二,隔了一段时间本地变量一直没有被使用,也会清理掉
上面两种线程在运行的时候,发现本地没有共享变量了之后,都会去主内存从新加载变量到本地内存。
下面可以看一个代码实例
这个方法是让线程能隔一段纳秒的时间在运行,纳秒的原因是我们可以把时间控制的非常短
上面就可能会让flag失效,从而去主内存里面获取最新的数据,然后跳出循环
下面讲一个Thread.yield(),他会释放当前线程的时间片,让当前线程进入一个可运行状态,并且保存数据,下次这个线程在次执行的时候,如果发现主内存已经修改了某个共享变量,就会从主内存去获取这个共享变量的值
为什么volatile也可以跳出循环
我们去看一下jvm中的字节码解释器源码bytecodeInterpreter.cpp
然后内部去调用了一个内存屏障处理
x86处理器中利用lock实现类似内存屏障的效果。
lock前缀指令的作用
会等待它之前所有指令完成,并且所有缓冲的写操作写回内存,也就是将store buffer中的内容写入内存之后才开始执行
lock会立即把本地内存修改的变量刷新到主内存里面,同时会让其他处理器中的本地内存的缓存副本失效,它失效,然后又会从主内存读取共享变量
他不是一个内存屏障的指令,但是它有内存屏障的效果
volatile的本质:
下面这个方法
UnsafeFactory.getUnsafe().storeFence();
核心也是
//能够跳出循环 内存屏障
//System.out.println(count);
//LockSupport.unpark(Thread.currentThread());
上面都是调用内存屏障
还有就是Thread.sleep(1)这种操作也是调用了内存屏障
定义为final也会保证某个变量的可见性