线程核心一:实现多线程的正确姿势
实现多线程到底有几种
网上有说 2 种,3 种,4 种,6 种等等 🤦♂️
我们看 Oracle 官网 API 是怎么描述的。
官方描述为两种
:
- 继承 Thread 类
- 实现 Runnable 接口
有两种方法可以创建新的执行线程。 一种是将一个类声明为 Thread 的子类。 该子类应重写 Thread 类的 run 方法。 然后可以分配并启动子类的实例。
public class ThreadTest extends Thread {
@Override
public void run() {
System.out.println("线程执行....");
}
public static void main(String[] args) {
new ThreadTest().start();
}
}
创建线程的另一种方法是声明一个实现 Runnable 接口的类。 然后,该类实现 run 方法。 然后可以分配该类的实例,在创建 Thread 时将其作为参数传递并启动。
public class RunnableTest implements Runnable{
@Override
public void run() {
System.out.println("线程执行....");
}
public static void main(String[] args) {
new Thread(new RunnableTest()).start();
}
}
两种方式的对比
实现 Runnable 接口相对于继承 Thread 类来说,有如下显著的好处:
- 1、适合多个相同程序代码的线程去处理同一资源的情况,把虚拟 CPU(线程)同程序的代码,数据有效的分离,较好地体现了面向对象的设计思想。
- 2、可以避免由于 Java 的单继承特性带来的局限。我们经常碰到这样一种情况,即当我们要将已经继承了某一个类的子类放入多线程中,由于一个类不能同时有两个父类,所以不能用继承 Thread 类的方式,那么,这个类就只能采用实现 Runnable 接口的方式了。
- 3、有利于程序的健壮性,代码能够被多个线程共享,代码与数据是独立的。当多个线程的执行代码来自同一个类的实例时,即称它们共享相同的代码。多个线程操作相同的数据,与它们的代码无关。当线程被构造时,需要的代码和数据通过一个对象作为构造函数实参传递进去,这个对象就是一个实现了 Runnable 接口的类的实例。
两种方法的本质
区别:
通过 Thread 的 run()方法源码我们可以看到如下代码:
@Override
public void run() {
if (target != null) {
target.run();
}
}
如果 target 不等于 null 则,调用 target 的 run 方法,因此我们可以猜到 target 就是 Runnable 对象。
/* What will be run. */
private Runnable target;
由于我们通过继承 Thread 类的时候已经重写了 run()方法,所以并不会执行这段代码。
如果我们是通过实现 Runnable 接口的话,在创建 Thread 对象的时候就通过构造器传入了当前实现 Runnable 接口的对象,所以 target 不等于 null。
由此我们可以知道:
- 继承 Thread 类:run()方法整个被重写
- 实现 Runnable 接口:最终调用 target.run()
思考:如果同时继承了 Thread 类又实现了 Runnable 会出现什么情况?
public static void main(String[] args) {
new Thread(() -> {
System.out.println("我来自Runnable");
}) {
@Override
public void run() {
System.out.println("我来自Thread");
}
}.start();
}
我来自Thread
简单点一句话来说,就是我们覆盖了 Thread 类的 run(),里面的 target 那几行代码都被我们覆盖了。所以肯定不会执行 Runnable 的 run()方法了。
总结
准确的讲,创建线程只有一种方式那就是构造 Thread 类,而实现线程的执行单元有两种方式。
- 实现 Runnable 接口的 run()方法,并把 Runnable 实例传给 Thread 类。
- 继承 Thread 类,重写 Thread 的 run()方法。
典型错误观点分析
线程池
创建线程也算是一种新建线程的方式
我们通过线程池源码,可以看到底层还是通过 Thread 类来新建一个线程传入了我们的 Runnable。
通过 Callable 和 FutureTask 创建线程,也算是一种新建线程的方式
就不过过多赘述了,很清楚的可以看到,还是使用到了 Thread 类,和实现 Runnable 接口。
无返回值是实现 Runnable 接口,有返回值是实现 callable 接口,所以 callable 是新的实现线程的方式
还是通过 Runnable 接口来实现的。
典型错误观点总结
多线程的实现方式,在代码中写法千变万化,但其本质万变不离其宗。
线程核心二:多线程启动的正确姿势
start()方法和 run()方法区别是什么?
代码演示:
public static void main(String[] args) {
Runnable runnable = () -> {
System.out.println(Thread.currentThread().getName());
};
runnable.run();
new Thread(runnable).start();
}
main
Thread-0
我们可以发现执行了 run()方法是有主线程来执行的,并不是新建了一个线程。
run()和 start()的区别可以用一句话概括:单独调用 run()方法,是同步执行;通过 start()调用 run(),是异步执行。
start()方法原理解读
- start()方法含义:
启动新线程
- start()方法调用后,并不意味着该线程立马运行,只是通知 JVM 在一个合适的时间运行该线程。 有可能很长时间都不会运行,比如遇到饥饿的情况。
- 调用 start()的先后顺序并不能决定线程执行的顺序。
准备工作
首先会让自己处于就绪状态。就绪状态指的是,我已经获取到除了 CPU 以外的其他资源。比如该线程已经设置了上下文,栈,线程状态,以及 PC, 做完准备工作后,线程才可以被 JVM 或者操作系统进一步调度到执行状态。调度到执行状态后,等待获取 CPU 资源,然后才会进入到运行状态,执行 run()方法里面的代码。
不能重复调用start()方法
不然会出现异常:java.lang.IllegalThreadStateException
start()方法源码解析
- 启动新线程检查线程状态
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();
/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
* and the group's unstarted count can be decremented. */
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
我们看到第一行语句就是:
if (threadStatus != 0)
throw new IllegalThreadStateException();
而 threadStatus 初始化就是 0。
/* Java thread status for tools,
* initialized to indicate thread 'not yet started'
*/
private volatile int threadStatus = 0;
- 加入线程组
- 调用本地方法 start0()
run()方法原理解读
在 Thread 类源码中我们之前已经看过了只有三行代码,其实只是一个普普通通的方法。
@Override
public void run() {
if (target != null) {
target.run();
}
}
线程核心三:线程停止、中断的正确姿势
如何正确停止线程?
使用 interrupt()方法 来通知
,而不是强制
。
interrupt() 字面上是中断的意思,但在 Java 里 Thread.interrupt()方法实际上通过某种方式通知线程,并不会直接中止该线程。
因为相对于开发人员,被停止线程的本身更清楚什么时候停止。
与其说如何正确停止线程,不如说是如何正确通知线程。
代码示例:
public class ThreadTest implements Runnable {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new ThreadTest());
thread.start();
Thread.sleep(500);
thread.interrupt();
}
@Override
public void run() {
int i = 0;
while (i <= Integer.MAX_VALUE / 2) {
if (i % 20000 == 0) {
System.out.println(i);
}
i++;
}
System.out.println("任务执行完成");
}
}
由于打印出来数据特别的多,我就只展示最后一部分输出结果:
1073680000
1073700000
1073720000
1073740000
任务执行完成
感兴趣的话,可以试一下该段代码,可以发现在 0.5 秒后发起的通知线程中断并没有反应,我们的 run() 方法还是执行到了最后。(执行时间超过 0.5 秒)
这样我们也证实了 interrupt () 方法的确是没有立即暂停线程。
我们需要在 while 条件里增加一个判断,在每一次循环时候判断是否已经发起通知中断请求。
public class ThreadTest implements Runnable {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new ThreadTest());
thread.start();
Thread.sleep(500);
thread.interrupt();
}
@Override
public void run() {
int i = 0;
while (!Thread.currentThread().isInterrupted() && i <= Integer.MAX_VALUE / 2) {
if (i % 20000 == 0) {
System.out.println(i);
}
i++;
}
System.out.println("任务执行完成");
}
}
运行结果:
70900000
70920000
70940000
70960000
任务执行完成
我们可以很清楚的看到,1073740000
70960000
两个数值差距非常大,证明的确是在 0.5 秒后就中断了线程。
另外一种情况就是在线程睡眠的时候我们通知中断会怎样?
上代码:
public class ThreadTest {
public static void main(String[] args) {
Runnable runnable = () -> {
try {
int i = 0;
while (!Thread.currentThread().isInterrupted() && i <= 300) {
if (i % 100 == 0) {
System.out.println(i);
}
i++;
}
Thread.sleep(5000);
} catch (InterruptedException e) {
System.out.println("线程在睡眠中被吵醒了!");
e.printStackTrace();
}
};
Thread thread = new Thread(runnable);
thread.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread.interrupt();
}
}
运行结果:
0
100
200
300
线程在睡眠中被吵醒了!
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.suanfa.thread.ThreadTest.lambda$main$0(ThreadTest.java:16)
at java.lang.Thread.run(Thread.java:748)
通过代码我们可以看到在睡眠中我们进行通知中断的话会报出InterruptedException
异常,所以在写代码的过程中要及时处理 InterruptedException 才能正确停止线程。
另还有一种情况就是在循环中每次线程都会睡眠的时候我们通知中断会怎样?
上代码:
public class ThreadTest {
public static void main(String[] args) {
Runnable runnable = () -> {
try {
int i = 0;
while (!Thread.currentThread().isInterrupted() && i <= 30000) {
if (i % 100 == 0) {
System.out.println(i);
}
i++;
Thread.sleep(10);
}
} catch (InterruptedException e) {
System.out.println("线程在睡眠中被吵醒了!");
e.printStackTrace();
}
};
Thread thread = new Thread(runnable);
thread.start();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread.interrupt();
}
}
运行结果:
0
100
200
300
400
线程在睡眠中被吵醒了!
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.suanfa.thread.ThreadTest.lambda$main$0(ThreadTest.java:14)
at java.lang.Thread.run(Thread.java:748)
结果还是会在 5 秒后抛出了InterruptedException
异常。
但是我们其实不用在 while 条件中加入!Thread.currentThread().isInterrupted()
的判断,因为在通知中断时候,发现线程在 sleep 中的话,也会进行中断。
如果循环中包含sleep或者wait等方法则不需要在每次循环中检查是否已经收到中断请求。
实际开发中的两种最佳实践
- 第一种:优先选择:
传递
中断
我们先看一段代码:
public class ThreadTest {
public static void main(String[] args) {
Runnable runnable = () -> {
while(!Thread.currentThread().isInterrupted()){
System.out.println("执行了while里的代码");
throwInMethod();
}
};
Thread thread = new Thread(runnable);
thread.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread.interrupt();
}
private static void throwInMethod() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
这段代码看起来貌似没什么问题。但是请注意一定一定不要在最内层来进行try/catch
。否则就会如下结果所示:
执行了while里的代码
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.suanfa.thread.ThreadTest.throwInMethod(ThreadTest.java:24)
at com.suanfa.thread.ThreadTest.lambda$main$0(ThreadTest.java:9)
at java.lang.Thread.run(Thread.java:748)
执行了while里的代码
执行了while里的代码
执行了while里的代码
执行了while里的代码
执行了while里的代码
我们发现异常是抛出来了,但是外面的 run()方法依旧在进行 while 循环。并且由于已经抛出了InterruptedException
异常,我们的 while 条件中的!Thread.currentThread().isInterrupted()
已经被重置了。所以会一直循环下去,稍不注意线程就无法被回收。
解决办法:将异常抛给 run() 方法来解决。
public class ThreadTest {
public static void main(String[] args) {
Runnable runnable = () -> {
try {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("执行了while里的代码");
throwInMethod();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
};
Thread thread = new Thread(runnable);
thread.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread.interrupt();
}
private static void throwInMethod() throws InterruptedException {
Thread.sleep(2000);
}
}
运行结果:
执行了while里的代码
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.suanfa.thread.ThreadTest.throwInMethod(ThreadTest.java:27)
at com.suanfa.thread.ThreadTest.lambda$main$0(ThreadTest.java:10)
at java.lang.Thread.run(Thread.java:748)
一定要注意,不要将 try/cath 写在 while 里面!否则还是会无限循环
- 第二种:不想或无法传递:
恢复
中断
这种情况下我们在最内层的方法中 try/catch 了以后一定要在 catch 或者 finally 中重新设置中断即可。
public class ThreadTest {
public static void main(String[] args) {
Runnable runnable = () -> {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("执行了while里的代码");
throwInMethod();
}
};
Thread thread = new Thread(runnable);
thread.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread.interrupt();
}
private static void throwInMethod() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
Thread.currentThread().interrupt();
}
}
}
运行结果:
执行了while里的代码
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.suanfa.thread.ThreadTest.throwInMethod(ThreadTest.java:24)
at com.suanfa.thread.ThreadTest.lambda$main$0(ThreadTest.java:9)
at java.lang.Thread.run(Thread.java:748)
响应中断的方法总结列表
- Object.wait()
- Thread.sleep()
- Thread.join()
- BlockingQueue.take() / put()
- Lock.lockInterruptibly()
- CountDownLatch.await()
- CyclicBarrier.await()
- Exchanger.exchange()
- java.nio.channels.InterruptibleChannel 的相关方法
- java.nio.channels.Selector 的相关方法
错误的停止方法
- 被弃用的
stop()
、suspend()
、resume()
方法 - 用
volatile
设置 boolean 标志位
volatile 可以使用但是要分场景:
- 在没有阻塞的时候,可以使用 volatile
- 在有阻塞的情况下,volatile 不再适用
停止线程相关重要函数解析
判断是否
已被中断相关方法
- static boolean interrupted() :返回当前线程是否已经被中断
在 Thread 里中源码我们可以看到他传入了一个 true,这个参数的意思是是否清除中断状态。
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
- boolean isInterrupted() :返回当前线程是否已经被中断
在 Thread 里中源码我们可以看到他传入了一个 false ,也就是不清除标志位状态。
public boolean isInterrupted() {
return isInterrupted(false);
}
- Thread.interrupted()的目的对象
我们下面代码:
public class ThreadTest implements Runnable {
public static void main(String[] args) throws InterruptedException {
Thread thread01 = new Thread(new ThreadTest());
thread01.start();
Thread.sleep(500);
thread01.interrupt();
System.out.println(thread01.isInterrupted()); //true
System.out.println(Thread.interrupted()); //false
System.out.println(thread01.isInterrupted()); //true
}
@Override
public void run() {
while (true){}
}
}
可能有些人会觉得答案不应该是 true true false 吗?
重点就是在这句Thread.interrupted()
,执行这句话的是 main 线程,所以判断的就是 main 线程的状态
,结果为 false,因为并没有清除掉 Thread-0 线程的标志位状态,所以他还是 true。
下面例子可以表达的很明确:
public class ThreadTest implements Runnable {
public static void main(String[] args) throws InterruptedException {
Thread thread01 = new Thread(new ThreadTest());
thread01.start();
Thread.sleep(500);
System.out.println("在main方法里判断:"+thread01.isInterrupted());
}
@Override
public void run() {
Thread.currentThread().interrupt();
System.out.println("在run方法里判断:"+Thread.currentThread().isInterrupted());
System.out.println("调用清除标志位的判断方法:" + Thread.interrupted());
System.out.println("在run方法里判断:"+Thread.currentThread().isInterrupted());
}
}
运行结果:
在run方法里判断:true
调用清除标志位的判断方法:true
在run方法里判断:false
在main方法里判断:false
线程核心四:解释线程声明周期的正确姿势
线程一共有六种
状态
- 新建状态(New)
新创建了一个线程对象,但还没有调用 start()方法。
- 可运行状态(Runnable)
在 Java 虚拟机中执行的线程处于此状态。此状态说明了线程已经获得 cpu 的执行权力,但包含两种执行状态:
1、Ready:线程可执行,但当前 cpu 被其他正在执行的线程占用,而处于等待中。
2、Running:线程可执行,且正在 cpu 中处于执行状态。
- 阻塞状态(Blocked)
线程阻塞于锁。
- 无限等待状态(Waiting)
进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
- 限时等待状态(Timed Waiting)
线程因调用 sleep 方法处于休眠状态中,规定时间后可醒来,回到可运行状态(Runnable)。
- 终止状态(Terminated)
表示该线程已经执行完毕。
下面代码先展示一下新建状态、可运行状态、终止状态
public class ThreadTest implements Runnable {
public static void main(String[] args) {
Thread thread = new Thread(new ThreadTest());
System.out.println(thread.getState());
thread.start();
System.out.println(thread.getState());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(thread.getState());
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(10);
System.out.print(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getState());
}
}
运行结果:
NEW
RUNNABLE
0123456789RUNNABLE
TERMINATED
可以看到即使在运行中,状态也是 Runnable 而不是 Running。
接下来我们看一下限时等待状态、无限等待状态、阻塞状态
public class ThreadTest implements Runnable {
public static void main(String[] args) {
ThreadTest threadTest = new ThreadTest();
Thread thread1 = new Thread(threadTest);
Thread thread2 = new Thread(threadTest);
thread1.start();
thread2.start();
System.out.println(thread1.getState());
System.out.println(thread2.getState());
try {
Thread.sleep(1300);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(thread1.getState());
}
@Override
public void run() {
sync();
}
private synchronized void sync() {
try {
Thread.sleep(1000);
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
运行结果:
TIMED_WAITING
BLOCKED
WAITING
一般习惯而言,把 Blocked、Waiting、Timed_waiting 都成为阻塞状态
线程核心五:解释 Thread 和 Object 类中线程方法的正确姿势
方法概览
类 | 方法 | 简介 |
---|---|---|
Thread | sleep | 在指定的毫秒数内让当前“正在执行的线程”休眠(暂停执行) |
Thread | join | 等待其它线程执行完毕 |
Thread | yield | 放弃已经获得到的 cpu 资源 |
Thread | currentThread | 获取当前执行线程的引用 |
Thread | start、run | 启动线程相关 |
Thread | interrtupt | 中断线程 |
Thread | stop、suspend | 已废弃 |
Object | wait、notify、notifyAll | 让线程暂停休息和唤醒 |
wait、notify、notifyAll 方法详解
wait
当调用了 wait()方法后,当前线程进入阻塞
阶段,同时会释放锁,直到以下四种情况之一发生时,才会被唤醒。
- 另一个线程调用这个对象的 notify()方法,随机唤醒的正好是当前线程。
- 另一个线程调用这个对象的 notifyAll()。
- 设置了 wait(long timeout)的超时时间,如果传入 0 就是永久等待。
- 线程自身调用了 interrupt()方法。
notify、notifyAll
notify 的作用就是唤醒某一个线程(随机唤醒)。
notifyAll 的作用就是唤醒所有等待的线程。
代码演示 wait 和 notify 的基本用法:
/**
* 演示wait和notify的基本用法
* 1. 研究代码执行顺序
* 2. 证明wait释放锁
*/
public class ThreadTest {
public static Object object = new Object();
public static void main(String[] args) {
Thread01 thread01 = new Thread01();
thread01.start();
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread02 thread02 = new Thread02();
thread02.start();
}
static class Thread01 extends Thread {
@Override
public void run() {
synchronized (object) {
System.out.println(Thread.currentThread().getName() + "开始执行!");
try {
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "又获得到锁!");
}
}
}
static class Thread02 extends Thread {
@Override
public void run() {
synchronized (object) {
System.out.println(Thread.currentThread().getName() + "执行notify()方法!");
object.notify();
}
}
}
}
运行结果:
Thread-0开始执行!
Thread-1执行notify()方法!
Thread-0又获得到锁!
代码演示 notifyAll:
public class ThreadTest implements Runnable {
public static final Object object = new Object();
public static void main(String[] args) {
Runnable r = new ThreadTest();
Thread threadA = new Thread(r, "thread01");
Thread threadB = new Thread(r, "thread02");
threadA.start();
threadB.start();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread thread03 = new Thread(() -> {
synchronized (object) {
System.out.println(Thread.currentThread().getName() + "开始notifyAll!");
object.notifyAll();
}
}, "thread03线程");
thread03.start();
}
@Override
public void run() {
synchronized (object) {
System.out.println(Thread.currentThread().getName() + "开始执行!");
try {
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "又获得到锁!");
}
}
}
运行结果:
thread01开始执行!
thread02开始执行!
thread03线程开始notifyAll!
thread02又获得到锁!
thread01又获得到锁!
由此结果可以知道,先 start 的在 notifyAll 后不一定先获得到锁。
需要注意的是,wait只是释放当前锁
wait、notify、notifyAll 特点、性质
- 使用之前必须
先获取到
monitor,也就是获得到锁,不然会抛出IllegalMonitorStateException
- 只能唤醒其中
一个
- 属于
Object
类 - wait 只是释放当前锁
- 类似功能的
Condition
实现生产者消费者设计模式
public class ProducerConsumer {
public static void main(String[] args) {
EventStorage eventStorage = new EventStorage();
Producer producer = new Producer(eventStorage);
Consumer consumer = new Consumer(eventStorage);
new Thread(producer).start();
new Thread(consumer).start();
}
}
class Producer implements Runnable {
private EventStorage storage;
public Producer(EventStorage storage) {
this.storage = storage;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
storage.put();
}
}
}
class EventStorage {
private int maxSize;
private LinkedList<Date> storage;
public EventStorage() {
maxSize = 10;
storage = new LinkedList<>();
}
public synchronized void take() {
while (storage.size() == 0) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("拿到了" + storage.poll() + ",现在仓库还剩下" + storage.size());
notify();
}
public synchronized void put() {
while (storage.size() == maxSize) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
storage.add(new Date());
System.out.println("仓库目前有" + storage.size() + "个产品了");
notify();
}
}
class Consumer implements Runnable {
private EventStorage storage;
public Consumer(EventStorage storage) {
this.storage = storage;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
storage.take();
}
}
}
运行结果:
仓库目前有1个产品了
仓库目前有2个产品了
仓库目前有3个产品了
仓库目前有4个产品了
仓库目前有5个产品了
仓库目前有6个产品了
仓库目前有7个产品了
仓库目前有8个产品了
仓库目前有9个产品了
仓库目前有10个产品了
拿到了Mon Sep 14 16:27:28 CST 2020,现在仓库还剩下9
拿到了Mon Sep 14 16:27:28 CST 2020,现在仓库还剩下8
拿到了Mon Sep 14 16:27:28 CST 2020,现在仓库还剩下7
拿到了Mon Sep 14 16:27:28 CST 2020,现在仓库还剩下6
拿到了Mon Sep 14 16:27:28 CST 2020,现在仓库还剩下5
拿到了Mon Sep 14 16:27:28 CST 2020,现在仓库还剩下4
拿到了Mon Sep 14 16:27:28 CST 2020,现在仓库还剩下3
仓库目前有4个产品了
仓库目前有5个产品了
仓库目前有6个产品了
仓库目前有7个产品了
仓库目前有8个产品了
仓库目前有9个产品了
仓库目前有10个产品了
拿到了Mon Sep 14 16:27:28 CST 2020,现在仓库还剩下9
拿到了Mon Sep 14 16:27:28 CST 2020,现在仓库还剩下8
拿到了Mon Sep 14 16:27:28 CST 2020,现在仓库还剩下7
拿到了Mon Sep 14 16:27:28 CST 2020,现在仓库还剩下6
拿到了Mon Sep 14 16:27:28 CST 2020,现在仓库还剩下5
拿到了Mon Sep 14 16:27:28 CST 2020,现在仓库还剩下4
拿到了Mon Sep 14 16:27:28 CST 2020,现在仓库还剩下3
拿到了Mon Sep 14 16:27:28 CST 2020,现在仓库还剩下2
拿到了Mon Sep 14 16:27:28 CST 2020,现在仓库还剩下1
拿到了Mon Sep 14 16:27:28 CST 2020,现在仓库还剩下0
仓库目前有1个产品了
仓库目前有2个产品了
仓库目前有3个产品了
拿到了Mon Sep 14 16:27:28 CST 2020,现在仓库还剩下2
拿到了Mon Sep 14 16:27:28 CST 2020,现在仓库还剩下1
拿到了Mon Sep 14 16:27:28 CST 2020,现在仓库还剩下0
wait、notify、notifyAll 常见面试题
- 手写生产着消费者设计模式
- 为什么 wait()需要在
同步代码块
中使用?
如果我们不从同步上下文中调用 wait() 或 notify() 方法,我们将在 Java 中收到 IllegalMonitorStateException。
但是为什么呢?
比如生产者是两个步骤:
count+1;
notify();
消费者也是两个步骤:
检查 count 值;
睡眠或者减一;
万一这些步骤混杂在一起呢?比如说,初始的时候 count 等于 0,这个时候消费者检查 count 的值,发现 count 小于等于 0 的条件成立;就在这个时候,发生了上下文切换,生产者进来了,噼噼啪啪一顿操作,把两个步骤都执行完了,也就是发出了通知,准备唤醒一个线程。这个时候消费者刚决定睡觉,还没睡呢,所以这个通知就会被丢掉。紧接着,消费者就睡过去了……
- 为什么线程通信方法 wait(),notify()和 notifyAll()被定义在 Object 类中?
Wait-notify 机制是在获取对象锁的前提下不同线程间的通信机制。在 Java 中,任意对象都可以当作锁来使用,由于锁对象的任意性,所以这些通信方法需要被定义在 Object 类里。
- 在 java 的内置锁机制中,每个对象都可以成为锁,也就是说每个对象都可以去调用 wait,notify 方法,而 Object 类是所有类的一个父类,把这些方法放在 Object 中,则 java 中的所有对象都可以去调用这些方法了。
- 一个线程可以拥有多个对象锁,wait,notify,notifyAll 跟对象锁之间是有一个绑定关系的,比如你用对象锁 Object 调用的 wait()方法,那么你只能通过 Object.notify()或者 Object.notifyAll()来唤醒这个线程,这样 jvm 很容易就知道应该从哪个对象锁的等待池中去唤醒线程,假如用 Thread.wait(),Thread.notify(),Thread.notifyAll()来调用,虚拟机根本就不知道需要操作的对象锁是哪一个。
- wait 方法属于 Object 对象,那调用 Thread.wait 会怎样?
首先,Thread 是一个普通的对象,但是 Thread 类有点特殊。
在线程结束的时候,JVM 会自动调用线程对象的 notifyAll 方法(为了配合 join 方法)。
避免在 Thread 对象上使用 wait 方法。
sleep 方法详解
作用
:只想让线程在预期的时间执行,其它时间不要占用 cpu 资源。
特点
:不释放锁,包括 synchronized,Lock。
演示 sleep 不释放锁代码:
public class ThreadTest implements Runnable {
public static void main(String[] args) {
ThreadTest threadTest = new ThreadTest();
new Thread(threadTest).start();
new Thread(threadTest).start();
}
@Override
public void run() {
synchronized (this) {
System.out.println(Thread.currentThread().getName() + "开始执行!");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "退出同步代码块!");
}
}
}
运行结果:
Thread-0开始执行!
Thread-0退出同步代码块!
Thread-1开始执行!
Thread-1退出同步代码块!
总结
sleep 方法可以让线程进入 Waiting 状态,并且不占用 CPU 资源,但是不释放锁,直到规定时间后再执行,休眠期间如果被中断,会抛出异常并清除中断状态。
join 方法详解
作用
:是等待这个线程结束。
用法
:t.join()方法阻塞调用此方法的线程进入 TIMED_WAITING 状态,直到线程 t 完成,此线程再继续;
代码演示:
public class ThreadTest implements Runnable {
public static void main(String[] args) throws InterruptedException {
ThreadTest threadTest = new ThreadTest();
Thread thread1 = new Thread(threadTest);
thread1.start();
Thread thread2 = new Thread(threadTest);
thread2.start();
thread1.join();
thread2.join();
System.out.println("主线程执行完毕!");
}
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"执行完毕!");
}
}
运行结果:
Thread-0执行完毕!
Thread-1执行完毕!
主线程执行完毕!
但是如果我们注释掉两行 join()会怎样呢?
运行如下:
主线程执行完毕!
Thread-0执行完毕!
Thread-1执行完毕!
这就是 join()的基本用法。
在join期间mian线程状态为WAITING.
分析 join 源码
public final void join() throws InterruptedException {
join(0);
}
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(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
参数 millis 传递为 0 的意思是无限睡眠时间。
第一个 if 就是如果参数小于 0 就抛出异常。
如果等于 0 或者不等 0 最终都会进行 wait,而如果 == 0 我们 wait(0)的话,我们知道这样就会无限睡眠。
可是我们的代码并没有进行 notify,他是如何醒的呢?
其实前面我们已经讲过一次了。在线程结束的时候,JVM 会自动调用线程对象的 notifyAll 方法(为了配合 join 方法)。
join 方法 常见面试题
- join 期间,线程处于哪有线程状态?
在 join 期间线程处于WAITING
状态。
yield 方法详解
作用:
释放我的 CPU 时间片。需要注意的是,执行 yield()后,线程状态依然是RUNNABLE
状态,并不会释放锁,也不会阻塞。可能会在下一秒 CPU 调度又会调度当前线程。
定位:
JVM 不保证遵循
总结
:举个例子:一帮人在排队上公交车,轮到 Yield 的时候,他突然说:我不想先上去了,咱们大家来竞赛上公交车。然后所有人就一块冲向公交车。有可能是其他人先上车了,也有可能是 Yield 先上车了。
但是线程是有优先级的,优先级越高的人,就一定能第一个上车吗?这是不一定的,优先级高的人仅仅只是第一个上车的概率大了一点而已,最终第一个上车的,也有可能是优先级最低的人。并且所谓的优先级执行,是在大量执行次数中才能体现出来的。
线程核心六:了解线程各属性的正确姿势
概览
属性 | 用途 |
---|---|
编号(ID) | 每个线程都有自己的 ID,用于标识不同的线程 |
名称(Name) | 让用户或者程序员在开发、调试中更容易区分和定位问题 |
是否是守护线程(isDaemon) | true 代表【守护线程】,false 代表【用户线程】 |
优先级(Priority) | 这个属性目的是告诉线程调度器,我们希望那些线程多运行,哪些少运行 |
线程ID
id 是自增有小到大,从 0 开始。但是在一开始就进行了自增操作所以是从1开始
。
private static synchronized long nextThreadID() {
return ++threadSeqNumber;
}
代码演示:
public class ThreadTest {
public static void main(String[] args) {
System.out.println("主线程id为:"+Thread.currentThread().getId());
Thread thread = new Thread();
System.out.println("子线程id为:"+thread.getId());
}
}
运行结果:
主线程id为:1
子线程id为:10
为什么是 10 呢?其实不难理解,因为在伴随着 JVM 启动的时候也会有一些线程跟着启动,比如:Finalizer 、 Reference Handler 、 Signal Dispatcher 等。
线程名称
其实就是在一开始的时候线程初始化分配的默认名称,同时调用的 nextThreadNum()方法是被 synchronized 修饰的,并且从 0 开始,不会出现重复的名称。
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}
private static int threadInitNumber;
private static synchronized int nextThreadNum() {
return threadInitNumber++;
}
我们接下来看一下修改名字的源码
public final synchronized void setName(String name) {
checkAccess();
if (name == null) {
throw new NullPointerException("name cannot be null");
}
this.name = name;
if (threadStatus != 0) {
setNativeName(name);
}
}
前面做了检查之后,我们进行 name 赋值,但是线程状态如果是未启动状态下,我们可以修改本地 native 方法去修改线程名称。如果在已经启动阶段,我们只能修改 Java 中的线程名称。
守护线程
作用
:给用户线程提供服务。比如:垃圾收集线程
守护线程的三个特性:
- 线程类型默认
继承
自父线程 被谁
启动不影响
JVM 退出
守护线程和普通线程的区别
- 整体无区别
- 唯一区别在于是否能影响 JVM 的退出。只要用户线程执行完毕 JVM 就会关闭退出。
线程优先级
线程包含 10 个级别,最小为 1,最大为 10,默认为 5。
/**
* The minimum priority that a thread can have.
*/
public final static int MIN_PRIORITY = 1;
/**
* The default priority that is assigned to a thread.
*/
public final static int NORM_PRIORITY = 5;
/**
* The maximum priority that a thread can have.
*/
public final static int MAX_PRIORITY = 10;
但我们程序设计不应该依赖优先级
。
因为不同操作系统不一样。在 win 中只有 7 个级别,而 linux 中没有优先级别。
并且优先级会被操作系统改变。
总结
属性名称 | 用途 | 注意事项 |
---|---|---|
编号(ID) | 标识不同的线程 | 唯一性,不允许被修改 |
名称(Name) | 定位问题 | 清晰有意义的名字;默认的名称 |
是否是守护线程(isDaemon) | 守护线程、用户线程 | 二选一;继承父线程;setDaemon() |
优先级(Priority) | 相对多运行 | 默认和父线程优先级相等,共有 10 个等级,不应该依赖 |
线程核心七:处理线程异常的正确姿势
为什么需要 UncaughtExceptionHandler?
- 因为在子线程中发生的异常并不会影响主线程的运行。
public class ThreadTest implements Runnable {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new ThreadTest());
thread.start();
int num = 0;
TimeUnit.SECONDS.sleep(3);
for (int i = 0; i < 1000; i++) {
num ++;
}
System.out.println(num);
}
@Override
public void run() {
throw new RuntimeException();
}
}
运行结果:
Exception in thread "Thread-0" java.lang.RuntimeException
at com.suanfa.thread.ThreadTest.run(ThreadTest.java:25)
at java.lang.Thread.run(Thread.java:748)
1000
- 子线程异常无法用传统方法捕获。
public class ThreadTest implements Runnable {
public static void main(String[] args) {
try {
new Thread(new ThreadTest()).start();
new Thread(new ThreadTest()).start();
new Thread(new ThreadTest()).start();
} catch (RuntimeException e) {
System.out.println("捕获到了异常");
}
}
@Override
public void run() {
throw new RuntimeException();
}
}
运行结果:
Exception in thread "Thread-0" Exception in thread "Thread-1" Exception in thread "Thread-2" java.lang.RuntimeException
at com.suanfa.thread.ThreadTest.run(ThreadTest.java:24)
at java.lang.Thread.run(Thread.java:748)
java.lang.RuntimeException
at com.suanfa.thread.ThreadTest.run(ThreadTest.java:24)
at java.lang.Thread.run(Thread.java:748)
java.lang.RuntimeException
at com.suanfa.thread.ThreadTest.run(ThreadTest.java:24)
at java.lang.Thread.run(Thread.java:748)
我们发现还是会抛出三个异常,因为 try/catch 只处理当前线程也就是主线程的异常,所以会失效。
两种解决方案
- 方案一,在每个 run 方法里进行 try/catch(不推荐)
- 方案二,利用 UncaughtExceptionHandler(推荐)
UncaughtExceptionHandler 接口
在这个接口中只包含一个方法
@FunctionalInterface
public interface UncaughtExceptionHandler {
/**
* Method invoked when the given thread terminates due to the
* given uncaught exception.
* <p>Any exception thrown by this method will be ignored by the
* Java Virtual Machine.
* @param t the thread
* @param e the exception
*/
void uncaughtException(Thread t, Throwable e);
}
实现一个简单的异常处理器:
public class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
@Override
public void uncaughtException(Thread t, Throwable e) {
Logger logger = Logger.getAnonymousLogger();
logger.log(Level.WARNING,"线程异常,已终止"+t.getName(),e);
}
}
public class ThreadTest extends MyUncaughtExceptionHandler implements Runnable {
public static void main(String[] args) {
Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
new Thread(new ThreadTest()).start();
new Thread(new ThreadTest()).start();
}
@Override
public void run() {
throw new RuntimeException();
}
}
运行结果:
九月 14, 2020 8:33:23 下午 com.suanfa.thread.MyUncaughtExceptionHandler uncaughtException
警告: 线程异常,已终止Thread-0
java.lang.RuntimeException
at com.suanfa.thread.ThreadTest.run(ThreadTest.java:20)
at java.lang.Thread.run(Thread.java:748)
九月 14, 2020 8:33:23 下午 com.suanfa.thread.MyUncaughtExceptionHandler uncaughtException
警告: 线程异常,已终止Thread-1
java.lang.RuntimeException
at com.suanfa.thread.ThreadTest.run(ThreadTest.java:20)
at java.lang.Thread.run(Thread.java:748)
线程核心八:理解线程安全的正确姿势
什么是线程安全
《Java Concurrency In Practice》的作者 Brian Goetz 对 “线程安全”有一个比较恰当的定义:“当多个线程访问一个对象时,如果不用考虑
这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步
,或者在调用方进行其它的协调操作,调用这个对象的行为都可以获得正确的结果
,那这个对象就是线程安全的。”
什么情况下会出现线程安全问题,怎么避免?
- 第一种:运行结果出错
a++多线程下出现结果错误问题
代码演示:
public class ThreadTest implements Runnable {
static int num = 0;
public static void main(String[] args) throws InterruptedException {
ThreadTest threadTest = new ThreadTest();
Thread thread = new Thread(threadTest);
thread.start();
Thread thread1 = new Thread(threadTest);
thread1.start();
thread.join();
thread1.join();
System.out.println(num);
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
num++;
}
}
}
运行结果:
14876
结果可以看到没有达到 20000,随机性十足。
简单理解就是我们线程 1 进行加 1 后,但是并没有赋值给 i,这个时候 cpu 调度切换到了线程 2,线程 2 这个时候拿到的 i 还是 1,也进行了加 1,最后进行赋值为 2,而线程 1 也赋值 2。结果就会导致不满 20000 的问题。
- 第二种:死锁
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象。
代码演示死锁:
public class ThreadTest implements Runnable {
static Object o1 = new Object();
static Object o2 = new Object();
int flag = 1;
public static void main(String[] args) {
ThreadTest threadTest01 = new ThreadTest();
ThreadTest threadTest02 = new ThreadTest();
threadTest02.flag = 2;
new Thread(threadTest01).start();
new Thread(threadTest02).start();
}
@Override
public void run() {
if (flag == 1) {
synchronized (o1) {
System.out.println("我拿到flag = " + flag + "拿到第一把锁!");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2) {
System.out.println("我拿到flag = " + flag + "拿到第二把锁!");
}
}
} else {
synchronized (o2) {
System.out.println("我拿到flag = " + flag + "拿到第二把锁!");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1) {
System.out.println("我拿到flag = " + flag + "拿到第一把锁!");
}
}
}
}
}
运行结果:
我们可以发现 flag = 1 的迟迟获得不到第二把锁,flag = 2 的也迟迟获得不到第一把锁。就造成了死锁,相互等待的局面。
- 对象
发布
和初始化
时候的安全问题
什么是发布?
发布(publish)对象意味着其作用域之外的代码可以访问操作此对象。例如将对象的引用保存到其他代码可以访问的地方,或者在非私有的方法中返回对象的引用,或者将对象的引用传递给其他类的方法。
什么是逸出?
- 1.方法返回一个 private 对象
public class ThreadTest {
private List<Integer> list = new ArrayList<>();
public List<Integer> getList() {
return list;
}
}
2.还未完成初始化(构造函数未完全执行完毕)就把对象提供给外界
- 在构造函数中未初始化完毕就 this 赋值
- 隐式逸出--注册监听事件
- 构造函数中运行线程
public class ThreadTest {
static Test test;
public static void main(String[] args) throws InterruptedException {
new Thread(() ->{
new Test();
}).start();
TimeUnit.SECONDS.sleep(1);
System.out.println(test);
}
}
class Test {
private int i;
private int j;
public Test() {
this.i = 1;
ThreadTest.test = this;
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.j = 2;
}
@Override
public String toString() {
return "Test{" + "i=" + i + ", j=" + j + '}';
}
}
运行结果:
Test{i=1, j=0}
可以发现并没有初始化完 j。
如何避免逸出
- 方法返回一个 private 对象
我们返回一个"副本"
private List<Integer> list = new ArrayList<>();
public List<Integer> getList() {
return new ArrayList<>(list);
}
- 还未完成初始化(构造函数未完全执行完毕)就把对象提供给外界
导致 this 引用逸出需要满足两个条件:
一个是在构造函数中创建内部类(EventListener),另一个是在构造函数中就把这个内部类给发布了出去(source.registerListener)。
因此,我们要防止这一类 this 引用逸出的方法就是避免让这两个条件同时出现。
也就是说,如果要在构造函数中创建内部类,那么就不能在构造函数中把他发布了,应该在构造函数外发布,即等构造函数执行完初始化工作,再发布内部类。
正如如下所示,使用一个私有的构造函数进行初始化和一个公共的工厂方法进行发布。
public class ThreadTest {
public final int id;
public final String name;
private final EventListener listener;
private ThreadTest() {
id = 1;
listener = new EventListener() {
public void onEvent(Object obj) {
System.out.println("id: " + ThreadTest.this.id);
System.out.println("name: " + ThreadTest.this.name);
}
};
name = "Thread";
}
public static ThreadTest getInstance(EventSource<EventListener> source) {
ThreadTest safe = new ThreadTest();
source.registerListener(safe.listener);
return safe;
}
}
class EventSource<E> {
public void registerListener(E listener) {
}
}
总结:需要考虑线程安全的情况
- 访问
共享
的变量或资源 - 所有依赖
时序
的操作 - 不同的数据之间存在
捆绑
关系的时候
线程核心九:深入浅出 Java 内存模型的正确姿势
JVM 内存结构、Java 内存模型、Java 对象模型对别
- JVM 内存结构:和 Java 虚拟机的
运行时区域
有关。 - Java 内存模型:和 Java 的
并发
编程有关。 - Java 对象模型:和 Java 对象在
虚拟机中的表现形式
有关。
Java 内存模型是什么
Java Memory Model(JMM)是一组规范
,需要各个 JVM 的实现来遵守 JMM 规范,以便于开发者可以利用这些规范,更方便地开发多线程程序
。
如果没有这样的一个 JMM 内存模型来规范,那么很可能经过了不同 JVM 内存模型来规范,那么很可能经过了不同 JVM 的不同规则的重排序后,导致不同的虚拟机上运行的结果不一样
。
volatile、synchronized、Lock 等原理都是 JMM
如果没有 JMM,那就需要我们自己指定书目时候用内存栅栏等,那是相当麻烦的,幸好有 JMM,我们只需要用同步工具和关键字
就可以开发并行程序。
JMM 最重要的三点:重排序
、可见性
、原子性
。
重排序
我们看下面代码例子:
public class ThreadTest {
static int a = 0;
static boolean flag = false;
public static void main(String[] args) {
while (true) {
Thread t1 = new Thread(() -> {
a = 1;
flag = true;
});
Thread t2 = new Thread(() -> {
if (flag && a == 0) {
System.out.println("----------指令重排序------------");
}
});
t1.start();
t2.start();
a = 0;
flag = false;
}
}
}
运行结果:
----------指令重排序------------
----------指令重排序------------
我们可以看到在 t1 线程里面代码顺序和我们想的并不一样,在有些情况下执行出来的结果是先给 flag 赋值为 true,但 a 还是 0,所以在线程 2 中判断出来之后打印了指令重排序。
由此我们可以很清楚的知道,代码指令并不是严格按照代码语句的顺序执行的
。
那为什么需要重排序呢?
因为这样可以提高处理速度
我们看下图没有重排序是什么样子的:
先 load 一个 a 然后 set 一个 3 保存,b 也同理,最后 load a 加成 4 保存。
但是排序之后就是这个样子:
可见性
我们看下面代码例子:
public class ThreadTest {
int i = 0;
boolean flag = true;
public static void main(String[] args) throws InterruptedException {
ThreadTest threadTest = new ThreadTest();
new Thread(() -> {
System.out.println("初始状态为:" + threadTest.flag);
while (threadTest.flag) {
threadTest.i++;
}
System.out.println(threadTest.i);
}).start();
Thread.sleep(3000L);
threadTest.flag = false;
System.out.println("最后状态为:" + threadTest.flag);
}
}
运行结果:
我们明明已经改为了 false,但是并没有打印出来 i 的值,程序也没有停止。
主要导致的原因其实就是,主线程中修改了 flag,在子线程中不可见。 一部分原因是 CPU 高速缓存在极短的时间内不可见,另外一点即使 flag 已经同步到了主内存中,但是子线程中还是没有读到 flag。
CPU 高速缓存在极短的时间内不可见的,一段时间后还是会同步到主内存中,但是 while 是一个循环不停的从主内存中获取 flag 的值,每次都是 true 这是因为 JVM 和 JIT 优化导致的,方法体中的循环被多次执行,JIT 将循环体中缓存到了方法区,每次运行直接从方法区中读取缓存,而方法区缓存的 flag=true,导致 while 循环不能被终止。其实就是主线程写和子线程读的原因。
使用 volatile 关键字
public class ThreadTest {
int i = 0;
volatile boolean flag = true;
public static void main(String[] args) throws InterruptedException {
ThreadTest threadTest = new ThreadTest();
new Thread(() -> {
System.out.println("初始状态为:" + threadTest.flag);
while (threadTest.flag) {
threadTest.i++;
}
System.out.println(threadTest.i);
}).start();
Thread.sleep(3000L);
threadTest.flag = false;
System.out.println("最后状态为:" + threadTest.flag);
}
}
运行结果:
初始状态为:true
最后状态为:false
1967391405
我们加上 volatile 后 i 的值打印了出来,程序也正常的结束了。
为什么 volatile 关键可以解决这个问题呢? volatile 如何实现它的语义的呢?
- 禁止缓存:volatile 变量的访问控制符会加上 ACC_VOLATILE
- 对 volatile 变量相关的指令不做重排序
JVM 的规范中,文档对 ACC_VOLATILE 的解释:Declared volatile;cannot be cached.(不能够被缓存)
加上 volatile 后会强制(flush)将 flag 的值刷入到主内存中,子线程就可以读取到修改后的值
每个线程都有自己的工作内存,而可能存在 writer-thread 到写入操作,还没有同步到主内存,reader-thread 会从主内存中读取。
volatile 会将写入操作,强制刷入到主内存中。
为什么会有可见性问题
CPU 有多级缓存,导致读的数据过期
- 高速缓存的容量比主内存小,但是速度仅次于寄存器,所以在 CPU 和主内存之间就多了 Cache 层。
- 线程间的对于共享变量的可见性问题不是直接由多核引起的,而是由多缓存引起的。
- 如果所有的核心都只用一个缓存,那么也就不存在内存可见性问题。
- 每个核心都会将自己需要的数据读到独占缓存中,数据修改后也是写入到缓存中,然后等待刷入到主存中,所以会导致有些核心读取的值是一个过期的值。
JMM 的抽象:主内存和本地内存
- Java 作为高级语言,屏蔽了这些底层细节,用 JMM 定义了一套读写内存数据的规范,虽然我们不在需要关心以及缓存和二级缓存的问题,但是 JMM 抽象了主内存和本地内存的概念。
- 这里说的本地内存并不是真的是一块给每个线程分配的内存,而是 JMM 的一个抽象。
- 所有的变量都存储在主内存中,同时每个线程也有自己独立的工作内存,工作内存中的变量内容是主内存中的拷贝。
- 线程不能直接读写主内存中的变量,而是只能操作自己工作内存中的变量,然后在同步到主内存中。
- 主内存是多个线程共享的,但线程间不共享工作内存,如果线程间需要通信,必须借助主内存中转换来完成
所有但共享变量存在于主内存中,每个线程有自己的本地内存,而且线程读写共享数据也是通过本地内存交换的,所以才导致了可见性问题。
Happens-Before 原则
- Happens-Before 原则是用来解决可见性问题的:在时间上,动作 A 发生在动作 B 之前,B 保证能看见 A,这就是 Happens-Before。
- 两个操作可以用 Happens-Before 来确定它们的执行顺序:如果一个操作 Happens-Before 于另一个操作,那么我们说第一个操作对于第二个操作是可见的。
- 当程序包含两个没有被 Happens-Before 关系排序的冲突访问时,就存在数据竞争,遵循了这个原则,也就意味着有些代码不能进行重排序,有些数据不能缓存。
Happens-Before 的规则有哪些?
- 单线程原则(Happens-Before 并不影响重排序)
- 锁操作(解锁前的所有操作,都对加锁后的可见)
- volatile(修改数据后会立即刷新到主存,通知其他线程来更新)
- 线程启动(在子线程启动的时候,能获得到主线程之前的语句发生的结果)
- 线程 join(在 join 之后的语句一定能看到 join 之前所有语句发生的结果)
- 传递性:如果 hb(A,B)而且 hb(B,C),那么可以推出 hb(A,C)
- 中断:一个线程被其他线程 interrupt 时,那么检测中断(isInterrupted)或者抛出 InterruptedException 一定能看到。
- 构造方法:对象构造方法的最后一行指令 Happens-Before 于 finalize()方法的第一行指令。
工具类的 Happens-Before 原则
- 线程安全的容器,get 一定能看到在此之前的 put 等存入操作
- CountDownLatch
- Smaphore(信号量)
- Future
- 线程池
- CyclicBarrier
volatile 关键字详解
volatile 是什么?
volatile 是一种同步机制
,比 synchronized 或者 Lock 更为轻量级
,因为使用 volatile 并不会发生上下文切换
等开销很大的行为。
如果一个变量被修饰成 volatile,那么 JVM 就知道这个变量可能会被并发修改。
开销小,那么响应的能力也小,虽然 volatile 是用来同步的保证线程安全的,但是 volatile 做不到 synchronized 那样的原子保护,volatile 仅在很有限的场景下才能发挥作用。
volatile 的适用场合
不适用:a++
还是直接上代码:
public class ThreadTest implements Runnable {
volatile int a;
public static void main(String[] args) throws InterruptedException {
ThreadTest threadTest = new ThreadTest();
Thread thread01 = new Thread(threadTest);
Thread thread02 = new Thread(threadTest);
thread01.start();
thread02.start();
thread01.join();
thread02.join();
System.out.println("结果为:" + threadTest.a);
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
a++;
}
}
}
运行结果:
结果为:16639
适用
场合
- boolean flag,如果一个共享变量自始至终只被各个线程赋值,而没有其他的操作,那么就可以用 volatile 来代替。synchronized 或者代替原子变量,因为赋值资深是有原子性的,而 volatile 又保证了可见性,所以就足以保证线程安全。
并不是所有 boolean 都可以,如果不是一个明确的值则会有线程安全问题。
boolean flag = true;
private void test(){
flag = true; //安全
flag = !flag; //不安全
}
- 作为刷新之前变量的
触发器
触发器就是充当,之前的操作都是被其他线程可见的,在如下代码中让 b 来充当触发器,当线程 2 读到 b=0 的时候,那么线程 1 的修改肯定是对线程 2 可见的。
int a = 1;
volatile int b = 2;
int abc = 1;
int abcd = 1;
private void change() {
abc = 7;
abcd = 70;
a = 3;
b = 0;
}
private void print() {
if (b == 0) {
//当b = 0当时候,可以确保b之前的所有操作都是可见的
System.out.println("b=" + b + " a=" + a);
}
}
volatile 的两点作用
可见性
:读一个 volatile 变量之前,需要先使相应的本地缓存失效,这样就必须到主内存读取最新值,写一个 volatile 属性会立即刷入到主内存中。指令重排序
:解决单例双重锁乱序问题
volatile 小结
- volatile 修饰符适用于以下场景:某个属性被多个线程共享,其中有一个线程修改了此属性,其他线程可以立即得到修改后的值,比如 boolean flag,或者作为触发器,实现轻量级同步。
- volatile 属性的读写操作都是
无锁
的,它不能替代 synchronized,因为他没有提供原子性
,互斥性
。因为无锁,不需要花费时间在获取锁和释放锁上,所以它是低成本
的。 - volatile 只作用于属性,用 volatile 修饰属性,这样编译器就不会对这个属性做指令重排序。
- volatile 提供了
可见性
,任何一个线程对其修改将立马对其他线程可见。volatile 属性不会被线程缓存,始终从主存中读取。 - volatile 提供了 Happens-Before 保证,对 volatile 变量 v 的写入 Happens-Before 所有其他线程后续对 v 的读操作都是最新的值。
- volatile 可以使得 long 和 double 的赋值是原子的,后续会讲 long 和 double 的原子性。
原子性
什么是原子性
一系列的操作,要么全部执行成功,要么全部不执行,不会出现执行一半的情况,是不可分割的。
Java 中的原子操作有哪些
- 除了 long 和 double 之外的
基本类型
(int,byte,boolean,short,char,float)的赋值操作 - 所有引用
reference 的赋值操作
,不管是 32 位的机器还是 64 位机器 - java.concurrent.Atomic.* 包中所有类的原子操作
long 和 double 的原子性
官方文档的描述: 非原子化处理 double 和 long,出于 Java 编程语言存储器模型的目的,对非易失性 long 或 double 值的单个写入被视为两个单独的写入:每个 32 位半写一个。这可能导致线程从一次写入看到 64 位值的前 32 位,而另一次写入看到第二次 32 位的情况。 volatile long 和 double 的写入和读取始终是原子的。对引用的写入和读取始终是原子的,无论它们是实现为 32 位还是 64 位。
在 32 位上的 JVM 上 long 和 double 的操作不是原子的,但是在 64 位的 JVM 上是原子的。
实际开发中:商用 Java 虚拟机中不会出现
。
原子操作 + 原子操作 != 原子操作
- 简单地把原子操作组合在一起,并
不能保证
整体依然具有原子性 - 比如 ATM 机两次取钱是两次独立的原子操作,但是期间有可能银行卡被借走了,被其他线程打断并修改
全同步的 HashMap 也不完全安全
synchronized 详解
synchronized
是什么?
synchronized 是 Java 中解决并发问题的一种最常用的方法,也是最简单的一种方法。Synchronized 的作用主要有三个:
- 原子性:确保线程互斥的访问同步代码。
- 可见性:保证共享变量的修改能够及时可见,其实是通过 Java 内存模型中的 “对一个变量 unlock 操作之前,必须要同步到主内存中;如果对一个变量进行 lock 操作,则将会清空工作内存中此变量的值,在执行引擎使用此变量前,需要重新从主内存中 load 操作或 assign 操作初始化变量值” 来保证的。
- 有序性:有效解决重排序问题,即 “一个 unlock 操作先行发生(Happen-Before)于后面对同一个锁的 lock 操作”。
从语法上讲,synchronized 可以把任何一个非 null 对象作为"锁",在 HotSpot JVM 实现中,锁有个专门的名字:对象监视器(Object Monitor)
。
synchronized 总共有三种用法:
- 当 synchronized 作用在实例方法时,监视器锁(monitor)便是对象实例(this)。
- 当 synchronized 作用在静态方法时,监视器锁(monitor)便是对象的 Class 实例,因为 Class 数据存在于永久代,因此静态方法锁相当于该类的一个全局锁。
- 当 synchronized 作用在某一个对象实例时,监视器锁(monitor)便是括号括起来的对象实例。
synchronized 能够保证在同一时刻
最多只有一个
线程执行该段代码,以达到保证并发安全的效果。
synchronized 的地位
synchronized 是 Java 的关键字
,被 Java 语言原生支持。
是最基本的互斥同步手段。
代码演示:i++问题
之前我们已经使用过 volatile 来保证在并发环境下结果正确的情况。我们使用 synchronized 来演示一下。
public class ThreadTest implements Runnable {
int a;
public static void main(String[] args) throws InterruptedException {
ThreadTest threadTest = new ThreadTest();
Thread thread01 = new Thread(threadTest);
Thread thread02 = new Thread(threadTest);
thread01.start();
thread02.start();
thread01.join();
thread02.join();
System.out.println("结果为:" + threadTest.a);
}
@Override
public synchronized void run() {
for (int i = 0; i < 10000; i++) {
a++;
}
}
}
运行结果:
结果为:20000
在一个线程拿到锁后,另一个线程只能阻塞等待其他线程释放这把锁,释放了之后他们会进行争抢这把锁,而不是有序性。
也就是说 synchronized 是非公平锁
。
synchronized 的两个用法
对象锁
包括方法锁(默认锁对象为 this 当前实例对象)和同步代码块锁(自己指定锁对象)。
i++问题的另一种对象锁写法:
@Override
public void run() {
synchronized (this) {
for (int i = 0; i < 10000; i++) {
a++;
}
}
}
锁某一个对象:
Object lock = new Object();
@Override
public void run() {
synchronized (lock) {
for (int i = 0; i < 10000; i++) {
a++;
}
}
}
类锁
指 synchronized 修饰静态的方法或者指定锁为 Class 对象。
i++问题的类锁写法(Class 对象):
@Override
public void run() {
synchronized (ThreadTest.class) {
for (int i = 0; i < 10000; i++) {
a++;
}
}
}
i++问题的另一种类锁写法(synchronized 加在 static 方法上):
public class ThreadTest implements Runnable {
static int a;
public static void main(String[] args) throws InterruptedException {
ThreadTest threadTest = new ThreadTest();
Thread thread01 = new Thread(threadTest);
Thread thread02 = new Thread(threadTest);
thread01.start();
thread02.start();
thread01.join();
thread02.join();
System.out.println("结果为:" + ThreadTest.a);
}
@Override
public void run() {
method();
}
private static synchronized void method() {
for (int i = 0; i < 10000; i++) {
a++;
}
}
}
synchronized 性质
性质一:可重入
什么是可重入?
可重入指的是统一线程的外层函数获得锁之后,内层函数可以直接再次获得该锁。
优点:避免死锁。
粒度:
- 证明同一个方法是可重入的。
public class ThreadTest {
int a;
public static void main(String[] args) {
new ThreadTest().method1();
}
private synchronized void method1() {
System.out.println(Thread.currentThread().getName()+"进入method1...");
if (a == 0) {
a++;
method1();
}
}
}
运行结果:
main进入method1...
main进入method1...
因为这些方法输出了相同的线程名称,表明即使递归使用 synchronized 也没有发生死锁,证明其是可重入的。
- 证明可重入的不要求是同一个方法
public class ThreadTest {
public static void main(String[] args) {
new ThreadTest().method1();
}
private synchronized void method1() {
System.out.println(Thread.currentThread().getName() + "进入method1...");
method2();
}
private synchronized void method2() {
System.out.println(Thread.currentThread().getName() + "进入method2...");
}
}
运行结果:
main进入method1...
main进入method2...
- 证明可重入的不要求是同一个类中的
public class ThreadTest extends FatherTest {
public static void main(String[] args) {
new ThreadTest().method1();
}
private synchronized void method1() {
System.out.println(Thread.currentThread().getName() + "进入method1...");
method2();
}
}
class FatherTest {
public synchronized void method2() {
System.out.println(Thread.currentThread().getName() + "进入父类method2...");
}
}
运行结果:
main进入method1...
main进入父类method2...
性质二:不可中断
什么是不可中断性质?
一旦这个锁已经被别人获得了,如果我还想获得,我只能选择等待或者阻塞,直到别的线程释放
这个锁,如果别人永远不释放锁,那么我只能永远等下去。
加锁和释放锁的原理
每一个 java 对象都可以用作 monitor,monitor 被称为内置锁,线程在进入同步代码块之前会自动获取 monitor lock,并且它在退出同步代码块的时候,会自动释放 monitor lock(无论是正常执行返回还是抛出异常退出)。所以获取 monitor lock 的唯一途径就是进入到 Synchronized 所保护的方法中。
通过反编译我们可以看到下图字节码:
这个时候我们想要的 monitorenter 和 monitorexit 出现了。
monitorenter 入口只有一个,但是 monitorexit 的出口有多个,因为程序异常也会执行 monitorexit。
可重入原理:加速次数计数器
- JVM 负责跟踪对象被加锁的次数。
- 线程第一次给对象加锁的时候,计数器变为 1,每当这个相同的线程在此对象上再次获得锁时,计数器会递增。
- 每当任务离开时候,计数器递减,当计数器为 0 的时候,锁被释放。
可见性原理
在释放锁之前一定会将数据写回主内存
一旦一个代码块或者方法被 synchronized 所修饰,那么它执行完毕之后,被锁住的对象所做的任何修改都要在释放之前,从线程内存写回到主内存。也就是说它不会存在线程内存和主内存内容不一致的情况。
在获取锁之后一定从主内存中读取数据
同样的,线程在进入代码块得到锁之后,被锁定的对象的数据也是直接从主内存中读取出来的,由于上一个线程在释放的时候会把修改好的内容回写到主内存,所以线程从主内存中读取到数据一定是最新的。
就是通过这样的原理,synchronized 关键字保证了我们每一次的执行都是可靠的,它保证了可见性。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。