编程知识 cdmana.com

Java基础系列:多线程基础

小伙伴们,我们认识一下。

俗世游子:专注技术研究的程序猿

这节我们来聊一下Java中多线程的东西

本人掐指一算:面试必问的点,:slightly_smiling_face:

好的,下面在聊之前,我们先了解一下多线程的基本概念

基本概念

进程

那我们先来聊一聊什么是程序

  • 程序是一个指令的集合,和编程语言无关
  • 在CPU层面,通过编程语言所写的程序最终会编译成对应的指令集执行

通俗一点来说,我们在使用的任意一种软件都可以称之为程度,比如:

  • QQ,微信,迅雷等等

而操作系统用来分配系统资源的基本单元叫做进程,相同程序可以存在多个进程

windows系统的话可以通过任务管理器来进行查看正在执行的进程:

任务管理器查看进程

进程是一个静态的概念,在进程执行过程中,会占用特定的地址空间,比如:CPU,内存,磁盘等等。可以说进程是申请系统资源最小的单位且都是独立的存在

而且我们要注意一点就是:

  • 在单位时间内,进程在一个处理器中是单一执行的,CPU处理器每次只能够处理一个进程。只不过CPU的切换速度特别快

现在CPU所说的4核8线程、6核12线程就是在提高计算机的执行能力

那么这样就牵扯到一个问题:上下文切换

当操作系统决定要把控制权从当前进程转移到某个新进程时, 就会进行上下文切换,即保存当前进程的上下文、恢复新进程的上下文,然后将控制权传递到新进程。新进程就会从它上次停止的地方开始

摘自:《深入理解计算机系统》:1.7.1 进程

这也就是进程数据保存和恢复

线程

好,上面聊了那么多,终于进入到了主题:线程

前面说进程是申请资源最小的单位,那么线程是进程中的最小执行单元,是进程中单一的连续控制流程,并且进程中最少拥有一个线程:也就是我们所所的主线程

如果了解过Android开发的话,那么应该更能明白这一点

进程中最少执行线程名称

进程中可以拥有多个并行线程,最少会拥有一个线程。线程在进程中是互相独立的,多个线程之间的执行不会产生影响,但是如果多个线程操作同一份数据,那么肯定会产生影响(这也就是我们在前面所说的线程安全问题)

典型案例:卖票

进程中的线程共享相同的内存单元(内存地址空间),包括可以访问相同的变量和对象,可以从同一个堆中分配对象,可以做通信,数据交换、数据同步的操作

而且共享进程中的CPU资源,也就是说线程执行顺序通过抢占进程内CPU资源,谁能抢占上谁就可以执行。

后面聊到线程状态再细说

还有一种叫做:纤程/协程(一样的概念)

更轻量级别的线程,运行在线程内部,是用户空间级别的线程。后面再聊

面试高频:进程和线程区别

  1. 最根本的区别:进程是操作系统用来分配资源的基本单位,而线程是执行调度的最小单元
  2. 线程的执行依托于进程,且线程共享进程中的资源

  3. 每个进程都有独立的资源空间,CPU在进行进程切换的时候开销较大,而线程的开销较小

实现方式

了解完了基本概念之后,就要进入到具体的实操环节,在Java中,如果想要创建多线程的话,其表现形式一共有5中方式,记住:是表现形式。

下面我们先来看其中两种形式

继承Thread实现

在Thread源码中,包含对Java中线程的介绍,如何创建线程的两种表现形式,包括如何启动创建好的线程:

Thread中的介绍信息

所以说,一个类的注释文档地方非常重要

那么我们来自己创建一个线程:

class CusThread1 extends Thread {

    @Override
    public void run() {
        super.run();
        System.out.println("当前执行的线程名称:" + Thread.currentThread().getName());
    }
}

public class ThreadDemo1 {
    public static void main(String[] args) {
        System.out.println("当前执行线程名称:" + Thread.currentThread().getName());

        CusThread1 cusThread1 = new CusThread1();
        cusThread1.start();
    }
}

这就是一个最简单的线程创建,我们来看一下是否是成功的

Thread第一个程序执行结果

所以说这里创建线程分为两步:

  • 定义一个类,继承Thread主类并重写其中的run()
  • 调用start()方法开始执行

这里需要注意的一点,我们如果要启动一个线程的话,必须是调用start()方法,而不能直接调用run(),两者是有区别的:

  • 调用start()方法是Java虚拟机将调用此线程的run()方法,这里会创建两个线程:
    • 当前线程(从调用返回到start方法)
    • 执行run()的线程
public synchronized void start() {
    /**
         * This method is not invoked for the main method thread or "system"
         * group threads created/set up by the VM. Any new functionality added
         * to this method in the future may have to also be added to the VM.
         *
         * A zero status value corresponds to state "NEW".
         */
    if (threadStatus != 0)
        throw new IllegalThreadStateException();

    /* Notify the group that this thread is about to be started
         * so that it can be added to the group's list of threads
         * and the group's unstarted count can be decremented. */
    group.add(this);

    boolean started = false;
    try {
        start0();
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
            /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
        }
    }
}

// 这里是start()方法中具体开始执行的方法
private native void start0();
  • 而如果直接调用run()方法的话,相当于是普通方法的调用,是不会创建新的线程的,这里我们需要重点注意

这是一种方式,但是我们并不推荐该方式:

  • Java是单继承的,如果通过继承Thread,那么该类还需要继承其他类的话,就没有办法了
  • Thread启动时需要new当前对象,如果该类中存在共享属性的话,那么就意味着每次创建新的对象都会在新对象的堆空间中拥有该属性,那么我们每次操作该属性其实操作的就是当前对象堆空间中的属性

可能会有点难理解,我们来做个试验

public class ThreadDemo1 {

    public static void main(String[] args) {
        System.out.println("当前执行线程名称:" + Thread.currentThread().getName());

        CusThread1 cusThread1 = new CusThread1();
        CusThread1 cusThread2 = new CusThread1();
        CusThread1 cusThread3 = new CusThread1();
        cusThread1.start();
        cusThread2.start();
        cusThread3.start();
    }
}

class CusThread1 extends Thread {

    public int i = 1;

    @Override
    public void run() {
        for (int j = 0; j < 5; j++) {
            System.out.printf("当前线程:%s, i=%s \n", Thread.currentThread().getName(), i++);
        }
    }
}

Thread共享变量

当然,这种问题也是有解决的:

  • 就是将共享变量设置成static,我们看一下效果

Thread共享变量

实现Runnable接口

那我们来看下这种方式,Runnable是一个接口,其中只包含run()方法,我们通过重写其接口方法就可以实现多线程的创建

具体实现方式如下

class CusThread2 implements Runnable {
    public int i = 1;

    @Override
    public void run() {
        for (int j = 0; j < 5; j++) {
            System.out.printf("当前线程:%s, i=%s \n", Thread.currentThread().getName(), i++);
        }
    }
}

CusThread2 thread = new CusThread2();

new Thread(thread).start();
new Thread(thread).start();
new Thread(thread).start();

这里创建线程并启动也分为两步:

  • 线程类实现Runnable接口,并且重写run()方法
  • 通过new Thread(Runnable)的形式创建线程并调用start()启动

这里推荐采用这种方式,因为:

  • Java虽然是单继承,但是是多实现的方式,通过Runnable接口的这种方式即不影响线程类的继承,也可以实现多个接口
  • 就是共享变量问题,上面看到,线程类中的共享变量没有定义static,但是不会出现Thread方式中的问题

Runnable共享变量

因为在创建线程的时候,线程类只创建了一次,启动都是通过Thread类来启动的,所以就不会出现上面的问题

扩展:代理模式

从这种方式可以引出一种模式叫做:代理模式。那什么是代理模式呢?

  • 就是说为其他对象提供一种代理对象,通过代理对象来控制这个对象的访问

比如上面的Runnable/Thread,实际的业务逻辑写在Runnable接口中,但是我们却是通过Thread来控制其行为如:start, stop等

代理模式的关键点在于:

  • 利用了Java特性之一的多态,确定代理类和被代理类
  • 代理类和被代理类都需要实现同一个接口

这里给大家推荐一本设计模式的书:《设计模式之禅》

下面我们来做个案例,深入了解一下多线程

多窗口卖票案例

下面我们分别用两种创建线程的方式来做一下卖票这个小例子:

public class TicketThreadDemo {

    public static void main(String[] args) {

//        startTicketThread();
        startTicketRunnable();

    }

    private static void startTicketRunnable() {
        TicketRunnable ticketRunnable = new TicketRunnable();

        List<Thread> ticketThreads = new ArrayList<Thread>(5) {{
            for (int i = 0; i < 5; i++) {
                add(new Thread(ticketRunnable));
            }
        }};

        ticketThreads.forEach(Thread::start);
    }

    private static void startTicketThread() {
        List<TicketThread> ticketThreads = new ArrayList<TicketThread>(5) {{
            for (int i = 0; i < 5; i++) {
                add(new TicketThread());
            }
        }};

        ticketThreads.forEach(TicketThread::start);
    }
}

// Runnable方式
class TicketRunnable implements Runnable {

    private int ticketCount = 10;

    @Override
    public void run() {
        while (ticketCount > 0) {
            System.out.printf("窗口:%s, 卖出票:%s \n", Thread.currentThread().getName(), ticketCount--);
        }
    }
}

// Thread方式
class TicketThread extends Thread {

    // 记住,共享变量这里必须使用static,
    private static int ticketCount = 10;

    @Override
    public void run() {
        while (ticketCount > 0) {
            System.out.printf("窗口:%s, 卖出票:%s \n", Thread.currentThread().getName(), ticketCount--);
        }
    }
}

写到一起,就不拆分了,大家可以自己尝试下

TicketDemo

常用API属性及方法

这里我们来介绍一下在多线程中常用到的一些方法,上面我们已经使用到了:

  • start()

该方法也介绍过了,这里就不过多写了,下面看其他方法

sleep()

根据系统计时器和调度程序的精度和准确性,使当前正在执行的线程进入休眠状态(暂时停止执行)达指定的毫秒数。 该线程不会失去任何监视器的所有权

通俗一点介绍,就是将程序睡眠指定的时间,等睡眠时间过后,才会继续执行,这是一个静态方法,直接调用即可。

需要注意的一点:睡眠时间单位是毫秒

// 方便时间字符串的方法,自己封装的,忽略
System.out.println(LocalDateUtils.nowTimeStr());
try {
    // 睡眠2s
    Thread.sleep(2000L);
} catch (InterruptedException e) {
    e.printStackTrace();
}
System.out.println(LocalDateUtils.nowTimeStr());

sleep

isAlive()

验证当前线程是否活动,活动为true, 否则为false

private static void alive() {
    // 上一个例子,我拿来使用一下
    TicketThread ticketThread = new TicketThread();
    System.out.println(ticketThread.isAlive()); // false
    ticketThread.start();
    System.out.println(ticketThread.isAlive()); // true
}

join()

上面我们知道了线程是通过抢占CPU资源来执行的,那么线程的执行肯定是不可预测的,但是通过join()方法,会让其他线程进入阻塞状态,等当前线程执行完成之后,再继续执行其他线程

public static class JoinThread extends Thread{
    private int i = 5;

    public JoinThread(String name) {
        super(name);
    }

    @Override
    public void run() {
        while (i > 0) {
            System.out.println("当前线程【" + this.getName() + "】, 执行值【" + i-- + "】");
        }
    }
}

private static void join() {
    JoinThread t1 = new JoinThread("T1");
    JoinThread t2 = new JoinThread("T2");

    // 默认情况
    t1.start();
    t2.start();

    // 添加了join后的情况
    t1.start();
    t1.join();

    t2.start();
    t2.join();
}

join

yield

当前线程愿意放弃对处理器的当前使用,也就是说当前正在运行的线程会放弃CPU的资源从运行状态直接进入就绪状态,然后让CPU确定进入运行的线程,如果没有其他线程执行,那么当前线程就会立即执行

当前线程会进入到就绪状态,等待CPU资源的抢占

多数情况下用在两个线程交替执行

stop

stop()很好理解,强行停止当前线程,不过当前方法因为停止的太暴力已经被JDK标注为过时,推荐采用另一个方法:interrupt()

中断此线程

多线程的状态

线程主要分为5种状态:

  • 新生状态

就是说线程在刚创建出来的状态,什么事情都没有做

TicketThread ticketThread = new TicketThread();
  • 就绪状态

当创建出来的线程调用start()方法之后进入到就绪状态,这里我们要注意一点,start()之后并不一定就开始运行,而是会将线程添加到就绪队列中,然后他们开始抢占CPU资源,谁能抢占到谁就开始执行

ticketThread.start();
  • 运行状态

进入就绪状态的线程抢占到CPU资源后开始执行,这个执行过程就是运行状态。

在这个过程中业务逻辑开始执行

  • 阻塞状态

当程序运行过程中,发生某些异常信息时导致程序无法继续正常执行下去,此时会进入阻塞状态

当进入阻塞状态的原因消除后,线程就会重新进入就绪状态,随机抢占CPU资源然后等待执行

造成线程进入阻塞状态的方法:

  1. sleep()
  2. join()
  • 死亡状态

当程序业务逻辑正常运行完成或因为某些情况导致程序结束,这样就会进入死亡状态

进入死亡状态的方法:

  1. 程序正常运行完成
  2. 抛出异常导致程序结束
  3. 人为中断

Thread状态

总结

这篇大部分都是概念,代码方面很少,大家需要理解一下

就先写到这里,还有线程同步,线程池的内容,我们下一篇继续介绍

版权声明
本文为[osc_m53xdida]所创,转载请带上原文链接,感谢
https://my.oschina.net/u/4409765/blog/4840184

Scroll to Top