Java 多线程

目录

1 线程与进程

2 线程调度

3 线程的两种实现方式

4 Thread 的各种方法

5 线程安全

6 线程死锁

7 线程间通信/交互

8 线程的六种状态

9 线程的第三种实现方式

10 线程池

1 线程与进程

进程:一个内存中运行的应用程序,每个进程都有一个独立的内存空间。

线程:进程中的一个执行路径,同一进程中的线程共享一个内存空间,线程之间可以自由切换,并发执行。

  • 一个进程最少拥有一个线程。

  • 线程实际上是在进程的基础上的进一步划分,一个进程启动后,里面的若干执行路径又可以划分成若干个路线。

  • 每个线程都拥有自己的栈空间(也就是说,由同一个线程调用的方法,都会在该线程内部执行),线程之间共用一份堆内存。

2 线程调度

  1. 分时调度

    所有线程轮流获得 CPU 的使用权,平均分配每个线程占用 CPU 的时间。

  2. 抢占式调度

    优先级高的线程先使用 CPU,如果线程的优先级相同,则谁拿到使用权是随机的(线程随机性)。

Java 中使用的线程调度方式为抢占式调度。CPU 采用抢占式调度模式在多个线程间进行着高速切换,某个时刻CPU 的一个核只能执行一个线程,由于切换速度飞快,看上去就像在同时进行多个任务一样。 所以多线程并不能提高程序的运行速度,但通过提高 CPU 的使用率,从而提高了程序的运行效率。

3 线程的两种实现方式

  1. 继承Thread

通过继承Thread类定义一个线程类,并通过重写run()方法,在run()方法中完成线程需要执行的任务。在一个现有线程中创建一个线程类并调用其start()方法开启线程(注意不是直接调用run()方法,直接调用run()并不会启动新线程,而只是在当前线程中调用了一个方法而已)

// MyThread.java
// 定义一个线程类
public class MyThread extends Thread {
    // run() 中的程序就是线程要执行的任务
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("秦时明月汉时关" + i);
        }
    }
}

// Test.java
public class Test {
    public static void main(String[] args) {
        MyThread t = new MyThread();
        t.start();
        for (int i = 0; i < 10; i++) {
            System.out.println("万里长征人未还" + i);
        }
    }
}

运行结果:

秦时明月汉时关0
万里长征人未还0
秦时明月汉时关1
万里长征人未还1
秦时明月汉时关2
万里长征人未还2
秦时明月汉时关3
万里长征人未还3
秦时明月汉时关4
万里长征人未还4
秦时明月汉时关5
万里长征人未还5
秦时明月汉时关6
万里长征人未还6
秦时明月汉时关7
秦时明月汉时关8
万里长征人未还7
秦时明月汉时关9
万里长征人未还8
万里长征人未还9
  1. 实现Runnable接口

通过实现Runnable接口定义一个任务类,再将该任务类对象交给Thread对象执行,从而完成线程的创建。

// MyRunnable.java
// 在 MyRunnable 类中重写 run() 写入需要执行的任务
public class MyRunnable implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("但使龙城飞将在" + i);
        }
    }
}

// Test.java
public class Test {
    public static void main(String[] args) {
        MyRunnable r = new MyRunnable();
        Thread t = new Thread(r);
        t.start();
        for (int i = 0; i < 10; i++) {
            System.out.println("不教胡马度阴山" + i);
        }
    }
}

运行结果:

但使龙城飞将在0
不教胡马度阴山0
但使龙城飞将在1
不教胡马度阴山1
但使龙城飞将在2
不教胡马度阴山2
但使龙城飞将在3
不教胡马度阴山3
但使龙城飞将在4
不教胡马度阴山4
但使龙城飞将在5
不教胡马度阴山5
但使龙城飞将在6
不教胡马度阴山6
但使龙城飞将在7
不教胡马度阴山7
但使龙城飞将在8
不教胡马度阴山8
但使龙城飞将在9
不教胡马度阴山9

实现Runnable的方式跟继承Thread的方式相比具有如下优势:

  • 通过创建任务再给线程分配的方式实现多线程,更适合多个线程都执行相同任务的情况;
  • 可以避免单继承所带来的麻烦(Runnable是接口,Thread是类);
  • 任务与线程是分离的,降低了耦合性,提高了程序的健壮性
  • 线程池技术中,接收Runnable类型的任务,而不接收Thread类型的线程。
  1. 内部类

当然,如果只需要执行一次某个线程的话,通过以内部类或者匿名内部类的方式继承Thread可以很简单的实现。内部类的另一个好处是可以很方便的访问外部的局部变量。

// 新线程
new Thread() {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("秦时明月汉时关" + i);
        }
    }
}.start();
// 当前线程
for (int i = 0; i < 10; i++) {
    System.out.println("万里长征人未还" + i);
}

4 Thread 的各种方法

  1. 线程名称
class MyRunable implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());  // 获取当前线程名称
    }
}

// Test.java
public class Test {
    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName()); // 主线程名称为 "main"
        new Thread(new MyRunable(), "壹").start();  // 设置线程名称为 "貳"
        Thread t = new Thread(new MyRunable());
        System.out.println(t.getName()); // 默认名称为 "Thread-0"
       	t.setName("貳");  // 设置线程名称为 "貳"
        t.start();
        new Thread(new MyRunable()).start(); // 默认名称为 "Thread-1"
        new Thread(new MyRunable()).start(); // 默认名称为 "Thread-2"
    }
}

运行结果:

main
Thread-0
壹
Thread-2
Thread-1
貳
  1. 线程休眠
public class Test {
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 5; i++) {
            System.out.println(i);
            Thread.sleep(1000); // 线程休眠 1s
        }
    }
}

运行结果:从 0 开始每隔 1s 输出加 1

0
1
2
3
4

还有一个重载的sleep()两参方法:

// 毫秒 + 纳秒
// 纳秒允许范围 [0, 999999]
public static void sleep(long millis, int nanos) {}
  1. 线程中断

通过调用线程的interrupt(),通知线程关闭,由线程中的任务代码决定是否结束自己的生命。

// MyRunable.java
public class MyRunnable implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + ": " + i);
            try {
                Thread.sleep(1000); // 线程在 sleep 期间检查是否有中断标记,若有则抛出异常
            } catch (InterruptedException e) {
                System.out.println("发现中断标记,线程自杀");
                return; // 任务终止,线程从此结束
            }
        }
    }
}
// Test.java
public class Test {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(new MyRunnable());
        t.start();
        // 主线程每隔一秒输出一次数字,共输出 5 次,因此主线程循环先结束
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + ": " + i);
            Thread.sleep(1000);
        }
        t.interrupt(); // 结束循环之后通知线程 t 中断
    }
}

运行结果:

Thread-0: 0
main: 0
main: 1
Thread-0: 1
Thread-0: 2
main: 2
Thread-0: 3
main: 3
Thread-0: 4
main: 4
Thread-0: 5
发现中断标记,线程自杀
  1. 守护线程
    • 线程分为用户线程守护线程
    • 用户线程:当一个进程不包含任何存活的用户线程时,进程结束;
    • 守护线程:用于守护用户线程,通常被用来做日志、性能统计等工作。当一个进程中最后一个用户线程结束时,所有守护线程自动终止,进程结束。
public class Test {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(){
            public void run(){
                for (int i = 0; i < 10; i++){
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.printf("%s 运行了:%ss\n",Thread.currentThread().getName(), i);
                }
            }
        };
        t.setDaemon(true); // 设置为守护线程
        t.start();
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + ": " + i);
            Thread.sleep(1000);
        }
    }
}

运行结果:

main: 0
Thread-0 运行了:0s
main: 1
Thread-0 运行了:1s
main: 2
Thread-0 运行了:2s
main: 3
Thread-0 运行了:3s
main: 4
Thread-0 运行了:4s

5 线程安全

  1. 线程同步问题
// Ticket.java
public class Ticket implements Runnable{
    private int count = 5; // 剩余票数
    @Override
    public void run() {
        while (count > 0) {
            System.out.println("出票中...");
            try {
                Thread.sleep(800);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            count--;
            System.out.println("出票成功,余票为:" + count);
        }
    }
}

// Test.java
public class Test {
    public static void main(String[] args) throws InterruptedException {
        Runnable run = new Ticket();
        new Thread(run).start();
        new Thread(run).start();
        new Thread(run).start();
    }
}

运行结果:

出票中...
出票中...
出票中...
出票成功,余票为:4
出票中...
出票成功,余票为:3
出票中...
出票成功,余票为:2
出票中...
出票成功,余票为:-1
出票成功,余票为:0
出票成功,余票为:1

上面例子中出现了最后余票为 -1 的情况。原因是在最后阶段余票为 1 时,同时有不止一个线程进入了出票程序中,造成了不止一次的出票操作。因此上面代码的操作流程是线程不安全的。

  1. 线程同步

    • synchronized同步代码块

    synchronized同步代码块使用一个对象 o 作为锁,表示当前线程独占该对象,这个对象又叫做同步对象,任何对象都可以作为同步对象。

    Object o = new Object();
    synchronized (lock){
    	//只有占有了 lock 后才可以执行此块的代码
    }
    

    如上面代码所示为使用 synchronized 语法的格式。只有线程占用了锁,它才能执行大括号部分代码块中的程序,执行结束又会释放对锁的占用。锁被占用期间,其它试图占用它的线程就会等待而不会进入代码块中执行,直到锁被释放,等待的线程就会争抢该锁,抢到占用的线程又进入代码块中执行程序,其它线程继续等待。

    修改上面的出票任务为同步代码块的方式如下:

    // Ticke.java
    public class Ticket implements Runnable{
        private int count = 5; // 剩余票数
        final Object o = new Object();
        @Override
        public void run() {
            while (true) {
                synchronized (o) {
                    if (count > 0) {
                        System.out.println(Thread.currentThread().getName() + "出票中...");
                        try {
                            Thread.sleep(500);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        count--;
                        System.out.println("出票成功,余票为:" + count);
                    } else {
                        break;
                    }
                    // 让当前线程执行完一次出票操作后,休眠一段时间,给其它线程更大的机会来抢占锁
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
    // Test.java
    public class Test {
        public static void main(String[] args) throws InterruptedException {
            Runnable run = new Ticket();
            new Thread(run).start();
            new Thread(run).start();
            new Thread(run).start();
        }
    }
    

    运行结果:

    Thread-0出票中...
    出票成功,余票为:4
    Thread-2出票中...
    出票成功,余票为:3
    Thread-2出票中...
    出票成功,余票为:2
    Thread-2出票中...
    出票成功,余票为:1
    Thread-1出票中...
    出票成功,余票为:0
    

    可以看到出票正常,个线程间没有交叉出票的情况。当然,整个程序的执行时间也变长了,和单线程运行时间差不多。

    注意:必须使用同一个对象作为锁,才能达到同步的效果。如果各个线程占用不同的对象作为锁,实际上是没有作用的,它们还是在一起执行同一段代码块,没有实现等待的效果。

    • synchronized同步方法

    这种方式将需要同步的代码块提取为一个方法,并使用synchronized修饰。这种方式的锁(同步对象)被隐式的指定为方法所在对象,即this对象。如果同步方法被static修饰,那么此时的同步对象为方法所在类的字节码文件对象,即类名.class

    修改上面的出票任务为同步方法的方式如下:

    // Ticket.java
    public class Ticket implements Runnable{
        private int count = 5; // 剩余票数
        @Override
        public void run() {
            while (true) {
                if (!sale()) break;
                // 让当前线程执行完一次出票操作后,休眠一段时间,给其它线程更大的机会来抢占锁
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    
        private synchronized boolean sale() {
            if (count > 0) {
                System.out.println(Thread.currentThread().getName() + "出票中...");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                count--;
                System.out.println("出票成功,余票为:" + count);
                return true;
            }
            return false;
        }
    }
    // Test.java
    public class Test {
        public static void main(String[] args) throws InterruptedException {
            Runnable run = new Ticket();
            new Thread(run).start();
            new Thread(run).start();
            new Thread(run).start();
        }
    }
    
    

    运行结果:

    Thread-0出票中...
    出票成功,余票为:4
    Thread-2出票中...
    出票成功,余票为:3
    Thread-1出票中...
    出票成功,余票为:2
    Thread-2出票中...
    出票成功,余票为:1
     Thread-0出票中...
    出票成功,余票为:0
    
   

   
   不论是同步代码块或者时同步方法,只要使用了同一个锁,当锁被某个线程占用时,任何使用该锁的代码块或者方法都会被同步化,即其它线程不能执行其中的任何代码块或者方法。
   
   - `Lock`显式锁
   
   显式锁的方式通过直接定义一个 Java 中的`Lock`对象来实现同步效果,通过调用该对象的`lock()`方法上锁,与`synchronized`语法自动释放锁不同,`Lock`对象必须通过显式地调用`unlock()`方法来释放锁。所以,把`unlock()`放在`finally`中可以确保释放的执行。
   
   修改上面的出票任务为显式锁的方式如下:
   
   ```java
   // Ticket.java
   import java.util.concurrent.locks.Lock;
   import java.util.concurrent.locks.ReentrantLock;
   
   public class Ticket implements Runnable{
       private int count = 5; // 剩余票数
       private Lock lk = new ReentrantLock();
       @Override
       public void run() {
           while (true) {
               lk.lock();
               try {
                   if (count <= 0) break;
                   System.out.println(Thread.currentThread().getName() + "出票中...");
                   Thread.sleep(500);
                   count--;
                   System.out.println("出票成功,余票为:" + count);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               } finally {
                   lk.unlock();
               }
               // 让当前线程执行完一次出票操作后,休眠一段时间,给其它线程更大的机会来抢占锁
               try {
                   Thread.sleep(100);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
       }
   }
   // Test.java
   public class Test {
       public static void main(String[] args) throws InterruptedException {
           Runnable run = new Ticket();
           new Thread(run).start();
           new Thread(run).start();
           new Thread(run).start();
       }
   }

运行结果:

Thread-1出票中...
出票成功,余票为:4
Thread-0出票中...
出票成功,余票为:3
Thread-2出票中...
出票成功,余票为:2
Thread-1出票中...
出票成功,余票为:1
Thread-0出票中...
出票成功,余票为:0

Java 中的Lock是一个接口,所以需要使用它的实现类ReentrantLock创建一个Lock对象。同样的,为了达到同步的效果,即线程安全的目的,必须要让各个线程使用同一个Lock对象才行。

  • 公平锁

上述的三种线程同步的实现中,使用的都不是公平锁。前面说过 Java 中采用抢占式调度模式切换线程,就是说,当某个锁被释放时,并不一定是最先前来请求占用的线程占用到该锁,而是随机的,所以是“不公平”的。

可以在显式锁的方式中实现公平锁,即让线程排队获取锁的占用权。只需要在创建ReentrantLock对象时,传入为boolean类型参数传入true就行。

Lock lk = new ReentrantLock(true);

6 线程死锁

线程死锁的现象:两个线程占有了对方需要的锁,导致了两边都在等待对方释放锁,使得程序永远处于僵持状态。或者多个线程之间占用了后面线程需要的锁,形成了死锁环。下面通过一个应聘者和招聘者之间的例子展示两个线程之间的死锁现象。

// Candidate.java
public class Candidate {
    public synchronized void sayTo(Recruiter recruiter) {
        System.out.println("应聘者:你给我工作,我就有工作经验了!");
        // 停一段时间,给另一个线程有足够的时间占用 recruiter 锁
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 等待应聘者回应
        recruiter.response();
    }

    public synchronized void response() {
        System.out.println("好我有工作经验了!");
    }
}
// Recruiter.java
public class Recruiter {
    public synchronized void sayTo(Candidate candidate) {
        System.out.println("面试官:你有工作经验,我就给你工作!");
        // 停一段时间,另一个线程有足够的时间占用 candidate 锁
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 等待应聘者回应
        candidate.response();
    }

    public synchronized void response() {
        System.out.println("好我给你工作!");
    }

}
// DeadLock.java 对话僵局
public class DeadLock {
    public static void main(String[] args) {
        Candidate candidate = new Candidate();
        Recruiter recruiter = new Recruiter();
        new Thread(new Runnable() {
            @Override
            public void run() {
                candidate.sayTo(recruiter);
            }
        }).start();
        recruiter.sayTo(candidate);
    }
}

程序会进入死锁,对象recruiter在等待对象candidate释放作为锁的自己,反过来candidate也在等待recruiter释放作为锁的自己,程序卡住了,永远不会结束,局面十分焦灼。。。

image

在程序中避免死锁现象的最直接有效的办法就是,在会产生锁的代码块或方法中不要调用另外一个会产生锁的代码块或方法。

7 线程间通信/交互

同步线程之间通过调用锁对象的wait()/notify()/notifyAll()方法相互通知。某个线程通过调用锁对象.wait()让自己进入等待状态,之后该线程不再暂停运行。直到另外某个线程调用锁对象.notify(),前面暂停的线程才可能重新回到运行状态,只是可能,因为notify()只会让处于等待状态的线程中的一个重新运行。或者当某个线程运行中调用锁对象.notifyAll(),这会让所有处于等待状态中的线程都恢复运行。下面是一个炒菜与上菜的简单例子:

// Food.java 食物
public class Food {
    private String name;
    private String taste;
	// 做菜
    public synchronized void setNameAndTaste(String name, String taste) {
        System.out.println("开始做菜...");
        this.name = name;
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.taste = taste;
        System.out.println("菜做好了。");
    }
	// 上菜
    public synchronized void get() {
        System.out.println("上菜: ");
        System.out.println(this.name + "-" + this.taste);
    }
}
// Cook.java 厨师
public class Cook implements Runnable{
    private final Food f;
    public Cook(Food f) {
        this.f = f;
    }
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            if (i % 2 == 0) {
                f.setNameAndTaste("西红柿炒番茄", "麻辣味");
            } else {
                f.setNameAndTaste("马铃薯烧土豆", "芥末味");
            }
        }
    }
}
// Waiter.java 服务员
public class Waiter implements Runnable{
    private final Food f;
    public Waiter(Food f) {
        this.f = f;
    }
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            f.get();
        }
    }
}
// 上菜服务
public class Serving {
    public static void main(String[] args) {
        Food f = new Food();
        new Thread(new Cook(f)).start();
        new Thread(new Waiter(f)).start();
    }
}

运行结果:

开始做菜...
菜做好了。
上菜:
西红柿炒番茄-麻辣味
开始做菜...
菜做好了。
上菜:
马铃薯烧土豆-芥末味
上菜:
马铃薯烧土豆-芥末味
上菜:
马铃薯烧土豆-芥末味
上菜:
马铃薯烧土豆-芥末味
开始做菜...
菜做好了。
开始做菜...
菜做好了。
开始做菜...
菜做好了。

上面程序中,即使Food中的方法已经同步化了,使得做菜setNameAndTaste()和上菜get()的过程你不会相互干扰,但上菜服务的流程仍然是不合理的,waiter再上完一道菜后,没等cook 调用setNameAndTaste()做下一道菜,就开始继续调用get()上菜了。同样可能的是,cook做完一道菜还没等waiter调用get()上菜,就继续调用setNameAndTaste()换做下一道菜了。这个问题称为生产者与消费者问题。这里cook作为生产者,waiter作为消费者。

合理的流程应该是生产者在产生数据时,消费者应该暂停执行使用数据的操作,反过来,消费者在使用数据时,生产者应该暂停更新数据的才做。上面例子的问题可以通过线程间通信的方式解决,需要做修改的是Food类中的setNameAndTaste()get()

public class Food {
    private String name;
    private String taste;

    public synchronized void setNameAndTaste(String name, String taste) {
        System.out.println("开始做菜...");
        this.name = name;
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.taste = taste;
        System.out.println("菜做好了。");
        this.notifyAll(); // 通知其它线程恢复运行
        try {
            this.wait(210); // 进入等待状态 210ms
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public synchronized void get() {
        System.out.println("上菜: ");
        System.out.println(this.name + "-" + this.taste);
        this.notifyAll(); // 通知其它线程恢复运行
        try {
            this.wait(210); // 进入等待状态 210ms
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

这样的Serving.java运行结果为:

开始做菜...
菜做好了。
上菜: 
西红柿炒番茄-麻辣味
开始做菜...
菜做好了。
上菜: 
马铃薯烧土豆-芥末味
开始做菜...
菜做好了。
上菜: 
西红柿炒番茄-麻辣味
开始做菜...
菜做好了。
上菜: 
马铃薯烧土豆-芥末味
开始做菜...
菜做好了。
上菜: 
西红柿炒番茄-麻辣味

8 线程的六种状态

线程的六种状态以枚举形式存在于Thread类中。

public enum State {
    NEW, // 已创建未运行的状态
    RUNNABLE, // 运行状态
    BLOCKED, // 阻塞状态,等待占用锁对象从而进入到同步代码块或者同步方法中
    WAITING, // 等待状态,
    TIMED_WAITING, // 定时等待状态,线程暂停运行一段时间
    TERMINATED; // 线程生命结束
}

这几种状态再线程的生命周期中的转换关系如下图:

image

9 线程的第三种实现方式

Java 中除了上面使用ThreadRunnable的两种方式,还可以使用Callable接口实现线程。与前两种方式不同,Callable可以实现有返回值的线程,而前两种没有返回值。

使用Callable实现线程的步骤如下:

  1. 实现Callable接口,实现其中的call()方法;
  2. 创建FutureTask对象,并传入一个前面实现Callable接口的类的对象;
  3. 通过Thread创建线程,传入第二步中的FutureTask对象作为线程的执行任务,启动线程。

获取线程返回值的方式是调用FutureTask对象的get()方法,此方法会阻塞主进程执行,一直等到FutureTask对象所在线程结束返回。具体实现如下例:

// MyCallable.java
import java.util.concurrent.Callable;
// 1. 实现 Callable 接口
public class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws InterruptedException {
        for (int i = 0; i < 5; i++) {
            Thread.sleep(100);
            System.out.println(Thread.currentThread().getName() + ": " + i);
        }
        return 100;
    }
}
// Test.java
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class Test {
    public static void main(String[] args)
        	throws ExecutionException, InterruptedException {
        // 2. 创建 FutureTask 对象,并传入一个实现 Callable 接口的类的对象;
        Callable<Integer> call = new MyCallable();
        FutureTask<Integer> task = new FutureTask<>(call);
        // 3. 通过 Thread 创建线程,传入 task,启动线程。
        new Thread(task).start();
        Integer num = task.get(); // 主线程会等待执行 task 的线程执行结束并接收一个返回值,然后再继续往下执行
        System.out.println("task 返回结果:" + num);
        for (int i = 0; i < 10; i++) {
            Thread.sleep(100);
            System.out.println(Thread.currentThread().getName() + ": " + i);
        }
    }
}

运行结果:

Thread-0: 0
Thread-0: 1
Thread-0: 2
Thread-0: 3
Thread-0: 4
task 返回结果:100
main: 0
main: 1
main: 2
main: 3
main: 4

我们也可以在主线程中不用一直等待task所在线程结束,而是在主线程执行过程中某个时机调用task.isDone()来查看task所在线程是否结束,检查到已经结束时,在取得其返回值。具体来说,修改上面Test.java如下:

public class Test {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Callable<Integer> call = new MyCallable();
        FutureTask<Integer> task = new FutureTask<>(call);
        System.out.println("task 返回结果:" + num);
        for (int i = 0; i < 10; i++) {
            if (task.isDone()) {
                int num = task.get();
                System.out.println("task 返回结果:" + num);
                break;
            }
            Thread.sleep(100);
            System.out.println(Thread.currentThread().getName() + ": " + i);
        }
    }
}

运行结果:

Thread-0: 0
main: 0
Thread-0: 1
main: 1
main: 2
Thread-0: 2
Thread-0: 3
main: 3
Thread-0: 4
main: 4
main: 5
task 返回结果:100

还可以通过调用task.cancel(true)来主动取消task所在线程的执行,如果取消成功,该方法会返回true,这表示该线程在自然结束之前被取消了;如果取消失败,则返回false,这就表示该线程在执行取消操作之前已经结束了。

public class Test {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Callable<Integer> call = new MyCallable();
        FutureTask<Integer> task = new FutureTask<>(call);
        new Thread(task).start();
        for (int i = 0; i < 3; i++) {
            Thread.sleep(100);
            System.out.println(Thread.currentThread().getName() + ": " + i);
        }
        if (task.cancel(ture)) {
            System.out.println("取消成功!");
        }
    }
}

运行结果:

main: 0
Thread-0: 0
main: 1
Thread-0: 1
main: 2
Thread-0: 2
取消成功!

10 线程池

如果为了频繁地执行很多任务,且这些任务的执行时间很短,我们为此频繁地创建了很多线程,因为创建线程和销毁线程需要时间,所以大量的时间都消耗在了线程的创建和销毁上,这样就导致了系统的效率大大降低。 线程池就是为了解决这个问题的。线程池是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,从而可以省下很多的时间和资源。

线程池的好处:

  • 降低资源消耗
  • 提高响应速度
  • 提高线程的可管理性

Java 中提供了四种线程池,分别是缓存线程池、定长线程池、单线程线程池和周期性任务定长线程池。

  • 缓存线程池

    缓存线程池的线程数组长度无限制,可以变化。其执行流程是:

    1. 判断线程池是否存在空闲线程
    2. 存在则使用
    3. 不存在,则创建线程 并放入线程池, 然后使用
// 使用缓存线城市
ExecutorService service = Executors.newCachedThreadPool();
// 指挥线程池执行新的任务
service.execute(() -> System.out.println(Thread.currentThread().getName() + " haha"));
service.execute(() -> System.out.println(Thread.currentThread().getName() + " keke"));
try {
    Thread.sleep(100);
} catch (InterruptedException e) {
    e.printStackTrace();
}
service.execute(() -> System.out.println(Thread.currentThread().getName() + " didi"));

运行结果(可以看到第三个任务在前面的第二个线程中执行,因为前面的线程已经空闲下来了):

pool-1-thread-2 keke
pool-1-thread-1 haha
pool-1-thread-2 didi
  • 定长线程池

    定长线程池的线程数组的长度是指定不变的。其执行流程如下:

    1. 判断线程池是否存在空闲线程
    2. 存在则使用
    3. 不存在空闲线程,且线程池未满的情况下,则创建线程并放入线程池,,然后使用
    4. 不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程
ExecutorService service = Executors.newFixedThreadPool(2);
service.execute(() -> {
    System.out.println(Thread.currentThread().getName() + " hahaha");
    try {
        Thread.sleep(2000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
});
service.execute(() -> {
    System.out.println(Thread.currentThread().getName() + " hahaha");
    try {
        Thread.sleep(2000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
});
service.execute(() -> System.out.println(Thread.currentThread().getName() + "hahaha"));

运行结果(可以看到线程池不会第三个任务创建新的线程,而是会等待前两个线程空闲下来再执行第三个任务):

pool-1-thread-1 hahaha
pool-1-thread-2 xixixi
pool-1-thread-1 yuyuyu
  • 单线程线程池

    单线程线程池相当于将定长线程池的线程数组长度指定为 1。所以执行流程和前面一样,只是始终只有一个线程可供使用。它可以用来操作需要排队执行的任务。

ExecutorService service = Executors.newSingleThreadExecutor();
service.execute(() -> {
    System.out.println(Thread.currentThread().getName() + " hahaha");
    try {
        Thread.sleep(2000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
});
service.execute(() -> {
    System.out.println(Thread.currentThread().getName() + " xixixi");
    try {
        Thread.sleep(2000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
});
service.execute(() -> {
    System.out.println(Thread.currentThread().getName() + " bobobo");
    try {
        Thread.sleep(2000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
});

运行结果(可以看到从头到尾只有一个线程执行任务):

pool-1-thread-1 hahaha
pool-1-thread-1 xixixi
pool-1-thread-1 bobobo
  • 周期性任务定长线程池

    周期性任务定长线程池的执行流程和定长线程池差不多,但它可以在同一个线程中周期性地执行同一个任务。

ScheduledExecutorService service = Executors.newScheduledThreadPool(2);
// 指定 3 个时间单位后执行任务,时间单位为秒
service.schedule(() -> System.out.println(Thread.currentThread().getName() + "hehe"), 3, TimeUnit.SECONDS);
// 指定 2 个时间单位后执行任务,每个 1 个时间单位执行一次同样的任务,时间单位为秒
service.scheduleAtFixedRate(() -> System.out.println(Thread.currentThread().getName() + "wuwu"), 2, 1, TimeUnit.SECONDS);

运行结果(pool-1-thread-1 在 2s 时启动,并每隔 1 秒打印 wuwu,pool-1-thread-2 在 3s 时启动并打印一次 hehe 后结束):

pool-1-thread-1 wuwu
pool-1-thread-1 wuwu
pool-1-thread-2 hehe
pool-1-thread-1 wuwu
pool-1-thread-1 wuwu
pool-1-thread-1 wuwu
... ...
posted @ 2021-09-02 22:45  alterwl  阅读(64)  评论(0编辑  收藏  举报