欢迎访问:我的个人网站
Java 多线程
进程与线程
- 进程:
- 操作系统分配资源的基本单位,它往往代表了一个程序的实例,是一段程序的执行的过程。比如我们打开的网页,播放器等软件。在这些软件打开的时候,操作系统就会为其分配资源,并加入到就绪队列,当其获得CPU时间片之后,就开始正式运行了。
- 每一个进程拥有其独立的内存空间(包含要执行的代码,数据区域,堆栈),且与其他的进程是不共享的。当CPU时间完之后,将会切换到其他的线程进行工作,而这种切换是会带来较大的开销的。如果一个进程由于意外而终止,不会影响到其他的进程。同样的,进程之间是无法进行控制的。
- 线程:
- 任务调度和执行的最小单位,一个进程在运行的时候可以再次创建多个线程(至少有一个线程,不然这个进程的存在是无意义的)。进程可以看作是线程的容器,这些线程共享该进程的内存空间,所以单个线程实际上持有的资源是很少的(一个寄存器,一个优先权,一个堆栈),在进行上下文切换的时候,效率也较高一些。由于线程是隶属于一个进程的,所以线程之间是可以进行高效的通信,并能对其他的线程做出控制行为。
针对上面的内容,用之前在知乎看到的一个回答来进行举例说明。 如果说进程是火车,那么线程就是一节一节的车厢了,所以:
- 一个进程可以包含多个线程(一辆火车可以有多个车厢)
- 不同进程间数据很难共享(一辆火车上的乘客很难换到另外一辆火车,比如站点换乘)
- 同一进程下不同线程间数据很易共享(A车厢换到B车厢很容易)
- 进程要比线程消耗更多的计算机资源(采用多列火车相比多个车厢更耗资源)
- 进程间不会相互影响,一个线程挂掉将导致整个进程挂掉(一列火车不会影响到另外一列火车,但是如果一列火车上中间的一节车厢着火了,将影响到所有车厢)
- 进程可以拓展到多机,进程最多适合多核(不同火车可以开在多个轨道上,同一火车的车厢不能在行进的不同的轨道上)
- 进程使用的内存地址可以上锁,即一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。(比如火车上的洗手间)-“互斥锁”
- 进程使用的内存地址可以限定使用量(比如火车上的餐厅,最多只允许多少人进入,如果满了需要在门口等,等有人出来了才能进去)-“信号量”
线程的状态
线程状态大致分为以下5中情况:一个进程的运行就是在这5中状态上进行轮转直到运行结束。
- 新建状态: 新建了一个线程,但是目前该线程仍旧是一个空的对象,因为操作系统还未给其分配资源,仅仅处于创建的状态。
- 就绪状态: 此时该线程是执行了start()方法,操作系统为其分配了初CPU外的所有资源,线程此时已经是进入了就绪队列,参与对CPU的竞争。在得到CPU时间片之后,线程将转为运行态。
- 运行状态: 此时线程已经得到CPU的时间片,正处于运行阶段。
- 阻塞状态: 线程在运行过程中由于某些原因导致无法继续运行而不得不放弃对CPU的使用权,一般而言造成阻塞的原因有以下几点:
- 同步阻塞:线程准备执行的代码被加锁,此时由于无法获取到这部分代码的锁,线程无法继续执行下去,只能转向阻塞状态,参与下一轮竞争。
- 等待阻塞: 线程在运行过程中被其他线程调整为等待wait()策略而无法继续运行,这一般而言是因为当前线程需要等待其他线程完成某些操作。如果后续的某个时间允许当前线程继续运行了,则会使用notify()/notifyAll()方法来唤醒当前线程。唤醒之后将转入就绪队列参与下一次的竞争。
- 其他阻塞: 当前线程可能被其他线程调用了join()/sleep()方法以至于不能运行,也有可能是因为当前线程需要进行IO操作,这些都将导致线程进行阻塞状态。当sleep()/join()结束或者IO结束之后,将重新转入就绪队列参与竞争。
- 死亡状态: 线程执行完毕,操作系统回收其资源。
Java中进程创建方式
在进行多线程开发之前,需要简单了解一下进程的创建与使用,在Java中,提供了两种方式来进行进程的操作:
- 使用Runtime类。
- 使用ProcessBuilder类。
//使用Runtime建立进程
Runtime runtime = Runtime.getRuntime();
Process p = runtime.exec("命令");
//获取到传递给该进程的信息
OutputStream os = p.getOutputStream();
//使用ProcessBuilder建立进程
ProcessBuilder pb = new ProcessBuilder("命令");
Process p = pb.start();
OutputStream os = p.getOutputStream();
Java中线程的开发方式
Java虚拟机允许程序并发的执行多个执行线程,每个线程都有一个优先级,高优先级的线程优先于低优先级的线程运行,在创建一个线程的时候,这个被创建的线程优先级与创建它的线程相同,当创建线程为守护线程的时候,它创建出来的线程才会是守护线程,但是线程可以在创建之后被标记为守护线程。Java中有两种方式来实现多线程开发:
- 继承Thread类
- 实现Runnable
1.继承Thread
Thread是一个具体的类 ,内部封装了线程的行为。如果想要启动一个线程,需要继承该类且实现其中的run()方法, 这个run()方法是从Runnable接口得到的。这个方法是线程运行时候执行的内容。
//使用一个匿名类重写run方法。
Thread t = new Thread(){
@Override
public void run(){
System.out.println("新线程运行...");
}
};
t.start();
//使用Lambda表达式重写
new Thread(()->{
System.out.println("新线程运行...");
}).start();
以上两种形式还是遵循了继承Thread类并重写run()方法的流程,在一切完成之后,调用start()方法,让线程转为就绪态并运行。
2.实现Runnable
由于Java仅支持单继承,所以如果一个类已经继承了其他类,此时如果当前类还想作为线程运行的类,那么可以实现Runnable接口。以这种方式建立线程之后,如果要启动该线程,需要让当前对象作为参数并构造一个Thread实例,才可以运行:
class K implements Runnable{
@Override
public void run() {
System.out.println("Hello!");
}
}
//调用
public class Main{
public static void main(String[] args){
new Thread(new K()).start();
}
}
定时器TimerTask
它是一种特殊的线程,用于安排一次或者重复执行的任务。它本身为一个抽象类,如果想要创建一个这样的线程,需要继承这个TimerTask并重写run()方法:
class D extends TimerTask{
@Override
public void run() {
System.out.println("定时线程");
}
}
在实例化之后,不能像之前那样通过start()进行启动,他需要一个特殊的类Timer来启动,Timer是一个工具,用于安排在后台线程中执行的任务,可以定时执行一次,也可以重复执行。多个Timer对象对应单个后台线程,并顺序性的执行添加的各个任务,如果要执行的某个任务执行的时间过长,那么会造成其他任务的延迟执行。
对Timer的引用结束,并且要执行的任务都已经执行完成,那么这个后台线程就会被回收。因为这个后台线程并不是以守护线程的身份来运行的,所以如果还存在要执行的任务,主线程的退出并不会导致程序结束, 直到这个后台线程中的任务均执行完毕。如果在存在任务的情况下,放弃所有的任务转而退出,需要调用Timer类下的cancel()方法。
主要的几个启动方法:
//在指定时间运行该任务
void schedule(Task task, Date time);
//在指定的时间data开始重复运行任务,间隔为period
void schedule(Task task, Date time, long period);
//在指定延迟之后执行一次任务
void schedule(Task task, long period);
//在指定延迟之后,重复执行任务,时间间隔peroid
void schedule(Task task, long delay, long period);
//安排指定任务在指定时间以固定执行速率重复执行
void scheduleAtFixedRate(Task task, Date firstTime, long period);
//安排指定任务在指定延迟后以固定速率重复执行
void scheduleAtFixedRate(Task task, Date firstTime, long period);
其中固定时间间隔重复执行与固定速率重复执行区别在于:如果使用固定时间间隔,那么如果一个任务执行时间过长,仍旧会按照队列的形式一个个执行,造成后续任务的堆积。如果使用固定的速率,则会按照执行时间调整执行顺序,保证所有的任务都是按照一定的速度在执行都能得到相对均衡的执行机会,而不会因为一个任务长时间占用执行线程推迟了其他任务。
class D extends TimerTask{
private long sleep;
public D(long time){
this.sleep = time;
}
@Override
public void run() {
System.out.println("定时线程执行, 会休眠"+sleep);
try {
Thread.sleep(sleep);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class Main implements Serializable{
public static void main(String[] args){
Timer timer = new Timer();
timer.schedule(new D(1), 3000, 1000);
timer.schedule(new D(5000), 3000, 1000);
System.out.println("主线程结束");
}
}
此时主函数已经执行完毕,但是虚拟机并未停止,继续重复执行定时任务。因为启动定时任务的时候是按照固定延迟void schedule()进行的,所以休眠1ms的线程必须等待休眠5s的线程休眠完成并执行完毕之后才会有机会再次运行。严格按照队列顺序进行,这就是一个任务执行时间过长,造成其他线程堆积的现象。输出结果:
主线程结束
定时线程执行, 会休眠1
定时线程执行, 会休眠5000
定时线程执行, 会休眠1
定时线程执行, 会休眠5000
定时线程执行, 会休眠1
定时线程执行, 会休眠5000
....
如果改写main()函数内容,将其由固定时间间隔改为固定速率执行,根据运行结果可以看到,休眠5s的任务占用执行时间过长,所以已经在适当的减少了它的执行次数,尽量保证在运行时间内与其他任务的执行次数是一致的:
主线程结束
定时线程执行, 会休眠1
定时线程执行, 会休眠5000
定时线程执行, 会休眠5000
定时线程执行, 会休眠1
定时线程执行, 会休眠1
...
线程池ExecutorService
线程的开销相较于进程是非常轻量,但是仍旧是具有一些开销的,针对一些简单的开发模型:有需求的时候创建线程并执行,执行完毕就销毁,这种工作本身做的是没问题的。但是如果引申到一些特殊场景,比如服务器开发领域中,针对每一个请求建立一个线程对该用户进行服务,使得多个用户都能够同时使用提供的服务。但是这一类的请求都有一个特点就是请求做的事很简单,但是请求量是非常巨大的。这就造成一个问题:创建/销毁一个线程所花费的代价甚至高于请求本身要执行业务逻辑的代价。可以简单理解为:杀鸡用牛刀,牛刀不好找,却还有好多鸡要杀…如果针对一个请求创建一个线程处理的话,那么很可能会造成由于创建了太多的线程而不加控制而大量的消耗系统资源。
根据以上的问题,需要有一种方案来解决多线程创建/销毁带来的资源消耗,并对线程数量进行控制。线程池就是 通过通过对线程的重用,可以尽可能的利用所有已经存在的线程而避免新建线程的开销,同时可以对已经创建的线程做出限制,保证线程的数量不会一致的增长下去。
线程池提供的功能包含以下几个方面:
- 更加合理的调配系统资源,日常维护一组空闲线程并对外提供服务,并根据系统繁忙情况,运行环境动态调整池中线程的数量以确保服务可用且对系统环境影响最小
- 维护池中的线程,避免创建线程的开销,做到线程的重用。
java.util.concurrent包下面提供了包括互斥,信号量,以及一些在并发操作下,仍能够正常使用的队列,散列表之类的集合类的实现。包中包含一个Executors类是一个以队列为基础的线程池。
//创建一个可根据情况自动调整线程数量的池,存在可用线程的时候,优先使用可用线程而非新建。
static ExecutorService newCachedThreadPool();
//创建一个固定数量的线程池,当有任务到达而无空闲线程时候,任务将被迫等待
static ExecutorService newFixedThreadPool(int n);
//创建一个线程池,维护了一个线程
static ExecutorService newSingleThreadExecutor();
//创建一个具有延时/重复执行的线程池,具有一定的线程数量n,如果使用此线程池,
static ScheduledExecutorService newScheduledThreadPool(int n);
//终止一个线程池,如果未关闭,JVM不会退出。
void shutdown();
线程调度
线程的概念之所以出现,就是为了使得多个任务能够同时运行,所以也就存在对线程之间的调度,资源分配等的操作。这些操作主要是通过几个方法进行的。
线程优先级
当多个线程在就绪队列中的时候,同时竞争CPU的使用权,这个过程中是有一个叫做优先级的概念存在的,用于决定在一个时刻,某个线程可获得CPU使用权的可能性大小。在Java中,优先级是使用int常用进行划分的,在Thread类下面有几个静态常量用于设置一个线程的优先级高低:
- MIN_PRIORITY = 1
- NORM_PRIORITY = 5
- MAX_PRIORITY = 10
通过方法setPriority(int val)设置一个线程的优先级, 虽然我们可以自己传入指定的int值,但是为了在不同平台上都具有相同的优先级规则,尽量还是使用上面的三个静态常量, 另外设置了优先级并不是人为的控制线程对CPU的竞争,它只是在一定程度上提升了竞争到CPU的可能性,但并不是绝对的。
obj.setPriority(int val);
常用函数
sleep(long millis)
该函数将导致当前执行的线程休眠millis毫秒,在休眠过程中,这个线程不是释放持有的锁,休眠完毕之后会继续转回到可运行态,参与CPU的竞争,直到执行完毕。如下示例:
建立一个类,让后续建立的线程调用该类中的方法, 由于每个线程执行时候是单独创建了一个Say的实例,所以这里字节对Say这个类进行同步,让这个类的所有实例公共同一把锁:
class Say{
public void say(String name, long millis) throws InterruptedException {
synchronized (Say.class) {
for (int i = 0; i < 5 ; i++) {
System.out.println("Say::"+name+":::"+i);
Thread.sleep(millis);
}
}
}
}
建立类实现Runnable接口:
class D implements Runnable {
private String name;
private long millis;
public D(String name, long millis){
this.name = name;
this.millis = millis;
}
@Override
public void run() {
try {
Say s = new Say();
s.say(name, millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
建立两个线程并运行:
public static void main(String[] args){
Thread a = new Thread(new D("A", 2000));
Thread b = new Thread(new D("B", 1000));
a.start();
b.start();
}
输出结果, 此时是A线程先执行,在A线程休眠的过程中,由于未释放对Say这个类的锁,此时即便B已经获得了CPU,但是由于未成功获得锁,还是会转入锁阻塞池,直到A执行完毕,释放了对Say这个类的锁。此时B才有机会执行。整体执行结果如下:
Say::A:::0
Say::A:::1
Say::A:::2
Say::A:::3
Say::A:::4
Say::B:::0
Say::B:::1
Say::B:::2
Say::B:::3
Say::B:::4
join()
对某个线程对象T调用该方法之后,当前线程会等待T运行完毕之后再次接着运行。如果主线程此时需要等待T线程的运行结果,那么就可以使用该方法。一个简单的例子:
//计算1~n的阶乘,为了刻意延长子线程的计算时间,加入休眠。
class D implements Runnable {
private int n;
public D(int n){
this.n = n;
}
@Override
public void run(){
int r = 1;
for (int i = 1; i <= n; i++) {
r*=i;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(n+"的阶乘结果是:"+r);
}
}
调用:
public static void main(String[] args) throws InterruptedException {
Thread calc = new Thread(new D(5));
System.out.println("正在计算:");
calc.start();
calc.join();
System.out.println("计算完成!");
}
输出,即便子线程的时间要远高于主线程,但是由于设置了让主线程等待calc线程执行完毕再运行,所以主线程只能等待calc运行之后才接着运行:
开始计算:
5的阶乘结果是:120
计算完成!
yield()
该方法将导致当前线程由运行态转为就绪态,让出CPU执行权给其他同优先级或更高优先级的线程。 在执行yield()方法之后,线程不会释放它持有的锁。但是在一定情况下,这个让步操作可能并不会达到预期的效果,因为当前线程虽然是主动放弃了CPU使用,但是在接下来又会转入可运行态并参与下一轮的竞争。
sleep()和yield()的区别 :
- sleep()将导致一个线程进行休眠状态,且在休眠的时候肯定不会再被执行,因为此时线程的状态为阻塞。对于yield()而言,只是让线程放弃本次的运行权,在做出让步之后,直接转为就绪态,又会立刻参与下一次的竞争。
- sleep()休眠一个线程的时候,其他低优先级的线程仍旧可以参与对CPU的竞争。但 yield() 方法执行后,线程直接转为就绪态并参与竞争,所以,不可能让比它优先级还低的线程些时获得 CPU 占有权。
- sleep()方法在使用的时候需要处理interruptException,但是yield()不需要
- sleep()方法比yield()方法有更好的移植性。
wait() / notify() / notifyAll()
- wait()将导致该线程释放它所获得的锁,进入等待池,只有再次调用notify()/notifyAll()方法之后,才能唤醒这个线程,使其进入等锁池,在又一次获取到了锁之后,才转为就绪状态,参与后续的CPU竞争继续运行。
- notify() 唤醒在此对象监视器上等待的单个线程。
- notifyAll() 唤醒在此对象监视器上等待的所有线程。
以上方法在调用的时候,必须存在于synchronized块里面,并且针对的是同一个监视器,一个经典的生产者-消费者问题,可以很好的演示这个问题:
class Store{
private int maxSize = 10;
//上面所说的同一监视器就是针对一个公共的加锁操作对象
private ArrayList list = new ArrayList();
public void producter(String name){
synchronized (list) {
while (list.size()==maxSize){
try {
System.out.println(name + "产品已经满了,不能生产");
//释放对list的锁转入休眠,这样的话其他线程可以获得list的锁并进行操作
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//添加一个新产品
list.add("");
System.out.println(name + "生产了一个产品,当前:"+list.size());
//之前可能存在由于产品空而休眠的线程,这里一并唤醒
list.notifyAll();
}
}
public void consumer(String name){
synchronized (list) {
while (list.size()==0){
try {
System.out.println(name + "产品已经空了,不能消费");
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
list.remove(0);
System.out.println(name + "消费了一个产品,当前:"+list.size());
list.notifyAll();
}
}
}
public class Main{
public static void main(String[] args) {
Store store = new Store();
for (int i = 0; i < 5; i++) {
final int a = i;
new Thread(()->{
while (true) {
store.producter("生产者"+a);
}
}).start();
}
for (int i = 0; i < 5; i++) {
final int a = i;
new Thread(()->{
while (true) {
store.consumer("消费者"+a);
}
}).start();
}
}
}
执行代码之后,商品数量会严格控制在10个以内,并且商品数量为0的时候,并不会继续出现消费的情况。
多线程顺序打印ABC也是一个很常见的问题,也同样可以展现wait()/notify()方法的配合使用:
class Print extends Thread{
private String c;
private Object prev;
private Object self;
public Print(String c, Object prev, Object self){
this.c = c;
this.prev = prev;
this.self = self;
}
@Override
public void run() {
int count = 0;
while (count < 2) {
synchronized (prev) {
synchronized (self) {
System.out.println(c);
count++;
self.notify();
}
try {
prev.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public class Main{
public static void main(String[] args) throws InterruptedException {
Object a = new Object();
Object b = new Object();
Object c = new Object();
new Print("A", c, a).start();
Thread.sleep(100);
new Print("B", a, b).start();
Thread.sleep(100);
new Print("C", b, c).start();
}
}
输出结果:
ABCABC