多线程–基本概念
并发和并行
-
并行:在同一时刻,有多个指令在多个CPU上同时执行
-
并发:在同一时刻,有多个指令在单个CPU上交替执行
两个概念的解释都出现了【同一时刻】这个描述,但是这两者又有所区别:
- 在并行中:由于是多个CPU执行多个指令,每个CPU都还是执行一条指令,因此【同一时刻】,可以在微观上理解为【同一时间点】
- 在并发中:由于是单个CPU执行多个指令,可以知道的是:一个CPU在同一时刻,只能执行一条指令,因此【同一时刻】,在微观上理解为【同一个时间段内】
进程和线程
进程:是正在运行的程序。特点:
- 独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位
- 动态性:进程的实质是程序的一次执行过程,进程是动态产生,动态消亡的
- 并发性:任何进程都可以同其他进程一起并发执行
线程:是进程中的单个顺序控制流,是一条执行路径
-
单线程:一个进程如果只有一条执行路径,则称为单线程程序
-
多线程:一个进程如果有多条执行路径,则称为多线程程序
线程是操作系统OS能够进行运算调度的最小单位。
进程、线程举例理解
不同的进程:360安全卫士是一个进程,QQ是另外一个进程,两者相互独立,可以在同一时刻并发运行
线程:木马查杀、电脑清理、系统修复是360安全卫士这个进程下的不同线程
多线程
**多线程:**指从软件或者硬件上实现执行多个线程的技术
- 具有多线程能力的计算机因有硬件支持而能够在同一时间执行多个线程,提升性能
- java多线程是并发的还是并行的?【暂认定为是并行的】
多线程–实现
方式一 | 继承Thread
方法介绍
方法名 | 说明 |
---|---|
void start() | 开启该线程,Java虚拟机会调用run()方法,执行程序 |
void run() | 方法内写有该线程的业务逻辑。在线程开启后,此方法将被调用执行。 |
实现步骤
继承、重写:
- 自定义一个类并且继承Thread类
- 在自定义类中重写run()方法
实例化、执行:
- 实例化自定义类
- 对实例化对象执行start()方法,开启线程
public class MyThread extends Thread {
@Override
public void run() {
for(int i=0; i<100; i++) {
System.out.println(i);
}
}
}
public class MyThreadDemo {
public static void main(String[] args) {
MyThread my1 = new MyThread();
MyThread my2 = new MyThread();
my1.start();
my2.start();
}
}
注意事项
为什么要重写run()方法?
- 因为run()是用来封装被线程执行的代码
run()方法和start()方法的区别?
-
run():封装线程执行的代码,若直接调用,相当于普通方法的调用,无法多线程执行
-
start():该方法启动线程;然后由JVM调用此线程的run()方法,实现多线程运行
方式二 | 实现Runnable + 调用Thread
Thread构造方法
方法名 | 说明 |
---|---|
Thread(Runnable target) | 分配一个新的Thread对象,名字默认 |
Thread(Runnable target, String name) | 分配一个新的Thread对象,赋予名字 |
实现步骤
实现接口、重写方法:
- 定义一个类MyRunnable实现Runnable接口
- 在MyRunnable类中重写run()方法
实例化、调用类:
- 创建MyRunnable类的对象
- 创建Thread类的对象,把MyRunnable对象作为构造方法的参数
- 启动线程
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 MyRunnableDemo {
public static void main(String[] args) {
//创建MyRunnable类的对象
MyRunnable my = new MyRunnable();
//创建Thread类的对象,把MyRunnable对象作为构造方法的参数
// Thread(Runnable target)
Thread t1 = new Thread(my);
Thread t2 = new Thread(my);
//Thread(Runnable target, String name)
Thread t1 = new Thread(my,"坦克");
Thread t2 = new Thread(my,"飞机");
//启动线程
t1.start();
t2.start();
}
}
方式三 | 实现Callable + 调用FutureTask、Thread
方法介绍
方法名 | 说明 |
---|---|
call() | 写有多线程业务逻辑,并需要return执行后的结果。若没有,则抛出一个异常 |
FutureTask(Callable callable) | 创建一个 FutureTask,传入实现Callable接口的自定义类的实例化对象 |
void get() | FutureTask的一个方法, 可以获取call()方法执行完毕后的返回结果 |
实现步骤
接口实现:
- 定义一个类MyCallable实现Callable接口
- 在MyCallable类中重写call()方法
实例化、类调用:
- 创建MyCallable类的对象
- 创建Future的实现类FutureTask对象,把MyCallable对象作为构造方法的参数
- 创建Thread类的对象,把FutureTask对象作为构造方法的参数
- 启动线程
- 再调用get方法,就可以获取线程结束之后的结果。
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
for (int i = 0; i < 100; i++) {
System.out.println("跟女孩表白" + i);
}
//返回值就表示线程运行完毕之后的结果
return "答应";
}
}
public class Demo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//线程开启之后需要执行里面的call方法
MyCallable mc = new MyCallable();
//可以获取线程执行完毕之后的结果.也可以作为参数传递给Thread对象
FutureTask<String> ft = new FutureTask<>(mc);
//创建线程对象
Thread t1 = new Thread(ft);
String s = ft.get();
//开启线程
t1.start();
String s = ft.get();
System.out.println(s);
}
}
三种多线程实现方法对比
优点 | 缺点 | |
---|---|---|
实现Runnable、Callable接口 | 扩展性强,实现该接口的同时还可以继承其他的类 | 编程相对复杂,不能直接使用Thread类中的方法 |
继承Thread类 | 编程比较简单,可以直接使用Thread类中的方法 | 可扩展性较差,不能再继承其他的类 |
多线程–理解
示例代码
public class MyThread extends Thread{
public MyThread() {
}
public MyThread(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName() + "@" + i);
}
}
}
package com.itheima.a04threadmethod1;
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
//1.创建线程的对象
MyThread t1 = new MyThread("飞机");
MyThread t2 = new MyThread("坦克");
MyThread t3 = new MyThread("GSF");
//2.开启线程
t1.start();
t2.start();
Thread t = Thread.currentThread();
String name = t.getName();
System.out.println(name);//main
System.out.println("11111111111");
Thread.sleep(5000);
System.out.println("22222222222");
t3.run();
System.out.println("33333333333");
}
}
main
11111111111
坦克@0
飞机@0
飞机@1
坦克@1
飞机@2
坦克@2
飞机@3
坦克@3
22222222222
飞机@4
坦克@4
GSF@0
飞机@5
坦克@5
GSF@1
飞机@6
坦克@6
GSF@2
飞机@7
坦克@7
GSF@3
飞机@8
坦克@8
GSF@4
飞机@9
坦克@9
GSF@5
GSF@6
GSF@7
GSF@8
GSF@9
33333333333
结果分析
-
首先是main线程开始执行,并在执行到start()方法的时候,将对应的线程唤醒
- 在执行到start()方法之前,只有一个main线程在执行
- 每执行一个start()方法,就多一个线程加入到执行的列表中,开始执行对应的线程方法
-
各线程相继运行,并将结果打印到控制台。
理解:
- 各个线程执行的先后没有绝对的优先级,谁争抢到执行权,就执行谁
- 因为main线程休眠的原因,所以休眠后执行到的代码,出现在结果最后面
多线程–线程方法
设置和获取线程名称
方法名 | 说明 |
---|---|
void setName(String name) | 设置线程名称 |
String getName() | 获取线程名称 |
Thread currentThread() | 返回对当前正在执行的线程对象的引用。理解:CSDN例子 |
public class MyThread extends Thread {
public MyThread() {}
public MyThread(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName()+":"+i);
}
}
}
public class MyThreadDemo {
public static void main(String[] args) {
MyThread my1 = new MyThread();
MyThread my2 = new MyThread();
//void setName(String name):将此线程的名称更改为等于参数 name
my1.setName("高铁");
my2.setName("飞机");
//Thread(String name)
MyThread my1 = new MyThread("高铁");
MyThread my2 = new MyThread("飞机");
my1.start();
my2.start();
//static Thread currentThread() 返回对当前正在执行的线程对象的引用
System.out.println(Thread.currentThread().getName()); // main:该代码是被main线程执行的
}
}
线程休眠
方法名 | 说明 |
---|---|
static void sleep(long millis) | 静态方法,使当前正在执行的线程停留(暂停执行)指定的毫秒数 |
public class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
try {
Thread.sleep(100); // 方法方法体里面,执行到该语句的线程是该类的一个具体实例化对象
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "---" + i);
}
}
}
public class Demo {
public static void main(String[] args) throws InterruptedException {
System.out.println("睡觉前");
Thread.sleep(3000); // 使main线程休息3s
System.out.println("睡醒了");
MyRunnable mr = new MyRunnable();
Thread t1 = new Thread(mr);
Thread t2 = new Thread(mr);
t1.start();
t2.start();
}
}
线程优先级
线程的调度方式有:
- 分时调度模型:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片
- 抢占式调度模型:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个线程执行
Java使用的是抢占式调度模型
优先级设置相关方法
方法名 | 说明 |
---|---|
final int getPriority() | 返回此线程的优先级 |
final void setPriority(int newPriority) | 更改此线程的优先级线程默认优先级是5;线程优先级的范围是:1-10**(数字越大,优先级越高)** |
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "---" + i);
}
return "线程执行完毕了";
}
}
public class Demo {
public static void main(String[] args) {
//优先级: 1 - 10 默认值:5
MyCallable mc = new MyCallable();
FutureTask<String> ft = new FutureTask<>(mc);
Thread t1 = new Thread(ft);
t1.setName("飞机");
t1.setPriority(10);
//System.out.println(t1.getPriority());//5
t1.start();
MyCallable mc2 = new MyCallable();
FutureTask<String> ft2 = new FutureTask<>(mc2);
Thread t2 = new Thread(ft2);
t2.setName("坦克");
t2.setPriority(1);
//System.out.println(t2.getPriority());//5
t2.start();
}
}
守护线程
当非守护线程都结束之后,守护线程也会相继结束
方法名 | 说明 |
---|---|
void setDaemon(boolean on) | 将此线程标记为守护线程,当运行的线程都是守护线程时,Java虚拟机将退出 |
public class MyThread1 extends Thread {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName() + "---" + i);
}
}
}
public class MyThread2 extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName() + "---" + i);
}
}
}
public class Demo {
public static void main(String[] args) {
MyThread1 t1 = new MyThread1();
MyThread2 t2 = new MyThread2();
t1.setName("女神");
t2.setName("备胎");
//把第二个线程t2设置为守护线程
//当普通线程t1执行完之后,那么守护线程也没有继续运行下去的必要了.
t2.setDaemon(true);
t1.start();
t2.start();
}
}
礼让线程和插入线程
方法名 | 说明 |
---|---|
public static void yield() | 出让线程,当前线程让出cpu |
public static void join() | 插入线程,把一个线程插入到当前线程前面,该线程执行完毕,才会执行当前线程后面的代码 |
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) throws InterruptedException {
MyThread t = new MyThread();
t.setName("土豆");
t.start();
// t:名字为土豆的线程;当前线程: main线程
// 表示把t这个线程,插入到当前线程之前
t.join();
// 当t线程执行完毕后,以下main线程中的后续代码才会执行
//执行在main线程当中的
for (int i = 0; i < 10; i++) {
System.out.println("main线程" + i);
}
}
}
线程的生命周期
多线程–线程同步
示例:卖票
案例需求
- 某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票
public class SellTicket implements Runnable {
private int tickets = 100;
//在SellTicket类中重写run()方法实现卖票,代码步骤如下
@Override
public void run() {
while (true) {
if(ticket <= 0){
//卖完了
break;
}else{
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
System.out.println(Thread.currentThread().getName() + "在卖票,还剩下" + ticket + "张票");
}
}
}
}
public class SellTicketDemo {
public static void main(String[] args) {
//创建SellTicket类的对象
SellTicket st = new SellTicket();
//创建三个Thread类的对象,把SellTicket对象作为构造方法的参数,并给出对应的窗口名称
Thread t1 = new Thread(st,"窗口1");
Thread t2 = new Thread(st,"窗口2");
Thread t3 = new Thread(st,"窗口3");
//启动线程
t1.start();
t2.start();
t3.start();
}
}
问题发现
卖票出现了问题
-
相同的票出现了多次
-
出现了负数的票
问题产生原因
- 线程执行的随机性导致的,可能在卖票过程中丢失cpu的执行权,导致出现问题
出现该安全问题的代码环境:
-
是多线程环境
-
有共享数据
-
有多条语句操作共享数据
如何解决多线程安全问题呢?
- 基本思想:改变可能出现问题的代码环境,让程序在一个安全的环境中
怎么实现呢?
-
把多条语句操作共享数据的代码给锁起来,让任意时刻只能有一个线程执行即可
-
Java提供了同步代码块的方式来解决
问题解决1:同步代码块
实现手段
synchronized(任意对象) {
多条语句操作共享数据的代码
}
好处和弊端
-
好处:解决了多线程的数据安全问题
-
弊端:当线程很多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率
// 实现接口
public class SellTicket implements Runnable {
private int tickets = 100;
private Object obj = new Object();
@Override
public void run() {
while (true) {
// 对可能有安全问题的代码加锁,多个线程必须使用同一把锁
synchronized (obj) {
//t1进来后,就会把这段代码给锁起来
if (tickets > 0) {
try {
Thread.sleep(100);
//t1休息100毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
//窗口1正在出售第100张票
System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
tickets--; //tickets = 99;
}
}
//t1出来了,这段代码的锁就被释放了
}
}
}
// 运行示例
public class SellTicketDemo {
public static void main(String[] args) {
SellTicket st = new SellTicket();
Thread t1 = new Thread(st, "窗口1");
Thread t2 = new Thread(st, "窗口2");
Thread t3 = new Thread(st, "窗口3");
t1.start();
t2.start();
t3.start();
}
}
问题解决2:同步方法
实现手段
非静态同步方法,默认的锁对象是:this
修饰符 synchronized 返回值类型 方法名(方法参数) {
方法体;
}
静态同步方法,默认的锁对象是:类名.class
修饰符 static synchronized 返回值类型 方法名(方法参数) {
方法体;
}
示例
public class MyRunnable implements Runnable {
private static int ticketCount = 100;
@Override
public void run() {
while(true){
if("窗口一".equals(Thread.currentThread().getName())){
//示例:同步方法
boolean result = synchronizedMthod();
if(result){
break;
}
}
if("窗口二".equals(Thread.currentThread().getName())){
//示例:同步代码块
synchronized (MyRunnable.class){
if(ticketCount == 0){
break;
}else{
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticketCount--;
System.out.println(Thread.currentThread().getName() + "在卖票,还剩下" + ticketCount + "张票");
}
}
}
}
}
// 定义的同步方法
private static synchronized boolean synchronizedMthod() {
if(ticketCount == 0){
return true;
}else{
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticketCount--;
System.out.println(Thread.currentThread().getName() + "在卖票,还剩下" + ticketCount + "张票");
return false;
}
}
}
问题解决3:线程锁Lock
方法名 | 说明 |
---|---|
ReentrantLock() | 创建一个ReentrantLock的实例 |
- Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化
方法名 | 说明 |
---|---|
void lock() | 获得锁 |
void unlock() | 释放锁 |
public class Ticket implements Runnable {
//票的数量
private int ticket = 100;
private Object obj = new Object();
static Lock lock = new ReentrantLock(); // 定义为静态变量
@Override
public void run() {
while (true) {
try {
lock.lock();
if (ticket <= 0) {
//卖完了
break;
} else {
Thread.sleep(100);
ticket--;
System.out.println(Thread.currentThread().getName() + "在卖票,还剩下" + ticket + "张票");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
}
public class Demo {
public static void main(String[] args) {
Ticket ticket = new Ticket();
Thread t1 = new Thread(ticket);
Thread t2 = new Thread(ticket);
Thread t3 = new Thread(ticket);
t1.setName("窗口一");
t2.setName("窗口二");
t3.setName("窗口三");
t1.start();
t2.start();
t3.start();
}
}
多线程–应用
死锁【非概念,是一种BUG现象】
线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行
什么情况下会产生死锁?
-
资源有限
-
同步嵌套
package com.itheima.a12deadlock;
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();
}
}
public class MyThread extends Thread {
static Object objA = new Object();
static Object objB = new Object();
@Override
public void run() {
//1.循环
while (true) {
if ("线程A".equals(getName())) {
synchronized (objA) {
System.out.println("线程A拿到了A锁,准备拿B锁");//A
synchronized (objB) {
System.out.println("线程A拿到了B锁,顺利执行完一轮");
}
}
} else if ("线程B".equals(getName())) {
if ("线程B".equals(getName())) {
synchronized (objB) {
System.out.println("线程B拿到了B锁,准备拿A锁");//B
synchronized (objA) {
System.out.println("线程B拿到了A锁,顺利执行完一轮");
}
}
}
}
}
}
}
生产者消费者模式示例
生产者消费者模式是线程模型中的经典问题:生产者和消费者在同一时间段内共用同一存储空间,生产者向空间里面生产数据,消费者从中取走数据
等待唤醒机制 | 线程锁实现
代码背景:
- 一张桌子(提供锁对象)、一个厨师(生产者)、一个顾客(消费者)
- 起初桌子上没有食物
实现逻辑:
若顾客先发现桌子上没有食物,就开始等待:
- 厨师发现没有食物,去制作食物并放到桌子上,提醒顾客吃饭,然后休息
- 顾客吃完,提醒厨师做饭,然后休息
- 厨师收到提醒,发现没有食物,开始做饭,放到桌子上,提醒顾客吃饭,然后休息
- 顾客收到提醒,发现有食物,开始吃饭,吃完提醒厨师做饭,然后休息
- 如此往复
若厨师先发现桌子上没有食物,就开始做饭:
- 厨师发现没有食物,开始做饭,放到桌子上,提供顾客来吃,然后休息
- 顾客收到提醒,发现有食物,开始吃饭,吃完提醒厨师做饭,然后休息
- 厨师收到提醒,发现没有食物,开始做饭,放到桌子上,提醒顾客吃饭,然后休息
- 如此往复
代码示例
桌子
public class Desk {
//是否有面条 0:没有面条 1:有面条
public static int foodFlag = 0;
//总个数
public static int count = 10;
//锁对象
public static Object lock = new Object();
生产者:厨师
public class Producer 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) {
e.printStackTrace();
}
}else{
//如果没有,就制作食物
System.out.println("厨师做了一碗面条");
//修改桌子上的食物状态
Desk.foodFlag = 1;
//叫醒等待的消费者开吃
Desk.lock.notifyAll();
}
}
}
}
}
}
消费者:顾客
public class Consumer 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();
}
}else{
//把吃的总数-1
Desk.count--;
//如果有,就开吃
System.out.println("吃货在吃面条,还能再吃" + Desk.count + "碗!!!");
//吃完之后,唤醒厨师继续做
Desk.lock.notifyAll();
//修改桌子的状态
Desk.foodFlag = 0;
}
}
}
}
}
}
主程序
public class ThreadDemo {
public static void main(String[] args) {
//创建线程的对象
Producer c = new Producer();
Consumer f = new Consumer();
Consumer f2 = new Consumer();
//给线程设置名字
c.setName("厨师");
f.setName("吃货1");
f2.setName("吃货2");
//开启线程
c.start();
f.start();
f2.start();
}
}
等待唤醒机制 | 阻塞队列实现
暂时没有很理解
多线程的6种状态
图中运行状态是不存在的,仅为了理解
新建状态 | New
-
一个尚未启动的线程的状态,也称之为初始状态、开始状态
-
这时候:线程已经被创建,但是并未启动,即还没调用start()方法
就绪状态 | RUNNABLE
- 当调用线程对象的start()方法,那么此时线程对象进入了RUNNABLE状态,此时才是真正的在JVM进程中创建了一个线程
- 线程一经启动并不是立即得到执行,线程的运行与否要听令与CPU的调度,此时这个中间状态称之为可执行状态(RUNNABLE)。
- 即:具备执行的资格,但没有执行权限
阻塞状态 | BLOCKED
- 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;
- 当该线程持有锁时,该线程将变成Runnable状态,继而开始运行
等待状态 | WAITING
- 一个正在等待的线程的状态,也称之为等待状态
- 造成线程等待的原因有两种:调用Object.wait()、Thread.join()方法
- 处于等待状态的线程,正在等待其他线程去执行一个特定的操作
- 因为wait()而等待的线程正在等待另一个线程去调用Object.notify()或Object.notifyAll()方法来唤醒该线程;
- 一个因为join()而等待的线程正在等待另一个线程结束,才会轮到该线程执行。
计时状态 | TIMED_WAITING
- 一个在限定时间内等待的线程的状态,也称之为限时等待状态
- 造成线程限时等待状态的原因有:Thread.sleep(long)
结束状态 | TERMINATED
- 一个完全运行完成的线程的状态,即run()方法执行完毕。也称之为终止状态、结束状态。
线程池
暂空