美文网首页
java 高级特性之多线程

java 高级特性之多线程

作者: 大鹏的鹏 | 来源:发表于2019-05-23 15:29 被阅读0次

    一、线程和进程

    1. 进程

    进程是系统中正在运行的一个程序,程序一旦运行就是进程。进程可以看成程序执行的一个实例。

    进程是系统资源分配的独立实体,每个进程都拥有独立的地址空间。一个进程无法访问另一个进程的变量和数据结构,如果想让一个进程访问另一个进程的资源,需要使用进程间通信,比如管道,文件,套接字等。

    2. 线程

    线程从属于进程,是程序的实际执行者。一个进程至少包含一个主线程,也可以有更多的子线程。

    3. 进程和线程的区别联系
    • 线程是进程的一部分,一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。
    • 资源分配给进程,同一进程的所有线程共享该进程的所有资源。
    • 虚拟机分给线程,即真正在虚拟机上运行的是线程。
    • 线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。

    二、并行和并发

    1. 并行

    两个或多个处理逻辑在同个一时刻内发生,cpu同时执行,是真正的同时。

    2. 并发

    并发是轮流处理多个任务。同一个时间段内多个任务同时都在执行,并且都没有执行结束。并发任务强调在一个时间段内同时执行,而一个时间段由多个单位时间累积而成,所以说并发的多个任务在单位时间内不一定同时在执行 。

    主要的CPU调度算法有如下两种:

    1. 分时调度:每个线程轮流获取CPU使用权,各个线程平均CPU时间片。
    2. 抢占式调度:Java虚拟机使用的就是这种调度模型。这种调度方式会根据线程优先级,先调度优先级高的线程,如果线程优先级相同,会随机选取线程执行。

    三、Java线程的创建

    Java中线程的创建常见有如三种基本形式。

    1. 继承Thread类,重写该类的run()方法。
    public class MyThread extends Thread {
        @Override
        public void run() {
            super.run();
            for (; ; ) {
                System.out.println(Thread.currentThread().getName());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            MyThread myThread = new MyThread();
            myThread.start();
        }
    }
    
    2. 实现Runnable接口,并重写该接口的run()方法。
    public class MyRunnable implements Runnable {
        @Override
        public void run() {
            for (; ; ) {
                System.out.println(Thread.currentThread().getName());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    public class Main {
        public static void main(String[] args) {
                MyRunnable myRunnable = new MyRunnable();
                Thread thread = new Thread(myRunnable);
                thread.start();
        }
    }
    
    3. 使用Callable和Future接口创建线程。
    public class Main {
        public static void main(String[] args) {
            FutureTask<Integer> task = new FutureTask<Integer>(new Callable<Integer>() {
                @Override
                public Integer call() throws Exception {
                    int i = 0;
                    for (; i < 100; ++i) {
                        System.out.println(Thread.currentThread().getName() + "  " + i);
                    }
                    return i;
                }
            });
            Thread thread = new Thread(task);
            thread.start();
            try {
                System.out.println("子线程的返回值" + task.get());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    

    实现Rnnable接口的好处在于:避免了单继承的局限性。 在定义线程时,建议使用实现Rnnable方式。

    在某一个时刻,只能有一个程序在运行(多核除外)。cpu在做着快速的切换,以达到看上去是同时运行的效果。我们可以形象把多线程的运行行为认为是在互相抢夺cpu的执行权。这就是多线程的一个特性:随机性。谁抢到谁执行,至于执行多长,cpu说了算。

    注意

    • start()方法的调用后并不是立即执行多线程代码,而是使得该线程变为可运行态(Runnable),什么时候运行是由操作系统决定的。
    • 从程序运行的结果可以发现,多线程程序是乱序执行。因此,只有乱序执行的代码才有必要设计为多线程。
    • Thread.sleep()方法调用目的是不让当前线程独自霸占该进程所获取的CPU资源,以留出一定时间给其他线程执行的机会。
    • 实际上所有的多线程代码执行顺序都是不确定的,每次执行的结果都是随机的。

    start()和run()区别
    run没有另起线程,而start才是真正意义的新开线程。

    四、线程的生命周期。

    1. 新建状态(new)

    当程序使用new关键字创建一个线程以后,该线程就处于新建状态,此时它和其他的JAVA对象一样,仅仅由JAVA虚拟机为其分配内存,并初始化成员变量的值。此时的线程对象没有表现出任何线程的动态特征,程序也不会执行线程的线程执行体。

    2. 就绪状态(Runnable)

    当线程对象调用start()方法以后,该线程进入就绪状态,JAVA虚拟机会为其创建方法调用栈和程序计数器。处于这种状态中的线程并没有开始运行,只是表示当前线程可以运行了,至于什么时候运行,则取决于JVM里线程调度器的调度。所以线程的执行是由底层平台控制, 具有一定的随机性。并不是说执行了t.start()此线程立即就会执行。

    3. 运行状态(Running)

    如果就绪状态的线程获取 CPU 资源,就可以执行 run()(所以run()方法是由线程获得CPU以后自动执行),此时线程便处于运行状态。。如果一个计算机只有一个CPU,那么在任意时刻只有一个线程处于运行状态。相对的,如果有多个CPU,那么在同一时刻就可以有多个线程并行执行。但是,当处于就绪状态的线程数大于处理器数时,仍然会存在多个线程在同一CPU上轮换执行的现象,只是计算机的运行速度非常快,人感觉不到而已。

    当一个线程开始运行后,它不可能一直持有CPU(除非该线程执行体非常短,瞬间就执行结束了)。所以,线程在执行过程中需要被中断,目的是让其它线程获得执行的CPU的机会。线程的调度细节取决于底层平台所采用的策略。对于抢占式策略的系统而言,系统会给每一个可执行线程一个时间段来处理任务,当该时间结束后,系统就会剥夺该线程所占用资源(即让出CPU),让其它线程获得执行机会。

    所有的现代桌面和服务器操作系统都采用抢占式调度策略,但一些小型设备,如:手机,则可能采用协作式调度策略。在这样的系统中,只有当一个线程调用了它的sleep()或yield()方法后才会放弃所占用的资源。也就是说,此时必须该线程主动放弃占用资源,才能轮到其他就绪状态的线程获得CPU,不然必须要等当前线阻塞/死亡以后,其他线程才有机会运行。

    处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。

    4. 阻塞状态(Blocked)

    当如下情况发生时,线程会进入阻塞状态:

    • 线程调用sleep()方法主动放弃占用的处理器资源;通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态;
    • 线程调用了一个阻塞式IO方法,在该方法返回以前,该线程被阻塞;
    • 线程试图获得一个同步监视器,但该监视器被其他线程持有;
    • 运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态;
    • 线程调用suspend()方法将该线程挂起;

    当正在执行的线程被阻塞以后,其他线程可以获得执行的机会,被阻塞的线程会在合适的时候进入就绪状态,而不是进入运行状态。也就是说,当线程阻塞解除后,必须重新等待线程调度器再次调度它,而不是马上获得CPU。所以针对上述线程阻塞情况,如何让线程重新进入就绪状态,有如下几种情况:

    • 调用sleep()方法的线程经过了指定时间;
    • 线程调用的阻塞式IO方法已经返回;
    • 线程成功地获得了试图取得的同步监视器;
    • 线程在等待通知时,其他线程发出了一个通知;
    • 处于挂起状态的线程被调用了resume()恢复方法;
    5. 死亡状态(Dead)

    一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。
    线程会在如下几种情况结束(结束后就处于死亡状态):

    • run()/call()方法执行完成,线程正常结束;
    • 线程抛出一个未捕获的Exception或Error;
    • 直接调用线程的stop()方法结束该线程——该方法容易导致死锁,通常不建议使用。

    如果主线程结束了以后,其他线程会不会受影响呢?结果是不会,一旦当子线程启动以后,它就拥有和主线程一样的地位,它不会受主线程的影响。

    注意:当线程死亡以后,不能再次调用start()方法来启动该线程,调用会返回IllegalThreadStateException异常。程序只能对处于新建状态的线程调用start()方法,而对处于新建状态的线程两次调用start()方法也是错误的,这都会引发IllegalThreadStateException异常。

    图示:


    线程从阻塞状态只能进入就绪状态,而不能直接进入运行状态。而就绪状态到运行状态之间的转换通常不受程序控制,而由系统线程调度所决定,当处于就绪状态的线程获得处理器资源时,该线程进入运行状态;当处于运行状态的线程失去处理器器资源时,该线程进入就绪状态。但有一个方法可以控制线程从运行状态转为就绪状态,那就是yiled()方法。

    五、线程调度

    1. 线程的调度

    调整线程优先级:Java线程有优先级,优先级高的线程会获得较多的运行机会。
    Java线程的优先级用整数表示,取值范围是1~10,Thread类有以下三个静态常量:

    • static int MAX_PRIORITY:线程可以具有的最高优先级,取值为10。
    • static int MIN_PRIORITY:线程可以具有的最低优先级,取值为1。
    • static int NORM_PRIORITY:分配给线程的默认优先级,取值为5。

    Thread类的setPriority()和getPriority()方法分别用来设置和获取线程的优先级。
    每个线程都有默认的优先级。主线程的默认优先级为Thread.NORM_PRIORITY。
    线程的优先级有继承关系,比如A线程中创建了B线程,那么B将和A具有相同的优先级。
    JVM提供了10个线程优先级,但与常见的操作系统都不能很好的映射。如果希望程序能移植到各个操作系统中,应该仅仅使用Thread类有以下三个静态常量作为优先级,这样能保证同样的优先级采用了同样的调度方式。

    2.线程睡眠

    Thread.sleep(long millis)方法,使线程转到阻塞状态。millis参数设定睡眠的时间,以毫秒为单位。当睡眠结束后,就转为就绪(Runnable)状态。sleep()平台移植性好。

    3.线程等待

    Object类中的wait()方法,导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 唤醒方法。这个两个唤醒方法也是Object类中的方法,行为等价于调用 wait(0) 一样。

    4.线程让步

    Thread.yield() 方法,暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程。

    5.线程加入

    join()方法,等待其他线程终止。在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪状态。

    6.线程唤醒

    Object类中的notify()方法,唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。选择是任意性的,并在对实现做出决定时发生。线程通过调用其中一个 wait 方法,在对象的监视器上等待。 直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程。被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争;例如,唤醒的线程在作为锁定此对象的下一个线程方面没有可靠的特权或劣势。类似的方法还有一个notifyAll(),唤醒在此对象监视器上等待的所有线程。

    六、线程池

    1. 定义

    直接创建线程的话,每个线程都要通过new Thread(xxRunnable).start()的方式来创建并运行一个线程,线程少的话这不会是问题,而真实环境可能会开启多个线程让系统和程序达到最佳效率,当线程数达到一定数量就会耗尽系统的CPU和内存资源,也会造成GC频繁收集和停顿,因为每次创建和销毁一个线程都是要消耗系统资源的,如果为每个任务都创建线程这无疑是一个很大的性能瓶颈。如果线程数量多的话,频繁的创建和销毁线程会大大浪费时间和效率,更重要的是浪费内存,因为正常来说线程执行完毕后死亡,线程对象就会变成垃圾。为了解决这个问题在Java中引入了线程池技术。

    谈到线程池就会想到池化技术,其中最核心的思想就是把宝贵的资源放到一个池子中;每次使用都从里面获取,用完之后又放回池子供其他人使用。ThreadPoolExecutor可以减少销毁和创建的次数,每个工作线程可以重复利用,可执行多个任务

    2. Java四种线程池的使用

    Java通过Executors提供四种线程池,分别为:

    JDK为我们封装了一套操作多线程的框架Executors,帮助我们可以更好的控制线程池,Executors下提供了一些线程池的工厂方法:

    • newFixedThreadPool:
            ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
            for (int i = 0; i < 10; i++) {
                final int index = i;
                fixedThreadPool.execute(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            System.out.println(index);
                            Thread.sleep(2000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                });
            }
    
    

    创建固定长度的线程池,线程池中的线程数量是固定的。超出的线程会在队列中等待。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小,线程池的大小一旦达到最大值就会保持不变。如果某个线程因为执行异常而结束,线程池会补充一个新的线程。

    缺点:不支持自定义拒绝策略,大小固定,难以扩展。

    • newCacheThreadPool:
            ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
            for (int i = 0; i < 10; i++) {
                final int index = i;
                try {
                    Thread.sleep(index * 1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
    
                cachedThreadPool.execute(new Runnable() {
    
                    @Override
                    public void run() {
                        System.out.println(index);
                    }
                });
            }
    

    创建一个可缓存的线程池,如果线程池的大小超过了处理任务所需要的线程数,那么就会回收部分空闲(60秒不执行任务)的线程;当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或JVM)能够创建的最大线程大小。

    缺点:一旦线程无限增长,会导致内存溢出。

    • newSingleThreadExecutor:
            ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
            for (int i = 0; i < 10; i++) {
                final int index = i;
                singleThreadExecutor.execute(new Runnable() {
    
                    @Override
                    public void run() {
                        try {
                            System.out.println(index);
                            Thread.sleep(2000);
                        } catch (InterruptedException e) {
                            // TODO Auto-generated catch block
                            e.printStackTrace();
                        }
                    }
                });
            }
    

    创建一个单线程的线程池,这个线程池只有一个线程在工作,也就是串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

    • newScheduledThreadPool:
            ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
            scheduledThreadPool.schedule(new Runnable() {
    
                @Override
                public void run() {
                    System.out.println("delay 3 seconds");
                }
            }, 3, TimeUnit.SECONDS);
    

    创建一个定长线程池,支持定时及周期性任务执行。

    缺点:任务是单线程方式执行,一旦一个任务失败其他任务也受影响。

    3. 四种线程池是如何执行的?

    这四种线程池实际调用的还是ThreadPoolExecutor的构造方法。
    Executors实际上调用的是ThreadPoolExecutor的构造方法(newScheduledThreadPool调用的是ScheduleThreadPoolExecutor的构造),我们看一下ThreadPoolExecutor参数最多的构造

    public ThreadPoolExecutor(int corePoolSize,//线程核心线程数。线程池长期维持的线程数,即使线程处于idle状态也不会回收
                                  int maximumPoolSize,//线程允许的最大线程数
                                  long keepAliveTime,//空闲线程存活时间。当前线程池总数大于核心线程数时,终止多余的空闲线程的时间
                                  TimeUnit unit,//销毁时间。超过这个时间,多余的线程会被回收
                                  BlockingQueue<Runnable> workQueue,//存储等待执行线程的工作队列
                                  ThreadFactory threadFactory,//创建线程工厂,定制线程的创建过程
                                  RejectedExecutionHandler handler) //拒绝策略。当工作队列、线程池全满时如何拒绝新任务
    

    这里有一个拒绝策略,它是如何处理的呢?
    拒绝策略

    • AbortPolicy简单粗暴,直接抛出拒绝异常,这也是默认的拒绝策略
    • CallerRunsPolicy如果线程池未关闭,则会在调用者线程中直接执行新任务,这会导致主线程提交线程性能变慢
    • DiscardPolicy表示不处理新任务,即丢弃
    • DiscardOldestPolicy抛弃最老的任务,从队列中取出最老的任务然后放入新的任务执行
      提交线程任务
      有两种方式submit()和execute()
    • execute没有返回值,如果不需要直到线程的结果就使用execute()
    • submit()返回一个Future对象,如果想知道线程结果就使用submit()提交,而且它能在主线程中通过Future的get方法捕获线程中的异常
      线程池的关闭
    • shutdown()不再接受新的任务,之前提交的任务等执行结束再关闭线程池
    • shutdownNow()不再接受新的任务,试图停止池中的任务再关闭线程池,返回所有未处理线程列表

    相关文章

      网友评论

          本文标题:java 高级特性之多线程

          本文链接:https://www.haomeiwen.com/subject/irqbzqtx.html