文章目录
概述
- 线程:
-
- 线程是操作系统能够进行运算调度的最小单位。它包含在进程之中,是进程中实际运作单位
-
- 简单理解:应用软件中互相独立,可以同时运行的功能
- 进程
-
- 进程是程序的基本执行实体
多线程
什么是多线程
- 有了多线程,我们就可以让程序同时做多件事情
作用
- 提高效率
应用场景
只要你想让多个事情同时运行就需要用到多线程
- 软件中的耗时操作
-
- 拷贝、迁移大文件
-
- 加载大量的资源文件
- 所有的聊天软件
- 所有的后台服务器
并发和并行
- 并发:
-
- 在同一时刻,有多个指令在单个CPU上交替执行
- 并行:
-
- 在同一时刻,有多个指令在多个CPU上同时执行
多线程的实现方式
继承Thread类的方式进行实现
- 1.自己定义一个类继承Thread
- 2.重写run方法
- 3.创建子类对象,并启动线程
public class MyThread extends Thread {
@Override
public void run() {
//线程要执行的代码
for (int i = 0; i < 100; i++) {
System.out.println(getName() + "你好,猫猫");
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
t1.setName("线程1");
t2.setName("线程2");
t1.start();
t2.start();
}
}
实现Runnable接口的方式进行实现
- 1.自己定义一个类实现Runnable接口
- 2.重新里面的run方法
- 3.创建自己的类的对象
- 4.创建一个Thread类对象,并开启线程
public class MyRun implements Runnable {
@Override
public void run() {
//书写线程要执行的代码
for (int i = 0; i < 100; i++) {
//先获取到当前线程的对象
/*Thread t = Thread.currentThread();
System.out.println(t.getName() + "你好,猫猫!");*/
System.out.println(Thread.currentThread().getName() + "你好,猫猫!");
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
//创建MyRun对象
//表示多线程要执行的任务
MyRun mr = new MyRun();
//创建线程对象
Thread t1 = new Thread(mr);
Thread t2 = new Thread(mr);
//设置线程名字
t1.setName("线程1");
t2.setName("线程2");
//开启线程
t1.start();
t2.start();
}
}
利用Callable接口和Future接口方式实现
特点:可以获取到多线程运行到结果
- 1.创建一个类MyCallable实现Callable接口
- 2.重写call()(有返回值的,表示多线程运行到结果)
- 3.创建MyCallable对象(表示多线程要执行的任务)
- 4.创建FutureTask对象(作用管理多线程运行的结果)
- 5.创建Thread类的对象,并启动(表示线程)
public class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
//求1~100之间的和
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum += i;
}
return sum;
}
}
public class ThreadDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable mc = new MyCallable();
FutureTask<Integer> ft = new FutureTask<>(mc);
Thread t1 = new Thread(ft);
t1.start();
Integer result = ft.get();
//5050
System.out.println(result);
}
}
三种实现方式对比
优点 | 缺点 | |
---|---|---|
继承Thread类 | 编程比较简单,可以直接使用Thread类中的方法 | 可以扩展性较差,不能再继承其他的类 |
实现Runnable接口/实现Callable接口 | 扩展性强,实现该接口的同时还可以继承其他的类 | 编程相对复杂,不能直接使用Thread类中的方法 |
- 继承Thread类/实现Runnable接口无法获得多线程的结果
- 实现Callable接口可以获取多线程的结果
常见的成员方法
方法名称 | 说明 |
---|---|
String getName() | 返回此线程的名称 |
void setName(String name) | 设置线程的名字(构造方法也可以设置名字) |
static Thread currentThread() | 获取当前线程的对象 |
static void sleep(long time) | 让线程休眠指定的时间,单位为毫秒 |
setPriority(int newPriority) | 设置线程的优先级 |
final int getPriority() | 获取线程的优先级 |
final void setDaemon(boolean on) | 设置为守护线程 |
public static void yield() | 出让线程 / 礼让线程 |
public static void join() | 插入线程 / 插队线程 |
public class MyThread extends Thread {
public MyThread() {
}
public MyThread(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName() + "@" + i);
}
}
}
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
/*
void setName(String name)
细节:
1。如果我们没有给线程设置名字,线程也是有默认的名字的
格式:Thread-X(X序号,从0开始)
2。如果我们要给线程设置名字,可以用set方法进行设置,也可以构造方法设置
static Thread currentThread()
细节:
当JVM虚拟机启动之后,会自动启动多条线程
其中有一条线程就叫做main线程
他的作用就是去调用main方法,并执行里面的代码
在以前,我们写的所有的代码,其实都是运行在main线程当中
static void sleep(long time)
细节:
1。哪条线程执行到这个方法,那么哪条线程就会在这里停留对应的时间
2。方法的参数:就表示睡眠的时间,单位毫秒
3。当时间到了之后,线程会自动的醒来,继续执行下面的其他代码
*/
Thread t = Thread.currentThread();
String name = t.getName();
//main
System.out.println(name);
System.out.println("11111");
Thread.sleep(5000);
System.out.println("22222");
MyThread t1 = new MyThread("朵朵");
MyThread t2 = new MyThread("钢镚");
t1.start();
t2.start();
}
}
线程优先级
- 线程的调度
-
- 抢占式调度(随机性)
-
- 非抢占式调度
public class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
System.out.println(Thread.currentThread().getName() + "---" + i);
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
MyRunnable mr = new MyRunnable();
Thread t1 = new Thread(mr, "朵朵");
Thread t2 = new Thread(mr, "钢镚");
//5
//System.out.println(t1.getPriority());
//5
//System.out.println(t2.getPriority());
//5 main
//System.out.println(Thread.currentThread().getPriority());
//不是绝对的,概率问题
t1.setPriority(1);
t2.setPriority(10);
t1.start();
t2.start();
}
}
- 设置守护线程
-
- 细节:当其他的非守护线程执行完毕之后,守护线程会陆续结束
-
- 通俗易懂:当"女神线程"结束了,那么"备胎线程"也没有寻找的必要了,陆续结束(不会立马结束)
public class MyThread1 extends Thread {
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
System.out.println(getName() + "@" + i);
}
}
}
public class MyThread2 extends Thread {
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
System.out.println(getName() + "@" + i);
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
MyThread1 t1 = new MyThread1();
MyThread2 t2 = new MyThread2();
t1.setName("女神");
t2.setName("备胎");
//第二线程设置为守护线程(备胎线程)
t2.setDaemon(true);
t1.start();
t2.start();
}
}
- 礼让线程
public class MyThread extends Thread {
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
System.out.println(getName() + "@" + i);
//表示出让当前CPU的执行权
Thread.yield();
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
t1.setName("朵朵");
t2.setName("钢镚");
t1.start();
t2.start();
}
}
- 插入线程
public class MyThread extends Thread {
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
System.out.println(getName() + "@" + i);
}
}
}
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
MyThread t = new MyThread();
t.setName("朵朵");
t.start();
//表示把t这个线程,插入到当前线程之前
//t:朵朵
//当前线程:main线程
t.join();
//执行在main线程当中的
for (int i = 0; i < 10; i++) {
System.out.println("main线程" + i);
}
}
}
线程的生命周期
- 问:sleep方法会让线程睡眠,睡眠时间到了之后,立马就会执行下面的代码吗?
- 答:不会,sleep方法时间到了之后处于就绪状态,会继续抢CPU的执行权,抢到了才会执行代码
线程安全问题
需求:某电影院目前正在上映国产大片,共100张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票
public class MyThread extends Thread {
//表示这个类所有的对象,都共享ticket数据
static int ticket = 0;
@Override
public void run() {
while (true) {
if (ticket < 100) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket++;
System.out.println(getName() + "正在卖" + ticket + "张票");
} else {
break;
}
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
买票引发的安全问题
- 1.相同的票出现了多次
-
- 线程执行时,有随机性
- 2.出现了超出范围的票
-
- 线程执行时候,有随机性
- 解决:把if里操作共享数据的的代码锁起来,有线程进来之后,就算其他的线程抢夺到了cpu的执行权,也得等着。
同步代码块
- 把操作共享数据的代码锁起来
- 格式:
synchronized(锁){
操作共享数据的代码
}
- 特点1:锁默认打开,有一个线程进去了,锁自动关闭
- 特点2:里面的代码全部执行完毕,线程出来,锁自动打开
public class MyThread extends Thread {
//表示这个类所有的对象,都共享ticket数据
static int ticket = 0;
//锁对象,一定要是唯一的
//static Object obj = new Object();
@Override
public void run() {
while (true) {
//同步代码块
//synchronized (obj) {
synchronized (MyThread.class) {
if (ticket < 1000) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket++;
System.out.println(getName() + "正在卖" + ticket + "张票");
} else {
break;
}
}
}
}
}
同步方法
- 就是把synchronized关键字加到方法上
- 格式:
修饰符 synchronized 返回值类型 方法名(方法参数) {…}
- 特点1: 同步方法是锁住方法里面所有的代码
- 特点2: 锁对象不能自己指定
-
- 非静态:this
-
- 静态:当前类的字节码文件对象
public class MyRunnable implements Runnable {
int ticket = 0;
@Override
public void run() {
//1。循环
while (true) {
//2。同步代码块
synchronized (MyRunnable.class) {
//3。判断共享数据是否到了末尾,如果到了末尾
if (ticket == 100) {
break;
} else {
//4。判断共享数据是否到了末尾,如果没有到末尾
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket++;
System.out.println(Thread.currentThread().getName() + "在卖第" + ticket + "张票");
}
}
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
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();
}
}
- 将同步代码块改成同步方法
- 选中while里的语句 --> option+commend+M -->抽取出方法
- 添加synchronized
public class MyRunnable implements Runnable {
int ticket = 0;
@Override
public void run() {
//1。循环
while (true) {
//2。同步代码块
synchronized (MyRunnable.class) {
if (method()) break;
}
}
}
//this
private synchronized boolean method() {
//3。判断共享数据是否到了末尾,如果到了末尾
if (ticket == 100) {
return true;
} else {
//4。判断共享数据是否到了末尾,如果没有到末尾
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket++;
System.out.println(Thread.currentThread().getName() + "在卖第" + ticket + "张票");
}
return false;
}
}
Lock锁
- 虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁
- 为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock
- Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作
- Lock中提供了获得锁和释放锁的方法(手动上锁、手动释放锁)
-
void lock()
:获得锁
-
void unlock()
:释放锁
- Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化
- ReentrantLock的构造方法
-
ReentrantLock()
:创建一个ReentrantLock的实例
public class MyThread extends Thread {
static int ticket = 0;
static Lock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
lock.lock();
try {
if (ticket == 100) {
break;
} else {
Thread.sleep(10);
ticket++;
System.out.println(getName() + "正在卖" + ticket + "张票");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
}
死锁
public class MyThread extends Thread {
static Object objA = new Object();
static Object objB = new Object();
@Override
public void run() {
while (true) {
if ("线程A".equals(getName())) {
synchronized (objA) {
System.out.println("线程A拿到了A锁,准备拿B锁");
synchronized (objB) {
System.out.println("线程A拿到了B锁,准备执行完一轮");
}
}
} else if ("线程B".equals(getName())) {
if ("线程B".equals(getName())) {
synchronized (objB) {
System.out.println("线程B拿到了B锁,准备拿A锁");
synchronized (objA) {
System.out.println("线程B拿到了A锁,准备执行完一轮");
}
}
}
}
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
t1.setName("线程A");
t2.setName("线程B");
t1.start();
t2.start();
//线程A拿到了A锁,准备拿B锁
//线程B拿到了B锁,准备拿A锁
}
}
生产者和消费者
生产者消费者模式是一个十分经典的多线程协作的模式
- 生产者 – 生产数据
- 消费者 – 消费数据
- 假设生产者是厨师,用来制作食物
- 假设消费者是吃货,用来进食食物
- 核心思想:利用桌子来控制线程的执行
等待唤醒机制
- 生产者
-
- 判断桌子上是否有食物
-
-
- 有:等待
-
-
-
- 没有:制作食物
-
-
- 把食物放在桌子上
-
- 叫醒等待的消费者开吃
- 消费者
-
- 判断桌子上是否有食物
-
-
- 如果没有就等待
-
-
-
- 如果有就开吃
-
-
- 吃完之后,唤醒厨师继续做
常见方法
方法名称 | 说明 |
---|---|
void wait() | 当前线程等待,直到被其他线程唤醒 |
void notify | 随机唤醒单个线程 |
void notifyAll | 唤醒所有线程 |
//作用:控制生产者和消费者的执行
public class Desk {
//是否有食物 0:没有 1:有
//boolean的弊端 只有两个结果,只能控制两条线程轮流执行
//如果以后需求变了,需要三条四条线程,无法满足,考虑到后面的通用性,用int
public static int foodFlag = 0;
//总个数
//吃货最多可以吃十碗
public static int count = 10;
//锁对象
public static Object lock = new Object();
}
public class Cook extends Thread {
@Override
public void run() {
/*
1。循环
2。同步代码块
3。判断共享数据是否到了末尾(先写到了末尾的情况)
4。判断共享数据是否到了末尾(没有到末尾的情况,执行题目的核心逻辑)
* */
while (true) {
synchronized (Desk.lock) {
if (Desk.count == 0) {
break;
} else {
//判断桌子上是否有食物
if (Desk.foodFlag == 1) {
//如果有,就等待
try {
Desk.lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
//如果没有,就制作食物
System.out.println("厨师做了一碗面条");
//修改桌子上的食物状态
Desk.foodFlag = 1;
//叫醒等待的吃货开吃
Desk.lock.notifyAll();
}
}
}
}
}
}
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) {
e.printStackTrace();
}
//唤醒跟这把锁绑定的所有线程
Desk.lock.notifyAll();
} else {
//如果有,就开吃
//把吃的总数-1
Desk.count--;
System.out.println("吃货正在吃面条,还能再吃" + Desk.count + "碗!!!");
//吃完之后,唤醒厨师继续做
Desk.lock.notifyAll();
//修改桌子的状态
Desk.foodFlag = 0;
}
}
}
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
//创建线程的对象
Cook c = new Cook();
Foodie f = new Foodie();
//给线程设置名字
c.setName("厨师");
f.setName("吃货");
//开启线程
c.start();
f.start();
}
}
等待唤醒机制(阻塞队列方式实现)
- put数据时:放不进去,会等着,也叫做阻塞
- take数据时:取出第一个数据,取不到会等着,也叫做阻塞
- 细节:
-
- 生产者和消费者必须使用同一个阻塞队列
阻塞队列的继承结构
public class Cook extends Thread {
ArrayBlockingQueue<String> queue;
public Cook(ArrayBlockingQueue<String> queue) {
this.queue = queue;
}
@Override
public void run() {
while (true) {
//不断的把面条放到阻塞队列中
try {
queue.put("面条");
System.out.println("厨师放了一碗面条");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Foodie extends Thread {
ArrayBlockingQueue<String> queue;
public Foodie(ArrayBlockingQueue<String> queue) {
this.queue = queue;
}
@Override
public void run() {
while (true) {
//不断的从阻塞队列中获取面条
try {
String food = queue.take();
System.out.println(food);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
//1。创建阻塞队列的对象
ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(1);
//2。创建线程的对象,并把阻塞队列传递过去
Cook c = new Cook(queue);
Foodie f = new Foodie(queue);
//3。开启线程
c.start();
f.start();
}
}
线程的六种状态
- 新建状态(NEW) --> 创建线程对象
- 就绪状态 (RUNNABLE) --> start方法
- 阻塞状态 (BLOCKED) --> 无法获得锁对象
- 等待状态 (WAITING) --> wait方法
- 计时等待 (TIMED_WAITING) --> sleep方法
- 结束状态 (TERMINATED) --> 全部代码运行完毕