Java中的多线程基础知识

JavaSE回顾-多线程基础知识整合

多线程

基本概念

程序(program)

  • 为了完成特定任务、用某种语言编写的一组指令的集合。即一段静态的代码静态对象

进程(process)

  • 程序的一次执行过程,或者是正在运行的一个程序,是动态过程。

  • 动态过程:有自身的产生、存在和消亡的过程。

  • 程序是静态的,而进程是动态的

线程(thread)

  • 进程可以进一步细化为线程,是一个程序内部的一条执行路径

  • 若一个程序可同一时间执行多个线程,那么这个程序就支持多线程。

Java中的线程

  • Java线程是依附于Java虚拟机中的本地线程来运行的,实际上是本地线程在执行Java线程代码,只有本地线程才是真正的线程实体。
  • Java代码中创建一个thread,虚拟机在运行期就会创建一个对应的本地线程,而这个本地线程才是真正的线程实体

多线程的用处

  • 发挥多核CPU的优势,大大提高效率
  • 防止多余的阻塞
    • 单核CPU不但不会发挥出多线程的优势,反而会因为在单核CPU上运行多线程导致线程上下文的切换,而降低程序整体的效率 ,但是单核CPU我们还是要应用多线程,就是为了防止阻塞 。
    • 多条线程同时运行,哪怕一条线程的代码执行读取数据阻塞,也不会影响其它任务的执行
  • 方便进行业务拆分,提升应用性能

时间片

  • 时间片是CPU调度给各个线程的时间。

并行和并发

  • 并行:多个任务同时进行,必须有多核CPU的支持
  • 并发:指多个任务都请求运行,而处理器只能接受一个任务,就是把多个任务轮流进行,由于轮转时间间隔过短,让人感觉是多个任务都在同时运行。

同步和异步

  • 同步和异步通常用来形容一次方法调用。
  • 同步方法调用一开始,调用者必须等待被调用的方法执行结束后,才能执行后面的代码。
  • 异步方法调用后,调用者不用理会调用方法是否执行完毕,都会继续执行后面的代码,当被调用的方法完成之后会通知调用者。

临界区

  • 临界区表示一种公共资源,共享数据,可以被多个线程使用。
  • 一旦临界区资源被一个线程占用时其他线程必须等待其使用完毕后才能使用。

阻塞和非阻塞

  • 阻塞和非阻塞通常用来形容多线程之间的相互影响。
  • 当一个线程占用了临界区资源,那么其他线程需要这个资源就必须等待资源被那个线程释放,这就会导致等待的线程挂起,这种情况就是阻塞。
  • 非阻塞强调没有一个线程可以阻塞到其他线程,所有的线程都会尝试地往前运行。和阻塞正好相反。

问题引入

  1. Java程序的运行原理?

    • Java命令会启动Java虚拟机,启动JVM,等于启动了一个应用程序,也就是启动了一个进程。这个进程会自动的启动一个主线程,然后主线程调用类中的main方法
    • 实际上java程序天生就是一个多线程程序,包含了:
      • (1)分发处理发送给给JVM信号的线程
      • (2)调用对象的finalize方法的线程
      • (3)清除Reference的线程;
      • (4)main线程,用户程序的入口
  2. JVM的启动是多线程吗?

    JVM的启动至少启动主线程垃圾回收线程,所以是多线程的。

多线程的实现方式

方式一: 继承Thread类

  • Thread类实现了Runnable接口,在java.long包下。

  • 创建执行线程方法一:将类继承Thread类,重写Thread类的run方法。接下来就可以分配并启动该子类的实例。

  • 具体步骤:

    1. 继承Thread类
    2. 重写run方法
    3. 将执行的代码写在run方法中
    4. 创建Thread类的子类对象
    5. 使用start方法开启线程。
  • 注意:调用run方法不能开启多线程

  • 只有调用了start()方法,才会表现出多线程的特性,不同线程的run()方法里面的代码交替执行。如果只是调用run()方法,那么代码还是同步执行的必须等待一个线程的run()方法里面的代码全部执行完毕之后另外一个线程才可以执行其run()方法里面的代码。

  • 一个线程不能多次开启是非法的

  • 代码示例:

    public class ThreadTest {
    
      public static void main(String[] args) {
          //4,创建Thread类的子类对象
          MyThread mt = new MyThread();
          mt.start();//5,使用start方法开启线程
          for (int i = 0; i < 10000; i++) {
              System.out.println("main" + i);
          }
      }
    }
    class  MyThread extends  Thread{ //1.继承Thread类
    
      //2,重写run方法
      @Override
      public void run(){
          //3,将执行的代码写在run方法中
          for (int i = 0; i <10000 ; i++) {
              System.out.println("mt"+i);
          }
      }
    }
    

方式二:实现Runnable接口(常用,优点多)

  • 声明实现Runnable接口的类,实现Runnable接口中仅有的run方法,然后分配实例对象,在创建Thread时作为一个参数来传递并启动。

  • 具体步骤

    • 1,定义类实现Runnable接口
    • 2,在该类中实现Runnable接口中的run()方法
    • 3,线程中具体要执行的东西写在run()方法中
    • 4,创建Thread类的对象,并在该对象中传入该实现Runnable接口的对象作参数
    • 5,Thread类的对象调用start()方法开启新线程,其内部会自动的调用run方法
    public class RunnableTest {
      public static void main(String[] args) {
          MyRunnable  mr =  new MyRunnable(); //4、创建自己定义的Runnable实现类的对象
          Thread  thread = new Thread(mr); //5、创建Thread类的对象,并将自定义Runnable实现类的对象作为参数传递给Thread的构造函数
          thread.start(); //使用thread类的start方法开启线程。
    
          for (int i = 0; i < 1000; i++) {
              System.out.println("main+"+i);
          }
      }
    }
    //1、定义一个Runnable实现类
    class MyRunnable implements Runnable{
      //2、实现Runnable接口中的抽象方法
      @Override
      public void run() {
          //3、在run方法中写入要使用多线程的具体方法
          for (int i = 0; i <1000; i++) {
              System.out.println("mr"+i);
          }
      }
    }
    
  • 实现Runnable接口方式的实现原理

    • 1、查看Thread 类的构造函数,传递了Runnable接口的引用,直接调用了init方法。
     public Thread(Runnable target) {
            init(null, target, "Thread-" + nextThreadNum(), 0);
        }
    • 2、追踪init方法,在init方法体中找到了传递的target参数,赋值给了Thread类的Runnable接口的成员变量的target
    this.target = target;
     /* What will be run. */
        private Runnable target;
    • 3、查看run方法时,发现run方法中有判断,如果target不为null就会调用实现Runnable接口子类对象的run方法
        @Override
        public void run() {
            if (target != null) {
                target.run();
            }
        }

为什么实例效果不明显?

  • 多线程指的是多个线程的代码块可以同时运行,而不必一个线程去等待另一个线程执行完才可以进行。
  • 对于单核CPU来说,无法做到真正意义上的多线程特性。只能会让用户看起来像是同时执行的,因为每个时间点上,CPU都会执行特定的代码,由于CPU执行代码时间非常快,多个线程代码块就会轮询执行,速度很快,但是同一个线程进行的轮询操作。
  • 具体执行某段代码多长时间和分时机制系统密切相关。
  • 分时系统CPU时间划分为多个时间片,操作系统以时间片为单位执行各个线程的代码,时间片越小,执行效率越高。

多线程的两种实现方式的区别

  • 源码中的区别
    • 继承Thread类方式:由于子类重写了Thread类的run(),当调用start()时,直接找子类的run()方法(Java虚拟机自动完成)
    • 实现Runnable方式:构造函数中传入了Runnable的引用,传给了Thread类中的成员变量,start()调用了run()方法时的内部判断成员变量Runnable的引用是否为空,若不为空,编译时看的是Runnable的run(),运行时执行的是具体实现类中的run()
  • 优缺点:
    • 继承Thread类方式
    • 好处:可以直接使用Thread类中的方法,代码简单
    • 弊端:同样也是面向对象中的继承的缺点:如果该具体类已经有了其他的父类,那么就不能多重继承Thread类,就不能使用这种方法。此时面向接口编程的优势脱颖而出。
    • 实现Runnable接口方式
    • 好处:即继承的弊端:即使自己定义的线程类有了其他父类也可以实现该Runnable接口。Java中的接口是多实现的,继承是单继承,比较有局限性。
    • 弊端:不能直接使用Thread类中的方法,需要先把Runnable具体实现类对象传递给Thread类并获取到线程对象后,才能得到Thread类的方法,代码相对复杂

匿名内部类实现线程的两种方式

即直接使用匿名内部类的方式简化代码:

  • 继承Thread类方式

    //匿名内部类
    new Thread(){
      @Override
      public void run() {
          for (int i = 0; i < 1000; i++) {
              System.out.println("t+"+i);
          }
      }
    }.start();
  • 实现Runnable接口方式

    //匿名内部类
    new Thread(new Runnable() {
      @Override
      public void run() {
          for (int i = 0; i <1000; i++) {
              System.out.println("mr"+i);
          }
      }
    }).start();

    Runnable接口是一个函数式接口,可以直接用Lambda表达式代替:

    //Lambda表达式
    new Thread(()->{
      for (int i = 0; i <1000; i++) {
          System.out.println("mr"+i);
      }
    }).start();

方式三:实现Callable接口

  • 步骤:
    1. 创建实体类,实现Callable接口
    2. 实现接口中的call()方法
    3. 利用 ExecutorService线程池对象 的 <T> Future<T> submit(Callable<T> task()方法提交该Callable接口的线程任务。
// 创建线程池
ExecutorService pool = Executors.newFixedThreadPool(2);
// 可以执行Runnable对象或者Callable对象代表的线程
Future<Integer> f1 = pool.submit(new MyCallable(100));
Future<Integer> f2 = pool.submit(new MyCallable(200));

// V get()
Integer i1 = f1.get();
Integer i2 = f2.get();

System.out.println(i1);
System.out.println(i2);
// 结束
pool.shutdown();
public class MyCallable implements Callable<Integer> {

    private int number;
    public MyCallable(int number) {
        this.number = number;
    }
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int x = 1; x <= number; x++) {
            sum += x;
        }
        return sum;
    }

}
  • 利用匿名内部类方式:
ExecutorService service = Executors.newSingleThreadExecutor();
         Future<String> future = service.submit(new Callable() {
             @Override
             public String call() throws Exception {
                 return "通过实现Callable接口";
             }
         });
         try {
             String result = future.get();
             System.out.println(result);
         } catch (InterruptedException e) {
             e.printStackTrace();
         } catch (ExecutionException e) {
             e.printStackTrace();
         }
  • Lambda表达式方式:
public class CallableTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //使用Executors工厂类创建一个单线程池
        ExecutorService es =  Executors.newSingleThreadExecutor();
        //使用这个单线程提交一个Callable接口线程服务,返回值为String
        //Callable接口是一个函数式接口,Java8开始可以直接使用Lambda表达式表示
        //其内部实现了call()方法  V call() throws Exception;
        //并得到该结果值打印
        System.out.println( es.submit(()->"使用lambda表达式的Callable接口").get());
        es.shutdown(); //关闭该线程池
    }

}
  • 实现callable接口,提交给ExecutorService返回值异步执行的。
  • 该方式的优缺点:
    • 优点:
    • 有返回值
    • 可以抛出异常
    • 缺点:
    • 代码较复杂,需要利用线程池

线程相关实例方法

获取线程ID- getId

  • 在一个Java应用程序中,有一个long型的全局唯一的线程ID生成器threadSeqNumber,每new出来一个线程就会自增一次,从0开始,并且赋值给线程的tid属性。
  • 用户只能获取ID,不能执行一个线程的ID,这是Thread类内部自己完成的。

获取和设置线程的名字

  • 获取线程名

    • 通过getName()方法获取线程对象名
    new Thread(){
        @Override
        public void run() {
            System.out.println(this.getName());//Thread-0
        }
    }.start();
  • 设置线程名

    • 通过构造函数传入String类型名
    new Thread("线程1"){
        @Override
        public void run() {
            System.out.println(this.getName());//线程1
        }
    }.start();
    
    new Thread("线程2"){
        @Override
        public void run() {
            System.out.println(this.getName());//线程2
        }
    }.start();
    
    //Lambda表达式的Runnable方式,Thread的构造函数
    Thread  t2 = new Thread(() ->
                            System.out.println("线程5的执行方法体"),"线程5");
    t2.start();
    System.out.println(t2.getName());//线程5
    • 通过setName(String name)方法设置
    new Thread(){
        @Override
        public void run() {
            this.setName("线程3");
            System.out.println(this.getName());//线程3
        }
    }.start();
    Thread t1 = new Thread() {
        @Override
        public void run() {
            System.out.println(this.getName());//线程4
        }
    };
    t1.setName("线程4");
    t1.start();
    Thread t1 = new Thread(()-> System.out.println("线程4的执行方法体"));
    t1.setName("线程4");
    t1.start();
    System.out.println(t1.getName());//线程4
    /*
            线程4
            线程4的执行方法体
      */

线程对象是否处于活动状态 - isAlive

  • t.isAlive() 测试线程t是否处于活动状态,只要线程启动并且没有终止,方法返回值就是true
  • start()之前,线程不处于活动状态,之后就处于了活动状态。

获取当前线程的对象

  • Thread.currentThread() 静态方法,获取当前执行线程, 主线程也可以获取

    //Runnable接口方式
    //new Thread(Runnable target,String threadName) 构造方法
    new Thread(()-> System.out.println(Thread.currentThread().getName()),"线程6")
                  .start();//线程6

    在main方法中可以获取主线程对象并设置:

    Thread.currentThread().setName("我是主线程");
    System.out.println(Thread.currentThread().getName());//我是主线程

休眠线程-sleep

  • Thread.sleep(毫秒) / Thread.sleep(毫秒,纳秒) 控制当前线程休眠若干毫秒

    • 1秒 = 1000毫秒
    • 1秒 = 1000 * 1000 * 1000 纳秒 (100,000,000)
    new Thread(()->{
      for(int i = 0; i < 10 ;i++){
          System.out.println(Thread.currentThread().getName());
          try{
              Thread.sleep(1000); //每个线程休眠1秒(1000毫秒)
          }catch(InterruptedException e){
              e.printStackTrace();
          }
      }
    },"测试线程1").start();
  • sleep方法不会释放锁,wait方法会释放锁

加入线程-join

  • join() 当前线程暂停,等待指定的线程执行结束后,当前线程才能再继续。即把指定的线程插队处理。
  • join(int ms) 可以等待指定的毫秒后再继续。
  • join()方法会使调用该方法的线程处于运行状态,让一开始所在的线程处于无限阻塞状态,直到调用了join方法的线程执行完毕,线程销毁为止。
  • 下面这个例子中,t2线程处于了阻塞状态,直到t1线程的run()方法执行完,线程死亡状态,t2线程才可以运行。
public static void main(String[] args) {
    //Java8中 匿名内部类调用外接方法和变量时,外接变量可以不用final修饰,但是其值还是不能改变
    Thread t1 = new Thread() { //此时的t1在Java8之前必须用final修饰,是不可变的
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName() + "aaa");
            }
        }
    };

    Thread t2 = new Thread() {
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                if (i == 2) {
                    try {
                        //Java8中 匿名内部类调用外接方法和变量时,外接变量可以不用final修饰,但是其值还是不能改变
                        t1.join();//t1线程对象来插队了,t1执行完之后t2才能继续执行
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(Thread.currentThread().getName() + "bbb");
            }
        }
    };
    t1.start();
    t2.start();
}

执行结果:

Thread-1bbb
Thread-1bbb
Thread-0aaa
Thread-0aaa
Thread-0aaa
Thread-0aaa
Thread-0aaa
Thread-0aaa
Thread-0aaa
Thread-0aaa
Thread-0aaa
Thread-0aaa
Thread-1bbb
Thread-1bbb
Thread-1bbb
Thread-1bbb
Thread-1bbb
Thread-1bbb
Thread-1bbb
Thread-1bbb

结果显示:当t2线程执行两个后,t1使用join方法来插队,t1执行完之后,t2才继续执行完。

让出线程-yield

  • Thread.yield() 使该线程让出cpu,给其他线程使用cpu执行
  • yield只会把时间片让给同优先级的线程
  • 使CPU调度到其他线程,让该线程从运行状态回到可运行状态

设置线程优先级

  • thread.setPriority(int priority) 设置线程的优先级

    • Thread类源码中有三种优先级:(1,5,10)
    /**
         * The minimum priority that a thread can have.
         */
    public final static int MIN_PRIORITY = 1;
    
    /**
         * The default priority that is assigned to a thread.
         */
    public final static int NORM_PRIORITY = 5;
    
    /**
         * The maximum priority that a thread can have.
         */
    public final static int MAX_PRIORITY = 10;
    • 优先级值:默认为5,最大为10,最小为1;

    • 不能超过1~10这个范围。

    t1.setPriority(Thread.MIN_PRIORITY);//最小
    t1.setPriority(Thread.MAX_PRIORITY);//最大

中断线程-Interrupt

  • 中断可以理解为线程的一个标志位,它表示了一个运行中的线程是否被其他线程进行了中断操作
  • 其他线程可以调用该线程的interrupt()方法对其进行中断操作,同时该线程可以调用isInterrupted()来感知其他线程对其是否进行了中断操作,从而做出相应。
  • 也可以调用Thread中的静态方法interrupted()对当前线程进行中断操作,该方法会清除中断标志位
  • 当抛出InterruptedException时,会清除中断标志位,也就是说在调用isInterrupted会返回false。
  • 如果线程调用了 wait()、sleep()、join()方法而导致的阻塞,可以中断线程,并抛出InterruptedException来唤醒
方法名 作用 备注
public void interrupt() 中断该线程对象 如果线程调用了 wait()、sleep()、join()方法而导致的阻塞,可以中断线程,并抛出InterruptedException来唤醒,并且中断标志位会被清除
public boolean isInterrupted() 测试该线程对象是否被中断 中断标志位不会被清除
public static boolean interrupted() 测试当前线程是否被中断 中断标志位会被清除

守护线程-Deamon

  • setDaemon(boolean on) 设置一个线程作为守护线程

  • 守护线程为其他线程的运行提供便利的服务,最典型的应用便是GC线程 。

  • 该线程不会单独执行,当其他非守护线程都执行结束后,守护线程就没有可服务的对象了,就会自动退出

    public static void main(String[] args) {
      Thread t1 = new Thread(()->{
          for (int i = 0; i < 3; i++) {
              System.out.println(Thread.currentThread().getName()+"非守护线程");
          }
      });
    
      Thread t2 = new Thread(()->{
          for (int i = 0; i < 30; i++) {
              System.out.println(Thread.currentThread().getName()+"守护线程");
          }
      });
    
      t2.setDaemon(true);//将t2设置成守护线程
      t1.start();
      t2.start();
    
    }
    • 第一次执行结果:
    Thread-0非守护线程
    Thread-0非守护线程
    Thread-0非守护线程
    
    • 说明:非守护线程直接执行完毕后,守护线程还未开启执行,就自动退出了。

    • 第二次执行结果:

    Thread-0非守护线程
    Thread-1守护线程
    Thread-1守护线程
    Thread-0非守护线程
    Thread-0非守护线程
    Thread-1守护线程
    Thread-1守护线程
    Thread-1守护线程
    Thread-1守护线程
    Thread-1守护线程
    • 根据结果发现,守护线程和非守护线程穿插执行,非守护线程执行完之后,守护线程继续执行了,没有立即停止,该现象为线程缓冲,即守护线程正在执行,需要等到非守护线程的执行完毕信号后,才能停止下来,自动退出。

wait()和notify()/notifyAll()

Object类中的wait()、notify()、notifyAll()三个方法,每个对象都是有的,结合多线程后可以起到很大的效果。

wait()

  • wait()方法作用是使当前执行的代码的线程进行等待,当前线程会进入等待队列中。
  • wait()代码处会停止执行,直到接到通知(notify())或者被中断(Interrupt())。
  • 调用wait()之前线程必须获取该对象的锁,因此wait()方法只能在同步代码中调用执行。
  • wait()方法可以使调用该线程的方法释放共享资源的锁,然后从运行状态退出,进入等待队列,直到再次被唤醒。

notify()

  • 唤醒等待的线程,如果有多个线程在等待队列中,那么会随机挑选一个等待的线程,对其发出唤醒通知,并且使它等待获取该对象的对象锁
  • 等待获取对象锁说明了即使收到了通知,wait 的线程也不会马上获取对象锁,会在锁池中进行等待notify方法的线程释放锁才可以,获取了对象锁之后才能从锁池中出去进入可运行状态
  • 在调用notify()之前,和wait()一样,必须在同步代码中调用。因为有锁的操作。
  • notify()不释放锁

notifyAll()

  • notifyAll()方法可以使所有正在等待队列中等待同一共享资源的全部线程从等待状态退出,随机进入锁池,等待拿到对象锁,进入可运行状态。

如果wait()方法和notify()/notifyAll()方法不在同步方法/同步代码块中被调用,那么虚拟机会抛出java.lang.IllegalMonitorStateException

☆ sleep()和wait()的区别

  1. 方法本质上:

    • wait()方法时Object类中的实例方法。可以传入参数,也可以不传入参数。
    • 而sleep()方法时Thread类中的静态方法。必须传入参数ms值。
  2. 使用环境上:

    • wait()方法必须要在同步方法或同步代码块中使用,因为它必须已经获得对象锁。
    • sleep()方法没有这个限制,它可以在任何地方使用。
  3. 是否释放锁:

    • wait()方法会释放占有的对象锁,使该线程进入等待池中。
    • sleep()方法不会释放对象锁,只会让出CPU
  4. 使其继续执行方式上:

    • wait()方法必须等待 notify()/notifyAll()方法的唤醒通知后,才会离开等待池并且如果再次获得CPU时间片才会继续执行。

    • sleep()方法在休眠时间到达后,如果再次获得CPU时间片就会继续执行。

Java中用到的线程调度算法

  • Java中用到的是抢占式的线程调度算法。一个线程用完CPU后,操作系统会根据线程优先级、线程饥饿程度等数据算出一个总的优先级并分配下一个时间片给某个线程。

Thread.sleep(0)的作用?

  • 平衡CPU控制权的一种操作:
    • 由于Java采用的是抢占式线程调度算法,因此可能就会出现某条线程综合来看常常会获取到CPU的控制权的情况,为了让某些优先级较低的线程也能获得到CPU控制权,可以使用Thread.sleep(0)手动出发一次操作系统分配时间片的操作,来平衡控制权。

线程六大状态

根据Thread类中定义的枚举类型State值,可以看出有6中状态:

    public enum State {
        NEW,
        RUNNABLE,
        WAITING,
        TIMED_WAITING,
        TERMINATED;
    }
  1. 新建状态 NEW

    新建了Thread类对象,但是没有启动的线程。new Thread()

  2. 可运行状态 RUNNABLE

    线程对象新建后,调用start()方法即处于了RUNNABLE状态。

    • 此状态线程可能在Java虚拟机中运行;
    • 可能在等待CPU处理器分配资源。
    • 一个线程只有获取到CPU的资源后,才可以运行其run()方法执行代码,否则就会处于排队等待
  3. 阻塞状态 BLOCKED

    该线程正在等待同步锁来进入一个同步代码块中来使用CPU资源,此时该线程就处于阻塞状态。

  4. 等待状态 WAITING

    线程调用以下方法时,会自己进入等待状态:

    • 不带超时的Object类中的wait()方法
    • 不带超时的Thread类中的join()方法
    • LockSupport类中的park()方法

    一直等待,直到手动唤醒

  5. 超时等待状态 TIMED_WAITING

    线程调用带有正的等待时间参数的下列各方法时,会处于超时等待状态:

    • Object中的wait()
    • Thread中的join()
    • Thread中的sleep()
    • LockSupport中的parkNanos()
    • LockSupport中的parkUntil()
  6. 终止状态 TERMINATED

    • 线程执行完毕,或run()方法全部执行结束后,线程进入终止状态。
    • 终止状态的线程不具备继续运行的能力。

线程状态图

Java线程状态转换图

  • 锁池队列:当资源被一个线程访问时,上锁后,其他线程就会进入锁池队列,当锁释放后,其他线程获得了锁,就会变成可运行状态。
  • 《Thinking in Java》中线程被阻塞的五种可能原因:
    1. 线程调用 sleep(ms) ,使线程睡眠,规定时间内,该线程不会运行。
    2. 使用suspend()暂停了线程的执行,除非收到resume()消息,否则不会进入可运行状态
    3. 线程正在等待一些IO操作完成
    4. 线程试图调用另一个对象的同步方法,但那个对象处于锁状态,暂时无法使用
    5. 调用wait()暂停了线程的执行,进入了等待队列。

怎么唤醒一个阻塞的线程

  • 如果线程调用了 wait()、sleep()、join()方法而导致的阻塞,可以中断线程,并抛出InterruptedException来唤醒
  • 如果该线程遇到了IO阻塞,只能等系统IO操作结束后,才能唤醒,Java代码无能为力,无法直接接触到底层操作系统的调度。

怎么检测一个线程是否持有对象监视器

  • Thread类提供了一个holdsLock(Object obj)方法,当且仅当对象obj的监视器被某条线程持有的时候才会返回true
public static boolean holdsLock(Object obj)

同步代码

需要同步的情况

  • 多线程并发,有多段代码同时执行时,希望某一段代码执行的过程中CPU不要切换到其他线程上,此时就需要同步。
  • 如果有两段代码是同步进行的,那么同一时间只能执行其中一段,在一段代码没执行结束之前不会执行另外一段代码。

同步代码块操作

  • 使用synchronized关键字加上一个锁对象定义一段代码,这就称为同步代码块

  • 如果多个同步代码块使用同一个锁对象,那么他们就是同步的

  • 同步代码块是锁机制,同一个锁对象,同步代码块是同步的。

  • 锁对象是任意对象,但不能是匿名对象,因为匿名对象不是同一个对象。

  • 当多个代码块使用了同一个锁对象synchronized 锁机制,只有当一个线程把 synchronized 代码块的代码全部执行完之后才能执行该同一锁对象的另一段代码

    • 即该多个代码块是同步的,同一时间只能执行其中一段,执行完之后,才能执行另一段。
    • 锁对象不一致,即不是同步的,会出现抢占线程执行的情况。
  • 具体操作:

    public class SynchronizeTest {
      public static void main(String[] args) {
          Consumer con  = new Consumer();
          new Thread(()->{
              while(true)
                  con.print1();
          }).start();
    
          new Thread(()->{
              while(true)
                  con.print2();
          }).start();
      }
    }
    
    class Consumer {
      //定义一个Object对象,作为锁对象
      Object  obj = new Object();
    
      public void print1(){
          //锁机制使用同一个锁对象
          synchronized (obj){
              System.out.print("同");
              System.out.print("步");
              System.out.print("代");
              System.out.print("码");
              System.out.print("块");
              System.out.println();
          }
      }
    
      public void  print2(){
          //锁机制使用同一个锁对象,作为同步代码块
          synchronized(obj){
              System.out.print("多");
              System.out.print("线");
              System.out.print("程");
              System.out.println();
          }
      }
    }
    

同步方法

  • 使用synchronized关键字修饰一个方法时,该方法中所有代码都是同步的。

    //同步方法只需在方法上加 synchronized 
    public synchronized void print1(){
      //锁机制使用同一个锁对象
      synchronized (obj){
          System.out.print("同");
          System.out.print("步");
          System.out.print("代");
          System.out.print("码");
          System.out.print("块");
          System.out.println();
      }
    }
  • 非静态同步函数的锁是this

    //同步方法只需在方法上加 synchronized 
    public synchronized void print1(){
    
          System.out.print("同");
          System.out.print("步");
          System.out.print("代");
          System.out.print("码");
          System.out.print("块");
          System.out.println();
    
    }
    public void  print2(){
      //非静态的同步方法的锁对象是this
      synchronized(this){
          System.out.print("多");
          System.out.print("线");
          System.out.print("程");
          System.out.println();
      }
    }
    

    此时 这两个方法时 同步的

  • 静态同步函数的锁是字节码对象

    • 静态域随着类的加载而加载,此时会产生该类的字节码对象,所以静态同步方法锁对象不能是this,而是产生的字节码对象
    public static synchronized void print1(){
    
          System.out.print("同");
          System.out.print("步");
          System.out.print("代");
          System.out.print("码");
          System.out.print("块");
          System.out.println();
    
    }
    public static void  print2(){
      //静态的同步方法的锁对象是随着类加载而产生的类的字节码对象
      synchronized(Customer.class){
          System.out.print("多");
          System.out.print("线");
          System.out.print("程");
          System.out.println();
      }
    }
    

同步方法和同步块,哪个是更好的选择

  • 基本原则:同步的范围越小越好
  • 同步块之外的代码是异步执行的,比同步整个方法更有效率。

线程安全

  • 如果你的代码在多线程下执行和在单线程下执行永远都能获得一样的结果,那么你的代码就是线程安全的

线程安全级别

  • 1、不可变

    String、Integer、Long这些,都是final类型的类,任何一个线程都改变不了它们的值,要改变除非新创建一个,因此这些不可变对象不需要任何同步手段就可以直接在多线程环境下使用

  • 2、绝对线程安全

    不管运行时环境如何,调用者都不需要额外的同步措施。要做到这一点通常需要付出许多额外的代价,Java中标注自己是线程安全的类,实际上绝大多数都不是线程安全的。不过绝对线程安全的类,Java中也有,比方说CopyOnWriteArrayListCopyOnWriteArraySet

  • 3、相对线程安全

    相对线程安全也就是我们通常意义上所说的线程安全,像Vector这种,add、remove方法都是原子操作不会被打断,但也仅限于此,如果有个线程在遍历某个Vector、有个线程同时在add这个Vector,99%的情况下都会出现ConcurrentModificationException,也就是fail-fast机制

  • 4、 线程非安全

    ArrayList、LinkedList、HashMap等都是线程非安全的类

常见的线程安全类

线程安全类 线程不安全类
Vector ArrayList
StringBuffer StringBuilder
Hashtable HashMap
LinkedList

- StringBuffer 线程安全(其append方法中加了synchronized修饰
- vector add、remove方法都是原子操作,加了synchronized修饰
- 但是Collections集合工具类中提供了静态方法synchronizedXXX(XXX),分别对应着线程不安全的那些集合类,可以让他们转换成线程安全的集合,所以Vector类淘汰了…

方法摘要 方法说明
static <T> Collection<T> synchronizedCollection(Collection<T> c) 返回指定 collection 支持的同步(线程安全的)collection。
static <T> List<T> synchronizedList(List<T> list) 返回指定列表支持的同步(线程安全的)列表。
static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) 返回由指定映射支持的同步(线程安全的)映射。
static <T> Set<T> synchronizedSet(Set<T> s) 返回指定 set 支持的同步(线程安全的)set。
static <K,V> SortedMap<K,V> synchronizedSortedMap(SortedMap<K,V> m) 返回指定有序映射支持的同步(线程安全的)有序映射。
static <T> SortedSet<T> synchronizedSortedSet(SortedSet<T> s) 返回指定有序 set 支持的同步(线程安全的)有序 set。

多线程中的线程安全问题

  • 多线程并发操作同一共享数据时,就会可能出现线程安全问题。

  • 使用同步技术可以解决这种问题, 把操作数据的代码进行同步, 就不会多个线程同时执行

多窗口卖票问题

  • 如果不开启锁同步 ,就会出现卖出票号为负数的现象
  • 在循环中使用锁同步,让各个线程进入循环之后进行同步,一个线程把ticketNum–后,其他线程再执行
  • 使用Runnable方式实现:

    public class SynchronizeTicketTest {
    
      public static void main(String[] args) {
    
          new Thread(new TicketSeller()).start();
          new Thread(new TicketSeller()).start();
          new Thread(new TicketSeller()).start();
          new Thread(new TicketSeller()).start();
    
      }
    }
    
    class TicketSeller implements Runnable{
      private static int tikcetNum = 10000;//总共10000张票,放到静态池中共享
      @Override
      public void run() {
          while(true){
              //在循环中使用锁同步,让各个线程进入循环之后进行同步,一个线程把ticketNum--后,其他线程再执行
              synchronized(TicketSeller.class){
                  if(tikcetNum <= 0)
                      break;
                  try {
                      //让线程睡10ms 如果不开启锁同步 就会出现票号为负数的现象
                      Thread.sleep(10);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
                  System.out.println(Thread.currentThread().getName() + "...这是第" + tikcetNum-- + "号票");
    
              }
          }
      }
    }
    

死锁问题

  • 线程A和线程B相互等待对方持有的锁导致程序无限死循环下去
  • 线程A持有锁H并且想获取锁W,此时线程B持有锁W并且想获取锁H,那么这两个线程AB就会永远等待下去,产生最简单的死锁。
  • 一个类可能发生死锁,并不意味着每次都会发生,往往在高并发、高负载的情况下,死锁出现概率高很多。

  • 多线程同步的时候, 如果同步代码嵌套, 使用相同锁, 就有可能出现死锁

写一个死锁程序

  • 哲学家进餐问题,使用同步代码块嵌套,互相先持有对方需要的锁对象

  • 写一个死锁程序步骤:

    1. 定义两个对象分别代表两个线程一开始就持有的锁对象
    2. 在run方法中使用 synchronized 同步代码块嵌套
    3. 外层synchronized锁对象对方所需求的自己所持有的内层synchronized锁对象对方所持有自己所需要的
    4. 当一个线程中的锁对象是自己持有的,还未走出外层代码块,需要对方所持有的锁对象时,cpu调度到了另一个线程,另一个线程正好也是这种情况,此时双方都持有了对方所需要的锁对象,发生了死锁。
    public class DeadLockTest {
      private static String left = "left one";
      private static String right = "right one";
      public static void main(String[] args) {
    
          new Thread(() -> {
              while(true){
                  synchronized (right){
                      System.out.println(Thread.currentThread().getName()+"--持有了right,想得到left");
                      synchronized(left){
                          System.out.println(Thread.currentThread().getName()+"--得到了left,可以开吃了");
                      }
                  }
              }
          }).start();
    
          new Thread(() -> {
              while(true){
                  synchronized (left){
                      System.out.println(Thread.currentThread().getName()+"--持有了left,想得到right");
                      synchronized(right){
                          System.out.println(Thread.currentThread().getName()+"--得到了right,可以开吃了");
                      }
                  }
              }
          }).start();
    
          /*
              Thread-1--持有了left,想得到right
              Thread-0--持有了right,想得到left
              执行到此时,就会发现这两个线程的锁对象谁都不想放,就会产生死锁。
           */
    
      }
    }

    结果:

    上方结果省略....
    Thread-1--持有了left,想得到right
    Thread-0--持有了right,想得到left
    

    执行到此时,就会发现这两个线程的锁对象谁都不想放,就会产生死锁。

避免死锁的方式

  • 注意和减少同步代码块嵌套问题
  • 设计时考虑清楚锁的顺序,尽量减少嵌套加锁交互数量
  • 由于死锁是因为两个或多个线程之间无限时间等待对方持有的锁对象而形成的,那么给同步代码块加个等待时间限制。
    • synchronized 关键字 不具备这个功能,使用Lock类中的tryLock方法,指定一个超时时限,在等待时,若超过该时限,就返回一个失败信息结束阻塞。

单例模式的线程安全问题

单例模式

  • 单例设计模式:保证一个类在内存中只有一个对象,内存唯一。
  • 保证类在内存中只有一个对象:
    • 1、控制类的创建,不让其他类来创建本类的对象,将本类的构造函数私有private
    • 2、在本类中定义一个本类的对象,并且外界无法修改。
    • 3、在本类中提供一个唯一的公共访问方法,可获取本类的对象。

饿汉式

  • 在类中直接创建一个不可修改的对象引用,不管有没有调用,都创建,空间换时间
  • 饿汉式在多线程环境下是线程安全的。
class Singleton {
    //1.将本类的构造函数私有private
    private  Singleton (){}
    //2. 在本类中定义一个本类的对象,并且外界无法修改。
    private  static Singleton s = new Singleton();

    //3. 在本类中提供一个唯一的公共访问方法,可获取本类的对象
    //饿汉式
    public static Singleton getInstance(){
        return s ;
    }
}

另一种饿汉式,利用final直接修饰

class Singleton {
    //1.将本类的构造函数私有private
    private  Singleton (){}
    //2. 在本类中定义一个本类的对象,并且外界无法修改。
    public final static Singleton s = new Singleton() ;

}

懒汉式

  • 在类中获取对象时加以判断,为空时才创建,即用到该类对象时才创建,时间换空间。
  • 懒汉式单例模式在多线程下是非线程安全的。
    • 当线程A判断为null时,正准备new,此时,被另一个线程B抢占了CPU资源,线程B也判断为null,new了之后,第一个线程A又抢回了CPU资源,此时线程A又new了。此时这两个线程就new了两次,就不是唯一的内存引用了。
class Singleton {
    //1.将本类的构造函数私有private
    private  Singleton (){}
    //2. 在本类中定义一个本类的对象,并且外界无法修改。
    private  static Singleton s ;
    //3. 在本类中提供一个唯一的公共访问方法,可获取本类的对象
    //懒汉式 对象引用为空 才创建,
    public static Singleton getInstance(){
        //用到时创建,用不到时不创建
        if(s == null)
             s = new Singleton() ;
        return s;
    }
}

饿汉式和懒汉式的区别

  • 线程安全上:
    • 饿汉式线程安全,多线程下也不会创建多个对象
    • 懒汉式非线程安全,多线程下可能会创建多个对象
  • 执行效果:
    • 饿汉式是 空间换时间,执行速度快。
    • 懒汉式是 时间换空间,延迟加载。

互斥锁

  • JDK1.5版本提供了java.util.concurrent.locks包,该包中提供了锁和等待条件的接口和类,可以用于代替JDK1.5之前的synchronized同步和监视器机制
  • 互斥锁指的是一次最多只能有一个线程持有的锁
  • 互斥锁在Java中的体现是Lock接口和其实现类ReentrantLock
  • Lock接口的出现主要替代了synchronized关键字的用处,其提供了一个比sychronized机制更广泛的锁定操作

Lock和sychronized机制的主要区别

  • synchronized机制提供了对于每个对象相关的隐式监视器锁的访问,并强制所有锁获取和释放都要出现在一个块结构中
  • 获取了多个锁时(多个synchronized代码块嵌套时),它们必须以相反的顺序释放
  • synchronized机制对锁的释放是隐式的,只要线程运行的代码超出了synchronized语句块范围,持有的锁对象就会自动释放
  • 锁定和取消锁定出现在不同作用范围中时,必须谨慎地确保保持锁定时所执行的所有代码用 try-finally 或 try-catch 加以保护,以确保在必要时释放锁
  • Lock机制必须是显式调用Lock对象的unlock()方法才能释放锁。
  • Lock机制可以不在同一个块结构中获取和释放锁,更加自由的释放锁

Lock接口

  • Lock 实现提供了使用 synchronized 方法和语句所没有的其他功能,包括提供了一个非块结构的获取锁尝试 tryLock()、一个获取可中断锁的尝试 lockInterruptibly() 和一个获取超时失效锁的尝试 tryLock(long, TimeUnit)
  • Lock 类还可以提供与隐式监视器锁完全不同的行为和语义,如保证排序、非重入用法或死锁检测。如果某个实现提供了这样特殊的语义,则该实现必须对这些语义加以记录。
  • 注意,Lock 实例只是普通的对象,其本身可以在 synchronized 语句中作为目标使用。获取 Lock 实例的监视器锁与调用该实例的任何 lock()方法没有特别的关系。为了避免混淆,建议除了在其自身的实现中之外,决不要以这种方式使用 Lock 实例。

lock

void lock() 获取锁

  • 如果锁处于空闲状态,当前线程将直接获取该lock对象锁。
  • 相反,如果锁已经被其他线程持有,将禁用当前线程,直到当前线程获取到锁.

unlock

void unlock() 释放锁

  • 当前线程将释放持有的锁。
  • 锁只能由持有者释放。
  • 如果线程并不持有锁,却执行了该方法,可能会导致异常的发生。

tryLock

boolean tryLock()

  • 仅在调用时锁为空闲状态才获取该锁。

  • 如果锁可用,则获取锁,并返回true

  • 如果锁不可用,立即返回false

  • 此方法可确保如果获取了锁,则会释放锁,如果未获取锁,则不会试图将其释放。即通常配合unlock()使用来释放锁。

    Lock lock = new ReentrantLock();
    if(lock.tryLock()){
    try{
       //获取到锁的一些操作
    }finally{
        //确保了获取锁,才能释放
        lock.unlock();
    }
    }else{
      //未获取到锁的一些操作
    }
    
  • tryLock()lock()方法的区别:

    • tryLock()方法只是试图获取锁,如果锁不可用,当前线程仍然可以继续往下执行.
    • lock()方法是一定要获取到锁,如果锁不可用,就会一直等待下去,锁定当前线程,在未获取指定锁对象之前,当前线程不会继续向下执行

Condition接口 - 条件

  • ConditionObject监视器方法wait、notify、notifyAll方法分解成不同的对象,为了方便通过将这些对象与任意Lock对象实现组合使用,为每个对象提供了多个等待set(wait-set)。其中,Lock替代了synchronized方法和语句的使用,Condition替代了Object监视器方法的使用

  • 使用Condition对象的相关方法,可以方便的挂起和唤醒线程,而且可以特定的唤醒其其中某个线程。这也是和Object对象的wait()、notify()、notifyAll()方法的区别。

  • Object对象的wait()、notify()、notifyAll()方法存在一个问题:如果多个线程调用了obj的wait()方法而挂起,那么我们无法做到调用obj的notify()和notifyAll()方法唤醒其中特定的一个线程,而Conditon对象就可以做到。
  • Condition对象只能通过Lock类的newCondition()方法获取,因此一个Condition对象必然会有一个与其绑定的Lock锁

await

void await() 造成当前线程再接到信号或被中断之前一直处于等待状态

  • 将当前线程处于等待状态,并释放该Condition对象所绑定的锁.
  • 使用await()方法前当前线程必须持有与该Condition对象绑定的锁,否则程序可能会抛出异常。

signal

void signal() 唤醒一个在该Condition对象上挂起的线程

  • 如果存在多个线程同时等待该Condition对象的唤醒,则随机选择其中一个唤醒。
  • 线程被唤醒之前,必须重新获取到锁,即与该Condition对象绑定的Lock对象。

signalAll

void signalAll() 唤醒所有在该Condition对象上挂起的线程

  • 所有被唤醒的线程将竞争与该Condition对象绑定的锁,只有获取到锁的线程才能恢复到运行状态。

一个实例

问题的描述:

启动3个线程打印递增的数字, 线程1先打印1,2,3,4,5, 然后是线程2打印6,7,8,9,10,然后是线程3打印11,12,13,14,15. 接着再由线程1打印16,17,18,19,20....以此类推,直到打印到75. 程序的输出结果应该为:

 线程1: 1
 线程1: 2
 线程1: 3
 线程1: 4
 线程1: 5
 线程2: 6
 线程2: 7
 线程2: 8
 线程2: 9
 线程2: 10
 ...
 线程3: 71
 线程3: 72
 线程3: 73
 线程3: 74
 线程3: 75

利用ReentrantLockCondition接口组合,可以轻松指定和分配各个线程该完成的操作。代码如下:


public class ReentrantLockTest {

    public static void main(String[] args) {
        NumPrinter n = new NumPrinter();
        //此处确定每个线程应该执行几次,总共75个数分3个线程,每个线程分25个数字,
        // 5*5=25,每个线程执行5次,每次打印5个数字
        new Thread(()->{
            //每个线程执行5次
            for (int i = 0; i < 5 ; i++) {
                try {
                    n.print1();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        new Thread(()->{
            for (int i = 0; i <5 ; i++) {

                try {
                    n.print2();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        new Thread(()->{
            for (int i = 0; i < 5 ; i++) {
                try {
                    n.print3();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

class NumPrinter{
    private  int num = 1;
    private  int flag = 1;
    //使用ReentrantLock类和Condition接口来配合使用,指定唤醒哪个线程
    private ReentrantLock lock = new ReentrantLock();
    private Condition c1 = lock.newCondition();
    private Condition c2 = lock.newCondition();
    private Condition c3 = lock.newCondition();

    public void print1() throws InterruptedException {
        //三个线程同时占用临界区的资源,应该使用同步
        lock.lock();   //获取锁 和 下面的 unlock  代替了 synchronized
        if(flag != 1)  //当flag不为1时,c1进行等待,因为此时其他的线程正在执行他们的操作
            c1.await();
        if(num <= 75)
            for (int i = 0; i < 5; i++) {
                System.out.println("1---"+num++);
            }
        flag = 2;       //标记指定为2,在print2中限制除了flag不为2时,c2进行等待
        c2.signal();    //唤醒指定的c2
        lock.unlock();  //释放锁
    }

    public void print2() throws InterruptedException {
        lock.lock();
        if(flag != 2)
            c2.await();
        if(num <= 75)
            for (int i = 0; i < 5; i++) {
                System.out.println("2---"+num++);
            }
        flag = 3;
        c3.signal();
        lock.unlock();
    }

    public void print3() throws InterruptedException {
        lock.lock();
        if(flag != 3)
            c3.await();
        if(num <= 75)
            for (int i = 0; i < 5; i++) {
                System.out.println("3---"+num++);
            }
        flag = 1;
        c1.signal();
        lock.unlock();
    }

}

线程组

线程组概述

  • Java中用ThreadGroup来表示线程组,它可以对一批线程进行分类管理,Java允许程序直接对线程组进行控制。

  • 默认情况下,所有的线程都属于主线程组。

    • public final ThreadGroup getThreadGroup() 通过线程对象获取所属的线程组
    • public final String getName() 通过线程组对象获取线程组的名字
    MyRunnable mr = new MyRunnable();
    Thread t1 = new Thread(mr, "张三");
    Thread t2 = new Thread(mr, "李四");
    //获取线程组
    // 线程类里面的方法:public final ThreadGroup getThreadGroup()
    ThreadGroup tg1 = t1.getThreadGroup();
    ThreadGroup tg2 = t2.getThreadGroup();
    // 线程组里面的方法:public final String getName()
    String name1 = tg1.getName();
    String name2 = tg2.getName();
    System.out.println(name1);
    System.out.println(name2);
    // 通过结果我们知道了:线程默认情况下属于main线程组
    // 通过下面的测试,你应该能够看到,默任情况下,所有的线程都属于同一个组
    System.out.println(Thread.currentThread().getThreadGroup().getName());
  • 给线程设置线程组

    • ThreadGroup(String name) 线程组对象的构造器并赋值名字
    • Thread(ThreadGroup group, Runnable target,String name) 线程对象的构造器,直接设置线程组
        // ThreadGroup(String name)
        ThreadGroup tg = new ThreadGroup("这是一个新的组");
    
        MyRunnable mr = new MyRunnable();
        // Thread(ThreadGroup group, Runnable target, String name)
        Thread t1 = new Thread(tg, mr, "张三");
        Thread t2 = new Thread(tg, mr, "李四");
    
        System.out.println(t1.getThreadGroup().getName());
        System.out.println(t2.getThreadGroup().getName());
    
        //通过组名称设置后台线程,表示该组的线程都是后台线程
        tg.setDaemon(true);

线程池

为什么会有线程池?(线程池概述)

  • 程序创建一个新的线程成本较高,因为它涉及到要与操作系统进行交互。频繁的线程创建和销毁,大大消耗时间和降低系统的效率。
  • 线程池的使用解决了这个问题,它使得多个线程能够一次创建完,放在线程池中,执行完后并不会被销毁,而是再次回到线程池中变成空闲状态,等待下一个对象来使用。并且即拿即用,不用每次都创建,大大提高了线程的复用性,提高系统效率。
  • JDK1.5开始,Java有了内置的线程池。Executors工厂类

内置线程池

JDK5新增了一个Executors工厂类来产生线程池,有如下几个方法:

  • public static ExecutorService newFixedThreadPool(int nThreads)

    创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。

    参数: nThreads - 池中的线程数

    返回: 新创建的线程池

  • public static ExecutorService newSingleThreadExecutor()

    创建一个使用单个 worker 线程的 Executor,以无界队列方式来运行该线程

  • Executors.newCachedThreadPool()

    创建一个缓冲池,缓冲池容量大小为Integer.MAX_VALUE

这些方法的返回值是ExecutorService对象,该对象表示一个线程池,可以执行Runnable对象或者Callable对象代表的线程

  • 下面是这三个静态方法的具体实现:

    public static ExecutorService newFixedThreadPool(int nThreads) {
      return new ThreadPoolExecutor(nThreads, nThreads,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>());
    }
    public static ExecutorService newSingleThreadExecutor() {
      return new FinalizableDelegatedExecutorService
          (new ThreadPoolExecutor(1, 1,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>()));
    }
    public static ExecutorService newCachedThreadPool() {
      return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                    60L, TimeUnit.SECONDS,
                                    new SynchronousQueue<Runnable>());
    }

    根据源码的具体实现来看,它们实际上也是调用了ThreadPoolExecutor,只不过参数都已配置好了。

    ThreadPoolExecutor构造器如下:

    public ThreadPoolExecutor(int corePoolSize,
                            int maximumPoolSize,
                            long keepAliveTime,
                            TimeUnit unit,
                            BlockingQueue<Runnable> workQueue)

    参数含义:

    • corePoolSize - 池中所保存的线程数,包括空闲线程
    • maximumPoolSize - 池中允许的最大线程数。
    • keepAliveTime - 当线程数大于核心时,此为终止前多余的空闲线程等待新任务的最长时间。
    • unit - keepAliveTime 参数的时间单位。
    • workQueue - 执行前用于保持任务的队列。此队列仅由保持 execute 方法提交的 Runnable 任务。
    • handler - 由于超出线程范围和队列容量而使执行被阻塞时所使用的处理程序。
  • 方法源码分析:

    • newFixedThreadPool创建的线程池corePoolSizemaximumPoolSize值是相等的,它使用的LinkedBlockingQueue
    • newSingleThreadExecutorcorePoolSizemaximumPoolSize都设置为1,也使用的LinkedBlockingQueue
    • newCachedThreadPoolcorePoolSize设置为0,将maximumPoolSize设置为Integer.MAX_VALUE,使用的SynchronousQueue,也就是说来了任务就创建线程运行,当线程空闲超过60秒,就销毁线程
    • 实际中,如果Executors提供的三个静态方法能满足要求,就尽量使用它提供的三个方法,因为自己去手动配置ThreadPoolExecutor的参数有点麻烦,要根据实际任务的类型和数量来进行配置。
    • 另外,如果ThreadPoolExecutor达不到要求,可以自己继承ThreadPoolExecutor类进行重写。
  • ExecutorService提供了如下方法:

    • Future<?> submit(Runnable task)

    提交一个 Runnable 任务用于执行,并返回一个表示该任务的 Future。该 Future 的 get 方法在成功 完成时将会返回 null。 参数: task - 要提交的任务

    返回: 表示任务等待完成的 Future

    • <T> Future<T> submit(Callable<T> task)

    提交一个返回值的任务用于执行,返回一个表示任务的未决结果的 Future。该 Future 的 get 方法在成功完成时将会返回该任务的结果

    • void shutdown()

    启动一次顺序关闭,执行以前提交的任务,但不接受新任务。如果已经关闭,则调用没有其他作用。

  • 代码示例:

ExecutorService pool = Executors.newFixedThreadPool(2);
// 可以执行Runnable对象
//或者Callable对象代表的线程
pool.submit(new MyRunnable());
pool.submit(new MyRunnable());
//结束线程池
pool.shutdown();

Future 接口

Future 表示异步计算的结果。

  • 它提供了检查计算是否完成的方法,以等待计算的完成,并获取计算的结果
  • 计算完成后只能使用 get 方法来获取结果,如有必要,计算完成前可以阻塞此方法。
  • 取消则由 cancel 方法来执行。
  • 还提供了其他方法,以确定任务是正常完成还是被取消了。一旦计算完成,就不能再取消计算。
  • 如果为了可取消性而使用 Future 但又不提供可用的结果,则可以声明 Future<?> 形式类型、并返回 null 作为底层任务的结果。
  • FutureTask 是其实现类
方法摘要
boolean cancel(boolean mayInterruptIfRunning) 试图取消对此任务的执行。
V get() 如有必要,等待计算完成,然后获取其结果。
V get(long timeout, TimeUnit unit) 如有必要,最多等待为使计算完成所给定的时间之后,获取其结果(如果结果可用)。
boolean isCancelled() 如果在任务正常完成前将其取消,则返回 true
boolean isDone() 如果任务已完成,则返回 true

实现多线程的第三种方式

  • 步骤:
    1. 创建实体类,实现Callable接口
    2. 实现接口中的call()方法
    3. 利用 ExecutorService线程池对象 的 <T> Future<T> submit(Callable<T> task()方法提交该Callable接口的线程任务。
// 创建线程池
ExecutorService pool = Executors.newFixedThreadPool(2);
// 可以执行Runnable对象或者Callable对象代表的线程
Future<Integer> f1 = pool.submit(new MyCallable(100));
Future<Integer> f2 = pool.submit(new MyCallable(200));

// V get()
Integer i1 = f1.get();
Integer i2 = f2.get();

System.out.println(i1);
System.out.println(i2);
// 结束
pool.shutdown();
public class MyCallable implements Callable<Integer> {

    private int number;
    public MyCallable(int number) {
        this.number = number;
    }
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int x = 1; x <= number; x++) {
            sum += x;
        }
        return sum;
    }

}
  • 利用匿名内部类方式:
ExecutorService service = Executors.newSingleThreadExecutor();
         Future<String> future = service.submit(new Callable() {
             @Override
             public String call() throws Exception {
                 return "通过实现Callable接口";
             }
         });
         try {
             String result = future.get();
             System.out.println(result);
         } catch (InterruptedException e) {
             e.printStackTrace();
         } catch (ExecutionException e) {
             e.printStackTrace();
         }
  • Lambda表达式方式:
public class CallableTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //使用Executors工厂类创建一个单线程池
        ExecutorService es =  Executors.newSingleThreadExecutor();
        //使用这个单线程提交一个Callable接口线程服务,返回值为String
        //Callable接口是一个函数式接口,Java8开始可以直接使用Lambda表达式表示
        //其内部实现了call()方法  V call() throws Exception;
        //并得到该结果值打印
        System.out.println( es.submit(()->"使用lambda表达式的Callable接口").get());
        es.shutdown(); //关闭该线程池
    }

}
  • 实现callable接口,提交给ExecutorService返回值异步执行的。
  • 该方式的优缺点:
    • 优点:
    • 有返回值
    • 可以抛出异常
    • 缺点:
    • 代码较复杂,需要利用线程池

猜你喜欢

转载自blog.csdn.net/hxhaaj/article/details/81087795