Power by GeekHades

Java多线程(一)——多线程基础

0x1 前言

首先在开始写有关教程之前,我想抛出几个问题并且给出自己的理解,也希望读者能够认真考虑这几个问题。

1. Java多线程深入学习的意义? Java多线程是许多初学者在学习Java的过程中比较难理解的知识模块之一,而且也是很难在短时间内收获成效的知识点之一。从而导致现在很多学习Java的人不太愿意去深入了解这一个模块,转而选择用某些相对成熟的多线程框架来充当解决方案,还有更甚者会直接粗暴的使用。就像下面的例子:

new Thread(){
    @Override
    public void run() {
        // do some work on background
        ...
        // work done to notify other
    }
 }.start();

而且有很多类似的代码充斥在一些低质量的小项目之中。所以在进行学习之前我还是想重新强调,你如果深入了解多线程,它可能并不会使你马上获得工作或者升职加薪。但是它能让你避免写出一些低质量的代码,能让你软件开发这条路走的更远、境界更高。

2. 网络上已经有大量介绍Java多线程的文章,为什么还要再写? 网上大多数的文章基本一上来就是介绍Thread/Runnable或是某个特别的方法。会给初学者一种好像多线程这个机制是Java特有的一样,我写的其实严格意义上来说是通过Java来更好的理解线程的概念,线程是操作系统的概念,并不是某一种语言独有的,语言只不过是对操作系统提供的接口做了封装,使得我们程序员不必要去理会底层相关的东西。因此我所写的Java多线程一系列的教程或多或少的参杂的操作系统的知识,因为这样才不会出现知识孤立现象,希望系列文章能够是您更好的、系统的了解何为线程?Java是如何做的。

3. 打算如何讲? 这个问题其实问的我自己心很慌。虽然我是撰写者,但我同时也是个审视者和学习者。我写的过程我自己也在不断纠正和温习,所以我打算从最根本的地方出发,打好基础以后的路我们就好走很多。借用一句经济学的话:

经济基础决定上层建筑

所以我会引入一些至关重要的操作系统概念,但不至于太深入,力求够用就行,如果读者有兴趣可以自行深入了解相关模块,毕竟我们的重点还是讲Java多线程。因为如果我们能理解基本概念我们学起来会更加的好。

同时每一章节我都会以一个令我印象深刻的问题来作为牵引线,我们利用所学的知识一步步解决这个问题。但是我需要指出的一点是,很有可能某一章节中最后解决的方案不是最好的甚至是错误的,但是我依然会选择这样做,因为我希望的是我们能利用那一节中所学到的知识去解决我们遇到的问题,学一点用一点,而不是像其他人所写的文章那样一下抛出比较满意的解决方案。

希望读者能够明白我们是在学习,每一节学完我都希望大家能够反思不足。这也是我为什么我明明知道某个方案不完美但还是选择抛出来的原因。在解决实际的问题中,我们不可能一蹴而就。养成一种良性的思考过程比什么都重要。


0x2 线程的基本概念

说到线程,我们必须先引进进程的基本概念。

进程(Process),是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位。那么也就意味着进程拥有两个基本元素程序代码数据集。我们目前可以粗略的认为只要软件在操作系统上运行,那么就创建了一条进程。

线程(Thread),它被包含在进程之中,是进程中实际运作的单位,是操作系统能够进行运算调度的最小单位。也就是说一个进程中至少包括一条线程。

多线程(Multi-Threading)单个线程单个进程中一个单一顺序的控制流,那么如果单一进程内有多个并发控制流,我们就称之为多线程。例如我们有一个火车售票系统(这也是本节我们需要动手做的案例),我们不可能只设计一个一个售票窗口,假设我们一条进程代表一个售票窗口的话,那么我们可以理解为这个系统(进程)就是多线程的。

线程的状态。线程有四种基本状态,与进程的五态模型相似但也有所区别。

  • 产生(spawn),可以理解为是新建了一条进程,当线程被创建的时候相应的属于这条线程的寄存器上下文和栈空间也被开辟出来。此时线程会被放入就绪队列中。值得注意的一点是,就绪队列并不代表线程是就绪态,与进程有所区别。

  • 阻塞(block),当线程需要等待某一个事件的时候,就会进入阻塞状态。就像你去买火车票回家,但是你前面还有x个人,那么你当前就是阻塞态。

  • 解除阻塞(unblock),当阻塞的一个线程的事件发生时,该线程就会被移到就绪队列。意思就是到你买票了。

  • 结束(finish),当一个线程完成时,其寄存器上下文和栈都会被释放。意思就是买到票了就麻溜走啊。

那么问题来了,为什么进程有运行态而线程没有?因为只有进程才有资格去争夺CPU的运行时间,进程争夺来的时间是让从就绪队列中选中的线程来使用的,记住,多线程是并发执行的,也就意味着每个线程是交替运行。

是否进入了就绪队列的线程是按先到先得的顺序来执行任务?我个人认为这是不可知的且需要分情况讨论,进入了就绪队列的线程还是需要竞争,所以就引入了优先级这个标准,优先级高的会更有竞争力,同级之间就是未知的了。

关于线程的基本概念和一些问题我们就讨论到这里,这些对我们学习Java的多线程够用了。(终于可以讲Java多线程了)


0x3 Java多线程

首先我们需要明确的一点,Java中使用简单的使用多线程并不会涉及我们之前所学过的那些基本知识,所谓简单就像文章开头所讲的那个案例一样。Java帮我们封装了许多东西,初衷只是让我们更快速的工作,但后来演变成人们越来越不想去了解其所以然。我们来看一下Java给我们提供的多线程工具到底是怎么样的。以下资料均来自Oracle官方文档,由本人翻译,因为水平有限可能翻译可能不是很到位。如有想获取更贴切的信息还请移玉步至官方文档

Runnable接口。emmm.与其说是接口不如说是线程运行通用协议更加贴切一点。它只有一个run()方法,启动线程时便会执行该方法。为什么先要提Runnable而不是Thread呢?原因是Runnable的作用实在是太重要了,虽然只有一个run()方法,但是它使得我们使用多线程、线程共享数据变得很方便,并且与继承Thread方式相比,实现Runnable更能保持类原来的特性,而且接口支持多继承。Api-Docs

Thread,它实现了RunnableThread负责创建和管理线程。它替我们程序员实现并封装了许许多多的线程概念。例如开启线程、线程优先级、线程休眠、等待线程、中断线程等等。建议读者可以使用我们上面介绍的概念去Api-Docs中去配对一下,这样更有助于你理解Thread

Thread的使用方法也很简单,这里给出一种建议的写法。

{
    // 假定我们已经实现了一个火车售票系统,TicketSeller是一个
    // 实现了Runnable方法的售票类
    TicketSeller seller1 = new TicketSeller("窗口1");
    Thread st1 = new Thread(seller1, seller1.getName());
    st1.start();
}

比起开头所说的那种写法,这种写法的可读性和健壮性更高。


0x4 火车售票系统

这个例子是以前我老师和我讲的一个多线程的例子,能够体现很多问题,让我印象非常深刻。

需求:设计一个火车票售票系统、一共有100万张票,分10个窗口出售。

Ticket类,相当于我们的票仓库。所有窗口售出的票都出自这里。

public class Ticket {

    // 火车票总数 所有Seller共享
    private int tickets = 100;

    public Ticket() {
    }

    public int getTickets() {
        return tickets;
    }

    public void setTickets(int tickets) {
        this.tickets = tickets;
    }
}

TicketSeller类是负责售票的人员

public class TicketSeller implements Runnable{


    // 售票窗口名字
    private String name;

    // 火车票库存对象
    private Ticket ticket;

    public TicketSeller(String name, Ticket ticket) {
        this.name = name;
        this.ticket = ticket;
    }

    @Override
    public void run() {
        while (ticket.getTickets() > 0) {
            ticket.setTickets( ticket.getTickets() - 1 );
            System.out.println(name + "售出一张票,总票数还剩:" + ticket.getTickets());
        }
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

main方法

public static void main(String[] args) {
    Ticket ticket = new Ticket();
    // 创建10个售票窗口
    ArrayList<Thread> sellWindows = new ArrayList<>();
    for (int i = 0; i < 10; i++) {
        TicketSeller ticketSeller = new TicketSeller("窗口"+(i+1), ticket);
        Thread sellWindow = new Thread(ticketSeller, ticketSeller.getName());
        sellWindows.add(sellWindow);
        // 启动线程
        sellWindow.start();
    }
}

这个简单的需求就完成了,让我们看一下控制台打印出来的结果(部分结果)。

窗口1售出一张票,总票数还剩:99
窗口1售出一张票,总票数还剩:95
窗口1售出一张票,总票数还剩:93
窗口1售出一张票,总票数还剩:91
窗口1售出一张票,总票数还剩:90
窗口3售出一张票,总票数还剩:97
窗口4售出一张票,总票数还剩:96
窗口9售出一张票,总票数还剩:85
窗口2售出一张票,总票数还剩:98
...
窗口8售出一张票,总票数还剩:3
窗口8售出一张票,总票数还剩:2
窗口8售出一张票,总票数还剩:1
窗口8售出一张票,总票数还剩:0
窗口3售出一张票,总票数还剩:36
窗口1售出一张票,总票数还剩:55
窗口7售出一张票,总票数还剩:70
窗口6售出一张票,总票数还剩:71
窗口5售出一张票,总票数还剩:72
窗口4售出一张票,总票数还剩:12
窗口10售出一张票,总票数还剩:14
窗口9售出一张票,总票数还剩:15
窗口2售出一张票,总票数还剩:17

尽管每台机器、每次运行打印出来的结果都不一样,但是问题总是很突出的,除了第一次窗口1出售的结果正常之外,其他结果都好像怪怪的。甚至还出现了剩下0张的时候,其他窗口居然还有票出售,黑幕啊。


0x5 不尽人意的结果

这个结果可能不是我们想要的,但是我们这一章学的东西好像只能解决那么多。所以这里抛出一个问题,怎样才能使得这个售票正常点?我会在(Java多线程(二)——同步与互斥)中分析结果并给出一个相对好一点的方案。

最后祝大家身体健康、学习进步以及工作顺利。



* 如果你对文章有任何意见或建议请发 邮件 给我!
* if you have any suggestion that you could send a E-mail to me, Please!