在并发编程中,有两个基本的执行单元:进程和线程。即使在只有单一的 CPU的计算机系统中,也有许多活动的进程和线程。因此,在任何给定的时刻,同一进程内只有一个线程在实际执行。处理器的处理时间是通过操作系统的时间片在进程和线程中共享的。
1.进程与线程
进程(Process)
进程具有一个独立的执行环境。通常情况下,进程拥有一个完整的、私有的基本运行资源集合。特别地,每个进程都有自己的内存空间。
进程往往被看作是程序或应用的代名词,然而,用户看到的一个单独的应用程序实际上可能是一组相互协作的进程集合。为了便于进程之间的通信,大多数操作系统都支持进程间通信(IPC),如pipes 和sockets。IPC不仅支持同一系统上的通信,也支持不同的系统。
Java虚拟机的大多数实现是单进程的。Java应用可以使用的ProcessBuilder对象创建额外的进程,多进程应用超出了本课的范围。
线程(Thread)
线程有时也被称为轻量级的进程。进程和线程都提供了一个执行环境,但创建一个新的线程比创建一个新的进程需要的资源要少。
线程是在进程中存在的 — 每个进程最少有一个线程。线程共享进程的资源,包括内存和打开的文件。这样提高了效率,但潜在的问题就是线程间的通信。
多线程的执行是Java平台的一个基本特征。每个应用都至少有一个线程 – 或几个,如果算上“系统”线程的话,比如内存管理和信号处理等。但是从程序员的角度来看,启动的只有一个线程,叫主线程。这个线程有能力创建额外的线程,我们将在下一节演示。
这里就已经明确了进程与线程的关系,一个进程至少包含一个线程,而进程是应用程序的基本运行实例。在windows操作系统中执行ctrl+alt+del 键并启动任务管理器就可以查看到当前用户下进程的运行状态,如下图所示:
而在Linux 操作系统下执行ps -ef 或相关命令就也可以查看当前用户下相关的进程信息:
所以无论是Java虚拟机还是其他应用程序,一旦运行必然会产生至少一个进程,如上图所示,我一共执行了3次java HelloWorld 命令,这样就产生了3个JVM 进程,他们之间有着不同的进程号与相关存储空间,所以他们之间是无法进行资源共享的。所以进程间一般是相互隔离的,进程间的相互通信与调用需要特殊手段来实现。
2.多线程的优点
一个进程包含多个线程,在单个进程中同时运行多个线程完成不同的工作就称为多线程。
1)资源利用率更好
一个应用程序需要从本地文件系统中处理多个文件的情景:读取文件需要4秒,处理文件需要2秒。
4秒读取文件A 2秒处理文件A 4秒读取文件B 2秒处理文件B 总处理时间:12秒
从磁盘中读取文件的时候,大部分的CPU时间用于等待磁盘去读取数据(在等待磁盘读取文件的时候,CPU大部分时间是空闲的)。在等待的时间里,CPU非常的空闲。此时利用空闲的CPU可以做一些别的事情。通过改变操作的顺序,就能够更好的使用CPU资源。
4秒读取文件A 4秒读取文件B + 2秒处理文件A 2秒处理文件B --------------------- 总处理时间:10秒
CPU首先读取文件A,然后开始读取文件B。在读取文件B的时间里,CPU可以利用CPU空闲去处理已读取的文件A。
所以在某些耗时等待的操作中,CPU往往可以被利用于其他的工作处理,这样就提高了CPU的利用率,从而提高了应用程序的效率。这些耗时CPU却比较空闲的操作有:磁盘IO、网络的IO、用户输入等等。
2)程序设计更简单
在单线程应用程序中,如果你想编写程序手动处理上面所提到的读取和处理的顺序,你必须记录每个文件读取和处理的状态。而利用多线程,你可以启动两个线程,每个线程处理一个文件的读取和操作。线程会在等待磁盘读取文件的过程中被阻塞。在等待的时间,其他的线程能够使用CPU去处理已经读取完的文件。其结果就是,磁盘总是在繁忙地读取不同的文件到内存中。这会带来磁盘和CPU利用率的提升。而且每个线程只需要记录一个文件,因此这种方式也很容易编程实现。
3)程序响应更快
将一个单线程应用程序变成多线程应用程序的另一个常见的目的是实现一个响应更快的应用程序。设想一个服务器应用,它在某一个端口监听进来的请求。当一个请求到来时,它去处理这个请求,然后再返回去监听。
如果一个请求需要占用大量的时间来处理,在这段时间内新的客户端就无法发送请求给服务端。只有服务器在监听的时候,请求才能被接收。另一种设计是,监听线程把请求传递给工作者线程(worker thread),然后立刻返回去监听。而工作者线程则能够处理这个请求并发送一个回复给客户端。
这种方式,服务端线程能够迅速地返回去监听。因此,更多的客户端能够发送请求给服务端,这个服务也变得响应更快。
并不是说任何程序都需要用到多线程,多线程的程序也会带来如:设计复杂、上下文切换的开销、增加资源消耗等缺点。
3.Java中多线程的实现方法
在Java中有两种基本的线程实现方式,一种是继承Thread 类;另一种是实现Runnable 接口:
1)继承Thread 类并重写Thread 类的run方法
public class ThreadA extends Thread { @Override public void run() { System.out.println(this.getName()); } public static void main(String[] args) { Thread t = new ThreadA(); // 启动线程 t.start(); } } //打印结果: Thread-0
2)实现Runnable 并实现Runnable 接口的run方法
public class ThreadB implements Runnable { public void run() { System.out.println(Thread.currentThread().getName()); } public static void main(String[] args) { Thread t = new Thread(new ThreadB()); // 启动线程 t.start(); } } //打印结果: Thread-0
上述两个例子都需要调用Thread.start()方法来启动一个新的线程相对来说,第二种更加通用,因为 Runnable对象可以继承于其他类(Java类只支持单继承,接口支持多继承,当一个类继承于 Thread类后,就无法继承与其他类)。第一种方法更易于在简单的应用程序中使用,但它的局限就是:你的任务类必须是 Thread的子类。Thread类定义了一些对线程管理十分有用的的方法。在这些方法中,有一些静态方法可以给当前线程调用,它们可以提供一些有关线程的信息,或者影响线程的状态。而其他一些方法可以由其他线程进行调用,用于管理线程和 Thread对象。
在单CPU或不支持多线程并发技术的操作系统中,多个线程的运行其实不是并发的,这是因为最开始,线程只是用于分配单个处理器的处理时间的一种工具。但随着硬件科技的发展操作系统本身已经支持多个处理器,如果每个线程就都可以分配到一个单独的处理器工作,那么就真正进入了“并发运算”状态,但是线程间能否进行“并行”或“串行”运行需要操作系统说的算。
4.线程的状态
当然线程不会只有运行和等待两种状态,线程一般具有六种基本状态,分别为:
NEW:至今尚未启动的线程处于这种状态。
RUNNABLE:可运行或正在运行线程的线程状态。
BLOCKED:受阻塞并等待某个监视器锁的线程状态。
WAITING:无限期地等待另一个线程来执行唤醒操作的状态。
TIMED_WAITING:指定时间后会被自动唤醒的WAITING状态。
TERMINATED:已退出的线程处于这种状态。
1)当我们新建一个线程后该线程的状态则为 NEW,此时该线程尚未被执行,所以 NEW状态表明该线程是新建还未执行过的线程。
2)因为新创建的线程不会自动运行,我们需要调用线程的 start() 方法来运行该线程。此后我们就正式启动了该线程,当 start()方法执行完毕后本线程就进入了 RUNNABLE状态,说明该线程已经就绪随时可以被执行。处于可运行状态的线程已经在 Java虚拟机中运行了,但该线程的 run() 方法并不一定会立即执行,线程可能还需要等待操作系统中的其他资源,比如与其他 RUNNABLE状态线程竞争,与 WAITING状态线程竞争。但一旦该线程获取到了CPU资源则会立即执行 run() 方法执行其中的代码。
其实RUNNABLE 可以理解为两种状态的统称:RUNNABLE与 RUNNING(注意 RUNNING 并不是标准状态)。因为在单 CPU的计算机系统中,无法同时运行多个线程,因此可能有多个线程已经处于可运行(RUNNABLE )状态,但同一个时刻却只能有一个线程处于运行(RUNNING)状态。对于多个处于可运行状态的线程是由 CPU时间片轮转等不同算法来对线程进行调度管理的。
正是因为多线程间需要竞争 CPU资源,所以处于 RUNNING 状态的线程未必会把该线程下 run() 方法的所有代码都执行完,可能只执行到了一半,CPU资源就被其他处于 RUNNABLE 状态的线程抢走了,就这样循环往复的竞争下去,直到各自都完成了 run() 方法的全部代码。
3)当一个线程的 run() 方法完整的执行完毕后或抛出异常、错误,该线程就会进入 TERMINATED 状态,证明该线程已经退出不需要再继续执行。
4)很多情况下线程并不会非常顺利的按照之前三个步骤顺序执行,当运行状态中的线程调用Object.wait(), Thread.join(),LockSupport.park()等方法后线程就会进入 WAITING状态。此时运行时线程(本线程)将放弃已经获取的 CPU等相关资源进入等待区(稍后会讲)。处于等待状态的线程不会主动去竞争 CPU资源,直到收到明确的唤醒命令:notify 或notifyAll。
5)与 WAITING状态不同,TIMED_WAITING状态是由于Thread.sleep(时间)、Object.wait(时间) 、Thread.join(时间)、LockSupport.parkNanos、LockSupport.parkUntil等操作而产生的,这些操作明确的指定了等待的时间,当等待时间一过该线程就会自动被唤醒。
6)BLOCKED是受阻塞并且正在等待监视器锁的某一线程的线程状态,也就是说 BLOCKED状态出现在同步的方法与对象中调用了 wait()方法的情况,如果几个线程都在某一同步资源上竞争,同一时间只会有一个线程获取到该资源的监视器锁,其他线程此时都在等待获取监视器锁,所以这些等待获取锁的线程状态就为阻塞状态(BLOCKED)。
5.线程优先级
每个线程都有一个优先级,分布在Thread.MIN_PRIORITY和Thread.MAX_PRIORITY之间(分别为1和10)。默认情况下,新创建的线程都拥有和创建它的线程相同的优先级。main方法所关联的初始化线程拥有一个默认的优先级,这个优先级是 Thread.NORM_PRIORITY (5)线程的当前优先级可以通过 getPriority方法获得。
线程的优先级可以通过setPriority方法来动态的修改,一个线程的最高优先级由其所在的线程组限定。
在线程创建后可以设置它的优先级
Thread a = new Thread(); a.setPriority(1-10);
当可运行的线程数超过了可用的CPU数目的时候,线程调度器更偏向于去执行那些拥有更高优先级的线程。具体的策略因平台而异。比如有些Java虚拟机实现总是选择当前优先级最高的线程执行。有些虚拟机实现将Java中的十个优先级映射到系统所支持的更小范围的优先级上,因此,拥有不同优先级的线程可能最终被同等对待。还有些虚拟机会使用老化策略(随着时间的增长,线程的优先级逐渐升高)动态调整线程优先级,另一些虚拟机实现的调度策略会确保低优先级的线程最终还是能够有机会运行。设置线程优先级可以影响在同一台机器上运行的程序之间的调度结果,但是这不是必须的。
线程优先级对语义和正确性没有任何的影响。特别是,优先级管理不能用来代替锁机制。优先级仅仅是用来表明哪些线程是重要紧急的,当存在很多线程在激励进行CPU资源竞争的情况下,线程的优先级标识将会显得非常有用。比如,在ParticleApplet中将particle animation线程的优先级设置的比创建它们的applet线程低,在某些系统上能够提高对鼠标点击的响应,而且不会对其他功能造成影响。但是即使setPriority方法被定义为空实现,程序在设计上也应该保证能够正确执行(尽管可能会没有响应)。
下面这个表格列出不同类型任务在线程优先级设定上的通常约定。在很多并发应用中,在任一指定的时间点上,只有相对较少的线程处于可执行的状态(另外的线程可能由于各种原因处于阻塞状态),在这种情况下,没有什么理由需要去管理线程的优先级。另一些情况下,在线程优先级上的调整可能会对并发系统的调优起到一些作用。
范围 | 用途 |
10 | Crisis management(应急处理) |
7-9 | Interactive, event-driven(交互相关,事件驱动) |
4-6 | IO-bound(IO限制类) |
2-3 | Background computation(后台计算) |
1 | Run only if nothing else can(仅在没有任何线程运行时运行的) |
6. 线程的各种操作
1)启动线程
我们已经知道使用 Thread.start()方法来启动线程,但是在 Thread类中还有一个 run()方法,将 start()方法替换成 run()方法程序依然会正常运行,但这两种方法的作用却是不同。
利用继承 Thread类的方式实现的线程:
public class ThreadA extends Thread { @Override public void run() { System.out.println(this.getName()); System.out.println(this.getId()); } public static void main(String[] args) { System.out.println(Thread.currentThread().getName()); System.out.println(Thread.currentThread().getId()); Thread t = new ThreadA(); System.out.println(t.getName()); System.out.println(t.getId()); // 启动线程 t.run(); } } //run()打印结果: main 1 Thread-0 9 main 1 //start()打印结果: main 1 Thread-0 9 Thread-0 9
因为 run()方法已经被重写,所以此处调用 run()方法其实就是调用重写的方法。
利用实现接口 Runnable方式实现线程:
public class ThreadB implements Runnable { public void run() { System.out.println(Thread.currentThread().getName()); System.out.println(Thread.currentThread().getId()); } public static void main(String[] args) { System.out.println(Thread.currentThread().getName()); System.out.println(Thread.currentThread().getId()); Thread t = new Thread(new ThreadB()); System.out.println(t.getName()); System.out.println(t.getId()); // 启动线程 t.run(); } } //run()打印结果: main 1 Thread-0 9 main 1 //start()打印结果: main 1 Thread-0 9 Thread-0 9
发现这种方式使用 run()方法也同样调用的是实现类的 run()方法。
所以我们就需要了解 start()方法与 run()方法的区别。
以下是start()方法的源代码:
public synchronized void start() { if (threadStatus != 0 || this != me) throw new IllegalThreadStateException(); group.add(this); start0(); if (stopBeforeStart) { stop0(throwableFromStop); }
start()方法首先会判断线程状态是否为0(0代表未启动)和线程对象是否正确。如已经启动则会抛出异常,所以我们无法调用两次 start()方法启动两次线程。然后 start()方法会将此线程加入到 ThreadGroup中,可以理解为加入到线程组中,接着调用本地方法启动线程。注意此时线程的状态就已经为可运行状态,随时可以被运行,但仍有可能还未被运行,如果在获取CPU资源运行前被调用了停止指令,start()方法还会去调用本地停止方法停止该线程。
所以执行了start()方法并不一定能保证线程中的 run()方法立刻被执行,需要与其他相关线程竞争资源才会被运行。
以下是 run()方法的源代码:
public void run() { if (target != null) { target.run(); } }
很简单只有几行代码,其中 target是 Runnable类型,如果 target实例存在,则直接执行 Runnable 实现的 run方法,也就是 ThreadB的 run()方法。换句话说 ThreadB的 run()方法被当做一个普通方法被执行了。API中写道:如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回。
所以启动线程必须调用 start()方法,而 run()方法并不会启动线程,而是直接调用要运行线程中的 run()方法。
2)暂停线程
在线程运行过程中随时会遇到暂停的情况,暂停线程有两种方式使用 sleep()方法和使用 wait()方法。
使用Thread.sleep()方法可以暂停当前线程一段时间。这是一种使处理器时间可以被其他线程或者运用程序使用的有效方式。sleep()方法还可以用于调整线程执行节奏和等待其他有执行时间需求的线程。
在Thread中有两个不同的sleep()方法,一个使用毫秒表示休眠的时间,而另一个是用纳秒。由于操作系统的限制休眠时间并不能保证十分精确。休眠周期可以被 interrups所终止。不管在何种情况下,我们都不应该假定调用了 sleep()方法就可以将一个线程暂停一个十分精确的时间周期。
另一种暂停方式就是使用 Object.wait()方法,wait()方法会导致该运行中的线程放弃持有监视器并进入等待区等待,直至有收到唤醒指令被唤醒为止。
sleep与wait最大的区别就是sleep的使用不会导致该线程失去再次运行的机会,该线程仍然会霸占(持有)着监视器,等到睡眠时间结束线程会继续执行。而wait方法会使得该线程放弃继续运行的机会,并进入等待区等待,这个线程如果一直不被唤醒,那么它将永远不会再执行。
就犹如排在你前面办业务的壮汉等着等着居然睡着了,而你却又不敢上前,就只能等他醒过来,他办完了你再去。
3)中断线程
中断是给线程的一个指示,告诉它应该停止正在做的事并去做其他事情。一个线程究竟要怎么响应中断请求取决于程序员,不过让其终止是很普遍的做法。
一个线程通过调用对被中断线程的 Thread对象的 interrupt()方法,发送中断信号。interrupt()方法不会中断一个正在运行的线程。它的作用是,在线程受到阻塞时抛出一个中断信号,这样线程就得以退出阻塞的状态。更确切的说,如果线程被Object.wait, Thread.join和Thread.sleep三种方法之一阻塞,那么,它将接收到一个中断异常(InterruptedException),从而提早地终结被阻塞状态。
4)join
Join()方法可以让一个线程等待另一个线程执行完成。若t1是一个正在执行的 Thread对象:
t1.join();
将会使当前线程暂停执行并等待t1执行完成。重载的join()方法可以让开发者自定义等待周期。然而,和sleep()方法一样join()方法依赖于操作系统的时间处理机制,你不能假定join()方法将会精确的等待你所定义的时长。
如同sleep()方法,join()方法响应中断并在中断时抛出InterruptedException。
7.线程监视器
Java 中的监视器支持两种线程:互斥和协作。Java 虚拟机通过对象锁来实现互斥,允许多个线程在同一共享数据上独立而互不干扰地工作。协作则是通过 Object 类的wait 方法和notify 方法来实现,允许多个线程为了同一目标而共同工作。
前面提到的第一种同步——互斥,可以在多线程环境中互斥地执行一段被称作监视区域的代码。在任何时候,特定监视器上只会有一个线程执行监视区域。通常,互斥只在多个现场需要共享数据或其他资源时显得重要,如果两个线程并没有任何公有数据或资源,它们通常会互不干扰,也就不需要互斥执行。可是如果Java虚拟机实现不是基于时间片的,即使没有线程共享数据,一个不被阻塞的高优先级的线程也将妨碍其他低优先级的线程。高优先级的线程会独占CPU,从而让低优先级的线程永远得不到CPU时间和执行的机会。
另一种提到的被监视器所支持的同步是协作。互斥帮助线程在访问共享数据时不被其他线程干扰,而协作则是帮助多线程间共同工作。
我们可以将监视器比作一桩建筑,在这座建筑中有三间房间,其中一个特殊房间存放着数据与资源,但是这个房间只允许一个线程进入,并且一旦线程进入该房间,那么该线程就全权拥有了此房间,到它离开之前,该线程可以独占地访问房间中的全部数据资源。如果将房间概念换成监视器,则进入建筑就会被称为——进入监视器;进入特殊房间称为——获得监视器;占据特殊房间称为——持有监视器;离开特殊房间称为——释放监视器;离开建筑称为——退出监视器。
对于一个监视器来说,监视区域是最小的、不可分割的代码块。也就是说在同一个监视器中,监视区域只会同时被一个线程执行,即使同时有多个并发的线程,监视器会保证在监视区域上同一时间只会执行一个线程。一个线程想要进入监视器唯一的途径就是到达该监视器监视区域的开始处,也就是监视器的入口。而线程想要继续执行线程的 run 方法唯一的途径就是获得监视器,也就是获得特殊房间的所有权。
下图就是Java中所使用的监视器模型:
这种监视器被称为“等待并唤醒”监视器(或称为”发信号并继续“监视器)。监视器分为三个区域:入口区,监视器持有者和等待区,其中入口区和等待区可以同时存在多个线程,而持有者只能有一个。
1)当一个新建的线程调用 start()方法后,该线程就会进入监视器的入口区。
2)此时假设监视器中只进入了刚才那一个线程,那么这个线程就会顺利的获得监视器持有权,进入那个神秘的房间,进行线程中 run()方法的执行,直至 run()方法执行完毕或中途抛出异常中断,则此线程会从监视器的出口退出监视器,该线程的流程就为1-->2-->5。
3)另一种情况就是当线程进入入口区后,发现监视器的所有区域都有线程在等待或执行。只有监视器所有者线程退出监视器或主动的交出监视器持有权后,这些等待的线程才能开始竞争,争夺监视器的所有权。
4)只要监视器持有者不再存在,入口区内所有的线程就都会参与竞争。如果监视器持有者正常退出,则参与竞争的线程只有入口区内的所有线程。如果监视器持有者主动转让持有权,并通过 notify()方法唤醒等待区内的一个线程加入竞争,则竞争者为入口区所有线程与等待区某一线程。如果监视器持有者通过 notifyAll()方法唤醒全部等待区线程加入竞争,则竞争者为入口区所有线程与等待区所有线程。
5)如果是监视器持有者执行了等待命令,那么它就会释放监视器持有权,并进入等待区进行等待,直至后续的监视器持有者唤醒。
6)无论是入口区所有线程竞争,还是入口区与等待区线程竞争,最终结果都是只有一个线程胜出获得监视器的持有权,从而开始执行线程代码。
等待并唤醒监视器之所以还被称为发信号并继续监视器,原因是在一个线程做了唤醒操作(发信号)后,它还会继续持有监视器并继续执行,经过一段时间后,唤醒线程释放监视器,等待线程才会苏醒,所以唤醒与苏醒不会是立刻执行完成的。就犹如去叫醒一个睡梦中的人,虽然你已经试着去叫醒他,但是他却未必能立刻醒来。
等待线程将自身挂起是因为监视器保护数据并不处于它想要继续执行的正确状态。同样,唤醒线程执行唤醒操作后还将继续执行,所以在继续执行的过程中它有可能又修改了数据,这就有可能导致等待线程无法继续工作。还有一种情况就是,第三个线程有可能在唤醒线程释放了监视器后,抢先一步获得了监视器,而且这个线程有可能会修改监视器保护数据的状态。所以一次唤醒操作往往被看成一次“提醒”,告诉等待线程“数据已经是你想要的状态了”。每次等待线程苏醒的时候,它都要再次检查状态,以确保可以继续完成工作。如果它不是所需状态,那么这个线程可能会再次执行等待命令,甚至放弃等待退出监视器。
以上可能不是很好理解,不过没关系,结合下面的文章实例再反复思考就明白了。
8.对象锁
通过之前文章的学习,我们知道堆内存和方法区是被所有线程共享的,所以Java程序需要为两种多线程访问的数据进行协调:堆中的实例变量和方法区中的类变量。因为Java栈中的数据是属于线程私有的,所以程序不需要协调这部分局部变量。
在Java虚拟机中,每个对象和类在逻辑上都是和一个监视器关联的,从 Object类中含有 wait 和 notify 方法就可以看出。对于对象来说,相关联的监视器保护对象的实例变量。对于类来说,监视器保护类的类变量。如果一个对象没有实例变量或一个类没有类变量,相关联的监视器就什么都不监视。
为了实现监视器的排他监视能力,Java虚拟机为每一个对象和类都关联一个锁(也可称为互斥体(mutex))。一个锁就像一种任何时候只允许一个线程“拥有”的特权。线程访问实例变量或者类变量不需要获取锁,但如果线程获取了锁,那么在它释放锁之前,就没有其他线程可以获取同样数据的锁了。
”锁住一个对象“就是获取对象相关联的监视器,这样估计更好理解一些。
类锁实际上用对象锁实现,当Java虚拟机装载一个class文件时,它会创建一个java.lang.Class实例,当锁住一个类的时候,实际上锁住的是那个类的 Class对象。
一个线程可以多次对同一个对象上锁。对于每一个对象来说,Java虚拟机维护一个计数器,记录着对象加了多少次锁。没有被锁的对象的计数器值为0,但一个线程第一次获得锁的时候,计数器变为1,每加一次锁计数器就加1。但是只有已经拥有了这个对象的锁的线程才能对该对象再次加锁。在它释放锁之前,其他的线程无法对这个对象加锁。每当线程释放一次锁,计数器就会减1。当计数器为0时说明这个对象的锁已经释放,已经不存在锁,其他线程才可以使用它。
Java虚拟机中的一个线程在它到达监视区域开始处时请求一个锁。在Java中,有两种监视区域:同步语句和同步方法。Java程序中每一个监视区域都和一个对象引用相关联这,当一个线程到达监视区域的第一条指令的时候,线程必须对该引用对象加锁,否则线程不允许执行其中的代码。一旦获得了锁,线程就进入了被保护的代码,当线程离开保护代码的时候,不管它是如何离开的,它都会释放相关对象上的锁。
Java变成人员不需要自己动手加锁,对象锁是在Java虚拟机内部使用的。在Java程序中,只需要编写同步语句或同步方法就可以标志一个监视区域。当Java虚拟机运行你的程序时,每一次进入一个监视区域的时候,它每次都会自动上锁对象或类。
9.同步的支持
Java语言提供了两种内置方式来标志监视区域:同步语句和同步方法。这两个机制实现了同步的互斥。
1)同步语句
要建立一个同步语句,在一个计算对象引用的表达式中加上 synchronized关键字就可以了。
synchronized (this) { //... }
如果没有获得当前对象(this)的锁,在同步语句块内的语句是不会被执行的。如果使用的不是 this引用,而是用一个表达式获得对另一个对象的引用,在线程执行语句体之前,需要获得那个对象的锁。如果用表达式获得对Class类实例的引用,就需要锁住那个类。
JVM中,方法内的同步语句块使用 monitorenter和 monitorexit两个操作码。
monitorenter:弹出objectref(对象引用),获得和objectref相关联的锁。
monitorexit:弹出objectref(对象引用),释放和objectref相关联的锁。
当Java虚拟机遇到 monitorenter的时候,它获得栈中 objectref锁引用的那个锁。如果线程已经拥有了那个对象的锁,锁的计数器将加1.线程中每条 monitorexit指令都会引起锁计数器减1.当计数器值变成0的时候,监视器就被释放了。
2)同步方法
要同步整个方法,只需要在方法修饰符中加上 synchronized关键字。
public synchronized void eatApple(String name) { System.out.println(name + " eat the apple."); }
Java虚拟机调用同步方法或者从同步方法中返回没有使用任何特别的操作码。当虚拟机解析对方法的符号引用时,它判断这个方法是否是同步的。如果是同步的,虚拟机就在调用方法之前获取一个锁。对于实例方法来说,虚拟机在方法将要被掉用的时候获取对象相关联的锁。对于类方法来说,它获取方法所属的类的锁(其实就是Class对象上锁)。当同步方法执行完毕的时候,不管是正常结束还是抛出异常,虚拟机都会释放这个锁。
10.线程状态转换
对象锁与监视器等概念基本已经掌握,接下来就可以通过一些实例来看一看线程的各种状态间是如何转换的,我们又可以如何控制它们。
正常情况下线程的状态是应该随着时间轴从左至右发展,但是很多特殊的场景下我们不得不人为的去干预线程的执行,将线程的状态人为的进行转换,最终能够达到预期的结果。
1)NEW --> RUNNABLE:新创建未执行的线程状态为 NEW,调用 start方法后该线程状态会变为RUNNABLE.
Thread t = new Thread(); System.out.println(t.getState()); t.start(); System.out.println(t.getState()); //打印结果: NEW RUNNABLE
2)RUNNABLE -->RUNNING:一旦可运行状态线程获取了 CPU资源,那么它就会持有监视器并开始执行 run()方法中的代码,虽然在标准的线程状态中没有 RUNNING这个状态,统一用 RUNNABLE来表示,但我们心中可以这样理解。
public class ThreadB extends Thread { public void run() { System.out.println("id:" + this.getId()); System.out.println("state:" + this.getState()); } public static void main(String[] args) { Thread t = new ThreadB(); System.out.println("id:" + t.getId()); System.out.println("state:" + t.getState()); t.start(); } } //打印结果: id:9 state:NEW id:9 state:RUNNABLE
3)RUNNING --> TERMINATED:线程完成 run()方法的执行或抛出异常中断的情况下会从 RUNNABLE状态变成 TERMINATED终止状态。
Thread t = new Thread(); t.start(); System.out.println("state:" + t.getState()); //人为终止线程 t.interrupt(); System.out.println("state:" + t.getState()); //打印结果: state:RUNNABLE state:TERMINATED
4)RUNNING --> WAITING(对应上图①流程):持有监视器正在运行的线程调用 Object.wait(),Thread.join(),LockSupport.park()方法将会处于等待状态。
public class ThreadA implements Runnable { public void run() { try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } public static void main(String[] args) { Thread t = new Thread(new ThreadA()); t.start(); System.out.println("state:" + t.getState()); } } //打印结果: Exception in thread "Thread-0" java.lang.IllegalMonitorStateException at java.lang.Object.wait(Native Method) at java.lang.Object.wait(Object.java:485) at ThreadA.run(ThreadA.java:4) at java.lang.Thread.run(Thread.java:662) state:RUNNABLE当我们在 run()方法中直接调用 wait()方法时却发现抛出了 IllegalMonitorStateException异常,说明当前的线程不是此对象监视器的持有者。也就是要在当前线程锁定对象,才能用锁定的对象执行这些方法,就需要用到 synchronized 关键字。因为通过 Object 类的wait 方法和notify 方法来实现协作,允许多个线程为了同一目标而共同工作。所以 notify()、notifyAll()、wait() 、wait(long)、 wait(long, int)操作都需要先获取对象锁,否则就会报 IllegalMonitorStateException异常。
将代码修改一下,加入同步对象,synchronized所修饰的部分为监视区域:
public class ThreadB extends Thread { public void run() { synchronized (this) { try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String[] args) { Thread t = new ThreadB(); t.start(); System.out.println("state:" + t.getState()); } } //打印结果: state:WAITING
结果有可能是RUNNABLE,不过不用担心,是因为线程此时有可能还没有执行。
注意:wait方法只能在同步语句中使用,因为wait的前提条件就是获取当前监视器持有权,并获取当前同步对象的锁。线程执行完wait后将释放监视器进入等待区,在等待区线程将暂停等待,直至被唤醒。
5)RUNNING --> TIMED_WAITING(对应上图②流程):TIMED_WAITING状态是调用wait方法是向其传递了等待时间,在等待时间结束后该线程会自动被唤醒而不需要其他线程干预。
public class ThreadB extends Thread { public void run() { synchronized (this) { try { this.wait(5000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("state:" + this.getState()); } } public static void main(String[] args) { Thread t = new ThreadB(); t.start(); } } //打印结果: state:RUNNABLE打印结果为 RUNNABLE说明线程已经被唤醒继续执行。
6)WAITING --> RUNNABLE(对应上图④流程):处于等待状态的线程需要当前监视器持有者主动调用唤醒操作,来提醒等待区的线程“你们可以苏醒了”。当监视器持有者放弃监视器所有权进入等待区后,等待区的一个或全部线程将会与入口区所有 RUNNABLE状态线程进行竞争,竞争监视器持有权。如果等待区内的一个线程获胜,它将从 WAITING状态变为 RUNNING状态,并持有监视器开始运行 run方法内未执行部分代码。
为了更清晰我利用了两个线程类 ThreadA和 ThreadB,他们俩做的是同一份工作,他们之间需要相互配合,所以一方做完就需要告诉另一方:“喂,到你了!” ,另一方收到提醒后继续工作。
ThreadA类:
public class ThreadA extends Thread { Object job; public ThreadA(Object job) { this.job = job; } public void run() { synchronized (job) { while (true) { job.notify(); System.out.println("A"); try { job.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } }
public class ThreadB extends Thread { Object job; public ThreadB(Object job) { this.job = job; } public void run() { synchronized (job) { while (true) { job.notify(); System.out.println("B"); try { job.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } }
public class ThreadRunner { public static void main(String[] args) { Object job = new Object(); Thread a = new ThreadA(job); Thread b = new ThreadB(job); a.start(); b.start(); } } //打印结果: A B A B ...
其中 Object job起到标识作用,标识两个线程对同一个对象进行同步,如果将 job改成不同的对象,那么结果就是谁都唤醒不了谁了,因为他们做的都不是同一件事。
7)TIMED_WAITING --> RUNNABLE(对应上图④流程):因为 TIMED_WAITING状态是我们调用 wait方法并指定了等待时间,所以等待时间一到线程自然会被唤醒进入 RUNNABLE状态。还有一种情况就是,等待时间还未到,如果有线程主动去唤醒该线程会出现什么结果?其实只需要将上面代码中某一个线程的wait方法加上超时时间即可,结果却是虽然超时时间根本还没到,线程依然被唤醒了。所以 TIMED_WAITING的唤醒更像是一个闹钟,在闹钟响之前我已经被人吵醒了,只能继续工作了。
8)RUNNABLE--> BLOCKED(对应上图③流程):出现 BLOCKED状态是因为两个线程在争夺同一对象锁而出现阻塞现象,一个线程不释放锁,另一个线程就无法继续运行而阻塞等待。
Apple类:
public class Apple { public void eatApple(String name) { System.out.println(name + " eat the apple."); } }
Apple类有一个吃苹果的方法,用来打印出谁吃了苹果。
ThreadA线程类:
public class ThreadA extends Thread { Apple apple; public ThreadA(Apple apple) { this.apple = apple; } public void run() { synchronized (apple) { apple.eatApple("A"); while (true) { } } } }
A已经吃到了苹果,但是A却不想其他线程也吃到,所以A就利用循环无限的持有着 apple的锁,这样导致其他线程无法获取到apple的锁。
ThreadB类:
public class ThreadB extends Thread { Apple apple; public ThreadB(Apple apple) { this.apple = apple; } public void run() { synchronized (apple) { apple.eatApple("B"); } } }
B也想吃苹果,可是却总是抢不到。
ThreadRunner类:
public class ThreadRunner { public static void main(String[] args) { Apple apple = new Apple(); Thread a = new ThreadA(apple); Thread b = new ThreadB(apple); a.start(); b.start(); System.out.println("A state:" + a.getState()); System.out.println("B state:" + b.getState()); } } //打印结果: A eat the apple. A state:RUNNABLE B state:BLOCKED
运行两个线程,我们发现只有A吃到了苹果,而B却一直阻塞在那里等着吃苹果。
9)BLOCKED--> RUNNING(对应上图⑤流程):从阻塞状态变为运行状态只需要该线程获取到对象锁即可,所以把ThreadA中的循环语句去掉,B就能吃到苹果了。
注意:注意图中阻塞状态的箭头方向。
11.守护线程与非守护线程
在Java虚拟机内部有两种线程:守护线程和非守护线程。守护线程通常是由虚拟机自己使用和维护的,比如执行垃圾收集任务的线程。但是,Java程序也可以把它创建的任何线程标记为守护线程。而Java程序中的起始线程——main()方法线程,就是一个非守护线程。
只要还有任何非守护线程在运行,那么这个Java程序也将继续运行(Java虚拟机还存活)。当该程序中所有非守护线程都终止后,虚拟机实例将自动退出。假若安全管理器允许,程序本身也能够通过调用 Runtime类或者 System类的 exit()方法退出。
public class ThreadA extends Thread { Apple apple; public ThreadA(Apple apple) { this.apple = apple; } public void run() { apple.eatApple("A"); while (true) { } } } public class ThreadB extends Thread { Apple apple; public ThreadB(Apple apple) { this.apple = apple; } public void run() { apple.eatApple("B"); } } public class ThreadRunner { public static void main(String[] args) { Apple apple = new Apple(); Thread a = new ThreadA(apple); Thread b = new ThreadB(apple); //设置为守护线程 a.setDaemon(true); a.start(); b.start(); } } //打印结果: A eat the apple. B eat the apple.
在A线程中,A吃完苹果利用循环无限的停留在了run方法的执行中,因为A和B都是我们创建的线程,默认都为非守护线程,所以程序会一直执行下去。但当我们把线程A设置为守护线程后,随着唯一的非守护线程B退出A也退出了。所以一般情况下不要将需要执行的程序设置成守护线程,否则可能代码还没有执行就已经退出了。
下一篇Java多线程高级篇,将介绍线程池及相关多线程高级特性。
相关推荐
Java多线程是Java编程语言中一个非常重要的概念,它允许开发者在一个程序中创建多个执行线程并行运行,以提高程序的执行效率和响应速度。在Java中,线程的生命周期包含五个基本状态,分别是新建状态(New)、就绪...
### Java多线程操作数据库:深入解析与应用 在当今高度并发的应用环境中,Java多线程技术被广泛应用于处理数据库操作,以提升系统的响应速度和处理能力。本文将基于一个具体的Java多线程操作数据库的应用程序,深入...
Java多线程设计模式上传文件Java多线程设计模式上传文件Java多线程设计模式上传文件Java多线程设计模式上传文件Java多线程设计模式上传文件Java多线程设计模式上传文件Java多线程设计模式上传文件Java多线程设计模式...
Java多线程是Java编程中的重要概念,它允许程序同时执行多个任务,极大地提升了程序的效率和性能。在Java中,实现多线程有两种主要方式:通过实现Runnable接口或者继承Thread类。本案例将深入探讨Java多线程中的关键...
Java多线程是Java编程中的一个重要概念,它允许程序同时执行多个任务,提高了程序的效率和响应速度。在Java中,实现多线程有两种主要方式:继承Thread类和实现Runnable接口。 1. 继承Thread类: 当我们创建一个新...
Java多线程是Java编程中的重要概念,尤其在如今的多核处理器环境下,理解并熟练掌握多线程技术对于提高程序性能和响应速度至关重要。本资料详细讲解了Java多线程的原理,并提供了丰富的实战代码,非常适合Java初学者...
### Java多线程分页查询知识点详解 #### 一、背景与需求分析 在实际的软件开发过程中,尤其是在处理大量数据时,如何高效地进行数据查询成为了一个关键问题。例如,在一个用户众多的社交平台上,当用户需要查看...
《汪文君JAVA多线程编程实战》是一本专注于Java多线程编程的实战教程,由知名讲师汪文君倾力打造。这本书旨在帮助Java开发者深入理解和熟练掌握多线程编程技术,提升软件开发的效率和质量。在Java平台中,多线程是...
java多线程PPT 多线程基本概念 创建线程的方式 线程的挂起与唤醒 多线程问题
Java多线程读大文件 java多线程写文件:多线程往队列中写入数据
Java多线程机制是Java编程中至关重要的一部分,它允许程序同时执行多个任务,提升应用程序的效率和响应性。以下是对各个知识点的详细说明: 9.1 Java中的线程: Java程序中的线程是在操作系统级别的线程基础上进行...
在本文中,我们将深入浅出Java多线程编程的世界,探索多线程编程的基本概念、多线程编程的优点、多线程编程的缺点、多线程编程的应用场景、多线程编程的实现方法等内容。 一、多线程编程的基本概念 多线程编程是指...
在Java编程中,多线程并发是提升程序执行效率、充分利用多核处理器资源的重要手段。本文将基于"java 多线程并发实例"这个主题,深入探讨Java中的多线程并发概念及其应用。 首先,我们要了解Java中的线程。线程是...
《JAVA多线程教学演示系统》是一篇深入探讨JAVA多线程编程的论文,它针对教育领域中的教学需求,提供了一种生动、直观的演示方式,帮助学生更好地理解和掌握多线程技术。这篇论文的核心内容可能包括以下几个方面: ...
本项目以"java多线程实现大批量数据导入源码"为题,旨在通过多线程策略将大量数据切分,并进行并行处理,以提高数据处理速度。 首先,我们需要理解Java中的线程机制。Java通过`Thread`类来创建和管理线程。每个线程...
综上所述,"java多线程查询数据库"是一个涉及多线程技术、线程池管理、并发控制、分页查询等多个方面的复杂问题。通过理解和掌握这些知识点,我们可以有效地提高数据库操作的效率和系统的响应速度。
《Java多线程编程实战指南》这本书深入浅出地讲解了Java多线程的核心概念和实战技巧,分为核心篇和设计模式篇,旨在帮助开发者掌握并应用多线程技术。 1. **线程基础** - **线程的创建**:Java提供了两种创建线程...
这份“JAVA多线程编程技术PDF”是学习和掌握这一领域的经典资料,涵盖了多线程的全部知识点。 首先,多线程的核心概念包括线程的创建与启动。在Java中,可以通过实现Runnable接口或继承Thread类来创建线程。创建后...
Java多线程编程是Java开发中的重要组成部分,它允许程序同时执行多个任务,极大地提高了程序的效率和响应性。在Java中,多线程主要通过继承Thread类或实现Runnable接口来实现。本教程《Java多线程编程核心技术》将...
Java多线程编程实战指南(核心篇) 高清pdf带目录 随着现代处理器的生产工艺从提升处理器主频频率转向多核化,即在一块芯片上集成多个处理器内核(Core),多核处理器(Multicore Processor)离我们越来越近了――如今...