一、线程的基本知识
1.1 线程的概念
是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。
一个进程可以有很多线程,每条线程并行执行不同的任务。
- 在多核CPU中,利用多线程可以实现真正意义上的并行执行。
- 在一个应用进程中,会存在多个同时执行的任务,如果其中一个任务 被阻塞,将会引起不依赖该任务的任务也被阻塞。通过对不同任务创 建不同的线程去处理,可以提升程序处理的实时性。
- 线程可以认为是轻量级的进程,所以线程的创建、销毁比进程更快。
为什么要用多线程?
- 异步执行(避免阻塞)。
- 利用多CPU资源实现真正意义行的并行执行。
1.3.1 继承Thread类
既然线程启动时会去调用 run 方法,那么我们只要重写 Thread 类的 run 方法也是可以定义出我们的线程类的。
public class ThreadDemo extends Thread {
@Override
public void run() {
System.out.println("当前线程:" + Thread.currentThread().getName());
}
public static void main(String[] args) {
ThreadDemo thread = new ThreadDemo();
thread.start();
}
}
当前线程:Thread-0
public class RunnableDemo implements Runnable {
public void run() {
System.out.println("当前线程:" + Thread.currentThread().getName());
}
public static void main(String[] args) {
RunnableDemo runnableDemo = new RunnableDemo();
Thread thread = new Thread(runnableDemo);
thread.start();
}
}
当前线程:Thread-0
import java.util.concurrent.*;
public class CallableDemo implements Callable<String> {
public String call() throws Exception {
System.out.println("当前线程:" + Thread.currentThread().getName());
Thread.sleep(10000); // 等待sleep执行完后才会返回“hello ly”
return "hello ly";
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(1);
Future<String> future = executorService.submit(new CallableDemo());
// future.get() 是一个阻塞方法
System.out.println(Thread.currentThread().getName() + " - " + future.get());
}
}
当前线程:pool-1-thread-1
main - hello ly
1.4 线程的生命周期
Java线程从创建到销毁,可能会经历一下6个状态:
- NEW:初始状态,线程被构建,但是还没有调用start方法。
- RUNNABLED:运行状态,JAVA线程把操作系统中的就绪和运行两种状态统一称为”运行中”。
- BLOCKED:阻塞状态,表示线程进入等待状态,也就是线程因为某种原因放弃了CPU使 用权,阻塞也分为几种情况。
- WAITING: 等待状态。
- TIME_WAITING:超时等待状态,超时以后自动返回。
- TERMINATED:终止状态,表示当前线程执行完毕。
import java.util.concurrent.TimeUnit;
public class ThreadStatusDemo {
public static void main(String[] args) {
// TIME_WAITING
new Thread(()->{
while (true) {
try {
TimeUnit.SECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "Time_Wating_Demo").start();
// WAITING
new Thread(()->{
while (true) {
synchronized (ThreadStatusDemo.class) {
try {
ThreadStatusDemo.class.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "Wating").start();
new Thread(new BlockedDemo(), "Blocked-Demo-01").start();
new Thread(new BlockedDemo(), "Blocked-Demo-02").start();
}
static class BlockedDemo extends Thread {
@Override
public void run() {
synchronized (BlockedDemo.class) {
try {
TimeUnit.SECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
1. 找到 `hsperfdata_用户名的文件夹`添加读写权限,例如本机路径`C:\Users\admin\AppData\Local\Temp\hsperfdata_admin`;
2. 右击-->属性-->安全-->高级;
二、线程的基本操作及原理
2.1 Thread.join的使用及原理
public class ThreadJoinDemo {
private static int x = 0;
private static int i = 0;
public static void main(String[] args) throws Exception {
Thread t1 = new Thread(()->{
i = 1;
x = 2;
});
Thread t2 = new Thread(()->{
i = x + 2;
});
t1.start();
t2.start();
Thread.sleep(1000);
System.out.println("result : " + i);
}
}
我们在`t1.start();`和`t2.start();`之间加入`t1.join();`就能保证了。
......
t1.start();
t1.join(); // t1线程的执行结果对于t2可见(t1线程一定会比t2线程优先执行)
t2.start();
......
使线程暂停执行一段时间,直到等待的时间结束才恢复执行或在这段时间内被中断。
import java.text.SimpleDateFormat;
public class ThreadSleepDemo extends Thread {
public static void main(String[] args) {
new ThreadSleepDemo().start();
}
@Override
public void run() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
long dt1 = System.currentTimeMillis();
System.out.println("begin: " + dt1 + " | " + sdf.format(dt1));
try {
Thread.sleep(3000);
long dt2 = System.currentTimeMillis();
System.out.println("end: " + dt2 + " | " + sdf.format(dt2));
long dt = dt2 - dt1;
System.out.println("dt2 - dt1 = " + dt);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
begin: 1650361034229 | 2022-04-19 17:37:14
end: 1650361037243 | 2022-04-19 17:37:17
dt2 - dt1 = 3014
- 假设现在是2022-04-19 12:00:00.000,如果调用Thread.Sleep(1000),那么在2022-04-19 12:00:01.000的时候,这个线程会不会被唤醒?
- Thread.Sleep(0)的意义
Thread.sleep(0)并非是真的让线程挂起0毫秒,意义在于调用Thread.sleep(0)的当前线程确实被冻结了一下,让其他线程有机会优先执行,Thread.sleep(0)是使你的线程暂时放弃cpu,也是释放一些未使用的时间片给其他线程或者进程使用,就相当于一个让位动作。
- 挂起线程并修改其运行状态。
- 用sleep()提供的参数来设置一个定时器。
- 当时间结束,定时器会触发,内核收到中断后修改线程的运行状态。
2.3.1 wait
- wait(),当前线程进入 无限等待状态,必须被唤醒才能继续执行,调用后会释放锁对象。
- wait(long timeout),wait(long timeout,int nanos),当前线程进入等待状态,可以被提前唤醒,但在指定时间后会自动唤醒。
- notify(), 随机唤醒一个在锁对象上调用wait的线程。
- notifyAll(),唤醒 全部在锁对象上调用wait的线程。
public class TestDemo {
public static void main(String[] args) {
// 创建锁对象,保证唯一
Object obj = new Object();
// 创建一个顾客线程(消费者)
new Thread() {
@Override
public void run() {
// 保证正等待和唤醒的线程只能有一个执行,需要使用同步技术
synchronized (obj) {
System.out.println("告诉老板要的包子的种类和数量");
// 调用wait方法,放弃cpu的执行,进入到WAITING状态(无限等待)
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 唤醒之后执行的代码
System.out.println("包子已经准备好了,开吃!");
}
}
}.start();
// 创建一个老板线程(生产者)
new Thread() {
@Override
public void run() {
// 花了5秒钟准备包子
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj) {
System.out.println("老板5秒钟之后准备好包子,告知顾客,可以吃包子了");
// 准备好包子之后,调用notify方法,唤醒顾客吃包子
obj.notify();
}
}
}.start();
}
}
告诉老板要的包子的种类和数量
老板5秒钟之后准备好包子,告知顾客,可以吃包子了
包子已经准备好了,开吃!
- 生产者Producer.java源码。
import java.util.Queue;
public class Producer implements Runnable {
private Queue<String> bags;
private int size;
public Producer(Queue<String> bags, int size) {
this.bags = bags;
this.size = size;
}
public void run() {
int i = 0;
while (true) {
i++;
synchronized (bags) {
while (bags.size() == size) {
System.out.println("bags 满了");
try {
bags.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("生产者 - 生产:bag " + i);
bags.add("bag " + i);
// 唤醒处于阻塞状态下的消费者
bags.notifyAll();
}
}
}
}
import java.util.Queue;
public class Consumer implements Runnable {
private Queue<String> bags;
private int size;
public Consumer(Queue<String> bags, int size) {
this.bags = bags;
this.size = size;
}
public void run() {
while (true) {
synchronized (bags) {
while (bags.isEmpty()) {
System.out.println("bags 为空。");
try {
bags.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
String bag = bags.remove();
System.out.println("消费者消费:" + bag);
// 唤醒处于阻塞状态下的生产者
bags.notifyAll();
}
}
}
}
import java.util.LinkedList;
import java.util.Queue;
public class WaitNotifyDemo {
public static void main(String[] args) {
Queue<String> queue = new LinkedList<String>();
int size = 10;
Producer producer = new Producer(queue, size);
Consumer consumer = new Consumer(queue, size);
Thread t1 = new Thread(producer);
Thread t2 = new Thread(consumer);
t1.start();
t2.start();
}
}
2.4.1 为什么Thread.stop不推荐使用?
因为它本质上是不安全的。停止线程会导致它解锁所有已锁定的监视器。(当ThreadDeath异常在堆栈中传播时,监视器被解锁。)如果之前由这些监视器保护的对象中的任何一个处于不一致状态,则其他线程现在可以以不一致的状态查看这些对象。据称这些物体被 损坏。当线程操作受损对象时,可能导致任意行为。这种行为可能微妙且难以检测,或者可能会发音。与其他未经检查的异常不同,可以 ThreadDeath静默地杀死线程;因此,用户没有警告他的程序可能被损坏。腐败现象可能会在实际损害发生后随时出现,甚至可能在未来数小时甚至数天。
import java.util.concurrent.TimeUnit;
public class StopDemo {
static volatile boolean bStop = false;
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new StopThread());
thread.start();
TimeUnit.SECONDS.sleep(2000);
bStop = true;
}
static class StopThread implements Runnable {
public void run() {
while(!bStop) {
System.out.println("持续运行");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
当其他线程通过调用当前线程的interrupt方法,表示向当前线程打个招呼,告诉他可以中断线程的执行了,至于什么时候中断,取决于当前线程自己。
import java.util.concurrent.TimeUnit;
public class InterruptDemo {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(()-> {
while(!Thread.currentThread().isInterrupted()) {
System.out.println("持续执行......");
}
});
thread.start();
Thread.sleep(1);
thread.interrupt();
System.out.println("线程中断了...");
}
}
当一个线程处于阻塞状态下(例如休眠)的情况下,调用了该线程的interrupt()方法,则会出现InterruptedException。
1. 不要生吞此异常;
2. 如果可以处理此异常:完成清理工作之后退出;
3. 不处理此异常,不继续执行任务:重新抛出;
4. 不处理此异常,继续执行任务:捕捉到异常之后恢复中断标记(交由后续程序检查中断)。
public class MyThreadDemo extends Thread {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("i = " + (i + 1));
}
}
public static void main(String[] args) {
MyThreadDemo thread = new MyThreadDemo();
thread.start();
thread.interrupt();
System.out.println("第一次调用thread.isInterrupted() : " + thread.isInterrupted());
System.out.println("第二次调用thread.isInterrupted() : " + thread.isInterrupted());
System.out.println("thread是否存活 : " + thread.isAlive());
}
}
第一次调用thread.isInterrupted() : true
第二次调用thread.isInterrupted() : true
thread是否存活 : true
i = 1
i = 2
i = 3
i = 4
i = 5
i = 6
i = 7
i = 8
i = 9
i = 10
public class MyThreadDemo extends Thread {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("i = " + (i + 1));
}
}
public static void main(String[] args) {
MyThreadDemo thread = new MyThreadDemo();
thread.start();
thread.interrupt();
System.out.println("第一次调用thread.isInterrupted() : " + thread.isInterrupted());
System.out.println("第二次调用thread.isInterrupted() : " + thread.isInterrupted());
System.out.println("第一次调用threadinterrupted() : " + thread.interrupted());
System.out.println("第二次调用thread.interrupted() : " + thread.interrupted());
System.out.println("thread是否存活 : " + thread.isAlive());
}
}
第一次调用thread.isInterrupted() : true
i = 1
第二次调用thread.isInterrupted() : true
i = 2
第一次调用threadinterrupted() : false
第二次调用thread.interrupted() : false
i = 3
thread是否存活 : true
i = 4
i = 5
i = 6
i = 7
i = 8
i = 9
i = 10
注意!!!这是一个坑!!!上面说到,interrupted()方法测试的是当前线程是否被中断。
这里当前线程是main线程,而thread.interrupt()中断的是thread线程。
所以当前线程main从未被中断过,尽管interrupted()方法是以thread.interrupted()的形式被调用,但它检测的仍然是main线程而不是检测thread线程,所以thread.interrupted()在这里相当于main.interrupted()。对于这点,下面我们再修改进行测试。
Thread.currentThread()函数可以获取当前线,下面代码中获取的是main线程。
public static void main(String[] args) {
Thread.currentThread().interrupt();
System.out.println("第一次调用Thread.currentThread().interrupt():"
+Thread.currentThread().isInterrupted());
System.out.println("第一次调用thread.interrupted():"
+Thread.currentThread().interrupted());
System.out.println("第二次调用thread.interrupted():"
+Thread.currentThread().interrupted());
}
第一次调用Thread.currentThread().interrupt():true
第一次调用thread.interrupted():true
第二次调用thread.interrupted():false
若果想要是实现调用interrupt()方法真正的终止线程,则可以在线程的run方法中做处理即可,比如直接跳出run()方法使线程结束,视具体情况而定,下面是一个例子。
public class MyThreadDemo extends Thread {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("i = " + (i + 1));
if(this.isInterrupted()) {
System.out.println("通过this.isInterrupted()检测到中断");
System.out.println("第一个interrupted()" + this.interrupted());;
System.out.println("第二个interrupted()" + this.interrupted());
break;
}
}
System.out.println("因为检测到中断,所以跳出循环,线程到这里结束,因为后面没有内容了。");
}
public static void main(String[] args) throws InterruptedException {
MyThreadDemo myThread = new MyThreadDemo();
myThread.start();
myThread.interrupt();
// sleep等待一秒,等myThread运行完
Thread.currentThread().sleep(1000);
System.out.println("myThread线程是否存活:" + myThread.isAlive());
}
}
i = 1
通过this.isInterrupted()检测到中断
第一个interrupted()true
第二个interrupted()false
因为检测到中断,所以跳出循环,线程到这里结束,因为后面没有内容了。
myThread线程是否存活:false
- interrupt() 是给线程设置中断标志。
- interrupted() 是检测中断并清除中断状态。
- isInterrupted() 只检测中断。
- 还有一点就是interrupted()作用于当前线程,interrupt() 和 isInterrupted() 作用于此线程,即代码中调用此方法的实例所代表的线程。
三、线程的安全性分析
3.1 可见性、原子性、有序性
3.1.1 线程安全
多个线程访问同一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他操作,调用这个对象的行为都可以获得正确的结果,那么这个对象就是线程安全的。
原子性是指一个事物的操作是不可分割的,要么都发生,要么都不发生。
举个例子:
张三到银行给李四转账1000元,张三卡里原来有2000元,李四卡里原来也有两千元,那么转账的步骤应该如下:
同样地反映到并发编程中会出现什么结果呢?
举个最简单的例子,大家想一下假如为一个32位的变量赋值过程不具备原子性的话,会发生什么后果?
i = 9;
那么就可能发生一种情况:当将低16位数值写入之后,突然被中断,而此时又有一个线程去读取i的值,那么读取到的就是错误的数据。
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
举个简单的例子,看下面这段代码:
//线程1执行的代码
int i = 0;
i = 10;
//线程2执行的代码
j = i;
此时线程2执行 j = i,它会先去主存读取i的值并加载到CPU2的缓存当中,注意此时内存当中i的值还是0,那么就会使得j的值为0,而不是10。
这就是可见性问题,线程1对变量i修改了之后,线程2没有立即看到线程1修改的值。
有序性:即程序执行的顺序按照代码的先后顺序执行。
举个简单的例子,看下面这段代码:
int i = 0;
boolean flag = false;
i = 1; //语句1
flag = true; //语句2
下面解释一下什么是指令重排序,一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。
比如上面的代码中,语句1和语句2谁先执行对最终的程序结果并没有影响,那么就有可能在执行过程中,语句2先执行而语句1后执行。
但是要注意,虽然处理器会对指令进行重排序,但是它会保证程序最终结果会和代码顺序执行结果相同,那么它靠什么保证的呢?再看下面一个例子:
int a = 10; //语句1
int r = 2; //语句2
a = a + 3; //语句3
r = a * a; //语句4
虽然重排序不会影响单个线程内程序执行的结果,但是多线程呢?下面看一个例子:
//线程1:
context = loadContext(); //语句1
inited = true; //语句2
//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
从上面可以看出,指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。
也就是说,要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。
在前面谈到了一些关于内存模型以及并发编程中可能会出现的一些问题。下面我们来看一下Java内存模型,研究一下Java内存模型为我们提供了哪些保证以及在Java中提供了哪些方法和机制来让我们在进行多线程编程时能够保证程序执行的正确性。
在Java虚拟机规范中试图定义一种Java内存模型(Java Memory Model,JMM)来屏蔽各个硬件平台和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。那么Java内存模型规定了哪些东西呢,它定义了程序中变量的访问规则,往大一点说是定义了程序执行的次序。注意,为了获得较好的执行性能,Java内存模型并没有限制执行引擎使用处理器的寄存器或者高速缓存来提升指令执行速度,也没有限制编译器对指令进行重排序。也就是说,在Java内存模型中,也会存在缓存一致性问题和指令重排序的问题。
Java内存模型规定所有的变量都是存在主存当中(类似于前面说的物理内存),每个线程都有自己的工作内存(类似于前面的高速缓存)。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。并且每个线程不能访问其他线程的工作内存。
i = 10;
那么Java语言 本身对 原子性、可见性以及有序性提供了哪些保证呢?
在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。
上面一句话虽然看起来简单,但是理解起来并不是那么容易。看下面一个例子i:
x = 10; //语句1
y = x; //语句2
x++; //语句3
x = x + 1; //语句4
语句1是直接将数值10赋值给x,也就是说线程执行这个语句的会直接将数值10写入到工作内存中。
语句2实际上包含2个操作,它先要去读取x的值,再将x的值写入工作内存,虽然读取x的值以及 将x的值写入工作内存 这2个操作都是原子性操作,但是合起来就不是原子性操作了。
同样的,x++和 x = x+1包括3个操作:读取x的值,进行加1操作,写入新的值。
所以上面4个语句只有语句1的操作具备原子性。
也就是说,只有简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作。
不过这里有一点需要注意:在32位平台下,对64位数据的读取和赋值是需要通过两个操作来完成的,不能保证其原子性。但是好像在最新的JDK中,JVM已经保证对64位数据的读取和赋值也是原子性操作了。
从上面可以看出,Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized和Lock来实现。由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。
对于可见性,Java提供了volatile关键字来保证可见性。
当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。
另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。
在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
在Java里面,可以通过volatile关键字来保证一定的“有序性”(具体原理在下一节讲述)。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。
另外,Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。
- 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作。
- 锁定规则:一个`unLock`操作先行发生于后面对同一个锁额lock操作。
- volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作。
- 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C。
- 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作。
- 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。
- 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行。
- 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始。
这8条规则中,前4条规则是比较重要的,后4条规则都是显而易见的。
对于程序次序规则来说,我的理解就是一段程序代码的执行在单个线程中看起来是有序的。注意,虽然这条规则中提到“书写在前面的操作先行发生于书写在后面的操作”,这个应该是程序看起来执行的顺序是按照代码顺序执行的,因为虚拟机可能会对程序代码进行指令重排序。虽然进行重排序,但是最终执行的结果是与程序顺序执行的结果一致的,它只会对不存在数据依赖性的指令进行重排序。因此,在单个线程中,程序执行看起来是有序执行的,这一点要注意理解。事实上,这个规则是用来保证程序在单线程中执行结果的正确性,但无法保证程序在多线程中执行的正确性。
第二条规则也比较容易理解,也就是说无论在单线程中还是多线程中,同一个锁如果出于被锁定的状态,那么必须先对锁进行了释放操作,后面才能继续进行lock操作。
第三条规则是一条比较重要的规则,也是后文将要重点讲述的内容。直观地解释就是,如果一个线程先去写一个变量,然后一个线程去进行读取,那么写入操作肯定会先行发生于读操作。
第四条规则实际上就是体现happens-before原则具备传递性。
3.2.1 synchronized简单介绍
synchronized中文意思是同步,也称之为同步锁。
synchronized的作用是保证在同一时刻, 被修饰的代码块或方法只会有一个线程执行,以达到保证并发安全的效果。
synchronized是Java中解决并发问题的一种最常用的方法,也是最简单的一种方法。
在JDK1.5之前synchronized是一个重量级锁,相对于j.u.c.Lock,它会显得那么笨重,随着Javs SE 1.6对synchronized进行的各种优化后,synchronized并不会显得那么重了。
- 原子性:确保线程互斥地访问同步代码;
- 可见性:保证共享变量的修改能够及时可见,其实是通过Java内存模型中的“对一个变量unlock操作之前,必须要同步到主内存中;如果对一个变量进行lock操作,则将会清空工作内存中此变量的值,在执行引擎使用此变量前,需要重新从主内存中load操作或assign操作初始化变量值” 来保证的;
- 有序性:有效解决重排序问题,即 “一个unlock操作先行发生(happen-before)于后面对同一个锁的lock操作”;
synchronized的3种使用方式:
- 修饰实例方法:作用于当前实例加锁。
- 修饰静态方法:作用于当前类对象加锁。
- 修饰代码块:指定加锁对象,对给定对象加锁。
Synchronized修饰一个方法很简单,就是在方法的前面加synchronized,synchronized修饰方法和修饰一个代码块类似,只是作用范围不一样,修饰代码块是大括号括起来的范围,而修饰方法范围是整个函数。
方法一:
public synchronized void method() {
// todo
}
public void method() {
synchronized(this) {
// todo
}
}
synchronized关键字不能继承。 虽然可以使用synchronized来定义方法,但synchronized并不属于方法定义的一部分,因此,synchronized关键字不能被继承。如果在父类中的某个方法使用了synchronized关键字,而在子类中覆盖了这个方法,在子类中的这个方法默认情况下并不是同步的,而必须显式地在子类的这个方法中加上synchronized关键字才可以。当然,还可以在子类方法中调用父类中相应的方法,这样虽然子类中的方法不是同步的,但子类调用了父类的同步方法,因此,子类的方法也就相当于同步了。这两种方式的例子代码如下:
在子类方法中加上synchronized关键字
class Parent {
public synchronized void method() { }
}
class Child extends Parent {
public synchronized void method() { }
}
class Parent {
public synchronized void method() { }
}
class Child extends Parent {
public void method() { super.method(); }
}
- 在定义接口方法时不能使用synchronized关键字。
- 构造方法不能使用synchronized关键字,但可以使用synchronized代码块来进行同步。
1. 一个线程访问一个对象中的synchronized(this)同步代码块时,其他试图访问该对象的线程将被阻塞。
class SyncThread implements Runnable {
private static int count;
public SyncThread() {
count = 0;
}
public void run() {
synchronized(this) {
for (int i = 0; i < 5; i++) {
try {
System.out.println(Thread.currentThread().getName() + ":" + (count++));
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public int getCount() {
return count;
}
}
public class Demo00 {
public static void main(String args[]){
//调用方式一:test01
//SyncThread s1 = new SyncThread();
//SyncThread s2 = new SyncThread();
//Thread t1 = new Thread(s1);
//Thread t2 = new Thread(s2);
//调用方式二:test02
SyncThread s = new SyncThread();
Thread t1 = new Thread(s);
Thread t2 = new Thread(s);
t1.start();
t2.start();
}
}
调用方式一中,thread1 和 thread2 同时在执行。这是因为 synchronized 只锁定对象,每个对象只有一个锁(lock)与之相关联。
class Counter implements Runnable{
private int count;
public Counter() {
count = 0;
}
public void countAdd() {
synchronized(this) {
for (int i = 0; i < 5; i ++) {
try {
System.out.println(Thread.currentThread().getName() + ":" + (count++));
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//非synchronized代码块,未对count进行读写操作,所以可以不用synchronized
public void printCount() {
for (int i = 0; i < 5; i ++) {
try {
System.out.println(Thread.currentThread().getName() + " count:" + count);
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void run() {
String threadName = Thread.currentThread().getName();
if (threadName.equals("A")) {
countAdd();
} else if (threadName.equals("B")) {
printCount();
}
}
}
public class Demo00{
public static void main(String args[]){
Counter counter = new Counter();
Thread thread1 = new Thread(counter, "A");
Thread thread2 = new Thread(counter, "B");
thread1.start();
thread2.start();
}
}
/**
* 银行账户类
*/
class Account {
String name;
float amount;
public Account(String name, float amount) {
this.name = name;
this.amount = amount;
}
//存钱
public void deposit(float amt) {
amount += amt;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//取钱
public void withdraw(float amt) {
amount -= amt;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public float getBalance() {
return amount;
}
}
/**
* 账户操作类
*/
class AccountOperator implements Runnable{
private Account account;
public AccountOperator(Account account) {
this.account = account;
}
public void run() {
synchronized (account) {
account.deposit(500);
account.withdraw(500);
System.out.println(Thread.currentThread().getName() + ":" + account.getBalance());
}
}
}
public class Demo00{
//public static final Object signal = new Object(); // 线程间通信变量
//将account改为Demo00.signal也能实现线程同步
public static void main(String args[]){
Account account = new Account("zhang san", 10000.0f);
AccountOperator accountOperator = new AccountOperator(account);
final int THREAD_NUM = 5;
Thread threads[] = new Thread[THREAD_NUM];
for (int i = 0; i < THREAD_NUM; i ++) {
threads[i] = new Thread(accountOperator, "Thread" + i);
threads[i].start();
}
}
}
当有一个明确的对象作为锁时,就可以用类似下面这样的方式写程序:
public void method3(SomeObject obj)
{
//obj 锁定的对象
synchronized(obj)
{
// todo
}
}
class Test implements Runnable
{
private byte[] lock = new byte[0]; // 特殊的instance变量
public void method()
{
synchronized(lock) {
// todo 同步代码块
}
}
public void run() {
}
}
synchronized 也可修饰一个静态方法,用法如下:
public synchronized static void method() {
// todo
}
/**
* 同步线程
*/
class SyncThread implements Runnable {
private static int count;
public SyncThread() {
count = 0;
}
public synchronized static void method() {
for (int i = 0; i < 5; i ++) {
try {
System.out.println(Thread.currentThread().getName() + ":" + (count++));
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void run() {
method();
}
}
public class Demo00{
public static void main(String args[]){
SyncThread syncThread1 = new SyncThread();
SyncThread syncThread2 = new SyncThread();
Thread thread1 = new Thread(syncThread1, "SyncThread1");
Thread thread2 = new Thread(syncThread2, "SyncThread2");
thread1.start();
thread2.start();
}
}
Synchronized还可作用于一个类,用法如下:
class ClassName {
public void method() {
synchronized(ClassName.class) {
// todo
}
}
}
/**
* 同步线程
*/
class SyncThread implements Runnable {
private static int count;
public SyncThread() {
count = 0;
}
public static void method() {
synchronized(SyncThread.class) {
for (int i = 0; i < 5; i ++) {
try {
System.out.println(Thread.currentThread().getName() + ":" + (count++));
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public synchronized void run() {
method();
}
}
- 无论 synchronized 关键字加在方法上还是对象上,如果它作用的对象是非静态的,则它取得的锁是对象;如果 synchronized 作用的对象是一个静态方法或一个类,则它取得的锁是对类,该类所有的对象同一把锁。
- 每个对象只有一个锁(lock)与之相关联,谁拿到这个锁谁就可以运行它所控制的那段代码。
- 实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。
谈 synchronized 的底层实现,就不得不谈数据在 JVM 内存的存储:Java 对象头,以及 Monitor 对象监视器。
在 JVM 中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。如下图所示:
- 对齐填充:由于虚拟机要求 对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐;
- 对象头:Java 对象头一般占有2个机器码(在32位虚拟机中,1个机器码等于4字节,也就是32bit,在64位虚拟机中,1个机器码是8个字节,也就是 64bit),但是如果对象是数组类型,则需要3个机器码,因为 JVM 虚拟机可以通过 Java 对象的元数据信息确定 Java 对象的大小,但是无法从数组的元数据来确认数组的大小,所以用一块来记录数组长度。
| 长度 | 内容 | 说明 |
| --------- | ---------------------- | ---------------------------------- |
| 32/64 bit | Mark Word | 存储对象的hashCode或锁信息等。 |
| 32/64 bit | Class Metadata Address | 存储到对象类型数据的指针。 |
| 32/64 bit | Array length | 数组的长度(如果当前对象是数组)。 |
| | 25 bit | 4 bit | 1 bit是否是偏向锁 | 2 bit锁标志位 |
| ------- | ------------- | ------------ | ---------------- | ------------- |
| 无锁状态 | 对象的hashCode | 对象分代年龄 | 0 | 01 |
在线程进入同步代码块的时候,如果此同步对象没有被锁定,即它的锁标志位是01,则虚拟机首先在当前线程的栈中创建我们称之为“锁记录(Lock Record)”的空间,用于存储锁对象的Mark Word的拷贝,官方把这个拷贝称为Displaced Mark Word。整个Mark Word及其拷贝至关重要。
Lock Record是线程私有的数据结构,每一个线程都有一个可用Lock Record列表,同时还有一个全局的可用列表。每一个被锁住的对象Mark Word都会和一个Lock Record关联(对象头的MarkWord中的Lock Word指向Lock Record的起始地址),同时Lock Record中有一个Owner字段存放拥有该锁的线程的唯一标识(或者object mark word),表示该锁被这个线程占用。如下图所示为Lock Record的内部结构:
| Lock Record | 描述 |
| ----------- | ------------------------------------------------------------ |
| Owner | 初始时为NULL表示当前没有任何线程拥有该monitor record,当线程成功拥有该锁后保存线程唯一标识,当锁被释放时又设置为NULL; |
| EntryQ | 关联一个系统互斥锁(semaphore),阻塞所有试图锁住monitor record失败的线程; |
| RcThis | 表示blocked或waiting在该monitor record上的所有线程的个数; |
| Nest | 用来实现 重入锁的计数; |
| HashCode | 保存从对象头拷贝过来的HashCode值(可能还包含GC age)。 |
| Candidate | 用来避免不必要的阻塞或等待线程唤醒,因为每一次只有一个线程能够成功拥有锁,如果每次前一个释放锁的线程唤醒所有正在阻塞或等待的线程,会引起不必要的上下文切换(从阻塞到就绪然后因为竞争锁失败又被阻塞)从而导致性能严重下降。Candidate只有两种可能的值0表示没有需要唤醒的线程1表示要唤醒一个继任线程来竞争锁。 |
任何一个对象都有一个Monitor与之关联,当且一个Monitor被持有后,它将处于锁定状态。
synchronized 在 JVM 里的实现都是 基于进入和退出 Monitor 对象来实现方法同步和代码块同步,虽然具体实现细节不一样,但是都可以通过成对的 MonitorEnter 和 MonitorExit 指令来实现。
MonitorEnter 指令:插入在同步代码块的开始位置,当代码执行到该指令时,将会尝试获取该对象 Monitor 的所有权,即尝试获得该对象的锁;
MonitorExit 指令:插入在方法结束处和异常处,JVM 保证每个 MonitorEnter 必须有对应的 MonitorExit;
与一切皆对象一样,所有的Java对象是天生的Monitor,每一个Java对象都有成为Monitor的潜质,因为在Java的设计中 ,每一个Java对象自打娘胎里出来就带了一把看不见的锁,它叫做内部锁或者Monitor锁。
也就是通常说Synchronized的对象锁,MarkWord锁标识位为10,其中指针指向的是Monitor对象的起始地址。在Java虚拟机(HotSpot)中,Monitor是由ObjectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的):
ObjectMonitor() {
_header = NULL;
_count = 0; // 记录个数
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL; // 处于wait状态的线程,会被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; // 处于等待锁block状态的线程,会被加入到该列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
- 首先会进入 _EntryList 集合,当线程获取到对象的monitor后,进入 _Owner区域并把monitor中的owner变量设置为当前线程,同时monitor中的计数器count加1;
- 若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSet 集合中等待被唤醒;
- 若当前线程执行完毕,也将释放monitor(锁)并复位count的值,以便其他线程进入获取monitor(锁);
监视器Monitor有两种同步方式:互斥与协作。多线程环境下线程之间如果需要共享数据,需要解决互斥访问数据的问题,监视器可以确保监视器上的数据在同一时刻只会有一个线程在访问。
一个线程向缓冲区写数据,另一个线程从缓冲区读数据,如果读线程发现缓冲区为空就会等待,当写线程向缓冲区写入数据,就会唤醒读线程,这里读线程和写线程就是一个合作关系。JVM通过Object类的wait方法来使自己等待,在调用wait方法后,该线程会释放它持有的监视器,直到其他线程通知它才有执行的机会。一个线程调用notify方法通知在等待的线程,这个等待的线程并不会马上执行,而是要通知线程释放监视器后,它重新获取监视器才有执行的机会。如果刚好唤醒的这个线程需要的监视器被其他线程抢占,那么这个线程会继续等待。Object类中的notifyAll方法可以解决这个问题,它可以唤醒所有等待的线程,总有一个线程执行。
锁解决了数据的安全性,但是同样带来了性能的下降。`hotspot` 虚拟机的作者经过调查发现,大部分情况下,加锁的代码不仅仅不存在多线程竞争,而且总是由同一个线程多次获得。所以基于这样一个概率。
synchronized 在 JDK1.6 之后做了一些优化,为了减少获得锁和释放锁来的性能开销,引入了偏向锁、轻量级锁、自旋锁、重量级锁,锁的状态根据竞争激烈的程度从低到高不断升级。
| 锁 | 优点 | 缺点 | 适用场景 |
| -------- | ------------------------------------------------------------------------ | ------------------------------------------------ | ------------------------------------ ------------- |
| 偏向锁 | 加锁和解锁不需要额外<br>的消耗,和执行非同步<br>方法比仅存在纳秒级的<br/>差距。| 如果线程间存在锁竞争,会<br/>带来额外的锁撤销的消耗。 | 适用于只有一个线程访<br/>问同步块场景。 |
| 轻量级锁 | 竞争的线程不会阻塞,<br/>提高了程序的响应速度。 | 如果始终得不到锁竞争的线<br/>程使用自旋会消耗CPU。 | 追求响应时间。<br/>同步块执行速度非常快。 |
| 重量级锁 | 线程竞争不使用自旋,<br/>不会消耗CPU。 | 线程阻塞,响应时间缓慢。 | 追求吞吐量。<br/>同步块执行速度较长。 |
volatile 的主要作用有两点:
- 保证变量的内存可见性 。
- 禁止指令重排序。
先来看看这个比较常见的多线程访问共享变量的例子。
/**
* 变量的内存可见性例子
*
* @author star
*/
public class VolatileExample {
/**
* main 方法作为一个主线程
*/
public static void main(String[] args) {
MyThread myThread = new MyThread();
// 开启线程
myThread.start();
// 主线程执行
for (; ; ) {
if (myThread.isFlag()) {
System.out.println("主线程访问到 flag 变量");
}
}
}
}
/**
* 子线程类
*/
class MyThread extends Thread {
private boolean flag = false;
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 修改变量值
flag = true;
System.out.println("flag = " + flag);
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
JMM(Java Memory Model):Java 内存模型,是 Java 虚拟机规范中所定义的一种内存模型,Java 内存模型是标准化的,屏蔽掉了底层不同计算机的区别。也就是说,JMM 是 JVM 中定义的一种并发编程的底层模型机制。
JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存中存储了该线程以读/写共享变量的副本。
- 所有的共享变量都存储于主内存。这里所说的变量指的是实例变量和类变量,不包含局部变量,因为局部变量是线程私有的,因此不存在竞争问题。
- 每一个线程还存在自己的工作内存,线程的工作内存,保留了被线程使用的变量的工作副本。
- 线程对变量的所有的操作(读,取)都必须在工作内存中完成,而不能直接读写主内存中的变量。
- 不同线程之间也不能直接访问对方工作内存中的变量,线程间变量的值的传递需要通过主内存中转来完成。
内存可见性是指当一个线程修改了某个变量的值,其它线程总是能知道这个变量变化。也就是说,如果线程 A 修改了共享变量 V 的值,那么线程 B 在使用 V 的值时,能立即读到 V 的最新值。
我们如何保证多线程下共享变量的可见性呢?也就是当一个线程修改了某个值后,对其他线程是可见的。
使用 synchronizer 进行加锁。
/**
* main 方法作为一个主线程
*/
public static void main(String[] args) {
MyThread myThread = new MyThread();
// 开启线程
myThread.start();
// 主线程执行
for (; ; ) {
synchronized (myThread) {
if (myThread.isFlag()) {
System.out.println("主线程访问到 flag 变量");
}
}
}
}
这里除了 synchronizer 外,其它锁也能保证变量的内存可见性。
使用 volatile 关键字修饰共享变量。
/**
* 子线程类
*/
class MyThread extends Thread {
private volatile boolean flag = false;
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 修改变量值
flag = true;
System.out.println("flag = " + flag);
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
volatile 保证了不同线程对共享变量操作的可见性,也就是说一个线程修改了 volatile 修饰的变量,当修改后的变量写回主内存时,其他线程能立即看到最新值。
总线嗅探机制
在现代计算机中,CPU 的速度是极高的,如果 CPU 需要存取数据时都直接与内存打交道,在存取过程中,CPU 将一直空闲,这是一种极大的浪费,所以,为了提高处理速度,CPU 不直接和内存进行通信,而是在 CPU 与内存之间加入很多寄存器,多级缓存,它们比内存的存取速度高得多,这样就解决了 CPU 运算速度和内存读取速度不一致问题。
由于 CPU 与内存之间加入了缓存,在进行数据操作时,先将数据从内存拷贝到缓存中,CPU 直接操作的是缓存中的数据。但在多处理器下,将可能导致各自的缓存数据不一致(这也是可见性问题的由来),为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,而嗅探是实现缓存一致性的常见机制。
上面的例子中,我们看到,使用 volatile 和 synchronized 锁都可以保证共享变量的可见性。相比 synchronized 而言,volatile 可以看作是一个轻量级锁,所以使用 volatile 的成本更低,因为它不会引起线程上下文的切换和调度。但 volatile 无法像 synchronized 一样保证操作的原子性。
所谓的原子性是指在一次操作或者多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,要么所有的操作都不执行。
在多线程环境下,volatile 关键字可以保证共享数据的可见性,但是并不能保证对数据操作的原子性。也就是说,多线程环境下,使用 volatile 修饰的变量是线程不安全的。
要解决这个问题,我们可以使用锁机制,或者使用原子类(如 AtomicInteger)。
这里特别说一下,对任意单个使用 volatile 修饰的变量的读 / 写是具有原子性,但类似于 flag = !flag 这种复合操作不具有原子性。简单地说就是,单纯的赋值操作是原子性的。
什么是重排序?
为了提高性能,在遵守 as-if-serial 语义(即不管怎么重排序,单线程下程序的执行结果不能被改变。编译器,runtime 和处理器都必须遵守。)的情况下,编译器和处理器常常会对指令做重排序。
- 编译器优化重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
- 指令级并行重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
- 内存系统重排序。由于处理器使用缓存和读 / 写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
int a = 0;
// 线程 A
a = 1; // 1
flag = true; // 2
// 线程 B
if (flag) { // 3
int i = a; // 4
}
| 是否重排序 | | 第二次操作 | |
| ------------- | ---------- | ------------ | ----------- |
| 第一次操作 | 普通读/写 | volatile读 | volatile写 |
| 普通读/写 | YES | YES | NO |
| volatile读 | NO | NO | NO |
| volatile写 | YES | NO | NO |
内存屏障是一组处理器指令,它的作用是禁止指令重排序和解决内存可见性的问题。
JMM把内存屏障指令分为下列四类:
| 屏障类型 | 指令示例 | 说明 |
| -------------- ------- | --------------------------- | ---------------------------------------------------------------------------------------------------- |
| LoadLoad Barriers | Load1;LoadLoad;Load2 | 确保Load1数据的装载先于Load2及所有后续装载指令的装载 |
| StoreStore Barriers | Store1;StoreStore;Stored | 确保Store1数据对其他处理器可见(刷新到内存)先于Store2<br>及所有后续存储指令的存储 |
| LoadStore Barriers | Load1;LoadStore;Store2 | 确保Load1数据装载咸鱼Store2及所有后续的存储指令刷新到内存 |
| StoreLoad Barriers | Store1;Store Load;Load2 | 确保Store1数据对其他处理器变得可见(指刷新到内存)先于Load2<br>及所有后续装载指令的装载。StoreLoad Barriers会使该屏障之前的<br>所有内存访问指令(存储和装载指令)完成之后,才执行该屏障之后<br>的内存访问指令 |
本质上来说:
volatile实际上是通过内存屏障来防止指令重排序以及禁止CPU高速缓存来解决可见性问题。
而#Lock指令,它本意上是禁止高速缓存解决可见性问题,但实际上在 这里,它表示的是一种内存屏障的功能。也就是说针对当前的硬件环境,`JMM`层面采用Lock指令作为内存屏障来解决可见性问题。
3.5.1 概述
final关键字代表最终、不可改变的。
1. 可以用来修饰一个类。
2. 可以用来修饰一个方法。
3. 还可以用来修饰一个局部变量。
4. 还可以用来修饰一个成员变量。
对于final域,编译器和处理器要遵守两个重排序规则。
- 在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一 个引用变量,这两个操作之间不能重排序。
- 初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操 作之间不能重排序。
当final关键字用来修饰一个类的时候,格式:
public final class 类名称 { // ... }
注意:一个类如果是final的,那么其中所有的成员方法都无法进行覆盖重写(因为没儿子。)
1. 含义:
当final关键字用来修饰一个方法的时候,这个方法就是最终方法,也就是不能被覆盖重写。
修饰符 final 返回值类型 方法名称(参数列表) {
// 方法体
}
public abstract class Fu {
public final void method() {
System.out.println("父类方法执行!");
}
public abstract /*final*/ void methodAbs() ; //有final会报红
}
public class Zi extends Fu {
@Override
public void methodAbs() {
}
// 错误写法!不能覆盖重写父类当中final的方法
// @Override
// public void method() {
// System.out.println("子类覆盖重写父类的方法!");
// }
}
// 正确写法!只要保证有唯一一次赋值即可
final int num3;
num3 = 30;
对于引用类型来说,不可变说的是变量当中的地址值不可改变。
public class Student {
private String name;
public Student() {
}
public Student(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
1. 若无final修饰
public class TestDemo {
public static void main(String[] args) {
final Student stu1 = new Student("张三");
System.out.println(stu1); // 打印地址
System.out.println(stu1.getName()); // 张三
stu1 = new Student("李四");
System.out.println(stu1);
System.out.println(stu1.getName()); // 李四
}
}
张三
com.gupaoedu.Student@34033bd0
李四
public class TestDemo {
public static void main(String[] args) {
final Student stu1 = new Student("张三");
System.out.println(stu1);
System.out.println(stu1.getName());
stu1.setName("李四");
System.out.println(stu1);
System.out.println(stu1.getName()); // 李四
}
}
张三
com.gupaoedu.Student@7006c658
李四
对于成员变量来说,如果使用final关键字修饰,那么这个变量也照样是不可变。
1. 由于成员变量具有默认值,所以用了final之后必须手动赋值,不会再给默认值。
如果选择在构造方法中赋值,则要把setname()函数取消掉。
3.6.1 happens-before 定义
happens-before的概念最初由Leslie Lamport在其一篇影响深远的论文(《Time,Clocks and the Ordering of Events in a Distributed System》)中提出。JSR-133使用happens-before的概念来指定两个操作之间的执行顺序。由于这两个操作可以在一个线程之内,也可以是在不同线程之间。因此,JMM可以通过happens-before关系向程序员提供跨线程的内存可见性保证(如果A线程的写操作a与B线程的读操作b之间存在happens-before关系,尽管a操作和b操作在不同的线程中执行,但JMM向程序员保证a操作将对b操作可见)。具体的定义为:
1. 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
2. 两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么这种重排序并不非法(也就是说,JMM允许这种重排序)。
上面的 1 是JMM对程序员的承诺。从程序员的角度来说,可以这样理解happens-before关系:如果A happens-before B,那么Java内存模型将向程序员保证——A操作的结果将对B可见,且A的执行顺序排在B之前。注意,这只是Java内存模型向程序员做出的保证!
2 是JMM对编译器和处理器重排序的约束原则。正如前面所言,JMM其实是在遵循一个基本原则:只要不改变程序的执行结果(指的是单线程程序和正确同步的多线程程序),编译器和处理器怎么优化都行。JMM这么做的原因是:程序员对于这两个操作是否真的被重排序并不关心,程序员关心的是程序执行时的语义不能被改变(即执行结果不能被改变)。因此,happens-before关系本质上和as-if-serial语义是一回事。
具体的一共有六项规则:
- 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
- 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
- volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
- start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。
- join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。
- 程序中断规则:对线程interrupted()方法的调用先行于被中断线程的代码检测到中断时间的发生。
- 对象finalize规则:一个对象的初始化完成(构造函数执行结束)先行于发生它的finalize()方法的开始。
3.7.1 什么是原子类?用处是什么?如何使用?
在java.util.concurrent.atomic包下,有一系列“Atomic”开头的类,统称为原子类。
private static final ExecutorService exec = Executors.newCachedThreadPool();
private static int i = 0;
public static void main(String[] args) throws InterruptedException {
Runnable r = () -> {
for (int k = 0; k < 1000; k++) {
i++;
sleep(1); // 交出CPU控制权,增大竞争
}
};
for (int j = 0; j < 10; j++) {
Future<?> f = exec.submit(r);
}
exec.shutdown();
exec.awaitTermination(60, TimeUnit.SECONDS); // 等待所有任务执行完毕
System.out.println(i);
}
这是由于i++这一步在字节码中分为了四次操作:getstatic将静态变量i的值入栈,iconst_1将1入栈,iadd将栈顶两个数出栈、并将它们的和入栈,putstatic将栈顶的值赋给变量i。
private static final ExecutorService exec = Executors.newCachedThreadPool();
private static int i = 0;
private static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Runnable r = () -> {
for (int k = 0; k < 1000; k++) {
synchronized (lock) { // 使用同步代码块
i++;
}
sleep(1);
}
};
for (int j = 0; j < 10; j++) {
Future<?> f = exec.submit(r);
}
exec.shutdown();
exec.awaitTermination(60, TimeUnit.SECONDS);
System.out.println(i);
}
private static final ExecutorService exec = Executors.newCachedThreadPool();
//private static int i = 0;
private static AtomicInteger i = new AtomicInteger(0); // 使用AtomicInteger类保证原子性
public static void main(String[] args) throws InterruptedException {
Runnable r = () -> {
for (int k = 0; k < 1000; k++) {
i.getAndIncrement(); // 使用AtomicInteger类保证原子性
sleep(1);
}
};
for (int j = 0; j < 10; j++) {
Future<?> f = exec.submit(r);
}
exec.shutdown();
exec.awaitTermination(60, TimeUnit.SECONDS);
System.out.println(i);
}
3.8.1 什么是 ThreadLocal?
ThreadLocal 是 Java 里一种特殊变量,它是一个线程级别变量,每个线程都有一个 ThreadLocal 就是每个线程都拥有了自己独立的一个变量,竞态条件被彻底消除了,在并发模式下是绝对安全的变量。
可以通过 ThreadLocal<T> value = new ThreadLocal<T>(); 来使用。
会自动在每一个线程上创建一个 T 的副本,副本之间彼此独立,互不影响,可以用 ThreadLocal 存储一些参数,以便在线程中多个方法中使用,用以代替方法传参的做法。
ThreadLocal 在没有外部强引用时,发生 GC 时会被回收,那么 ThreadLocalMap 中保存的 key 值就变成了 null,而 Entry 又被 threadLocalMap 对象引用,threadLocalMap 对象又被 Thread 对象所引用,那么当 Thread 一直不终结的话,value 对象就会一直存在于内存中,也就导致了内存泄漏,直至 Thread 被销毁后,才会被回收。
在使用完 ThreadLocal 变量后,需要我们手动 remove 掉,防止 ThreadLocalMap 中 Entry 一直保持对 value 的强引用,导致 value 不能被回收。
ThreadLocal 的特性也导致了应用场景比较广泛,主要的应用场景如下:
- 线程间数据隔离,各线程的 ThreadLocal 互不影响。
- 方便同一个线程使用某一对象,避免不必要的参数传递。
- 全链路追踪中的 `traceId` 或者流程引擎中上下文的传递一般采用 ThreadLocal。
- Spring 事务管理器采用了 ThreadLocal。
- Spring MVC 的 RequestContextHolder 的实现使用了 ThreadLocal。
四、如何安全发布对象
4.1 发布与逃逸
发布的意思是是一个对象能够被当前范围之外的代码所使用。
一种错误的发布,当一个对象还没有构造完成时,就使它被其他线程所见。
- 在静态初始化函数中初始化一个对象引用。
- 将对象的引用保存到volatile类型的域或者 AtomicReference 对象中(利用volatile happen-before规则)。
- 将对象的引用保存到某个正确构造对象的final类型域中(初始化安全性)。
- 将对象的引用保存到一个由锁保护的域中(读写都上锁)。
public class StaticDemo {
private StaticDemo(){}
private static StaticDemo instance = new StaticDemo();
private static StaticDemo getInstance(){
return instance;//当静态构造方法初始化加载的时候
//一定是安全发布的,但是不保证后续操作这个instance的安全
}
}
public class FinalDemo {
private final Map<String,String> states;
public FinalDemo(){
states = new HashMap<>();
states.put("yojofly","yojofly");
}
}
- 第一种单利模式,此时当多线程访问时候,可能存在instance == null都判断成功,返回多个实例的情况。
public class VolatileDemo {
public VolatileDemo(){}
private static VolatileDemo instance;
public static VolatileDemo getInstance(){
if (instance == null){
instance = new VolatileDemo();
return instance;
}
return instance;
}
}
public class VolatileDemo {
public VolatileDemo(){}
private static VolatileDemo instance;
public static VolatileDemo getInstance(){
if (instance == null){
synchronized (VolatileDemo.class){
if (instance == null){
instance = new VolatileDemo();
return instance;
}
}
}
return instance;
}
}
instance = new VolatileDemo();
例如:指令 m,n,p,那么m,n,p指令的执行顺序一定是顺序执行的吗,此时依然存在指令重排序问题。
public class VolatileDemo {
public VolatileDemo(){}
private volatile static VolatileDemo instance; //给发布对象加上volatile关键字,防止指令重排序
public static VolatileDemo getInstance(){
if (instance == null){
synchronized (VolatileDemo.class){
if (instance == null){
instance = new VolatileDemo();
return instance;
}
}
}
return instance;
}
}
五、J.U.C核心指AQS
5.1 重入锁ReentrantLock
重入锁的特点:
- 实现重进入功能
1. 线程再次获取锁。锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是则再次获取成功。
2. 锁的最终释放。线程重复n次获取了锁,随后在第n次释放该锁后,其他线程能够获取到该锁。锁的最终释放要求锁对获取进行计数自增,计数表示当前锁被重复获取的次数,而锁被释放时,计数自减,当计数器等于0时表示锁已经成功释放了。
- 公平锁:
对先发起请求的线程即等待最久的线程优先满足,获取锁是顺序的,符合FIFO原则,不会产生线程饥饿;
获取锁调用tryAcquire方法,与非公平锁不一样的地方在于判断条件多了hasQueuedPredecessors()方法,这个方法判断队列中是否有其他节点,如果队列中还有其他节点,但是head后面还没关联节点 / 或者队列中head节点的后继节点关联的线程不是当前线程,如果是返回true,则表示有线程比当前线程更早地请求获取锁,因为要等待前驱节点获取并释放锁后才嫩继续获取到锁。
- 非公平锁(默认的):
获取是使用nonfairTryAcquire方法,只要CAS设置同步状态成功,则当前线程获取了锁。
非公平锁比公平锁效率更高,因为公平锁为了保证公平性会去切换线程导致上下文切换,存在额外的开销,所以非公平锁性能更好(所以作为默认的实现方式),保证了更大的吞吐量,但是可能会产生线程饥饿。
什么是AQS?
ReentrantLock、ReentrantReadWriteLock 底层都是基于 AQS 来实现的
AQS的全称是 AbstractQueuedSynchronizer,是抽象队列同步器,其实他就是一个用来构建锁和同步器的框架,内部实现的关键是:先进先出的队列、state状态,在LOCK包中的相关锁(常用的有ReentrantLock、 ReadWriteLock)都是基于AQS来构建。
AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用。
AQS使用一个voliate int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。`AQS`使用`CAS`对该同步状态进行原子操作实现对其值的修改。
AQS定义了两种资源获取方式:独占(只有一个线程能访问执行)和共享(多个线程可同时访问执行)
CountDownLatch允许一个或者多个线程去等待其他线程完成操作。
CountDownLatch接收一个int型参数,表示要等待的工作线程的个数。
当然也不一定是多线程,在单线程中可以用这个`int`型参数表示多个操作步骤。
CountDownLatch通过内部类Sync来实现同步语义。
Sync继承AQS,源码如下:
private static final class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 4982264981922014374L;
// 设置同步状态的值
Sync(int count) {
setState(count);
}
// 获取同步状态的值
int getCount() {
return getState();
}
// 尝试获取同步状态,只有同步状态的值为0的时候才成功
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
// 尝试释放同步状态,每次释放通过CAS将同步状态的值减1
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
int c = getState();
// 如果同步状态的值已经是0了,不要再释放同步状态了,也不要减1了
if (c == 0)
return false;
// 减1
int nextc = c - 1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
}
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
// 如果被中断,抛出异常
if (Thread.interrupted())
throw new InterruptedException();
// 尝试获取同步状态
if (tryAcquireShared(arg) < 0)
// 获取同步状态失败,自旋
doAcquireSharedInterruptibly(arg);
}
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
如果获取失败,则会调用AQS的doAcquireSharedInterruptibly(int arg)函数自旋,尝试挂起当前线程:
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
// 将当前线程加入同步队列的尾部
final Node node = addWaiter(Node.SHARED);
try {
// 自旋
for (;;) {
// 获取当前节点的前驱节点
final Node p = node.predecessor();
// 如果前驱节点是头结点,则尝试获取同步状态
if (p == head) {
// 当前节点尝试获取同步状态
int r = tryAcquireShared(arg);
if (r >= 0) {
// 如果获取成功,则设置当前节点为头结点
setHeadAndPropagate(node, r);
p.next = null; // help GC
return;
}
}
// 如果当前节点的前驱不是头结点,尝试挂起当前线程
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} catch (Throwable t) {
cancelAcquire(node);
throw t;
}
}
countDown()源码如下:
public void countDown() {
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
// 尝试释放同步状态
if (tryReleaseShared(arg)) {
// 如果成功,进入自旋,尝试唤醒同步队列中头结点的后继节点
doReleaseShared();
return true;
}
return false;
}
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
int c = getState();
if (c == 0)
return false;
// 同步状态值减1
int nextc = c - 1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
private void doReleaseShared() {
for (;;) {
// 获取头结点
Node h = head;
if (h != null && h != tail) {
// 获取头结点的状态
int ws = h.waitStatus;
// 如果是SIGNAL,尝试唤醒后继节点
if (ws == Node.SIGNAL) {
if (!h.compareAndSetWaitStatus(Node.SIGNAL, 0))
continue; // loop to recheck cases
// 唤醒头结点的后继节点
unparkSuccessor(h);
}
else if (ws == 0 &&
!h.compareAndSetWaitStatus(0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
此时这个后继节点被唤醒,那么又是如何实现唤醒所有调用await()等待的线程呢?
回到线程被挂起的地方,也就是doAcquireSharedInterruptibly(int arg)方法中:
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
// 将当前线程加入同步队列的尾部
final Node node = addWaiter(Node.SHARED);
try {
// 自旋
for (;;) {
// 获取当前节点的前驱节点
final Node p = node.predecessor();
// 如果前驱节点是头结点,则尝试获取同步状态
if (p == head) {
// 当前节点尝试获取同步状态
int r = tryAcquireShared(arg);
if (r >= 0) {
// 如果获取成功,则设置当前节点为头结点
setHeadAndPropagate(node, r);
p.next = null; // help GC
return;
}
}
// 如果当前节点的前驱不是头结点,尝试挂起当前线程
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} catch (Throwable t) {
cancelAcquire(node);
throw t;
}
}
当头结点的后继节点被唤醒后,线程将从挂起的地方醒来,继续执行,因为没有return,所以进入下一次循环。
此时,获取同步状态成功,执行setHeadAndPropagate(node, r)。
// 如果执行这个函数,那么propagate一定等于1
private void setHeadAndPropagate(Node node, int propagate) {
// 获取头结点
Node h = head;
// 因为当前节点被唤醒,设置当前节点为头结点
setHead(node);
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
// 获取当前节点的下一个节点
Node s = node.next;
// 如果下一个节点为null或者节点为shared节点
if (s == null || s.isShared())
doReleaseShared();
}
}
如果当前节点的下一个节点是shared节点,调用doReleaseShared(),源码:
private void doReleaseShared() {
// 自旋
for (;;) {
// 获取头结点,也就是当前节点
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!h.compareAndSetWaitStatus(Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
else if (ws == 0 &&
!h.compareAndSetWaitStatus(0, Node.PROPAGATE))
continue; // loop on failed CAS
}
// 如果head没有改变,则调用break退出循环
if (h == head)
break;
}
}
其次,注意if (h != null && h != tail) 这个判断,保证队列至少要有两个节点(包括头结点在内)。
- 如果状态为SIGNAL,说明h的后继节点是需要被通知的。通过对CAS操作结果取反,将compareAndSetWaitStatus(h, Node.SIGNAL, 0)和unparkSuccessor(h)绑定在了一起。说明了只要head成功的从SIGNAL修改为0,那么head的后继节点对应的线程将会被唤醒。
- 如果状态为0,说明h的后继节点对应的线程已经被唤醒或即将被唤醒,并且这个中间状态即将消失,要么由于acquire thread获取锁失败再次设置head为SIGNAL并再次阻塞,要么由于acquire thread获取锁成功而将自己(head后继)设置为新head并且只要head后继不是队尾,那么新head肯定为SIGNAL。所以设置这种中间状态的head的status为PROPAGATE,让其status又变成负数,这样可能被被唤醒线程检测到。
- 如果状态为PROPAGATE,直接判断head是否变化。
5.5.1 什么是Semaphore
Semaphore(信号量),是JUC包下的一个工具类,我们可以通过其限制执行的线程数量,达到限流的效果。
当一个线程执行时先通过其方法进行获取许可操作,获取到许可的线程继续执行业务逻辑,当线程执行完成后进行释放许可操作,未获取达到许可的线程进行等待或者直接结束。
Semaphore的使用也是比较简单的,我们创建一个Runnable的子类,如下:
private static class MyRunnable implements Runnable {
// 成员属性 Semaphore对象
private final Semaphore semaphore;
public MyRunnable(Semaphore semaphore) {
this.semaphore = semaphore;
}
public void run() {
String threadName = Thread.currentThread().getName();
// 获取许可
boolean acquire = semaphore.tryAcquire();
// 未获取到许可 结束
if (!acquire) {
System.out.println("线程【" + threadName + "】未获取到许可,结束");
return;
}
// 获取到许可
try {
System.out.println("线程【" + threadName + "】获取到许可");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放许可
semaphore.release();
System.out.println("线程【" + threadName + "】释放许可");
}
}
}
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(2);
for (int i = 0; i <= 10; i ++) {
MyRunnable runnable = new MyRunnable(semaphore);
Thread thread = new Thread(runnable, "Thread-" + i);
thread.start();
}
}
在JUC包中为我们提供了一个同步工具类能够很好的模拟这类场景,它就是CyclicBarrier类。利用CyclicBarrier类可以实现一组线程相互等待,当所有线程都到达某个屏障点后再进行后续的操作。
CyclicBarrier字面意思是“可重复使用的栅栏”,CyclicBarrier 相比 CountDownLatch 来说,要简单很多,其源码没有什么高深的地方,它是 ReentrantLock 和 Condition 的组合使用。
首先,CyclicBarrier 的源码实现和 CountDownLatch 大同小异,CountDownLatch 基于 AQS 的共享模式的使用,而 CyclicBarrier 基于 Condition 来实现的。因为 CyclicBarrier 的源码相对来说简单许多,读者只要熟悉了前面关于 Condition 的分析,那么这里的源码是毫无压力的,就是几个特殊概念罢了。
在CyclicBarrier类的内部有一个计数器,每个线程在到达屏障点的时候都会调用await方法将自己阻塞,此时计数器会减1,当计数器减为0的时候所有因调用await方法而被阻塞的线程将被唤醒。这就是实现一组线程相互等待的原理。
六、线程池
6.1 线程池的基本认识
什么是线程池?
提前创建好若干个线程放在一个容器中。如果有任务需要处理,则将任务直接分配给线程池中的线程来执行,任务处理完以后这个线程不会被销毁, 而是等待后续分配任务。
线程池的优点:
- 避免线程因为不限制创建数量导致的资源耗尽风险。
- 任务队列缓冲任务,支持忙线不均的作用。
- 节省了大量频繁创建/销毁线程的时间成本。
Java通过Executors提供四种线程池,分别为:
- newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
- newFixedThreadPool创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
- newScheduledThreadPool创建一个定长线程池,支持定时和周期性任务执行。
- newSingleThreadExecutor创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO,LIFO,优先级)执行。
Java线程池主要用于管理线程组及其运行状态,以便Java虚拟机更好的利用CPU资源。Java线程池的工作原理为:JVM先根据用户给定的参数创建一定数量的可运行的线程任务,并将其放入队列中,在线程创建后启动这些任务,如果正在运行的线程数量超过了最大线程数量(用户设置的线程池大小),则超出数量的线程排队等候,在有任务执行完毕后,线程池调度器会发现有可用的线程,进而再次从队列中取出任务并执行。
线程池的主要作用是线程复用、线程资源管理、控制操作系统的最大[并发](https://so.csdn.net/so/search?q=并发&spm=1001.2101.3001.7020)数,以保证系统高效(通过线程资源复用实现)且安全(通过控制最大线程并发数实现)地运行。
在Java中,每个Thread类都有一个start方法。在程序调用start方法启动线程时,Java虚拟机会调用该类的run方法。在Thread类的run方法中其实调用了Runnable对象的run方法,吟慈可以继承Thread类,在start方法中不断循环调用传递进来的Runnable对象,程序就会不断执行run方法中的代码。可以将再循环方法中不断获取的Runnable对象放在Queue中,当线程在获取下一个Runnable对象之前是阻塞的,这样既能有效控制正在执行的线程个数,也能保证系统中正在等待执行的其他线程有序执行。
Java线程池主要由以下4个核心组件组成
- 线程池管理器:用户创建并管理线程池。
- 工作线程:线程池中执行具体任务的线程。
- 任务接口:用于定义工作线程的调度和执行策略,只有线程实现了该接口,线程中的任务才能够被线程池调度。
- 任务队列:存放待处理的任务,新的任务将会不断被加入队列中,执行完成的任务将被从队列中移除。
ThreadPoolExecutor构造函数的具体参数如下:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
...
}
- maximumPoolSize:线程池中最大线程的数量。
- keepAliveTime:当前线程数量超过corePoolSize时,空闲线程的存活时间。
- unit:keepAliveTime的时间单位。
- workQueue:任务队列,被提交但尚未被执行的任务存放的地方。
- threadFactory:线程工厂,用于创建线程,可使用默认的线程工厂或自定义线程工厂。
- handler:由于任务过多或者其他原因导致线程池无法处理时的任务拒绝策略。
Java线程池的工作流程为:线程池刚被创建时,只是向系统申请一个用于执行线程队列和管理线程池的线程资源。在调用execute()添加一个任务时,线程池会按照以下流程执行任务。
- 如果正在运行的线程数量少于corePoolSize(用户定义的核心线程数),线程池就会立刻创建线程并执行该线程任务。
- 如果正在运行的线程数量大于等于corePoolSize,该任务就将被放入阻塞队列中。
- 在阻塞队列已满且正在运行的线程数量少于maximumPoolSize时,线程池会创建非核心线程立刻执行该线程任务。
- 在阻塞队列已满且正在运行的线程数量大于等于maximumPoolSize时,线程池将拒绝执行该线程任务并抛出RejectException异常。
- 在线程任务执行完毕后,该任务将被从线程池队列中移除,线程池将从队列中取下一个线程任务继续执行。
- 在非核心线程处于空闲状态的时间超过keepAliveTime时间时,正在运行的线程数量超过corePoolSize,该非核心线程将会被认定为空闲线程并停止。因此,在线程池中所有线程任务都执行完毕后,线程池会收缩到corePoolSize大小。
线程池的拒绝策略
若线程池中的核心线程数被用完且阻塞队列已满,则此时线程池的线程资源已耗尽,线程池将没有足够的线程资源执行新的任务。为了保证操作系统的安全,线程池将通过拒绝策略处理新添加的线程任务。jdk内置的拒绝策略有AbortPolicy、CallerRunsPolicy、DiscardOldestPolicy、DiscardPolicy这4种,默认的拒绝策略在ThreadPoolExecutor中作为内部类提供。
1、AbortPolicy 直接抛出异常,组织线程正常运行。
2、CallerRunsPolicy 如果被丢弃的线程任务未关闭,则执行该线程任务。
3、DiscardOldestPolicy 移除线程队列中最早的一个线程任务,并尝试提交当前任务。
4、DiscardPolicy 丢弃当前的线程任务而不做任何处理。