Java基础教程 - 11 多线程

更好的阅读体验:点这里www.doubibiji.com

11 多线程

11.1 多线程概述

1 进程与线程

在计算机中,进程是指正在执行中的一个程序,它由程序、数据和进程控制块组成。每个进程都有独立的地址空间,相互之间不能直接访问,是操作系统对程序运行进行管理的单位,每个进程都可以拥有多个线程。

线程是指一条执行路径,它是进程中的一部分,可以与其他线程共享进程的资源和内存。线程是轻量级的进程,它比进程更快速,更容易创建和销毁。

2 并行与并发

并行

并行就是两个任务同时运行,就是A任务执行的同时,B任务也在进行,这是需要多核CPU支持的,A任务和B任务由不同的核来执行。

并发

并发是指两个任务都请求运行,而处理器只能接受一个任务,就把两个任务安排轮流进行,由于时间间隔较短,使人感觉两个任务都在运行。以前计算机是单核的时候,就是并发执行,轮流指定多个任务,但是切换任务的速度很快,以为是同时执行多个任务。


看一下下面的方法:

public class ThreadTest {
    public static void main(String[] args) {
        printEven();
        printOdd();
    }

    /**
     * 打印偶数
     */
    private static void printEven() {
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                System.out.println(i);
            }
        }
    }

    /**
     * 打印奇数
     */
    private static void printOdd() {
        for (int i = 0; i < 100; i++) {
            if (i % 2 != 0) {
                System.out.println(i);
            }
        }
    }
}

上面的代码,没有使用多线程,就是在主线程中执行的,代码会依次按照顺序执行,所以是没有办法同时打印偶数和奇数的,必须偶数打印完才能打印奇数。

11.2 多线程的使用

使用多线程有两种方式:继承 Thread 和 实现 Runnable 接口。

1 继承Thread类

创建一个 ThreadTest.java 类,编写代码如下:

首先编写自定义的类,继承 Thread 类,实现 run() 方法,在 run 方法中编写自己的代码功能。

然后使用这个类创建对象,并调用 start() 方法。

// 1.创建线程类,继承Thread
class PrintEvenThread extends Thread {
    // 2.重写run方法
    @Override
    public void run() {
        // 3.将要执行的代码写在run方法中
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}

/**
 * 测试类
 */
public class ThreadTest {
    public static void main(String[] args) {

        // 4.创建线程对象
        PrintEvenThread evenThread = new PrintEvenThread();
        // 5.启动线程
        evenThread.start();

        // 在主线程中执行打印奇数
        for (int i = 0; i < 100; i++) {
            if (i % 2 != 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}

在上面的代码中,从 main() 方法开始执行,main() 所在的现场为主线程,然后通过 PrintEvenThread 类创建了一个线程对象,并调用 start() 方法开启一条子线程,所以是两条线程在运行。

执行后,在主线程中打印奇数,在子线程中打印偶数,是同时执行的,在代码中,可以通过 Thread.currentThread() 获取执行当前代码所在的线程对象,从而通过对象可以获取到现场的 ID、名称等信息,在上面打印了线程的名称。

注意,启动线程是调用 start() 方法,不是调用 run() 方法,一个线程对象只能 start() 一次,调用多次会报错,如果要再开启一个线程,可以再创建一个 PrintEvenThread 对象,然后start()。

执行的时候需要注意:由于线程调度的不确定性,实际输出的顺序可能会与预期的顺序有所不同。例如,可能会出现一个奇数后跟一个偶数,或者连续出现多个偶数或奇数。此外,如果两个线程几乎同时打印数字,那么它们的输出可能会交织在一起。


2 实现Runnable接口

除了继承 thread 类,还可以使用实现 Runable 接口的方式实现。

// 1.实现Runnable接口
class PrintEvenThread implements Runnable {
    // 2.重写run方法
    @Override
    public void run() {
        // 3.将要执行的代码写在run方法中
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}

/**
 * 测试类
 */
public class ThreadTest {
    public static void main(String[] args) {

        // 4.创建线程对象
        PrintEvenThread evenThread = new PrintEvenThread();
        // 5.启动线程
        new Thread(evenThread).start();

        // 在主线程中执行打印奇数
        for (int i = 0; i < 100; i++) {
            if (i % 2 != 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}

首先实现 Runnable 接口,并实现其中的 run() 方法,然后创建 PrintEvenThread 对象,最后创建 Thread 对象并将 PrintEvenThread 对象传递给 Thread对象来启动线程。


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

如果我们现在有一个需求,有三个窗口卖100张票,那么可以通过开启三个子线程来实现。

通过继承 Thread 类实现:

class Windows 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(getName() + ":没票了");
                break;
            }
        }
    }
}

/**
 * 测试类
 */
public class ThreadTest {
    public static void main(String[] args) {
        Windows w1 = new Windows();
        w1.setName("窗口1");	// 设置线程名称

        Windows w2 = new Windows();
        w2.setName("窗口2");

        Windows w3 = new Windows();
        w3.setName("窗口3");

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

因为是三个线程对象共用 100 张票,所以需要将票设置为静态变量。

这里的代码存在线程安全的问题,所以运行的时候,可能存在多个人卖同一个号码的票,这个后面再处理。


通过实现 Runnable 接口实现:

class WindowsRunnable implements Runnable {
    private int ticket = 100;

    @Override
    public void run() {
        while(true){
            // 判断有没有票
            if (ticket > 0){
                System.out.println(Thread.currentThread().getName() + ":卖票,票号为: " + ticket);
                ticket--;
            }else{
                System.out.println(Thread.currentThread().getName() + ":没票了");
                break;
            }
        }
    }
}

/**
 * 测试类
 */
public class ThreadTest {
    public static void main(String[] args) {
        WindowsRunnable windowsRunnable = new WindowsRunnable();

        Thread w1 = new Thread(windowsRunnable);
        w1.setName("窗口1");	// 设置线程名称

        Thread w2 = new Thread(windowsRunnable);
        w2.setName("窗口2");

        Thread w3 = new Thread(windowsRunnable);
        w3.setName("窗口3");

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

和继承 Thread 线程不同,我们只需要创建一个 WindowsRunnable 对象,所以不需要将 100 张票设置成静态的,就可以共享数据。

两种方式比较:

  • 继承Thread类

优点:可以直接使用Thread类中的方法,代码简单;

缺点:如果已经有父类了,就不能使用这种方式,Java为单继承;

  • 实现Runnable接口

优点:即使自己定义的线程类有了父类也不影响,因为接口是可以多实现的,另外更适合多个线程共享数据;

缺点:不能直接使用Thread中的方法,需要先获取到线程对象后,才能得到Thread的方法;

在实际的使用中,推荐优先选择 Runnable 的方式。

4 使用匿名内部类实现

下面使用匿名类来实现上面创建线程的两种方式:

package com.doubibiji;

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

        // 使用 Thread 方式打印偶数
        new Thread(){
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    if (i % 2 == 0) {
                        System.out.println(i);
                    }
                }
            }
        }.start();

        // 使用 Runnable 方式打印奇数
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    if (i % 2 != 0) {
                        System.out.println(i);
                    }
                }
            }
        }).start();

        System.out.println("主线程执行完成");
    }
}

如果一个线程类只会用来执行一次,那么可以使用匿名类的方式来写,这样更为简洁。

11.3 线程的方法

下面介绍一下线程中的一些常用的方法。

1 线程名称

我们可以获取线程 ID 和线程的名称,还可以设置线程的名称,前面也设置过了。

可以通过在创建线程的时候使用构造方法传递线程名称,也可以调用 set 方法进行设置。

通过继承Thread的方式获取线程名称:

//通过构造方法设置线程名称
Thread t1 = new Thread("兔子") {
    @Override
    public void run() {
        //获取线程名称
        System.out.println("线程名称:" + this.getName());	// 可以通过this直接获取线程信息
    }
};
t1.setName("乌龟");   // 后设置的会覆盖构造方法设置的
t1.start();

通过使用实现Runnable接口的方式获取线程信息:

Thread t2 = new Thread(new Runnable() {
    @Override
    public void run() {
        //获取当前正在执行的线程并获取线程名称
        System.out.println("线程名称:" + Thread.currentThread().getName());
    }
});
//设置线程名称
t2.setName("乌龟");
t2.start();

实现Runnable接口的方式无法直接调用Thread的方法,需要先获取当前线程,在通过当前线程对象获取线程信息。

2 sleep()

sleep() 方法就是让线程休眠指定的时间,时间过后,线程继续执行。

举个栗子:

下面的程序实现一个倒计时20秒的功能。

public static void main(String[] args) {
    //创建一个子线程
    new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                for (int i = 20; i >= 0; i--) {
                    //让线程睡1秒
                    Thread.sleep(1000);
                    System.out.println("倒计时,第" + i + "秒");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }).start();
}

sleep() 方法的参数是毫秒,1000毫秒就是1秒,每次运行,休眠1秒。sleep() 会抛出编译时异常,所以需要 try-catch

当然也可以直接在主线程中实现:

public static void main(String[] args) throws InterruptedException {
    for (int i = 20; i >= 0; i--) {
        //让线程睡1秒
        Thread.sleep(1000);
        System.out.println("主线程倒计时,第" + i + "秒");
    }
}

3 setDaemon()

setDaemon() 方法是设置一个线程为守护线程,守护线程不会单独执行,当其他非守护线程都执行结束后,该线程自动退出。

下面的 demo 中,定义了2个线程,如果不设置线程2为守护线程,则在执行的时候,线程1打印2次,线程2打印1000次。
当设置线程2为守护线程时,当线程1执行完成,线程2很快就停止了,没有继续执行。理论上线程1执行完成线程2会立刻停止,但是有个时间差,即反应时间,所以线程2又执行了一段时间。

public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread() {
        @Override
        public void run() {
            for (int i = 0; i < 2; i++) {
                System.out.println(getName() + ":" + i);
            }
        }
    };

    Thread t2 = new Thread() {
        @Override
        public void run() {
            for (int i = 0; i < 1000; i++) {
                System.out.println(getName() + ":" + i);
            }
        }
    };

    //设置线程t2为守护线程
    t2.setDaemon(true);

    t1.setName("线程1");
    t2.setName("线程2");
    t1.start();
    t2.start();
}

守护线程(Daemon Thread)在Java中主要用于执行后台任务,这些任务在程序运行时默默地执行一些必要的操作,但不需要在程序正常结束时保持运行状态。守护线程的主要特点是在所有非守护线程结束时,它们会自动被终止,而不需要等待它们完成。这种特性使得守护线程非常适合用于执行一些清理工作、资源回收、日志记录等后台任务。

4 join()

join() :在线程a中调用线程b的join()方法,会暂停线程a,等待线程b执行完成后,再继续执行线程a。

join(int) :在线程a中调用线程b的join(int)方法,会暂停线程a指定的时间,时间过后,继续执行线程a。

举个栗子:

public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread() {
        @Override
        public void run() {
            try {
                for (int i = 0; i < 50; i++) {
                    System.out.println(getName() + ":" + i);
                    sleep(10);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    };

    Thread t2 = new Thread() {
        @Override
        public void run() {

            try {
                for (int i = 0; i < 50; i++) {

                    if (i == 2) {
                        // 暂停当前线程,等待t1线程执行完成再执行
                        t1.join();
                        // 暂停当前线程指定的时间,时间过后继续执行
                        //t1.join(100);
                    }

                    System.out.println(getName() + ":" + i);
                    sleep(10);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }


        }
    };

    t1.setName("线程1");
    t2.setName("线程2");
    t1.start();
    t2.start();
}

在上面的程序中,在线程 t2 中,让线程 t1插队先执行,等待线程 t1 执行完成再继续执行线程 t2。

5 yield()

yield() 的作用是让当前线程让出CPU,让其他线程执行。

该方法作用不明显,不太推荐使用。

Thread t1 = new Thread() {
    @Override
    public void run() {
        try {
            for (int i = 0; i < 50; i++) {
                if (i % 10 == 0) {
                    yield();	// 释放当前线程CPU的执行权
                }
                
                System.out.println(getName() + ":" + i);
                sleep(10);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
};

6 setPriority()

设置线程的优先级,最高为10,最低为1,不设置线程的优先级默认为5。

该方法并不能保证什么,作用有一点,也不明显。

public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread() {
        @Override
        public void run() {
            for (int i = 0; i < 1000; i++) {
                System.out.println(getName() + ":" + i);
            }
        }
    };
    t1.setName("线程1");
    t1.setPriority(Thread.MAX_PRIORITY);    // 设置最大优先级10

    Thread t2 = new Thread() {
        @Override
        public void run() {
            for (int i = 0; i < 1000; i++) {
                System.out.println(getName() + ":" + i);
            }
        }
    };
    t2.setName("线程2");
    t1.setPriority(Thread.MIN_PRIORITY);    // 设置最大优先级10

    t1.start();
    t2.start();
}

我设置了线程 t1 优先级最高,但也不能保证优先执行完,看运气。

11.4 线程同步

在一开始我们实现多个窗口买票的时候,发现会存在多个窗口卖同一个票号的票,而且还有可能卖0号和负号的票,这就是线程安全导致的问题。也就是说当多个线程同时操作同一个共享数据的时候,就会存在线程安全的问题。

先来看一下之前的代码:

在代码中添加了 sleep() 方法,让问题更容易暴漏出来。

class Windows extends Thread{
    // 多个窗口共卖一张票,这里需要设置为静态的
    private static int ticket = 100;

    @Override
    public void run() {
        while(true){
            // 判断有没有票
            if (ticket > 0) {

                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

                System.out.println(getName() + ":卖票,票号为: " + ticket);
                ticket--;
            } else {
                System.out.println(getName() + ":没票了");
                break;
            }
        }
    }
}

/**
 * 测试类
 */
public class ThreadTest {
    public static void main(String[] args) {
        Windows w1 = new Windows();
        w1.setName("窗口1");	// 设置线程名称

        Windows w2 = new Windows();
        w2.setName("窗口2");

        Windows w3 = new Windows();
        w3.setName("窗口3");

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

首先为什么会出现这个问题呢?

因为多个线程同时执行,一个线程的代码在打印了票号之后,在执行 ticket--; 之前,其他的线程也在打印票号,此时的票号就重复了。当执行到 ticket 等于1的时候,多个线程同时判断了 ticket > 0 都是成立的,一个线程把最后一张票卖了,此时 ticket 为 0,但是其他的现场已经进入了 if 判断,所以又买票了,导致卖 0号和负号的票。

如何解决这个问题呢?

就是让操作数据的代码,同时只能让一个线程执行,也就是让一个线程执行 if 判断的时候,其他的线程等待,直到这个线程卖完这张票。


实现的方式是可以使用 同步代码块同步方法

1 同步代码块

语法:

synchronized (同步监视器) {
  
}

举个栗子,修改上面的代码:

class Windows extends Thread{
  
    private static int ticket = 100;

    @Override
    public void run() {
        while(true){
          	// 使用同步代码块
            synchronized (Windows.class) {
                // 判断有没有票
                if (ticket > 0){

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }

                    System.out.println(getName() + ":卖票,票号为: " + ticket);
                    ticket--;
                }else{
                    System.out.println(getName() + ":没票了");
                    break;
                }
            }
        }
    }
}

/**
 * 测试类
 */
public class ThreadTest {
    public static void main(String[] args) {
        Windows w1 = new Windows();
        w1.setName("窗口1");	// 设置线程名称

        Windows w2 = new Windows();
        w2.setName("窗口2");

        Windows w3 = new Windows();
        w3.setName("窗口3");

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

在上面的代码中,使用 synchronized 同步代码块,将操作 ticket 数据的代码包裹起来,这样每次只能有一个线程执行这块代码。

关于同步监视器,同步监视器也叫 任何对象都可以作为同步监视器,但是只有多个线程调用同步代码块的时候,使用的是同一个锁对象,才能起到同步的作用。上面使用的是 Windows.class,这个表示的是使用 Windows 类作为同步锁,在 Java 中类也是对象。类只会加载一次,所以全局只有一个对象,用在这里,大家都是使用同一个锁对象,所以只会有一个线程执行同步代码块。


同样,也可以修改实现 Runnable 接口的多线程方式:

class WindowsRunnable implements Runnable {
    private int ticket = 100;

    @Override
    public void run() {
        while(true){
            // 使用同步代码块
            synchronized (this) {

                // 判断有没有票
                if (ticket > 0){
                  	try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                  
                    System.out.println(Thread.currentThread().getName() + ":卖票,票号为: " + ticket);
                    ticket--;
                }else{
                    System.out.println(Thread.currentThread().getName() + ":没票了");
                    break;
                }
            }
        }
    }
}

/**
 * 测试类
 */
public class ThreadTest {
    public static void main(String[] args) {
        WindowsRunnable windowsRunnable = new WindowsRunnable();

        Thread w1 = new Thread(windowsRunnable);
        w1.setName("窗口1");	// 设置线程名称

        Thread w2 = new Thread(windowsRunnable);
        w2.setName("窗口2");

        Thread w3 = new Thread(windowsRunnable);
        w3.setName("窗口3");

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

因为 Runnable 接口的实现类对象是有一个,所以 synchronized 同步代码块的监视器对象在上面的代码中使用了 this,表示当前对象,多个线程执行的时候,当前对象都是同一个对象,所以锁对象也是没问题的,但是使用 WindowsRunnable.class 就更没问题了,建议使用 WindowsRunnable.class

2 同步方法

如果操作共享数据的代码在整个方法中,我们也可以使用 synchronized 关键字修饰一个方法,该方法中所有的代码都是同步的。

但是需要注意同步方法也是有同步监视器对象的:

  • 非静态同步方法的锁对象是 this
  • 静态的同步方法的锁对象是 类对象,即 类.class 对象。

所以看一下下面的代码:

package com.doubibiji;


class Windows extends Thread {
    private static int ticket = 100;

    @Override
    public void run() {
        while(true){
            boolean success = sellTicket();
            if (!success) {
                break;
            }
        }
    }

    private synchronized boolean sellTicket() {
        // 判断有没有票
        if (ticket > 0){
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

            System.out.println(Thread.currentThread().getName() + ":卖票,票号为: " + ticket);
            ticket--;
            return true;
        } else{
            System.out.println(Thread.currentThread().getName() + ":没票了");
            return false;
        }
    }
}

/**
 * 测试类
 */
public class ThreadTest {
    public static void main(String[] args) {
        Windows w1 = new Windows();
        w1.setName("窗口1");	// 设置线程名称

        Windows w2 = new Windows();
        w2.setName("窗口2");

        Windows w3 = new Windows();
        w3.setName("窗口3");

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

上面的代码封装了 sellTicket() 方法,并使用 synchronized 关键字修饰方法,运行发现,并没有实现同步效果。这是因为 sellTicket() 方法的同步监视器是 this,但是三个线程是不同的对象,所以 this 指向的是三个不同的对象。

这种情况下,可以将 sellTicket() 方法变成静态方法就可以了。

private static synchronized boolean sellTicket() {
   // ...
}

而实现 Runnable 接口就没有这个问题了,因为多个线程用的是同一个对象,所以是同一个同步监视器:

package com.doubibiji;


class WindowsRunnable implements Runnable {
    private int ticket = 100;

    @Override
    public void run() {
        while(true){
            boolean success = sellTicket();
            if (!success) {
                break;
            }
        }
    }

    private synchronized boolean sellTicket() {
        // 判断有没有票
        if (ticket > 0){
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

            System.out.println(Thread.currentThread().getName() + ":卖票,票号为: " + ticket);
            ticket--;
            return true;
        } else{
            System.out.println(Thread.currentThread().getName() + ":没票了");
            return false;
        }
    }
}

/**
 * 测试类
 */
public class ThreadTest {
    public static void main(String[] args) {
        WindowsRunnable windowsRunnable = new WindowsRunnable();

        Thread w1 = new Thread(windowsRunnable);
        w1.setName("窗口1");	// 设置线程名称

        Thread w2 = new Thread(windowsRunnable);
        w2.setName("窗口2");

        Thread w3 = new Thread(windowsRunnable);
        w3.setName("窗口3");

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

所以在使用同步代码块和同步方法的时候,一定要注意同步监视器锁对象是否满足要求,否则会达不到同步的效果。

前面演示的时候只有一个 synchronized 同步代码块或方法,但是实际上所有使用同一个同步监视器的 synchronized 代码块和方法只能被一个线程执行。

另外一定要注意同步代码块和同步方法包裹的代码,不要包裹少了,达不到同步效果,也不要包裹多了,例如上面的代码就不能将 while 循环包裹进来,否则一个线程获得锁,就将所有的票都卖了。

另外需要注意,现成同步比不同步性能是要慢的,因为同时只有一个线程执行同步代码。

3 死锁

什么是死锁?

死锁就是不同的线程分别占用对方线程所需要的同步资源,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。

举个栗子:

两个线程吃饭,但只有一双筷子。

第一个线程先拿左筷子,拿到左筷子就开始拿右筷子,两支筷子都拿到就可以开吃。

第二个线程先拿右筷子,拿到右筷子就开始拿左筷子,同样两支筷子都拿到就可以开吃。

这样就会存在一个问题,当线程1拿到做筷子,去拿右筷子的时候,右筷子刚好被线程2拿到了,线程2开始拿左筷子,而左筷子刚好被线程1拿到了,这样线程1和线程2都拿不到第二根筷子,就出现了死锁。

public class HelloThread {
    private static String s1 = "左筷子";
    private static String s2 = "右筷子";

    public static void main(String[] args) {

        //开启线程1
        new Thread() {
            @Override
            public void run() {
                while (true) {
                    //先拿左筷子
                    synchronized (s1) {
                        System.out.println(getName() + "--拿到" + s1 + ", 准备拿" + s2);

                        synchronized (s2) {
                            System.out.println(getName() + "--拿到" + s2 + ", 开吃");
                        }
                    }
                }
            }
        }.start();

        //开启线程2
        new Thread() {
            @Override
            public void run() {
                while (true) {
                    //先拿右筷子
                    synchronized (s2) {
                        System.out.println(getName() + "--拿到" + s2 + ", 准备拿" + s1);

                        synchronized (s1) {
                            System.out.println(getName() + "--拿到" + s1 + ", 开吃");
                        }
                    }
                }
            }
        }.start();
    }
}

多线程同步的时候,如果使用同步代码嵌套,使用相同的锁对象,就有可能会出现死锁。所以尽量不要嵌套使用同步。

4 单例模式

什么是单例模式?

单例模式是一种常见的设计模式,它的主要目的是确保一个类仅能创建一个实例,并提供一个方法获取这个实例。

一般情况下,在实现数据库连接池、日志对象等功能的时候,没必要创建多个对象,会使用单例模式。

在 Java 中实现单例模式有两种方式:饿汉模式和懒汉模式。

饿汉模式

饿汉模式就是在类加载的时候就创建类的实例,以下是饿汉模式的代码实现:

public class Singleton {  
    // 饿汉式,在类加载的时候就完成初始化  
    private static Singleton instance = new Singleton();  
  
    private Singleton() {  
        // 私有构造方法,防止被外部类实例化  
    }  
  
    // 提供全局访问点  
    public static Singleton getInstance() {  
        return instance;  
    }  
}

单例模式,首先将构造方法私有化,这样在类的外部就无法创建类的实例了,然后创建一个当前类类型的静态变量,并初始化类的实例,然后提供一个 getInstance() 方法获取实例。

这样这个类只能创建一个类的实例,在类的外部,想要获取这个类的实例,通过 getInstance() 方法获取。

懒汉模式

懒汉模式的特点是延迟加载,在使用这个类实例的时候,再创建类的实例。

public class Singleton {  
    // 懒汉式,在第一次调用的时候初始化  
    private static Singleton instance;  
  
    private Singleton() {  
        // 私有构造方法,防止被外部类实例化  
    }  
  
    // 提供全局访问点  
    public static Singleton getInstance() {  
        if (instance == null) {  
            instance = new Singleton();  
        }  
        return instance;  
    }  
}

但是上面的代码存在线程安全的问题,当有多个线程同时调用 getInstance() 方法的时候,可能在进行 if 判断的时候,instance == null 都是成立的,然后创建了多个实例。

为了解决懒汉模式的线程安全问题,可以使用 synchronized 关键字对 getInstance() 方法进行同步,或者使用双重检查锁定(Double-Checked Locking)机制。

public class Singleton {  
    // 懒汉式,在第一次调用的时候初始化  
    private static Singleton instance;  
  
    private Singleton() {  
        // 私有构造方法,防止被外部类实例化  
    }  
  
    // 提供全局访问点  
    public static Singleton getInstance() {  
        if (instance == null) { // 第一次检查  
            synchronized (Singleton.class) {  
                if (instance == null) { // 第二次检查  
                    instance = new Singleton();  
                }  
            }  
        }  
        return instance;  
    }  
}

在有多个线程第一次同时调用 getInstance() 方法的时候,先进行 if 判断,如果有多个线程进入 if 判断,栽进行加锁,这样第二层 if 判断只有一个 线程能进入,保证了只能创建一个实例,后面再调用 getInstance() 方法,就不会进入 if 了,直接返回 instance 实例。

11.5 线程通信

什么是线程通信?

多个线程并发执行时,在默认情况下CPU是随机切换线程的。如果我们希望他们有规律的执行,就可以使用线程通信,例如每个线程执行一次打印。

线程该怎么通信呢?

两个线程间的通信,如果希望当前线程等待,就调用 wait() ;如果希望唤醒其他等待的线程,就调用 notify()

这两个方法必须在同步代码中执行,并且使用同步锁对象来调用。

举个栗子:

如果想实现两个线程打印100以内的整数,通过锁来解决线程同步的问题,避免数字重复错乱,那么可以这样写:

class Printer implements Runnable {
    private static int number = 0;

    @Override
    public void run() {
        while (true) {
            synchronized (Printer.class) {
                if (number >= 100) {
                    break;
                }

                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

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

/**
 * 测试类
 */
public class ThreadTest {
    public static void main(String[] args) {
        Printer printer = new Printer();

        Thread t1 = new Thread(printer);
        t1.setName("线程1");

        Thread t2 = new Thread(printer);
        t2.setName("线程2");

        t1.start();
        t2.start();
    }
}

在上面的代码中,避免出现线程同步问题,所以使用了同步代码块。但是上面的代码在打印的时候,谁抢到CPU执行权谁就打印,如果想要一个线程打印偶数,一个线程打印奇数,依次打印,那么可以这样写:

class Printer implements Runnable {
    private static int number = 0;

    @Override
    public void run() {
        while (true) {
            synchronized (Printer.class) {
                // 唤醒另外一个线程
                Printer.class.notify();

                if (number >= 100) {
                    break;
                }

                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

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


                try {
                    // 当前线程打印完了就休眠
                    Printer.class.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }
}

/**
 * 测试类
 */
public class ThreadTest {
    public static void main(String[] args) {
        Printer printer = new Printer();

        Thread t1 = new Thread(printer);
        t1.setName("线程1");

        Thread t2 = new Thread(printer);
        t2.setName("线程2");

        t1.start();
        t2.start();
    }
}

解释一下关键代码,两个线程都有如下的代码:

synchronized (Printer.class) {
    // 唤醒另外一个线程
    Printer.class.notify();

		// ....打印数字

    try {
        // 当前线程打印完了就休眠
        Printer.class.wait();
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
}

首先是同步代码块,并使用了相同的同步监视器,所以两个线程只有一个线程能执行同步代码块的代码,所以只有一个线程进入到同步代码中执行。

假设线程 t1 进入到同步代码中执行,然后使用同步监视器执行对象执行 Printer.class.notify() ,这个会唤醒线程 t2,但是第一次执行的时候这句代码没有效果,因为线程 t2 没有 wait ,然后线程 t1 打印了一个数字,然后 t1调用了 Printer.class.wait() ,线程 t1 就会阻塞等待,并会释放同步监视器锁,所以线程 t2 会进入到同步代码块,t2 调用 Printer.class.notify() ,那么会唤醒线程 t1,但是此时 t1 虽然被唤醒,但是没有获得同步监视器锁,需要等待 t2 调用 Printer.class.wait() 来释放同步监视器,才能进入到同步代码块,整个过程就是这样。

上面调用了 notify() 方法,调用该方法会唤醒被 wait 的一个线程。如果有多个线程被 wait,就唤醒优先级高的那个。另外还有一个notifyAll() 方法,调用此方法,就会唤醒所有被 wait 的线程。

11.6 ReentrantLock

在 JDK 1.5版本以后新增了 ReentrantLock 类。

通过使用ReentrantLock类的 lock()unlock() 方法可以替代 synchronized

使用 ReentrantLock 类的 newCondition() 方法可以获取 Condition 对象。需要等待的时候可以使用 Condition 对象的 await() 方法,唤醒的时候调用 signal() 方法。不同的线程使用不同的 Condition,这样可以区分唤醒的指定的线程。


还是使用两个线程来打印 0~100 的整数,通过锁来解决线程同步的问题,避免数字重复错乱:

class Printer implements Runnable {
    private static int number = 0;

    //1.实例化ReentrantLock
    private ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {

            // 2.调用锁定方法:lock()
            lock.lock();

            if (number > 100) {
                // 3.提前结束一定要unlock()
                lock.unlock();
                break;
            }

            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

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

            //3.调用解锁方法:unlock()
            lock.unlock();
        }

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

/**
 * 测试类
 */
public class ThreadTest {
    public static void main(String[] args) {
        Printer printer = new Printer();

        Thread t1 = new Thread(printer);
        t1.setName("线程1");

        Thread t2 = new Thread(printer);
        t2.setName("线程2");

        t1.start();
        t2.start();
    }
}

在上面的代码中使用了 ReentrantLock 替代了同步代码块,实现了相同的效果。

但是和 synchronized 不同的是 synchronized 在执行完相应的同步代码以后,会自动的释放同步监视器,而 ReentrantLock 需要手动的调用 lock() 启动同步,结束同步也需要手动调用 unlock()。


使用 ReentrantLock 也可以进行线程间的通信,还是实现上面的两个线程依次打印:

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

class Printer implements Runnable {
    private static int number = 0;

    // 1.实例化ReentrantLock
    private ReentrantLock lock = new ReentrantLock();
    // 2.创建监视器
    private Condition condition = lock.newCondition();

    @Override
    public void run() {
        while (true) {

            // 调用锁定方法:lock()
            lock.lock();

            // 唤醒另一个线程
            condition.signal();

            if (number > 100) {
                // 提前结束一定要unlock()
                lock.unlock();
                break;
            }

            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

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

            try {
                // 线程阻塞等待
                condition.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                // 调用解锁方法:unlock()
                lock.unlock();
            }
        }

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

/**
 * 测试类
 */
public class ThreadTest {
    public static void main(String[] args) {
        Printer printer = new Printer();

        Thread t1 = new Thread(printer);
        t1.setName("线程1");

        Thread t2 = new Thread(printer);
        t2.setName("线程2");

        t1.start();
        t2.start();
    }
}

11.7 线程的状态

也就是线程的生命周期,线程有5中状态:

新建:创建线程对象;

就绪:线程对象已经 start() 启动了,但是还没有获取到CPU的执行权;

运行:获取到了CPU的执行权;

阻塞:没有CPU的执行权,回到就绪;

死亡:代码运行完毕,线程消亡。


下图是各个状态的转换:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

11.8 使用Callable创建线程

Callable 是 JDK1.5 新增的实现多线程的方式之一。在 JDK1.5 之前,我们通常通过继承 Thread 类或者实现 Runnable 接口来创建线程。然而,这两种方式都有一个共同的缺陷:在执行完任务之后无法直接获取执行结果。为了解决这个问题,JDK1.5 引入了 Callable 和 Future 接口。

介绍一下 Callable 的使用,举个栗子,在子线程中计算 1~100 的和,在主线程中获取计算结果:

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

// 1.创建一个实现Callable的实现类<Integer>是泛型,表示返回值的类型
class MyCallable implements Callable<Integer> {

    // 2.实现call方法,在其中实现功能
    @Override
    public Integer call() throws InterruptedException {
        int sum = 0;
        for (int i = 0; i <= 100; i++) {
            sum += i;

            Thread.sleep(10);
        }
        return sum;
    }
}

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

        // 3.创建Callable接口实现类的对象
        MyCallable callable = new MyCallable();

        // 4.创建FutureTask的对象,将Callable接口实现类的对象作传递到给FutureTask,<Integer>表示返回值的类型
        FutureTask<Integer> futureTask = new FutureTask(callable);

        // 5.创建Thread对象,将FutureTask的对象作为参数传递到Thread,并start()
        new Thread(futureTask).start();

        try {
            // 6.获取Callable中call方法的返回值,如果不想获取返回值,可以忽略该步骤
            // get()返回值即为call()的返回值
            Integer sum = futureTask.get();
            System.out.println("总和为:" + sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

    }
}

如果需要返回值,可以通过 futureTask 获取返回值,如果对返回值不感兴趣,可以不获取返回值。使用futureTask.get()方法来获取call()方法的返回值。这个方法会阻塞,直到call()方法执行完成并返回结果。如果call()方法执行过程中抛出异常,get()方法会抛出ExecutionException;如果在等待结果的过程中当前线程被中断,get()方法会抛出InterruptedException

Callable 的方式比 Runnable 方式功能要强大,主要体现在:

  1. call() 方法可以有返回值;
  2. call() 方法可以抛出异常,被外部的操作捕获;
  3. 返回值支持泛型(泛型后面再细讲);

11.9 线程池

线程池概述

系统启动一个新线程的成本是比较高的,因为它涉及到要与操作系统进行交互。而使用线程池可以很好的提高性能,尤其是当程序中要创建大量生命周期很短的线程时,更应该考虑使用线程池。例如使用 APP 刷新闻,列表中会列出很多条新闻,列表从下往上滑动,会不停的显示新的新闻,同时加载包含的图片,每加载一张图片都会开启一个新的线程,如果不使用线程池,就会导致线程频繁的创建。

而使用线程池,线程池里的每一个线程代码结束后,并不会死亡,而是再次回到线程池中成为空闲状态,等待下一个对象来使用。在 JDK5 之前,我们必须手动实现自己的线程池,从 JDK5 开始,Java 内置支持线程池。


内置线程池的使用

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

// 创建指定数量线程的线程池
public static ExecutorService newFixedThreadPool(int nThreds)

// 创建只有一个线程的线程池
public static ExecutorService newSingleThreadExecutor()

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

Future<?> submit(Runnable task)
<T> Future<T> submit(Callable<T> task)

使用步骤:

  1. 创建线程池对象
  2. 创建Runnable实例
  3. 提交Runnable实例
  4. 关闭线程池

举个栗子:

package com.doubibiji;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class HelloThread {
    public static void main(String[] args) {
        // 1.创建线程池,最多支持10个线程同时运行
        ExecutorService pool = Executors.newFixedThreadPool(10);

        // 2.将线程放到线程池中并执行
        pool.execute(new MyRunnable());
        pool.execute(new MyRunnable());

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

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}

将线程放到线程池中并执行,除了使用 execute() 方法,还可以使用 submit() 方法。execute() 方法没有返回值,一般用来执行 Runnablesubmit() 方法有 Future 返回值,一般又来执行Callable

pool.shutdown(); 为关闭线程池,如果不执行 pool.shutdown(); 则 main 方法在执行完成后,是不会关闭的。一直会运行,等待新的线程调用。

11.10 Timer计时器

Timer 可以安排 TimerTask 执行任务,可实现定时执行一次或多次的任务。

使用步骤:

  1. 首先创建TimerTask的子类,也可以使用匿名内部类的方式创建。
  2. 使用 Timer 对象执行 TimerTask;
import java.util.Timer;
import java.util.TimerTask;
import java.util.Date;
public class TimerDemo {
    public static void main(String[] args) {
        // 创建TimerTask
        TimerTask task1 = new TimerTask() {
            @Override
            public void run() {
                System.out.println("task1");
            }
        };
        TimerTask task2 = new TimerTask() {
            @Override
            public void run() {
                System.out.println(new Date());
            }
        };

      	// 创建 timer
        Timer timer = new Timer();
        // 延迟1秒执行
        timer.schedule(task1, 1000);
        // 延迟1秒执行,每一秒执行一次
        timer.schedule(task2, 1000, 1000);
    }
}

如果要取消某个 TimerTask 任务,可以使用 TimerTask 对象调用 cancel() 方法,如果要取消所有的任务,可以使用 Timer 对象调用 cancel() 方法。

  • 16
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

山石岐渡

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值