• 从零开始学习Java多线程(一)


    1. 什么是进程?

           对其概念需要自行goole,简单理解就是:进程是计算机系统进行资源分配和调度的基本单位,是正在运行程序的实体;每一个进程都有它自己的内存空间和系统资源;进程是线程的容器。如:打开IDEA写代码是一个进程,打开有道词典也是一个独立的进程。

           如果我们在用IDEA写代码的同时打开有道词典那就是多进程,多进程具有独立性,动态性,并发性,异步性。鉴于多数人混淆并行和并发,在此简单介绍:

    • 并发:多个CPU实例同时执行一段代码或处理逻辑,具有物理意义上的同时发生。
    • 并行:计算机通过算法调度获得CPU时间片继而执行属于自己的执行计划,CPU的高效切换在转瞬间完成,让用户感觉像是同时发生,实际上只是逻辑上的同时发生。              

          那么IDEA和有道词典是同时进行的吗?取决于CPU的个数,单个CPU在某个时间点上只能做一件事情,而多核(多个CPU)可以同时进行。多进程的意义在于,提高了CPU使用率。值得一提的是,Java是不能够通过调用系统资源来开启一个进程的,例如在windows系统中,Java通过调用C语言底层代码来开启进程。

    2. 什么是线程?

          线程:是进程中的单个顺序控制流,计算机最小的执行单元,一条执行路径。一个进程如果只有一条执行路径,成为单线程程序;如果有多条执行路径,则成为多线程程序;多线程共享该进程的全部资源。如:打开QQ后,好友聊天属于一条线程,浏览QQ空间又属于一条线程。

         假如我们的计算机只有一个CPU,那么CPU在某一个时刻只能执行一条指令,线程只有得到CPU时间片才能拥有使用权,才可以执行指令,那么Java是如何对线程进行调用的呢?

          线程调用的两种模型:

    1.  分时调度模型 : 所有的线程轮流获得CPU的使用权,平均分配每个线程占用CPU
    2.  抢占式调度模型:优先让优先级高的线程使用CPU,如果优先级相同,那么会从中随机选取一个,优先级高的线程获取的CPU时间片相对多一些。
    3. Java使用的是抢占式调度模型。
    4. 可利用API设置和获取线程优先级。 

      public final int getPriority()

      public final void setPriority(int newPriority)

          现在大致了解进程和线程之间的关系后,再来看Java程序运行原理。

                Java命令会启动Java虚拟机,启动JVM,等于启动了一个进程。该进程会自动启动一个"主线程",然后主线程去调用某个类的main方法,所有main方法运行在主线程中,在此之前的所有程序都是单线程的。Java虚拟机的启动是多线程的,因为JVM启动至少启动了垃圾回收线程和主线程。

    3. 多线程的意义

          进程具有独立性,多进程之间是没有共享资源的,但是多线程可以共享内存资源,而且十分简单。系统创建进程是需要为该进程重新分配系统资源,浪费了大量资源,但创建线程的代价要小很多,因此多线程实现多任务的并发要比多进程的效率高。

        总结起来:

    1.  共享内存资源
    2.  并发效率高
    3.  多线程的作用不是提高执行速度,而是提高应用程序的使用率   

        而多线程的实际应用包括:

      •  浏览器必须能同时下载多张图片
      • 一台服务器必须能同时响应多个用户请求
      • JVM本身就在后台提高了一个超级线程进行垃圾回收 

     4. Java多线程实现

        (一)继承Thread类,复写run()方法

     1 /**
     2  * @author supiaol
     3  * @date 2019/3/7
     4  * @time 9:26
     5  */
     6 public class MyThread extends Thread {
     7 
     8     //多线程运行的代码块
     9     public void run() {
    10         System.out.println("Thread is running");
    11     }
    12 
    13     public static void main(String[] args) {
    14 
    15         MyThread myThread1 = new MyThread();
    16         MyThread myThread2 = new MyThread();
    17 
    18         //运行多线程
    19         myThread1.start();
    20         myThread2.start();
    21 
    22     }
    23 }

           Thread类本质上是实现Runable接口的一个实例。Thread 类中有一些关键属性,如:name属性代表线程的名称,可以通过Thread类的构造器中参数来指定线程名称;priority属性代表线程优先级,上文提高优先级高的线程抢占CPU时间可能性越大,默认优先级为5,最小值为1,最大值为10;daemon属性表示线程是否是守护线程,target属性代表要执行的任务。

           下面是Thread类中常用的api:

            1. run()方法   新建线程(新建状态)

                需要明确的是run()方法不是用来运行线程的,也不需要用户调用,当线程获得CPU执行时间,会进入run()方法执行代码块。

            2. start()方法    启动线程(就绪状态)

                线程启动的方法,调用start()方法后,系统会开启一个新的线程用来执行用户定义的任务,在此过程中,为线程分配系统资源。需要注意的是,调用start()方法后,并不会立即执行定义的任务,而是赋予线程可以抢占CPU时间片的资格,只有得到CPU时间片才能执行计划任务。

            3. sleep()方法   睡眠线程(堵塞状态)

                线程睡眠,必须指定睡眠时间,在适当的位置调用sleep(),让该线程睡眠,也就是交出CPU,让CPU来执行其它任务。特别需要关注的是,sleep()方法不会释放锁或者监视器,也就是说如果当前线程持有某个对象的锁,那么即使调用sleep()方法,其他线程也无法访问该对象,关于该方法和锁的关系会在后续详细说明和演示。

             4.yield()方法    礼让线程(就绪状态)

                调用yield()方法同样可以让该线程交出CPU时间片,失去执行权,类似于sleep()方法,同样不会释放锁对象或者监视器,而区别之处在于,yield()不能控制具体交出CPU的时间,而且交出的CPU时间片只能允许相同优先级的线程获取,该进程返回到就绪状态而不是堵塞状态。

             5.join()方法      线程加入(堵塞状态)

                join方法有三个重载版本:

    join()
    join(long millis)     //参数为毫秒
    join(long millis,int nanoseconds)    //第一参数为毫秒,第二个参数为纳秒

            它们的区别在于指定的参数,假如我们在main()所属的主线程中调用另外一个从线程thread.join()方法,则main()方法失去执行权,只有等到thread线程执行完毕或者等待一定的时间后重新获得执行权。如何调用无参join()方法,需要等待thread线程执行完毕,调用指定时间的带参join()方法,则等到指定时间过后获取执行权。

            通过查看源码发现,join实际上调用了wait()方法实现主线程等待,至于wait()方法,后面学习线程安全时候着重讲述,在此先做了解。

     1 public final synchronized void join(long millis)
     2     throws InterruptedException {
     3         long base = System.currentTimeMillis();
     4         long now = 0;
     5 
     6         if (millis < 0) {
     7             throw new IllegalArgumentException("timeout value is negative");
     8         }
     9         //空参join 需要等待从线程执行完毕
    10         if (millis == 0) {
    11             while (isAlive()) {
    12                 wait(0);
    13             }
    14         } else {
    15             //带参join,等待指定时间后重新获得执行权
    16             while (isAlive()) {
    17                 long delay = millis - now;
    18                 if (delay <= 0) {
    19                     break;
    20                 }
    21                 wait(delay);
    22                 now = System.currentTimeMillis() - base;
    23             }
    24         }
    25     }

               6. interrupt()  线程中断(堵塞状态)

                   顾名思义,interrupt即中断的意思。调用interrupt()方法能够使处于堵塞状态的线程抛出异常,其实质上就是用来中断处于堵塞状态的线程,通常配合isInterrupted()方法来停止正常运行的线程。

               7. stop()方法 线程停止(线程中断)

                  stop方法是一个已经被废弃的方法,自身不安全。因为调用stop方法会直接终止run方法的调用,并且抛出ThreadDeath异常,如果该线程调用stop方法之前持有某个对象锁,之后会完全释放锁对象,导致对象状态不一致。

               8.destory() 方法  已被废弃,不会用到。

        (二)  实现Runnable接口,重写run()方法。

     1 /**
     2  * @author supiaol
     3  * @date 2019/3/7
     4  * @time 14:49
     5  */
     6 public class MyThread extends OtherClass implements Runnable {
     7     @Override
     8     public void run() {
     9         System.out.println(Thread.currentThread().getName() + "is running");
    10     }
    11 
    12     public static void main(String[] args) {
    13         MyThread myThread = new MyThread();
    14         Thread thread1 = new Thread(myThread);
    15         Thread thread2 = new Thread(myThread);
    16         thread1.setName("线程1:");
    17         thread2.setName("线程2:");
    18         thread1.start();
    19         thread2.start();
    20     }
    21 }

            实现Runnable接口实现多现成的好处就在于弥补Java单继承的缺陷。更适合多个相同程序的代码去处理一个资源的情况,这样线程同程序的代码,数据有效分离,较好的体现了面向对象的设计思想。区别于继承Thread类启动线程,实现Runnable接口启动线程时,需要将实现Runnable接口的实例作为target目标传入Thread实例,然后调用start()方法启动线程。

            如果需要对线程设置名称,可以通过线程对象调用setName方法进行设置,也可以通过Thread的构造方法设置,而getName()方法可以获取线程名称,也可以通过Thread.currentThread().getName()方法获取当前线程的名称。

           (三) 基于线程池实现多线程,用到不多,在此不多介绍

    5. 线程的生命周期

    1.    新建:创建线程对象,从new一个线程对象到调用start()方法之间都是新建状态
    2.    就绪:调用start()方法后,线程对象已经启动,但是还没有获取到CPU的执行权
    3.    运行:获取到CPU时间片,开始执行run()方法中的代码
    4.    堵塞:失去执行权,回到就绪状态。
    5.    结束:代码运行完毕,或者main方法执行完毕,线程消亡

        以上就是一个线程完整的生命周期,一个线程最基本的生命周期包括:新建,就绪,运行,结束。

       

      

  • 相关阅读:
    SQL练习题28:创建一个actor表,包含如下列信息(注:sqlite获取系统默认时间是datetime('now','localtime'))
    SQL练习题27:你能使用子查询的方式找出属于Action分类的所有电影对应的title,description吗
    SQL练习题26: 将employees表的所有员工的last_name和first_name拼接起来作为Name,中间以一个空格区分 (注:该数据库系统是sqllite,字符串拼接为 || 符号,不支持concat函数)
    SQL练习题25:使用join查询方式找出没有分类的电影id以及名称
    SQL练习题24:给出每个员工每年薪水涨幅超过5000的员工编号emp_no、薪水变更开始日期from_date以及薪水涨幅值salary_growth,并按照salary_growth逆序排列。 (数据保证每个员工的每条薪水记录to_date-from_date=1年,而且同一员工的下一条薪水记录from_data=上一条薪水记录的to_data)
    SQL练习题23:汇总各个部门当前员工的title类型的分配数目,即结果给出部门编号dept_no、dept_name、其部门下所有的当前(dept_emp.to_date = '9999-01-01')员工的当前(titles.to_date = '9999-01-01')title以及该类型title对应的数目count
    SQL练习题23:获取员工其当前的薪水比其manager当前薪水还高的相关信息,当前表示to_date='9999-01-01', 结果第一列给出员工的emp_no, 第二列给出其manager的manager_no, 第三列给出该员工当前的薪水emp_salary, 第四列给该员工对应的manager当前的薪水manager_salary
    SQL练习题22:获取所有非manager员工当前的薪水情况,给出dept_no、emp_no以及salary ,当前表示to_date='9999-01-01'
    SQL练习题22:查找所有员工自入职以来的薪水涨幅情况,给出员工编号emp_no以及其对应的薪水涨幅growth,并按照growth进行升序
    SQL练习题21:查找当前薪水(to_date='9999-01-01')排名第二多的员工编号emp_no、薪水salary、last_name以及first_name,你可以不使用order by完成吗
  • 原文地址:https://www.cnblogs.com/supiaol/p/10482757.html
Copyright © 2020-2023  润新知