1. 基本概念

1.1 程序、进程、线程

  • 程序:程序是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。
  • 进程:是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程:有它自身产生、存在和消亡的过程——生命周期

    • 如运行中的浏览器、运行中的视频播放器
    • 程序是静态的,而进程是动态的
    • 进程作为资源分配的基本单位,系统在运行时会为每个进程分配不同的内存区域
  • 线程:进程可进一步细化为线程,是一个程序内部的一条执行路径

    • 若一个进程同一时间并行执行多个线程,就是支持多线程的
    • 线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc)线程切换的开销小
    • 一个进程中的多个线程共享相同的内存单元/内存地址空间,他们从同一个堆中分配对象,可以访问相同的变量和对象,这就使得线程间通信更简洁、高效。但是多个线程操作共享源可能会带来安全隐患。

1.2 CPU运行理解

  • 单核CPU与多核CPU

    • 单核CPU,其实是一种假的多线程,因为在一个时间单元内,也只能执行一个线程的任务。例如:虽然有多车道,但是只有一个收费站在收费,那么CPU好比收费站,如果有某个车不想交钱,那么可以将它挂起,让其他车辆继续通行,当这辆车准备好通过时再让它通行。由于CPU时间单元很短,因此感觉不到
    • 如果是多核心的话,才能更好地发挥多线程的效率。
    • 一个Java应用程序,至少拥有三个线程:main主线程、gc垃圾回收线程、以及异常处理线程。
  • 并行与并发

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

1.3 多线程的优点

  1. 提高应用程序的响应。对图形化界面更有意义,可增强用户体验
  2. 提高计算机系统CPU的利用率
  3. 改善程序结构,将长又复杂的进程分为多个线程,独立运行,利于理解和修改

1.4 何时需要多线程

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

2. 线程的创建和使用

  • Java语言的JVM允许程序运行多个线程,它通过java.lang.Thread类来实现
  • Thread类的特性

    • 每个线程都是通过某个特定Thread对象的run()方法来完成操作的,经常把run()方法的主体称为线程体
    • 通过该Thread对象的start()方法来启动这个线程,而非直接调用run()

2.1 方式一:继承于Thread类

步骤:

  1. 创建一个继承于Thread类的子类
  2. 重写Threadrun()方法,将此线程执行的工作声明在run()
  3. 创建Thread类的子类的对象
  4. 通过此对象调用Thread类的四大方法
public class ThreadTest {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
        // 主线程中输出带标记的偶数
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                System.out.println(i+"*");
            }
        }
    }
}

class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                System.out.println(i);
            }
        }
    }
}
执行结果中,带标记的偶数和不代标记的偶数交替出现,且每次运行结构都不一样,说明两个线程是同时进行的。如果直接调用run()方法,则仅仅是调用了对象重写的方法,导致其中的内容仍然只在主线程中执行,以下程序即可证明此点:
public class ThreadTest {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.run();
        System.out.println(Thread.currentThread().getName());
    }
}

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}

运行结果:

main
main

两个输出都是main,说明run()方法中的内容也在主线程中执行,而使用start()方法则将方法体放到新的线程中执行

注意点:

  • 我们不能通过直接调用run()方法启动线程
  • 再启动一个线程,如果直接使用已经start的线程,则会报IllegalThreadStateException异常,需要重新创建一个线程对象

补充:如果线程中执行的代码只需要执行一次,可以直接创建Thread类的匿名子类

public class ThreadDemo {
    public static void main(String[] args) {
        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();


        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                if (i % 2 != 0) {
                    System.out.println(Thread.currentThread().getName() + ": " + i);
                }
            }
        }).start();
    }
}
以上两种方式都属于创建Thread类的匿名子类对象,前者使用常规方法,后者使用java8新添加的lambda表达式。

2.2 方式二:实现Runnable接口

步骤:

  1. 创建一个实现了Runnable接口的类
  2. 在实现类中实现Runnable类中的抽象方法:run()
  3. 创建实现类的对象
  4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
  5. 通过Thread类调用start()
public class RunnableTest {
    public static void main(String[] args) {
        MThread mThread = new MThread();
        new Thread(mThread).start();
        new Thread(mThread).start();// 可共用一个Runnable对象
    }
}

class MThread implements Runnable {

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

2.3 两种方式对比

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

  1. 实现方式没有类的单继承局限性
  2. 实现的方式更适合处理多个线程共享数据的情况

二者的联系:

Thread类本身也实现了Runnable接口,两种方式都需要重写run(),将线程要执行的逻辑声明在run()中。

3. 线程中的常用方法

  • start():启动当前线程;调用当前线程的run()
  • run():通常需要重写Thread类中的此方法,将创建的线程要求执行的操作声明在此方法中
  • currentThread():静态方法,返回执行当前代码的线程
  • getName():获取当前线程的名字
  • setName()设置当前线程的名字
  • yield():一旦执行此方法表示释放当前CPU执行权(有可能释放后马上又抢占到资源)
  • join():在线程A中调用线程B的join()后,线程A即进入阻塞状态,直到线程B完全执行完毕,线程A才结束阻塞状态
  • stop():强制结束线程,不建议使用
  • sleep(long millitime):让当前线程阻塞指定毫秒数
  • isAlive()判断线程是否存活

4. 线程的调度

  • 调度策略:

    • 时间片:
    • 抢占式:高优先级的线程抢占CPU
  • Java的调度方法

    • 同优先级线程组成先进先出队列(先到先服务),使用时间片策略
    • 对高优先级,使用优先调度的抢占式策略
  • Java中线程的优先级

    • MAX_PRIORITY:10
    • MIN_PRIORITY:1
    • NORM_PRIORITY:5
  • 涉及的方法:

    • getPriority():返回线程的优先级
    • setPriority():设置线程的优先级
  • 说明:

    • 线程创建时继承父线程优先级
    • 低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调度

5. 线程的生命周期

JDK中使用ThreadState类定义了线程的几种状态,要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread及其子类的对象来表示线程,在它的一个完整生命周期中通常要经历如下五中状态:

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

线程生命周期

6. 线程的同步

当多个线程操作同一个资源时,若一个线程操作资源的过程中,其它线程在尚未操作完成时也来进行操作,就容易出现线程安全问题。例如多个线程买票,当一个线程读取剩余票数后,还未出票前,其它线程也来读取票数,这样执行以后将会出现重复售票问题。

为了解决上述问题,可以使一个线程操作一个共享资源时,阻止其他线程操作资源,只有线程操作完毕以后,其它线程才能继续操作。

在Java中,使用同步机制解决线程安全问题

6.1 方式一:同步代码块

使用关键字synchronized声明同步代码块,代码块中为操作共享数据的代码。

synchronized(同步监视器){
    //需要被同步的代码
}

同步监视器,俗称“锁”,任何一个类的对象都可以充当同步监视器,要求多个线程必须共用同一个。

注意:使用继承方式实现多线程时,同步监视器要慎用this关键字,同步代码块中内容要合适,不可包含过多,也不能包含少,应仅仅包含操作共享资源的代码。包含过多会导致失去多线程的优点,包含少了可能无法解决线程安全问题。

6.2 方式二:同步方法

如果操作共享数据的代码完整地声明在一个方法中,可以把此方法声明为同步的

class Window1 implements Runnable {
    private int ticket = 100;
    private Object obj = new Object();

    @Override
    public void run() {
        while (true) {
            buyticket();
        }
    }

    public synchronized void buyticket(){
        if (ticket > 0) {
            System.out.println(Thread.currentThread().getName() + "卖票,票号为" + ticket);
            ticket--;
        }
    }
}
同步方法仍然涉及同步监视器,只是不需要显示声明。非静态同步方法中默认的同步监视器为this,静态同步方法中同步监视器为类名.class

6.3 同步的优缺点

  • 同步的方式解决了线程安全问题
  • 操作同步代码时,只能有一个线程参与,其它线程则等待。相当于是一个单线程的过程,降低效率。

6.4 线程死锁

  • 死锁

    • 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
    • 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
  • 解决方法

    • 专门的算法、原则
    • 尽量减少同步资源的定义
    • 尽量避免嵌套同步
/**
 * @ClassName ThreadTest
 * @Description 演示线程死锁
 * @Author hxuanyu
 * @Date 2021/3/16 15:19
 * @Version 1.0
 **/
public class ThreadTest {
    public static void main(String[] args) {

        StringBuffer s1 = new StringBuffer();
        StringBuffer s2 = new StringBuffer();

        new Thread() {
            @Override
            public void run() {
                synchronized (s1){
                    s1.append("a");
                    s1.append("1");
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (s2) {
                        s1.append("b");
                        s2.append("2");
                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }
        }.start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (s2){
                    s1.append("c");
                    s1.append("3");
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (s1) {
                        s1.append("d");
                        s2.append("4");
                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }
        }).start();
    }
}
以上代码中,由于两个线程先后使用了两个嵌套的同步代码块,实际运行后会出现死锁现象

6.5 Lock锁解决线程同步

  1. 创建ReenTrantLock类的对象
  2. 调用lock()方法
  3. 调用unlock()方法
class Window implements Runnable {

    private int ticket = 100;
    // true:先进先出,false: 默认情况
    private ReentrantLock lock = new ReentrantLock(true);

    @Override
    public void run() {
        while (true) {
            try {
                //调用lock方法
                lock.lock();
                if (ticket > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ": 售票,票号为: " + ticket);
                    ticket--;
                } else {
                    break;
                }
            } finally {
                // 解锁
                lock.unlock();
            }
        }
    }
}

public class LockTest {
    public static void main(String[] args) {
        Window w = new Window();
        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);

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

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

    }
}

synchronized与Lock的区别:

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

    • synchronized机制在执行完相应的同步代码块后,自动释放监视器
    • Lock需要手动启动同步,同时结束同步也需要手动实现

同步方法优先顺序:

  1. Lock
  2. 同步代码块
  3. 同步方法

7. 线程的通信

涉及到的方法:

  • wait():使当前线程进入阻塞状态,并释放同步监视器
  • notify():唤醒被wait()的一个线程,如果有多个,则唤醒优先级较高的那个
  • notifyAll():唤醒所有被wait的方法

注意点:

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

sleep()wait()方法的异同

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

    • 生命的未知不一样:sleep()声明在Thread中,wait()声明在Object
    • 调用要求不同,sleep()可以在任何需要的场景下使用,而wait()必须在同步方法或同步代码块中调用
    • 关于时都释放同步监视器:如果两个方法都使用在同步方法或同步代码块中,sleep()不会释放同步监视器,wait()会释放同步监视器

8. 多线程应用——生产者消费者问题

生产者将产品交给店员,而消费者从店员处取走产品,店员一次只能持有固定数量的产品,如果生产者试图生产更多的产品,店员会让生产者停下来,如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品了,店员会让消费者等一下,如果店中有产品了再通知消费者来取走产品。

分析:

  1. 是否是多线程:是,生产者线程、消费者线程
  2. 是否有共享数据:是,店员(或产品)
  3. 如何解决线程安全问题:同步机制(三种方法)
  4. 是否涉及线程通信:是
class Clerk {

    private int productCount = 0;

    // 生产产品
    public synchronized void produceProduct() {
        if (productCount < 20) {
            productCount++;
            System.out.println(Thread.currentThread().getName() + ":开始生产第" + productCount + "个产品");
            notify();
        } else {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }


    // 消费产品
    public synchronized void consume() {
        if (productCount > 0) {
            System.out.println(Thread.currentThread().getName() + ":开始消费第" + productCount + "个产品");
            productCount--;
            notify();
        } else {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class Producer extends Thread {
    private Clerk clerk;

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

    @Override
    public void run() {
        System.out.println(getName() + "开始生产产品。。。。。。");
        while (true) {
            try {
                sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            clerk.produceProduct();
        }
    }
}

class Consumer extends Thread {
    private Clerk clerk;

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

    @Override
    public void run() {
        System.out.println(getName() + "开始消费产品。。。。。。");
        while (true) {
            try {
                sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            clerk.consume();
        }
    }
}


public class ProductTest {
    public static void main(String[] args) {
        Clerk clerk = new Clerk();
        Producer producer = new Producer(clerk);
        producer.setName("生产者");

        Consumer consumer = new Consumer(clerk);
        consumer.setName("消费者");
        
        producer.start();
        consumer.start();
    }
}

9. JDK5.0中新增的线程创建方式

9.1 实现Callable接口

  • 与使用Runnable接口相比,Callable功能更强大

    • 相比run()方法,可以有返回值
    • 方法可以抛出异常
    • 支持泛型的返回值
    • 需要借助FutureTask类,比如获取返回结果
  • Future接口

    • 可以对具体RunnableCallable任务的执行结果进行取消、查询是否完成、获取结构等操作
    • FutureTaskFuture接口的唯一实现类
    • FutureTask同时实现了Runnable接口、Future接口。它既可以作为Runnable被线程执行,也可以作为Future得到Callable的返回值。

使用步骤:

  1. 创建一个Callable接口的实现类
  2. 实现cal()方法,将线程需要执行的操作声明在方法体中
  3. 创建实现类的对象
  4. 将上一步的对象传递到FutureTask类的构造器中,创建新的FutureTask对象
  5. FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()方法启动线程
  6. 获取Callablecall()的返回值
class NumberThread implements Callable {

    @Override
    public Object call() throws Exception {
        // 遍历100以内偶数并返回和
        int sum = 0;
        for (int i = 0; i <= 100; i++) {
            if (i % 2 == 0) {
                System.out.println(i);
                sum += i;
            }
        }
        return sum;
    }
}

public class ThreadNew {
    public static void main(String[] args) {
        NumberThread numberThread = new NumberThread();
        FutureTask futureTask = new FutureTask(numberThread);
        new Thread(futureTask).start();
        try {
            // get方法返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值
            Object o = futureTask.get();
            System.out.println(o);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

9.2 线程池

  • 背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大
  • 思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具
  • 好处:

    • 提高响应速度(减少了创建新线程的时间)
    • 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
    • 便于线程管理

线程池相关API

  • JDK 5.0起提供了线程池相关API:ExecutorServiceExecutors
  • ExecutorService:真正的线程连接池接口。常见子类ThreadPoolExecutor

    • void execute(Runnable command):执行任务或命令,没有返回值,一般用来执行Runnable
    • <T> Future<T> submit(Callable<T> task):执行任务,有返回值,一般用来执行Callable
    • void shutdown():关闭线程池
  • Executors:工具类、线程池工厂类,用于创建并返回不同类型的线程池

    • Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
    • Executors.newFixedThreadPool():创建一个可重用固定线程数的线程池
    • Executors.newSingleThreadExecutor():创建一个只有一个线程的线程池
    • Executors.newScheduledThreadPool():创建一个线程池,它可以安排在给定延迟后运行命令或定期执行。

使用步骤:

  1. 提供指定线程数量的线程池
  2. 执行指定的线程操作,需要提供实现Runnable接口实现类的对象
  3. 关闭连接池
实际开发中一般会使用线程池的方式
最后修改:2021 年 03 月 16 日
如果觉得我的文章对你有用,请随意赞赏