JAVA并发编程的艺术(10)并发编程基础

in java并发 with 0 comment

这是《JAVA并发编程的艺术》(方腾飞)第四章的内容,这里主要做下总结。

线程简介

什么是线程

每启动一个Java程序,操作系统就会创建一个Java进程。现代操作系统调度的最小单元是线程,也叫轻量级进程,在一个进程里可以创建多个线程,这个线程都拥有各自的计数器、堆栈和局部变量等属性,并且能够访问共享的内存变量。

处理器在这些线程上高速切换,让使用者感觉到这些线程在同时执行。

为什么要使用多线程。

正确使用多线程,能够给开发人员带来显著的好处,而使用多线程的原因主要有以下几点:

  1. 更多的处理器核心以及超线程技术的广泛应用。我们知道线程是大多数操作系统调度的基本单元,一个程序作为一个进程来运行,程序运行过程中能创建多个线程,而一个线程在一个时刻只能运行在一个处理器核心上。如果一个单线程程序在运行时只能使用一个处理器核心,那么再多的处理器核心加入也无法显著提升该程序的执行效率。相反,如果该程序使用多线程技术,将计算逻辑分配到多个处理器核心上,就会显著减少程序运行的处理时间,并且随着更多处理器核心加入而变得有效率。
  2. 更快的响应时间。例如,一笔订单的创建,它包括插入订单数据、生成订单快照、发送邮件通知卖家和记录货品销售数量等。用户从单击“订购”按钮开始,就要等待这些操作全部完成才能看到订购成功的结果。在上面的场景中,可以使用多线程技术,即将数据一致性不强的操作派发给其他线程处理,如生成快照、发送邮件等。这样做的好处是响应用户请求的线程能够尽可能快地处理完成,缩短响应时间,提升了用户体验
  3. 更好的编程模型。Java为多线程编程提供了良好、考究并且一致性的编程模型,使开发人员能够更加专注于问题的解决。

线程优先级

现代操作系统基本采用时分的形式调度运行的线程,操作系统会分出一个个时间片,线程会分配到若干时间片,当线程的时间片用完了就会发生线程调度,并等待着下一次。线程优先级就是决定线程需要多或少分配一些处理器资源的线程属性。但是有些操作系统会忽略对线程优先级的设定。。。程序正确性不能依赖线程的优先级高低。

线程的状态

Java线程在运行的生命周期可能处于6种不同的状态,给定一个时刻,线程只能处于其中的一个状态。

状态名称说明
NEW初始状态,线程被构建,但是还没有调用start()方法
RUNNABLE运行状态,Java线程将操作系统中的就绪和运行两种状态笼统地称做"运行中"
BLOCKED阻塞状态,表示线程阻塞于锁
WAITING等待状态,表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程做出一些特定动作(通知或中断)
TIME_WAITING超时等待状态,该状态不同于WAITING,它是可以在指定的时间自行返回的
TERMINATED终止状态,表示当前线程以及执行完毕

线程在自身的生命周期中,并不是固定地处于某个状态,而是随着代码的执行在不同的状态之间进行切换.

启动和终止线程

线程的启动是通过调用线程的start()方法进行启动,随着run()方法的执行完毕,线程也随之终止。

构造线程

在运行线程之前首先要构造一个线程对象,线程对象在构造的时候需要提供线程所需要的属性,如线程所属的线程组、线程优先级。

private Thread(ThreadGroup g, Runnable target, String name,
                   long stackSize, AccessControlContext acc,
                   boolean inheritThreadLocals) {
if (name == null) {
            throw new NullPointerException("name cannot be null");
        }
	//当前线程就是该线程的父线程
        Thread parent = currentThread();
	......
	......
this.group = g;
        this.daemon = parent.isDaemon();
        this.priority = parent.getPriority();
        if (security == null || isCCLOverridden(parent.getClass()))
            this.contextClassLoader = parent.getContextClassLoader();
        else
            this.contextClassLoader = parent.contextClassLoader;
        this.inheritedAccessControlContext =
                acc != null ? acc : AccessController.getContext();
        this.target = target;
        setPriority(priority);
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        /* Stash the specified stack size in case the VM cares */
        this.stackSize = stackSize;

        /* Set thread ID */
        this.tid = nextThreadID();
    }

经过上面的过程,一个能够运行的线程对象就初始化好了,在堆内存中等待运行

启动线程

线程对象在初始化完成后,调用start()方法就可以启动这个线程。线程的start()方法的含义就是:

理解中断

中断可以理解为一个线程的一个标识位属性,它表示一个运行中的线程是否被其他线程进行了中断操作。好比其他线程对该线程打了个招呼,其他线程通过调用该线程的interupt()方法对其进行中断操作。

安全地终止线程

中断状态是线程的一个标识位,而中断操作是一种简便的线程间交互方式,这种交互方式最适合用来取消或者停止任务

除了中断以外,还**可以利用一个boolean变量来控制是否需要停止任务并终止该线程。

线程间通信

线程开始运行,拥有自己的栈空间,按照既定的代码一步一步的执行,直到终止。但是每个运行中的线程,如果仅仅是孤立地运行,那么价值很少。如果多个线程能够相互配合完成工作,这将会带来巨大的价值。

volatile和synchronized关键字

关键字volatile可以用来修饰字段,就是告知程序任何对该变量的访问均需要从共享内存中获取,而对它的改变必须同步刷新回共享内存,它能保证所有线程对变量访问的可见性关于volatile原理

关键字synchronized可以修饰方法或者以同步块的形式来使用,它主要确保多个线程在同一时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性。 关于synchronized原理

等待/通知机制

等待/通知机制,是指一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B调用了对象O的notify()或者notifyAll()方法,线程A收到通知后,从对象O的wait()方法返回。进而执行后续操作。

对象上的wait()和notify()/notifyAll()的关系就好比开关信号一样,用来完成等待方和通知方之间的交互工作。

等待/通知相关方法:

方法名称描述
notify()通知一个在对象上等待的线程,使其从wait()方法返回,而返回的前提是该线程获取到了对象的锁
notifyAll()通知所有等在该对象上的线程
wait()调用该方法的线程进入WAITING状态,只有等待另外线程的通知或被中断才会返回需要注意,调用wait()方法,回释放对象的锁
wait(long)超时等待一段时间,这里的参数时间是毫秒,也就是等待长达n毫秒,如果没有通知就会超时返回
wait(long,int)对于超时时间的更细粒度的控制,可以达到纳秒

一个线程修改了一个对象的值,而另一个线程感知到了变化,然后进行相应的操作,整个过程开始于一个线程,而最终执行又是另一个线程。前者是生产者,后者就是消费者。在功能层面上实现了解耦,体系结构上具备了良好的伸缩性。

等待/通知的经典范式

该范式分为两部分,分别针对等待方(消费者)和通知方(生产者)。

等待方遵循如下原则。

  1. 获取对象的锁。
  2. 如果条件不满足,那么调用对象的wait()方法,被通知后仍要检查条件。
  3. 条件满足则执行对应的逻辑。

通知方遵循如下原则。

  1. 获得对象的锁。
  2. 改变条件。
  3. 通知所有等待在线程上的进程。

Thread.join()方法

thread.join()其含义是:当线程A等待thread线程终止后,菜虫thread.join()返回。 源码如下:

public final synchronized void join(final long millis)
    throws InterruptedException {
        if (millis > 0) {
            if (isAlive()) {
                final long startTime = System.nanoTime();
                long delay = millis;
                do {
                    wait(delay);
                } while (isAlive() && (delay = millis -
                        TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime)) > 0);
            }
        } else if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        } else {
            throw new IllegalArgumentException("timeout value is negative");
        }
    }

当线程终止时,会调用线程自身的notifyAll()方法,会通知所有等待在该线程对象上的对象。可以看见join()方法的逻辑结构与等待/通知经典范式一致,即加锁、循环和处理逻辑三个步骤。

等待超时模式

等待超时模式就是在等待/通知范式基础上增加了超时控制,这使得该模式相比于原有范式更具灵活性,因为即使方法执行时间过长,也不会“永久”阻塞调用者,而是按照调用者的要求“按时”返回。

简单线程池

线程池是通过构造函数初始化连接的最大上限,通过一个双向队列来维护连接。 线程池会预先创建若干数量的线程,并且不能由用户直接对线程的创建进行控制,在这个前提下重复使用固定或较为固定的数目的线程来完成任务的执行。这样的好处有如下好处

  1. 消除了频繁创建和消亡线程的系统资源开销。
  2. 面对过量任务的提交能够平缓的劣化。

线程池的本质就是使用了一个线程安全的工作队列连接工作者线程和客户端线程,客户端线程将任务放回工作队列后便返回,而工作者线程则不断的从工作队列上取出工作并执行。当工作队列为空时,所有的工作者线程均等待在工作队列上,当有客户端提交了一个任务之后会通知任意一个工作者进程,随着大量的任务被提交,更多的工作者线程会被唤醒。