本章主要介绍java中多线程并发编程基础知识,包括的内容有:
- 进程,线程,协程的区别
- 多线程的实现方式
- 线程中断和优先级
- 线程状态的切换
一、进程、线程和协程
1.1 进程、线程和协程
进程是资源分配的最小单位,操作系统会以进程为单位,分配系统资源(CPU时间片、内存等资源)。进程拥有自己独立的堆和栈,既不共享堆,也不共享栈。
线程是CPU调度的最小单位,线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,共享堆不共享栈。
协程是一种比线程更加轻量级的存在,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。
【区别】
- 调度性:进程是操作系统资源分配的单位,线程作为调度和分配的基本单位。
- 并发性:进程可以并发进行,同一个进程内的线程也可以并发进行;
- 资源分配:进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源。
- 系统开销:进程的创建和销毁,涉及到操作系统资源的分配和回收,系统开销要大于线程。
- 健壮性:进程之间相对独立,一个进程崩溃后,在保护模式下不会对其它进程产生影响。线程是一个进程中的不同执行路径,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮。
【联系】
- 一个线程只能属于一个进程,一个进程至少拥有一个线程
- 资源分配给进程,同一进程内的线程共享该进程的所有资源
- 处理机分配给线程,CPU调度是分配给线程的
与多线程相比,协程具有的优势特点是:
- 线程是用户程序控制的,协程切换没有线程切换的开销,和多线程相比,线程数量越多,协程的性能优势越明显。
- 协程中控制共享资源不需要加锁,也不存在同时写变量的冲突,执行效率比线程要高。
1.2 并行和并发:
并行是时间上各不影响的同时执行,而并发在时间上是相互应用时间片段,宏观上看是并行的。
1.3 阻塞和非阻塞:
形容多线程之间的相互影响,当一个线程独占临街资源时,其他线程需要等待,导致线程挂起,这就是阻塞。
1.4 临界区:
表示一种公共资源,可以被多个线程使用,但是每次只允许一个线程独占。
1.5 死锁、活锁
死锁:指两个或多个线程在执行的过程中,由于资源相互竞争造成阻塞的现象。若无相关操作,他们将被无限期阻塞下去。死锁是一个静态问题,进程会被卡死,但是不会占用cpu。
活锁:是一个动态过程,比如:线程A、B都需要临界资源a和临界资源b,线程A占用a,需要b,线程B占用b,需要a,放弃了资源以后,A又获得了b资源,B又获得了a资源,如此反复,则发生了活锁。
1.6 饥饿:
指某一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执行。
二、多线程的实现方式
创建多线程,可以通过以下几种方式:
- 继承Thread,重写run方法
- 实现runnable接口,重写run方法
- 实现callable接口,重写run方法,并把任务提交给线程池
- 继承FutureTask,FutureTask提供Callable和Runnable接口的构造函数,
- 通过ForkJoin框架并行执行任务,其内部的ForkJoinTask继承Future接口。
三、线程的中断和优先级
3.1 线程的优先级
java 中线程默认的优先级有10级,MAX_PRIORITY和MIN_PRIORITY分别是最高级10和最低级1,当然还有默认级别是5;线程优先级的一些特性:
- 线程优先级的继承特性:也就是如果线程A启动线程B,那么线程A和B的优先级是一样的;
- 线程优先级的规则性:即线程会优先级的大小顺序执行,但不一定是优先级较大的先执行完,因为线程的优先级还有下面第三个特性:
- 线程优先级的随机特性;
可以通过Thread.setPriority(level)来实现对线程优先级的控制。
java 中有两种线程:用户线程和守护线程,可以通过isDaemon()方法来区别它们:如果返回false,则说明该线程是“用户线程”;否则就是“守护线程”。
当Java虚拟机启动时,通常有一个单一的非守护线程(该线程通过是通过main()方法启动)。JVM会一直运行直到下面的任意一个条件发生,JVM就会终止运行:
(01) 调用了exit()方法,并且exit()有权限被正常执行。
(02) 所有的“非守护线程”都死了(即JVM中仅仅只有“守护线程”)。
每一个线程都被标记为“守护线程”或“用户线程”。当只有守护线程运行时,JVM会自动退出。
3.2 线程的中断
线程中断:中断是一种协作机制,调用线程的interrupt方法不一定会中断正在运行的线程,它会要求线程在合适的时机结束自己。每个线程都会有一个boolean的中断状态,interrupt方法只是将状态设置为true,对于非阻塞的线程,只是状态进行了改变,并不一定会立即停止。如果线程上使用了Thread.sleep(), Object.wait(), Thread.join(),这个线程收到中断信号后, 会抛出InterruptedException, 同时会把中断状态置回为false。
Thread.stop():会释放掉所有的监管monitor,无论线程执行到哪里,都会立即停止线程,不推荐使用。
wait和notify区别
共同点:
1)都是在多线程的环境下,都可以在程序的调用处阻塞指定的毫秒数,并返回。
2)wait()和sleep()都可以通过interrupt()方法 打断线程的暂停状态 ,从而使线程立刻抛出InterruptedException。
不同点:
1)sleep和yield方法都是Thread类方法,wait和notify,notifyAll都是object的方法;
2)sleep睡眠时,保持对象锁,仍然占有该锁;而wait睡眠时,释放对象锁。
3)sleep可以在任何地方使用,并且需要捕获编译异常;wait和notify,notifyAll需要在同步块中使用。
四、线程的基本状态
1)新建状态:被new()出来
2)runnable状态:线程start
3)running状态:线程获得时间片,正执行
4)等待状态:线程被wait,释放掉锁的状态
5)等锁状态:线程被notify后,进入等锁池
6)阻塞状态:线程sleep,join或者Io阻塞
7)dead:线程执行完
线程之间的扭转关系可有:
五、多线程中的happen-before
Java语言中有一个“先行发生”(happen—before)的规则,它是Java内存模型中定义的两项操作之间的偏序关系。Java内存模型中的八条可保证happen—before的规则,它们无需任何同步器协助就已经存在。
- 程序顺序原则:一个线程内保证语义的串行性
- volatile规则:volatile变量的写,先发生于读,这保证了volatile变量的可见性
- 锁规则:解锁(unlock)必然发生在随后的加锁(lock)前
- 传递性:A先于B,B先于C,那么A必然先于C
- 线程的start()方法先于它的每一个动作
- 线程的所有操作先于线程的终结(Thread.join())
- 线程的中断(interrupt())先于被中断线程的代码
- 对象的构造函数执行结束先于finalize()方法
public class HappenBeforeTest extends Thread {
private boolean flag;
@Override
public void run(){
while(!flag){
System.out.println("thread run");
}
System.out.println("thread stop");
}
public boolean getStop(){
flag = true;
return flag;
}
public static void main(String[] args) throws InterruptedException {
HappenBeforeTest test = new HappenBeforeTest();
test.start();
Thread.sleep(1000);
test.getStop();
Thread.sleep(1000);
}
}
这个程序在client模式下能够正常的打印thread run和thread stop。但是在Server模式下可能将是无限循环。因为虽然getStop函数设定了结束标识,但是线程不一定能取到值,甚至会运行抛出异常。