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

1. 什么是进程?

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

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

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

      那么QQ和网易云是同时进行的吗?取决于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.  多线程的作用不是提高执行速度,而是提高应用程序的使用率   

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

    •  浏览器必须能同时下载多个图片
    • 一台服务器必须n能同时响应多个用户请求
    • 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("Runnable realize multithreading");
10     }
11 
12     public static void main(String[] args) {
13         MyThread myThread1 = new MyThread();
14         MyThread myThread2 = new MyThread();
15 
16         new Thread(myThread1).start();
17         new Thread(myThread2).start();
18 
19     }
20 }

        实现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方法执行完毕,线程消亡

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

   

  

猜你喜欢

转载自www.cnblogs.com/supiaol/p/10482757.html