day14 多线程01

day14 多线程01

线程的构造

引例

执行路径的角度理解多线程:

  1. 一个线程代表一条代码的执行路径,所有的代码都运行在某一条执行路径中
  2. 在同一执行路径中运行的代码,会按照先后顺序依次执行
  3. 在不同执行路径中运行的代码,他们相互独立,互不干扰,同时执行

image-20220113094632040

为什么要有多线程?

  • 首先理解多进程(为了提高CPU的利用率)
  • 要理解多进程,首先理解什么是进程(进程就是一个执行的程序)

java运行一个Java程序的过程?

​ a. 其实java命令,它启动了一个jvm进程
​ b. 该jvm进程,在执行的时候,首先会创建一个线程,main线程
​ c. 在main线程中,运行主类中的main方法代码

jvm是单线程还是多线程?(多线程,因为至少还有一个线程在运行着垃圾回收器)

​ 一边创建数组,创建完后马上让它变成垃圾

线程的实现

Thread实现方式一:

​ 1.继承Thread
​ 2.重写子类的run方法
​ 3.创建该子类的对象
​ 4.启动线程 start()

注意事项:

  1. 一个Thread类(Thread子类)对象代表一个线程

  2. 为什么我们重写Thread类中的run方法
    ——> 只有Thread run()方法中的代码,才会执行在子线程中
    为了保证,子线程中运行的是我们想要在子线程中运行的代码

  3. 但是,如果想要让代码,在子线程中运行,并非一定,代码要写在run方法方法体中
    对于,定义在该Thread子类中,其他方法方法体中的代码,也可以运行在子线程,只要
    在run方法中调用它们即可

    换句话说,一个方法,被哪个线程中的代码调用,被调用的方法,就运行在,调用它的线程中

4,启动线程,必须使用start()方法来启动,这样才能是Thread中的run方法运行在子线程中
如果, 如果通过调用run方法,来执行Thread的run方法代码,这仅仅只是普通的方法调用

  1. 同一个Thread或Thread子类对象(代表同一个线程),只能被启动一次
    如果,我们要启动多个线程即 创建多个线程对象,并启动这些线程对象
线程的实现方式二:

​ 1.定义实现Runnable接口的子类
​ 2.实现Runnable接口的run方法
​ 3.创建该子类对象
​ 4.在创建Thread对象的时候,将创建好的Runnable子类对象作为初始化参数,传递给Thread对象
​ 5.启动Thread对象(启动线程)

 注意事项:
    1. 我们Runnable接口子类的run()方法代码,会运行在子线程当中。
    2. 所以,在线程的第二种实现方式中,我们自己定义子类,实现Runnable接口的run方法,
       将要在子线程中执行的代码,方法run()方法中
    3. 但是,Runnable子类对象,并不代表线程,它只代表,要在线程中执行的任务。

我们认为,从逻辑上说,第二种方法逻辑十分清晰:

  1. 线程就是一条执行路径,至于在线程这条执行路径上,究竟执行的是什么样的具体代码,
    应该和线程本身没有关系的
  2. 也就是说,线程,和在线程(执行路径)上执行的任务应该是没有什么直接关系的
  3. 线程实现的第二种方式,把线程(Thread对象代表线程) 和在 线程上执行的任务(Ruannable子类对象)分开

实现方法举例:

public class Demo3SecondType {

    public static void main(String[] args) {

        // 创建Runnable接口的子类对象(在子线程中执行的任务)
        MyRunnable myRunnable = new MyRunnable();

        // 创建Thread对象
        Thread thread = new Thread(myRunnable);

        // 启动线程
        thread.start();
    }
}

class MyRunnable implements Runnable {

    /*
        Runnable接口的run方法也会运行在子线程中
     */
    @Override
    public void run() {
        System.out.println("hello, thread");
    }
}

看源码的方法:就是抓住一条主线,看target到底在哪儿

Runnable源码解析:

//源码中Thread方法的构造方法解析:
public Thread(Runnable target) {
    init(null, target, "Thread-" + nextThreadNum(), 0);
}
//再看init方法中的target对象在哪儿:
private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
    //有关target的地方就一个:
	this.target = target;
}
//再看start方法,但是start方法中有一个start0()方法,是native的,不能看到底层源代码,所以不能再看了

image-20220114223732093

对比一下方式一和方式二:

方式二一 VS 方式二:
1. 方式一实现步骤较方式二少
2. 方式一的实现方式,存在单重继承的局限性
3. 方式二将线程和任务解耦(Runnable对象表示任务,Thread对象表示线程)
4. 方式二,便于多线程数据的共享

第2点的阐述:
逻辑层面,如果要实现对classA的代码复用,对方式1来说,要extend两个父类对象(thread和A),而对于方式二来说,就可以extend A implement Runnable,上代码:

public class Demo4Compare {
}

class A {
    public void method() {

    }
}
 //类的单重继承的限制
class ToolClass extends A implement Runnable {//可以继承类A的同时实现Runnable接口
    @Override
    public void run() {
        method();
    }
}

为什么方式二便于多线程数据共享?

在以下的案例中可以体现:

多线程仿真如下场景:
假设A电影院正在上映某电影,该电影有100张电影票可供出售,现在假设有3个窗口售票。
请设计程序模拟窗口售票的场景。

分析:
    1.3个窗口售票,互不影响,同时进行。
    2.3个窗口共同出售这100张电影票
public class Demo1 {

    public static void main(String[] args) {
        // 线程的第一种实现方式
        //first();

        // 代表一个具体的售票任务
        SalesTask salesTask = new SalesTask();

        Thread window1 = new Thread(salesTask, "窗口1");
        Thread window2 = new Thread(salesTask,"窗口2");
        Thread window3 = new Thread(salesTask,"窗口3");//三个窗口一同卖100张票

        window1.start();
        window2.start();
        window3.start();
    }

    private static void first() {
        WindowThread window1 = new WindowThread("窗口1");
        WindowThread window2 = new WindowThread("窗口2");
        WindowThread window3 = new WindowThread("窗口3");

        window1.start();
        window2.start();
        window3.start();
    }
}

/*
        线程的实现方式1模拟
 */
class WindowThread extends Thread {
    // 100张电影票
    static int tickets = 100;

    public WindowThread(String name) {
        super(name);
    }

    @Override
    public void run() {
        while (tickets > 0) {
            System.out.println(getName() + ": 售卖除了第" + this.tickets-- + "张票");
        }
    }
}

/*
       线程的第二种实现方式
 */
class SalesTask implements Runnable {


    // 100张电影票
    int tickets = 100;

    @Override
    public void run() {
        while (tickets > 0) {
            System.out.println(Thread.currentThread().getName() + ": 售卖除了第" + this.tickets-- + "张票");
        }
    }
}

图解:
image-20220114225344359

线程实现方式二就是下面的这个图,三个线程同时访问一个Runnable对象,而在方式一中,三个窗口分别卖出100张票

/*	
		售票过程中有延迟时间,用sleep方法模拟线程的售票延迟
      在增加售票延迟之后,发现了两个错误
        1. 相同的票售卖除了多次   多卖
          窗口2: 售卖除了第67张票
          窗口1: 售卖除了第67张票

        2. 售卖出了不存在的票  超卖
        窗口2: 售卖除了第1张票
        窗口1: 售卖除了第2张票
        窗口3: 售卖除了第0张票

        总结一下,以上问题发生的原因,都是因为一个窗口的售票过程中(一张票的售卖过程还没有结束),因为发生线程切换,导致又有另外一个线程也来售票,其实从本质上讲,不管多卖还是超卖问题,都属于多线程的数据安全问题:
        在多线程运行环境下,在多线程访问多线程共享的数据,访问到了错误的共享数据的值

 */
public class Demo1 {

    public static void main(String[] args) {
        SalesTask salesTask = new SalesTask();

        Thread window1 = new Thread(salesTask, "窗口1");
        Thread window2 = new Thread(salesTask, "窗口2");
        Thread window3 = new Thread(salesTask, "窗口3");

        window1.start();
        window2.start();
        window3.start();
    }
}

/*
       线程的第二种实现方式
 */
class SalesTask implements Runnable {


    // 100张电影票
    int tickets = 100;

    @Override
    public void run() {
        // 售卖出了不存在的票:
        //  假设,ticket的值是1
        //  窗口1线程: 判断有没有票, 1 > 0, 所以说明有票,窗口1准备售票, 此时发生线程切换
        //  窗口2线程:  判断有没有票, 1 > 0, 所以窗口2也认为有票,
        while (tickets > 0) {


            try {
                // 模拟售票的延迟时间
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            //  前提: tickets--  1. 读取tickets变量的值   2. 计算ticket变量值 - 1  3. 将2中计算的结果赋值给ticket变量
            // tickets 当前值 67
            // 窗口1线程:  计算输出参数, 执行字符串拼接 ——> "窗口1: 售卖除了第" +  67 ,计算ticket变量值 - 1,此时发生线程切换
            // 窗口2线程:  计算输出参数, 执行字符串的拼接 ——> "窗口1: 售卖除了第" + 67
            System.out.println(Thread.currentThread().getName() + ": 售卖除了第" + this.tickets-- + "张票");
        }
    }
}

如何解决上述的多卖和超卖的问题?

/*
         简单分析下,出现上述问题的原因:
            1. 多线程运行环境
            2. 数据共享
            3. 共享数据的非原子操作
                原子操作: 一组不可分割,要执行都执行,都不执行

          如何解决呢? 打破多线程的运行条件即可
          1. 多线程条件 打破不了 需求决定
          2. 数据共享 打破不了 需求决定
          3. 非原子操作  可以打破

          所以解决多线程数据安全问题 ——> 如何让一组操作变成原子操作的问题?
        思路1:阻止线程切换,这一组操作就变成了原子操作。
     	但是思路1实现不了: 抢占式线程调度,代码层面(线程中执行的代码),无法控制线程调度

        思路2:我们无法阻止线程切换,但是我们换个思路,我们给共享变量,加一把锁,利用锁来实现原子操作,使用锁,可以给共享变量加锁,从而保证:
              a. 只有加锁的线程,能够访问到共享变量
              b. 而且,在加锁线程,没有完成对共享共享变量的,一组操作之前,不会释放锁,
              c. 只要不释放锁.其他线程,即使被调度执行,也无法访问共享变量

        在java语言层面,我们就需要使用 synchronized代码块,实现原子操作
        Java中构建原子操作,最简单的方式——同步代码块
           synchronized(锁对象) {
                 需要同步的代码块(是指对共享数据访问的一组操作)
           }

   同步代码块的细节:
    a. synchronized代码块中的锁对象,可以是java语言中的任意对象(java语言中的任意一个对象,都可以充当锁的角色仅限于synchronized(代码块中):
        1)因为java中所有对象,内部都存在一个标志位,表示加锁和解锁的状态
        2)所以其实锁对象,就充当着锁的角色
        所谓的加锁解锁,其实就是设置随对象的标志位,来表示加锁解锁的状态。

    b. 任何一把锁,只能同时被一个线程加锁成功
       我们的代码都是在某一条执行路径(某一个线程中运行),当某个线程执行到同步代码块时,
        会尝试在当前线程中,对锁对象加锁
        1) 此时,如果锁对象处于未加锁状态,jvm就会设置锁对象的标志位(加锁),并在锁对象中记录,是哪个线程加的锁
            然后,让加锁成功的当前线程,执行同步代码块中的代码

        2) 此时,如果锁对象已经被加锁,且加锁线程不是当前线程,系统会让当前线程处于阻塞状态(等着),
            直到加锁线程,执行完了对共享变量的一组操作,并释放锁


    c. 加锁线程何时释放锁?
       当加锁线程,执行完了同步代码块中的代码(对共享变量的一组操作),在退出同步代码块之前,
       jvm自动清理锁对象的标志位,将锁对象变成未上锁状态(释放锁)






 */
public class Demo1 {

    public static void main(String[] args) {

        SalesTask salesTask = new SalesTask();

        Thread window1 = new Thread(salesTask, "窗口1");
        Thread window2 = new Thread(salesTask, "窗口2");
        Thread window3 = new Thread(salesTask, "窗口3");

        window1.start();
        window2.start();
        window3.start();
    }
}

/*
       线程的第二种实现方式
 */
class SalesTask implements Runnable {


    // 100张电影票
    int tickets = 100;
    // 锁对象
    private Object lockObj = new Object();

    @Override
    public void run() {
           // ticket = 1

            // 售卖100张票
            while (tickets > 0) {
                try {
                    // 模拟售票的延迟时间
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                synchronized (lockObj) {
                    if (tickets > 0) {
                        // double check
                        System.out.println(Thread.currentThread().getName() + ": 售卖除了第" + this.tickets-- + "张票");
                    }
                }

            }
        }
}

注意事项:

千万要注意:
a. 虽然,synchronized代码块,中的锁对象,可以是java语言中的任意对象
b. 但是,在多线程运行环境下,想要让访问 同一个共享变量的, 多个synchronized代码块中的代码是原子操作
注意,对同一个共享变量的访问,必须使用同一个锁对象

如何解决多线程的数据安全问题:
     通过加锁,实现线程同步,通过线程同步,解决多线程数据安全问题

很好理解,解决多线程数据安全问题,是通过加锁,构造一个线程对共享变量的原子操作。但其实,加锁是再完成线程同步
涉及两个概念:
1. 同步:你走我不走,我走你不走,所有加锁失败的线程,步调变的一致了,都需要等待锁对象被释放
2. 异步: 你走你的,我走我的,多线程天生异步,不同的线程,相互独立互不影响,各自按照,按照各自的执行步调,执行各自代码

所以,最终,其实我们线程同步,来解决多线程的数据安全问题的

线程同步的优缺点:
优点:解决了多线程线程安全问题。
缺点:相比于异步,因为等待锁资源而引发的阻塞,降低了程序运行效率。

将前面的售卖一张票的部分抽离出来,经过处理,形成了同步方法:

原代码:

synchronized (lockObj) {
                    if (tickets > 0) {
                        // double check
                        System.out.println(Thread.currentThread().getName() + ": 售卖除了第" + this.tickets-- + "张票");
                    }
                }

修改后,抽成一个同步方法:

// 普通成员同步方法
private synchronized void salesTicket() {
    if (tickets > 0) {
        // double check
        System.out.println(Thread.currentThread().getName() + ": 售卖除了第" + this.tickets-- + "张票");
    }
}

那么,同步代码块的锁对象是谁?

同步方法:  相当于将方法的方法体变成了一个大的 同步代码块
同步方法的锁对象是谁呢?
a. 普通成员方法, 其锁对象是隐式的,就是该方法运行时的当前对象 this
b. 静态方法,静态方法属于类,静态方法的锁对象就是静态方法所属类对应的Class对象

线程有关的API

1.设置或者获取线程的名称

public final String getName()			//获取线程的名称
public final void setName(String name)	//设置线程的名称
static Thread currentThread()
                  //返回对 当前正在执行的线程 对象的引用。
                  //当前正在执行的线程, 即调用currentThread()的调用

用法:

public class Demo1Name {

    public static void main(String[] args) {
        // getName和setName
        name();

        // 在main线程中调用currentThread()方法
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName()); 			//结果是main
    }

    private static void name() {
        // 创建Thread子类对象
        NameThread nameThread1 = new NameThread();

        // 获取线程名
        String name = nameThread1.getName();
//        System.out.println(name);           			//Thread-0

        //修改线程的名字
        nameThread1.setName("nameThread");
        //System.out.println(nameThread1.getName());	//结果为nameThread

//        NameThread nameThread2 = new NameThread();
//        System.out.println(nameThread2.getName());

        // 启动线程
        //nameThread1.start();
    }
}

class NameThread extends Thread {

    @Override
    public void run() {
        System.out.println(this.getName());
    }
}

2.多线程的优先级

public final int getPriority()
public final void setPriority(int priority)
Thread.MIN_PRIORITY = 1
Thread.NORM_PRIORITY = 5
Thread.MAX_PRIORITY = 10

注意事项:
1. 多线程的优先级的取值范围1 <= priority <=10
2. 线程的默认优先级为5
3. 然而……

然而,我们在java语言中设置的线程优先级,它仅仅只能被看做是一种"建议"(对操作系统的建议),实际上,操作系统本身,有它自己的一套线程优先级 (静态优先级 + 动态优先级)

java官方: 线程优先级并非完全没有用,我们Thread的优先级,它具有统计意义,总的来说,高优先级的线程占用的cpu执行时间多一点,低优先级线程,占用cpu执行时间,短一点

3.jvm采用的是哪种线程调度的模式?

线程调度主要有两种模式:

  1. 协同式线程调度(Cooperative Thread-Scheduling)(线程的执行时间,由线程本身来控制,线程将自己的工作执行完了以后,主动通知系统切换到另一个线程上去,好处是实现简单,坏处是线程执行时间不可控)
  2. 抢占式调度(preemptive Thread-Scheduling)(每个线程由系统来分配时间,线程的借还不由线程本身决定,最大的好处是线程的执行时间是可控的)

jvm采用的是抢占式调度

image-20220114214652505

4.让当前正在执行的线程休眠

public static native void sleep(long millis)
 1. 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)
 2. millis - 以毫秒为单位的休眠时间。

注意这种方法时用native定义的,即底层代码是由c或c++实现的

5.是线程1等待线程2终止

public final void join()
  等待该线程终止。
  谁等待?   当前线程——> 调用join方法的线程等待
  等待谁? 该线程 ——> 调用join方法的那个线程对象所代表的线程

实现的代码:

public static void main(String[] args) throws InterruptedException {
        JoinThread joinThread = new JoinThread();
        joinThread.start();

        // 调用join
        // main线程中调用join方法
        joinThread.join();

        System.out.println("main end");
    }
}


class JoinThread extends Thread {

    @Override
    public void run() {
        System.out.println("joinThread start ");
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("joinThread end ");
    }
}

运行结果:

image-20220114215456501

运行过程模拟:
image-20220114215544493

6.暂停自己的线程的方法

public static native void yield()//礼让线程
   a.暂停当前正在执行的线程对象
   b.并执行其他线程。

由于b是说执行其他线程,而这是CPU的事情(线程调度),所以yield方法只能做到暂停当前正在执行的线程对象。

由于线程的调度是杂乱无章的,调度的顺序不确定,所以不会确定的1执行一次,2执行一次,所以输出的结果也是不确定的。

7.守护线程

public final void setDaemon(boolean on)
                   true        false
1. 将该线程标记为 守护线程 或 用户线程(默认为用户线程)。
2. 当正在运行的线程都是守护线程时,Java虚拟机退出(java虚拟机终止守护线程)3. 该方法必须在启动线程前调用。

守护线程典型使用场景:
   垃圾回收期就运行在守护线程中
public class Demo4Daemon {
    public static void main(String[] args) {
        // main 线程本身是一个用户线程
        DaemonThread daemonThread = new DaemonThread();
        // 将线程设置为守护线程
        daemonThread.setDaemon(true);
        daemonThread.start();


        for (int i = 0; i < 10; i++) {
            System.out.println("main: i = " + i );
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        // main方法执行完毕,也就意味着main线程执行完毕
    }
}

class DaemonThread extends Thread {

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(getName() + ": i = " + i);
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

运行结果:

image-20220114220912258

守护线程为Thread,此线程在i不到100的时候终止运行了

8.interrupt方法(中断线程)

public void interrupt()
1. 中断线程。
2. 如果线程在调用 Object 类的 wait()wait(long)wait(long, int) 方法,或者该类(Thread)join()join(long)join(long, int)sleep(long)sleep(long, int)
   方法过程中受阻,它还将收到一个 InterruptedException。

interrupt方法,只会针对线程的阻塞状态,打断线程的阻塞状态

实现过程:

public class Demo5Interrupt {

    public static void main(String[] args) {

        InterruptThread interruptThread = new InterruptThread();
        interruptThread.start();

        // 在主线程中,打断子线程的阻塞(休眠)
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("准备打断子线程了");
        interruptThread.interrupt();

    }
}

class InterruptThread extends Thread {

    @Override
    public void run() {
        System.out.println("begin");

        // 休眠阻塞5秒
        try {
            Thread.sleep(5000);
            System.out.println("run");
            System.out.println("end");
        } catch (InterruptedException e) {
            System.out.println("被打断了休眠...");
        }


    }
}

运行结果:
image-20220114221909208

9.stop()方法,不安全

线程的几种状态转化

image-20220114222218914

也可以让阻塞状态的线程直接转化为结束状态,用interrupt方法

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值