参考: java并发编程艺术
Juc01_多线程概述、四种实现方式、常用方法API、生命周期、买票案例、synchronized锁
进程和线程
进程:是操作系统中正在运行的应用程序,是程序的集合,是操作系统资源分配的基本单位。一个进程往往可以包含多个线程,至少要1个。
线程:是进程的执行单元或说执行场景,用来执行具体的任务和功能,是CPU调度和分派的基本单位。
进程是操作系统调度和资源分配的最小单位,线程是CPU调度和分派的最小单位。
java默认有几个线程?至少2个:main主线程,GC垃圾回收线程
**java真的可以开启线程吗?**开不了。
它是先通过调用start()方法:public synchronized void start()
该方法里先是把该线程加入到一个线程组中:group.add(this),然后调用了start0()方法,这是个本地方法,它是调用底层的C++程序,要明白一点,java是无法直接操作硬件的。而是通过这些本地方法去调用底层的c或c++程序来操作硬件。
并发&并行
并发:在同一时刻只能有一条指令执行,它只能把CPU运行时间划分成若干个时间段,再将时间段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状态,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。
并行:指在同一时刻,有多条指令在多个处理器上同时执行,当系统有一个以上CPU时,则线程的操作有可能非并发。当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)。
区别:并发和并行是即相似又有区别的两个概念,并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔内发生。在多道程序环境下,并发性是指在一段时间内宏观上有多个程序在同时运行,但在单处理机系统中,每一时刻却仅能有一道程序执行,故微观上这些程序只能是分时地交替执行。倘若在计算机系统中有多个处理机,则这些可以并发执行的程序便可被分配到多个处理机上,实现并行执行,即利用每个处理机来处理一个可并发执行的程序,这样,多个程序便可以同时执行。
多线程的实现方式
继承Thread
package com.juc.base;
//注意:打印出来的结果是线程交替执行
public class ThreadDemo {
public static void main(String[] args) {
//4.创建Thread类的子类对象
MyThread myThread = new MyThread();
myThread.setName("继承Thread类线程");
//5.调用start()方法开启线程
//[ 会自动调用run方法这是JVM做的事情,源码看不到 ]
myThread.start();
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + "输出" + i);
}
/**
* output
* main输出0
* 继承Thread类线程输出0
* main输出1
* 继承Thread类线程输出1
* main输出2
* main输出3
* main输出4
* 继承Thread类线程输出2
* main输出5
* 继承Thread类线程输出3
* ...
*/
}
}
//1.定义一个类继承Thread
class MyThread extends Thread {
//2.重写run方法
@Override
public void run() {
//3.将要执行的代码写在run方法中
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + "输出" + i);
}
}
}
实现Runnable接口
package com.juc.base;
public class RunnableDemo {
public static void main(String[] args) {
//4.创建Runnable的子类对象
MyRunnale mr = new MyRunnale();
//5.将子类对象当做参数传递给Thread的构造函数,并开启线程
//MyRunnale taget=mr; 多态
new Thread(mr).start();
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + "输出" + i);
}
/**
* output
* main输出0
* Thread-0输出0
* main输出1
* Thread-0输出1
* main输出2
* main输出3
* Thread-0输出2
* main输出4
* Thread-0输出3
* Thread-0输出4
* Thread-0输出5
* Thread-0输出6
* ...
*/
}
}
//1.定义一个类实现Runnable
class MyRunnale implements Runnable {
//2.重写run方法
@Override
public void run() {
//3.将要执行的代码写在run方法中
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + "输出" + i);
}
}
}
源码分析
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
if (name == null) {
throw new NullPointerException("name cannot be null");
}
this.name = name;
// 当前线程就是该线程的父线程
Thread parent = currentThread();
SecurityManager security = System.getSecurityManager();
if (g == null) {
/* Determine if it's an applet or not */
/* If there is a security manager, ask the security manager
what to do. */
// 如果 SecurityManager 不为空,从中获取 ThreadGroup
if (security != null) {
g = security.getThreadGroup();
}
/* If the security doesn't have a strong opinion of the matter
use the parent thread group. */
// 如果安全性对此事没有特殊要求,则使用父线程组
if (g == null) {
g = parent.getThreadGroup();
}
}
/* checkAccess regardless of whether or not threadgroup is
explicitly passed in. */
// 不管是否显式传入线程组,都执行 checkAccess
g.checkAccess();
/*
* Do we have the required permissions?
*/
// 查看是否具有所需的权限
if (security != null) {
if (isCCLOverridden(getClass())) {
security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
}
}
g.addUnstarted();
this.group = g;
// 将 daemon 、 priority 属性设置为父线程的对应属性
this.daemon = parent.isDaemon();
this.priority = parent.getPriority();
if (security == null || isCCLOverridden(parent.getClass()))
this.contextClassLoader = parent.getContextClassLoader();
else
this.contextClassLoader = parent.contextClassLoader;
this.inheritedAccessControlContext =
acc != null ? acc : AccessController.getContext();
this.target = target;
setPriority(priority);
// 将父线程的 InheritableThreadLocal 复制过来
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
/* Stash the specified stack size in case the VM cares */
// 存放指定的堆栈大小,以防 VM cares
this.stackSize = stackSize;
/* Set thread ID */
// 分配一个线程 ID
tid = nextThreadID();
}
在上述过程中,一个新构造的线程对象是由其parent线程来进行空间分配的,而child线程继承了 parent是否为 Daemon、优先级和加载资源的 contextClassLoader以及可继承的 ThreadLocal,同时还会分配一个唯一的 ID来标识这个 child线程。至此,一个能够运行的线程对象就初始化好了,在堆内存中等待着运行。
Callable接口(创建线程)
. Callable接口中的call方法和Runnable接口中的run方法的区别
- 是否有返回值(Runnable接口没有返回值 Callable接口有返回值)
- 是否抛异常(Runnable接口不会抛出异常 Callable接口会抛出异常)
- 落地方法不一样,一个是call() ,一个是run()
Future接口概述
- FutureTask是Future接口的唯一的实现类
- RunnableFuture同时实现了Runnable接口与Future接口,FutureTask实现了RunnableFuture接口,且构造方法接收Callable入参,所以它既可以作为Runnable被线程执行,又可以作为Futrue得到Callable的返回值。
/**
* Creates a {@code FutureTask} that will, upon running, execute the
* given {@code Callable}.
*
* @param callable the callable task
* @throws NullPointerException if the callable is null
*/
public FutureTask(Callable<V> callable) {
if (callable == null)
throw new NullPointerException();
this.callable = callable;
this.state = NEW; // ensure visibility of callable
}
package com.juc.base;
import java.util.concurrent.*;
public class FutureTaskDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
FutureTask<String> futureTask = new FutureTask<String>(new CallableThread());
Thread t1 = new Thread(futureTask, "继承Callable线程");
t1.start();
System.out.println(Thread.currentThread().getName() + "\t ----忙其它任务了");
//System.out.println(futureTask.get());
//System.out.println(futureTask.get(3,TimeUnit.SECONDS));
while (true) {
if (futureTask.isDone()) {
System.out.println(futureTask.get());
break;
} else {
//暂停毫秒
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("正在处理中,不要再催了,越催越慢 ,再催熄火");
}
}
/**
* output
* main ----忙其它任务了
* 继承Callable线程 -----come in
* 正在处理中,不要再催了,越催越慢 ,再催熄火
* 正在处理中,不要再催了,越催越慢 ,再催熄火
* 正在处理中,不要再催了,越催越慢 ,再催熄火
* 继承Callable线程 -----come out
* 正在处理中,不要再催了,越催越慢 ,再催熄火
* 计算结果为:5050
*/
}
}
class CallableThread implements Callable {
@Override
public Object call() throws Exception {
System.out.println(Thread.currentThread().getName() + "\t -----come in");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum += i;
}
System.out.println(Thread.currentThread().getName() + "\t -----come out");
return "计算结果为:" + sum;
}
}
/**
* 1 get容易导致阻塞,一般建议放在程序后面,一旦调用不见不散,非要等到结果才会离开,不管你是否计算完成,容易程序堵塞。
* 2 假如我不愿意等待很长时间,我希望过时不候,可以自动离开.
*/
FutureTask原理解析
有了Runnable,为什么还要有Callable接口?我们假设一共有四个程序需要执行,第三个程序时间很长 | Runnable接口会按照顺序去执行,会依次从上到下去执行,会等第三个程序执行完毕,才去执行第四个 | Callable接口会把时间长的第三个程序单独开启一个线程去执行,第1、2、4 线程执行不受影响
比如主线程让一个子线程去执行任务,子线程可能比较耗时,启动子线程开始执行任务。子线程就去做其他的事情,过一会儿才去获取子任务的执行结果
注意事项
- get( )方法建议放在最后一行,防止线程阻塞(一旦调用了get( )方法,不管是否计算完成都会阻塞)
- 一个FutureTask,多个线程调用call( )方法只会调用一次
- 如果需要调用call方法多次,则需要多个FutureTask
package com.juc.base;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class CallableDemo {
public static void main(String[] args) throws Exception {
CallAble c = new CallAble();
FutureTask<Integer> futureTask = new FutureTask<>(c);
new Thread(futureTask, "线程A").start();
new Thread(futureTask, "线程B").start();
Integer integer = futureTask.get();
System.out.println("integer = " + integer);
/**
* output
* 线程A ----调用call方法
* integer = 6
*/
}
}
class CallAble implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName() + "\t ----调用call方法");
return 6;
}
}
线程池
设置和获取线程名称
- void setName(String name):将此线程的名称更改为等于参数 name
- String getName( ):返回此线程的名称,注意:要是类没有继承Thread,不能直接使用getName( ) ;要是没有继承Thread,要通过Thread.currentThread得到当前线程,然后调用getName( )方法。
线程优先级(setPriority)
线程有两种调度模型 [ 了解 ]
- 分时调度模式:所有线程轮流使用CPU的使用权,平均分配每个线程占有CPU的时间片
- 抢占式调度模型:优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的CPU时间片相对多一些 [ Java使用的是抢占式调度模型 ]
Thread类中设置和获取线程优先级的方法
public final void setPriority(int newPriority):更改此线程的优先级
public final int getPriority():返回此线程的优先级
- 线程默认优先级是5;线程优先级范围是:1-10;
- 线程优先级高仅仅表示线程获取的CPU时间的几率高,但是要在次数比较多,或者多次运行的时候才能看到你想要的效果
线程控制(sleep、join、yield、setDeamon)
- static void sleep(long millis):使当前正在执行的线程停留(暂停执行)指定的毫秒数 (休眠线程)
- void join():当前线程暂停,等待调用该方法的的线程执行结束后,当前线程再继续 (相当于插队加入)
- void join(int millis):当前线程暂停,等待调用该方法的的线程millis后,当前线程再继续 ,当millis等于0时意味着永远等待,相当于join()方法(相当于插队,有固定的时间)
- void yield():让当前线程从运行状态 转为 就绪状态
- void setDaemon(boolean on):将此线程标记为守护线程,当运行的线程都是守护线程时,Java虚拟机将退出(守护线程)(相当于象棋中的帅,要是帅没了,别的棋子都会没用了)
守护线程是区别于用户线程哈,用户线程即我们手动创建的线程,而守护线程是程序运行的时候在后台提供一种通用服务的线程。垃圾回收线程就是典型的守护线程
守护线程拥有自动结束自己生命周期的特性,非守护线程却没有。如果垃圾回收线程是非守护线程,当JVM 要退出时,由于垃圾回收线程还在运行着,导致程序无法退出,这就很尴尬。这就是为什么垃圾回收线程需要是守护线程t1.setDaemon(true)一定要在start( )方法之前使用。在构建 Daemon线程时,不能依靠 finally块中的内容来确保执行关闭或清理资源的逻辑。
Daemon线程
Daemon线程是一种支持型线程 (常被叫做守护线程 ),因为它主要被用作程序中后台调度以及支持性工作。这意味着,当一个 Java虚拟机中不存在非 Daemon线程的时候,Java虚拟机将会退出。可以通过调用 Thread.setDaemon(true)将线程设置为 Daemon线程。
注意:Daemon属性需要在启动线程之前设置,不能在启动线程之后设置。Daemon线程被用作完成支持性工作,但是在 Java虚拟机退出时 Daemon线程中的 finally块并不一定会执行。
package com.juc.base;
import java.util.concurrent.*;
public class DaemonDemo
{
public static void main(String[] args)//一切方法运行的入口
{
Thread t1 = new Thread(() -> {
try {
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}finally {
System.out.println("daemon thread finally run !");
}
},"t1");
t1.setDaemon(true);
t1.start();
System.out.println(Thread.currentThread().getName()+"\t ----end 主线程");
}
}
运行Daemon程序,可以看到在终端或者命令提示符上只有main线程输出。 main线程(非 Daemon线程)在启动了线程 t1之后随着 main方法执行完毕而终止,而此时 Java虚拟机中已经没有非 Daemon线程,虚拟机需要退出。 Java虚拟机中的所有Daemon线程都需要立即终止,因此 t1立即终止,但是 t1中的finally块并没有执行。
join
如果在一个线 程 A执 行了 thread.join()语 句,其含 义 是:当前线程 A等待 thread线 程(调用join方法的线程) 终止之后才从 thread.join()返回。在下面例子中main线程就是线程A,join线程为thread线程。 线 程 Thread除了提供 join()方法之外, 还 提供了 join(long millis)和 join(longmillis,int nanos)两个具备超时特性的方法。 这 两个超 时方法表示,如果线 程 thread在 给 定的超 时时间 里没有终 止,那么将会从该超时方法中返回。
public class ThreadJoinDemo {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
},"join线程");
thread.start();
thread.join();
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
System.out.println(Thread.currentThread().getName() + " end-------");
}
}
输出结果总是先打印完join线程的,最后打印完main线程的输出
若将第9行的thread.join();注释掉,则会两个线程轮流打印
理解中断
中断可以理解为线程的一个标识位属性,它表示一个运行中的线程是否被其他线程进行了中断操作。中断好比其他线程对该线程打了个招呼,其他线程通过调用该线程的interrupt()方法对其进行中断操作。
线程通过检查自身是否被中断来进行响应,线程通过方法isInterrupted()来进行判断是否被中断,也可以调用静态方法 Thread.interrupted()对当前线程的中断标识位进行复位。如果该线程已经 处于终结状态,即使该线程被中断过,在调用该线程对象的isInterrupted()时依旧会返回 false。
从Java的 API中可以看到,许多声明抛出 InterruptedException的方法(例如Thread.sleep(longmillis)方法)这些方法在抛出 InterruptedException之前, Java虚拟机会先将该线程的中断标识位清除,然后抛出InterruptedException,此时调用 isInterrupted()方法将会返回 false。
public class InterruptedDemo {
public static void main(String[] args) throws InterruptedException {
Thread sleepThread = new Thread(() -> {
while (true) {
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "sleepThread");
Thread busyThread = new Thread(() -> {
while (true) {
}
}, "busyThread");
sleepThread.setDaemon(true);
busyThread.setDaemon(true);
sleepThread.start();
busyThread.start();
// 休眠5秒,让sleepThread与busyThread充分运行
TimeUnit.SECONDS.sleep(5);
sleepThread.interrupt();
busyThread.interrupt();
System.out.println("sleepThread interrupt is " + sleepThread.isInterrupted());
System.out.println("busyThread interrupt is " + busyThread.isInterrupted());
// 防止sleepThread与busyThread立刻退出
TimeUnit.SECONDS.sleep(2);
}
}
从结果可以看出,抛出InterruptedException的线程 sleepThread,其中断标识位被清除了,而一直忙碌运作的线程 busyThread,中断标识位没有被清除。
弃用的 suspend()、 resume()和 stop()
方法 | 作用 |
---|---|
suspend() | 暂停此线程,如果线程是活动的,它会被挂起并且不会继续前进,除非它被恢复。 |
resume() | 恢复挂起的线程。如果线程处于活动状态但被挂起,它会被恢复并被允许在其执行中取得进展。 |
stop() | 强制线程停止执行 |
不建议使用的原因主要有:以suspend()方法为例,在调用后,线程不会释放已经占有的资源(比如锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题。同样,stop()方法在终结一个线程时不会保证线程的资源正常释放,通常是没有给予线程完成资源释放工作的机会,因此会导致程序可能工作在不确定状态下。
线程的生命周期
java线程状态
public enum State {
NEW,//新建状态
RUNNABLE,//运行状态
BLOCKED,//阻塞状态
WAITING,//等待(死死地等)
TIMED_WAITING,//超时等待(超过一定时间就不等了)
TERMINATED;//终止状态
}
- 初始状态(NEW):线程被创建,但是还没有调用start()方法;
- 运行状态(RUNNABLE):java 线程将操作系统的就绪和运行两种状态笼统的称作“运行中”
- 阻塞状态(BLOCKED):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
- 等待阻塞:运行的线程执行wait()方法,该线程进入等待池中
- 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则该线程进入锁池中
- 其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
- 等待状态(WAITING):表示线程进入等待状态,进入该状态标识当前线程需要等待其它线程做出一些特定的动作(通知或终端)
- 超时等待状态(TIMED_WAITING),该状态不同于 ,它是可以在指定的时间自行返回的;
- 终止状态(TERMINATED):表示当前线程已经执行完毕;
线程在自身的生命周期中,并不是固定地处于某个状态,而是随着代码的执行在不同的状态之间进行切换,Java线程状态变迁如下图 所示。
由图4可以看出,线程创建之后,调用 start()方法开始运行。当线程执行 wait()方法之后,线程进入等待状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而超时等待状态相当于在等待状态的基础上增加了超时限制,也就是超时时间到达时将会返回到运行状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到阻塞状态。线程在执行 Runnable的 run()方法之后将会进入到终止状态。
Java将操作系统中的运行和就绪两个状态合并称为运行状态。阻塞状态是线程阻塞在进入 synchronized关键字修饰的方法或代码块(获取锁)时的状态,但是阻塞在 java.concurrent包中 Lock接口的线程状态却是等待状态,因为 java.concurrent包中Lock接口对于阻塞的实现均使用了 LockSupport类中的相关方法。
package com.juc.base;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
/**
* @author xu
* @date 2022-06-10 11:04
*/
public class SynchronizedBlockDemo {
public static void main(String[] args) {
Object object1 = new Object();
Object object2 = new Object();
new Thread(()->{
synchronized (object1){
System.out.println(Thread.currentThread().getName()+"进入同步方法,等待获取object2锁");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (object2){
System.out.println();
}
}
},"线程A").start();
new Thread(()->{
synchronized (object2){
System.out.println(Thread.currentThread().getName()+"进入同步方法,等待获取object1锁");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (object1){
System.out.println();
}
}
},"线程B").start();
}
}
package com.juc.base;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author xu
* @date 2022-06-10 11:11
*/
public class LockWaitngDemo {
public static void main(String[] args) {
Lock lock = new ReentrantLock();
new Thread(() -> {
System.out.println(Thread.currentThread().getName()+"线程开始执行");
lock.lock();
try {
System.out.println(Thread.currentThread().getName()+"进入同步方法");
TimeUnit.SECONDS.sleep(20);
} catch (Exception e) {
} finally {
lock.unlock();
}
}, "线程A").start();
new Thread(() -> {
System.out.println(Thread.currentThread().getName()+"线程开始执行");
lock.lock();
try {
System.out.println(Thread.currentThread().getName()+"进入同步方法");
TimeUnit.SECONDS.sleep(20);
} catch (Exception e) {
} finally {
lock.unlock();
}
}, "线程B").start();
}
}
线程状态查看
jstack
jstack(Stack Trace for Java)命令用于生成虚拟机当前时刻的线程快照(一般称为threaddump或者javacore文件)。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的目的通常是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间挂起等,都是导致线程长时间停顿的常见原因。线程出现停顿时通过jstack来查看各个线程的调用堆栈,就可以获知没有响应的线程到底在后台做些什么事情,或者等待着什么资源。
jstack命令格式:
jstack [ option ] vmid
option选项的合法值与具体含义如表所示。
vmid:使用jps指令获取当前类的虚拟机进程id
jps [ options ] [ hostid ]
从JDK 5起,java.lang.Thread类新增了一个getAllStackTraces()方法用于获取虚拟机中所有线程的StackTraceElement对象。使用这个方法可以通过简单的几行代码完成jstack的大部分功能,在实际项目中不妨调用这个方法做个管理员页面,可以随时使用浏览器来查看线程堆栈。
线程通信
线程开始运行, 拥 有自己的 栈 空 间 ,就如同一个脚本一 样 ,按照既定的代 码 一步一步地 执 行,直到 终 止。但是,每个运行中的 线 程,如果 仅仅 是孤立地运行,那么没有一点儿价 值 ,或者 说 价 值 很少,如果多个 线 程能 够 相互配合完成工作, 这 将会 带 来巨大的价 值 。
volatile和 synchronized关键字
Java支持多个线程同时访问一个对象或者对象的成员变量,由于每个线程可以拥有这个变量的拷贝(虽然对象以及成员变量分配的内存是在共享内存中的,但是每个执行的线程还是可以拥有一份拷贝,这样做的目的是加速程序的执行,这是现代多核处理器的一个显著特性),所以程序在执行过程中,一个线程看到的变量并不一定是最新的。
volatile关键字 被用来保证可见性,即保证共享变量的内存可见性以解决缓存一致性问题。一旦一个共享变量被 volatile关键字 修饰,那么就具备了两层语义:内存可见性和禁止进行指令重排序。在多线程环境下,volatile关键字 主要用于及时感知共享变量的修改,并使得其他线程可以立即得到变量的最新值
关键字synchronized可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步 块中,它保证了线程对变量访问的可见性和排他性。
对于同步块的实现使用了 monitorenter和 monitorexit指令,而同步方法则是依靠方法修饰符上的 ACC_SYNCHRONIZED来完成的。无论采用哪种方式,其本质是对一个对象的监视器( monitor)进行获取,而这个获取过 程是排他的,也就是同一时刻只能有一个线程获取到由 synchronized所保护对象的监视器。任意一个对象都拥有自己的监视器,当这个对象由同步块或者这个对象的同步方法调用时,执行方法的线程必须先获取到该对象的监视器才能进入同步块或者同步方法,而没有获取到监视器(执行该方法)的线程将会被阻塞在同步块和同步方法的入口处,进入 BLOCKED状态
从上图中可以看到,任意线程对 Object Object由 synchronized保护)的访问,首先要获得 Object的监视器。如果获取失败,线程进入同步队列,线程状态变为BLOCKED。当访问 Object 的前驱(获得了锁的线程)释放了锁,则该释放操作唤醒阻塞在同步队列中的线程,使其重新尝试对监视器的获取。
等待 /通知机制
一个线程修改了一个对象的值,而另一个线程感知到了变化,然后进行相应的操作,整个过程开始于一个线程,而最终执行又是另一个线程。前者是生产者,后者就是消费者,这种模式隔离了 “做什么 what)和 “怎么做 How),在功能层面上实现了解耦,体系结构上具备了良好的伸缩性,Java通 过 内置的等待 /通知机制来实现这样的功能。等待/通知的相关方法是任意 Java对 象都具 备 的,因 为这 些方法被定 义 在所有 对 象的超 类 java.lang.Object上来供我们使用:
- void notify( ):唤醒正在此对象的监视器上等待的单个线程,使其从wait方法返回,而返回的前提是该线程获取到了对象的锁,如果有其它任何线程正在等待该对象,则选择其中一个被唤醒。该选择是任意的,并由实施自行决定;
- void notifyAll( ):唤醒正在此对象的监视器上等待的所有线程;
- viod wait( ):调用该方法的线程进入WAITING状态,只有等待另外线程的通知(此对象调用notify方法或notifyAll)或中断(interrupt)才会返回,需要注意调用wait方法后会释放对象的锁;
- void wait(long timeout):超时等待一段时间,这里的时间参数是毫秒;
- void wait(long timeout, int nanos):对超时时间更细粒度的控制,可以达到纳秒;
(注意:wait、notify、notifyAll方法必须要在同步块或同步方法里且成对出现使用)
等待/通知机制,是指一个线程 A调用了对象O的 wait()方法进入等待状态,而另一个线程 B调用了对象 O的 notify()或者 notifyAll()方法,线程 A收到通知后从对象 O的wait()方法返回,进而执行后续操作。上述两个线程通过对象 O来完成交互,而对象上的wait()和 notify/notifyAll()的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。
示例:
package com.juc.signal;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;
public class SynchronizedSignalDemo {
static boolean flag = true;
static Object lock = new Object();
private static String getDate() {
return new SimpleDateFormat("HH:mm:ss").format(new Date());
}
static class Wait implements Runnable {
@Override
public void run() {
// 加锁,拥有lock的Monitor
synchronized (lock) {
while (flag) {
System.out.println(Thread.currentThread().getName() + "flag is true, time is " + getDate());
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 条件满足时,完成工作
System.out.println(Thread.currentThread().getName() + " flag is false,running wait @" + getDate());
}
}
}
static class Notify implements Runnable {
@Override
public void run() {
// 加锁,拥有lock的Monitor
synchronized (lock) {
// 获取待lock锁
System.out.println(Thread.currentThread().getName() + " hold lock, notify @ " + getDate());
lock.notifyAll();
flag = false;
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 再次加锁
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + " hold lock again, sleep @ " + getDate());
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread waitThread = new Thread(new Wait(), "WaitThread");
waitThread.start();
TimeUnit.SECONDS.sleep(1);
Thread notifyThread = new Thread(new Notify(), "NotifyThread");
notifyThread.start();
}
}
输出结果
上述例子主要说明了调用 wait()、
notify()以及 notifyAll()时需要注意的细节,如下。
- 使用 wait()、 notify()和 notifyAll()时需要先对调用对象加锁。
- 调用 wait()方法后,线程状态由 RUNNING变为 WAITING,并将当前线程放置到对象的等待队列。
- notify()或 notifyAll()方法调用后,等待线程依旧不会从 wait()返回,需要调用notify()或 notifAll()的线程释放锁之后,等待线程才有机会从 wait()返回。
- notify()方法将等待队列中的一个等待线程从等待队列中移到同步队列中,而notifyAll() 方法则是将等待队列中所有的线程全部移到同步队列,被移动的线程状态由 WAITING变为 BLOCKED。
- 从 wait()方法返回的前提是获得了调用对象的锁。
从上述细节 中可以看到,等待 /通知机制依托于同步机制,其目的就是确保等待 线 程从 wait()方法返回 时 能 够 感知到通知 线 程 对变 量做出的修改。下图描述了上述示例的 过 程。
在图中, WaitThread首先获取了对象的锁,然后调用对象的 wait()方法,从而放弃了锁并进入了对象的等待队列 WaitQueue中,进入等待状态。由于 WaitThread释放了对象的锁, NotifyThread随后获取了对象的锁,并调用对象的 notify()方法,将WaitThread从 WaitQueue移到 SynchronizedQueue中,此时 WaitThread的状态变为阻塞状态。 NotifyThread释放了锁之后, WaitThread再次获取 到锁并从 wait()方法返回继续执行。
等待 /通知的经典范式
从上面的示例中可以提炼出等待 /通知的经典范式,该范式分为两部分,分别针对等待方(消费者)和通知方(生产者)。
等待方遵循如下原则
- 获取对象的锁。
- 如果条件不满足,那么调用对象的 wait()方法,被通知后仍要检查条件。
- 条件满足则执行对应的逻辑。
对应的伪代码如下。
synchronized (对象) {
while (条件不满足) {
对象.wait();
}
条件满足时,对应的处理逻辑
}
通知方遵循如下原则 :
- 获得对象的锁。
- 改变条件。
- 通知所有等待在对象上的线程。
对应的伪代码如下。
synchronized (对象) {
改变条件
对象.notifyAll();
}