咱们在阅读程序时,表面看来是在跟踪程序的处理流程,实际上跟踪的是线程的执行。
单线程程序java
在单线程程序中,在某个时间点执行的处理只有一个。数据库
Java 程序执行时,至少会有一个线程在运行,这个运行的线程被称为主线程(Main Thread)。编程
Java 程序在主线程运行的同时,后台线程也在运行,例如:垃圾回收线程、GUI 相关线程等。缓存
Java 程序的终止是指除守护线程(Daemon Thread)之外的线程所有终止。守护线程是执行后台做业的线程,例如垃圾回收线程。咱们能够经过 setDaemon() 方法把线程设置为守护线程。服务器
多线程程序网络
由多个线程组成的程序称为多线程程序(Multithreaded Program)。多个线程运行时,各个线程的运行轨迹将会交织在一块儿,同一时间点执行的处理有多个。多线程
多线程应用场景:并发
P.S. 使用 java.nio 包中的类,有时即使不使用线程,也能够执行兼具性能和可扩展性的 I/O 处理。异步
并行(parallel)与并发(concurrent)的区别ide
程序运行存在顺序、并行与并发模式。
并发相对于顺序和并行来讲比较抽象。单个 CPU 并发处理即为顺序执行,多个 CPU 并发处理能够并行执行。
若是是单个 CPU,即使多个线程同时运行,并发处理也只能顺序执行,在线程之间不断切换。
并发处理包括:并发处理的顺序执行、并发处理的并行执行。
线程和进程的区别
当执行紧密关联的多项工做时,一般线程比进程更加适合。
多线程程序的优势和成本
优势:
缺点(成本):
相对而言,如果存在耗时任务须要放入子线程中实际执行,线程使用成本能够不计。
多线程编程的重要性
硬件条件知足多线程并行执行的条件以外,还须要程序逻辑可以保证多线程正确地运行,考虑到线程之间的互斥处理和同步处理。
建立与启动线程的两种方法:
线程的建立与启动步骤——方法一:
Thread 实例和线程自己不是同一个东西,建立 Thread 实例,线程并未启动,直到 start() 方法调用,一样就算线程终止了,实例也不会消失。可是一个 Thread 实例只能建立一个线程,一旦调用 start() 方法,无论线程是否正常/异常结束,都没法再次经过调用 start() 方法建立新的线程。而且重复调用 start() 方法会抛出 IllegalThreadStateException 异常。
Thread run( ) 方法 和 start() 方法:
线程的建立与启动步骤——方法二:
Thread(Runnable target)
无论是利用 Thread 类的子类实例化的方法(1),仍是利用 Runnable 接口的实现类实例化的方法(2),启动新线程的方法最终都是 Thread 类的 start() 方法。
Java 中存在单继承限制,若是类已经有一个父类,则不能再继承 Thread 类,这时能够经过实现 Runnable 接口来实现建立并启动新线程。
Thread 类自己实现了 Runnable 接口,并将 run() 方法的重写(override)交由子类来完成。
id 和 name
经过 Thread(String name)
构造方法或 void setName(String name)
,给 Thread 设置一个友好的名字,能够方便调试。
优先级
Java 语言中,线程的优先级从1到10,默认为5。但因程序实际运行的操做系统不一样,优先级会被映射到操做系统中的取值,所以 Java 语言中的优先级主要是一种建议,多线程编程时不要过于依赖优先级。
Thread.State 枚举类型(Enum)包括:
NEW
RUNNABLE
BLOCKED
WAITING
TIMED_WAITING
TERMINATED
Thread 类的静态方法 currentThread() 返回当前正在执行的线程对象。
Thread 类的静态方法 sleep() 可以暂停(休眠)当前线程(执行该语句的线程)运行,放弃占用 CPU。线程休眠期间能够被中断,中断将会抛出 InterruptedException
异常。sleep() 方法的参数以毫秒做为单位,不过一般状况下,JVM 没法精确控制时间。
sleep() 方法调用须要放在 try catch 语句中,可能抛出 InterruptedException
异常。InterruptedException
异常可以取消线程处理,可使用 interrupt() 方法在中途唤醒休眠状态的线程。
多线程示例程序中常用 sleep() 方法模拟耗时任务处理过程。
Thread 类的静态方法 yield() 可以暂停当前线程(执行该语句的线程)运行,让出 CPU 给其余线程优先执行。若是没有正在等待的线程,或是线程的优先级不高,当前线程可能继续运行,即 yield() 方法没法确保暂停当前线程。yield() 方法相似 sleep() 方法,可是不能指定暂停时间。
Thread 类的实例方法,持有 Thread 实例的线程,将会等待调用 join() 方法的 Thread 实例表明的线程结束。等待期间能够被中断,中断将会抛出 InterruptedException
异常。
示例程序:
public class HelloThread extends Thread { @Override public void run() { System.out.println("hello"); } } public class Main { public static void main(String[] args) throws InterruptedException { Thread thread = new HelloThread(); thread.start(); thread.join(); } }
main() 方法所在的主线程将会等待 HelloThread 子线程执行 run() 方法结束后再执行,退出程序。
原子性概念来源于数据库系统,一个事务(Transaction)中的全部操做,要么所有完成,要么所有不完成,不会结束在中间某个环节。事务在执行过程当中发生错误,会被恢复(Rollback)到事务开始前的状态,就像这个事务历来没有执行过同样。
并发编程的原子性指对于共享变量的操做是不可分的,Java 基本类型除 long、double 外的赋值操做是原子操做。
非原子操做例如:
counter++;
Java 语言的解决方式:
java.util.concurrent.atomic
包计算机结构中,CPU 负责执行指令,内存负责读写数据。CPU 执行速度远超内存读写速度,缓解二者速度不一致引入了高速缓存。 预先拷贝内存数据的副本到缓存中,便于 CPU 直接快速使用。
所以计算机中除内存以外,数据还有可能保存在 CPU 寄存器和各级缓存当中。这样一来,当访问一个变量时,可能优先从缓存中获取,而非内存;当修改一个变量时,可能先将修改写到缓存中,稍后才会同步更新到内存中。
对于单线程程序来讲没有太大问题,可是多线程程序并行执行时,内存中的数据将会不一致,最新修改可能还没有同步到内存中。须要提供一种机制保证多线程对应的多核 CPU 缓存中的共享变量的副本彼此一致——缓存一致性协议。
Java 语言的解决方式:
若是只是解决内存可见性问题,使用 synchronized 关键字成本较高,考虑使用 volatile 关键字更轻量级的方式。
有序性:即程序执行的顺序严格按照代码的前后顺序执行。
Java 容许编译器和处理器为了提升效率对指令进行重排序,重排序过程不会影响到单线程程序的执行,却会可能影响到多线程程序并发执行时候的正确性。
volatile 关键字细节
Java 使用 volatile
关键字修饰变量,保证可见性、有序性。
可是 volatile
关键字没法保证对变量操做是原子性的。
每一个线程拥有独立的程序计数器(指令执行行号)、栈(方法参数、局部变量等信息),多个线程共享堆(对象),这些区域对应 JVM 内存模型。当多个线程操做堆区的对象时候,可能出现多线程共享内存的问题。
银行取款问题
if(可用余额大于等于取款金额) {可用余额减去取款金额}
多个线程同时操做时,余额确认(可用余额大于等于取款金额)和取款(可用余额减去取款金额)两个操做可能穿插执行,没法保证线程之间执行顺序。
线程 A | 线程 B |
---|---|
可用余额(1000)大于等于取款金额(1000)?是的 | 切换执行线程 B |
线程 A 处于等待状态 | 可用余额(1000)大于等于取款金额(1000)?是的 |
线程 A 处于等待状态 | 可用余额减去取款金额(1000-1000 = 0) |
切换执行线程 A | 线程 B 结束 |
可用余额减去取款金额(0 - 1000 = -1000) | 线程 B 结束 |
当有多个线程同时操做同一个对象时,可能出现竞态条件(race condition),没法预期最终执行结果,与执行操做的时序有关,须要“交通管制”——线程的互斥处理。
Java 使用 synchronized
关键字执行线程的互斥处理。synchronized
关键字能够修饰类的实例方法、静态方法和代码块。
synchronized 关键字保护的是对象而非方法、代码块,使用锁来执行线程的互斥处理。
synchronized 修饰静态方法和实例方法时保护的是不一样的对象:
每一个对象拥有一个独立的锁,同一对象内的全部 synchronized 方法共用。
基于 synchronized 关键字保护的是对象原则,有以下推论:
synchronized 方法具备可重入性,即获取锁后能够在一个 synchronized 方法,调用其余须要一样锁的 synchronized 方法。
通常在保护实例变量时,将全部访问该变量的方法设置为 synchronized 同步方法。
若是只是想让方法中的某一部分由一个线程运行,而非整个方法,则可以使用 synchronized 代码块,精确控制互斥处理的执行范围。
死锁是指两个或两个以上的进程(线程)在执行过程当中,因争夺资源而形成的一种互相等待的现象,若无外力做用,它们都将没法推动下去。
死锁产生的四个必要条件
- 互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。若是此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。
- 请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对本身已得到的其它资源保持不放。
- 不剥夺条件:指进程已得到的资源,在未使用完以前,不能被剥夺,只能在使用完时由本身释放。
- 循环等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合 {P0,P1,P2,···,Pn} 中的 P0 正在等待一个 P1 占用的资源;P1 正在等待 P2 占用的资源,……,Pn 正在等待已被 P0 占用的资源。
产生死锁必须同时知足上述四个条件,只要其中任一条件不成立,死锁可避免。
应该尽可能避免在持有一个锁的同时,申请另外一个锁。若是确实须要多个锁,应该按照相同的顺序获取锁。
多线程之间除了在竞争中作互斥处理,还须要相互协做。协做的前提是清楚共享的条件变量。
wait()、notify()、notifyAll() 都是 Object 类的实例方法,而不是 Thread 类中的方法。这三个方法与其说是针对线程的操做,倒不如说是针对实例的条件等待队列的操做。
操做 obj 条件等待队列中的线程(唤醒、等待):
每一个对象拥有一个锁和锁的等待队列,另外还有一个表示条件的等待队列,用于线程间的协做。调用 wait() 方法会将当前线程放入条件队列等待,等待条件须要等待时间或者依靠其余线程改变(notify()/notifyAll() )。等待期间一样能够被中断,中断将会抛出 InterruptedException
异常。
Object 类的 wait() 方法和 Thread 类的 sleep() 方法在控制线程上主要区别在于对象锁是否释放,从方法所属类能够看出 Object 类的 wait() 方法包含对象锁管理机制。
WAITING
、TIMED_WAITING
状态。等待时间或者被其余线程唤醒(notify()/notifyAll() ),从条件队列中移除等待线程。
RUNNABLE
状态,从 wait() 方法返回,从新执行等待条件检查。BLOCKED
状态,加入对象锁的等待队列,继续等待。notify() 和 notifyAll() 方法的区别
一般使用 notifyAll() 方法,相比于 notify() 方法代码更具健壮性,可是唤醒多个线程速度慢些。
注意:调用 notify() 方法以后,唤醒条件队列中等待的线程,并将其移除队列。被唤醒的线程并不会当即运行,由于执行 notify() 方法的线程还持有着锁,等待 notify() 方法所处的同步(synchronized)代码块执行结束才释放锁。随后等待的线程得到锁从 wait() 方法返回,从新执行等待条件检查。
总结:
java.lang.IllegalMonitorStateException
。生产者线程和消费者线程经过共享队列进行协做,
生产者/消费者模式在生产者和消费者之间加入了一个桥梁角色,该桥梁角色用于消除线程间处理速度的差别。
Channel 角色持有共享队列 Data,对 Producer 角色和 Consumer 角色的访问执行互斥处理,并隐藏多线程实现。
线程正常结束于 run() 方法执行完毕,但在实际应用中多线程模式每每是死循环,考虑到存在特殊状况须要取消/关闭线程。Java 使用中断机制,经过协做方式传递信息,从而取消/关闭线程。
public static boolean interrupted() public boolean isInterrupted() public void interrupt()
线程存在 interrupted 中断状态标记,用于判断线程是否中断。
NEW
、TERMINATED
RUNNABLE
BLOCKED
BLOCKED
状态的线程WAITING
、TIMED_WAITING
InterruptedException
异常。这是一个受检异常,线程必须进行处理。中断的使用
对于提供线程服务的模块,应该封装取消/关闭线程方法对外提供接口,而不是交由调用者自行调用 interrupt() 方法。
结合线程的方法(Thread 类 + Object 类)来看线程的状态转换:
注:
Callable 接口经常使用与配合 Future、FutureTask 类获取异步执行结果。