Java多线程
Java多线程

Java多线程

Java 给多线程编程提供了内置的支持。 一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

Java程序默认是多线程的, 程序启动后默认会存在两条线程:

  • 主线程
  • 垃圾回收线程

多线程是多任务的一种特别的形式,但多线程使用了更小的资源开销。

这里定义和线程相关的另一个术语 – 进程:一个进程包括由操作系统分配的内存空间,包含一个或多个线程。一个线程不能独立的存在,它必须是进程的一部分。一个进程一直运行,直到所有的非守护线程都结束运行后才能结束。

创建线程

Java提供了三种创建线程的方法:

  • 通过继承 Thread 类本身;
  • 通过实现 Runnable 接口;
  • 通过 Callable 和 Future 创建线程。

继承Thread类

继承Thread类创建线程的步骤如下:

  1. 编写一个类继承Thread
  2. 重写run方法
  3. 将线程任务代码写在run方法中
  4. 创建线程对象
  5. 调用start方法开启线程

但需要注意的是,调用start方法开启线程, 会自动的调用run方法执行。

另外,只有调用了start方法, 才是开启了新的线程,所有线程并发执行;如果只是调用run方法,仅仅是调用方法,不会并发执行,只会顺序执行。

示例:

class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i <=200; i++){
            System.out.println("线程任务执行了" + i);
        }
    }
}

public class ThreadDemo1 {
    public static void main(String[] args) {
        MyThread myThread1 = new MyThread();
        MyThread myThread2 = new MyThread();
        myThread1.start();
        myThread2.start();
    }
}

显然,两个线程是并发执行的。

实现 Runnable 接口

实现Runnable接口创建线程的步骤如下:

  1. 编写一个类实现Runnable接口
  2. 重写run方法
  3. 将线程任务代码写在run方法中
  4. 创建线程任务资源
  5. 创建线程对象, 将资源传入
  6. 使用线程对象调用start方法, 开启线程
public class ThreadDemo3 {

    public static void main(String[] args) {
        // 4. 创建线程任务资源
        MyRunnable mr = new MyRunnable();
        // 5. 创建线程对象, 将资源传入
        Thread t = new Thread(mr);
        // 6. 使用线程对象调用start方法, 开启线程
        t.start();

        for (int i = 1; i <= 2000; i++) {
            System.out.println("main线程执行了");
        }
    }
}

// 1. 编写一个类实现Runnable接口
class MyRunnable implements Runnable {
    // 2. 重写run方法
    @Override
    public void run() {
        // 3. 将线程任务代码写在run方法中
        for (int i = 1; i <= 200; i++) {
            System.out.println("线程任务执行了" + i);
        }
    }
}

这里发现,不仅创建的线程 t 执行了,下方的for循环语句也执行了,这是因为Java程序默认是多线程的。

另外,线程任务资源 Runnable 可以传入线程对象 Thread,是因为 Thread底层源码中存在:

通过 Callable 和 Future 创建线程

通过 Callable 和 Future 创建线程的步骤如下:

  1. 编写一个类实现Callable接口
  2. 重写call方法
  3. 将线程任务代码写在call方法中
  4. 创建线程任务资源对象
  5. 创建线程任务对象, 封装线程资源
  6. 创建线程对象, 传入线程任务
  7. 使用线程对象调用start开启线程
public class ThreadDemo4 {
    
    public static void main(String[] args) throws Exception {
        // 创建线程任务资源对象
        MyCallable mc = new MyCallable();
        // 创建线程任务对象, 封装线程资源
        FutureTask<Integer> task1 = new FutureTask<>(mc);
        FutureTask<Integer> task2 = new FutureTask<>(mc);
        // 创建线程对象, 传入线程任务
        Thread t1 = new Thread(task1);
        Thread t2 = new Thread(task2);
        // 使用线程对象调用start开启线程
        t1.start();
        t2.start();

        Integer result1 = task1.get();
        Integer result2 = task2.get();
        System.out.println("task1获取到的结果为:" + result1);
        System.out.println("task2获取到的结果为:" + result2);

    }
}

// 1. 编写一个类实现Callable接口
class MyCallable implements Callable<Integer> {
    // 2. 重写call方法
    @Override
    public Integer call() throws Exception {
        // 3. 将线程任务代码写在call方法中
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
            sum += i;
        }
        return sum;
    }
}

这里来看一下线程任务对象 FutureTask 为什么可以传入线程对象 Thread 中,FutureTask 的底层源码为:

可以发现,FutureTask 实际上继承了 Runnable ,自然可以传入线程对象 Thread 中。

线程相关方法

此处仅讲解几个重点方法。

设置线程优先级

线程的调度方式分为抢占式调度(随机)和非抢占式调度(轮流使用)

设置线程优先级是抢占式调度,线程优先级分为1-10,默认是5,优先级数字越大,优先级越大

设置守护线程

在Java中,守护线程(Daemon Thread)是一种特殊类型的线程,它是为其他线程提供服务的后台线程。当所有非守护线程结束时,守护线程也会随之结束。

创建守护线程的方式与创建普通线程相同,只需要将线程的setDaemon(true)方法设置为true即可。守护线程通常用于执行后台任务,例如垃圾回收、自动保存或其他不需要与用户交互的任务。

public class ThreadMethodDemo3 {
    /*
        public final void setDaemon(boolean on) : 设置为守护线程
     */
    public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i = 1; i <= 20; i++){
                        System.out.println(Thread.currentThread().getName() + "---" + i);
                }
            }
        }, "线程A: ");

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i = 1; i <= 200; i++){
                    System.out.println(Thread.currentThread().getName() + "---" + i);
                }
            }
        }, "线程B: ");

        t2.setDaemon(true);

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

在上面的代码中,创建了两个线程 t1 和 t2 ,将 t2 设置为守护线程后,当 t1 线程结束后,t2 自动结束(不会瞬间结束)。

线程安全与同步

安全问题出现的条件:

  • 多线程环境
  • 有共享数据
  • 有多条语句操作共享数据

同步技术:将多条语句操作共享数据的代码给锁起来,让任意时刻只能有一个线程可以执行

  • 同步代码块
  • 同步方法
  • Lock 锁

同步代码块

synchronized(锁对象) {
    多条语句操作共享数据的代码
}

注意事项:

  • 锁对象可以是任意对象,但是需要保证多条线程的锁对象,是同一把锁
  • 同步可以解决多线程的数据安全问题,但是也会降低程序的运行效率

给出一个示例代码:

public class ThreadTest2 {
    /*
        同步代码块:

            synchronized(锁对象) {
                多条语句操作共享数据的代码
            }
     */
    public static void main(String[] args) {
        // 这里实际上是3个锁对象
        TicketTask2 t1 = new TicketTask2();
        TicketTask2 t2 = new TicketTask2();
        TicketTask2 t3 = new TicketTask2();

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

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

// 继承Thread类
class TicketTask2 extends Thread {
    // static关键字修饰的变量在静态方法中,属于类变量,所有对象共享,保证线程锁一致
    private static int tickets = 2000;
    private static Object o = new Object();

    @Override
    public void run() {
        while (true) {
            synchronized (o) {
                if (tickets == 0) {
                    break;
                }
                // 作为Thread子类没有重写getName方法,所以可以省略super关键字
                System.out.println(getName() + "卖出了第" + tickets + "号票");
                tickets--;
            }
        }
    }
}

线程继承类的代码也可写为:

class TicketTask2 extends Thread {

    private static int tickets = 2000;

    @Override
    public void run() {
        while (true) {
            // 类的字节码对象唯一,非常适合作为锁对象
            synchronized (TicketTask2.class) {
                if (tickets == 0) {
                    break;
                }

                System.out.println(getName() + "卖出了第" + tickets + "号票");
                tickets--;
            }
        }
    }
}

同步方法

  • 在方法的返回值类型前面加入 synchronized 关键字
public synchronized void method() {
    多条语句操作共享数据的代码
}

注意事项:

  • 方法分为静态和非静态
  • 静态方法的锁对象是字节码对象,非静态方法的锁对象是 this

非静态同步方法

public class ThreadTest4 {

    public static void main(String[] args) {
        TicketTask4 task = new TicketTask4();

        Thread t1 = new Thread(task, "窗口1");
        Thread t2 = new Thread(task, "窗口2");
        Thread t3 = new Thread(task, "窗口3");

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

class TicketTask4 implements Runnable {

    private int tickets = 100;

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

            String name = Thread.currentThread().getName();

            if ("窗口1".equals(name)){
                if (method()) break;
            } else if ("窗口2".equals(name)) {
                // 非静态方法的锁对象是this
                synchronized (this) {
                    if (tickets == 0) {
                        break;
                    }
                    System.out.println(name + "卖出了第" + tickets + "号票");
                    tickets--;
                }
            }
        }
    }

    // 非静态的同步方法,内部锁对象是this
    private synchronized boolean method() {
        if (tickets == 0) {
            return true;
        }

        System.out.println(Thread.currentThread().getName() + "卖出了第" + tickets + "号票");
        tickets--;
        return false;
    }

}

静态同步方法

public class ThreadTest5 {

    public static void main(String[] args) {
        TicketTask5 task = new TicketTask5();

        Thread t1 = new Thread(task, "窗口1");
        Thread t2 = new Thread(task, "窗口2");

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

class TicketTask5 implements Runnable {

    private static int tickets = 100;

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

            String name = Thread.currentThread().getName();

            if ("窗口1".equals(name)) {
                if (method()) {
                    break;
                }
            } else if ("窗口2".equals(name)) {
                synchronized (TicketTask5.class) {
                    if (tickets == 0) {
                        break;
                    }
                    System.out.println(Thread.currentThread().getName() + "卖出了第" + tickets + "号票");
                    tickets--;
                }
            }


        }
    }

    // 静态同步方法 内部的锁是类的字节码对象
    private static synchronized boolean method() {
        if (tickets == 0) {
            return true;
        }

        System.out.println(Thread.currentThread().getName() + "卖出了第" + tickets + "号票");
        tickets--;
        return false;
    }
}

Lock锁

使用 Lock 锁,我们可以更清晰的看到哪里加了锁,哪里释放了锁

lock.lock();
...
lock.unlock();

Lock 是接口,无法直接创建创造对象

示例代码:

public class ThreadTest6 {
    /*
        互斥锁 ReentrantLock()
     */
    public static void main(String[] args) {
        TicketTask6 task = new TicketTask6();

        Thread t1 = new Thread(task, "窗口1");
        Thread t2 = new Thread(task, "窗口2");

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

class TicketTask6 implements Runnable {

    private int tickets = 100;
    private ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            // 使用try/catch保证出现异常也能使锁结束
            try {
                lock.lock();
                if (tickets == 0) {
                    break;
                }

                System.out.println(Thread.currentThread().getName() + "卖出了第" + tickets + "号票");
                tickets--;
            } finally {
                lock.unlock();
            }
        }
    }
}

线程通信

线程通信能确保线程能够按照预定的顺序执行,并且能够安全地访问共享资源。

比如两个线程抢占资源,可能出现一个线程一直抢占资源,而另一个线程分配不到资源的情况。

等待唤醒机制

需要注意的是,这些方法需要使用锁对象调用。另外,wait() 方法在等待的使用会释放锁对象。

给出一个双线程通信的示例代码

public class CorrespondenceDemo1 {
    /*
        两条线程通信
     */
    public static void main(String[] args) {
        Printer1 p = new Printer1();

        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    synchronized (Printer1.class) {
                        try {
                            p.print1();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    synchronized (Printer1.class) {
                        try {
                            p.print2();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }).start();

    }
}

class Printer1 {

    int flag = 1;

    public void print1() throws InterruptedException {

        if(flag != 1){
            Printer1.class.wait();
        }

        System.out.print("传");
        System.out.print("智");
        System.out.print("教");
        System.out.print("育");
        System.out.println();

        flag = 2;
        Printer1.class.notify();

    }

    public void print2() throws InterruptedException {

        if(flag != 2){
            Printer1.class.wait();
        }

        System.out.print("黑");
        System.out.print("马");
        System.out.print("程");
        System.out.print("序");
        System.out.print("员");
        System.out.println();

        flag = 1;
        Printer1.class.notify();

    }
}

双线程通信的代码中,锁对象使用了Printer1的字节码对象(字节码对象唯一,非常适合作为锁对象),同时使用 flag 信号量判断线程的等待与唤醒状态。

notifyAll 方法会唤醒所有等待的线程,对于多线程通信非常有效。

给出一个三线程通信的示例代码:

public class CorrespondenceDemo2 {
    /*
        三条线程通信

        问题: sleep方法和wait方法的区别?
        回答:
                sleep方法是线程休眠, 时间到了自动醒来, sleep方法在休眠的时候, 不会释放锁.
                wait方法是线程等待, 需要由其它线程进行notify唤醒, wait方法在等待期间, 会释放锁.
     */
    public static void main(String[] args) {

        Printer2 p = new Printer2();

        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    synchronized (Printer2.class) {
                        try {
                            p.print1();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    synchronized (Printer2.class) {
                        try {
                            p.print2();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    synchronized (Printer2.class) {
                        try {
                            p.print3();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }).start();
    }

}

class Printer2 {

    int flag = 1;

    public void print1() throws InterruptedException {

        while (flag != 1) {
            // 线程1等待
            Printer2.class.wait();
        }

        System.out.print("传");
        System.out.print("智");
        System.out.print("教");
        System.out.print("育");
        System.out.println();

        flag = 2;
        Printer2.class.notifyAll();

    }

    public void print2() throws InterruptedException {

        while (flag != 2) {
            // 线程2等待
            Printer2.class.wait();
        }

        System.out.print("黑");
        System.out.print("马");
        System.out.print("程");
        System.out.print("序");
        System.out.print("员");
        System.out.println();

        flag = 3;
        Printer2.class.notifyAll();

    }

    public void print3() throws InterruptedException {

        while (flag != 3) {
            Printer2.class.wait();
        }

        System.out.print("传");
        System.out.print("智");
        System.out.print("大");
        System.out.print("学");
        System.out.println();

        flag = 1;
        Printer2.class.notifyAll();

    }
}

三线程通信的代码对比双线程通信,在信号量判断线程状态时将 if 改为了 while ,确保不会出现线程死锁。

也可以使用使用 ReentrantLock 实现同步,并获取 Condition 对象

在三线程通信的代码上使用 Lock 锁进行优化:

public class CorrespondenceDemo3 {
    /*
        三条线程通信 - 优化
     */
    public static void main(String[] args) {

        Printer3 p = new Printer3();

        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    try {
                        p.print1();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    try {
                        p.print2();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    try {
                        p.print3();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
    }

}

class Printer3 {

    ReentrantLock lock = new ReentrantLock();

    Condition c1 = lock.newCondition();
    Condition c2 = lock.newCondition();
    Condition c3 = lock.newCondition();

    int flag = 1;

    public void print1() throws InterruptedException {
        lock.lock();

        if (flag != 1) {
            // 线程1等待, c1绑定线程1
            c1.await();
        }

        System.out.print("传");
        System.out.print("智");
        System.out.print("教");
        System.out.print("育");
        System.out.println();

        flag = 2;
        // 若未绑定,则随机唤醒线程
        c2.signal();

        lock.unlock();
    }

    public void print2() throws InterruptedException {
        lock.lock();

        if (flag != 2) {
            // 线程2等待, c2绑定线程2
            c2.await();
        }

        System.out.print("黑");
        System.out.print("马");
        System.out.print("程");
        System.out.print("序");
        System.out.print("员");
        System.out.println();

        flag = 3;
        c3.signal();

        lock.unlock();
    }

    public void print3() throws InterruptedException {
        lock.lock();

        if (flag != 3) {
            c3.await();
        }

        System.out.print("传");
        System.out.print("智");
        System.out.print("大");
        System.out.print("学");
        System.out.println();

        flag = 1;
        c1.signal();

        lock.unlock();
    }
}

线程生命周期

线程启动后的各种状态构成了线程的生命周期,线程被创建并启动以后,它并不是一启动就进入了执行状态,也不是一直处于执行状态。线程对象在不同的时期有不同的状态。

Java 中的线程状态被定义在了 java.lang.Thread.State 枚举类中。

线程池

系统创建一个线程的成本是比较高的,因为它涉及到与操作系统交互,当程序中需要创建大量生存期很短暂的线程时,频繁的创建和销毁线程,就会严重浪费系统资源,线程池则应运而生。

将线程对象交给线程池维护,可以降低系统成本,从而提升程序的性能。

自定义线程池

使用 ThreadPoolExecutor 类,其构造方法:


关于拒绝策略,最常用的是 AbortPolicy ,具体如下:

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

index