一、基础概念

多线程的学习从一些概念开始,进程和线程,并发与并行,同步与异步,高并发。

1.1 进程与线程

几乎所有的操作系统都支持同时运行期多个任务,所有运行中的任务通常就是一个进程,进程是处于运行过程中的程序,进程是操作系统进行资源分配和调度的一个独立单位。

多线程与高并发(一)多线程入门-LMLPHP

进程有三个如下特征:

  • 独立性:进程是系统中独立存在的实体,它可以拥有自己独立的资源,每一个进程都拥有自己私有的地址空间。在没有经过进程本身允许的情况下,一个用户进程不可以直接访问其他进程的地址空间。

  • 动态性:进程与程序的区别在于,程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合。在进程中加入了时间的概念,进程具有自己的生命周期和各种不同的状态,这些概念在程序中部是不具备的。

  • 并发性:多个进程可以在单个处理器上并发执行,多个进程之间不会互相影响。

线程是进程的组成部分,一个进程可以拥有多个线程,而线程必须有一个父进程,线程可以有自己的堆栈、自己的程序计数器和自己的局部变量,但不拥有系统资源。比如使用QQ时,我们可以同事传文件,发送图片,聊天,这就是多个线程在进行。

线程可以完成一定的任务,线程能够独立运行的,它不知道有其他线程的存在,线程的执行是抢占式的,当前线程随时可能被挂起。

总之:一个程序运行后至少有一个进程,一个进程里可以有多个线程,但至少要有一个线程。

1.2 并发和并行

并发和并行是比较容易混淆的概念,他们都表示两个或者多个任务一起执行,但并发侧重多个任务交替执行,同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行,使得在宏观上具有多个进程同时执行的效果。而并行确实真正的同时执行,有多条指令在多个处理器上同时执行,并行的前提条件就是多核CPU。

1.3 同步和异步

同步和异步通常用来形容一次方法调用。同步方法调用一旦开始,调用者必须等到方法调用返回后,才能继续后续的行为。异步方法调用更像一个消息传递,一旦开始,方法调用就会立即返回,调用者可以继续后续的操作。

1.4 高并发

高并发一般是指在短时间内遇到大量操作请求,非常具有代表性的场景是秒杀活动与抢票,高并发是互联网分布式系统架构设计中必须考虑的因素之一,高并发相关常用的一些指标有响应时间(Response Time),吞吐量(Throughput),每秒查询率QPS(Query Per Second),并发用户数等。

多线程在这里只是在同/异步角度上解决高并发问题的其中的一个方法手段,是在同一时刻利用计算机闲置资源的一种方式

1.5 多线程的好处

线程在程序中是独立的、并发的执行流,拥有独立的内存单元,多个线程共享父进程里的全部资源,线程共享的环境有进程的代码段,进程的公有数据等,利用这些共享数据,线程很容易实现相互之间的通信,可以提高程序的运行效率。

多线程的好处主要有:

  • 进程之间不能共享内存,但线程之间共享内存非常容易。

  • 系统创建进程时需要给进程重新分配系统资源,但创建线程代价小得多,所以使用多线程实现多任务并发比多进程效率高

  • Java语言内置了多线程功能支持。

二、使用多线程

上面讲了多线程的一些概念,都有些抽象,下面将学习如何使用多线程,创建多线程的方式有三种。

2.1 继承Thread类创建

继承Thread创建并启动多线程有三个步骤:

  1. 定义类并继承Thread,重写run()方法,run()方法中为需要多线程执行的任务。

  2. 创建该类的实例,即创建了线程对象。

  3. 调用实例的start()方法启动线程。

public class FirstThread extends Thread {

    private int i=0;
    public void run() {
        for (; i < 100; i++) {
            //获取当前线程名称
            System.out.println(this.getName() + " " + i);
        }
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            //Thread的静态方法currentThread,获取当前线程
            System.out.println(Thread.currentThread().getName());
            if (i == 20) {
                //创建线程并启动
                new FirstThread().start();
                new FirstThread().start();
            }

        }
    }
}

运行结果可以看到两个线程的i并不是连续的,说明他们并不共享数据。

2.2 实现Runnable接口

实现Runnable接口创建并启动多线程也有以下步骤:

  1. 定义类并继承Runnable接口,重写run()方法,run()方法中为需要多线程执行的任务。

  2. 创建该类的实例,并以此实例作为target为参数来创建Thread对象,这个Thread对象才是真正的多线程对象。

public class SecondThread implements Runnable {
    private int i = 0;

    @Override
    public void run() {
        for (; i < 100; i++) {
            //此时想要获取到多线程对象,只能使用Thread.currentThread()方法
            System.out.println(Thread.currentThread().getName() + " " + i);
        }
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            //Thread的静态方法currentThread,获取当前线程
            System.out.println(Thread.currentThread().getName());
            if (i == 20) {
                //创建线程并启动
                SecondThread secondThread=new SecondThread();
                new Thread(secondThread,"线程一").start();
                new Thread(secondThread,"线程二").start();
            }

        }
    }
}

2.3 使用Callable和Future

Callable是Runnable的增加版,主要是接口中的call()方法可以有返回值,并且可以申明抛出异常,使用Callable创建的步骤如下:

  1. 定义类并继承Callable接口,重写call()方法,run()方法中为需要多线程执行的任务。

  2. 创建类实例,使用FutureTask来包装对象实例,

  3. 使用FutureTask对象作为Thread的target来创建多线程,并启动线程。

  4. 调用FutureTask对象的get()方法来获取子线程结束后的返回值。

public class ThirdThread {

    public static void main(String[] args) {
        //使用lambda表达式
        FutureTask<Integer> task = new FutureTask<>((Callable<Integer>) () -> {
            int i = 0;
            for (; i < 100; i++) {
                System.out.println(Thread.currentThread().getName() + "的循环变量i的值:" + i);
            }
            return i;
        });
        for (int i = 0; i < 100; i++) {
            //Thread的静态方法currentThread,获取当前线程
            System.out.println(Thread.currentThread().getName());
            if (i == 20) {
                //创建线程并启动
                new Thread(task, "有返回值的线程").start();
            }
        }
        try {
            System.out.println("线程的返回值:" + task.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

这里使用了lambda表达式,不使用表达式的方式也很简单,可以去源码中查看。Callable与Runnable方式基本相同,只不过增加了返回值且可允许声明抛出异常。

使用三种方式都可以创建线程,且方式也相对简单,大体分为实现接口和实现Thread类两种,这两种都各有优缺点。

继承接口实现:

  • 优点:除了继承接口之外,还可以继承其他类。这种方式多个线程共享一个target对象,可以处理用于共同资源的情况。
  • 缺点:编程稍微复杂一些,并且没有直接获取当前线程对象的方式,必须使用Thread.currentThread()方式。

基础Thread类:

  • 优点:编程简单

  • 缺点:不能继承其他类

三、多线程的生命周期

线程状态是线程中非常重要的一个概念,然而我看过很多资料,线程的状态理解有很多种方式,很多人将其分为五个基本状态:新建、就绪、运行、阻塞、死亡,但在状态枚举中并不是这五个状态,我不知道是什么原因(有大神可以解答更好),只能按照枚举中的状态根据自己的理解。

  1. 初始(NEW):新创建了一个线程对象,但还没有调用start()方法,而且就算调用了改方法也不代表状态立即改变。

  2. 运行(RUNNABLE):在运行的状态肯定就处于RUNNABLE状态。

  3. 阻塞(BLOCKED):表示线程阻塞,或者说线程已经被挂起了。

  4. 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。

  5. 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。

  6. 终止(TERMINATED):表示该线程已经执行完毕。

状态流程图如下:

多线程与高并发(一)多线程入门-LMLPHP

理解:初始状态很好理解,这个时候其实还不能被称为一个线程,因为他还没被启动,当调用start()方法后,线程正式启动,但是也不代表立即就改变了状态。

运行状态中其实包含两种状态,运行中(RUNING)就绪(READY)

就绪状态表示你有资格运行,只要CPU还未调度到你,就处于就绪状态,有几个状态会是线程状态编程就绪状态

  • 调用线程的start()方法。

  • 当前线程sleep()方法结束,其他线程join()结束,等待用户输入完毕,某个线程拿到对象锁。

  • 当前线程时间片用完了,调用当前线程的yield()方法。

  • 锁池里的线程拿到对象锁后。

运行中(RUNING)状态比较好理解,线程调度程序选择了当前线程作。

阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态。

等待状态是指线程没有被CPU分配执行时间,需要等待,这种等待是需要被显示的唤醒,否则会无限等待下去。

超时等待状态是这现在没有被CPU分配执行时间,需要等待,不过这种等待不需要被显示的唤醒,会设置一定的时间后zi懂唤醒。

死亡状态也很好理解,说明线程方法被执行完成,或者出错了,线程一旦进入这个状态就代表彻底的结束

06-26 17:46