day09【多线程】
今日内容介绍
java
进程概念
线程概念
线程的创建方式
线程名字设置获取
线程安全问题引发
同步代码块
同步方法
死锁
第一章 多线程的概念【理解】
1.1 并行和并发
java
并行: 同一时刻,多个程序同时执行。
并发: 同一时间段,多个程序交替执行。
1.2 进程和线程的介绍
java
进程:正在执行的一个软件/程序
每个进程是独立的内存空间
注意 这个内存空间的申请是非常消耗CPU资源的。
线程:进程中的子程序,进程中的一个执行路径。
每个线程都有自己独立的执行空间---栈内存
记住:每个线程都有独立栈空间。
它的空间消耗 要远远小于进程的空间消耗。
什么叫多线程?
每个进程至少一个线程。
有多个线程的进程被称为多线程程序。
好处
在我们看来是不是同时执行多个程序。
1.3 线程调度的原理
java
我们程序的执行顺序,不取决与程序员,取决于CPU的调度。
注意 在我们肉眼凡胎看来,是同时执行的没有顺序的。但是微观的角度有,我们看不到。
Cpu调度
分时调度: 为每个线程分配相同的执行时间。
抢占式调度---没有任何规律可言--也就是在java程序中 多线程程序,多次执行效果不一样。
CPU优先执行优先级别高的程序,级别相同的随机执行。
bug 这个优先级高 跟 北京 车 摇号。
一千万分之一 一千万分之2
总结 java中的线程调度方式是 抢占式的,就代表多线程程序每次程序执行 执行效果都可能不一样。
1.4 主线程
java
我们以前的程序入口 都是 main方法,当我们 运行程序的时候,计算机会为当前程序申请一个 内存空间(进程产生。)
我们程序的代码 都在入口main方法中,故而 我们当前的执行路径,被称为主线程。
主线程是一块独立的栈空间,如果在主线程中开辟新的线程,那么JVM虚拟机就会创建一个新的栈空间出来。
第二章 线程的创建-继承方式
2.1 创建线程的第一种方式
java
java.lang.Thread类: 代表线程类
线程 是程序中的执行线程。Java 虚拟机允许应用程序并发地运行多个执行线程。
创建线程的第一种方式: 继承Thread类
1:定义一个类继承Thread,
2:子类重写run方法.作用 就是线程要执行什么代码。※
3:在 main方法中,主线程代码中 创建一个子类对象。
4:调用start方法开启一个新的线程。
注意:
1:start是用来开启线程的。run方法是一个普通方法。
调用run方法就相当于调用一个普通方法,不会产生新的线程。
调用start方法 就相当于开启一个新的线程,开启之后立刻调用run方法。
2:start方法调用执行会很快。瞬间调用,瞬间结束。
呼叫JVM申请一个新的栈空间用于给新的线程操作。
栈空间调用run功能。
3:多线程程序执行具备随机性。
java
package com.itheima.thread01;
//1:定义一个类继承Thread类
public class MyThread extends Thread{
//重写run方法 该线程要执行的代码
@Override
public void run() {
for (int i = 0; i <100 ; i++) {
System.out.println("MyThread..."+i);
}
}
}
java
package com.itheima.thread01;
public class Demo02Thread {
public static void main(String[] args) {
System.out.println("当前是main线程 ");
System.out.println("在main线程中创建一个 新的线程对象 ");
// 创建线程对象
MyThread myThread = new MyThread();//main线程中new的。
// 开启线程?
myThread.start();//开启线程的意思~!!!
//main线程也执行 输出语句
for (int i = 0; i <100 ; i++) {
System.out.println("main线程"+i);
}
}
}
2.2 多线程运行原理
2.2.1 Thread 类源码 分析
java
思考
既然Thread有strat功能,为什么我们不直接使用Thread对象调用start()方法,而采用子类对象呢?
Thread thread = new Thread();
thread.start();
从代码上可以开启,但是开启之后执行啥?
没有效果,原因是start()开启新的线程了,是不是thread类中run没有代码?
/* What will be run. */ 执行的目标代码是什么
private Runnable target;
Runnable是一个接口,代表目标执行的代码
@Override
public void run() {
if (target != null) {//有没有目标
target.run();//有目标执行目标的run
}
}
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}
private void init(ThreadGroup g, Runnable target, String name,
long stackSize) {
init(g, target, name, stackSize, null, true);
}
没有目标 run方法啥也不干。
所以为甚么不直接创建Thread空参对象 调用 strat开启线程。
没有意义,因为啥也没有执行。
为什么通过子类对象调用
因为子类对象,重写了run方法。
重写就不判断目标了,直接执行我写好的线程代码。
2.2.2 设置线程名字获取线程名字的方法
2.2.3 在 main 中开启两个线程 试试 效果
java
package com.itheima.thread02;
public class SubThread02 extends Thread {
@Override
public void run() {
for (int i = 0; i <1000 ; i++) {
//获取线程名称
String name = getName();
System.out.println("当前线程名称"+name+"输出的索引"+i);
}
}
}
java
package com.itheima.thread02;
public class Demo01Thread {
public static void main(String[] args) {
//创建两个线程对象
SubThread02 t1 = new SubThread02();
SubThread02 t2 = new SubThread02();
//线程具有默认名字 Thread-0 Thread-1
//设置名字
t1.setName("小强");
t2.setName("旺财");
t1.start();//开启一个新的线程
t2.start();//又开启一个新的线程
}
}
2.3 创建线程的第二种方式
根据 API 我们提炼出 创建线程第二种方式步骤
java
1:定义一个类实现Runnable接口。
2: 重写run方法。
3: 创建实现类对象。
4: 创建Thread对象,并把我们的实现类对象当成参数传递。
5: 调用start方法开启线程
先写一个 自定义类 实现了 Runnabel 接口
java
package com.itheima.thread03;
// 线程任务类 自定义类实现线程任务接口
public class MyRunnable implements Runnable {
//run 代表线程要执行代码
@Override
public void run() {
for (int i = 0; i <1000 ; i++) {
// 获取当前线程对象 谁来执行这个代码 线程对象是谁
Thread thread = Thread.currentThread();//获取当前正在执行的线程对象
System.out.println("当前线程名称:"+thread.getName()+"...."+i);
}
}
}
重写了 run 方法--线程任务方法
在测试类中 main 线程中 创建一个 线程任务对象
创建 Thread 对象,并把我们的实现类对象当成参数传递。
线程对象调用了 start 方法
java
package com.itheima.thread03;
public class Demo {
public static void main(String[] args) {
//获取当前线程对象 当前是main线程
String name = Thread.currentThread().getName();
System.out.println("当前是一个单线程,线程对象是:"+name);
//创建线程任务对象
MyRunnable task = new MyRunnable();
//创建Thread对象 并传递我的线程任务
Thread t1 = new Thread(task);
t1.start();
for (int i = 0; i <100 ; i++) {
System.out.println(name+"..."+i);
}
}
}
2.3.1 源码看--看调用的流程(是 OK)
java
MyRunnable task = new MyRunnable();--是Runnabel的实现类对象,重写run方法
@Override
public void run() {
for (int i = 0; i <1000 ; i++) {
// 获取当前线程对象 谁来执行这个代码 线程对象是谁
Thread thread = Thread.currentThread();//获取当前正在执行的线程对象
System.out.println("当前线程名称:"+thread.getName()+"...."+i);
}
}
private Runnable target;//new MyRunnable()
Thread t1 = new Thread(task);
public Thread(Runnable target) {// new MyRunnable()
init(null, target, "Thread-" + nextThreadNum(), 0);
}
private void init(ThreadGroup g, Runnable target, String name,
long stackSize) {
init(g, target, name, stackSize, null, true);
}
.... this.target = target;
start() JVM开启新的线程 运行 run方法
@Override
public void run() {;
if (target != null) {//new MyRunnable()
target.run();
}
}
2.4 Thread 创建方式 和 Runnable 实现方式对比
java
既然可以继承形式,开启线程 为什么还出现了实现接口方式呢?
实现方式有如下几个好处:
1:避免java类单继承的局限性。
2:降低了 耦合度。
线程对象 和 线程任务的 耦合度。
第一种方式 MyThread是线程对象,同时也是线程任务对象。
第二种方式 Thread线程对象,线程任务对象是MyRunnable。
专业的人干专业的事。
3:可实现多个线程共享一个任务。
2.5 匿名内部类方式创建线程
java
package com.itheima.thread03;
public class Demo02 {
public static void main(String[] args) {
//能不能 不在外面创建类的情况下 在当前的main线程中 开启新的线程呢?
/*
匿名内部类
本质:实现一个接口或者继承某个类的子类对象。
格式:
new 父接口/父类(){
重写方法
}
这种对象没有名字。
*/
// 第一种方式 创建线程--匿名内部类形式
new Thread(){
//重写run方法
public void run(){
for (int i = 0; i <1000 ; i++) {
//Thread.currentThread() 获取正在执行的线程对象 谁执行这段代码 这个对象是谁
//当前线程名称
System.out.println(Thread.currentThread().getName()+"。。。"+i);
}
}
}.start();
// 第二种 创建线程方式 --实现方式
Runnable runnable = new Runnable(){
@Override
public void run() {
for (int i = 0; i <1000 ; i++) {
//Thread.currentThread() 获取正在执行的线程对象 谁执行这段代码 这个对象是谁
//当前线程名称
System.out.println(Thread.currentThread().getName()+"。。。"+i);
}
}
};//这个格式整体 new MyRunnable()
new Thread(runnable).start();
}
}
2.6 Thread 里面的 sleep 静态方法
咱们现在代码比较简单,cpu 不到一秒就可能执行完了。
public static void sleep(long 毫秒)
让你当前的线程 睡一会儿
增长你这块代码执行实现。
java
package com.itheima.thread03;
public class Demo04 {
public static void main(String[] args) throws InterruptedException {
for (int i = 10; i >=1 ; i--) {
Thread.sleep(1000);//休息一秒再继续
System.out.println(i);
}
System.out.println("发射!!!咻~~咻~~咻~~咻~~");
}
}
第三章 线程安全问题 及解决
3.1 模拟 电影院买票案例
步骤提炼
java
1:定义线程任务类--实现Runnable接口
2:重写run方法
实现 买票操作
怎么买票?
定义一个票数的成员变量。ticket =100
run方法
判断 票数 是否大于0
大于0 有票就卖
加一个出票时间再卖
打印票号,然后票-- 票号-1
3:在测试类main线程中,创建线程任务对象--卖票任务。
4: 创建三个线程对象表示三个窗口,实施卖票操作。
new Thread(线程任务)
调用start()开始卖票
线程任务代码 实现卖票操作
java
package com.itheima.ticket01;
//卖票任务类 实现 runnable
public class SellTicketTask implements Runnable {
//总票数 --因为三个窗口要共同的卖 100张
private int ticket = 100;
@Override
public void run() {
//为了方便模拟 我们假设 窗口开启不关闭
while(true){
//有票卖票
if(ticket>0){//代表有票
//模拟 出票的时间 --为了线程能有更好的效果
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//先获取 当前线程对象的名称
String chuangkou = Thread.currentThread().getName();
System.out.println(chuangkou+"正在售卖第:"+ticket+"号票");
//卖完这个号 做--
ticket--;
}
}
}
}
线程测试代码
java
package com.itheima.ticket01;
public class Demo {
public static void main(String[] args) {
//创建卖票任务类对象
SellTicketTask task = new SellTicketTask();
//创建三个窗口 实施卖票任务
Thread t1 = new Thread(task,"窗口1");
Thread t2 = new Thread(task,"窗口2");
Thread t3 = new Thread(task,"窗口3");
//开放窗口卖票
t1.start();
t2.start();
t3.start();
}
}
3.2 出现了问题 --
3.2.1 重复票 问题
java
重复票的原因
窗口3卖完票6 -- 5.
窗口1 窗口2 窗口3 抢夺cpu
窗口1 窗口2 抢到的时候 票5张
窗口2 开始卖票 在它卖完之后,打印完,即将要进行--
突然 又被 窗口1抢过去 窗口1 卖票 5
窗口1 5-- 4
窗口2 5-- 4
cpu资源在执行某个线程执行某段代码的时候,还没执行完就被其他线程抢夺过去了。
造成了 重复票问题。
3.2.2 负数票问题
java
负数票原因
3个线程进入判断的时候 ticket都是1,ke可以进来。
但是 在进行输出的时候ticket已经发生改变。
输出的都是改变后的值,0和负数了。
3.2.3 以上问题统称为 多个线程中数据不同步---线程不安全
数据不同步问题,也可以称为多个线程数据并发修改问题。
出现问题的前提
java
1:该程序必须是多线程程序。
2:多个线程一定要共享相同数据。
3:多个线程对数据有修改的操作(写)。
解决 多线程不安全问题(多个线程数据不同步)
java
我们把 会出现安全问题代码 锁起来,只允许一个线程进行执行,其他线程在外面等着,
当某一个线程执行完了,其他线程才允许争夺cpu,这样我们就保证在某一时刻,
修改 数据的代码 只有 一个线程在执行,这样就保证另外多个线程的数据同步。
保持着 某一时刻只有 一个线程在修改,修改完数据同步完了。
其他线程拿到的也是最新的数据。
3.3 解决线程不同步的方法 --- 同步机制解决
什么 是同步机制呢?
java
就是通过一套写到方案 来保证 多个线程在操作同一资源的时候,某一时刻只有一个线程在执行。
这就完成了数据的同步。
方案由以下三种
1:同步代码块机制
2:同步方法机制
3:Lock锁机制。明天讲
3.4 同步代码块解决 线程不安全问题
java
格式:
synchronized(锁对象){
可能出现线程不安全的代码
}
注意:
1:操作共享数据的代码要放在{}中。
2:锁对象--任意对象都行,专业术语叫对象监视器。
3:锁对象是任何类型对象都行,但是多个操作线程 它们必须得设置成同一把锁才能保证线程安全性。
因为这多个线程,执行的代码使用同一把锁,才可以实现 一个执行其他等待。
java
package com.itheima.ticket02;
//卖票任务类 实现 runnable
public class SellTicketTask implements Runnable {
//总票数 --因为三个窗口要共同的卖 100张
private int ticket = 100;
//设置锁对象
Object lock = new Object();
@Override
public void run() {//三个线程都会调用run方法 new Object() new 三次 不是相同锁 就无法保证一个时间段内只允许一个线程执行
//为了方便模拟 我们假设 窗口开启不关闭
while(true){
synchronized (lock){//保证对象的唯一性 保证new一次
//有票卖票
if(ticket>0){//代表有票
//模拟 出票的时间 --为了线程能有更好的效果
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//先获取 当前线程对象的名称
String chuangkou = Thread.currentThread().getName();
System.out.println(chuangkou+"正在售卖第:"+ticket+"号票");
//卖完这个号 做--
ticket--;
}
}
}
}
}
线程安全的话,效率一定是低的。
3.5 同步方法解决线程不安全问题
java
格式
修饰符 synchronized 返回值类型 方法名(参数...){}
public synchronized void sell(){
线程有安全问题的代码
}
同步方法是同步代码块 另外一种化身。
你知道 当前的同步锁对象是谁吗?
this
同步方法不用设置同步锁 默认是this
静态同步方法
给静态方法加上 synchronized 关键字
修饰符 synchronized static 返回值类型 方法名(参数...){}
它也是 同步代码块的化身
它的同步锁是什么?
类名.class 就是类本身 因为类文件是唯一的(反射的时候重点说)
java
package com.itheima.ticket03;
//卖票任务类 实现 runnable
public class SellTicketTask implements Runnable {
//总票数 --因为三个窗口要共同的卖 100张
private int ticket = 100;
@Override
public void run() {
//为了方便模拟 我们假设 窗口开启不关闭
while(true){
// synchronized (this){//this代表 当前线程任务对象
// sell();
// }
sell();
}
}
public synchronized void sell(){
//有票卖票
if(ticket>0){//代表有票
//模拟 出票的时间 --为了线程能有更好的效果
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//先获取 当前线程对象的名称
String chuangkou = Thread.currentThread().getName();
System.out.println(chuangkou+"正在售卖第:"+ticket+"号票");
//卖完这个号 做--
ticket--;
}
}
}
第四章 死锁 (了解,以后避免出现就行)
4.1 死锁案例
筷子 A 筷子 B 可以理解为 两个锁对象,萝莉拿到筷子 A 在拿到筷子 B 才能吃上大盘鸡。
只要任何一个人 拿到 A 和 B 都能吃。
还有可能 一人一根 筷子 A 被一个人抢到了 筷子 B 被另一个人抢到了。
java
死锁指的是两个或者两个以上的线程在执行过程中,由于竞争锁出现了一种阻塞的现象。
如无外力作用,两个线程都停不下来,也没执行代码,形成假死状态,所以这种现象称为死锁。
演示:
java
package com.itheima.dieLock;
public class KuaiZi {
//定义两个锁对象 为了方便使用 我设置为静态
public static final KuaiZi LOCK_A = new KuaiZi();//筷子A
public static final KuaiZi LOCK_B = new KuaiZi();//筷子B
}
java
package com.itheima.dieLock;
import javax.jws.soap.SOAPBinding;
import java.util.Random;
public class ChiJi implements Runnable{
int rd = new Random().nextInt(2);//0 1
// 先抢到A 在抢到B 能吃 先抢到B 在抢到A
@Override
public void run() {
String name = Thread.currentThread().getName();
while(true){
if(rd==0){//先抢筷子A 再抢筷子B
synchronized (KuaiZi.LOCK_A){
System.out.println(name+"抢到了筷子A");
//出现了锁嵌套
synchronized (KuaiZi.LOCK_B){
System.out.println(name+"抢到了筷子B");
System.out.println(name+"大口吃肉");
}
}
}else{
synchronized (KuaiZi.LOCK_B){
System.out.println(name+"抢到了筷子B");
//出现了锁嵌套
synchronized (KuaiZi.LOCK_A){
System.out.println(name+"抢到了筷子A");
System.out.println(name+"大口吃鸡");
}
}
}
rd = new Random().nextInt(2);
}
}
}
java
package com.itheima.dieLock;
public class Demo {
public static void main(String[] args) {
/*
模拟 两个人吃鸡
两个人两个线程
都是吃鸡任务
*/
//创建吃鸡任务对象
ChiJi cj = new ChiJi();
//创建两个线程对象
Thread t1 = new Thread(cj,"萝莉");
Thread t2 = new Thread(cj,"楚人美");
t1.start();
t2.start();
}
}
第五章 线程的状态
在 java 程序中的线程状态。线程可以处于下列状态之一:
线程状态 | 导致状态发生条件 |
---|---|
NEW(新建) | 线程刚被创建,但是并未启动。还没调用 start 方法。 |
Runnable(可运行) | 线程可以在 java 虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器。 |
Blocked(锁阻塞) | 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入 Blocked 状态;当该线程持有锁时,该线程将变成 Runnable 状态。 |
Waiting(无限等待) | 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入 Waiting 状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用 notify 或者 notifyAll 方法才能够唤醒。 |
Timed Waiting(计时等待) | 同 waiting 状态,有几个方法有超时参数,调用他们将进入 Timed Waiting 状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有 Thread.sleep 、Object.wait。 |
Teminated(被终止) | 因为 run 方法正常退出而死亡,或者因为没有捕获的异常终止了 run 方法而死亡。 |