自我提升(基础技术篇)——java线程简介

96
justCode_
2017.08.18 17:25* 字数 11121

前言:虽然自己平时都在用多线程,也能完成基本的工作需求,但总觉得,还是对线程没有一个系统的概念,所以,查阅了一些资料,理解那些大神和官方的资料,写这么一篇关于线程的文章

本来想废话一番,讲讲自己的经历,不过,还是直接上正题吧。

想要系统的认识一个东西,我们还是得一步步来,从基础出发,这样才能比较系统的了解一个东西!

线程基础

什么是线程?

几乎每种操作系统都支持进程的概念 ―― 进程就是在某种程度上相互隔离的、独立运行的程序。

线程化是允许多个活动共存于一个进程中的工具。大多数现代的操作系统都支持线程,而且线程的概念以各种形式已存在了好多年。Java 是第一个在语言本身中显式地包含线程的主流编程语言,它没有把线程化看作是底层操作系统的工具。

有时候,线程也称作轻量级进程。就象进程一样,线程在程序中是独立的、并发的执行路径,每个线程有它自己的堆栈、自己的程序计数器和自己的局部变量。但是,与分隔的进程相比,进程中的线程之间的隔离程度要小。它们共享内存、文件句柄和其它每个进程应有的状态

进程可以支持多个线程,它们看似同时执行,但互相之间并不同步。一个进程中的多个线程共享相同的内存地址空间,这就意味着它们可以访问相同的变量和对象,而且它们从同一堆中分配对象。尽管这让线程之间共享信息变得更容易,但您必须小心,确保它们不会妨碍同一进程里的其它线程。(锁的问题)

Java 线程工具和 API 看似简单。但是,编写有效使用线程的复杂程序并不十分容易。因为有多个线程共存在相同的内存空间中并共享相同的变量,所以您必须小心,确保您的线程不会互相干扰

每个 Java(Android) 程序都使用线程

每个 Java 程序都至少有一个线程 ― 主线程。当一个 Java 程序启动时,JVM 会创建主线程,并在该线程中调用程序的main()方法。

JVM 还创建了其它线程,您通常都看不到它们 ― 例如,与垃圾收集、对象终止和其它 JVM 内务处理任务相关的线程。其它工具也创建线程,如 AWT(抽象窗口工具箱(Abstract Windowing Toolkit))或 Swing UI 工具箱、servlet 容器、应用程序服务器和 RMI(远程方法调用(Remote Method Invocation))。

每一个Android程序也是如此,而Android程序,默认是UI线程。

为什么使用线程?

在 Java (Android)程序中使用线程有许多原因:

* 使 UI 响应更快

* 利用多处理器系统

* 简化建模

* 执行异步或后台处理

响应更快的 UI:

事件驱动的 UI 工具箱(如 AWT 和 Swing)有一个事件线程,它处理 UI 事件,如击键或鼠标点击。

AWT 和 Swing 程序把事件侦听器与 UI 对象连接。当特定事件(如单击了某个按钮)发生时,这些侦听器会得到通知。事件侦听器是在 AWT 事件线程中调用的。

如果事件侦听器要执行持续很久的任务,如检查一个大文档中的拼写,事件线程将忙于运行拼写检查器,所以在完成事件侦听器之前,就不能处理额外的 UI 事件。这就会使程序看来似乎停滞了,让用户不知所措。

要避免使 UI 延迟响应,事件侦听器应该把较长的任务放到另一个线程中,这样 AWT 线程在任务的执行过程中就可以继续处理 UI 事件(包括取消正在执行的长时间运行任务的请求)。

Android 的话,举个最简答的例子,界面显示和网络请求,界面展示在UI线程,网络请求在子线程。网络请求是耗时操作,UI线程只负责界面渲染。就可以让页面流畅,等网络请求结束后再刷新界面。这样,就不会因为网络请求而阻塞界面的显示了。

利用多处理器系统

多处理器系统比过去更普及了。以前只能在大型数据中心和科学计算设施中才能找到它们。现在许多低端服务器系统 ― 甚至是一些台式机系统 ― 都有多个处理器。

现代操作系统,包括 Linux、Solaris 和 Windows,Mac都可以利用多个处理器并调度线程在任何可用的处理器上执行。

调度的基本单位通常是线程;如果某个程序只有一个活动的线程,它一次只能在一个处理器上运行。如果某个程序有多个活动线程,那么可以同时调度多个线程。在精心设计的程序中,使用多个线程可以提高程序吞吐量和性能。

简化建模

在某些情况下,使用线程可以使程序编写和维护起来更简单。考虑一个仿真应用程序,您要在其中模拟多个实体之间的交互作用。给每个实体一个自己的线程可以使许多仿真和对应用程序的建模大大简化。

另一个适合使用单独线程来简化程序的示例是在一个应用程序有多个独立的事件驱动的组件的时候。例如,一个应用程序可能有这样一个组件,该组件在某个事件之后用秒数倒计时,并更新屏幕显示。与其让一个主循环定期检查时间并更新显示,不如让一个线程什么也不做,一直休眠,直到某一段时间后,更新屏幕上的计数器,这样更简单,而且不容易出错。这样,主线程就根本无需担心计时器。

这个比较抽象,嗯,我试着用更简单的话来解释:就举例快递吧,单线程就是只有一个快递员,但是有那么多的包裹要配送,这一个快递员,就需要去想,我该按什么顺序送呢?先送哪里后送哪里才最快等等?这样这个快递员就像想很多问题(其实就是linux或者cpu或者程序员要想)。现在多线程,就是我有很多快递员了,我就指派A快递员负责A区域的快递,B负责B区域的,以此类推。我每个快递员,就只需要负责自己那块区域,这样既高效又省事。(当然,具体要设计的cpu的线程调度问题了,就这么理解着吧)那么主线程的快递呢,那就是vip了,走专门的路线,到指定的地点。

异步或后台处理

服务器应用程序从远程来源(如套接字)获取输入。当读取套接字时,如果当前没有可用数据,那么对SocketInputStream.read()的调用将会阻塞,直到有可用数据为止。

如果单线程程序要读取套接字,而套接字另一端的实体并未发送任何数据,那么该程序只会永远等待,而不执行其它处理。相反,程序可以轮询套接字,查看是否有可用数据,但通常不会使用这种做法,因为会影响性能。

但是,如果您创建了一个线程来读取套接字,那么当这个线程等待套接字中的输入时,主线程就可以执行其它任务。您甚至可以创建多个线程,这样就可以同时读取多个套接字。这样,当有可用数据时,您会迅速得到通知(因为正在等待的线程被唤醒),而不必经常轮询以检查是否有可用数据。使用线程等待套接字的代码也比轮询更简单、更不易出错。

是不是感觉,这样子,多线程好爽,也好简单呀,但是,简单却有风险

虽然 Java 线程工具非常易于使用,但当您创建多线程程序时,应该尽量避免一些风险。

当多个线程访问同一数据项(如静态字段、可全局访问对象的实例字段或共享集合)时,需要确保它们协调了对数据的访问,这样它们都可以看到数据的一致视图,而且相互不会干扰另一方的更改。为了实现这个目的,Java 语言提供了两个关键字:synchronizedvolatile。(稍后再详细讲这两兄弟,因为他们太重要了)

当从多个线程中访问变量时,必须确保对该访问正确地进行了同步对于简单变量,将变量声明成volatile也许就足够了,但在大多数情况下,需要使用同步。

如果您将要使用同步来保护对共享变量的访问,那么必须确保在程序中所有访问该变量的地方都使用同步

同时还需要注意:

虽然线程可以大大简化许多类型的应用程序,过度使用线程可能会危及程序的性能及其可维护性。线程消耗了资源。因此,在不降低性能的情况下,可以创建的线程的数量是有限制的。

尤其在单处理器系统中,使用多个线程会使主要消耗 CPU 资源的程序运行得更快。

还是举个代码例子吧,毕竟是程序员嘛

以下示例使用两个线程,一个用于计时,一个用于执行实际工作。主线程使用非常简单的算法计算素数

在它启动之前,它创建并启动一个计时器线程,这个线程会休眠十秒钟,然后设置一个主线程要检查的标志。十秒钟之后,主线程将停止。请注意,共享标志被声明成volatile。


小结

Java 语言包含了内置在语言中的功能强大的线程工具。您可以将线程工具用于:

增加 GUI 应用程序的响应速度

利用多处理器系统

当程序有多个独立实体时,简化程序逻辑

在不阻塞整个程序的情况下,执行阻塞 I/O

当使用多个线程时,必须谨慎,遵循在线程之间共享数据的规则,我们将在共享对数据的访问中讨论这些规则。所有这些规则归结为一条基本原则:不要忘了同步

线程的生命

创建线程

在 Java 程序中创建线程有几种方法。每个 Java 程序至少包含一个线程:主线程。其它线程都是通过Thread构造器或实例化继承类Thread的类来创建的。

Java 线程可以通过直接实例化Thread对象或实例化继承Thread的对象来创建其它线程。在线程基础中的示例(其中,我们在十秒钟之内计算尽量多的素数)中,我们通过实例化CalculatePrimes类型的对象(它继承了Thread),创建了一个线程。

当我们讨论 Java 程序中的线程时,也许会提到两个相关实体:完成工作的实际线程或代表线程的Thread对象。正在运行的线程通常是由操作系统创建的;Thread对象是由 Java VM 创建的,作为控制相关线程的一种方式。

启动线程

在一个线程对新线程的Thread对象调用start()方法之前,这个新线程并没有真正开始执行。Thread对象在其线程真正启动之前就已经存在了,而且其线程退出之后仍然存在。这可以让您控制或获取关于已创建的线程的信息,即使线程还没有启动或已经完成了。

通常在构造器中通过start()启动线程并不是好主意。这样做,会把部分构造的对象暴露给新的线程。如果对象拥有一个线程,那么它应该提供一个启动该线程的start()或init()方法,而不是从构造器中启动它。

结束线程

线程会以以下三种方式之一结束:

线程到达其run()方法的末尾。

线程抛出一个未捕获到的Exception或Error。

另一个线程调用一个弃用的stop()方法。弃用是指这些方法仍然存在,但是您不应该在新代码中使用它们,并且应该尽量从现有代码中除去它们。

通常用interrupt()方法,这个方法是中断线程的方法

当 Java 程序中的所有线程都完成时,程序就退出了。

加入线程

Thread API 包含了等待另一个线程完成的方法:join()方法。当调用Thread.join()时,调用线程将阻塞,直到目标线程完成为止。

Thread.join()通常由使用线程的程序使用,以将大问题划分成许多小问题,每个小问题分配一个线程。嗯,例子后面补充,先讲概念。

调度

除了何时使用Thread.join()Object.wait()外,线程调度和执行的计时是不确定的。如果两个线程同时运行,而且都不等待,您必须假设在任何两个指令之间,其它线程都可以运行并修改程序变量。如果线程要访问其它线程可以看见的变量,如从静态字段(全局变量)直接或间接引用的数据,则必须使用同步以确保数据一致性。(这个大家,自己写一段程序就可以实验了)

休眠

Thread API 包含了一个sleep()方法,它将使当前线程进入等待状态,直到过了一段指定时间,或者直到另一个线程对当前线程的Thread对象调用了Thread.interrupt(),从而中断了线程。当过了指定时间后,线程又将变成可运行的,并且回到调度程序的可运行线程队列中。

如果线程是由对Thread.interrupt()的调用而中断的,那么休眠的线程会抛出InterruptedException,这样线程就知道它是由中断唤醒的,就不必查看计时器是否过期。

Thread.yield()方法就象Thread.sleep()一样,但它并不引起休眠,而只是暂停当前线程片刻,这样其它线程就可以运行了。在大多数实现中,当较高优先级的线程调用Thread.yield()时,较低优先级的线程就不会运行。

CalculatePrimes示例使用了一个后台线程计算素数,然后休眠十秒钟。当计时器过期后,它就会设置一个标志,表示已经过了十秒。

守护程序线程

我们提到过当 Java 程序的所有线程都完成时,该程序就退出,但这并不完全正确。隐藏的系统线程,如垃圾收集线程和由 JVM 创建的其它线程会怎么样?我们没有办法停止这些线程。如果那些线程正在运行,那么 Java 程序怎么退出呢?

这些系统线程称作守护程序线程Java 程序实际上是在它的所有非守护程序线程完成后退出的

任何线程都可以变成守护程序线程。可以通过调用Thread.setDaemon()方法来指明某个线程是守护程序线程。您也许想要使用守护程序线程作为在程序中创建的后台线程,如计时器线程或其它延迟的事件线程,只有当其它非守护程序线程正在运行时,这些线程才有用。

说了这么多,来个例子:用多个线程分解大任务

本来想截图的,但是,不知道为什么,上传不了,就上代码吧

public class TenThreads {

private static class WorkerThread extends Thread {

int max = Integer.MIN_VALUE;

int[] ourArray;

public WorkerThread(int[] ourArray) {

this.ourArray = ourArray;

}

// Find the maximum value in our particular piece of the array

public void run() {

for (int i = 0; i < ourArray.length; i++)

max = Math.max(max, ourArray[i]);

}

public int getMax() {

return max;

}

}

public static void main(String[] args) {

WorkerThread[] threads = new WorkerThread[10];

int[][] bigMatrix = getBigHairyMatrix();

int max = Integer.MIN_VALUE;

// Give each thread a slice of the matrix to work with

for (int i=0; i < 10; i++) {

threads[i] = new WorkerThread(bigMatrix[i]);

threads[i].start();

}

// Wait for each thread to finish

try {

for (int i=0; i < 10; i++) {

threads[i].join();

max = Math.max(max, threads[i].getMax());

}

}

catch (InterruptedException e) {

// fall through

}

System.out.println("Maximum value was " + max);

}

}

小结

就象程序一样,线程有生命周期:它们启动执行,然后完成。一个程序或进程也许包含多个线程,而这些线程看来互相单独地执行。

线程是通过实例化Thread对象或实例化继承Thread的对象来创建的,但在对新的Thread对象调用start()方法之前,这个线程并没有开始执行。当线程运行到其run()方法的末尾或抛出未经处理的异常时,它们就结束了。

sleep()方法可以用于等待一段特定时间;而join()方法可能用于等到另一个线程完成。

共享对数据的访问

线程,我之前有举例,说线程可以看作快递员,既然是快递员,就要送包裹,打包包裹,这个包裹,就是我们说的数据。而线程应用中,核心也是对数据的处理和访问。

共享变量

要使多个线程在一个程序中有用,它们必须有某种方法可以互相通信共享它们的结果。

让线程共享其结果的最简单方法是使用共享变量。它们还应该使用同步来确保值从一个线程正确传播到另一个线程,以及防止当一个线程正在更新一些相关数据项时,另一个线程看到不一致的中间结果。

之前在计算素数的示例使用了一个共享布尔变量,用于表示指定的时间段已经过去了。这说明了在线程间共享数据最简单的形式是:轮询共享变量以查看另一个线程是否已经完成执行某项任务。

存在于同一个内存空间中的所有线程

正如前面讨论过的,线程与进程有许多共同点,不同的是线程与同一进程中的其它线程共享相同的进程上下文,包括内存。这非常便利,但也有重大责任。只要访问共享变量(静态或实例字段),线程就可以方便地互相交换数据,但线程还必须确保它们以受控的方式访问共享变量,以免它们互相干扰对方的更改。

任何线程可以访问所有其作用域内的变量,就象主线程可以访问该变量一样。素数示例使用了一个公用实例字段,叫做finished,用于表示已经过了指定的时间。当计时器过期时,一个线程会写这个字段;另一个线程会定期读取这个字段,以检查它是否应该停止。注:这个字段被声明成volatile,这对于这个程序的正确运行非常重要。在本章的后面,我们将看到原因。

受控访问的同步

为了确保可以在线程之间以受控方式共享数据,Java 语言提供了两个关键字:synchronizedvolatile

Synchronized有两个重要含义:它确保了一次只有一个线程可以执行代码的受保护部分(互斥,mutual exclusion 或者说 mutex),而且它确保了一个线程更改的数据对于其它线程是可见的(更改的可见性)。

如果没有同步,数据很容易就处于不一致状态。例如,如果一个线程正在更新两个相关值(比如,粒子的位置和速率),而另一个线程正在读取这两个值,有可能在第一个线程只写了一个值,还没有写另一个值的时候,调度第二个线程运行,这样它就会看到一个旧值和一个新值。同步让我们可以定义必须原子地运行的代码块,这样对于其他线程而言,它们要么都执行,要么都不执行。

同步的原子执行互斥方面类似于其它操作环境中的临界段的概念。

确保共享数据更改的可见性

确保共享数据更改的可见性

同步可以让我们确保线程看到一致的内存视图。

处理器可以使用高速缓存加速对内存的访问(或者编译器可以将值存储到寄存器中以便进行更快的访问)。在一些多处理器体系结构上,如果在一个处理器的高速缓存中修改了内存位置,没有必要让其它处理器看到这一修改,直到刷新了写入器的高速缓存并且使读取器的高速缓存无效。

这表示在这样的系统上,对于同一变量,在两个不同处理器上执行的两个线程可能会看到两个不同的值!这听起来很吓人,但它很常见。它只是表示在访问其它线程使用或修改的数据时,必须遵循某些规则。

Volatile比同步更简单,只适合于控制对基本变量(整数、布尔变量等)的单个实例的访问。当一个变量被声明成volatile,任何对该变量的写操作都会绕过高速缓存,直接写入主内存,而任何对该变量的读取也都绕过高速缓存,直接取自主内存。这表示所有线程在任何时候看到的volatile变量值都相同。

如果没有正确的同步,线程可能会看到旧的变量值,或者引起其它形式的数据损坏。

用锁保护的原子代码块

Volatile对于确保每个线程看到最新的变量值非常有用,但有时我们需要保护比较大的代码片段,如涉及更新多个变量的片段。

同步使用监控器(monitor)或锁的概念,以协调对特定代码块的访问。

每个 Java 对象都有一个相关的锁。同一时间只能有一个线程持有 Java 锁。当线程进入synchronized代码块时,线程会阻塞并等待,直到锁可用,当它可用时,就会获得这个锁,然后执行代码块。当控制退出受保护的代码块时,即到达了代码块末尾或者抛出了没有在synchronized块中捕获的异常时,它就会释放该锁。

这样,每次只有一个线程可以执行受给定监控器保护的代码块。从其它线程的角度看,该代码块可以看作是原子的,它要么全部执行,要么根本不执行。

Java 锁定

Java 锁定合并了一种互斥形式。每次只有一个线程可以持有锁。锁用于保护代码块或整个方法,必须记住是锁的身份保护了代码块,而不是代码块本身,这一点很重要。一个锁可以保护许多代码块或方法。

反之,仅仅因为代码块由锁保护并不表示两个线程不能同时执行该代码块。它只表示如果两个线程正在等待相同的锁,则它们不能同时执行该代码。

例子:

这个例子中,两个线程都能够执行set方法,其实,可以这样想,就是,你是两个对象,在执行各自的方法。当然,其实并不是我说的这样,只是,以现在的知识不知道该如何解释,希望懂的朋友可以帮忙解释一下。

同步的方法

创建synchronized块的最简单方法是将方法声明成synchronized。这表示在进入方法主体之前,调用者必须获得锁:

public class Point {

public synchronized void setXY(int x, int y) {

this.x = x;

this.y = y;

}

}

对于普通的synchronized方法,这个锁是一个对象,将针对它调用方法。对于静态synchronized方法,这个锁是与Class对象相关的监控器,在该对象中声明了方法。

仅仅因为setXY()被声明成synchronized并不表示两个不同的线程不能同时执行setXY(),只要它们调用不同的Point实例的setXY()就可同时执行。对于一个Point实例,一次只能有一个线程执行setXY(),或Point的任何其它synchronized方法。

同步的块

synchronized块的语法比synchronized方法稍微复杂一点,因为还需要显式地指定锁要保护哪个块。Point的以下版本等价于前一页中显示的版本:

public class Point {

public void setXY(int x, int y) {

synchronized (this) {

this.x = x;

this.y = y;

                             }

                }

}

使用this引用作为锁很常见,但这并不是必需的。这表示该代码块将与这个类中的synchronized方法使用同一个锁。

由于同步防止了多个线程同时执行一个代码块,因此性能上就有问题,即使是在单处理器系统上。最好在尽可能最小的需要保护的代码块上使用同步。

访问局部(基于堆栈的)变量从来不需要受到保护,因为它们只能被自己所属的线程访问。

大多数类并没有同步

因为同步会带来小小的性能损失,大多数通用类,如java.util中的 Collection 类,不在内部使用同步。这表示在没有附加同步的情况下,不能在多个线程中使用诸如HashMap这样的类。

通过每次访问共享集合中的方法时使用同步,可以在多线程应用程序中使用 Collection 类。对于任何给定的集合,每次必须用同一个锁进行同步。通常可以选择集合对象本身作为锁。

下一页中的示例类SimpleCache显示了如何使用HashMap以线程安全的方式提供高速缓存。但是,通常适当的同步并不只是意味着同步每个方法。

Collections类提供了一组便利的用于List、Map和Set接口的封装器。您可以用Collections.synchronizedMap封装Map,它将确保所有对该映射的访问都被正确同步。

如果类的文档没有说明它是线程安全的,那么您必须假设它不是。

如以下代码样本所示,SimpleCache.java使用HashMap为对象装入器提供了一个简单的高速缓存。load()方法知道怎样按对象的键装入对象。在一次装入对象之后,该对象就被存储到高速缓存中,这样以后的访问就会从高速缓存中检索它,而不是每次都全部地装入它。对共享高速缓存的每个访问都受到synchronized块保护。由于它被正确同步,所以多个线程可以同时调用getObject和clearCache方法,而没有数据损坏的风险。


小结

由于线程执行的计时是不确定的,我们需要小心,以控制线程对共享数据的访问。否则,多个并发线程会互相干扰对方的更改,从而损坏数据,或者其它线程也许不能及时看到对共享数据的更改。

通过使用同步来保护对共享变量的访问,我们可以确保线程以可预料的方式与程序变量进行交互。

每个 Java 对象都可以充当锁,synchronized块可以确保一次只有一个线程执行由给定锁保护的synchronized代码。


未完待续。。。。。

继续上一次的接着说,本来想一次写完的,但是,比较懒吧,也想放松一下,周末,就休息了。

今天就补全剩下的一部分吧


同步详细信息

(这一部分,相对前面要难一点,做好心理准备,可能需要多看几次)

互斥

共享对数据的访问中,我们讨论了synchronized块的特征,并在实现典型互斥锁(即,互斥或临界段)时说明了它们,其中每次只有一个线程可以执行受给定锁保护的代码块。(换句话说,就是互斥,其实就是相互排斥的意思,只有一个线程可以持有,不能存在多个线程都持有,这里的持有可以理解为使用权限,举个例子,你打电话时,只能和一个人保持通话。)

互斥是同步所做工作的重要部分,但同步还有其它几种特征,这些特征对于在多处理器系统上取得正确结果非常重要。

可见性

除了互斥,同步(如volatile)强制某些可见性约束。当对象获取锁时,它首先使自己的高速缓存无效,这样就可以保证直接从主内存中装入变量。

(这里,可能有同学就不理解了,因为,他们不明白,高速缓存和内存的区别,这里,如果想要了解的,自行百度。我就简要说一下:计算机读取数据的过程,首次读取,是硬盘读到内存,然后从内存读到cpu,但是,后来人们发现这样太慢了,能不能快一点呢?于是,就从cpu中开辟了一个“空间”,这个空间就叫缓存,把一部分数据(这里是有条件的,条件就不说了)就放到这个缓存中,然后,再次读取这部分数据时,cpu就直接从缓存读取了。)

同样,在对象释放锁之前,它会刷新其高速缓存,强制使已做的任何更改都出现在主内存中。

这样,会保证在同一个锁上同步的两个线程看到在synchronized块内修改的变量的相同值。(加了synchronized之后,所有线程看到这个块都是同一个块。)

什么时候必须同步?

要跨线程维护正确的可见性,只要在几个线程之间共享非 final 变量,就必须使用synchronized(或volatile)以确保一个线程可以看见另一个线程做的更改。

可见性同步的基本规则是在以下情况中必须同步:

读取上一次可能是由另一个线程写入的变量

写入下一次可能由另一个线程读取的变量

用于一致性的同步

用于一致性的同步

除了用于可见性的同步,从应用程序角度看,您还必须用同步来确保一致性得到了维护。当修改多个相关值时,您想要其它线程原子地看到这组更改 ― 要么看到全部更改,要么什么也看不到。这适用于相关数据项(如粒子的位置和速率)和元数据项(如链表中包含的数据值和列表自身中的数据项的链)。例子

不变性和 final 字段(这是我最喜欢的一个东西之一,因为他太好了,不需要我做任何处理)

许多 Java 类,包括String、Integer和BigDecimal,都是不可改变的:一旦构造之后,它们的状态就永远不会更改。如果某个类的所有字段都被声明成final,那么这个类就是不可改变的。(实际上,许多不可改变的类都有非 final 字段,用于高速缓存以前计算的方法结果,如String.hashCode(),但调用者看不到这些字段。)

不可改变的类使并发编程变得非常简单。因为不能更改它们的字段,所以就不需要担心把状态的更改从一个线程传递到另一个线程。在正确构造了对象之后,可以把它看作是常量。

同样,final 字段对于线程也更友好。因为 final 字段在初始化之后,它们的值就不能更改,所以当在线程之间共享 final 字段时,不需要担心同步访问。

什么时候不需要同步(这是我非常喜欢的东西,理由参考前一节)

在某些情况中,您不必用同步来将数据从一个线程传递到另一个,因为 JVM 已经隐含地为您执行同步。这些情况包括:

由静态初始化器(在静态字段上或static{}块中的初始化器)初始化数据时

访问 final 字段时

在创建线程之前创建对象时(这里,稍微讲一个道理,先有了对象,不需要管同步,可以这么理解,没有人来共享这个资源,自然就不涉及到同步了)

线程可以看见它将要处理的对象时

死锁(我最讨厌的东西,没有之一)

只要您拥有多个进程,而且它们要争用对多个锁的独占访问,那么就有可能发生死锁。如果有一组进程或线程,其中每个都在等待一个只有其它进程或线程才可以执行的操作,那么就称它们被死锁了。

最常见的死锁形式是当线程 1 持有对象 A 上的锁,而且正在等待与 B 上的锁,而线程 2 持有对象 B 上的锁,却正在等待对象 A 上的锁。这两个线程永远都不会获得第二个锁,或者释放第一个锁。它们只会永远等待下去。

要避免死锁,应该确保在获取多个锁时,在所有的线程中都以相同的顺序获取锁。

是不是,很讨厌,就像两个人争东西一样,你不给我,我也不给你,然后,要么一直耗着,要么打一架,打赢了就得到锁,打输了,就挂了。其实,避免这个问题,从思路上说,就是注意顺序,大家排队领东西。

性能考虑事项

关于同步的性能代价有许多说法 ― 其中有许多是错的。同步,尤其是争用的同步,确实有性能问题,但这些问题并没有象人们普遍怀疑的那么大。

许多人都使用别出心裁但不起作用的技巧以试图避免必须使用同步,但最终都陷入了麻烦。一个典型的示例是双重检查锁定模式。这种看似无害的结构据说可以避免公共代码路径上的同步,但却令人费解地失败了,而且所有试图修正它的尝试也失败了。

在编写并发代码时,除非看到性能问题的确凿证据,否则不要过多考虑性能。瓶颈往往出现在我们最不会怀疑的地方。投机性地优化一个也许最终根本不会成为性能问题的代码路径 ― 以程序正确性为代价 ― 是一桩赔本的生意。

所以,简单来说,在初期,不要考虑什么性能问题。当然也因为,在初期,基本遇不到多少线程同步带来的性能问题。

同步准则

当编写synchronized块时,有几个简单的准则可以遵循,这些准则在避免死锁和性能危险的风险方面大有帮助:

使代码块保持简短。Synchronized块应该简短 ― 在保证相关数据操作的完整性的同时,尽量简短。把不随线程变化的预处理和后处理移出synchronized块。

不要阻塞。不要在synchronized块或方法中调用可能引起阻塞的方法,如InputStream.read()。

在持有锁的时候,不要对其它对象调用方法。这听起来可能有些极端,但它消除了最常见的死锁源头。

这一部分没有小结,因为,东西并没有讲完,这时候总结,为时尚早。等以后,有机会再补全这一个部分吧。

其它线程 API 详细信息

wait()、notify() 和 notifyAll() 方法

除了使用轮询(它可能消耗大量 CPU 资源,而且具有计时不精确的特征),Object类还包括一些方法,可以让线程相互通知事件的发生。

Object类定义了wait()、notify()和notifyAll()方法。要执行这些方法,必须拥有相关对象的锁。

Wait()会让调用线程休眠,直到用Thread.interrupt()中断它、过了指定的时间、或者另一个线程用notify()或notifyAll()唤醒它。

当对某个对象调用notify()时,如果有任何线程正在通过wait()等待该对象,那么就会唤醒其中一个线程。当对某个对象调用notifyAll()时,会唤醒所有正在等待该对象的线程。

这些方法是更复杂的锁定、排队和并发性代码的构件。但是,notify()和notifyAll()的使用很复杂。尤其是,使用notify()来代替notifyAll()是有风险的。除非您确实知道正在做什么,否则就使用notifyAll()。

与其使用wait()和notify()来编写您自己的调度程序、线程池、队列和锁,倒不如使用util.concurrent包,这是一个被广泛使用的开放源码工具箱,里面都是有用的并发性实用程序。JDK 1.5 将包括java.util.concurrent包;它的许多类都派生自util.concurrent。

随便说一句,如果对线程不是很熟悉,慎用。

线程优先级

Thread API 让您可以将执行优先级与每个线程关联起来。但是,这些优先级如何映射到底层操作系统调度程序取决于实现。在某些实现中,多个 ― 甚至全部 ― 优先级可能被映射成相同的底层操作系统优先级。

在遇到诸如死锁、资源匮乏或其它意外的调度特征问题时,许多人都想要调整线程优先级。但是,通常这样只会把问题移到别的地方。大多数程序应该完全避免更改线程优先级。

所以,不要试图偷懒,用这种方式,去解决线程调度的问题,在程序和做人上,当前问题,就留个当前,不要转移,不然,后果自负。

线程组

ThreadGroup类原本旨在用于把线程集合构造成组。但是,结果证明ThreadGroup并没有那样有用。您最好只使用Thread中的等价方法。

ThreadGroup确实提供了一个有用的功能部件(Thread中目前还没有):uncaughtException()方法。线程组中的某个线程由于抛出了未捕获的异常而退出时,会调用ThreadGroup.uncaughtException()方法。这就让您有机会关闭系统、将一条消息写到日志文件或者重新启动失败的服务。

最后

用了不短不长的时间,整理了这一篇文章,算是对java线程有了一个很基础的认识。线程这个东西,你单独用的时候,并没有什么用,但在整个程序的运行中,他却先得异常重要。如果,把程序看作是我们的国家或者城市,那么线程就可以看作物流,运算,或者快递了。这么一说,相信,就算没接触过程序的人,也会觉得线程是多么的重要了吧。当然,我这里,只是粗浅的聊了聊,希望以后自己完全弄懂线程之后,再来聊聊线程到底是什么,怎么工作,原理是什么吧。

日记本