Java内存模型与线程
实现多线程我们知道可以继承Thread、实现Runnable接口等,但是为什么就实现呢?这篇文章就是解释多线程实现的底层原理。
一、主内存和工作内存
Java内存模型规定了所有的变量都存储在主内存(Main Memory)中,每条线程还有自己的工作内存(Working Memory)。
线程的工作内存中保存了被该线程使用的变量的主内存副本。线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的数据。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成,线程、主内存、工作内存三者的交互关系如图。
- lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
- unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
- read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
- load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
- use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
- assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
- store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。
- write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。
二、原子性、可见性、有序性(并发编程三要素)
2.1 原子性
2.2 可见性
2.3 有序性
有序性,即程序的执行顺序按照代码的先后顺序来执行,如果在本线程内观察,所有的操作都是有序的。
三、volatile
上面也大致讲了volatile保证可见性的底层原因。
Stack=2, Locals=0, Args_size=0
0: getstatic
3: iconst_1
4: iadd
5: putstatic
用volatile修饰 i ,然后进行自增:
public static volatile int i = 0;
i++;
当把 i 的值取到栈顶时,volatile关键字保证了 i 的值在此时是正确的,但是在执行iconst_1、iadd这些指令的时候,其他线程可能已经把 i 的值改变了,而操作栈顶的值就变成了过期的数据,所以putstatic指令执行后就可能把较小的 i 值同步回主内存中。volatitle关键字用于一写多读的情景,即一个操作线程,多个读的线程。
四、线程相关问题汇总
4.1 Runnable和Callable的区别
两者都需要调用Thread.start()启动线程;Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛。
因为Callable的call方法提供Future类型的返回值,所以当你需要知道任务执行的结果时,Callable是个不错的选择。
Runnable
Callable
4.2 Callable如何获取返回值?
Callalble接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。
public class Main {
public static void main(String[] args) throws InterruptedException, ExecutionException {
ExecutorService executor = Executors.newFixedThreadPool(2);
//创建一个Callable,3秒后返回String类型
Callable myCallable = new Callable() {
@Override
public String call() throws Exception {
Thread.sleep(3000);
System.out.println("calld方法执行了");
return "call方法返回值";
}
};
System.out.println("提交任务之前 "+getStringDate());
Future future = executor.submit(myCallable);
System.out.println("提交任务之后,获取结果之前 "+getStringDate());
System.out.println("获取返回值: "+future.get());
System.out.println("获取到结果之后 "+getStringDate());
}
public static String getStringDate() {
Date currentTime = new Date();
SimpleDateFormat formatter = new SimpleDateFormat("HH:mm:ss");
String dateString = formatter.format(currentTime);
return dateString;
}
}
}
结果:
4.3 get()方法的阻塞性:
在调用submit提交任务之后,主线程本来是继续运行了。但是运行到future.get()的时候就阻塞住了,一直等到任务执行完毕,拿到了返回的返回值,主线程才会继续运行。
因为调用get()方法时,任务还没有执行完,所以会一直等到任务完成,形成了阻塞。任务是在调用submit方法时就开始执行了,如果在调用get()方法时,任务已经执行完毕,那么就不会造成阻塞。
System.out.println("提交任务之前 "+getStringDate());
Future future = executor.submit(myCallable);
System.out.println("提交任务之后 "+getStringDate());
Thread.sleep(4000);
System.out.println("已经睡了4秒,开始获取结果 "+getStringDate());
System.out.println("获取返回值: "+future.get());
System.out.println("获取到结果之后 "+getStringDate());
因为睡了4秒,任务已经执行完毕,所以get方法立马就得到了结果。
submit两个任务时,总阻塞时间是最长的那个。例如,有两个任务,一个3秒,一个5秒:
Callable myCallable = new Callable() {
@Override
public String call() throws Exception {
Thread.sleep(5000);
System.out.println("calld方法执行了");
return "call方法返回值";
}
};
Callable myCallable2 = new Callable() {
@Override
public String call() throws Exception {
Thread.sleep(3000);
System.out.println("calld2方法执行了");
return "call2方法返回值";
}
};
System.out.println("提交任务之前 "+getStringDate());
Future future = executor.submit(myCallable);
Future future2 = executor.submit(myCallable2);
System.out.println("提交任务之后 "+getStringDate());
System.out.println("开始获取第一个返回值 "+getStringDate());
System.out.println("获取返回值: "+future.get());
System.out.println("获取第一个返回值结束,开始获取第二个返回值 "+getStringDate());
System.out.println("获取返回值2: "+future2.get());
System.out.println("获取第二个返回值结束 "+getStringDate());
提交任务之前 14:14:47
提交任务之后 14:14:48
开始获取第一个返回值 14:14:48
calld2方法执行了
calld方法执行了
获取返回值: call方法返回值
获取第一个返回值结束,开始获取第二个返回值 14:14:53
获取返回值2: call2方法返回值
获取第二个返回值结束 14:14:53
获取第一个结果阻塞了5秒,所以获取第二个结果立马就得到了。
4.4 submit(Runnable task):
因为Runnable是没有返回值的,所以如果submit一个Runnable的话,get得到的为null。
Runnable myRunnable = new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + " run time: " + System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Future future = executor.submit(myRunnable);
System.out.println("获取的返回值: "+future.get());
pool-1-thread-1 run time: 1493966762524
获取的返回值: null
4.5 submit(Runnable task, T result)
虽然submit传入Runnable不能直接返回内容,但是可以通过submit(Runnable task, T result)传入一个载体,通过这个载体获取返回值。这个其实不能算返回值了,是交给线程处理一下。
先新建一个载体类Data:
public static class Data {
String name;
String sex;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
}
然后在Runnable的构造方法中传入:
static class MyThread implements Runnable {
Data data;
public MyThread(Data name) {
this.data = name;
}
@Override
public void run() {
try {
Thread.sleep(2000);
System.out.println("线程 执行:");
data.setName("新名字");
data.setSex("新性别");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
然后调用:
Data data = new Data();
Future<Data> future = executor.submit(new MyThread(data), data);
System.out.println("返回的结果 name: " + future.get().getName()+", sex: "+future.get().getSex());
System.out.println("原来的Data name: " + data.getName()+", sex: "+data.getSex());
线程 执行:
返回的结果 name: 新名字, sex: 新性别
原来的Data name: 新名字, sex: 新性别
发现原来的data也变了。
4.6 get(long var1, TimeUnit var3)
前面都是用的get()方法获取返回值,那么因为这个方法是阻塞的,有时需要等很久。所以有时候需要设置超时时间。
get(long var1, TimeUnit var3)这个方法就是设置等待时间的。
如下面的任务需要5秒才能返回结果:
Callable myCallable = new Callable() {
@Override
public String call() throws Exception {
Thread.sleep(5000);
return "我是结果";
}
};
Future future1 = executor.submit(myCallable);
System.out.println("开始拿结果 "+getStringDate());
System.out.println("返回的结果是: "+future1.get()+ " "+getStringDate());
System.out.println("结束拿结果 "+getStringDate());
开始拿结果 16:00:43
返回的结果是: 我是结果 16:00:48
结束拿结果 16:00:48
现在要求最多等3秒,拿不到返回值就不要了,所以用get(long var1, TimeUnit var3)这个方法
方法的第一个参数是长整形数字,第二个参数是单位,跟线程池ThreadPoolExecutor的构造方法里一样的。
Future future1 = executor.submit(myCallable);
System.out.println("开始拿结果 "+getStringDate());
try {
System.out.println("返回的结果是: "+future1.get(3, TimeUnit.SECONDS)+ " "+getStringDate());
} catch (TimeoutException e) {
e.printStackTrace();
System.out.println("超时了 "+getStringDate());
}
System.out.println("结束拿结果 "+getStringDate());
然后输出是
过了三秒就抛出超时异常了,主线程继续运行,不会再继续阻塞。
五、什么是守护线程?
在java多线程开发中,有两类线程,分别是User Thread(用户线程)和Daemon Thread(守护线程) 。
守护线程又被称为“服务进程”、“精灵线程”或“后台线程”,是指在程序运行时在后台提供一种通用服务的线程,比如垃圾回收线程就是一个很称职的守护者,并且这种线程并不属于程序中不可或缺的部分。通俗点讲,任何一个守护线程都是整个JVM中所有非守护线程的“保姆”,守护线程,类似于操作系统里面是守护进程。
5.1 用户线程与守护线程的区别
JVM 程序在什么情况下能够正常退出?答:当 JVM 中不存在任何一个正在运行的非守护线程时,则 JVM 进程即会退出。
事实上,User Thread(用户线程)和Daemon Thread(守护线程)从本质上来说并没有什么区别,唯一的不同之处就在于虚拟机的离开:如果用户线程已经全部退出运行了,只剩下守护线程存在了,虚拟机也就退出了。因为没有了被守护者,守护线程也就没有工作可做了,也就没有继续运行程序的必要了。
5.2 区分用户线程和守护线程
isDaemon( ),返回true为守护线程,反之为用户线程
5.3 手动设置守护线程
守护线程并非只有虚拟机内部可以提供,用户也可以手动将一个用户线程设定/转换为守护线程。
在调用start( )前调用对象的setDeamon(true)方法,若将参数改为false,则表示的是用户线程模式。
注意:当在一个守护线程中产生了其他线程,那么这些新产生的线程默认还是守护线程。
class ThreadDemo extends Thread{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+":begin");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":end");
}
}
public class Test {
public static void main(String[] args) {
System.out.println("test3:begin");
Thread t1 = new ThreadDemo();
t1.setDaemon(true);
t1.start();
System.out.println("test3:end");
}
}
//test3:begin
//test3:end
//Thread-0:begin
从运行结果中可以发现,没有输出Thread-0:end。因为在启动线程前将其设置为守护线程了,当程序中只有守护线程存在时,JVM是可以退出的,也就是说,当JVM中只有守护线程运行时,JVM会自动关闭。因此,当test3方法调用结束后,main线程将退出,此时线程t1还处于休眠状态没有运行结束,但是由于此时只有这个守护线程在运行,JVM将会关闭,因此不会输出Thread-0:end。
5.4 使用守护线程的注意事项
(1)必须在线程运行前设置是否为守护线程
thread.setDaemon(true)必须在thread.start()之前设置。否则将引发IllegalThreadStateException异常。这意味着正在运行的常规线程不能设置为守护进程线程。
(2)守护线程创建的新线程也是守护线程
(3)守护线程不要去操作固有资源
并非所有用户线程都可以分配给守护线程进行服务,例如读写操作或计算逻辑。因为这个应用程序可能在DaemonThread有时间操作之前就退出了虚拟机。这意味着守护进程线程永远不应该访问固有的资源,例如文件和数据库,因为它可以在任何时候被中断,甚至在操作的中间。
5.5 守护线程优雅地停止用户线程
虽然java的Thread类里面,提供了很多让线程停止和销毁的方法,但早在jdk1.2版本就不推荐使用了。主要原因是强制停止线程容易造成死锁。对操作中的数据非常不友好。但守护线程很好地解决了这一问题。通过守护线程,可以优雅地停止用户线程。
public class DaemonTest1 {
public static boolean flag = true;
static class MyThread implements Runnable {
@Override
public void run() {
while (flag) {
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() +" " + (i+1));
}
}
}
}
public static void main(String[] args) {
Thread t1 = new Thread(new MyThread(), "用户线程");
Thread t2 = new Thread(() -> {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = false;
}, "守护线程!");
t2.setDaemon(true);
t1.start();
t2.start();
}
}
用户线程 1
用户线程 2
用户线程 3
用户线程 4
用户线程 5
用户线程 1
用户线程 2
用户线程 3
用户线程 4
用户线程 5
在main方法中,启动了两个线程,t1为用户线程,如果flag标志为true,则进行循环报数,每50毫秒报数1-5。t2为守护线程,100毫秒后,将flag标志置为false,也就让t1停止报数。当t1停止报数后,t1线程运行结束,由于没有其他线程t2守护线程运行结束。
在100毫秒的时间内,t1用户线程报了两轮数。然后整个线程运行结束。