zoukankan      html  css  js  c++  java
  • 多线程

    1.概述

     1.1 并发与并行

    • 并行:指两个或多个事件在同一时刻发生(同时发生)。

    • 并发:指两个或多个事件在同一个时间段内发生。

           在操作系统中,安装了多个程序,并发指的是在一段时间内宏观上有多个程序同时运行,这在单 CPU 系统中,每一时刻只能有一道程序执行,即微观上这些程序是分时的交替运行,只不过是给人的感觉是同时运行,那是因为分时交替运行的时间是非常短的。

      而在多个 CPU 系统中,则这些可以并发执行的程序便可以分配到多个处理器上(CPU),实现多任务并行执行,即利用每个处理器来处理一个可以并发执行的程序,这样多个程序便可以同时执行。目前电脑市场上说的多核 CPU,便是多核处理器,核越多,并行处理的程序越多,能大大的提高电脑运行的效率。

    注意:单核处理器的计算机肯定是不能并行的处理多个任务的,只能是多个任务在单个CPU上并发运行。同理,线程也是一样的,从宏观角度上理解线程是并行运行的,但是从微观角度上分析却是串行运行的,即一个线程一个线程的去运行,当系统只有一个CPU时,线程会以某种顺序执行多个线程,我们把这种情况称之为线程调度。

    1.2 线程与进程

    • 进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。

    • 线程:进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。

          进程:

        线程

      进程与线程的区别

    • 进程:有独立的内存空间,进程中的数据存放空间(堆空间和栈空间)是独立的,至少有一个线程。

    • 线程:堆空间是共享的,栈空间是独立的,线程消耗的资源比进程小的多。

      注意:

      1:因为一个进程中的多个线程是并发运行的,那么从微观角度看也是有先后顺序的,哪个线程执行完全取决于 CPU 的调度,程序员是干涉不了的。而这也就造成的多线程的随机性。

      2:Java 程序的进程里面至少包含两个线程,主进程也就是 main()方法线程,另外一个是垃圾回收机制线程。每当使用 java 命令执行一个类时,实际上都会启动一个 JVM,每一个 JVM 实际上就是在操作系统中启动了一个线程,java 本身具备了垃圾的收集机制,所以在 Java 运行时至少会启动两个线程。

      3:由于创建一个线程的开销比创建一个进程的开销小的多,那么我们在开发多任务运行的时候,通常考虑创建多线程,而不是创建多进程。

    线程调度:

      计算机通常只有一个CPU时,在任意时刻只能执行一条计算机指令,每一个进程只有获得CPU的使用权才能执行指令。所谓多进程并发运行,从宏观上看,其实是各个进程轮流获得CPU的使用权,分别执行各自的任务。那么,在可运行池中,会有多个线程处于就绪状态等到CPU,JVM就负责了线程的调度。JVM采用的是抢占式调度,没有采用分时调度,因此可以能造成多线程执行结果的的随机性。

    2.创建线程

     2.1 Thread类

      Java使用java.lang.Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。每个线程的作用是完成一定的任务,实际上就是执行一段程序流即一段顺序执行的代码。Java使用线程执行体来代表这段程序流。Java中通过继承Thread类来创建启动多线程的步骤如下:

    1. 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体。

    2. 创建Thread子类的实例,即创建了线程对象。

    3. 调用线程对象的start()方法来启动该线程。

    代码如下:

    //自定义线程类:
    public class MyThread extends Thread{
        /*
         * 利用继承中的特点 
         *   将线程名称传递  进行设置
         */
        public MyThread(String name){
            super(name);
        }
        /*
         * 重写run方法
         *  定义线程要执行的代码
         */
        public void run(){        
            for (int i = 0; i < 20; i++) {
                //getName()方法 来自父亲
                System.out.println(getName()+i);
            }
        }
    }
    
    //测试类
    public class Demo {
        public static void main(String[] args) {
              System.out.println("这里是main线程");
            MyThread mt = new MyThread("小强");        
            mt.start();//开启了一个新的线程
            for (int i = 0; i < 20; i++) {
                System.out.println("旺财:"+i);
            }
        }
    }    

    以上代码自定义MyThread类继承Thread类,然后重写了run()方法,通过创建MyThread对象调用start()方法启动线程,执行run方法中的代码块。

    原理图如下:

      程序启动运行main时候,java虚拟机启动一个进程,主线程main在main()调用时候被创建。随着调用mt的对象的start方法,另外一个新的线程也启动了,这样,整个应用就在多线程下运行。

    通过这张图我们可以很清晰的看到多线程的执行流程,那么为什么可以完成并发执行呢?我们再来讲一讲原理。

      多线程执行时,到底在内存中是如何运行的呢?以上个程序为例,进行图解说明:

      多线程执行时,在栈内存中,其实每一个执行线程都有一片自己所属的栈内存空间。进行方法的压栈和弹栈。

     

    当执行线程的任务结束了,线程自动在栈内存中释放了。但是当所有的执行线程都结束了,那么进程就结束了。

    在以上创建并启动线程的过程中,我们用到了java.lang.Thread类。在java里该类中定义了一些有关线程操作的API,具体如下:

    构造方法:

      • public Thread():分配一个新的线程对象。

      • public Thread(String name):分配一个指定名字的新的线程对象。

      • public Thread(Runnable target):分配一个带有指定目标新的线程对象。

      • public Thread(Runnable target,String name):分配一个带有指定目标新的线程对象并指定名字。

    常用方法:

      • public String getName():获取当前线程名称。

      • public void start():导致此线程开始执行; Java虚拟机调用此线程的run方法。

      • public void run():此线程要执行的任务在此处定义代码。

      • public static void sleep(long millis):使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。

      • public static Thread currentThread():返回对当前正在执行的线程对象的引用。

     2.2 Runnable接口

    通过实现java.lang.Runnable接口也是一种比较常见的创建线程的方法,我们只需要重写run方法即可。

    步骤如下:

      1. 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。

      2. 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。

      3. 调用线程对象的start()方法来启动线程。

      代码如下

     1 //创建Runnable实现类
     2 public class MyRunnable implements Runnable{
     3     @Override
     4     public void run() {
     5         for (int i = 0; i < 20; i++) {
     6             System.out.println(Thread.currentThread().getName()+" "+i);
     7         }
     8     }
     9 }
    10 
    11 //测试类
    12 public class Demo {
    13     public static void main(String[] args) {
    14         //创建自定义类对象  线程任务对象
    15         MyRunnable mr = new MyRunnable();
    16         //创建线程对象
    17         Thread t = new Thread(mr, "小强");
    18         t.start();
    19         for (int i = 0; i < 20; i++) {
    20             System.out.println("旺财 " + i);
    21         }
    22     }
    23 }

      通过实现Runnable接口,使得该类有了多线程类的特征。run()方法是多线程程序的一个执行目标。所有的多线程代码都在run方法里面。Thread类实际上也是实现了Runnable接口的类。

      在启动的多线程的时候,需要先通过Thread类的构造方法Thread(Runnable target) 构造出对象,然后调用Thread对象的start()方法来运行多线程代码。

      实际上所有的多线程代码都是通过运行Thread的start()方法来运行的。因此,不管是继承Thread类还是实现Runnable接口来实现多线程,最终还是通过Thread的对象的API来控制线程的,熟悉Thread类的API是进行多线程编程的基础。

    Tips:Runnable对象仅仅作为Thread对象的target,Runnable实现类里包含的run()方法仅作为线程执行体。而实际的线程对象依然是Thread实例,只是该Thread线程负责执行其target的run()方法。

      Thread和Runnable的区别

      如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。

    总结:

      实现Runnable接口比继承Thread类所具有的优势:

      1. 适合多个相同的程序代码的线程去共享同一个资源。

      2. 可以避免java中的单继承的局限性。

      3. 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。

      4. 线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类。

      2.3 匿名内部类方式

      使用线程的内匿名内部类方式,可以方便的实现每个线程执行不同的线程任务操作。

      使用匿名内部类的方式实现Runnable接口,重新Runnable接口中的run方法:

    代码如下:

     1 public class NoNameInnerClassThread {
     2        public static void main(String[] args) {           
     3 //        new Runnable(){
     4 //            public void run(){
     5 //                for (int i = 0; i < 20; i++) {
     6 //                    System.out.println("刘德华:"+i);
     7 //                }
     8 //            }  
     9 //           }; //---这个整体  相当于new MyRunnable()
    10         Runnable r = new Runnable(){
    11             public void run(){
    12                 for (int i = 0; i < 20; i++) {
    13                       System.out.println("刘德华:"+i);
    14                 }
    15             }  
    16         };
    17         new Thread(r).start();
    18 
    19         for (int i = 0; i < 20; i++) {
    20               System.out.println("张学友:"+i);
    21         }
    22        }
    23 }

     2.4 Callable接口

     通过实现java.util.concurrent.Callable接口也能够创建线程

    步骤如下:

      1. 创建一个线程任务对象,实现Callable接口,申明线程执行的结果类型,并重写call方法。
      2. 创建一个未来任务对象FutureTask对象,包装Callable实现类对象。
      3. 创建一个线程对象Thread来包装FutureTask对象。
      4. 启动线程。
      5. 获取线程执行的结果。

    代码如下:

    //创建Callable实现类,声明结果返回值类型
    class CallableTarger implements Callable<String>{
        @Override
        public String call() throws Exception {
            int sum = 0 ;
            for(int i = 1 ; i <= 5 ; i++ ){
                sum+=i;
                System.out.println(Thread.currentThread().getName()+"=>输出:"+i);
            }
            return Thread.currentThread().getName()+"求和返回:"+sum;
        }
    }
    
    
    //测试类
    public class Test {
        public static void main(String[] args) {
            // (2)创建一个未来任务对象FutureTask对象,包装Callable实现类对象。
            CallableTarger targer = new CallableTarger();
            //  未来任务对象的功能:可以在线程执行完毕以后得到线程的执行结果。
            //  未来任务对象实际上就是一个Runnable对象
            FutureTask<String> task = new FutureTask<>(targer);
            // public Thread(Runnable task)
            // (3)创建一个线程对象Thread来包装FutureTask对象
            Thread t = new Thread(task);
            // (4)启动线程。
            t.start();
    
            for(int i = 1 ; i <= 5 ; i++ ){
                System.out.println(Thread.currentThread().getName()+"=>输出:"+i);
            }
    
            // (5)获取线程执行的结果。
            try {
                String rs = task.get();
                System.out.println(rs);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    通过实现Callable接口,可以创建一个可以返回执行结果的线程。通过调用java.util.concurrent.FutureTask<V>的get()方法,可以获得该线程的执行结果。

    注意:get()方法会阻塞主线程直到获取‘将来’结果;当不调用此方法时,主线程不会阻塞!

    总结:

      实现Callable接口比继承Thread类以及实现Runnable所具有的优势:

    1. 其特性非常适合做线程池。
    2. 线程对象只是实现了Callable接口,可以继续继承其他类,可以继续实现其他接口。
    3. 也适合作用于资源共享
    4. 可以得到线程执行的结果。

     3.线程安全

     3.1线程安全概述

    如果有多个线程在同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。

    我们通过一个案例,演示线程的安全问题:

    电影院要卖票,我们模拟电影院的卖票过程。假设要播放的电影是 “葫芦娃大战奥特曼”,本次电影的座位共100个(本场电影只能卖100张票)。

    我们来模拟电影院的售票窗口,实现多个窗口同时卖 “葫芦娃大战奥特曼”这场电影票(多个窗口一起卖这100张票)

    需要窗口,采用线程对象来模拟;需要票,Runnable接口子类来模拟

    模拟票:

     1 //定义执行类Runnable
     2 public class Ticket implements Runnable {
     3     private int ticket = 100;
     4     /*
     5      * 执行卖票操作
     6      */
     7     @Override
     8     public void run() {
     9         //每个窗口卖票的操作 
    10         //窗口 永远开启 
    11         while (true) {
    12             if (ticket > 0) {//有票 可以卖
    13                 //出票操作
    14                 //使用sleep模拟一下出票时间 
    15                 try {
    16                     Thread.sleep(100);
    17                 } catch (InterruptedException e) {
    18                     // TODO Auto-generated catch block
    19                     e.printStackTrace();
    20                 }
    21                 //获取当前线程对象的名字 
    22                 String name = Thread.currentThread().getName();
    23                 System.out.println(name + "正在卖:" + ticket--);
    24             }
    25         }
    26     }
    27 }
    28 
    29 //测试类
    30 public class Demo {
    31     public static void main(String[] args) {
    32         //创建线程任务对象
    33         Ticket ticket = new Ticket();
    34         //创建三个窗口对象
    35         Thread t1 = new Thread(ticket, "窗口1");
    36         Thread t2 = new Thread(ticket, "窗口2");
    37         Thread t3 = new Thread(ticket, "窗口3");
    38         
    39         //同时卖票
    40         t1.start();
    41         t2.start();
    42         t3.start();
    43     }
    44 }

    结果中有一部分这样现象:

    发现程序出现了两个问题:

      1. 相同的票数,比如5这张票被卖了两回。

      2. 不存在的票,比如0票与-1票,是不存在的。

    这种问题,几个窗口(线程)票数不同步了,这种问题称为线程不安全。

    线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。

      3.2线程同步

    线程同步是为了解决线程安全问题。

    当我们使用多个线程访问同一资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全问题。

    要解决上述多线程并发访问一个资源的安全性问题:也就是解决重复票与不存在票问题,Java中提供了同步机制(synchronized)来解决。

    根据案例简述:

    窗口1线程进入操作的时候,窗口2和窗口3线程只能在外等着,窗口1操作结束,窗口1和窗口2和窗口3才有机会进入代码去执行。也就是说在某个线程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢夺CPU资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的现象。

    为了保证每个线程都能正常执行原子操作,Java引入了线程同步机制。

    那么怎么去使用呢?有三种方式完成同步操作:

    1. 同步代码块。

    2. 同步方法。

    3. 锁机制。

      3.3同步代码块

    同步代码块synchronized关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。

     1 //使用同步代码块处理多线程资源共享问题
     2 public class Ticket implements Runnable{
     3     private int ticket = 100;
     4     
     5     Object lock = new Object();
     6     /*
     7      * 执行卖票操作
     8      */
     9     @Override
    10     public void run() {
    11         //每个窗口卖票的操作 
    12         //窗口 永远开启 
    13         while(true){
    14             synchronized (lock) {
    15                 if(ticket>0){//有票 可以卖
    16                     //出票操作
    17                     //使用sleep模拟一下出票时间 
    18                     try {
    19                         Thread.sleep(50);
    20                     } catch (InterruptedException e) {
    21                         // TODO Auto-generated catch block
    22                         e.printStackTrace();
    23                     }
    24                     //获取当前线程对象的名字 
    25                     String name = Thread.currentThread().getName();
    26                     System.out.println(name+"正在卖:"+ticket--);
    27                 }
    28             }
    29         }
    30     }
    31 }

    当使用了同步代码块后,上述的线程的安全问题,解决了。

    3.4 同步方法

    同步方法:使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。

    格式:

    public synchronized void method(){
    可能会产生线程安全问题的代码

    }

     

    同步锁是谁?

    对于非static方法,同步锁就是this。

    对于static方法,我们使用当前方法所在类的字节码对象(类名.class)。

    使用同步方法代码如下:

    public class Ticket implements Runnable{
        private int ticket = 100;
        /*
         * 执行卖票操作
         */
        @Override
        public void run() {
            //每个窗口卖票的操作 
            //窗口 永远开启 
            while(true){
                sellTicket();
            }
        }
        
        /*
         * 锁对象 是 谁调用这个方法 就是谁 
         *   隐含 锁对象 就是  this
         *    
         */
        public synchronized void sellTicket(){
            if(ticket>0){//有票 可以卖    
                //出票操作
                //使用sleep模拟一下出票时间 
                try {
                      Thread.sleep(100);
                } catch (InterruptedException e) {
                      // TODO Auto-generated catch block
                      e.printStackTrace();
                }
                //获取当前线程对象的名字 
                String name = Thread.currentThread().getName();
                System.out.println(name+"正在卖:"+ticket--);
            }
        }
    }

     3.5 Lock锁

    java.util.concurrent.locks.Lock机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。

    Lock锁也称同步锁,加锁与释放锁方法化了,如下:

    • public void lock():加同步锁。

    • public void unlock():释放同步锁。

    使用如下:

     1 public class Ticket implements Runnable{
     2     private int ticket = 100;
     3     
     4     Lock lock = new ReentrantLock();
     5     /*
     6      * 执行卖票操作
     7      */
     8     @Override
     9     public void run() {
    10         //每个窗口卖票的操作 
    11         //窗口 永远开启 
    12         while(true){
    13             lock.lock();
    14             if(ticket>0){//有票 可以卖
    15                 //出票操作 
    16                 //使用sleep模拟一下出票时间 
    17                 try {
    18                     Thread.sleep(50);
    19                 } catch (InterruptedException e) {
    20                     // TODO Auto-generated catch block
    21                     e.printStackTrace();
    22                 }
    23                 //获取当前线程对象的名字 
    24                 String name = Thread.currentThread().getName();
    25                 System.out.println(name+"正在卖:"+ticket--);
    26             }
    27             lock.unlock();
    28         }
    29     }
    30 }

    4. 线程状态

     4.1线程状态概述

     当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,有几种状态呢?在API中java.lang.Thread.State这个枚举中给出了六种线程状态:

    线程状态 导致状态发生的条件
    NEW(新建) 线程刚被创建,但是并未启动。还没调用start方法。
    Runnable(可运行)
    线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器。
    Blocked(锁阻塞) 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。
    Waiting(无限等待) 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。
    Timed Waiting(计时等待)
    同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、Object.wait。
    Teminated(被终止) 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。

      实际中其实不需要去研究这几种状态的实现原理,只需知道在做线程操作中存在这样的状态。那应该怎么去理解这几个状态呢,新建与被终止还是很容易理解的,以下就研究一下线程从Runnable(可运行)状态与非运行状态之间的转换问题。

     4.2 睡眠sleep方法

      以上状态中有一个状态叫做计时等待,可以通过Thread类的方法来进行演示.

      public static void sleep(long time) :让当前线程进入到睡眠状态,到毫秒后自动醒来继续执行

    1 public class Test{
    2   public static void main(String[] args){
    3     for(int i = 1;i<=5;i++){
    4           Thread.sleep(1000);//调用sleep()方法,设置休眠参数为1000毫秒
    5         System.out.println(i)   
    6     } 
    7   }
    8 }

      这时会发现主线程执行到sleep方法会休眠1秒后再继续执行。

     4.3 等待和唤醒

    Object类的方法

    public void wait() : 让当前线程进入到等待状态 此方法必须锁对象调用.

     1 public class Demo1_wait {
     2     public static void main(String[] args) throws InterruptedException {
     3        // 步骤1 : 子线程开启,进入无限等待状态, 没有被唤醒,无法继续运行.
     4         new Thread(() -> {
     5             try {
     6 
     7                 System.out.println("begin wait ....");
     8                 synchronized ("") {
     9                     "".wait();
    10                 }
    11                 System.out.println("over");
    12             } catch (Exception e) {
    13             }
    14         }).start();
    15     }

    public void notify() : 唤醒当前锁对象上等待状态的线程 此方法必须锁对象调用.

     1 public class Demo2_notify {
     2     public static void main(String[] args) throws InterruptedException {
     3        // 步骤1 : 子线程开启,进入无限等待状态, 没有被唤醒,无法继续运行.
     4         new Thread(() -> {
     5             try {
     6 
     7                 System.out.println("begin wait ....");
     8                 synchronized ("") {
     9                     "".wait();
    10                 }
    11                 System.out.println("over");
    12             } catch (Exception e) {
    13             }
    14         }).start();
    15 
    16         //步骤2:  加入如下代码后, 3秒后,会执行notify方法, 唤醒wait中线程.
    17         Thread.sleep(3000);
    18         new Thread(() -> {
    19             try {
    20                 synchronized ("") {
    21                     System.out.println("唤醒");
    22                     "".notify();
    23                 }
    24             } catch (Exception e) {
    25             }
    26         }).start();
    27     }
    28 }

     5.线程池

     5.1线程池的思想

      在使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题:

      如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。

      那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务?

      在Java中可以通过线程池来达到这样的效果。接下来就来详细讲解一下Java的线程池。

    5.2 线程池的概念

      • 线程池:其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。

      由于线程池中有很多操作都是与优化资源相关的,这里就不多赘述。可以通过一张图来了解线程池的工作原理:

     

      合理利用线程池能够带来三个好处:

      1. 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。

      2. 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。

      3. 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。

      5.2 线程池的使用

    Java里面线程池的顶级接口是java.util.concurrent.Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是    java.util.concurrent.ExecutorService

    要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在java.util.concurrent.Executors线程工厂类里面提供了一些静态工厂,生成一些常用的线程池。官方建议使用Executors工程类来创建线程池对象。

     

    Executors类中有个创建线程池的方法如下:

    • public static ExecutorService newFixedThreadPool(int nThreads):返回线程池对象。(创建的是有界线程池,也就是池中的线程个数可以指定最大数量)

      获取到了一个线程池ExecutorService 对象,那么怎么使用呢,在这里定义了一个使用线程池对象的方法如下:

    • public Future<?> submit(Runnable task):获取线程池中的某一个线程对象,并执行

    Future接口:用来记录线程任务执行完毕后产生的结果。

    使用线程池中线程对象的步骤:

      1. 创建线程池对象。

      2. 创建Runnable接口子类对象。(task)

      3. 提交Runnable接口子类对象。(take task)

      4. 关闭线程池(一般不做)。

    Runnable实现类代码:

     1 public class MyRunnable implements Runnable {
     2     @Override
     3     public void run() {
     4         System.out.println("我要一个教练");
     5         try {
     6             Thread.sleep(2000);
     7         } catch (InterruptedException e) {
     8             e.printStackTrace();
     9         }
    10         System.out.println("教练来了: " + Thread.currentThread().getName());
    11         System.out.println("教我游泳,交完后,教练回到了游泳池");
    12     }
    13 }

     

     1 //测试类
     2 public class ThreadPoolDemo {
     3     public static void main(String[] args) {
     4         // 创建线程池对象
     5         ExecutorService service = Executors.newFixedThreadPool(2);//包含2个线程对象
     6         // 创建Runnable实例对象
     7         MyRunnable r = new MyRunnable();
     8 
     9         //自己创建线程对象的方式
    10         // Thread t = new Thread(r);
    11         // t.start(); ---> 调用MyRunnable中的run()
    12 
    13         // 从线程池中获取线程对象,然后调用MyRunnable中的run()
    14         service.submit(r);
    15         // 再获取个线程对象,调用MyRunnable中的run()
    16         service.submit(r);
    17         service.submit(r);
    18         // 注意:submit方法调用结束后,程序并不终止,是因为线程池控制了线程的关闭。
    19         // 将使用完的线程又归还到了线程池中
    20         // 关闭线程池
    21         //service.shutdown();
    22     }
    23 }

     

     

     

    Callable测试代码:

    • <T> Future<T> submit(Callable<T> task) : 获取线程池中的某一个线程对象,并执行.

      Future : 表示计算的结果.

    • V get() : 获取计算完成的结果。

     1 public class ThreadPoolDemo2 {
     2     public static void main(String[] args) throws Exception {
     3         // 创建线程池对象
     4       ExecutorService service = Executors.newFixedThreadPool(2);//包含2个线程对象
     5 
     6         // 创建Runnable实例对象
     7         Callable<Double> c = new Callable<Double>() {
     8             @Override
     9             public Double call() throws Exception {
    10                 return Math.random();
    11             }
    12         };
    13 
    14         // 从线程池中获取线程对象,然后调用Callable中的call()
    15         Future<Double> f1 = service.submit(c);
    16         // Futur 调用get() 获取运算结果
    17         System.out.println(f1.get());
    18 
    19         Future<Double> f2 = service.submit(c);
    20         System.out.println(f2.get());
    21 
    22         Future<Double> f3 = service.submit(c);
    23         System.out.println(f3.get());
    24     }
    25 }

    6.死锁

     6.1什么是死锁

    死锁,就是指两个线程都在等待彼此先完成,造成了程序的停滞状态,一般程序的死锁都是在程序的运行时出现的。

    以下是死锁代码:

     1 public class Demo05 {
     2     public static void main(String[] args) {
     3         MyRunnable mr = new MyRunnable();
     4 
     5         new Thread(mr).start();
     6         new Thread(mr).start();
     7     }
     8 }
     9 
    10 class MyRunnable implements Runnable {
    11     Object objA = new Object();
    12     Object objB = new Object();
    13 
    14     /*
    15     嵌套1 objA
    16     嵌套1 objB
    17     嵌套2 objB
    18     嵌套1 objA
    19      */
    20     @Override
    21     public void run() {
    22         synchronized (objA) {
    23             System.out.println("嵌套1 objA");
    24             synchronized (objB) {// t2, objA, 拿不到B锁,等待
    25                 System.out.println("嵌套1 objB");
    26             }
    27         }
    28 
    29         synchronized (objB) {
    30             System.out.println("嵌套2 objB");
    31             synchronized (objA) {// t1 , objB, 拿不到A锁,等待
    32                 System.out.println("嵌套2 objA");
    33             }
    34         }
    35     }
    36 }

    总结:死锁就是多个线程互相进入僵持状态,互相在等待对方释放资源。死锁是危险的,死锁以后只能重启或者想办法释放资源。在开发过程中需要考虑避免死锁的出现

     

  • 相关阅读:
    Spring Boot2 系列教程(二十)Spring Boot 整合JdbcTemplate 多数据源
    Spring Boot 如何给微信公众号返回消息
    Spring Boot2 系列教程(十九)Spring Boot 整合 JdbcTemplate
    Spring Boot2 系列教程(十八)Spring Boot 中自定义 SpringMVC 配置
    Spring Boot 开发微信公众号后台
    Spring Boot2 系列教程(十七)SpringBoot 整合 Swagger2
    Spring Boot2 系列教程(十六)定时任务的两种实现方式
    Spring Boot2 系列教程(十五)定义系统启动任务的两种方式
    Spring Boot2 系列教程(十四)CORS 解决跨域问题
    JavaScript二维数组
  • 原文地址:https://www.cnblogs.com/XJP-now/p/10473852.html
Copyright © 2011-2022 走看看