Java并发编程的基础
现代操作系调度的最小单元是线程,也叫轻量级进程,在一个进程里可以创建多个线程,这些线程都拥有各自的计数器、栈和局部变量,并且能够访问共享变量。
处理器在这些线程上高速切换。
执行main()方法是一个名称为main的线程。
线程简介
线程优先级
现代操作系统基本采用时分的形式调度运行的线程,操作系统会分出一个个时间片,线程会分配到若干时间片,当线程的时间片用完了就会发生线程调度,并等待者下次分配。线程分配到的时间片多少决定了线程使用处理器资源的多少。
Java线程中,通过控制整形变量priority来控制优先级。
优先级高的线程分配的时间片数量要多于优先级低的线程。
频繁阻塞(睡眠或者I/O操作)的线程设蛰较高的优先级,而偏重计算(需要较多的CPU时间或者偏运算)的线程设置较低的优先级,确保处理器不会被独占。
线程优先级不能作为程序正确性的依赖,因为操作系统可以完全不用理会Java线程对于优先级的设定。
线程的状态
状态名称 | 说明 |
---|---|
NEW | 线程初始状态,刚被创建没有调用start()方法 |
RUNNABLE | 运行状态,包括操作系统中的”就绪”和”运行”两种状态 |
BLOCKED | 阻塞状态,表示线程阻塞与锁 |
WAITING | 等待状态,进入该状态的线程需要等待其他线程做一些特定动作(通知或者中断) |
TIME_WAITING | 超时等待状态,在等待时间结束返回 |
TERMINATED | 终止状态,表示当前线程执行完毕 |
线程在自身的生命周期中,并不固定于某个状态,而是随着代码的执行在不同的状态之间进行切换。
从图中可以看出,线程创建之后,调用start()方法开始运行。当线程执行wait()方法之后,线程进入等待状态。进入等待状态的线程需要依赖其他线程的通知才能够返回到运行状态,而超时等待状态相当于在等待状态的基础上增加了超时限制,也就是超时时间一过,就会返回到运行状态。当线程调用同步方法时候,在没有获取到锁的状态下,线程将会进入到阻塞状态。线程在执行Runable的run()方法之后,将会进入到终止状态。
Daemon线程
守护线程是一种支持型线程,因为它主要被用作在后台调度以及支持性工作。这意味着,当一个Java虚拟机中不存在非Deamon线程的时候,Java虚拟机将会退出。可以调用Thread.setDaemon(true)将线程设置为Daemon线程。
Daemon属性需要在启动线程之前设置,不能在启动线程之后设置。
|
|
运行上述程序,可以看到在终端或者命令提示符上没有任何输出。main线程(非Daemon线程)在启动了线程DaemonRunner之后随着main方法执行完毕而终止,而此时Java虚拟机已经没有非Daemon线程,虚拟机退出。所有线程终止,所以DaemonRunner中的finnally块没有执行。
在构建Daemon线程时,不能依靠finnally块来保证资源的释放
启动和终止线程
构造线程
|
|
启动线程
上述的构造线程完成后,一个能够运行的线程对象就初始化了,在堆内存中等待着运行。
此时,调用start()方法就可以启动这个线程。线程starrt()方法含义是:当前线程(即parent线程)同步告知虚拟机,只要线程规划器空闲,应该立即调用start()方法的线程。
理解中断
中断可以理解为线程的一个标识位属性,它表示一个运行中的线程是否被其他线程进行了中断操作。中断好比其他线程对该线程打了个招呼,其他线程通过调用线程的interrupt()方法对其进行中断操作。
线程通过检查自身是否被中断来进行响应,线程通过方法isInterrupted()来进行判断是否被中断,也可以调用静态方法Thread.interrupted()对当前线程的中断标识位进行复位。如果该线程已经处于终结状态,即使该线程被中断过,在调用该对象的isInterrupted()方法时,依旧会返回false。
过期的suspend()、resume()和stop()
不建议使用三个过期的函数的原因:
以suspend()方法为例,在调用后,线程不会释放已经占有的资源(比如锁),而是占着有资源进入睡眠状态,这样容易引发死锁问题。同样滴,stop()方法在终结一个线程时不会保证资源的正常释放,通常是没有给予线程完成资源释放工作的机会,因此会导致程序可能工作在不确定的状态。
安全地终止线程
|
|
示例在执行过程中,main线程通过中断操作和cancel()方法都可以让Thread线程终止。这种通过标识位或者中断操作方式让线程在终止的时候可以有机会去清理资源,而不是直接武断地终止线程。
线程间的通信
volatile和synchronized关键字
volitale
volatile可以用来修饰字段(成员变量),就是告知程序任何对该变量的访问均需要从共享内存中获取,而对它的改变必须同步刷新回共享内存,它能保证所有线程对变量的访问的可见性。
synchronized
|
|
任意一个对象都拥有自己的监视器,当这个对象由同步块或者这个对象的同步方法调用时,执行方法的线程必须先获取到该对象的监视器才能够进入同步块或同步方法,而没有获取到监视器的线程将会被阻塞在同步块和同步方法的入口处,进入阻塞状态。
任意线程对Object(Object由synchronized保护)的访问,首先要获取Object的监视器,如果获取失败,线程进入同步队列,线程状态变为阻塞。
当访问前驱(获取了锁的线程)释放了锁,该释放操作唤醒阻塞在同步队列中的线程,使其尝试获取监视器。
等待/通知机制
等待/通知机制,是指一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B调用了对象的O的notify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进行后续的操作。
上述的两个线程通过对象O来完成交互,而对象上的wait()和notify()的关系就如开关信号一样,用来完成等待方和通知方之间的交互工作。
调用上述方法的细节:
- 使用wait()、notify()和notifyAll()需要先对调用对象加锁
- 调用wait()方法后,将当前线程放置到对象的等待队列。
- notify()/notifyAll()调用后,等待线程依旧不会从wait()返回,需要调用notfify()/notifyAll()的线程释放锁之后,等待线程才有机会从wait()返回。
- notify()将等待队列中的一个等待线程从等待队列移到同步队列,而notifyAll()方法将等待队列中的所有线程全部移到同步队列。
- 从wait()方法中返回的前提是获得了对象的锁。
等待/通知机制依托于同步机制,其目的就是确保等待线程从wait()返回时能够感知到通知线程对变量做出的改变。
如上图,WaitThread首先获取了对象的锁,然后调用了对象的wait()方法,从而放弃了锁并进入了等待队列WaitQueue中,进入等待状态。由于WaitThread释放了锁,NotifyThread随后获取了对象的锁,在业务逻辑处理后,调用了对象的notify()方法,将WaitThread从WaitQueue移到SynchronizedQueue中,此时WaitThread状态变为阻塞状态。NotifyThread释放了锁之后,从SynchronizedQueue取出一个线程来获取锁,这个WaitThread再次获取锁,并从wait()方法返回继续执行。
等待/通知的经典范式
等待方(消费者)、通知方(生产者)
等待方遵循的规则
- 获取对象的锁
- 如果条件不满足,调用对象的wait()方法,被通知后再次检查条件
- 条件满足执行后续操作
|
|
通知方的规则
- 获取对象的锁
- 改变条件
- 通知所有等待在对象上的线程
|
|
管道输入/输出流
管道输入/输出流与普通文件输入/输出流或者网络输入/输出流不同之处在于,它主要用于线程之间的数据传输,而传输的媒介为内存。
PipedReader和PipedWriterr面向字符。
|
|
ThreadLocal的使用
ThreadLock:线程本地变量,是一个以ThreadLock对象为键,任意对象为值的存储结构。这个结果被附带在线程上,也就是说一个线程可以根据一个ThreadLock对象查询到绑定到一个线程上的值。
可以通过get()和set()方法来获取和设置值。
|
|
若使用上述的Profiler的好处有,两个方法的调用不用在同一个方法或者类中。
例如在AOP中,可以在方法的调用前的切入点执行begin()方法,而在方法的调用之后执行切入点end()方法。
Thread.join()的使用
|
|
每个线程的终止的前提是,前驱线程的终止,每个线程等待前驱线程终止后,才从join()返回,这里涉及了等待/通知机制(等待前驱线程终止,接受前驱线程结束通知)。
当线程终止时,会调用线程自身的notifyAll()方法,会通知所有等待在该线程对象上的线程。
比如有两个线程,mainThread和ThreadA,在线程ThreadA中获取mainThread线程,并且调用mainThread.join(),那么线程A的终止前提条件是,mainThread终止。
Thread.yield()方法的使用
参考博客:点击跳转
使当前线程从执行状态(运行状态)变为可执行态(就绪状态)。cpu会从众多的可执行态里选择,也就是说,当前也就是刚刚的那个线程还是有可能会被再次执行到的,并不是说一定会执行其他线程而该线程在下一次中不会执行到了。
Java线程中有一个Thread.yield( )方法,很多人翻译成线程让步。顾名思义,就是说当一个线程使用了这个方法之后,它就会把自己CPU执行的时间让掉,让自己或者其它的线程运行。
打个比方:现在有很多人在排队上厕所,好不容易轮到这个人上厕所了,突然这个人说:“我要和大家来个竞赛,看谁先抢到厕所!”,然后所有的人在同一起跑线冲向厕所,有可能是别人抢到了,也有可能他自己有抢到了。我们还知道线程有个优先级的问题,那么手里有优先权的这些人就一定能抢到厕所的位置吗? 不一定的,他们只是概率上大些,也有可能没特权的抢到了。