Skip to content

day09【多线程】

今日内容介绍

java
进程概念
线程概念
线程的创建方式
线程名字设置获取
线程安全问题引发
同步代码块
同步方法
死锁

第一章 多线程的概念【理解】

1.1 并行和并发

java
并行: 同一时刻,多个程序同时执行。
并发: 同一时间段,多个程序交替执行。

1627952602060

1.2 进程和线程的介绍

java
进程:正在执行的一个软件/程序
   每个进程是独立的内存空间
   注意 这个内存空间的申请是非常消耗CPU资源的。

线程:进程中的子程序,进程中的一个执行路径。
   每个线程都有自己独立的执行空间---栈内存
   记住:每个线程都有独立栈空间。
        它的空间消耗 要远远小于进程的空间消耗。
什么叫多线程?
   每个进程至少一个线程。
   有多个线程的进程被称为多线程程序。
   好处
      在我们看来是不是同时执行多个程序。

1641435403286

1.3 线程调度的原理

java
我们程序的执行顺序,不取决与程序员,取决于CPU的调度。
注意 在我们肉眼凡胎看来,是同时执行的没有顺序的。但是微观的角度有,我们看不到。
Cpu调度
   分时调度: 为每个线程分配相同的执行时间。

   抢占式调度---没有任何规律可言--也就是在java程序中 多线程程序,多次执行效果不一样。
        CPU优先执行优先级别高的程序,级别相同的随机执行。
        bug  这个优先级高 跟  北京 车 摇号。
        一千万分之一  一千万分之2

 总结 java中的线程调度方式是 抢占式的,就代表多线程程序每次程序执行 执行效果都可能不一样。

1.4 主线程

java
我们以前的程序入口 都是 main方法,当我们 运行程序的时候,计算机会为当前程序申请一个 内存空间(进程产生。)
    我们程序的代码 都在入口main方法中,故而 我们当前的执行路径,被称为主线程。
     主线程是一块独立的栈空间,如果在主线程中开辟新的线程,那么JVM虚拟机就会创建一个新的栈空间出来。

1641436045351

第二章 线程的创建-继承方式

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);
        }
    }
}

1641437694027

1641438239134

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 设置线程名字获取线程名字的方法

1641438673182

1641438691734

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();//又开启一个新的线程
    }
}

1641441115874

1641441208204

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);
        }
    }
}

1641441875886

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 模拟 电影院买票案例

1641452826721

步骤提炼

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 出现了问题 --

1641453989476

3.2.1 重复票 问题

1641455777984

java
重复票的原因
    窗口3卖完票6 --  5.
    窗口1 窗口2 窗口3 抢夺cpu
    窗口1 窗口2 抢到的时候  票5张
    窗口2 开始卖票  在它卖完之后,打印完,即将要进行--
    突然 又被 窗口1抢过去  窗口1 卖票 5
    窗口1  5--   4
    窗口2  5--   4

    cpu资源在执行某个线程执行某段代码的时候,还没执行完就被其他线程抢夺过去了。
    造成了 重复票问题。

1641456542283

3.2.2 负数票问题

1641455762349

java
负数票原因
     3个线程进入判断的时候 ticket都是1,ke可以进来。
     但是 在进行输出的时候ticket已经发生改变。
     输出的都是改变后的值,0和负数了。

1641457034986

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--;
        }
    }
}

1641460323276

第四章 死锁 (了解,以后避免出现就行)

4.1 死锁案例

1641460653249

筷子 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();
    }
}

1641461795547

第五章 线程的状态

1641461852126

在 java 程序中的线程状态。线程可以处于下列状态之一:


线程状态导致状态发生条件
NEW(新建)线程刚被创建,但是并未启动。还没调用 start 方法。
Runnable(可运行)线程可以在 java 虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器。
Blocked(锁阻塞)当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入 Blocked 状态;当该线程持有锁时,该线程将变成 Runnable 状态。
Waiting(无限等待)一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入 Waiting 状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用 notify 或者 notifyAll 方法才能够唤醒。
Timed Waiting(计时等待)同 waiting 状态,有几个方法有超时参数,调用他们将进入 Timed Waiting 状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有 Thread.sleep 、Object.wait。
Teminated(被终止)因为 run 方法正常退出而死亡,或者因为没有捕获的异常终止了 run 方法而死亡。

Released under the MIT License.