• 详解 线程


    在讲解线程之前,本人要先来讲解下进程
    因为线程是依赖于进程存在的

    那么,什么是进程呢?

    进程就是正在运行的程序
    是系统进行资源分配和调用独立单位
    每一个进程都有它自己的内存空间系统资源

    可能通过上述的讲解,同学们有这种疑惑:
    线程是依赖进程存在的,那么,进程完全可以处理线程能做到的事,为什么还要存在线程呢?
    答曰:

    对于单核计算机来讲,同一时刻,CPU只能运行一个线程/进程。 但是 进程是只能一个进程运行完才运行下一个进程,
    而多个线程可以同时竞争CPU,也就是我们常说的“多线程” 那么,就拿游戏来举个例:游戏进程 和 音乐进程 就会冲突。
    这时,多线程就体现出了它的作用: 多线程使得 游戏进程和音乐进程 间做着频繁切换,且切换速度很快。
    所以,我们感觉游戏和音乐在同时进行,其实并不是同时执行的



    线程

    ====

    基本概念:

    首先,本人来介绍下什么是线程

    概述

    在一个进程内部又可以执行多个任务,而这每一个任务我们就可以看成是一个线程。
    线程 是 程序使用CPU的基本单位
    所以,进程拥有资源的基本单位线程CPU调度的基本单位


    在上文中,本人提到了多线程这个名词。
    现在,本人来总结下多线程的意义

    多线程的意义

    多线程的作用不是提高执行速度,而是为了提高应用程序的使用率

    那么我们怎么来理解这个问题呢?
    解释

    我们程序在运行的使用,都是在抢CPU的时间片(执行权)
    如果是多线程的程序,那么在抢到CPU的执行权的概率应该比较单线程程序抢到的概率要大
    那么也就是说:
    CPU在 多线程程序 中 执行的时间 要比 单线程 多
    所以就提高了程序的使用率
    但是即使是多线程程序,那么他们中的哪个线程能抢占到CPU的资源呢?
    这个是不确定的,所以多线程具有随机性


    现在,本人来介绍下 线程的五种状态

    线程的五种状态:

    1. 创建态
      用new运算符和Thread类或其子类建立一个线程对象后,该线程对象就处于创建态。处于创建态的线程有自己的内存空间,通过调用start()方法进入就绪态。

    2. 就绪态
      处于就绪态的线程已经具备了运行条件,但是还没有分配到CPU,因而将进入线程就绪队列,等待系统为其分配CPU。一旦获得CPU,线程就进入运行状态并自动调用自己的run()方法。

    3. 运行态
      在运行态的线程执行自己的run()方法中的代码,直到调用其它方法而终止,或等待某资源而阻塞,或完成任务而销毁。

    4. 阻塞态
      处于运行状态的线程在某些情况下,如执行了sleep()方法,或等待I/O设备等资源,让出CPU并暂时终止自己的运行,进入阻塞态。
      在阻塞状态的线程不能进入就绪队列。只有当引起阻塞的原因消除时,如睡眠时间已到,或等待的I/O设备空闲下来,线程便转入就绪态,重新到就绪队列中排队等待CPU。当再次获得CPU时,便从原来中止位置开始继续运行。

    5. 销毁态:
      销毁态是线程生命周期中的最后一个阶段。线程销毁的原因有两个:

      1. 是正常运行的线程完成了它的全部工作;
      2. 是线程被强行终止运行,如通过stop()或destroy()方法来终止一个线程。

    下图是这五种状态的转换关系
    在这里插入图片描述那么,对于上图,本人来做以下几点说明

    • 进程创建后并不是立刻进入运行态,而实现进入就绪态,在就绪态的进程队列中,与其他进程一起竞争CPU
    • 只有处于就绪态的进程才有资格竞争CPU
    • 就绪态的进程,除了CPU以外,不需要等待其他计算机资源
    • 处于阻塞态的进程,只能由处于运行态的进程唤醒
    • 阻塞态进程被唤醒后进入就绪态,与就绪态队列内的进程一起竞争CPU
    • 一般地,有几个CPU,就能同时运行几个进程
    • 进程是在运行态时,将自己阻塞起来,使自己进入阻塞态的

    那么,现在,本人再来介绍两个名词 —— 并行并发

    并行 与 并发:

    并行

    指应用能够 同时执行不同的任务
    在这里插入图片描述
    例:吃饭的时候,边吃饭边打电话

    并发

    指应用能够交替执行不同的任务
    其实并发有点类似于多线程的原理(多线程并非是如果你开两个线程同时执行多个任务)
    在这里插入图片描述
    例:吃饭的时候,吃一口饭喝一口水

    那么,现在,本人再来扩展一个知识点:

    Java程序运行原理
    Java命令会启动java虚拟机,
    启动JVM,等于启动了一个应用程序,也就是启动了一个进程。
    该进程会自动启动一个 “主线程” ,然后主线程去调用该类的 main 方法
    所以:main方法运行在主线程中
    并且,JVM启动至少启动了垃圾回收线程主线程,所以是多线程的。

    在本人上面的讲解中,本人已经提到了:
    线程只有得到 CPU时间片,也就是使用权,才可以执行指令。
    那么Java是如何对线程进行调用的呢?

    线程有两种调度模型:

    • 分时调度模型
      所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片
    • 抢占式调度模型
      优先让优先级高的线程使用 CPU,如果线程的优先级相同,
      那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些。

    Java使用的是抢占式调度模型


    现在,本人来介绍下 多线程程序实现的方式

    多线程程序实现的方式:

    方式1 —— 继承Thread类:

    本人先来展示下这种实现方式:

    首先本人给出一个 继承了Thread类 的自定义线程类

    package edu.youzg.about_thread.core;
    
    public class MyThread extends Thread {
    
        @Override
        public void run() {
            for (int i = 0; i < 4; i++) {
                //获取线程的名称
                Thread thread = Thread.currentThread();
                String name = thread.getName();
                System.out.println(name + ":" + i);
            }
        }
    
    }
    

    现在,本人来给出一个测试类

    package edu.youzg.about_thread.core;
    
    public class Test {
        
        public static void main(String[] args) {
            new MyThread().start();
        }
        
    }
    

    现在,本人来展示下运行结果
    在这里插入图片描述
    那么,可能同学们对上面的展示有如下疑问

    1. 启动线程使用的是那个方法?

    答曰:start()方法

    1. run()和start()方法的区别 是什么?

    答曰:
    start()方法,使该线程开始执行;
    然后Java 虚拟机运行run()方法 里的代码。、

    1. 为什么要重写run方法?

    答曰:
    run()方法中封装应该是必须被线程执行的代码

    1. 同一个线程能不能多次启动?

    答曰:
    不能。
    会报 IllegalThreadStateException


    那么,现在,本人来介绍下第二种实现方式:

    方式2 —— 实现Runnable接口:

    本人先来展示下这种实现方式:

    首先,本人来给出一个 实现了 Runnable 接口的类

    package edu.youzg.about_thread.core;
    
    public class MyRunnable implements Runnable{
     
        @Override
        public void run() {
            for (int i = 0; i < 4; i++) {
                //获取线程的名称
                Thread thread = Thread.currentThread();
                String name = thread.getName();
                System.out.println(name + "线程的第" + i + "次输出");
            }
        }
    
    }
    

    现在,本人来给出一个测试类

    package edu.youzg.about_thread.core;
    
    public class Test {
    
        public static void main(String[] args) {
            new MyRunnable().run();
        }
    
    }
    

    现在,本人来展示下运行结果
    在这里插入图片描述

    优势

    这种方式扩展性强 实现一个接口 还可以再去继承其他类
    可以避免由于Java单继承带来的局限性


    那么,现在,本人来介绍下第三种实现方式:

    方式3 —— 实现 Callable 接口:

    首先,本人来给出一个 实现了 Callable 接口的类

    package edu.youzg.about_thread.core;
    
    import java.util.concurrent.Callable;
    
    public class MyCallable implements Callable<Integer>  {
    
        @Override
        public Integer call() throws Exception {
            return 666;
        }
    
    }
    

    现在,本人来给出一个测试类

    package edu.youzg.about_thread.core;
    
    import java.util.concurrent.ExecutionException;
    import java.util.concurrent.FutureTask;
    
    public class Test {
    
        public static void main(String[] args) throws ExecutionException, InterruptedException {
            MyCallable myCallable = new MyCallable();
            FutureTask<Integer> task = new FutureTask<>(myCallable);
            Thread thread = new Thread(task);
            thread.start();
            //线程执行完之后,可以获取结果
            Integer integer = task.get();
            System.out.println(integer);
        }
    
    }
    

    现在,本人来展示下运行结果
    在这里插入图片描述
    本人现在来讲解下 第三种实现方式的条件

    条件

    1. 调用get()方法,需要抛出 ExecutionException, InterruptedException 异常;
    2. 执行 Callable 方式,需要 FutureTask 实现类的支持,用于接收运算结果。

    其实第三种实现方式,是第二种实现方式的变形,具体理由如下:

    Callable接口 是 Runnable接口 的 子接口
    实现了该接口,相当于变相地实现了 Runnable接口

    现在本人来讲解下这种实现方式的优势

    优势

    1. 这种方式扩展性强 实现一个接口 还可以再去继承其他类,可以避免由于Java单继承带来的局限性
    2. 实现线程的同时,可以有返回值

    现在,本人来介绍下 我们处理多线程问题 时所要调用的方法:

    常用方法:

    Thread类基本获取和设置方法

    • public final String getName():
      获取线程名称
    • public final void setName(String name):
      设置线程名称

    展示

    package edu.youzg.about_thread.core;
    
    public class Test {
    
        public static void main(String[] args) {
            MyThread myThread = new MyThread();
            System.out.println(myThread.getName());
            myThread.setName("右转哥的展示线程");
            System.out.println(myThread.getName());
        }
    
    }
    

    运行结果
    在这里插入图片描述


    设置和获取线程优先级

    • public final int getPriority():
      获取线程的优先级
    • public final void setPriority(int newPriority):
      设置线程的优先级

    展示

    package edu.youzg.about_thread.core;
    
    public class Test {
    
        public static void main(String[] args) {
            MyThread myThread1 = new MyThread();
            MyThread myThread2 = new MyThread();
            myThread1.setName("1号嘉宾");
            myThread2.setName("2号嘉宾");
            Thread.currentThread().setName("男主角");
            myThread1.setPriority(Thread.MAX_PRIORITY);
            myThread2.setPriority(Thread.MIN_PRIORITY);
            Thread.currentThread().setPriority(Thread.NORM_PRIORITY);
            System.out.println(myThread1.getName()+ "优先级为:" + myThread1.getPriority());
            System.out.println(myThread2.getName() + "优先级为:" + myThread2.getPriority());
            System.out.println(Thread.currentThread().getName() + "优先级为:" + Thread.currentThread().getPriority());
            myThread1.start();
            myThread2.start();
            System.out.println("男主角的代码1");
            System.out.println("男主角的代码2");
            System.out.println("男主角的代码3");
        }
    
    }
    

    运行结果
    在这里插入图片描述

    现在,本人来对这两个方法可能会出现的问题做下讲解:

    1. 有的时候我们给线程设置了指定的优先级,
      但是该线程并不是按照优先级高的线程执行,那是为什么呢?

    因为线程的优先级的大小仅仅表示这个线程被CPU执行的概率增大了.
    但是我们都知道多线程具有随机性,
    所以有的时候一两次的运行说明不了问题

    1. 为什么主线程能够比所有线程都先运行

    主线程就在CPU
    所以,若是我们不对其他线程调用join()方法
    即使主线程优先级最低,也会 大几率 优先执行完主线程


    线程休眠:

    • public static void sleep(long millis) :
      线程休眠

    展示

    package edu.youzg.about_thread.core;
    
    import java.util.concurrent.ExecutionException;
    
    public class Test {
    
        public static void main(String[] args) throws InterruptedException, ExecutionException {
            long startTime = System.currentTimeMillis();
            Thread.sleep(1000);
            long endTime = System.currentTimeMillis();
            System.out.println("休眠了" + ((endTime - startTime) / 1000) + "秒");
            System.out.println("之后的代码");
        }
    
    }
    

    运行结果
    在这里插入图片描述


    加入线程:

    • public final void join():
      等待该线程执行完毕了以后,其他线程才能再次执行
      (注:该方法需 待线程启动之后,才能调用方法)

    展示

    package edu.youzg.about_thread.core;
    
    public class Test {
    
        public static void main(String[] args) throws InterruptedException {
            MyThread th1 = new MyThread();
            MyThread th2 = new MyThread();
            MyThread th3 = new MyThread();
            //也是说现在,三个线程是并发执行
            //join()在线程开启之后,去调用
            th1.setName("大傻");
            th1.start();
            th1.join();
            th2.setName("二傻");
            th2.start();
            th2.join();
            th3.setName("三傻");
            th3.start();
            th3.join();
            //串行:多个线程按顺序执行
        }
    
    }
    

    运行结果

    在这里插入图片描述


    礼让线程:

    • public static void yield():
      暂停当前正在执行的线程对象,并执行其他线程

    展示

    package edu.youzg.about_thread.core;
    
    public class Test extends Thread {
    
        public Test() {
            super();
        }
    
        @Override
        public void run() {
            for (int i = 0; i <= 4; i++) {
                System.out.println("" + this.getName() + "-----" + i);
                // 当i为2时,该线程就会把CPU时间让掉,让其他或者自己的线程执行(也就是谁先抢到谁执行)
                if (i == 2) {
                    this.yield();
                }
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            Test th1 = new Test();
            Test th2 = new Test();
            th1.setName("孔融");
            th2.setName("孔融他弟");
            th1.start();
            th2.start();
        }
    
    }
    

    运行结果
    在这里插入图片描述
    上面的例子看起来貌似并没有礼让,反而有种更加争取CPU的样子了,这是为什么呢?

    答曰:
    这个礼让是要暂停当前正在执行的线程,并不会释放锁
    这个暂停的时间是相当短的,
    如果在这个线程暂停完毕以后,其他的线程还没有抢占到CPU的执行权,
    那么这个时候这个线程应该再次和其他线程抢占CPU的执行权


    中断线程

    • public final void stop():
      停止线程的运行
    • public void interrupt():
      当线程调用wait(),sleep(long time)方法的时候处于阻塞状态,
      可以通过这个方法清除阻塞(只能是wait()、sleep()造成的阻塞)

    展示

    package edu.youzg.about_thread.core;
    
    public class Test2 extends Thread {
    
        public static void main(String[] args) throws InterruptedException {
            MyThread th1 = new MyThread();
            th1.setName("测试线程");
            th1.start();
            Thread.sleep(1000);
            //th1.stop(); //强行停止线程
            th1.interrupt();//打断线程的一个阻塞状态
        }
    
    }
    

    运行结果
    在这里插入图片描述


    守护线程:

    • public final void setDaemon(boolean on):
      将该线程标记为 守护线程用户线程 (被守护的线程)
      当正在运行的线程都是守护线程时,Java 虚拟机退出

    展示

    package edu.youzg.about_thread.core;
    
    public class Test{
    
        public static void main(String[] args) {
            //主线程也称之为用户线程
            Thread.currentThread().setName("刘备");
            MyThread th1 = new MyThread();
            th1.setName("张飞");
            MyThread th2 = new MyThread();
            th2.setName("关羽");
            //在线程开启之前,可以设为守护线程
            th1.setDaemon(true);
            th2.setDaemon(true);
            th1.start();
            th2.start();
            System.out.println("刘备亡了");
        }
    
    }
    

    运行结果

    在这里插入图片描述


    那么,现在,本人就来讲解下 有关这个 用户线程 和 守护线程 的知识点吧:

    用户线程 和 守护线程:

    首先,本人来讲解下这两个知识点的异同吧:

    相同点
    用户线程和守护线程都是线程
    不同点
    Java虚拟机所有用户线程dead后,程序就会结束
    不管是否还有守护线程还在运行
    守护线程还在运行,则会马上结束
    很好理解,守护线程是用来辅助用户线程的,
    如公司的保安和员工,各司其职,当员工都离开后,保安自然下班了。

    现在,本人来讲解下这两种线程的适用场景

    适用场景

    由两者的区别及dead时间点可知:
    守护线程不适合用于输入输出或计算等操作
    因为用户线程执行完毕,程序就dead了。
    守护线程适用于辅助用户线程的场景
    JVM的垃圾回收内存管理都是守护线程
    还有就是在做数据库应用的时候,使用的数据库连接池,连接池本身也包含着很多后台线程,监听连接个数、超时时间、状态等。

    那么,本人再来讲解下 如何创建守护线程

    调用线程对象的方法setDaemon(true),设置线程为守护线程。
    条件

    1. thread.setDaemon(true)必须在thread.start()之前设置
    2. Daemon线程中产生的新线程也是Daemon的。
    3. 不是所有的应用都可以分配给Daemon线程来进行服务
      (比如读写操作或者计算逻辑)
      因为Daemon Thread还没来得及进行操作,虚拟机可能已经退出了。

    还有最后一点本人要强调一下:
    可能会有同学学习过Linux,本人在这里要说明一点:

    Java中的守护线程 与 Linux中的守护线程不是一个概念
    Linux守护进程是后台服务进程没有控制台
    Windows中,你可以运行javaw来达到释放控制台的目的
    Unix下你加&在命令的最后就行了
    所以守护进程并非一定需要的


    那么,在最后,本人来通过一个例子来总结下我们这篇博文所学的内容:

    题:
    一部由大导演右转哥拍的电影《右转哥大战Bug星人》正在热映
    但是,只有一家影院被授权可以播放这部电影。
    于是,这家影院为了防止观众太多,导致卖票环节太过于耗时,就开放了三个售票窗口
    请用代码实现下卖票过程。

    要求:
    创建3个线程,分别处理一个静态资源的分配。

    那么,本人现在来展示下代码:

    首先,本人来创建个售票线程

    package edu.youzg.about_thread.core;
    
    public class SellTicketsThread extends Thread {
        static int piao = 100;
        @Override
        public void run() {
    
            while (piao != 0){
                if(piao > 0){
                    String name = this.getName();
                    System.out.println(name+"正在出售"+(piao--)+"张票");
                }
            }
        }
    
    }
    

    现在,本人来给出一个测试类

           SellTicketsThread th1 = new SellTicketsThread();
           SellTicketsThread th2 = new SellTicketsThread();
           SellTicketsThread th3 = new SellTicketsThread();
           th1.setName("窗口1");
           th2.setName("窗口2");
           th3.setName("窗口3");
           th1.start();
           th2.start();
           th3.start();
    

    现在,本人来展示下运行结果:
    在这里插入图片描述

    可以看到,出现了出售同一张票的错误。
    那么,若是我们多次运行,还会出现这样的错误:
    在这里插入图片描述

    这是为什么呢?

    本人现在来通过几张图来解释下:

    起初,当售票线程都还没有跑起来的时候:
    在这里插入图片描述
    然后,假设th1争取到了CPU:
    在这里插入图片描述
    这时,假设th2争取到了CPU:
    在这里插入图片描述
    由于num--并不是原子操作,而是由对num的读、改、写 三步 组成的,
    这时,假设th1完成了num--的操作,而th2还没完成:
    在这里插入图片描述
    这时,假设th3获得了CPU,读取到的num是99,然后将num--的操作也完成了:
    在这里插入图片描述
    这时,th2又争取到了CPU,然后,就会将num=99传回主存:
    在这里插入图片描述
    这就是上述两个问题的出现的原因。


    那么,对于上述问题,我们有一个专门 的名称来形容 —— 线程安全问题

    线程安全问题:

    首先,本人来讲解下线程安全问题的出现原因

    出现原因:

    • 多线程环境
    • 有共享数据
    • 有多条语句操作共享数据

    那么,我们该如何处理这种问题呢?

    把多个语句操作共享数据的代码给锁起来
    任意时刻只能有一个线程执行即可


    那么,线程安全问题该如何解决呢
    请观看本人博文 —— 《详解 锁》

    (本人 《详解 多线程》 博文 链接:https:////www.cnblogs.com/codderYouzg/p/12418935.html

  • 相关阅读:
    apache配置文件参数优化
    apache 虚拟主机详细配置:http.conf配置详解
    Apache安装问题:configure: error: APR not found . Please read the documentation
    lamp安装
    Linux运维常用命令总结
    mysql主从日志的定期清理
    python写的分析mysql binlog日志工具
    mysql5.6主从参数详解
    京东MySQL监控之Zabbix优化、自动化
    CentOS 6.5 生产环境编译安装LNMP
  • 原文地址:https://www.cnblogs.com/codderYouzg/p/12418954.html
Copyright © 2020-2023  润新知