Java基础系列:多线程基础

小伙伴们,咱们认识一下。java

俗世游子:专一技术研究的程序猿面试

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

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

好的,下面在聊以前,咱们先了解一下多线程的基本概念设计模式

基本概念

进程

那咱们先来聊一聊什么是程序安全

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

通俗一点来讲,咱们在使用的任意一种软件均可以称之为程度,好比:微信

  • QQ,微信,迅雷等等

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

windows系统的话能够经过任务管理器来进行查看正在执行的进程:编程语言

任务管理器查看进程

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

并且咱们要注意一点就是:

  • 在单位时间内,进程在一个处理器中是单一执行的,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状态

总结

这篇大部分都是概念,代码方面不多,你们须要理解一下

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

相关文章
相关标签/搜索