1. 认识线程(Thread)
1.1 概念
1)线程是什么
一个线程就是一个“执行流”,每个线程之间都可以按照顺序执行自己的代码,多个线程之间“同时”执行着多份代码
2)为啥要有线程
首先,“并发编程”成为“刚需”
单核CPU的发展遇到了瓶颈,要想提高算力,就需要多核CPU,而并发编程能更充分地利用多核CPU资源
有些任务场景需要“等待IO”,为了让等待IO的时间能够去做一些其他的工作,也需要用到并发编程
其次,虽然多线程也能实现并发编程,但是线程比进程更轻量
创建线程比创建进程更快
销毁线程比销毁进程更快
调度线程比调度进程更快
最后,线程虽然比进程更轻量,但是人们还不满足,于是又有了“线程池(ThreadPool)”
3)进程和线程的区别
进程是包含线程的,每个进程至少有一个线程存在,即主线程
进程和进程之间不共享内存空间,同一个进程的线程之间共享同一个内存空间
进程是OS分配资源的最小单位,线程是OS调度的最小单位
进程的创建、切换和销毁的开销较大,线程创建、切换和销毁的开销较小
4)Java的线程和操作系统线程的关系
线程是操作系统的概念,操作系统内核实现了线程这样的机制,并且堆用户层提供了一些API供用户使用
Java标准库中Thread类可以视为是对操作系统提供的API进行了进一步的抽象和封装
1.2 创建线程
1.2.1 方法1 继承Thread类
1)继承Thread类创建一个线程类
public class MyThread1 extends Thread{ @Override public void run() { System.out.println("线程1被调用"); } }
2)创建线程实例,调用start()方法运行线程
// 创建线程1 实例 MyThread1 thread1 = new MyThread1(); // 线程1开始运行 thread1.start();
1.2.2 方法2 实现Runnable接口
1)实现Runnable接口创建线程类
public class MyThread2 implements Runnable{ @Override public void run() { System.out.println("线程2被调用"); } }
2)以线程类的实例为参数创建Thread类实例,调用start() 方法运行线程
// 创建线程2 实例 Thread thread2 = new Thread(new MyThread2()); // 线程2 开始运行 thread2.start();
1.2.3 实现 Callable 接口,使用 FutureTask 接收线程返回值
创建 MyThread3 类并实现 Callable 接口,重写 call() 方法,该方法为线程的执行体,并且该方法有返回值
实例化 MyThread3 对象,使用 FutureTask 类封装 MyThread3 对象
使用 Thread 类封装 FutureTask 对象,使用 Thread 类的 start() 方法启动线程
使用 FutureTask 对象的 get() 方法得到线程的返回值,此方法在使用时需要抛出异常
public class MyThread3 implements Callable<String> { @Override public String call() throws Exception { return "我是 MyThread3 线程"; } public static void main(String[] args) throws ExecutionException, InterruptedException { // 实例化 MyThread3 对象 MyThread3 myThread3 = new MyThread3(); // 使用 FutureTask 类接收 myThread3 返回值 FutureTask<String> stringFutureTask = new FutureTask<>(myThread3); // 使用 Thread 类创建对象 Thread thread = new Thread(stringFutureTask); thread.start(); // get() 方法得到返回值 System.out.println(stringFutureTask.get()); } }
1.2.4 对比上面两种方法
方法1继承Thread类,直接使用this就可以表示当前线程对象的引用
方法2实现Runnable接口,需要使用Thread.currentThread()来表示当前线程的引用
方法3实现 Callable 接口,重写 call 方法来执行线程任务,使用 FutureTask 接收线程返回值;需要使用Thread.currentThread()来表示当前线程的引用
2. Thread类及常见方法
Thread类是JVM用来管理线程的有一个类,换句话说,每个线程都有一个唯一的Thread对象与之关联
2.1 Thread的常见构造方法
Thread t1 = new Thread(); Thread t2 = new Thread(new MyRunnable()); Thread t3 = new Thread("这是我的名字"); Thread t4 = new Thread(new MyRunnable(), "这是我的名字");
2.2 Thread的几个常见属性
属性 | 获取方法 |
ID | getId() |
名称 | getName() |
状态 | getState() |
优先级 | getPriority() |
是否后台线程 | isDaemon() |
是否存活 | isAlive() |
是否被中断 | isInterrupted() |
ID 是线程的唯一标识,不同线程不会重复
名称是各种调试工具用到
状态表示线程当前所处的一个情况
优先级高的线程理论上来说更容易被调度到
关于后台线程,需要记住一点:JVM会在一个线程的所有非后台线程结束后,才会结束运行
是否存活,既简单的理解,为run方法是否运行结束
2.3 启动一个线程-start()
线程对象被创建出来并不意味着线程就开始运行了
重写run方法是提供给线程要执行的代码
调用start()方法之后,线程才真正运行起来了
调用start()方法,才真正的在操作系统的底层创建出了一个线程
2.4 中断一个线程
线程一旦进入运行状态,就会按照代码的步骤去运行,不完成是不会结束的,但是如果我们想认为让线程停止工作该怎么做呢
常见的让线程停止工作的方法有:
通过一个标志位来判断是否需要中断线程
调用interrupt()方法来中断线程
示例1 使用自定义的变量来作为标志位
需要给标志位加 volatile 关键字
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类型的变量来作为是否被中断的标志位
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(); } }
2.5 等待一个线程-join()
有时我们需要等待一个线程完成他的工作后,才能进行下一步工作,这时我们需要一个方法明确等待线程的结束
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("王五工作结束了"); } }
方法 | 说明 |
public void join() | 等待线程结束 |
public void join(long millis) | 等待线程结束,最多等待millis毫秒 |
public void join(long millis, int nanos) | 同上,但可以更高精度 |
2.6 获取当前线程的引用
方法 | 说明 |
public static Thread currentThread() | 返回当前线程对象的引用 |
2.7 休眠当前线程
由于线程的调度是不可控的,所以此方法只能保证实际休眠时间大于等于参数设置的休眠时间
方法 | 说明 |
public static void sleep(long millis) throws InterruptException | 休眠当前线程 millis 毫秒 |
3. 线程的状态
3.1 线程的所有状态
线程的状态是一个枚举类型 Thread.State
线程的状态有以下几种:
NEW:线程被创建,但还未开始运行
RUNABLE:线程开始运行或运行中
BLOCKED:由于锁的原因,线程被阻塞
WAITING:线程处于等待状态
TIMED_WAITING:线程还是等待状态,但是等待时间超过预设时间
TERMINATED:线程运行结束
3.2 线程各状态之间的转移
解释两个方法的用途:
- yield() :当前线程让出CPU供其他线程调度
- notify() :唤醒当前线程
4. 线程安全
4.1 概念
如果多线程环境下代码的运行结果与单线程环境下的结果一致,则说明这个程序是线程安全的
4.2 线程不安全的原因
4.2.1 修改共享数据
当多个线程对同一个共享的数据做修改时,就有可能产生多次修改或者修改不成功的情况,就会导致结果与预期有多偏差
4.2.2 原子性
当客户端A检查还有一张票时,将票卖掉,还没有执行更新数据库时,客户端B检查了票数,发现大于0,于是又将票卖了出去,然后A将票数更新回数据库,此时就出现了同一张票被卖了两次
什么是原子性
我们把一段代码想象成一个房间,每个线程就是要进入房间的人,如果没有任何保护机制,A进入房间之后,还没有出来,B也进入了房间,就会打断A在房间内的隐私操作,这个就是不具备原子性的
那么如何解决这个问题呢,是不是可以在房间加一把锁,A进去后就把门锁上,其他人就进不去了,这样就保证了原子性
这个现象就叫做同步互斥,表示操作是互相排斥的
不保证原子性会对多线程带来什么问题
如果一个线程正在对一个变量操作,中途其他线程插入进来,如果这个操作被打断了,结果可能就是错误的
4.2.3 内存可见性
内存可见性指,一个线程对共享变量值的修改,能够及时的被其他线程看到
Java内存模型(JMM)
内存模型存在的目的是屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各平台下都能达到一致的并发效果
线程之间的共享变量存在主内存
每一个线程都有自己的 “工作内存”
当线程要读取一个共享变量时,会先把变量从主内存拷贝到工作内存,再从工作内存读取数据
当线程要修改一个共享内存的时候,也会先修改工作内存中的副本,再同步回主内存
由于每个线程都有自己的工作内存,这些工作内存中的内容相当于同一个共享变量的 “副本”,此时修改线程1的工作内存中的值,线程2的工作内存不一定会及时变化
初始情况下,两个线程的工作内存内容一致
- 一旦线程1 修改了a的值,此时主内存不一定及时同步,对应的线程2的工作内存的a的值也不一定能及时同步
这个时候代码中就容易出现问题
4.2.4 代码重排序
什么是代码重排序
假如有一段代码是这样的:
去前台取下U盘
去教室写10分钟作业
去前台取下快递
如果在单线程情况下,JVM、CPU指令集会对其进行优化,比如,按照 1-> 3 -> 2的顺序执行,也是没有问题的,可以少跑一次前台,这种叫做指令重排序
5. synchronized关键字
5.1 synchronized的特性
1)互斥
synchronized 会起到互斥效果,某个线程执行到某个对象的synchronized中时,其他线程如果也执行到同一个对象synchronized就会阻塞等待
进入synchronized修饰的代码块,相当于加锁
退出synchronized修饰的代码块,相当于解锁
synchronized用的锁是存在Java对象头中的
理解阻塞等待
针对每一把锁,操作系统内部都维护了一个等待队列,当这个锁被某个线程占有的时候,其他线程尝试加锁,就加不上了,就会阻塞等待,一直等到之前的线程解锁后,由操作系统唤醒一个新的线程,再来获取到这个锁
注意:
上一个线程解锁之后,下一个线程并不是立即就能获取到锁,而是要靠操作系统来唤醒,这也就是操作系统调度的一部分工作
假设由A、B、C三个线程,A先获取到锁,然后B尝试获取锁,然后C再尝试获取锁,此时B和C都在阻塞队列中排队等待,但是当A释放锁之后,虽然B比C先来的,但是B不一定就能获取到锁,而是和C重新竞争,并不遵循先来后到的规则
2)刷新内存
synchronized 的工作过程:
获取互斥锁
从主存变量的最新副本拷贝到工作内存
执行代码
将更改后的共享变量的值刷新到主内存
释放互斥锁
所以synchronized也能保证内存可见性
3)可重入
synchronized同步块对同一个线程来说是可重入的,不会出现把自己锁死的问题
理解把自己锁死
按照之前对于锁的设定,第二次加锁的时候,就会阻塞等待,但是当这个线程没有释放锁,然后又尝试再次加锁,就会死锁
这样的锁被称为 不可重入锁
代码示例
在以下代码中:
increase 和 increase2 两个方法都加了 synchronized, 此处的 synchronized 都是针对 this 当前对 象加锁的.
在调用 increase2 的时候, 先加了一次锁, 执行到 increase 的时候, 又加了一次锁. (上个锁还没释放, 相当于连续加两次锁)
这段代码是完全没有问题的,因为synchronized是可重入锁
static class Counter { public int count = 0; synchronized void increase() { count++; } synchronized void increase2() { increase(); } }
说明
在可重入锁的内部,包含了“线程持有者”和“计数器”两个信息
如果线程加锁的时候,发现锁被其他人占用,但是恰好占用的正是自己,那么仍然可以获取到锁,并让计数器自增
解锁的时候计数器递减到0的时候,才真正释放锁
5.2 synchronized使用示例
synchronized本质是修改指定对象的对象头,从使用角度来看,synchronized也必须搭配一个具体的对象来使用
1)直接修饰普通方法
锁的synchronizedDemo对象
public class SynchronizedDemo { public synchronized void methond() { } }
2)修饰静态方法
锁的synchronizedDemo对象
public class SynchronizedDemo { public static synchronized void methond() { } }
3)修饰代码块
明确指定锁哪个对象
锁当前对象
public class SynchronizedDemo { public void methond() { synchronized (this) { } } }
锁指定类对象
public class SynchronizedDemo { public void methond() { synchronized (SynchronizedDemo.class) { } } }
5.3 Java标准库中的线程安全类
Java标准库中很多都是线程不安全的,这些类可能会涉及到多线程修改共享数据,又没有任何加锁措施
ArrayList
LinkedList
HashMap
TreeMap
HashSet
TreeSet
StringBuilder
但还又一些是线程安全的,使用了一些锁机制来控制
Vector
HashTable
ConcurrentHashMap
StringBuffer
还有虽然没有加锁,但是不涉及修改,仍然是线程安全的
String
6. volatile关键字
volatile能保证内存可见性
volatile修饰的变量能够保证 “内存可见性”
代码在写入volatile修饰的变量时:
改变线程工作内存中volatile变量副本的值
将改变后的副本的值从工作内存刷新到主内存
代码在读取volatile修饰的变量时:
从主内存中读取volatile变量的最新值到线程的工作内存中
从工作内存中读取volatile变量的副本
volatile 不保证原子性
volatile 和 synchronized 有着本质的区别,synchronized 能够保证原子性,volatile只能保证内存可见性
7. wait 和 notify
由于线程之间是抢占式执行的,因此线程之间执行的先后顺序难以预知,但是实际开发中有时候我们希望合理的协调多个线程之间的执行顺序
完成这个工作,主要涉及到三个方法:
wait() / wait(long timeout):让当前线程进入等待状态
notify() / notifyAll():唤醒当前对象上等待的线程
7.1 wait() 方法
wait 做的事情
使当前执行代码的线程进入等待状态(把线程放到等待队列中)
释放当前的锁
满足一定条件时被唤醒,尝试重新获取这个锁
注:wait要搭配synchronized来使用,脱离synchronized使用wait会直接抛出异常
wait 等待结束的条件
其他线程调用该对象的notify方法
wait等待的时间超过预定时间
其他线程调用该等待线程的interrupted方法,导致wait抛出异常
7.2 notify() 方法
notify 方法是唤醒等待的线程
notify方法也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象对象锁的其他线程,对其发出通知,使他们重新获取该对象的对象锁
如果有多个线程等待,则由线程调度器随机挑选一个呈wait状态的线程
在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行 完,也就是退出同步代码块之后才会释放对象锁。
7.3 notifyAll() 方法
notify 方法只是唤醒一个等待线程,使用notifyAll方法可以一次唤醒所有等待线程
**注意:**虽然是同时唤醒所有等待线程,但是这些线程还是需要竞争锁,所以唤醒之后并不是通知执行,仍然是有先有后的执行
7.4 wait 和 sleep的对比
两者的相同点是都可以让线程放弃执行一段时间
不同点:
wait需要搭配synchronized使用,sleep不需要
wait是Object的方法,sleep是Thread的静态方法
9. 多线程案例
9.1 单例模式
单例模式能保证某个类在程序中只存在唯一一份实例,而不会创建多个实例
单例模式具体的实现方式分成 “ 饿汉 ” 和 “ 懒汉 ” 两种
饿汉模式
类加载的同时,创建实例
class Singleton { private static Singleton instance = new Singleton(); private Singleton() {} public static Singleton getInstance() { return instance; } }
懒汉模式–单线程版
类加载的时候不创建实例,第一次使用的时候才创建实例
class Singleton { private static Singleton instance = null; private Singleton() {} public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
懒汉模式–多线程版
上面的懒汉模式的实现是线程不安全的
线程安全问题发生在首次创建实例时,如果在多个线程中同时调用 getInstance 方法,就可能导致创建出多个实例
加上synchronized可以改善这里线程不安全的问题
class Singleton { private static Singleton instance = null; private Singleton() {} public synchronized static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
懒汉模式–多线程版(改进)
以下代码在加锁的基础上,做出了一些改动:
- 使用双重 if 判定,降低锁竞争的频率
- 给 instance 加上了 volatile
class Singleton { private static volatile Singleton instance = null; private Singleton() {} public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }
理解上面代码中的双重 if 判定和 volatile
加锁 / 解锁是一件开销比较高的事情,而懒汉模式的线程不安全只发生在首次创建实例的时候,因此后续使用的时候,不必再进行加锁了
外层的 if 就是判定当前是否已经把实例创建出来了,如果当前实例还未创建,再对对象进行加锁
同时为了避免 “ 内存可见性 ” 导致读取到的实例对象出现偏差,于是补充上 volatile
当多个线程首次调用同步方法时,大家可能都发现实例还未创建,于是继续往下执行来竞争锁,其中竞争成功的线程,再完成创建实例的操作
当这个实例被创建完成后,其他竞争到锁的线程就被里层 if 拦住了,也就不会再继续创建实例了
9.2 阻塞队列
阻塞队列是什么
阻塞队列是一种特殊的队列,也遵守 “ 先进先出 ” 的原则
阻塞队列是一种线程安全的数据结构,并且有以下特性:
当队列满的时候,继续入队列就会阻塞,直到有其他线程从队列中取走元素
当队列空的时候,继续出队列就会阻塞,直到有其他线程往队列中插入元素
阻塞队列的一个典型应用场景就是 “ 生产者消费者模型 ”,这是一种典型的开发模型
生产者消费者模型
生产者消费者模型就是通过一个容器来解决生产者和消费者的强耦合问题
生产者和消费者彼此之间不直接通讯,而通过阻塞队列进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取
阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力
阻塞队列也能使生产者和消费者之间解耦
标准库中的阻塞队列
在Java标准库中内置了阻塞队列,如果我们需要在一些程序中使用阻塞队列,直接使用标准库中的即可
BlockingQueue是一个接口,真正实现的类是LinkedBlockingQueue
入队使用put方法,出队使用take方法
BlockingQueue也有offer、poll、peek等方法,但是这些方法不带有阻塞特性
BlockingQueue<String> queue = new LinkedBlockingQueue<>(); // 入队列 queue.put("abc"); // 出队列. 如果没有 put 直接 take, 就会阻塞. String elem = queue.take();
9.3 定时器
定时器是什么
定时器也是软件开发中的一个重要组件,类似于一个 “ 闹钟 ”,达到一个设定时间之后,就执行某个指定好的代码
标准库中的定时器
标准库中提供了一个Timer类,Timer类的核心方法为schedule
schedule包含两个参数,第一个参数指定即将要执行的任务代码,第二个参数指定多长时间之后执行(单位为毫秒)
Timer timer = new Timer(); timer.schedule(new TimerTask() { @Override public void run() { System.out.println("hello"); } }, 3000);
9.4 线程池
为什么会有线程池
我们使用一个线程的时候就去创建一个线程,这样实现起来非常简便,但是有一个实际性的问题:
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间
有了线程池就可以做到线程的复用,即执行完一个任务,并不销毁,而是可以执行其他的任务
线程池是什么
线程池其实就是一个容纳多个线程的容器,其中的线程可以复用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源
一张图理解线程池的工作原理
线程池的使用
使用executor类下的方法创建线程池,常用的有以下几种:
newFixedThreadPool(int n):创建一个固定长度为n的线程池,每当提交一个任务,就创建一个线程,直到达到线程池的最大数量,这时线程规模将不再变化,当某一个线程发生未预期的错误而结束时,线程池就会补充一个新的线程
newCachedThreadPool():创建一个可缓存的线程池,如果线程池的规模超过了处理需求,将会自动回收空闲线程,当需求增加时,则会自动添加新的线程,线程池的规模不存在任何限制
newSingleThreadExecutor():这是一个单线程的Executor,它创建单个线程来执行任务,如果这个线程发生异常而结束,会创建一个新的线程来替代它,它的特点是确保依照任务在队列中的顺序来串行执行
newScheduledThreadPool():设置延迟时间后执行命令,或者定期执行命令,是进阶版的Timer
Executors 本质上是ThreadPoolExecutor类的封装,ThreadPoolExecutor提供了更多的可选参数可以进一步细化线程池的设定
submit:提交线程到线程池
shutdown:关闭线程池
创建线程池及使用
// 创建线程类 public class Thread1 extends Thread { @Override public void run() { System.out.println("任务1开始"); try { sleep(3000); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("任务1结束"); } } // 创建线程类 public class Thread2 extends Thread { @Override public void run() { System.out.println("任务2开始"); try { sleep(3000); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("任务2结束"); } } // 在主类中创建线程池 public class Main { public static void main(String[] args) { // 创建固定长度为 2 的线程池 ExecutorService executorService = Executors.newFixedThreadPool(2); // ScheduledExecutorService executorService = Executors.newScheduledThreadPool(2); // 得到 thread1实例 Thread1 thread1 = new Thread1(); // 得到 thread2实例 Thread2 thread2 = new Thread2(); // 设置 5 秒之后执行任务1 // executorService.schedule(thread1, 5, TimeUnit.SECONDS); // 执行任务1 executorService.submit(thread1); // 执行任务2 executorService.submit(thread2); // 再次执行任务1 executorService.submit(thread1); // 关闭线程池 executorService.shutdown(); } }
10. 保证线程安全的思路
- 使用没有共享资源的模型
- 使用共享资源只读、不写的模型
- 保证原子性
- 保证顺序性
- 保证可见性