zoukankan      html  css  js  c++  java
  • JavaSE学习笔记(三十一)—— 多线程(上)

    一、多线程概述

    1.1 多线程引入

      如果程序只有一条执行路径,那么该程序就是一个单线程程序。

      如果程序有多条执行路径,那么该程序就是一个多线程程序。举例:扫雷程序、迅雷下载。

    1.2 进程以及多进程的意义

      要想了解多线程,必须先了解线程,而要想了解线程,必须先了解进程,因为线程是依赖于进程而存在。 

    【什么是进程】

      通过任务管理器我们就看到了进程的存在。而通过观察,我们发现只有运行的程序才会出现进程。所以:
      进程就是正在运行的程序。
      进程是系统进行资源分配和调用的独立单位。每一个进程都有它自己的内存空间和系统资源。

    【多进程有什么意义】

      单进程的计算机只能做一件事情,而我们现在的计算机都可以做多件事情。
        举例:一边玩游戏(游戏进程),一边听音乐(音乐进程)。
      也就是说现在的计算机都是支持多进程的,可以在一个时间段内执行多个任务。并且呢,可以提高CPU的使用率

    【问题】
      一边玩游戏,一边听音乐是同时进行的吗?
      不是。因为单CPU在某一个时间点上只能做一件事情。而我们在玩游戏,或者听音乐的时候,是CPU在做着程序间的高效切换让我们觉得是同时进行的。

    1.3 线程以及多线程的意义

    【什么是线程】

      在同一个进程内又可以执行多个任务,而这每一个任务我就可以看成是一个线程。
      线程:是程序的执行单元,执行路径。是程序使用CPU的最基本单位。
      单线程:如果程序只有一条执行路径。
      多线程:如果程序有多条执行路径。

    【多线程有什么意义】

      多线程的存在,不是提高程序的执行速度。其实是为了提高应用程序的使用率
      程序的执行其实都是在抢CPU的资源,CPU的执行权。
      多个进程是在抢这个资源,而其中的某一个进程如果执行路径比较多,就会有更高的几率抢到CPU的执行权。我们是不敢保证哪一个线程能够在哪个时刻抢到,所以线程的执行有随机性

      多线程会给我们产生一个错觉:让我们认为多个线程是并发执行的。其实不是。
      因为多个线程共享同一个进程的资源(堆内存和方法区),但是栈内存是独立的,一个线程一个栈。所以他们仍然是在抢CPU的资源执行。一个时间点上只有能有一个线程执行。而且谁抢到,这个不一定,所以,造成了线程运行的随机性。

    1.4 并发和并行

      前者是逻辑上同时发生,指在某一个时间内同时运行多个程序。

      后者是物理上同时发生,指在某一个时间点同时运行多个程序。

      那么,我们能不能实现真正意义上的并发呢,是可以的,多个CPU就可以实现,不过你得知道如何调度和控制它们。

    1.5 Java程序的运行原理

      java 命令会启动 java 虚拟机,启动 JVM,等于启动了一个应用程序,也就是启动了一个进程。该进程会自动启动一个 “主线程” ,然后主线程去调用某个类的 main 方法。所以 main方法运行在主线程中。在此之前的所有程序都是单线程的。

    【思考】

      jvm虚拟机的启动是单线程的还是多线程的?

      多线程的。原因是垃圾回收线程也要先启动,否则很容易会出现内存溢出。现在的垃圾回收线程加上前面的主线程,最低启动了两个线程,所以,jvm的启动其实是多线程的。

    二、多线程的实现方案

      由于线程是依赖进程而存在的,所以我们应该先创建一个进程出来。而进程是由系统创建的,所以我们应该去调用系统功能创建一个进程。Java是不能直接调用系统功能的,所以,我们没有办法直接实现多线程程序。但是呢?Java可以去调用C/C++写好的程序来实现多线程程序。由C/C++去调用系统功能创建进程,然后由Java去调用这样的东西,然后提供一些类供我们使用。我们就可以实现多线程程序了。

      那么Java提供的类是什么呢?Thread。通过查看API,我们知道了有2种方式实现多线程程序:继承Thread类或实现Runnable接口。

    2.1 继承Thread类

    【步骤】

    1. 自定义类MyThread继承Thread类。
    2. MyThread类里面重写run()
    3. 创建对象
    4. 启动线程
    public class MyThread extends Thread {
        @Override
        public void run() {
            // 自己写代码
            // System.out.println("好好学习,天天向上");
            // 一般来说,被线程执行的代码肯定是比较耗时的。所以我们用循环改进
            for (int x = 0; x < 100; x++) {
                System.out.println(x);
            }
        }
    }

      该类要重写run()方法,为什么呢?
      因为不是类中的所有代码都需要被线程执行的。而这个时候,为了区分哪些代码能够被线程执行,java提供了Thread类中的run()用来包含那些被线程执行的代码

    public class MyThreadDemo {
        public static void main(String[] args) {
            // 创建线程对象
            // MyThread my = new MyThread();
    
            // 启动线程
            // my.run();
            // my.run();
            // 调用run()方法为什么是单线程的呢?
            // 因为run()方法直接调用其实就相当于普通的方法调用,所以你看到的是单线程的效果
            // 要想看到多线程的效果,就必须说说另一个方法:start()
    
            // my.start();
            // IllegalThreadStateException:非法的线程状态异常
            // 为什么呢?因为这个相当于是my线程被调用了两次。而不是两个线程启动。
            // my.start();
    
            // 创建两个线程对象
            MyThread my1 = new MyThread();
            MyThread my2 = new MyThread();
    
            my1.start();
            my2.start();
        }
    }

      run()和start()的区别?

      run():仅仅是封装被线程执行的代码,直接调用是普通方法

      start():首先启动了线程,然后再由jvm去调用该线程的run()方法。

    2.2 实现Runnable接口(推荐)

    【步骤】

    1. 自定义类MyRunnable实现Runnable接口
    2. 重写run()方法
    3. 创建MyRunnable类的对象
    4. 创建Thread类的对象,并把3步骤的对象作为构造参数传递
    public class MyRunnable implements Runnable {
        @Override
        public void run() {
            for (int x = 0; x < 100; x++) {
                // 由于实现接口的方式就不能直接使用Thread类的方法了,但是可以间接的使用
                System.out.println(Thread.currentThread().getName() + ":" + x);
            }
        }
    }
    public class MyRunnableDemo {
        public static void main(String[] args) {
            // 创建MyRunnable类的对象
            MyRunnable my = new MyRunnable();
    
            // 创建Thread类的对象,并把C步骤的对象作为构造参数传递
            // Thread(Runnable target)
            /*Thread t1 = new Thread(my);
            Thread t2 = new Thread(my);
    
            t1.setName("张三");
            t2.setName("李四");*/
    
            // Thread(Runnable target, String name)
            Thread t1 = new Thread(my, "张三");
            Thread t2 = new Thread(my, "李四");
    
            t1.start();
            t2.start();
        }
    }

     【问题】

      有了方式1,为什么还会有方式2呢?实现接口的方式有什么好处?

    1. 可以避免由于Java单继承带来的局限性。
    2. 适合多个相同程序的代码去处理同一个资源的情况,把线程同程序的代码,数据有效分离,较好的体现了面向对象的设计思想。

      所以推荐使用方式2。

    三、获取和设置线程名称

    3.1 如何获取线程对象的名称呢

      public final String getName():获取线程的名称。

    public class MyThread extends Thread {
        @Override
        public void run() {
            for (int x = 0; x < 200; x++) {
                System.out.println(getName() + ":" + x);
            }
        }
    }
    public class MyThreadDemo {
        public static void main(String[] args) {
            // 创建两个线程对象
            MyThread my1 = new MyThread();
            MyThread my2 = new MyThread();
    
            my1.start();
            my2.start();
        }
    }

      运行结果为:

      

      名称为什么是:Thread-? 编号

      可以简单的查看Thread的源码:

    class Thread {
        private char name[];
    
        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);
        }
        
         private void init(ThreadGroup g, Runnable target, String name,
                          long stackSize, AccessControlContext acc) {
            //大部分代码被省略了
            this.name = name.toCharArray();
        }
        
        public final void setName(String name) {
            this.name = name.toCharArray();
        }
        
        
        private static int threadInitNumber; //0,1,2
        private static synchronized int nextThreadNum() {
            return threadInitNumber++; //return 0,1
        }
        
        public final String getName() {
            return String.valueOf(name);
        }
    }
    
    class MyThread extends Thread {
        public MyThread() {
            super();
        }
    }

       针对不是Thread类的子类中如何获取线程对象名称呢?  publicstatic Thread currentThread():返回当前正在执行的线程对象

    public class MyThreadDemo {
        public static void main(String[] args) {
            //我要获取main方法所在的线程对象的名称,该怎么办呢?
            //遇到这种情况,Thread类提供了一个很好玩的方法:
            //public static Thread currentThread():返回当前正在执行的线程对象
            System.out.println(Thread.currentThread().getName());// main
        }
    }

    3.2 如何设置线程对象的名称

      public final void setName(String name):设置线程的名称

    public class MyThreadDemo {
        public static void main(String[] args) {
            // 创建两个线程对象
            // 无参构造+setXxx()
            MyThread my1 = new MyThread();
            MyThread my2 = new MyThread();
    
            // 调用方法设置名称
            my1.setName("张三");
            my2.setName("李四");
    
            my1.start();
            my2.start();
        }
    }

       

      也可以带参构造方法给线程起名字,注意MyThread类默认是不提供带参构造的,需要我们手动设置:

    public class MyThread extends Thread {
        public MyThread() {
        }
    
        public MyThread(String name) {
            super(name);
        }
    
        @Override
        public void run() {
            for (int x = 0; x < 200; x++) {
                System.out.println(getName() + ":" + x);
            }
        }
    }
    public class MyThreadDemo {
        public static void main(String[] args) {
            // 创建两个线程对象
            // 无参构造+setXxx()
            // MyThread my1 = new MyThread();
            // MyThread my2 = new MyThread();
    
            // 调用方法设置名称
            // my1.setName("张三");
            // my2.setName("李四");
    
            // 带参构造方法给线程起名字
            MyThread my1 = new MyThread("张三");
            MyThread my2 = new MyThread("李四");
    
            my1.start();
            my2.start();
        }
    }

    四、线程调度

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

      线程有两种调度模型:

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

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

      如何获取线程对象的优先级?  public final int getPriority():返回线程对象的优先级

      如何设置线程对象的优先级? public final void setPriority(int newPriority):更改线程的优先级。 

    public class ThreadPriority extends Thread {
        @Override
        public void run() {
            for (int x = 0; x < 100; x++) {
                System.out.println(getName() + ":" + x);
            }
        }
    }
    public class ThreadPriorityDemo {
        public static void main(String[] args) {
            ThreadPriority tp1 = new ThreadPriority();
            ThreadPriority tp2 = new ThreadPriority();
            ThreadPriority tp3 = new ThreadPriority();
    
            tp1.setName("东方不败");
            tp2.setName("岳不群");
            tp3.setName("林平之");
    
            // 获取默认优先级
            // 我们的线程没有设置优先级,肯定有默认优先级。
            // 那么,默认优先级是多少呢?5
            // System.out.println(tp1.getPriority());//5
            // System.out.println(tp2.getPriority());//5
            // System.out.println(tp3.getPriority());//5
    
            // 设置线程优先级
            // tp1.setPriority(100000); //IllegalArgumentException:非法参数异常。抛出的异常表明向方法传递了一个不合法或不正确的参数。
            
            //设置正确的线程优先级
            tp1.setPriority(10);
            tp2.setPriority(1);
    
            tp1.start();
            tp2.start();
            tp3.start();
        }
    } 

      注意:
      线程默认优先级是5。线程优先级的范围是:1-10。
      线程优先级高仅仅表示线程获取的 CPU时间片的几率高,但是要在次数比较多,或者多次运行的时候才能看到比较好的效果。

    五、线程控制

      我们已经知道了线程的调度,接下来我们就可以使用如下方法对线程进行控制。

    5.1 线程休眠

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

    public class ThreadSleep extends Thread {
        @Override
        public void run() {
            for (int x = 0; x < 100; x++) {
                System.out.println(getName() + ":" + x + ",日期" + new Date());
                // 睡眠
                // 困了,我稍微休息1秒钟
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    public class ThreadSleepDemo {
        public static void main(String[] args) {
            ThreadSleep ts1 = new ThreadSleep();
            ThreadSleep ts2 = new ThreadSleep();
            ThreadSleep ts3 = new ThreadSleep();
    
            ts1.setName("林青霞");
            ts2.setName("林志玲");
            ts3.setName("林志颖");
    
            ts1.start();
            ts2.start();
            ts3.start();
        }
    }

    5.2 线程加入

       线程加入:public final void join():等待该线程终止。 

    public class ThreadJoin extends Thread {
        @Override
        public void run() {
            for (int x = 0; x < 100; x++) {
                System.out.println(getName() + ":" + x);
            }
        }
    }
    public class ThreadJoinDemo {
        public static void main(String[] args) {
            ThreadJoin tj1 = new ThreadJoin();
            ThreadJoin tj2 = new ThreadJoin();
            ThreadJoin tj3 = new ThreadJoin();
    
            tj1.setName("李渊");
            tj2.setName("李世民");
            tj3.setName("李元霸");
    
            tj1.start();
            try {
                tj1.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            tj2.start();
            tj3.start();
        }
    }

       现象:调用join()方法的线程运行完毕后,其它线程才能开始运行

    5.3 线程礼让

      public static void yield():暂停当前正在执行的线程对象,并执行其他线程。
      让多个线程的执行更和谐,但是不能靠它保证一人一次。

    public class ThreadYield extends Thread {
        @Override
        public void run() {
            for (int x = 0; x < 100; x++) {
                System.out.println(getName() + ":" + x);
                Thread.yield();
            }
        }
    }
    public class ThreadYieldDemo {
        public static void main(String[] args) {
            ThreadYield ty1 = new ThreadYield();
            ThreadYield ty2 = new ThreadYield();
    
            ty1.setName("林青霞");
            ty2.setName("刘意");
    
            ty1.start();
            ty2.start();
        }
    }

    5.4 线程守护

      public final void setDaemon(boolean on):将该线程标记为守护线程或用户线程。
      当正在运行的线程都是守护线程时,Java 虚拟机退出。 该方法必须在启动线程前调用

    public class ThreadDaemon extends Thread {
        @Override
        public void run() {
            for (int x = 0; x < 100; x++) {
                System.out.println(getName() + ":" + x);
            }
        }
    }
    public class ThreadDaemonDemo {
        public static void main(String[] args) {
            ThreadDaemon td1 = new ThreadDaemon();
            ThreadDaemon td2 = new ThreadDaemon();
    
            td1.setName("关羽");
            td2.setName("张飞");
    
            // 设置守护线程
            td1.setDaemon(true);
            td2.setDaemon(true);
    
            td1.start();
            td2.start();
    
            Thread.currentThread().setName("刘备");
            for (int x = 0; x < 5; x++) {
                System.out.println(Thread.currentThread().getName() + ":" + x);
            }
        }
    }

      现象:td1和td2设置成守护线程后,一旦主线程执行完毕后,守护线程也将终止运行。

      这就有点像坦克大战游戏:

       

      上图中的关羽和张飞可以看成守护线程,当刘备主线程消亡以后,两者都不可能再具有获得CPU执行权的资格。

    5.5 线程中断

      public final void stop():让线程停止,过时了,但是还可以使用。不安全,所以不建议使用
      public void interrupt():中断线程。 把线程的状态终止,并抛出一个InterruptedException。

    public class ThreadStop extends Thread {
        @Override
        public void run() {
            System.out.println("开始执行:" + new Date());
    
            // 我要休息10秒钟,亲,不要打扰我哦
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                // e.printStackTrace();
                System.out.println("线程被终止了");
            }
    
            System.out.println("结束执行:" + new Date());
        }
    }
    public class ThreadStopDemo {
        public static void main(String[] args) {
            ThreadStop ts = new ThreadStop();
            ts.start();
    
            // 你超过三秒不醒过来,我就干死你
            try {
                Thread.sleep(3000);
                // ts.stop();
                ts.interrupt();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    六、线程的生命周期

      新建:创建线程对象

      就绪:有执行资格,没有执行权

      运行:有执行资格,有执行权

      阻塞:由于一些操作让线程处于该状态。没有执行资格,没有执行权。而另一些操作却可以把它给激活,激活后处于就绪状态

      死亡:线程对象变成垃圾,等待被回收。

  • 相关阅读:
    Lambda表达式详解 (转)
    usb驱动开发21之驱动生命线
    usb驱动开发18之设备生命线
    usb驱动开发17之设备生命线
    usb驱动开发16之设备生命线
    usb驱动开发15之设备生命线
    usb驱动开发14之设备生命线
    usb驱动开发13之设备生命线
    usb驱动开发12之设备生命线
    usb驱动开发11之设备生命线
  • 原文地址:https://www.cnblogs.com/yft-javaNotes/p/10910476.html
Copyright © 2011-2022 走看看