本次课程内容
- 什么是多线程 为什么要使用多线程
- 多线程的两种实现方式
- 线程的常用方式
- 线程安全问题产生的原因
- 使用线程解决安全问题
- 线程的死锁
详细内容
什么是多线程 为什么使用多线程
什么情况下需要使用多线程 :
能否提高资源的利用率
多线程在java 底层怎么实现的
多线程在Java虚拟机底层是靠时间片的快速轮询来实现的
就是在jvm虚拟机底层 实际上你可以认为只有一个 进程在工作。只不过时间片切换的比较快,给你造成一种假象
好像是多个线程同时工作的
总结:
线程是进程的进一步划分,线程是为了解决 进程开销大,内存不能共享的问题
线程是程序级别的 要不要 使用多线程 判别的标准:能否提供资源的利用率
多线程是怎么实现的? 通过时间片快速轮询 来实现,实际上在底层 (jvm底层)不可能多个线程同时工作
cpu核心 进程 线程 (线程 跟 cpu 核心 没有直接的关联 逻辑上的进程)
多线程创建的两种方式
使用继承Thread类的方式
线程 :Thread
举例 :模拟三个窗口卖票 每个窗口买自己的票,这三个窗口同时卖
package com.aaa.thread;
/**
* @Author Lst
* @Date 2020/12/29 13:08
* @Version 1.0
* 多线程实现的第一种方式
* 继承 Thread类
*/
public class Test01 extends Thread {
// 窗口名称
private String name;
// 每个线程 模拟一个窗口,每个窗口都有票的资源
private Integer tickets = 3;
public Test01(String name) {
this.name = name;
}
/**
* 执行线程的方法
*/
@Override
public void run() {
for (Integer i = 0; i < tickets; i++) {
System.out.println(name+"卖出了第 "+(i+1)+" 张票");
}
super.run();
}
public static void main(String[] args) {
// 创建一个窗口的对象
Test01 test01 = new Test01("窗口1");
Test01 test02 = new Test01("窗口2");
Test01 test03 = new Test01("窗口3");
// 启动上面的三个线程, 开始卖票
// 启动线程只能而且只有一个方法就是线程类的start方法
test01.start();
test02.start();
test03.start();
}
}
使用实现Runnable接口的方式
举例 :模拟三个窗口卖票 每个窗口买自己的票,这三个窗口同时卖
package com.aaa.thread;
/**
* @Author Lst
* @Date 2020/12/29 13:50
* @Version 1.0
* 多线程创建的第二种方式
* 使用 Runnable接口
*/
public class Test02 implements Runnable {
// 窗口名称
private String name;
// 每个线程 模拟一个窗口,每个窗口都有票的资源
private Integer tickets = 3;
public Test02(String name) {
this.name = name;
}
public void run() {
for (Integer i = 0; i < tickets; i++) {
System.out.println(name+"卖出了第 "+(i+1)+" 张票");
}
}
public static void main(String[] args) {
// 创建多个窗口 需要先创建Runnable接口的实现类对象
Test02 test01 = new Test02("窗口1");
Thread thread01 = new Thread(test01);
Thread thread02 = new Thread(new Test02("窗口2"));
Thread thread03 = new Thread(new Test02("窗口3"));
// 启用线程
thread01.start();
thread02.start();
thread03.start();
}
}
两种实现方式 优缺点 : 第一种方式继承Thread类的方式使用起来跟简单,但是实际开发中一般还是推荐使用第二种方式,实现Runnable接口的方式 原因
- java 是单继承,一个类如果继承了 Thread类就没办法在继承其他类的
- 使用Runnable接口的方式可以方便的实现多个线程操作同一个资源。
举例:刚才需求都是每个窗口卖自己的票,但是实际生活中,一般都是多个窗口买同一批票。
package com.aaa.thread;
/**
* @Author Lst
* @Date 2020/12/29 14:09
* @Version 1.0
* 多个线程共享同一资源
* 多个窗口卖同一批票
*/
public class Test03 implements Runnable {
// 票的资源
private Integer tickets = 3;
public void run() {
for (Integer i = 0; i < 3; i++) {
if (tickets > 0) {
// 获取当前正在执行的线程名称
Thread thread = Thread.currentThread();
// 在获取线程名称
String name = thread.getName();
System.out.println(name+" 卖出了第"+(tickets)+"张票");
tickets--;
}
}
}
public static void main(String[] args) {
// 先创建票的资源
Test03 test03 = new Test03();
// 创建多个窗口(线程)让这多个窗口都操作test03这一个资源
Thread thread01 = new Thread(test03,"窗口1");
Thread thread02 = new Thread(test03,"窗口2");
Thread thread03 = new Thread(test03,"窗口3");
// 启动三个线程
thread01.start();
thread02.start();
thread03.start();
}
}
现在已经实现了多个窗口卖同一票,但是有可能多个窗口买同一张票的问题
线程的常用的方法
方法名 | 说明 |
---|---|
Thread.currentThread() | 获取当前正在运行的线程 |
getName() | 获取线程的名称 |
Thread.sleep(min) | 让当前正在运行的线程进入休眠状态,参数为休眠时间,单位是毫秒 |
setPriority(int) | 设置线程运行的优先级,优先级是一个整数 最小值 1,最大值是 10,默认值是 5优先级高的线程 有大的几率可能先执行但是不保证。设置线程优先级必须在线程启动之前设置才有效 |
yield() | 线程礼让的方法,对一个线程调用 yield()方法,当前获取时间片的线程,会放弃执行权,然后所有的线程再重新竞争一次时间片,谁获取到时间片谁执行。但是注意放弃时间片的线程,有可能再次抢到时间片 |
wait() | Object类中提供的线程方法,让当前线程处于等待状态 |
分析线程安全问题产生的原因
解决线程安全问题,还可以使用java并发编程中 ReentrantLock类,
// 声明可重入锁对象
private ReentrantLock reentrantlock = new ReentrantLock();
// 为代码加锁
reentrantLock.lock();
// 为代码解锁
reentrantLock.unlock();
// 如果出异常的 不解锁 就死锁了 为了能让出现异常也能 最终 执行 这个解锁代码 使用 try{}catch(){}finally{}
死锁
解决线程同步问题的时候,如果出现了嵌套的不同步代码块,有可能会出现死锁的问题。
生活中的死锁的例子 : 爸爸对儿子说,如果给我成绩单 ,我就给你玩具。儿子对爸爸说,如果给我玩具,我就给你成绩单。
Father
package com.aaa.deadlock;
/**
* @Author Lst
* @Date 2021/1/1 14:30
* @Version 1.0
* 父亲的类
*/
public class Father {
/**
* 爸爸发言了
*/
public void say() {
System.out.println("给我成绩单,就给你玩具");
}
/**
* 爸爸得到成绩单的方法
*/
public void get() {
System.out.println("爸爸得到了成绩单");
}
}
Son
package com.aaa.deadlock;
/**
* @Author Lst
* @Date 2021/1/1 14:28
* @Version 1.0
* 创建儿子的类
*/
public class Son {
/**
* 儿子发言了
*/
public void say() {
System.out.println("给我玩具,就给你成绩单");
}
/**
* 儿子得到玩具的方法
*/
public void get() {
System.out.println("儿子得到了玩具");
}
}
TestDead
package com.aaa.deadlock;
/**
* @Author Lst
* @Date 2021/1/1 14:32
* @Version 1.0
* 测试死锁的代码
*/
public class TestDead implements Runnable {
private Son son;
private Father father;
private boolean flag;
public TestDead(Son son, Father father, boolean flag) {
this.son = son;
this.father = father;
this.flag = flag;
}
public void run() {
if (flag) {
synchronized (father) {
son.say();
synchronized (son) {
son.get();
}
}
}else {
synchronized (son) {
father.say();
synchronized (father) {
father.get();
}
}
}
}
public static void main(String[] args) {
// 创建儿子和父亲的对象
Father father = new Father();
Son son = new Son();
// 创建测试死锁的对象
TestDead dead01 = new TestDead(son,father,true);
TestDead dead02 = new TestDead(son,father,false);
Thread thread01 = new Thread(dead01);
Thread thread02 = new Thread(dead02);
// 启动上面的两个线程
thread01.start();
thread02.start();
}
}
线程的声明周期
线程的声明周期 五中状态
- 创建状态 new 方法 会让线程对象处于创建状态
- 就绪状态 调用 线程类的 strat()方法 启用线程
- 执行状态 获取到时间片 会让线程进入运行状态
- 死亡状态(消亡)线程运行结束
- 阻塞状态 sleep() , wait() 方法 都会让线程进入阻塞状态 解除后进入 就绪状态
线程总结
-
什么时候使用 多线程 标准 :能否提高资源利用率
现在在jvm底层怎么运行的? 通过时间片的快熟切换来运行的,一个时刻只有一个线程在运行。
-
线程的实现的两种方法 继承 Thread类,实现Runnable接口
推荐使用Runnable接口的方式,原因两个 : 1 java 单继承 2 实现 Runnable接口 可以方便的实现多线程对单资源的共享
-
线程安全问题产生的原因 : 多个线程修改同一资源
-
解决线程安全问题 : 三个方式 同步代码块,同步方法,重入锁
同步的代码 越少越好 因为同步也是 耗费资源的
-
线程的死锁 : 死锁产生的原因 同步代码块的嵌套使用,有可能出现线程的问题
-
线程的声明周期 :创建状态 就绪状态 运行状态 堵塞状态 死亡状态