zl程序教程

您现在的位置是:首页 >  后端

当前栏目

【JavaSE】10-多线程

2023-09-11 14:19:28 时间

十、 多线程

10.1 基本概念:程序、进程、线程

10.1.1 三者的概念

  • 程序(program):一段静态的文本代码。
  • 进程(process):当程序加载进内存运行起来后,就成了进程。是一个动态的过程:有其自身的生命周期。
  • 线程(thread):一个进程里面的一条或者几条执行路径。
    • 若一个进程同一时间并行执行多个代码,就是支持多线程的。
    • 但是由于线程共享JVM中的方法区和堆,多个线程操作共享的系统资源可能会有冲突,带来安全隐患。

10.1.2 线程在虚拟机中的分配

每个线程独享一份每个线程共享的
虚拟机栈 (VM Stack)堆 (Heap)
程序计数器 (Program Counter Register)方法区 (Method Area)

10.1.3 并行与并发的概念

并行并发
多个CPU同时执行多个任务。比如:多个人同时做不同的事。一个CPU( 采用时间片)同时执行多个任务。比如:秒杀、多个人做同一件事。

10.1.4 何时需要多线程

  • 程序需要同时执行两个或多个任务。
  • 程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等。
  • 需要 一些后台运行的程序时。

10.2 线程的创建和使用

10.2.1 创建方式一:继承Thread父类

( 1 1 1) 创建一个继承于Thread类的子类;

( 2 2 2) 重写Thread类中的run()方法;

( 3 3 3) 创建Thread类的子类的对象;

( 4 4 4) 通过对象调用start()方法开始执行此多线程任务。

  • 其中,start() 方法的作用有:启动当前线程和调用当前线程的run() 方法。

例:

public class CreateThreadTest extends Thread{

    @Override
    public void run() {
        for (int i=0;i<20;i++){
            if (i%2==0){
                System.out.println(i);
            }
        }
    }

    public static void main(String[] args) {
        CreateThreadTest thread1 = new CreateThreadTest();
        thread1.start();

        //以下是主线程输出奇数
        for (int i=0;i<20;i++){
            if (i%2!=0){
                System.out.println(i+"*******main********");
            }
        }
    }
}

输出:

1*******main********
3*******main********
5*******main********
7*******main********
0
2
4
6
8
10
12
14
16
18
9*******main********
11*******main********
13*******main********
15*******main********
17*******main********
19*******main********

从结果可以看到:

  • thread1 线程中的输出偶数的线程和main() 函数主线程中的输出奇数的线程,是同时进行的。
  • 两个线程的输出由于是同时进行的,因此输出结果会互相穿插。

10.2.2 创建线程的注意点

  • 不能通过调用 thread1.run(); 的方式来启动该线程。否则仍然是在主线程 (main) 中按顺序执行罢了。并没有开辟新的线程去执行。

  • 想要再启动一个线程,不能再写一次 thread1.start(); 。否则在main 线程中出现 2 2 2thread1.start(); 会报illegalThreadStateException 异常。正确的方法是:再new一个线程对象。

    CreateThreadTest thread1 = new CreateThreadTest();
    thread1.start();
    
    //正确的再次启动线程方法
    CreateThreadTest thread2 = new CreateThreadTest();
    thread2.start();
    

10.2.3 创建线程的练习

题目:

练习:创建两个分线程,其中一个线程遍历100以内的偶数,另一个线程遍历100以内的奇数。

我的首次答案:

public class ThreadExer1 {
    public static void main(String[] args) {
        Thread1 t1 = new Thread1();
        t1.start();
        Thread2 t2 = new Thread2();
        t2.start();
    }
}

/**
 * @Author: Sihang Xie
 * @Description: 线程1-遍历100以内的偶数
 * @Date: 2022/3/14 10:16
 */
class Thread1 extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}

/**
 * @Author: Sihang Xie
 * @Description: 线程2-遍历100以内的奇数
 * @Date: 2022/3/14 10:20
 */
class Thread2 extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i % 2 != 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}

输出结果:

Thread-0:0
Thread-0:2
Thread-0:4
Thread-0:6
Thread-0:8
Thread-0:10
Thread-0:12
Thread-0:14
Thread-0:16
Thread-0:18
Thread-0:20
Thread-0:22
Thread-0:24
Thread-0:26
Thread-0:28
Thread-0:30
Thread-0:32
Thread-0:34
Thread-0:36
Thread-0:38
Thread-0:40
Thread-0:42
Thread-0:44
Thread-0:46
Thread-0:48
Thread-0:50
Thread-0:52
Thread-0:54
Thread-0:56
Thread-0:58
Thread-0:60
Thread-0:62
Thread-0:64
Thread-0:66
Thread-0:68
Thread-0:70
Thread-0:72
Thread-0:74
Thread-1:1
Thread-1:3
Thread-1:5
Thread-1:7
Thread-1:9
Thread-1:11
Thread-1:13
Thread-1:15
Thread-1:17
Thread-1:19
Thread-0:76
Thread-0:78
Thread-0:80
Thread-0:82
Thread-0:84
Thread-0:86
Thread-0:88
Thread-0:90
Thread-0:92
Thread-0:94
Thread-0:96
Thread-0:98
Thread-1:21
Thread-1:23
Thread-1:25
Thread-1:27
Thread-1:29
Thread-1:31
Thread-1:33
Thread-1:35
Thread-1:37
Thread-1:39
Thread-1:41
Thread-1:43
Thread-1:45
Thread-1:47
Thread-1:49
Thread-1:51
Thread-1:53
Thread-1:55
Thread-1:57
Thread-1:59
Thread-1:61
Thread-1:63
Thread-1:65
Thread-1:67
Thread-1:69
Thread-1:71
Thread-1:73
Thread-1:75
Thread-1:77
Thread-1:79
Thread-1:81
Thread-1:83
Thread-1:85
Thread-1:87
Thread-1:89
Thread-1:91
Thread-1:93
Thread-1:95
Thread-1:97
Thread-1:99

练习的体会:

  • 输出的线程会互相交错出现,说明2条线程确实是并行运行的。

  • 可以用

    System.out.println(Thread.currentThread().getName());
    

    来查看当前执行的线程是哪一个。

  • 当需要执行多个不同功能的线程时,需要创建不同的子类来实现。

10.2.3.1 练习更优的写法

  • 上述练习创建的线程对象,都是只用一次。因此可以创建匿名子类的匿名对象来启动线程。

优化后的代码:

//线程1-遍历100以内的偶数
new Thread(){
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}.start();

//线程2-遍历100以内的奇数
new Thread(){
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i % 2 != 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}.start();

10.2.4 线程的常用方法

线程方法作用
void start()启动线程,并执行对象的 run()方法
run()线程在被调度时执行的操作
String getName()返回线程的名称
void setName()设置该线程名称
static Thread currentThread()返回当前线程。在 Thread 子类中就是this ,通常用于主线程和 Runnable实现类
static void yield()线程让步:暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程;若队列中没有同优先级的线程,忽略此方法
join()当某个程序执行流中调用其他线程的 join() 方法时,调用线程将被阻塞,直到 join() 方法加入的 join 线程执行完为止。低优先级的线程也可以获得执行。
static void sleep(long millis):(指定时间:毫秒)① 令当前活动线程在指定时间段内放弃对 CPU 控制,使其他线程有机会被执行,时间到后重新排队。② 抛出 InterruptedException 异常。
stop()强制线程生命期结束,不推荐使用!
boolean isAlive()返回 boolean ,判断线程是否还活着

使用例子

( 1 1 1) 设置线程名称

  • 次线程可通过 setName() 设置名字。
  • 主线程可通过 currentThread().setName() 设置名字。
public class ThreadMethods {
    public static void main(String[] args) {
        ThreadName threadName = new ThreadName();
        threadName.setName("求偶数的线程");
        threadName.start();
        
        //主线程设置名字操作
        Thread.currentThread().setName("主线程");
        for (int i = 0; i < 20; i++) {
            if (i % 2 != 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}

//展示如何给线程取名的操作
class ThreadName extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}

输出:

主线程:1
主线程:3
主线程:5
求偶数的线程:0
求偶数的线程:2
求偶数的线程:4
求偶数的线程:6
求偶数的线程:8
主线程:7
主线程:9
主线程:11
主线程:13
主线程:15
主线程:17
主线程:19
求偶数的线程:10
求偶数的线程:12
求偶数的线程:14
求偶数的线程:16
求偶数的线程:18

( 2 2 2) yield线程让步

  • 放弃当前的线程。然后有两种可能:

    ① 被放弃的线程被别的线程抢到执行;

    ② 又被自己抢到执行机会。这样就会看起来没什么区别。

  • 例子:当 i i i 能被4整除时,调用 yield

public class ThreadMethods {
    public static void main(String[] args) {
        ThreadName threadName = new ThreadName();
        threadName.setName("求偶数的线程");
        threadName.start();

        //主线程设置名字操作
        Thread.currentThread().setName("主线程");
        for (int i = 0; i < 20; i++) {
            if (i % 2 != 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}

//展示如何给线程取名的操作
class ThreadName extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }

            if (i % 4 == 0) {
                Thread.yield();
            }
        }
    }
}

输出:

主线程:1
主线程:3
主线程:5
主线程:7
主线程:9
求偶数的线程:0
主线程:11
求偶数的线程:2
求偶数的线程:4
主线程:13
主线程:15
主线程:17
主线程:19
求偶数的线程:6
求偶数的线程:8
求偶数的线程:10
求偶数的线程:12
求偶数的线程:14
求偶数的线程:16
求偶数的线程:18
  • 可以看到,当 求偶数的线程:4 时,线程放弃执行机会,后面就被主线程抢到。直到主线程输出完所有20以内的奇数为止。

( 3 3 3) join操作:打断当前线程的执行,插入其他线程

  • join() 操作就是一个插队操作。在当前线程中插入别的线程的 join() 方法后,当前线程就中断,去执行另一个线程直到执行完毕再恢复原来的线程。

  • join() 方法要处理异常。

  • 下面的例子中,当主线程的 i i i 执行到 5 5 5 时,主线程被中断,插入了另一个线程threadName.join() 方法。只有当 treadName 线程执行完毕,才能恢复主线程的执行。

public class ThreadMethods {
    public static void main(String[] args) {
        ThreadName threadName = new ThreadName();
        threadName.setName("求偶数的线程");
        threadName.start();

        //主线程设置名字操作
        Thread.currentThread().setName("主线程");
        for (int i = 0; i < 20; i++) {
            if (i % 2 != 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }

            //join()方法的测试
            if (i == 5) {
                try {
                    threadName.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

class ThreadName extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}

输出:

主线程:1
主线程:3
主线程:5
求偶数的线程:0
求偶数的线程:2
求偶数的线程:4
求偶数的线程:6
求偶数的线程:8
求偶数的线程:10
求偶数的线程:12
求偶数的线程:14
求偶数的线程:16
求偶数的线程:18
主线程:7
主线程:9
主线程:11
主线程:13
主线程:15
主线程:17
主线程:19

( 4 4 4) sleep(long millis):让线程阻塞 millis 的时间

  • sleep(long millis) 需要异常处理。
  • 下面的例子是 threadName 线程每隔 1000 1000 1000 毫秒 ( 1 1 1 秒) 才输出一个偶数。可以看到有倒计时的效果。
public class ThreadMethods {
    public static void main(String[] args) {
        ThreadName threadName = new ThreadName();
        threadName.setName("求偶数的线程");
        threadName.start();

        //主线程设置名字操作
        Thread.currentThread().setName("主线程");
        for (int i = 0; i < 20; i++) {
            if (i % 2 != 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }

            //join()方法的测试
            if (i == 5) {
                try {
                    threadName.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

//展示如何给线程取名的操作
class ThreadName extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            if (i % 2 == 0) {

                //sleep(long millis)的测试
                try {
                    sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}

( 5 5 5) isAlive():让线程阻塞 millis 的时间

  • 线程在 run() 方法执行完之后就死亡了。可以使用 isAlive() 方法判断该线程是否还存活。
public class ThreadMethods {
    public static void main(String[] args) {
        ThreadName threadName = new ThreadName();
        threadName.setName("求偶数的线程");
        threadName.start();

        //主线程设置名字操作
        Thread.currentThread().setName("主线程");
        for (int i = 0; i < 20; i++) {
            if (i % 2 != 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }

            //join()方法的测试
            if (i == 5) {
                try {
                    threadName.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

        }
        //isAlive()方法测试
        System.out.println("线程threadName是否存活:" + threadName.isAlive());
    }
}

//展示如何给线程取名的操作
class ThreadName extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            if (i % 2 == 0) {

                //sleep(long millis)的测试
                try {
                    sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}

输出:

主线程:1
主线程:3
主线程:5
求偶数的线程:0
求偶数的线程:2
求偶数的线程:4
求偶数的线程:6
求偶数的线程:8
求偶数的线程:10
求偶数的线程:12
求偶数的线程:14
求偶数的线程:16
求偶数的线程:18
主线程:7
主线程:9
主线程:11
主线程:13
主线程:15
主线程:17
主线程:19
线程threadName是否存活:false

10.2.5 线程的优先级

  • 线程优先级分为 1 1 1 ~ 10 10 10 级。其中 10 10 10 级为最高优先级。

  • Thread 类中默认设置了三档优先级属性:

    优先级属性
    最大优先级MAX_PRIORITY:10
    默认优先级NORM_PRIORITY:5
    最小优先级MIN _PRIORITY:1
    • 如果没有明确声明,一般线程优先级默认设置为 5 5 5
  • 关于优先级的方法:

    方法名作用
    getPriority()获取当前线程的优先级
    setPriority(Thread.MAX_PRIORITY)设置当前线程的优先级

说明

  • 至少要在 start() 前,设置优先级。
  • 线程被设置为最大优先级时,并不意味着该线程一定能执行完,才去执行低优先级的线程。只是有很大概率能被 CPU 优先执行。

例子

public class ThreadMethods {
    public static void main(String[] args) {
        ThreadName threadName = new ThreadName();
        threadName.setName("求偶数的线程");

        //设置线程优先级为最大
        threadName.setPriority(Thread.MAX_PRIORITY);
        threadName.start();

        //主线程设置名字操作
        Thread.currentThread().setName("主线程");

        //设置主线程优先级为最小
        Thread.currentThread().setPriority(Thread.MIN_PRIORITY);

        for (int i = 0; i < 20; i++) {
            if (i % 2 != 0) {
                System.out.println("优先级:" + Thread.currentThread().getPriority() +
                        "|" + Thread.currentThread().getName() + ":" + i);
            }
        }
        
        //isAlive()方法测试
        System.out.println("线程threadName是否存活:" + threadName.isAlive());
    }
}

//线程ThreadName
class ThreadName extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            if (i % 2 == 0) {
                System.out.println("优先级:" + getPriority() +
                        "|" + Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}

输出:

优先级:1|主线程:1
优先级:10|求偶数的线程:0
优先级:10|求偶数的线程:2
优先级:10|求偶数的线程:4
优先级:10|求偶数的线程:6
优先级:10|求偶数的线程:8
优先级:10|求偶数的线程:10
优先级:1|主线程:3
优先级:10|求偶数的线程:12
优先级:10|求偶数的线程:14
优先级:10|求偶数的线程:16
优先级:10|求偶数的线程:18
优先级:1|主线程:5
优先级:1|主线程:7
优先级:1|主线程:9
优先级:1|主线程:11
优先级:1|主线程:13
优先级:1|主线程:15
优先级:1|主线程:17
优先级:1|主线程:19
线程threadName是否存活:false
  • 可以看到优先级为 1 的主线程,居然在第一行被优先执行了,说明优先级高的并不一定先被执行。但从整体来说,求偶数的线程 被优先执行的概率确实是比较大的。

10.2.6 例子:卖票-线程安全问题

题目

创建三个窗口卖票,总票数为100张。

首先创建一个卖票的窗口类Window:

  • 其中,ticket 变量必须设置为 static 的,所有的线程实例都共享这个静态变量。否则会出现每个线程各自卖 100 100 100 张票。
class Window extends Thread {

    private static int ticket = 100;

    @Override
    public void run() {
        while (true) {
            if (ticket > 0) {
                System.out.println(getName() + ":卖票,票号为:" +
                        ticket);
                ticket--;
            } else {
                System.out.println("票已售罄");
                break;
            }
        }
    }
}

然后,在主线程中创建3个窗口实例。开始卖票:

public class WindowTest {
    public static void main(String[] args) {

        Window w1 = new Window();
        w1.setName("窗口1");
        Window w2 = new Window();
        w2.setName("窗口2");
        Window w3 = new Window();
        w3.setName("窗口3");

        w1.start();
        w2.start();
        w3.start();
    }
}

输出:

image-20220315101106010

  • 可以看到,3个窗口实例线程都卖出了票号为 100 100 100 的票。这显然是不符合现实逻辑的。这就引出了线程安全问题,引出了下面第二种创建线程的方式。

10.2.7 创建方式二:实现Runnable接口

( 1 1 1) 创建一个实现了Runnable接口的类;

( 2 2 2) 实现类去实现Runnable中的抽象方法:run();

( 3 3 3) 创建实现类的对象;

( 4 4 4) 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象;

( 5 5 5) 通过Thread类的对象调用start()。

//1.创建一个实现了Runnable接口的类
class CreateTread implements Runnable {

    private int ticket = 10;//注意这里没加static

    //2.实现类去实现Runnable中的抽象方法:run()
    @Override
    public void run() {
        while (true) {
            if (ticket > 0) {
                System.out.println(Thread.currentThread().getName() +
                        ":卖票,票号为:" + ticket);
                ticket--;
            } else {
                System.out.println("票已售罄");
                break;
            }
        }
    }
}


public class ThreadCreate2 {
    public static void main(String[] args) {
        
        //3.创建实现类的对象
        CreateTread c = new CreateTread();
        
        //4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
        Thread t1 = new Thread(c);
        t1.setName("窗口1");
        
        //5.通过Thread类的对象调用start()
        t1.start();

        //启动新线程
        Thread t2 = new Thread(c);
        t2.setName("窗口2");
        t2.start();
    }
}

输出:

窗口1:卖票,票号为:10
窗口1:卖票,票号为:9
窗口1:卖票,票号为:8
窗口1:卖票,票号为:7
窗口1:卖票,票号为:6
窗口1:卖票,票号为:5
窗口1:卖票,票号为:4
窗口1:卖票,票号为:3
窗口1:卖票,票号为:2
窗口1:卖票,票号为:1
票已售罄
窗口2:卖票,票号为:10
票已售罄
  • 可以看到,虽然 ticket 没有加 static 修饰,但2个线程是共用这10张票的。因为线程 t1 和 t2 都共用 c 这一个对象创建线程。
  • 可以看到还是存在 10 10 10 号票被2个窗口都卖出去的情况。并没有解决实际问题。

10.2.8 两种线程创建方式的比较

  • 开发中,优先选择:实现Runnable接口的方式。

    • 原因1:实现的方式没有类的单继承性的局限性。而继承的方式就不能继承其他父类了。
    • 原因2:实现的方式更适合来处理多个线程有共享数据的情况。而继承的方式处理多个线程共享数据时需要把此数据定义为 static 的。
  • 联系:Thread 类实际上也实现了 Runnable 接口。

    public class Thread implements Runnable {
        ……
    }
    
  • 相同点:两种方式都需要重写 run() ,将线程要执行的逻辑声明在 run() 中。

10.3 线程的生命周期

10.3.1 线程的 5 5 5 种生命周期

线程状态描述
新建当一个 Thread 类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
就绪处于 新建 状态的线程被 start() 后,将进入线程队列等待 CPU 时间片。此时它已具备了运行的条件 ,只是没分配到 CPU 资源。
执行当就绪的线程被调度并获得 CPU 资源时,便进入运行状态, run() 方法定义了线程的操作和功能
阻塞在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中止自己的执行,进入阻塞状态
死亡线程完成了它的全部工作、或线程被提前强制性地中止、或出现异常导致结束

10.3.2 线程生命周期

image-20220315132247492

  • 死亡 是线程的最终状态。任何线程必然走向死亡。 阻塞不是线程的最终状态,而是中间的、暂时的一种状态。

10.4 线程的同步

10.4.1 线程安全问题

  • 以上方卖票窗口的代码为例,会出现重票、甚至错票的问题。

    • 重票

    image-20220315101106010

    • 错票

      image-20220315140030407

问题出现的原因

  • 当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作车票。

如何解决?

  • 当一个线程a在操作 ticket 的时候,其他线程不能参与进来。直到线程a操作完 ticket 时,其他线程才可以开始操作 ticket 。这种情况即使线程a出现了阻塞,也不能被改变。
  • 相当于上厕所时,给厕所门锁上。只有当自己方便完之后,门外的人才能进来。

10.4.2 同步机制一:同步代码块

在Java中,通过 线程同步机制 来解决线程安全问题。

格式:

  • 在需要对共享数据进行操作的部分,用下面的语句包起来:
  • synchronized () {} 包裹的代码块,只允许一个线程进来。该线程执行过程中,其他所有线程都必须在代码块外等待。直到该线程执行完毕,其他线程再抢夺进入该代码块的执行权。
synchronized (同步监视器) {
    需要被同步的代码;
}

说明

  • 操作共享数据的代码,即为需要被同步的代码。 -->不能包含代码多了,也不能包含代码少了。

  • 共享数据:多个线程共同操作的变量。比如:ticket 就是共享数据。

  • 同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。

  • 要求:多个线程必须要共用同一把锁

  • 补充:在实现 Runnable 接口创建多线程的方式中,我们可以考虑使用 this 充当同步监视器。注意继承 Thread 方式创建的线程则不能用 this 。因为继承方式每一个线程都是不同的对象。

同步代码块在继承法 Thread 创建的线程中的使用

  • 在继承 Thread 方式创建的线程时,第一种方法是同步监视器必须要创建新的 static 对象。这样才能保证每个线程实例对象都共用同一个同步监视器。

    static Object obj = new Object();
    
  • 在继承 Thread 方式创建的线程的第二种同步监视器方式是 线程类名.class

    synchronized (线程类名.class) {
        
    }
    

例子

  • 在 实现 Runnable 接口创建多线程的方式中采用 synchronized () {} 同步代码块:
class CreateTread implements Runnable {

    private int ticket = 100;
    Object obj = new Object();//创建了一个对象用作同步监视器

    //2.实现类去实现Runnable中的抽象方法:run()
    @Override
    public void run() {
        while (true) {

            //线程同步方式一:同步代码块synchronized () {}
            synchronized (obj) {

                if (ticket > 0) {

                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    System.out.println(Thread.currentThread().getName() +
                            ":卖票,票号为:" + ticket);
                    ticket--;
                } else {
                    System.out.println("票已售罄");
                    break;
                }
            }
        }
    }
}


public class ThreadCreate2 {
    public static void main(String[] args) {

        //3.创建实现类的对象
        CreateTread c = new CreateTread();

        //4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
        Thread t1 = new Thread(c);
        t1.setName("窗口1");

        //5.通过Thread类的对象调用start()
        t1.start();

        //启动新线程
        Thread t2 = new Thread(c);
        t2.setName("窗口2");
        t2.start();

        Thread t3 = new Thread(c);
        t3.setName("窗口3");
        t3.start();
    }
}

输出:

窗口1:卖票,票号为:50
窗口1:卖票,票号为:49
窗口1:卖票,票号为:48
窗口1:卖票,票号为:47
窗口1:卖票,票号为:46
窗口1:卖票,票号为:45
窗口1:卖票,票号为:44
窗口1:卖票,票号为:43
窗口1:卖票,票号为:42
窗口1:卖票,票号为:41
窗口1:卖票,票号为:40
窗口1:卖票,票号为:39
窗口1:卖票,票号为:38
窗口1:卖票,票号为:37
窗口1:卖票,票号为:36
窗口1:卖票,票号为:35
窗口1:卖票,票号为:34
窗口1:卖票,票号为:33
窗口1:卖票,票号为:32
窗口1:卖票,票号为:31
窗口1:卖票,票号为:30
窗口3:卖票,票号为:29
窗口3:卖票,票号为:28
窗口3:卖票,票号为:27
窗口3:卖票,票号为:26
窗口3:卖票,票号为:25
窗口3:卖票,票号为:24
窗口3:卖票,票号为:23
窗口3:卖票,票号为:22
窗口3:卖票,票号为:21
窗口3:卖票,票号为:20
窗口3:卖票,票号为:19
窗口3:卖票,票号为:18
窗口3:卖票,票号为:17
窗口3:卖票,票号为:16
窗口3:卖票,票号为:15
窗口3:卖票,票号为:14
窗口3:卖票,票号为:13
窗口3:卖票,票号为:12
窗口3:卖票,票号为:11
窗口3:卖票,票号为:10
窗口3:卖票,票号为:9
窗口3:卖票,票号为:8
窗口3:卖票,票号为:7
窗口3:卖票,票号为:6
窗口3:卖票,票号为:5
窗口3:卖票,票号为:4
窗口3:卖票,票号为:3
窗口3:卖票,票号为:2
窗口2:卖票,票号为:1
票已售罄
票已售罄
票已售罄
  • 可以看到,重票和错票就已经没有再发生了。

  • 好处:同步的方式,解决了线程的安全问题。

  • 坏处:操作同步代码时,只能有一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低。

10.4.3 同步机制二:同步方法

1. 同步方法处理 实现 Runnable 接口线程

  • 如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明同步的。

  • 同步方法仍然涉及到同步监视器,只是不需要我们显式的声明。

  • Runnable 接口线程此时的同步方法是非静态的,同步监视器默认为:this

  • 继承 Thread 线程的同步方法必须是静态的,同步监视器默认为:线程类名.class

  • 格式:

    private synchronized void show() {//同步监视器默认设置为:this
        需要被同步的代码;
    }
    

例子

//1.创建一个实现了Runnable接口的类
class Window2 implements Runnable {

    private int ticket = 100;//注意这里没加static

    //2.实现类去实现Runnable中的抽象方法:run()
    @Override
    public void run() {

        while (true) {
            show();

            if (ticket <= 0) {
                System.out.println("票已售罄");
                break;
            }
        }
    }

    //同步方法
    private synchronized void show() {//同步监视器默认设置为:this
        if (ticket > 0) {

            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println(Thread.currentThread().getName() +
                    ":卖票,票号为:" + ticket);
            ticket--;
        }
    }
}


public class WindowTest2 {
    public static void main(String[] args) {

        //3.创建实现类的对象
        Window2 w = new Window2();

        //4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
        Thread t1 = new Thread(w);
        t1.setName("窗口1");

        //5.通过Thread类的对象调用start()
        t1.start();

        //启动新线程
        Thread t2 = new Thread(w);
        t2.setName("窗口2");
        t2.start();

        Thread t3 = new Thread(w);
        t3.setName("窗口3");
        t3.start();
    }
}

2. 同步方法处理 继承 Thread 创建的线程

  • 根据同步监视器必须只有一个,因此继承 Thread 线程的同步方法必须是静态的,此时默认的同步监视器是 线程类名.class 。才能保证下面3个线程实例对象共用同一个静态同步方法。

  • 例子

    class Window3 extends Thread {
    
        private static int ticket = 100;
    
        @Override
        public void run() {
    
            while (true) {
                show();
    
                if (ticket <= 0) {
                    System.out.println("票已售罄");
                    break;
                }
            }
        }
    
        //静态同步方法
        private static synchronized void show() {//同步监视器默认设置为:Window3.class
    
            if (ticket > 0) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
    
                System.out.println(Thread.currentThread().getName() +
                        ":卖票,票号为:" + ticket);
                ticket--;
            }
        }
    }
    
    //主线程
    public class WindowTest3 {
        public static void main(String[] args) {
    
            Window3 w1 = new Window3();
            w1.setName("窗口1");
            Window3 w2 = new Window3();
            w2.setName("窗口2");
            Window3 w3 = new Window3();
            w3.setName("窗口3");
    
            w1.start();
            w2.start();
            w3.start();
        }
    }
    

10.4.4 单例模式中的线程安全

  • 在单例模式中,分为饿汉式和懒汉式。懒汉式是到了调用的时候才新建对象,这样会存在线程安全问题。

  • 要求:使用同步机制将单例模式中的懒汉式改写为线程安全的。

同步代码块

//懒汉的单例模式
class Bank {

    private Bank() {
    }

    private static Bank bank = null;
    public static Bank getInstance() {
        synchronized (Bank.class) {
            if (bank == null) {
                bank = new Bank();
            }
            return bank;
        }
    }
}

同步方法

//懒汉的单例模式
class Bank {

    private Bank() {
    }

    private static Bank bank = null;
    public static synchronized Bank getInstance() {//此时默认同步监视器是Bank.class
        if (bank == null) {
            bank = new Bank();
        }
        return bank;
    }
}

注意:

以上两种效率都不高。并不是说线程同步导致的效率低,而是:假如很多个线程同时进来 getInstance() 方法。第一个抢到执行权的线程 new 了一个 Bank 的实例对象,并返回了 bank 实例。后面的所有线程进来不用再 new 一个 Bank 的实例对象了。因此已经不存在线程安全问题,但是却还都要一个一个地排队 return bank 。这样效率显然很低,后面进来的线程不需要进入同步代码块了。

优化代码:

//懒汉的单例模式
class Bank {

    private Bank() {
    }

    private static Bank bank = null;
    public static Bank getInstance() {

        //方式一:效率稍差
//        synchronized (Bank.class) {
//            if (bank == null) {
//                bank = new Bank();
//            }
//            return bank;
//        }

        //方式二:效率更高
        if (bank==null){
            synchronized (Bank.class){
                if (bank==null){
                    bank=new Bank();
                }
            }
        }
        return bank;
    }
}

10.4.5 死锁的问题

image-20220315225625309

10.4.6 Lock锁解决线程安全问题

  • Lock锁是 JDK 5.0 之后新增的特性。在实际开发中,优先使用 Lock锁,JVM 将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性 (提供更多子类) 。

image-20220315231209925

1. Lock锁的使用步骤

  • Lock锁是一个接口,需要通过 ReentrantLock 实现类去实例化对象使用。

  • 实现Lock接口的类:ReentrantLock ,要声明成 privatefinal的:

    private final ReentrantLock lock = new ReentrantLock();
    
  • 通过 lock 对象调用 lock() 方法。此后代码只允许单线程进入,要用try-finally包起来,确保在 finally 中使用 unlock() 方法解锁:

    //上锁,此后代码只允许单线程进入
    lock.lock();
    
    try {
        需要同步的代码;
    } finally {
    	//解锁,此后代码可以恢复为多线程操作
        lock.unlock();
    }
    

2. 卖票窗口例子

class Window4 implements Runnable {

    private int ticket = 100;//注意这里没加static

    //实现Lock接口的类:ReentrantLock,要声明成private和final的
    private final ReentrantLock lock = new ReentrantLock();

    //2.实现类去实现Runnable中的抽象方法:run()
    @Override
    public void run() {

        while (true) {

            //上锁,此后代码只允许单线程进入,要用try-finally包起来确保解锁
            lock.lock();

            try {
                if (ticket > 0) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    System.out.println(Thread.currentThread().getName() +
                            ":卖票,票号为:" + ticket);
                    ticket--;
                } else {
                    System.out.println("票已售罄");
                    break;

                }
            } finally {
                //解锁,此后代码可以恢复为多线程操作
                lock.unlock();
            }
        }
    }
}


public class LockTest {
    public static void main(String[] args) {

        //3.创建实现类的对象
        Window4 w = new Window4();

        //4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
        Thread t1 = new Thread(w);
        t1.setName("窗口1");

        //5.通过Thread类的对象调用start()
        t1.start();

        //启动新线程
        Thread t2 = new Thread(w);
        t2.setName("窗口2");
        t2.start();

        Thread t3 = new Thread(w);
        t3.setName("窗口3");
        t3.start();
    }
}

3. synchronized 与 Lock锁的对比

  • 相同:二者都可以解决线程安全问题。

  • 不同

    • synchronized 机制在执行完相应的同步代码以后,自动地释放同步监视器。

    • Lock 需要手动的启动同步 lock(),同时结束同步也需要手动地实现unlock()

    • Lock 只有代码块锁,synchronized 有代码块锁和方法锁。

    • 使用 Lock锁,JVM 将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性 (提供更多子类) 。

4. 优先使用顺序

Lock --> synchronized同步代码块(已经进入了方法体,分配了相应资源) --> synchronized 同步方法(在方法体之外)

10.4.7 线程同步练习

题目

银行有一个账户。
有两个储户分别向同一个账户存3000元,每次存1000,存3次。每次存完打印账户余额。

问题:该程序是否有线程安全问题?如果有,如何解决?

存在线程安全的版本

//实现Runnable方式创建多线程
class Account implements Runnable {

    //共享的银行账户
    private double balance;

    @Override
    public void run() {

        int i = 3;//存3次
        while (i > 0) {

            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            balance += 1000;
            System.out.println(Thread.currentThread().getName() +
                    "存钱,余额为:" + balance);
            i--;
        }
    }
}

//主线程
public class AccountTest {
    public static void main(String[] args) {
        Account a = new Account();

        //两个储户线程
        Thread t1 = new Thread(a);
        t1.setName("储户1");
        Thread t2 = new Thread(a);
        t2.setName("储户2");

        //开始存钱
        t1.start();
        t2.start();
    }
}

输出

储户2存钱,余额为:1000.0
储户1存钱,余额为:1000.0
储户1存钱,余额为:3000.0
储户2存钱,余额为:3000.0
储户2存钱,余额为:5000.0
储户1存钱,余额为:4000.0
  • 可见,余额应该是 6000.0 6000.0 6000.0 元,但因为线程不同步导致余额为 5000.0 5000.0 5000.0 元。

1.用Lock锁解决线程安全的版本

//实现Runnable方式创建多线程
class Account implements Runnable {

    //共享的银行账户
    private double balance;
    //Lock锁
    private final ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {

        int i = 3;//存3次

        while (i > 0) {

            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            //上锁
            lock.lock();
            try {
                balance += 1000;
                System.out.println(Thread.currentThread().getName() +
                        "存钱,余额为:" + balance);
            } finally {
                //解锁
                lock.unlock();
            }
            i--;
        }
    }
}

//主线程
public class AccountTest {
    public static void main(String[] args) {
        Account a = new Account();

        //两个储户线程
        Thread t1 = new Thread(a);
        t1.setName("储户1");
        Thread t2 = new Thread(a);
        t2.setName("储户2");

        //开始存钱
        t1.start();
        t2.start();
    }
}

输出

储户2存钱,余额为:1000.0
储户1存钱,余额为:2000.0
储户2存钱,余额为:3000.0
储户1存钱,余额为:4000.0
储户2存钱,余额为:5000.0
储户1存钱,余额为:6000.0

2.用 synchronized 同步方法解决线程安全的版本

//实现Runnable方式创建多线程
class Account implements Runnable {

    //共享的银行账户
    private double balance;

    @Override
    public void run() {

        int i = 3;//存3次

        while (i > 0) {

            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
			//调用同步方法
            deposit();
            
            i--;
        }
    }
	
    //同步方法
    private synchronized void deposit() {
        balance += 1000;
        System.out.println(Thread.currentThread().getName() +
                "存钱,余额为:" + balance);
    }
}

//主线程
public class AccountTest {
    public static void main(String[] args) {
        Account a = new Account();

        //两个储户线程
        Thread t1 = new Thread(a);
        t1.setName("储户1");
        Thread t2 = new Thread(a);
        t2.setName("储户2");

        //开始存钱
        t1.start();
        t2.start();
    }
}

输出

储户1存钱,余额为:1000.0
储户2存钱,余额为:2000.0
储户2存钱,余额为:3000.0
储户1存钱,余额为:4000.0
储户1存钱,余额为:5000.0
储户2存钱,余额为:6000.0

3.用 synchronized 同步代码块解决线程安全的版本

//实现Runnable方式创建多线程
class Account implements Runnable {

    //共享的银行账户
    private double balance;

    @Override
    public void run() {

        int i = 3;//存3次

        while (i > 0) {

            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
			//同步代码块
            synchronized (this) {
                balance += 1000;
                System.out.println(Thread.currentThread().getName() +
                        "存钱,余额为:" + balance);
            }
            i--;
        }
    }
}

//主线程
public class AccountTest {
    public static void main(String[] args) {
        Account a = new Account();

        //两个储户线程
        Thread t1 = new Thread(a);
        t1.setName("储户1");
        Thread t2 = new Thread(a);
        t2.setName("储户2");

        //开始存钱
        t1.start();
        t2.start();
    }
}

输出:

储户1存钱,余额为:1000.0
储户2存钱,余额为:2000.0
储户2存钱,余额为:3000.0
储户1存钱,余额为:4000.0
储户2存钱,余额为:5000.0
储户1存钱,余额为:6000.0

10.5 线程的通信

10.5.1 例题

使用两个线程打印1~20。线程1、线程2交替打印。

解:

class Number implements Runnable {

    private int num = 1;

    @Override
    public void run() {

        while (true) {

            synchronized (this) {

                //唤醒被wait()的线程
                notify();

                if (num <= 20) {

                    try {
                        Thread.sleep(5);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }


                    System.out.println(Thread.currentThread().getName() +
                            "打印:" + num);
                    num++;

                    //使得调用如下wait()方法的线程进入阻塞状态
                    try {
                        wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                } else {
                    break;
                }
            }
        }
    }
}


public class ThreadCommunication {
    public static void main(String[] args) {
        Number n = new Number();

        Thread t1 = new Thread(n);
        t1.setName("线程1");
        Thread t2 = new Thread(n);
        t2.setName("线程2");

        t1.start();
        t2.start();
    }
}
  • 上述代码的执行经过:线程1抢到同步锁进入 synchronized() 代码块中。–>

  • 没有线程被 wait() ,因此 notify() 无效。–>

  • 线程1输出 number 1 1 1,执行 wait() 被阻塞,同时释放同步锁。–>

  • 线程2得到同步锁进入 synchronized() 代码块中。–>

  • 线程2执行 notify() ,激活线程1。但此时同步锁在线程2手上,因此线程1只能在 synchronized() 代码块外等待。–>

  • 线程2继续输出 number 2 2 2,执行 wait() 被阻塞,同时释放同步锁。–>

  • 线程1得到同步锁进入 synchronized() 代码块中。–>

  • 线程1执行 notify() ,激活线程2。但此时同步锁在线程1手上,因此线程2只能在 synchronized() 代码块外等待。–>

  • 线程1继续输出 number 3 3 3,执行 wait() 被阻塞,同时释放同步锁。–>

    ……

    如此循环交替输出。

输出

线程1打印:1
线程2打印:2
线程1打印:3
线程2打印:4
线程1打印:5
线程2打印:6
线程1打印:7
线程2打印:8
线程1打印:9
线程2打印:10
线程1打印:11
线程2打印:12
线程1打印:13
线程2打印:14
线程1打印:15
线程2打印:16
线程1打印:17
线程2打印:18
线程1打印:19
线程2打印:20

10.5.2 通信涉及的三个方法

通信方法作用
wait()一旦执行此方法,该线程就进入阻塞状态。并释放同步监视器的锁
notify()一旦执行此方法,就会唤醒被 wait() 的一个线程。如果有多个线程被 wait() ,那么唤醒优先级高的。如果都为默认优先级,则随机唤醒一个。
notifyAll()一旦执行此方法,就会唤醒所有wait() 的线程。

10.5.3 使用说明

  • wait()notify()notifyAll() 三个方法必须使用在同步代码块同步方法中。
  • wait()notify()notifyAll() 三个方法的调用者必须是同步代码块或同步方法中的同步监视器。否则,会出现IllegalMonitorStateException 异常。
  • wait()notify()notifyAll() 三个方法是定义在java.lang.Object类中。而不是 Thread 类中。

10.5.4 sleep()wait() 的异同?

相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态。

不同点

1)两个方法声明的位置不同:Thread 类中声明 sleep() , Object 类中声明 wait()

2)调用的要求不同:sleep() 可以在任何需要的场景下调用。 wait() 必须使用在同步代码块或同步方法中。

3)关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep() 不会释放锁,wait() 会释放锁。

10.5.5 经典例题:生产者/消费者问题

题目

image-20220316150522478

难点

  • 生产者端和消费者端的线程之间如何通信?

    答:让生产者端和消费者端的构造器都与店员 (Clerk) 关联起来。

    class Producer implements Runnable {
    
        private Clerk clerk;
    
        //把生产者端和中间端店员关联起来
        public Producer(Clerk clerk) {
            this.clerk = clerk;
        }
        
      ......
          
    }
    
  • 消费者线程一开始就因为产品为0而被 wait()notify() 应该加在哪儿呢?

    答:只要生产了1件商品,就可以通知消费者线程开始消费了。相应的,如果店员满了20件商品被 wait() ,只要消费了1件商品,就可以通知生产线程开始生产了。

    products++;
    notify();
    System.out.println(Thread.currentThread().getName() +
            "生产,产品数:" + products);
    

整体代码

//共享对象:店员
class Clerk {
    //固定的产品数量-20个
    protected int products;

    //生产
    public synchronized void produce() {//此时同步监视器为Clerk的对象

        //超过20个等一下
        if (products >= 20) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } else {
            products++;
            notify();
            System.out.println(Thread.currentThread().getName() +
                    "生产,产品数:" + products);
        }
    }


    //消费
    public synchronized void consume() {//此时同步监视器为Clerk的对象,与生产端一样

        //没有商品了等一下
        if (products <= 0) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } else {
            products--;
            notify();
            System.out.println(Thread.currentThread().getName() +
                    "消费,产品数:" + products);
        }
    }
}


//生产者
class Producer implements Runnable {

    private Clerk clerk;

    //把生产者端和中间端店员关联起来
    public Producer(Clerk clerk) {
        this.clerk = clerk;
    }

    @Override
    public void run() {

        //不断增加产品
        while (true) {

            //设置一下生产速度,暂定1秒生产1个
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            clerk.produce();
        }
    }
}

//消费者
class Customer implements Runnable {

    private Clerk clerk;

    public Customer(Clerk clerk) {
        this.clerk = clerk;
    }

    @Override
    public void run() {

        //不断消耗产品
        while (true) {

            //设置一下消费速度,暂定1秒消耗1个
            try {
                Thread.sleep(700);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            clerk.consume();
        }
    }
}


//主线程
public class ProductTest {
    public static void main(String[] args) {

        Clerk clerk = new Clerk();
        Producer p = new Producer(clerk);
        Customer c = new Customer(clerk);

        //生产者端
        Thread p1 = new Thread(p);
        p1.setName("生产者1");
        Thread p2 = new Thread(p);
        p2.setName("生产者2");
        Thread p3 = new Thread(p);
        p3.setName("生产者3");

        //消费者端
        Thread c1 = new Thread(c);
        c1.setName("消费者1");

        //多线程启动
        p1.start();
        p2.start();
        p3.start();
        c1.start();
    }
}

练习的体会

  • Clerk 类,既没有继承 Thread 类,也没有实现 Runnable 接口。因此没有线程的 run() 方法,也创建不了多线程。但是,其被实现了 Runnable 接口的生产者 Producer 类和消费者 Customer 类各自关联到自己的构造器当中了,且调用了Clerk 类中的同步方法 生产produce() 和 消费 consume() 。因此,Clerk 类中可以定义 synchronized 同步方法生产produce() 和 消费 consume() 。并在其中调用了线程通信的 wait()notify() 方法。
  • 两个不同的类:实现了 Runnable 接口的生产者 Producer 类和消费者 Customer 类,可以互相通知唤醒 notify()对方的线程。因为生产者 Producer 类和消费者 Customer 类都调用了Clerk 类中的同步方法 生产produce() 和 消费 consume() ,且这两个同步方法拥有相同的同步监视器:Clerk 类的对象 this

10.6 JDK5.0 新增线程创建方式

10.6.1 创建方式三:实现Callable接口

1. Callable接口创建线程步骤

  • 创建一个 Callable 的实现类;
  • 实现 call() 方法,将此线程需要执行的操作声明在call()中。
  • 创建一个 Callable 的实现类的对象;
  • 将此Callable 接口实现类的对象作为传递到 FutureTask 构造器中,创建 FutureTask 的对象;
  • FutureTask 的对象作为参数传递到 Thread 类的构造器中,创建 Thread 对象,并调用 start()
  • (可选) 获取 Callablecall 方法的返回值。

2. Callable比Runnable接口强大

  • 相比 run() 方法,call() 方法可以有返回值。
  • call() 方法可以抛出异常。
  • 支持泛型的返回值 (后面介绍什么是泛型)。
  • 需要借助 FutureTask 类,比如获取返回结果。
  • Future 接口:
    • 可以对具体RunnableCallable 任务的执行结果进行取消、查询是否完成、获取结果等。
    • FutureTask 类是Future 接口唯一的实现类。
    • FutureTask 类同时实现了RunnableFuture 接口。它既可以作为 Runnable 被线程执行,又可以作为Future 得到Callable 的返回值。

3. 例子

  • 输出20内的偶数,并返回这些偶数的和:
//1.创建一个Callable的实现类
class NumThread implements Callable {

    //2.实现call方法,将此线程需要执行的操作声明在call()中
    @Override
    public Object call() throws Exception {
        int sum = 0;
        for (int i = 0; i < 20; i++) {
            if (i % 2 == 0) {
                System.out.println(i);
                sum += i;
            }
        }
        return sum;//包装类Integer自动装箱
    }
}

public class ThreadNew {
    public static void main(String[] args) {
        //3.创建callable实现类的对象
        NumThread numThread = new NumThread();

        //4.将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
        FutureTask futureTask = new FutureTask(numThread);

        //5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
        Thread t1 = new Thread(futureTask);
        t1.start();

        try {
            //6.获取Callable中call方法的返回值
            Object sum = futureTask.get();
            System.out.println("sum = " + sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

    }
}

10.6.2 创建方式四:线程池

  • 开发中真正用的都是线程池
  • 背景: 经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
  • 思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。
  • 好处
    • 提高响应速度 (减少创建新线程的时间);
    • 降低资源消耗 (重复利用线程池中的线程,不需要每次都创建)
    • 便于线程管理:
      • corePoolSize:核心池大小
      • maximumPoolSize:最大线程数
      • keepAliveTime:线程没有任务时最多保持多长时间后会终止
      • ……

1. 线程池相关API

  • JDK5.0起提供了线程池相关API:ExecutorServiceExecutors

  • ExecutorService :真正的线程池接口。常见实现类 ThreadPoolExecutor

    • void executor(Runnable command)
      

      执行任务/命令,没有返回值,一般用来执行 Runnable

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

      执行任务,有返回值,一般用来执行 Callable

    • void shutdown()
      

      关闭线程池。

  • Executors :工具类、线程池的工厂类,用于创建并返回不同类型的线程池。

    • Executors.newCachedThreadPool()
      

      创建一个可根据需要,创建新线程的线程池。

    • Executors.newFixedThreadPool(n)
      

      创建一个可重用固定线程数的线程池。

    • Executors.newSingleThreadExecutor()
      

      创建一个只有一个线程的线程池。

    • Exrcutors.newScheduledThreadPool(n)
      

      创建一个线程池,它可以安排在给定延迟后运行命令或者定期地执行。

2. 线程池创建线程步骤

  • 提供指定线程数量的线程池:
ExecutorService service = Executors.newFixedThreadPool(10);
  • 执行指定的线程操作,需要提供实现Runnable或者Callable接口实现类的对象:
service.execute(new NumThread1());
Future future = service.submit(new NumThread2());
  • 关闭线程池
service.shutdown();

3. 线程池使用说明

  • 下面代码中,ExecutorService 是接口,因此 service 必定是 ExecutorService 是接口的某个实现类。
ExecutorService service = Executors.newFixedThreadPool(10);
  • 通过以下代码获取 service 的类:
System.out.println(service.getClass());

输出:

class java.util.concurrent.ThreadPoolExecutor

可以看到,ThreadPoolExecutorExecutorService 是接口的实现类:

image-20220317150235187

image-20220317150301779

  • 因此,就可以通过强转成 ThreadPoolExecutor 类的对象,来//设置线程池的属性:

    //强转
    ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
    

image-20220317150628679

4. 例子

class NumThread1 implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() +
                        ":" + i);
            }
        }
    }
}

//创建一个Callable的实现类
class NumThread2 implements Callable {

    //实现call方法,将此线程需要执行的操作声明在call()中
    @Override
    public Object call() throws Exception {
        int sum = 0;
        for (int i = 0; i < 20; i++) {
            if (i % 2 != 0) {
                System.out.println(Thread.currentThread().getName() +
                        ":" + i);
                sum += i;
            }
        }
        return sum;//包装类Integer自动装箱
    }
}

public class ThreadPool {
    public static void main(String[] args) {
        //1.提供指定线程数量的线程池
        ExecutorService service = Executors.newFixedThreadPool(10);

        //2.执行指定的线程操作,需要提供实现Runnable或者Callable接口实现类的对象
        service.execute(new NumThread1());//执行Runnable接口实现类对象的线程
        Future future = service.submit(new NumThread2());//执行Callable接口实现类对象的线程

        try {
            //获取Callable中call方法的返回值
            Object sum = future.get();
            System.out.println("sum = " + sum);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }

        //3.关闭线程池
        service.shutdown();
    }
}

输出:

pool-1-thread-1:0
pool-1-thread-1:2
pool-1-thread-1:4
pool-1-thread-1:6
pool-1-thread-1:8
pool-1-thread-1:10
pool-1-thread-1:12
pool-1-thread-1:14
pool-1-thread-1:16
pool-1-thread-1:18
pool-1-thread-2:1
pool-1-thread-2:3
pool-1-thread-2:5
pool-1-thread-2:7
pool-1-thread-2:9
pool-1-thread-2:11
pool-1-thread-2:13
pool-1-thread-2:15
pool-1-thread-2:17
pool-1-thread-2:19
sum = 100