多线程-初阶
本节目标
还是回到我们之前的银行的例子中。之前我们主要描述的是个人业务,即一个人完全处理自己的
业务。我们进一步设想如下场景:
一家公司要去银行办理业务,既要进行财务转账,又要进行福利发放,还得进行缴社保。
如果只有张三一个会计就会忙不过来,耗费的时间特别长。为了让业务更快的办理好,张三又找
来两位同事李四、王五一起来帮助他,三个人分别负责一个事情,分别申请一个号码进行排队,
自此就有了三个执行流共同完成任务,但本质上他们都是为了办理一家公司的业务。
此时,我们就把这种情况称为多线程,将一个大任务分解成不同小任务,交给不同执行流就分别
排队执行。其中李四、王五都是张三叫来的,所以张三一般被称为主线程(Main Thread)。
首先, “并发编程” 成为 “刚需”.
其次, 进程池虽然能解决上述问题,提高效率,同时也有问题.池子里的闲置进程,不使用的时候也在消耗系统资源.消耗的系统资源太多了。虽然多进程也能实现 并发编程, 但是线程比进程更轻量.
最后, 线程虽然比进程轻量, 但是人们还不满足, 于是又有了 “线程池”(ThreadPool) 和 “协程”(Coroutine)。关于线程池我们后面再介绍. 关于协程的话题我们此处暂时不做过多讨论.
线程是操作系统中的概念. 操作系统内核实现了线程这样的机制, 并且对用户层提供了一些 API 供用户使用(例如 Linux 的 pthread 库).
Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进行了进一步的抽象和封装.
感受多线程程序和普通程序的区别:
import java.util.Random;
public class ThreadDemo {
private static class MyThread extends Thread {
@Override
public void run() {
Random random = new Random();
while (true) {
// 打印线程名称
System.out.println(Thread.currentThread().getName());
try {
// 随机停止运行 0-9 秒
Thread.sleep(random.nextInt(10));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
t1.start();
t2.start();
t3.start();
Random random = new Random();
while (true) {
// 打印线程名称
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(random.nextInt(10));
} catch (InterruptedException e) {
// 随机停止运行 0-9 秒
e.printStackTrace();
}
}
}
}
Thread-0
Thread-0
Thread-2
Thread-1
Thread-2
Thread-1
Thread-0
Thread-2
main
main
Thread-2
Thread-1
Thread-0
Thread-1
main
Thread-2
Thread-2
......
方法1 继承 Thread 类
class MyThread extends Thread {
@Override
public void run() {
System.out.println("这里是线程运行的代码");
}
}
MyThread t = new MyThread();
t.start(); // 线程开始运行
方法2 实现 Runnable 接口
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("这里是线程运行的代码");
}
}
Thread t = new Thread(new MyRunnable());
t.start(); // 线程开始运行
对比上面两种方法:
MyRunnable
的引用. 需要使用 Thread.currentThread()
其他变形
// 使用匿名类创建 Thread 子类对象
Thread t1 = new Thread() {
@Override
public void run() {
System.out.println("使用匿名类创建 Thread 子类对象");
}
};
// 使用匿名类创建 Runnable 子类对象
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("使用匿名类创建 Runnable 子类对象");
}
});
// 使用 lambda 表达式创建 Runnable 子类对象
Thread t3 = new Thread(() -> {
System.out.println("使用匿名类创建 Thread 子类对象");
});
举个例子:
可以观察多线程在一些场合下是可以提高程序的整体运行效率的。
System.nanoTime()
可以记录当前系统的 纳秒 级时间戳.serial
串行的完成一系列运算. concurrency
使用两个线程并行的完成同样的运算.二者区别:
(1)System.nanoTime()的精确度更高一些,如今的硬件设备性能越来越好,如果要更精密计算执行某行代码或者某块代码所消耗的时间,该方法会测量得更精确。开发者可以根据需要的精确度来选择用哪一个方法。
(2)单独获取System.nanoTime()没有什么意义,因为该值是随机的,无法表示当前的时间。如果要记录当前时间点,用System.currentTimeMills()。
(3)System.currentTimeMills()得到的值能够和Date类方便地转换(在JDBC数据库中,使用java.mysql.timestamp进行转换为Date类型),jdk提供了多个接口来实现;但是System.nanoTime()则不行。
(4) System.currentTimeMills()的值是基于系统时间的,可以人为地进行修改;而System.nanoTime()则不能,所以如文章开头笔者碰到的问题一样,如果需要根据时间差来过滤某些频繁的操作,用System.nanoTime()会比较合适。
public class ThreadAdvantage {
// 多线程并不一定就能提高速度,可以观察,count 不同,实际的运行效果也是不同的
private static final long count = 10_0000_0000;
public static void main(String[] args) throws InterruptedException {
// 使用并发方式
concurrency();
// 使用串行方式
serial();
}
private static void concurrency() throws InterruptedException {
long begin = System.nanoTime();
// 利用一个线程计算 a 的值
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
int a = 0;
for (long i = 0; i < count; i++) {
a--;
}
}
});
thread.start();
// 主线程内计算 b 的值
int b = 0;
for (long i = 0; i < count; i++) {
b--;
}
// 等待 thread 线程运行结束
thread.join();
// 统计耗时
long end = System.nanoTime();
double ms = (end - begin) * 1.0 / 1000 / 1000;
System.out.printf("并发: %f 毫秒%n", ms);
}
private static void serial() {
// 全部在主线程内计算 a、b 的值
long begin = System.nanoTime();
int a = 0;
for (long i = 0; i < count; i++) {
a--;
}
int b = 0;
for (long i = 0; i < count; i++) {
b--;
}
long end = System.nanoTime();
double ms = (end - begin) * 1.0 / 1000 / 1000;
System.out.printf("串行: %f 毫秒%n", ms);
}
}
并发: 399.651856 毫秒
串行: 720.616911 毫秒
Thread 类是 JVM 用来管理线程的一个类,换句话说,每个线程都有一个唯一的 Thread 对象与之关联。
用我们上面的例子来看,每个执行流,也需要有一个对象来描述,类似下图所示,而 Thread 类的对象就是用来描述一个线程执行流的,JVM 会将这些 Thread 对象组织起来,用于线程调度,线程管理。
Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("这是我的名字");
Thread t4 = new Thread(new MyRunnable(), "这是我的名字");
public class ThreadDemo {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
System.out.println(Thread.currentThread().getName() + ": 我还
活着");
Thread.sleep(1 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + ": 我即将死去");
});
System.out.println(Thread.currentThread().getName()
+ ": ID: " + thread.getId());
System.out.println(Thread.currentThread().getName()
+ ": 名称: " + thread.getName());
System.out.println(Thread.currentThread().getName()
+ ": 状态: " + thread.getState());
System.out.println(Thread.currentThread().getName()
+ ": 优先级: " + thread.getPriority());
+ System.out.println(Thread.currentThread().getName()
+ ": 后台线程: " + thread.isDaemon());
System.out.println(Thread.currentThread().getName()
+ ": 活着: " + thread.isAlive());
System.out.println(Thread.currentThread().getName()
+ ": 被中断: " + thread.isInterrupted());
thread.start();
while (thread.isAlive()) {}
System.out.println(Thread.currentThread().getName()
+ ": 状态: " + thread.getState());
}
}
之前我们已经看到了如何通过覆写 run 方法创建一个线程对象,但线程对象被创建出来并不意味着线程就开始运行了。
package thread;
public class Demo9 {
public static void main(String[] args) {
Thread t = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
// t.run();
while (true) {
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
只是调用t.run()方法,就会按照顺序执行,没有创建多线程
调用t.start()方法,就会创建多线程t,和main线程并发执行
注意:虽然是并发执行,但是第一行大概率是打印hello main,本质上main线程和 t 子线程是同时并发并行的执行,打印的顺序是随机的。但子线程需要申请系统运行及时间片轮转调度,而main线程一直处于运行态,从概率上看main先执行的几率非常大。
调用 start 方法, 才真的在操作系统的底层创建出一个线程.
李四一旦进到工作状态,他就会按照行动指南上的步骤去进行工作,不完成是不会结束的。但有时我们需要增加一些机制,例如老板突然来电话了,说转账的对方是个骗子,需要赶紧停止转账,那张三该如何通知李四停止呢?这就涉及到我们的停止线程的方式了。
目前常见的有以下两种方式:
示例-1: 使用自定义的变量来作为标志位.
public class ThreadDemo {
private static class MyRunnable implements Runnable {
public volatile boolean isQuit = false;
@Override
public void run() {
while (!isQuit) {
System.out.println(Thread.currentThread().getName()
+ ": 别管我,我忙着转账呢!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()
+ ": 啊!险些误了大事");
}
}
public static void main(String[] args) throws InterruptedException {
MyRunnable target = new MyRunnable();
Thread thread = new Thread(target, "李四");
System.out.println(Thread.currentThread().getName()
+ ": 让李四开始转账。");
thread.start();
Thread.sleep(10 * 1000);
System.out.println(Thread.currentThread().getName()
+ ": 老板来电话了,得赶紧通知李四对方是个骗子!");
target.isQuit = true;
}
}
示例-2: 使用 Thread.interrupted() //静态方法
或者 Thread.currentThread().isInterrupted()//实例方法
代替自定义标志位.
Thread 内部包含了一个 boolean 类型的变量作为线程是否被中断的标记.
interrupted()
方法通知线程结束.public class ThreadDemo {
private static class MyRunnable implements Runnable {
@Override
public void run() {
// 两种方法均可以
while (!Thread.interrupted()) {
//while (!Thread.currentThread().isInterrupted()) {
System.out.println(Thread.currentThread().getName()
+ ": 别管我,我忙着转账呢!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println(Thread.currentThread().getName()
+ ": 有内鬼,终止交易!");
// 注意此处的 break
break;
}
}
System.out.println(Thread.currentThread().getName()
+ ": 啊!险些误了大事");
}
}
public static void main(String[] args) throws InterruptedException {
MyRunnable target = new MyRunnable();
Thread thread = new Thread(target, "李四");
System.out.println(Thread.currentThread().getName()
+ ": 让李四开始转账。");
thread.start();
Thread.sleep(10 * 1000);
System.out.println(Thread.currentThread().getName()
+ ": 老板来电话了,得赶紧通知李四对方是个骗子!");
thread.interrupt();
}
}
thread 收到通知的方式有两种:
如果线程因为调用 wait/join/sleep 等方法而阻塞挂起,则以 InterruptedException 异常的形式通知,清除中断标志
否则,只是内部的一个中断标志被设置,thread 可以通过
1. Thread.interrupted() 判断当前线程的中断标志被设置,清除中断标志
2. Thread.currentThread().isInterrupted() 判断指定线程的中断标志被设置,不清除中断标志
这种方式通知收到的更及时,即使线程正在 sleep 也可以马上收到。
示例-3 观察标志位是否清除
标志位是否清除, 就类似于一个开关.
Thread.isInterrupted() 相当于按下开关, 开关自动弹起来了. 这个称为 "清除标志位"
Thread.currentThread().isInterrupted() 相当于按下开关之后, 开关弹不起来, 这个称为"不清除标志位".
使用 Thread.isInterrupted() , 线程中断会清除标志位
public class ThreadDemo {
private static class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.interrupted());
}
}
}
public static void main(String[] args) throws InterruptedException {
MyRunnable target = new MyRunnable();
Thread thread = new Thread(target, "李四");
thread.start();
thread.interrupt();
}
}
true // 只有一开始是 true,后边都是 false,因为标志位被清
false
false
false
false
false
false
false
false
false
使用 Thread.currentThread().isInterrupted()
, 线程中断标记位不会清除.
public class ThreadDemo {
private static class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().isInterrupted());
}
}
}
public static void main(String[] args) throws InterruptedException {
MyRunnable target = new MyRunnable();
Thread thread = new Thread(target, "李四");
thread.start();
thread.interrupt();
}
}
true // 全部是 true,因为标志位没有被清
true
true
true
true
true
true
true
true
true
有时,我们需要等待一个线程完成它的工作后,才能进行自己的下一步工作。例如,张三只有等李四转账成功,才决定是否存钱,这时我们需要一个方法明确等待线程的结束。
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
Runnable target = () -> {
for (int i = 0; i < 10; i++) {
try {
System.out.println(Thread.currentThread().getName()
+ ": 我还在工作!");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + ": 我结束了!");
};
Thread thread1 = new Thread(target, "李四");
Thread thread2 = new Thread(target, "王五");
System.out.println("先让李四开始工作");
thread1.start();
thread1.join();
System.out.println("李四工作结束了,让王五开始工作");
thread2.start();
thread2.join();
System.out.println("王五工作结束了");
}
}
大家可以试试如果把两个 join 注释掉,现象会是怎么样的呢?
附录
带long millis的构造方法的解释:
因为join(2000)等待2s钟,但是t还没执行完,main停止等待,和t一起并发执行。t线程并不会结束运行,因为现在写的代码都是前台线程所有的执行完才会结束。
注意:关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行。
示例:
有三个线程,线程名称分别为:a,b,c。
每个线程打印自己的名称。
需要让他们同时启动,并按 c,b,a的顺序打印
public class dfg123 {
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
},"c");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
}
},"b");
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
try {
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
}
},"a");
t1.start();
t2.start();
t3.start();
}
}
关于 join 还有一些细节内容,我们留到下面再讲解。
public class ThreadDemo {
public static void main(String[] args) {
Thread thread = Thread.currentThread();
System.out.println(thread.getName());
}
}
也是我们比较熟悉一组方法,有一点要记得,因为线程的调度是不可控的,所以,这个方法只能保证实际休眠时间是大于等于参数设置的休眠时间的。
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
System.out.println(System.currentTimeMillis());
Thread.sleep(3 * 1000);
System.out.println(System.currentTimeMillis());
}
}
关于 sleep,以后我们还会有一些知识会给大家补充。
sleep是Thread的静态方法,作用是使当前线程“睡眠”一定时间,期间线程不会释放对象锁。
join是Thread的普通方法,作用是等待调用join方法的线程死亡,才能接着执行当前线程的后续方法。
join和sleep从使用效果上来看,都能使线程处于“阻塞”状态
两者的主要区别
线程的状态是一个枚举类型 Thread.State
public class ThreadState {
public static void main(String[] args) {
for (Thread.State state : Thread.State.values()) {
System.out.println(state);
}
}
}
创建态:当一个已经被创建的线程处于未被启动时,即:还没有调用start方法时,就处于这个状态。(Thread对象有了,内核中的线程还没有)
运行态:当线程已被占用,在Java虚拟机中正常执行时,就处于此状态。又可以分成正在工作中和即将开始工作.(内核中的线程没了,Thread对象还在)
阻塞态:当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态。当该线程持有锁时,该线程将自动变成RUNNABLE状态。
休眠态:一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。
指定时间休眠态:基本同WAITING状态,多了个超时参数,调用对应方法时线程将进入TIMED_WAITING状态,这一状态将一直保持到超时期满或者接收到唤醒通知,带有超时参数的常用方法有Thread.sleep。
结束态:从RUNNABLE状态正常退出而死亡,或者因为没有捕获的异常终止了RUNNABLE状态而死亡。
大家不要被这个状态转移图吓到,我们重点是要理解状态的意义以及各个状态的具体意思。
还是我们之前的例子:
刚把李四、王五找来,还是给他们在安排任务,没让他们行动起来,就是 NEW状态;
当李四、王五开始去窗口排队,等待服务,就进入到 RUNNABLE状态。该状态并不表示已经被银行工作人员开始接待,排在队伍中也是属于该状态,即可被服务的状态,是否开始服务,则看调度器的调度;
当李四、王五因为一些事情需要去忙,例如需要填写信息、回家取证件、发呆一会等等时,进入BLOCKED、 WATING、 TIMED_WAITING状态,至于这些状态的细分,我们以后再详解;
如果李四、王五已经忙完,为 TERMINATED状态。
所以,之前我们学过的 isAlive()方法,可以认为是处于不是 NEW和 TERMINATED的状态都是活着的。
观察 1:关注 NEW、 RUNNABLE、 TERMINATED状态的转换
public class ThreadStateTransfer {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
for (int i = 0; i < 1000_0000; i++) {
}
}, "李四");
System.out.println(t.getName() + ": " + t.getState());;
t.start();
while (t.isAlive()) {
System.out.println(t.getName() + ": " + t.getState());;
}
System.out.println(t.getName() + ": " + t.getState());;
}
}
观察 2:关注 WAITING、 BLOCKED、 TIMED_WAITING状态的转换
public static void main(String[] args) {
final Object object = new Object();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (object) {
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}, "t1");
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (object) {
System.out.println("hehe");
}
}
}, "t2");
t2.start();
}
使用 jconsole可以看到 t1的状态是 TIMED_WAITING , t2的状态是 BLOCKED
修改上面的代码,把 t1中的 sleep换成 wait
public static void main(String[] args) {
final Object object = new Object();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (object) {
try {
// [修改这里就可以了!!!!!]
// Thread.sleep(1000);
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "t1");
...
}
使用 jconsole可以看到 t1的状态是 WAITING
结论:
观察-3: yield()大公无私,让出 CPU
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
System.out.println("张三");
// 先注释掉, 再放开
// Thread.yield();
}
}
}, "t1");
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
System.out.println("李四");
}
}
}, "t2");
t2.start();
可以看到:
结论:
yield不改变线程的状态,但是会重新去排队.
static class Counter {
public int count = 0;
void increase() {
count++;
}
}
public static void main(String[] args) throws InterruptedException {
final Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.count);
}
大家观察下是否适用多线程的现象是否一致?同时尝试思考下为什么会有这样的现象发生呢?
想给出一个线程安全的确切定义是复杂的,但我们可以这样认为:
如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。
产生线程不安全的原因~
(如果是多个线程针对不同的变量进行修改,没事!如果多个线程针对同一个变量读,也没事!)可以通过调整代码结构,使不同线程操作不同变量~
共享变量一般在堆上.因此可以被多个线程共享访问.
原子性:一个操作不能被打断,要么全部执行完毕,要么不执行。
针对有些操作,比如读取变量的值,只是对应一条机器指令,此时这样的操作本身就可以视为是原子的。通过加锁操作,也就是把好几个指令给打包成一个原子的了。加锁操作,就是把这里的多个操作打包成—个原子的操作。
可见性指,一个线程对共享变量值的修改,能够及时地被其他线程看到.
一个具体的栗子。针对同一个变量,一个线程进行读操作(循环进行很多次),一个线程进行修改操作(合适的时候执行一次)。读取内存操作,相比于读取寄存器,是一个非常低效的操作!!!(慢3-4个数量级)。而且如果t2线程迟迟不修改, t1线程读到的值又始终是一样的值!!因此, t1就有了一个大胆的想法!!!就会不再从内存读数据了,而是直接从寄存器里读。(不执行load 了)一旦t1做出了这种大胆的假设,此时万一t2修改了count值, t1就不能感知到了。这是Java编译器进行代码优化产生的效果~~
咱们写的很多代码,彼此的顺序,谁在前谁在后无所谓。
编译器就会智能的调整这里代码的前后顺序从而提高程序的效率
保证逻辑不变的前提,再去调整顺序。
如果代码是单线程的程序,编译器的判定一般都是很准。
但是如果代码是多线程的,编译器也可能产生误判~
这里用到的机制,我们马上会给大家解释。
static class Counter {
public int count = 0;
synchronized void increase() {
count++;
}
}
public static void main(String[] args) throws InterruptedException {
final Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.count);
}
线程安全问题的解决方案(2个):
格式:synchronize(锁对象){
需要被同步的代码
}
同步代码块需要注意的事项:
修饰符 synchronized 返回值类型 函数名(形参列表..){
}
同步函数注意事项:
synchronized会起到互斥效果,某个线程执行到某个对象的 synchronized中时,其他线程如果也执行到同一个对象 synchronized就会阻塞等待.
理解 “阻塞等待”.
针对每一把锁,操作系统内部都维护了一个等待队列.当这个锁被某个线程占有的时候,其他线程尝试进行加锁,就加不上了,就会阻塞等待,一直等到之前的线程解锁之后,由操作系统唤醒一个新的线程,再来获取到这个锁.
注意:
synchronized的底层是使用操作系统的mutex lock实现的.
synchronized的工作过程:
所以 synchronized也能保证内存可见性.具体代码参见后面 volatile部分.
synchronized同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题;
理解 “把自己锁死”
一个线程没有释放锁,然后又尝试再次加锁.
// 第一次加锁, 加锁成功
lock();
// 第二次加锁, 锁已经被占用, 阻塞等待.
lock();
按照之前对于锁的设定,第二次加锁的时候,就会阻塞等待.直到第一次的锁被释放,才能获取到第二个锁.但是释放第一个锁也是由该线程来完成,结果这个线程已经躺平了,啥都不想干了,也就无法进行解锁操作.这时候就会死锁.
这样的锁称为不可重入锁.
synchronized
是可重入锁,因此没有上面的问题.
在可重入锁的内部,包含了 "线程持有者"和 "计数器"两个信息.
synchronized本质上要修改指定对象的 “对象头”.从使用角度来看, synchronized也势必要搭配一个具体的对象来使用.
1)直接修饰普通方法:锁的 SynchronizedDemo对象
public class SynchronizedDemo {
public synchronized void methond() {
}
}
2)修饰静态方法:锁的 SynchronizedDemo类的对象
public class SynchronizedDemo {
public synchronized static void method() {
}
}
3)修饰代码块:明确指定锁哪个对象.
锁当前对象
public class SynchronizedDemo {
public void method() {
synchronized (this) {
}
}
}
锁类对象
public class SynchronizedDemo {
public void method() {
synchronized (SynchronizedDemo.class) {
}
}
}
我们重点要理解,synchronized锁的是什么.两个线程竞争同一把锁,才会产生阻塞等待.
两个线程分别尝试获取两把不同的锁,不会产生竞争.
Java标准库中很多都是线程不安全的.这些类可能会涉及到多线程修改共享数据,又没有任何加锁措施.
但是还有一些是线程安全的.使用了一些锁机制来控制,比如:
StringBuffer的核心方法都带有 synchronized
还有的虽然没有加锁,但是不涉及 “修改”,仍然是线程安全的
Java内存模型 (JMM):
Java虚拟机规范中定义了Java内存模型.
目的是屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果.
线程之间的共享变量存在主内存 (Main Memory)。"主内存"才是真正硬件角度的 “内存”
每一个线程都有自己的 “工作内存” (Working Memory)。 “工作内存”,则是指 CPU的寄存器和高速缓存
当线程要读取一个共享变量的时候,会先把变量从主内存拷贝到工作内存,再从工作内存读取数据.
当线程要修改一个共享变量的时候,也会先修改工作内存中的副本,再同步回主内存.
此时修改线程1的工作内存中的值,线程2的工作内存不一定会及时变化.
2)一旦线程1修改了 a的值,此时主内存不一定能及时同步.对应的线程2的工作内存的 a的值也不一定能及时同步
这个时候代码中就容易出现问题.
1)为啥要这么麻烦的拷来拷去?
前面我们讨论内存可见性时说了,直接访问工作内存(实际是 CPU的寄存器或者 CPU的缓存),速度非常快,但是可能出现数据不一致的情况.加上 volatile ,强制读写内存.速度是慢了,但是数据变的更准确了。
由于线程之间是抢占式执行的,因此线程之间执行的先后顺序难以预知.
但是实际开发中有时候我们希望合理的协调多个线程之间的执行先后顺序.
球场上的每个运动员都是独立的 “执行流” ,可以认为是一个 “线程”.
而完成一个具体的进攻得分动作,则需要多个运动员相互配合,按照一定的顺序执行一定的动作,线程1先 “传球” ,线程2才能 “扣篮”.
完成这个协调工作,主要涉及到三个方法
注意: wait, notify, notifyAll都是 Object类的方法.
wait做的事情:
wait要搭配 synchronized来使用.脱离 synchronized使用 wait会直接抛出异常.
wait结束等待的条件:
代码示例:观察wait()方法使用
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
synchronized (object) {
System.out.println("等待中");
object.wait();
System.out.println("等待结束");
}
}
这样在执行到object.wait()之后就一直等待下去,那么程序肯定不能一直这么等待下去了。这个时候就需要使用到了另外一个方法唤醒的方法notify()。
notify方法是唤醒等待的线程.
其实理论上 wait和 sleep完全是没有可比性的,因为一个是用于线程之间的通信的,一个是让线程阻塞一段时间,
唯一的相同点就是都可以让线程放弃执行一段时间.
当然为了面试的目的,我们还是总结下:
wait()方法
wait()方法的作用是让当前线程进入等待状态,wait()会与notify()和notifyAll()方法一起使用。
notify()和notifyAll()方法的作用是唤醒等待中的线程,notify()方法:唤醒单个线程,notifyAll()方法:唤醒所有线程。
join()方法
join()方法是等待这个线程结束,完成其执行。它的主要起同步作用,使线程之间的执行从“并行”变成“串行”。
也就是说,当我们在线程A中调用了线程B的join()方法时,线程执行过程发生改变:线程A,必须等待线程B执行完毕后,才可以继续执行下去。
wait()方法和join()方法的相似处
wait()和join()方法都用于暂停Java中的当前线程,进入等待状态。
在Java中都可以调用interrupt()方法中断wait()和join()的线程状态。
wait()和join()都是非静态方法。
wait()和join()都在Java中重载。wait()和join()没有超时,但接受超时参数。
wait()方法和join()方法之间的区别
wait()方法需要在java.lang.Object类中声明;而,join()方法是在java.lang.Thread类中的普通方法。
wait()方法用于线程间通信;而join()方法用于在多个线程之间添加排序,第二个线程需要在第一个线程执行完成后才能开始执行。
我们可以通过使用notify()和notifyAll()方法启动一个通过wait()方法进入等待状态的线程。但是我们不能打破join()方法所施加的等待,除非中断或者调用连接的线程已执行完了。
wait()方法必须从同步(synchronized)的上下文调用,即同步块或方法,否则会抛出IllegalMonitorStateException异常。
但,在Java中有或没有同步的上下文,我们都可以调用join()方法。
啥是设计模式?
软件开发中也有很多常见的 “问题场景”.针对这些问题场景,大佬们总结出了一些固定的套路.按照这个套路来实现代码,也不会吃亏.
单例模式能保证某个类在程序中只存在唯一一份实例,而不会创建出多个实例.
这一点在很多场景上都需要.比如 JDBC中的 DataSource实例就只需要一个.
单例模式具体的实现方式,分成 "饿汉"和 "懒汉"两种.
主要应用懒汉模式。
饿汉模式
类加载的同时,创建实例.
package thread;
// 通过 Singleton 这个类来实现单例模式. 保证 Singleton 这个类只有唯一实例
// 饿汉模式
class Singleton {
// 1. 使用 static 创建一个实例, 并且立即进行实例化.
// 这个 instance 对应的实例, 就是该类的唯一实例.
private static Singleton instance = new Singleton();
// 2. 为了防止程序猿在其他地方不小心的 new 这个 Singleton, 就可以把构造方法设为 private
private Singleton() {}
// 3. 提供一个方法, 让外面能够拿到唯一实例.
public static Singleton getInstance() {
return instance;
}
}
public class Demo19 {
public static void main(String[] args) {
Singleton instance = Singleton.getInstance();
// Singleton instance2 = new Singleton();
}
}
懒汉模式-多线程版(最终版)
以下代码在加锁的基础上,做出了进一步改动:
package thread;
// 实现单例模式 - 懒汉模式
class Singleton2 {
// 1. 就不是立即就初始化实例.
private static volatile Singleton2 instance = null;
// 2. 把构造方法设为 private
private Singleton2() {}
// 3. 提供一个方法来获取到上述单例的实例
// 只有当真正需要用到这个 实例 的时候, 才会真正去创建这个实例.
public static Singleton2 getInstance() {
// 如果这个条件成立, 说明当前的单例未初始化过的, 存在线程安全风险, 就需要加锁~~
if (instance == null) {
synchronized (Singleton2.class) {
if (instance == null) {
instance = new Singleton2();
}
}
}
return instance;
}
}
public class Demo20 {
public static void main(String[] args) {
Singleton2 instance = Singleton2.getInstance();
}
}
理解双重 if判定 / volatile:
加锁 /解锁是一件开销比较高的事情.而懒汉模式的线程不安全只是发生在首次创建实例的时候.
因此后续使用的时候,不必再进行加锁了.
外层的 if就是判定下看当前是否已经把 instance实例创建出来了.
同时为了避免 "内存可见性"导致读取的 instance出现偏差,于是补充上 volatile .
当多线程首次调用 getInstance,大家可能都发现 instance为 null,于是又继续往下执行来竞争锁,
其中竞争成功的线程,再完成创建实例的操作.
当这个实例创建完了之后,其他竞争到锁的线程就被里层 if挡住了.也就不会继续创建其他实例.
1)有三个线程,开始执行 getInstance ,通过外层的 if (instance == null)知道了实例还没
有创建的消息.于是开始竞争同一把锁.
2)其中线程1率先获取到锁,此时线程1通过里层的 if (instance == null)进一步确认实例是否已经创建.如果没创建,就把这个实例创建出来.
3)当线程1释放锁之后,线程2和线程3也拿到锁,也通过里层的 if (instance == null) 来
确认实例是否已经创建,发现实例已经创建出来了,就不再创建了.
4)后续的线程,不必加锁,直接就通过外层 if (instance == null) 就知道实例已经创建了,从而不再尝试获取锁了.降低了开销.
阻塞队列是什么
阻塞队列是一种特殊的队列.也遵守 "先进先出"的原则.
阻塞队列能是一种线程安全的数据结构,并且具有以下特性:
阻塞队列的一个典型应用场景就是 “生产者消费者模型”.这是一种非常典型的开发模型.
生产者消费者模型
生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。
生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取.
1)阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力.
比如在 "秒杀"场景下,服务器同一时刻可能会收到大量的支付请求.如果直接处理这些支付请求,服务器可能扛不住(每个支付请求的处理都需要比较复杂的流程).这个时候就可以把这些请求都放到一个阻塞队列中,然后再由消费者线程慢慢的来处理每个支付请求.这样做可以有效进行 “削峰”,防止服务器被突然到来的一波请求直接冲垮.
2)阻塞队列也能使生产者和消费者之间解耦.
比如过年一家人一起包饺子.一般都是有明确分工,比如一个人负责擀饺子皮,其他人负责包.擀饺子皮的人就是 “生产者”,包饺子的人就是 “消费者”.擀饺子皮的人不关心包饺子的人是谁(能包就行,无论是手工包,借助工具,还是机器包),包饺子的人也不关心擀饺子皮的人是谁(有饺子皮就行,无论是用擀面杖擀的,还是拿罐头瓶擀,还是直接从超市买的).
标准库中的阻塞队列
在 Java标准库中内置了阻塞队列.如果我们需要在一些程序中使用阻塞队列,直接使用标准库中的即可.
public static void main(String[] args) throws InterruptedException {
BlockingQueue<String> queue = new LinkedBlockingQueue<>();
queue.put("hello"); // 入队列
String s = queue.take(); // 出队列. 如果没有 put 直接 take, 就会阻塞.
}
生产者消费者模型
public static void main(String[] args) throws InterruptedException {
BlockingQueue<Integer> blockingQueue = new LinkedBlockingQueue<Integer>();
Thread customer = new Thread(() -> {
while (true) {
try {
int value = blockingQueue.take();
System.out.println("消费元素: " + value);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "消费者");
customer.start();
Thread producer = new Thread(() -> {
Random random = new Random();
while (true) {
try {
int num = random.nextInt(1000);
System.out.println("生产元素: " + num);
blockingQueue.put(num);
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "生产者");
producer.start();
customer.join();
producer.join();
}
定时器
定时器是什么
定时器也是软件开发中的一个重要组件.类似于一个 “闹钟”.达到一个设定的时间之后,就执行某个指定好的代码.
定时器是一种实际开发中非常常用的组件.
比如网络通信中,如果对方 500ms内没有返回数据,则断开连接尝试重连.
比如一个 Map,希望里面的某个 key在 3s之后过期(自动删除).
类似于这样的场景就需要用到定时器.
标准库中的定时器
import java.util.Timer;
import java.util.TimerTask;
public class Demo23 {
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("hello timer");
}
}, 3000); //3秒之后打印“helo”
System.out.println("main");
}
}
虽然创建销毁线程比创建销毁进程更轻量,但是在频繁创建销毁线程的时候还是会比较低效.
线程池就是为了解决这个问题.如果某个线程不再使用了,并不是真正把线程释放,而是放到一个 "池子"中,下次如果需要用到线程就直接从池子中取,不必通过系统来创建了.
ExecutorService
和 Executors
代码示例:
ExecutorService
表示一个线程池实例.Executors
是一个工厂类,能够创建出几种不同风格的线程池.ExecutorService
的 submit
方法能够向线程池中提交若干个任务.ExecutorService pool = Executors.newFixedThreadPool(10);
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello");
}
})
Executors创建线程池的几种方式
Executors本质上是 ThreadPoolExecutor类的封装.
ThreadPoolExecutor提供了更多的可选参数,可以进一步细化线程池行为的设定.
ThreadPoolExecutor的构造方法
理解 ThreadPoolExecutor构造方法的参数
把创建一个线程池想象成开个公司.每个员工相当于一个线程.
corePoolSize:核心线程数(正式员工的数量)
maximumPoolSize:最大线程数(正式员工 +临时工的数目)
keepAliveTime:临时工允许的空闲时间.
TineUnit unit: keepaliveTime的时间单位,是秒,分钟,还是其他值.
threadFactory:创建线程的工厂,参与具体的创建线程工作.
BlockingQueue:传递任务的阻塞队列(线程池会提供一个submit方法让程序猿把任务注册到线程池中)
RejectedExecutionHandler:拒绝策略,如果任务量超出公司的负荷了接下来怎么处理.
代码示例:
ExecutorService pool = new ThreadPoolExecutor(1, 2, 1000, TimeUnit.MILLISECONDS,
new SynchronousQueue<Runnable>(),
Executors.defaultThreadFactory(),
new
ThreadPoolExecutor.AbortPolicy());
for(int i=0;i<3;i++) {
pool.submit(new Runnable() {
@Override
void run() {
System.out.println("hello");
}
});
}
虽然线程池的参数这么多,但是使用的时候最最重要的参数,还是第一组参数线程池中线程的个数!!!
有一个程序,这个程序要并发的/多线程的来完成一些任务w,如果使用线程池的话,这里的线程数设为多少合适?
正确做法:要通过性能测试的方式,找到合适的值~~
根据这里不同的线程池的线程数,来观察程序处理任务的速度和程序持有的CPU的占用率~
可以通过调用线程池的shutdown或shutdownNow方法来关闭线程池。他们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无响应中断的任务可能永远无法停止。
但是他们存在一定的区别
只要调用了这两个关闭方法的一个,isShutdown就会返回true。当所有的任务都关闭后,才表示线程池关闭成功,这时调用isTerminated方法会返回true
至于应该调用哪一种方法来关闭线程池,应该由提交到线程池的任务特性决定,通常调用shutdown方法来关闭线程池,如果任务不一定执行完,则可以调用shutdownNow方法。