1. 基础知识引入
1.1 线程
操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位
单线程:一个进程如果只有一条执行路径,则称为单线程程序
多线程:一个进程如果有多条执行路径,则称为多线程程序
1.2 进程
程序的基本执行实体
- 独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位
- 动态性:进程的实质是程序的一次执行过程,进程是动态产生,动态消亡的
- 并发性:任何进程都可以同其他进程一起并发执行
1.3 多线程应用场景
- 软件中的耗时操作
- 拷贝、迁移大文件
- 加载大量的资源文件
1.4 并发和并行
并发:在同一时刻,有多个指令在单个CPU上交替执行
并行:在同一时刻,有多个指令在多个CPU上同时执行
2. 多线程的实现方式
2.1 实现多线程方式一:继承Thread类
package com.bobo.Thread;
public class Test {
public static void main(String[] args) {
/*
多线程的第一种启动方式:
1.自己定义一个类继承Thread类
2.重写run方法
3.创建子类的对象,并启动线程
*/
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
t1.setName("t1");
t2.setName("t2");
//开启线程
t1.start();
t2.start();
}
}
package com.bobo.Thread;
public class MyThread extends Thread{
@Override
public void run() {
//书写线程要执行的代码
for (int i = 0; i < 20; i++) {
System.out.println(getName() + "hello");
}
}
}
2.2 实现多线程方式二:实现Runnable接口
package com.bobo.Thread1;
public class Test {
public static void main(String[] args) {
/*
多线程的第二种启动方式:
1.自已定义一个类实现Runnable接口
2.重写里面的run方法
3.创建自已的类的对象
4.创建个Thread类的对象,并开启线程
*/
//创建MyRun的对象
//表示多线程要执行的任务
MyRun mr = new MyRun();
//创建线程对象
Thread t1 = new Thread(mr);
Thread t2 = new Thread(mr);
//给线程设置名字
t1.setName("t1");
t2.setName("t2");
//开启线程
t1.start();
t2.start();
}
}
package com.bobo.Thread1;
public class MyRun implements Runnable{
@Override
public void run() {
//书写线程要执行的代码
for (int i = 0; i < 20; i++) {
//获取到当前线程的对象
Thread t = Thread.currentThread();
System.out.println(t.getName() + "hello");
}
}
}
2.3 实现多线程方式三: 实现Callable接口和Future接口
package com.bobo.Thread2;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class Test {
public static void main(String[] args) throws ExecutionException, InterruptedException {
/*
多线程的第三种实现方式:
特点:可以获取到多线程运行的结果
1.创建一个类MyCallab1e实现ca11ab1e接口
2.重写ca11(是有返回值的,表示多线程运行的结果)
3.创建MyCa11ab1e的对象(表示多线程要执行的任务)
4.创建FutureTask的对象(作用管理多线程运行的结果)
5.创建Thread类的对象。并启动(表示线程)
*/
//创建MyCallable的对象(表示多线程要执行的任务)
MyCallable mc = new MyCallable();
//创建FutureTask的对象(作用:管理多线程运行的结果)
FutureTask<Integer> ft = new FutureTask<>(mc);
//创建线程的对象
Thread t1 = new Thread(ft);
//启动线程
t1.start();
//获取多线程结果
Integer res = ft.get();
System.out.println(res);
}
}
package com.bobo.Thread2;
import java.util.concurrent.Callable;
public class MyCallable implements Callable<Integer>{
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i < 100; i++) {
sum += i;
}
return sum;
}
}
2.4 三种实现方式的对比
- 实现Runnable、Callable接口
- 好处: 扩展性强,实现该接口的同时还可以继承其他的类
- 缺点: 编程相对复杂,不能直接使用Thread类中的方法
- 继承Thread类
- 好处: 编程比较简单,可以直接使用Thread类中的方法
- 缺点: 可以扩展性较差,不能再继承其他的类
3. 多线程中常用的成员方法
package com.bobo.ThreadMethod;
public class Test {
public static void main(String[] args) throws InterruptedException {
/*
细节:
1、如果我们没有给线程设置名字,线程也是有默认的名字的
格式:Thread-X(X序号,从0开始的)
2、如果我们要给线程设置名字,可以用set方法进行设置,也可以利用Thread中的构造方法设置
*/
//创建线程的对象 以及 设置线程名字
MyThread t1 = new MyThread("aaa");
MyThread t2 = new MyThread("bbb");
//开启线程
t1.start();
t2.start();
/*
static Thread currentThread():获取当前线程的对象
细节:
当JVM虚拟机启动之后,会自动的启动多条线程
其中有一条线程就叫做main线程
他的作用就是去调用main方法,并执行里面的代码
在以前,我们写的所有的代码,其实都是运行在main线程当中
*/
System.out.println(Thread.currentThread().getName());//main
System.out.println("1111111");
Thread.sleep(1000);
System.out.println("2222222");
//创建线程要执行的参数对象
MyRunnable mr = new MyRunnable();
//创建线程对象 以及 设置线程名字
Thread t3 = new Thread(mr,"ccc");
Thread t4 = new Thread(mr,"ddd");
//查看线程默认优先级
System.out.println(t4.getPriority());//5
System.out.println(t3.getPriority());//5
System.out.println(Thread.currentThread().getPriority());//5
//自定义线程优先级-越大越优先-优先级越大不代表一定先运行它
t4.setPriority(10);
t3.setPriority(1);
//启动线程
t3.start();
t4.start();
//创建线程的对象 以及 设置线程名字
MyThread t5 = new MyThread("eee");
MyThread1 t6 = new MyThread1("fff");
/*
细节:
当其他的非守护线程执行完毕之后,守护线程会陆续结束
*/
//设置为守护线程(备胎线程)
t6.setDaemon(true);
t5.start();
t6.start();
//创建线程的对象 以及 设置线程名字
MyThread t7 = new MyThread("飞机✈");
t7.start();
//表示把t7这个线程插入到当前线程(main线程)之前
t7.join();
//执行在main线程当中
for (int i = 0; i < 10; i++) {
System.out.println("main线程" + i);
}
}
}
package com.bobo.ThreadMethod;
public class MyThread extends Thread{
public MyThread() {
}
public MyThread(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName() + "@" + i);
}
}
}
package com.bobo.ThreadMethod;
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "@" + i);
}
}
}
package com.bobo.ThreadMethod;
public class MyThread1 extends Thread{
public MyThread1() {
}
public MyThread1(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName() + "@" + i);
Thread.yield();//表示让出当前CPU的执行权,目的使打印更加均匀
}
}
}
4. 线程生命周期
5. 同步代码块
把操作共享数据的代码锁起来
5.1 同步代码块格式
synchronized(任意对象) {
多条语句操作共享数据的代码
}
特点1:锁默认打开,有一个线程进去了,锁自动关闭
特点2:里面的代码全部执行完毕,线程出来,锁自动打开
5.2 同步代码块代码实例
package com.bobo.ThreadDemo;
public class Test {
public static void main(String[] args) {
/*
需求:
某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票
*/
//创建线程对象
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
//起名字
t1.setName("窗口一");
t2.setName("窗口二");
t3.setName("窗口三");
//开启线程
t1.start();
t2.start();
t3.start();
}
}
package com.bobo.ThreadDemo;
public class MyThread extends Thread{
//表示这个类所有对象,都共享ticket
static int ticket = 0;
//锁对象,一定要是唯一的
static Object obj = new Object();
@Override
public void run() {
while (true){
/*
锁对象:
this:当前线程,不唯一
MyThread.class:当前类的字节码文件,唯一
*/
synchronized (obj){
if (ticket < 100){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket++;
System.out.println(getName() + "正在卖第" + ticket + "张票!!!");
}else {
break;
}
}
}
}
}
6. 同步方法
把synchronized关键字加到方法上
6.1 同步方法格式
修饰符 synchronized 返回值类型 方法名(方法参数) {
方法体;
}
特点1:同步方法是锁住方法里面所有的代码
特点2:锁对象不能自己指定
- 非静态方法,锁对象为:this
- 静态方法,锁对象为:当前类的字节码文件对象
6.2 同步方法代码实例
package com.bobo.ThreadDemo1;
public class Test {
public static void main(String[] args) {
/*
需求:
某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票
利用同步方法实现
*/
//用于创建线程的参数
MyRunnable mr = new MyRunnable();
Thread t1 = new Thread(mr);
Thread t2 = new Thread(mr);
Thread t3 = new Thread(mr);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
package com.bobo.ThreadDemo1;
public class MyRunnable implements Runnable {
int ticket = 0;
@Override
public void run() {
//1.循环
while (true) {
//2.同步代码块(同步方法)
if (method()) break;
}
}
private synchronized boolean method() {
if (ticket == 100) {
return true;
} else {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
ticket++;
System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票!!!");
}
return false;
}
}
7.Lock锁
为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock
Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作
Lock中提供了手动上锁、手动释放锁的方法
- void lock():获得锁
- void unlock():释放锁
Lock是接口,不能直接实例化,这里采用它的实现类ReentrantLock来实例化
-
ReentrantLock构造方法
方法名 说明 ReentrantLock() 创建一个ReentrantLock的实例
代码实例:
package com.bobo.ThreadLock;
public class Test {
public static void main(String[] args) {
/*
需求:
某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票
*/
//创建线程对象
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
//起名字
t1.setName("窗口一");
t2.setName("窗口二");
t3.setName("窗口三");
//开启线程
t1.start();
t2.start();
t3.start();
}
}
package com.bobo.ThreadLock;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class MyThread extends Thread {
//表示这个类所有对象,都共享ticket
static int ticket = 0;
//必须加static,不然 锁 不唯一
static Lock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
lock.lock();//上锁
try {
if (ticket < 100) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket++;
System.out.println(getName() + "正在卖第" + ticket + "张票!!!");
} else {
break;
}
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
lock.unlock();//解锁
}
}
}
}
8. 死锁
8.1 概述
线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行
8.2 什么情况下会产生死锁
- 资源有限
- 同步嵌套
8.3 死锁代码实例
package com.bobo.ThreadLock1;
public class Test {
public static void main(String[] args) {
/*
死锁
*/
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
t1.setName("线程A");
t2.setName("线程B");
t1.start();
t2.start();
}
}
package com.bobo.ThreadLock1;
public class MyThread extends Thread{
//定义两把锁
static Object objectA = new Object();
static Object objectB = new Object();
@Override
public void run() {
while (true){
if ("线程A".equals(getName())){
synchronized (objectA){
System.out.println("线程A拿到了A锁,准备拿B锁");
synchronized (objectB){
System.out.println("线程A拿到了B锁,顺利执行完一轮");
}
}
} else if ("线程B".equals(getName())) {
synchronized (objectB){
System.out.println("线程B拿到了B锁,准备拿A锁");
synchronized (objectA){
System.out.println("线程B拿到了A锁,顺利执行完一轮");
}
}
}
}
}
}
9. 生产者和消费者
9.1 常见方法
方法名 | 说明 |
---|---|
void wait() | 导致当前线程等待,直到另一个线程调用该对象的 notify()方法或 notifyAll()方法 |
void notify() | 唤醒正在等待对象监视器的单个线程 |
void notifyAll() | 唤醒正在等待对象监视器的所有线程 |
9.2 代码实例
package com.bobo.ProducerAndConsumer;
public class Test {
public static void main(String[] args) {
/*
需求:完成生产者和消费者(等待唤程机制的代码)
实现线程轮流交替执行的效果
*/
Cook cook = new Cook();
Foodie foodie = new Foodie();
Foodie foodie1 = new Foodie();
cook.setName("厨师");
foodie.setName("吃货1");
foodie1.setName("吃货2");
cook.start();
foodie.start();
foodie1.start();
}
}
package com.bobo.ProducerAndConsumer;
public class Cook extends Thread{
@Override
public void run() {
while (true){
synchronized (Desk.lock){
if (Desk.count == 0){
//面料不足
break;
}else{
//判断桌子上是否有食物
if (Desk.foodFlag == 1){
//桌子上有面条
try {
Desk.lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}else {
//桌子上没有面条
System.out.println("cook做了一碗");
//修改桌子上食物状态
Desk.foodFlag = 1;
//叫醒等待的消费者开吃
Desk.lock.notifyAll();
}
}
}
}
}
}
package com.bobo.ProducerAndConsumer;
public class Desk {
/*
控制生产者和消费者的执行
*/
//是否有面条 0:没有面条 1:有面条
public static int foodFlag = 0;
//限制cook 一天内可做的面条碗数总个数
public static int count = 10;
//锁对象
public static Object lock = new Object();
}
package com.bobo.ProducerAndConsumer;
public class Foodie extends Thread{
@Override
public void run() {
while (true) {
synchronized (Desk.lock){
if (Desk.count == 0){
//面料不足
break;
}else {
//面料充足
//判断桌子上是否有面条
if (Desk.foodFlag == 0){
//桌子上没有面条-等着做-给厨师发信号让他做
try {
//让当前线程与锁进行绑定
Desk.lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//唤醒跟这把锁绑定的所有线程
Desk.lock.notifyAll();
}else {
//桌子上有面条-吃面条-给厨师发信号-修改桌子状态
//把吃的总数 -1
Desk.count--;
//开始吃
System.out.println(getName() + "开吃,面材料还够做" + Desk.count + "碗");
//吃完之后,唤醒厨师继续做
Desk.lock.notifyAll();
//修改桌子状态
Desk.foodFlag = 0;
}
}
}
}
}
}
10. 阻塞队列
10.1 阻塞队列继承结构
-
常见BlockingQueue:
ArrayBlockingQueue: 底层是数组,有界
LinkedBlockingQueue: 底层是链表,无界.但不是真正的无界,最大为int的最大值
-
BlockingQueue的核心方法:
put(anObject): 将参数放入队列,如果放不进去会阻塞
take(): 取出第一个数据,取不到会阻塞
10.2 代码实例
package com.bobo.BlockQueue;
import java.util.concurrent.ArrayBlockingQueue;
public class Test {
public static void main(String[] args) {
/*
需求:利用阻塞队列完成生产者和消费者(等待唤醒机制)的代码
细节:
生产者和消费者必须使用同一个阻塞队列
*/
//创建阻塞队列的对象
ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(1);
//创建线程的对象,并把阻塞队列传递过去
Cook c = new Cook(queue);
Foodie f = new Foodie(queue);
c.start();
f.start();
}
}
11. 线程状态
通过源码我们可以看到Java中的线程存在6种状态,每种线程状态的含义如下
线程状态 | 具体含义 |
---|---|
NEW | 一个尚未启动的线程的状态。也称之为初始状态、开始状态。线程刚被创建,但是并未启动。还没调用start方法。MyThread t = new MyThread()只有线程象,没有线程特征。 |
RUNNABLE | 当我们调用线程对象的start方法,那么此时线程对象进入了RUNNABLE状态。那么此时才是真正的在JVM进程中创建了一个线程,线程一经启动并不是立即得到执行,线程的运行与否要听令与CPU的调度,那么我们把这个中间状态称之为可执行状态(RUNNABLE)也就是说它具备执行的资格,但是并没有真正的执行起来而是在等待CPU的度。 |
BLOCKED | 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。 |
WAITING | 一个正在等待的线程的状态。也称之为等待状态。造成线程等待的原因有两种,分别是调用Object.wait()、join()方法。处于等待状态的线程,正在等待其他线程去执行一个特定的操作。例如:因为wait()而等待的线程正在等待另一个线程去调用notify()或notifyAll();一个因为join()而等待的线程正在等待另一个线程结束。 |
TIMED_WAITING | 一个在限定时间内等待的线程的状态。也称之为限时等待状态。造成线程限时等待状态的原因有三种,分别是:Thread.sleep(long),Object.wait(long)、join(long)。 |
TERMINATED | 一个完全运行完成的线程的状态。也称之为终止状态、结束状态 |
各个状态的转换,如下图所示:
12. 线程池
12.1 以前写多线程的弊端
- 用到线程的时候就创建
- 用完之后线程消失
12.2 线程池主要核心原理
- ①创建一个池子,池子中是空的
- ②提交任务时,池子会创建新的线程对象,任务执行完毕,线程归还给池子,下回再次提交任务时,不需要创建新的线程,直接复用已有的线程即可
- ③但是如果提交任务时,池子中没有空闲线程,也无法创建新的线程,任务就会排队等待
12.3 线程池实现步骤
- 创建线程池
- 提交任务
- 所有的任务全部执行完毕,关闭线程池
12.4 线程池代码实现
Executors:线程池的工具类通过调用方法返回不同类型的线程池对象。
public static ExecutorService newCachedThreadPool() //创建一个没有上限的线程池
public static newFixedThreadPool(int nThreads) //创建有上限的线程池
代码实例:
package com.bobo.ThreadPool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Test {
public static void main(String[] args) throws InterruptedException {
//获取线程池对象
ExecutorService pool1 = Executors.newCachedThreadPool();
//提交任务
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
//销毁线程池
pool1.shutdown();
}
}
package com.bobo.ThreadPool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Test1 {
public static void main(String[] args) {
//获取线程池对象
ExecutorService pool1 = Executors.newFixedThreadPool(3);
//提交任务
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());//第四个不起作用
//销毁线程池
pool1.shutdown();
}
}
package com.bobo.ThreadPool;
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "---" + i);
}
}
}
12.5 自定义线程池
12.5.1 创建自定义线程池对象
代码实例:
ThreadPoolExecutor pool = new ThreadPoolExecutor(
3,//核心线程数量
6,//最大线程数(核心线程 + 空闲线程)
60,//空闲线程最大存活时间
TimeUnit.SECONDS,//时间单位
new ArrayBlockingQueue<>(3),//任务队列
Executors.defaultThreadFactory(),//创建线程工厂
new ThreadPoolExecutor.AbortPolicy()//任务的拒绝策略
);
12.5.2 自定义线程池(任务拒绝策略)
12.5.3 自定义线程池小结
自定义线程池步骤:
- 创建一个空的池子
- 有任务提交时,线程池会创建线程去执行任务,执行完毕归还线程
不断的提交任务,会有以下三个临界点:
- 当核心线程满时,再提交任务就会排队
- 当核心线程满,队伍满时,会创建临时线程
- 当核心线程满,队伍满,临时线程满时,会触发任务拒绝策略
12.5.3 自定义线程池拓展
什么是最大并行数?
答:4核8线程,最大并行数为8
线程池多大?