一、为什么要有多线程?
1、线程与进程
进程:进程是程序的基本执行实体
举例:在任务管理器中,一个软件运行之后,它就是一个进程
线程:(简单理解,线程就说应用软件中互相独立,可以同时运行的功能)
单线程程序:所有的都在一个线程中执行,耗时长
2、多线程的应用场景
3、小结
二、多线程中的两个概念(并发和并行)
1、并发
2、并行
以2核4线程为例:(如果计算机中只要4条线程,那么它是不用切换的,但如果线程越来越多,那么这个红线就会在多个线程之间随机的进行切换)
3、小结
三、多线程的三种实现方式
1、继承Thread类的方式进行实现
自己定义一个类继承Thread并重写run方法
创建子类的对象,并启动线程
2、实现Runnable接口的方式进行实现
自己定义一个类实现Runnable接口,并重新里面的run方法
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()+"HelloWorld");
*/
System.out.println(Thread.currentThread().getName()+"HelloWorld");
}
}
}
package com.yaqi.a02threadcase2;
public class ThreadDemo {
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("线程1");
t1.setName("线程2");
//开启线程
t1.start();
t2.start();
}
}
结果
3、利用Callable接口和Future接口方式的实现
package com.yaqi.a03threadcase3;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class ThreadDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
/*
* 多线程的第三种实现方式:
* 特点:可以获取到多线程运行的结果
*
* 1. 创建一个类MyCallable实现Callable接口
* 2. 重写call (是有返回值的,表示多线程运行的结果)
*
* 3. 创建MyCallable的对象(表示多线程要执行的任务)
* 4. 创建FutureTask的对象(作用管理多线程运行的结果)
* 5. 创建Thread类的对象,并启动(表示线程)
* */
//创建MyCallable的对象(表示多线程要执行的任务)
MyCallable mc = new MyCallable();
//创建FutureTask的对象(作用管理多线程运行的结果)
FutureTask<Integer> ft = new FutureTask<>(mc);
//创建线程的对象
Thread t1 = new Thread(ft);
//启动线程
t1.start();
//获取多线程运行的结果
Integer result = ft.get();
System.out.println(result);
}
}
package com.yaqi.a03threadcase3;
import java.util.concurrent.Callable;
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 = sum + i;
}
return sum;
}
}
4、多线程三种实现方式对比
四、常见的成员方法
1、get/setName方法 -- 线程名字
默认名字的由来:
序号自增
/*
String getName() 返回此线程的名称
void setName(String name) 设置线程的名字(构造方法也可以设置名字)
细节:
1、如果我们没有给线程设置名字,线程也是有默认的名字的
格式:Thread-X(X序号,从0开始的)
2、如果我们要给线程设置名字,可以用set方法进行设置,也可以构造方法设置
*/
//1.创建线程的对象
MyThread t1 = new MyThread("飞机");
MyThread t2 = new MyThread("坦克");
//2.开启线程
t1.start();
t2.start();
MyThread
package com.yaqi.a04threadmethod1;
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);
}
}
}
2、currentThread方法 -- 获取当前线程对象
static Thread currentThread() 获取当前线程的对象 细节: 当JVM虚拟机启动之后,会自动的启动多条线程 其中有一条线程就叫做main线程 他的作用就是去调用main方法,并执行里面的代码 在以前,我们写的所有的代码,其实都是运行在main线程当中
//哪条线程执行到这个方法,此时获取的就是哪条线程的对象
Thread t = Thread.currentThread();
String name = t.getName();
System.out.println(name);//main
3、sleep方法 -- 线程休眠
static void sleep(long time) 让线程休眠指定的时间,单位为毫秒 细节: 1、哪条线程执行到这个方法,那么哪条线程就会在这里停留对应的时间 2、方法的参数:就表示睡眠的时间,单位毫秒 1 秒= 1000毫秒 3、当时间到了之后,线程会自动的醒来,继续执行下面的其他代码
System.out.println("11111111111");
Thread.sleep(5000);
System.out.println("22222222222");
4、set/getPriority方法 -- 线程优先级
没有设置,优先级则默认为5,优先级越高,抢到CPU的概率就越高
- 最小1
- 最大10
- 默认5
5、setDaemon方法 -- 守护线程
两个线程执行的代码不同:守护线程是陆续结束的,所以守护线程也叫做备胎线程
应用场景
6、yield方法 -- 礼让线程
但是只是尽可能的均匀,不是绝对的
7、join方法 -- 插入线程
插入线程:将土豆插入到main线程之前,只有当土豆线程执行完毕,才会轮到main线程
8、线程的生命周期
五、线程安全的问题
1、练习:设计一个程序模拟电影院卖票
出现了超出票范围或者重复票的情况:
2、买票引发的安全问题
相同的票出现多次
出现了超出范围的票
2.1、重复票的由来:(线程在执行代码的过程中,CPU的执行权随时有可能被抢走)
2.2、出现了超出范围的票:(和上面的原因相同)
3、安全问题的解决办法 -- 同步代码块
示例代码
结果
4、同步代码块中的两个小细节
4.1、细节1:synchronized要写在循环的里面
4.2、细节2:synchronized中的锁对象一定是唯一的
5、同步方法
示例代码
将同步代码块改成同步方法:
6、StringBuilder和StringBuffer的区别
两个类的方法都是相同的
但是StringBuffer是线程安全的,它里面所有的方法都是线程同步的
StringBulider:代码单线程的不需要考虑多线程当中数据安全的情况
StringBuffer:多线程环境下需要考虑数据安全则选择StringBuffer
7、Lock锁(手动加锁、释放锁)
7.1、Lock使用不规范造成的两个安全问题
Ⅰ、重复票以及超出范围票
我们在使用Thread类实现多线程时,创建自己的类,一定要注意锁对象需要唯一,即在相关变量前加上static关键字
Ⅱ、程序无法正常终止
这是由于当满足条件时,循环直接被终止,导致lock锁没有被释放
Ⅲ、正确代码(标准写法)
即将容易产生异常的代码块放入try…catch中
六、死锁
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。
注意事项:千万不要让两个锁嵌套起来!
七、生产者和消费者(等待唤醒机制)
生产者消费者模式是一个十分经典的多线程协作的模式
1、消费者等待
2、生产者等待
3、常见方法(wait/notify/notifyAll)
4、消费者与生产者代码实现
4.1、Cook.java
public class Cook extends Thread{
@Override
public void run() {
/*
* 1. 循环
* 2. 同步代码块
* 3. 判断共享数据是否到了末尾(到了末尾)
* 4. 判断共享数据是否到了末尾(没有到末尾,执行核心逻辑)
* */
while (true){
synchronized (com.yaqi.a13waitandnotify.Desk.lock){
if(com.yaqi.a13waitandnotify.Desk.count == 0){
break;
}else{
//判断桌子上是否有食物
if(com.yaqi.a13waitandnotify.Desk.foodFlag == 1){
//如果有,就等待
try {
com.yaqi.a13waitandnotify.Desk.lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
//如果没有,就制作食物
System.out.println("厨师做了一碗面条");
//修改桌子上的食物状态
com.yaqi.a13waitandnotify.Desk.foodFlag = 1;
//叫醒等待的消费者开吃
com.yaqi.a13waitandnotify.Desk.lock.notifyAll();
}
}
}
}
}
}
4.2、Desk.java
package com.yaqi.a13waitandnotify;
public class Desk {
/*
* 作用:控制生产者和消费者的执行
*
* */
//是否有面条 0:没有面条 1:有面条
public static int foodFlag = 0;
//总个数
public static int count = 10;
//锁对象
public static Object lock = new Object();
}
4.3、Foodie.java
public class Foodie extends Thread{
@Override
public void run() {
/*
* 1. 循环
* 2. 同步代码块
* 3. 判断共享数据是否到了末尾(到了末尾)
* 4. 判断共享数据是否到了末尾(没有到末尾,执行核心逻辑)
* */
while(true){
synchronized (com.yaqi.a13waitandnotify.Desk.lock){
if(com.yaqi.a13waitandnotify.Desk.count == 0){
break;
}else{
//先判断桌子上是否有面条
if(com.yaqi.a13waitandnotify.Desk.foodFlag == 0){
//如果没有,就等待
try {
com.yaqi.a13waitandnotify.Desk.lock.wait();//让当前线程跟锁进行绑定
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
//把吃的总数-1
com.yaqi.a13waitandnotify.Desk.count--;
//如果有,就开吃
System.out.println("吃货在吃面条,还能再吃" + com.yaqi.a13waitandnotify.Desk.count + "碗!!!");
//吃完之后,唤醒厨师继续做
com.yaqi.a13waitandnotify.Desk.lock.notifyAll();
//修改桌子的状态
com.yaqi.a13waitandnotify.Desk.foodFlag = 0;
}
}
}
}
}
}
4.4、ThreadDemo.java
package com.yaqi.a13waitandnotify;
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();
}
}
5、阻塞队列方式(另一种等待唤醒机制)
5.1、阻塞队列的继承结构
5.2、阻塞队列实现等待唤醒机制
7、多线程的6中状态
八、综合练习
1、多线程练习1(卖电影票)
2、多线程练习2(送礼品)
3、多线程练习3(打印奇数数字)
4、多线程练习4(抢红包)
精确运算:(BigDecimal)