并发[线程及线程池]

守护线程

Java 提供了两种类型的线程:守护线程 和 用户线程

  • 用户线程 是高优先级线程。JVM 会在终止之前等待任何用户线程完成其任务。
  • 守护线程 是低优先级线程。其唯一作用是为用户线程提供服务。

由于守护线程的作用是为用户线程提供服务,并且仅在用户线程运行时才需要,因此一旦所有用户线程完成执行,JVM 就会终止。也就是说 守护线程不会阻止 JVM 退出

这也是为什么通常存在于守护线程中的无限循环不会导致问题,因为任何代码(包括 finally 块 )都不会在所有用户线程完成执行后执行。

这也是为什么我们并不推荐 在守护线程中执行 I/O 任务 。因为可能导致无法正确关闭资源。

但是,守护线程并不是 100% 不能阻止 JVM 退出的。守护线程中设计不良的代码可能会阻止 JVM 退出。例如,在正在运行的守护线程上调用Thread.join() 可以阻止应用程序的关闭。

作用:就是将守护线程用于后台支持任务,比如垃圾回收、释放未使用对象的内存、从缓存中删除不需要的条目。

要将普通线程设置为守护线程,方法很简单,只需要调用 Thread.setDaemon() 方法即可。

创建线程

1、继承Thread
2、实现Runnalb接口
3、实现Callable接口
4、线程池

实现 Runnable 接口相对于继承 Thread 类来说,有如下显著的好处:
1、由于Java“单继承,多实现”的特性,Runnable接口使用起来比Thread更灵活。
2、Runnable接口出现更符合面向对象,将线程单独进行对象的封装。
3、Runnable接口出现,降低了线程对象和线程任务的耦合性。
4、如果使用线程时不需要使用Thread类的诸多方法,显然使用Runnable接口更为轻量。

6种状态

1、New(新创建)
2、Runnable(可运行)
3、Blocked(被阻塞)
4、Waiting(等待)
5、Timed Waiting(计时等待)
6、Terminated(被终止)
在这里插入图片描述

wait 和 sleep

1、为什么 wait 必须在 synchronized 保护的同步代码中使用?
结论:避免出现lost wake up问题,wait/notify是线程之间的通信,必须保证在满足条件的情况下才进行wait,错误的wait可能永远无法被notify到,所以需要强制wait/notify在synchronized中。

首先说一下多线程编程里面臭名昭著的问题"Lost wake-up problem"。这个问题并不是说只在Java语言中会出现,而是会在所有的多线程环境下出现。假如有两个线程,一个消费者线程,一个生产者线程。生产者线程的任务可以简化成将count加一,而后唤醒消费者;消费者则是将count减一,而后在减到0的时候陷入睡眠:
生产者伪代码:

count+1;

notify();

消费者伪代码:

while(count<=0)
   wait()

count--

熟悉多线程的朋友一眼就能够看出来,这里面有问题。什么问题呢?

生产者是两个步骤:
1、count+1;
2、notify();

消费者也是两个步骤:
1、检查count值;
2、睡眠或者减一;

万一这些步骤混杂在一起呢?比如说,初始的时候count等于0,这个时候消费者检查count的值,发现count小于等于0的条件成立;就在这个时候,发生了上下文切换,生产者进来了,噼噼啪啪一顿操作,把两个步骤都执行完了,也就是发出了通知,准备唤醒一个线程。这个时候消费者刚决定睡觉,还没睡呢,所以这个通知就会被丢掉。紧接着,消费者就睡过去了……
在这里插入图片描述
这就是所谓的lost wake up问题。

wait与notify原理
  重量级锁通过对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的MutexLock实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。前面我们在讲Java对象头的时候,讲到了monitor这个对象,在hotspot虚拟机中,通过ObjectMonitor类来实现monitor。他的锁的获取过程的体现会简单很多.
在这里插入图片描述

1、调用wait() 首先会获取监视器锁,获得成功后,会让线程进入等待状态进入等待队列并且释放锁;
2、然后当其他线程调用notify或者notifyall以后,会选择从等待队列中唤醒任意一个线程
3、而执行完notify方法以后,并不会立马唤醒线程,原因是当前线程仍然持有这把锁,处于等待状态的线程无法获得锁。必须要等到当前的线程执行完按monitorexit指令之后,也就是被释放之后,处于等待队列的线程就可以开始竞争锁了
在这里插入图片描述

wait和notify为什么要放在synchronized里面?
wait方法的语义有两个:
1、释放当前的对象锁、2、使得当前线程进入阻塞队列,
而这些操作都和监视器是相关的,所以wait必须要获得一个监视器锁。
notify也一样,它是唤醒一个线程,所以需要知道待唤醒的线程在哪里,就必须找到这个对象获取这个对象的锁然后去到这个对象的等待队列去唤醒一个线程。

为什么 wait/notify/notifyAll 被定义在 Object 类中,而 sleep 定义在 Thread 类中?
我们来看第二个问题,为什么 wait/notify/notifyAll 方法被定义在 Object 类中?而 sleep 方法定义在 Thread 类中?主要有两点原因:

1、因为 Java 中每个对象都有一把称之为 monitor 监视器的锁,由于每个对象都可以上锁,这就要求在对象头中有一个用来保存锁信息的位置。这个锁是对象级别的,而非线程级别的,wait/notify/notifyAll 也都是锁级别的操作,它们的锁属于对象,所以把它们定义在 Object 类中是最合适,因为 Object 类是所有对象的父类。
2、因为如果把 wait/notify/notifyAll 方法定义在 Thread 类中,会带来很大的局限性,比如一个线程可能持有多把锁,以便实现相互配合的复杂逻辑,假设此时 wait 方法定义在 Thread 类中,如何实现让一个线程持有多把锁呢?又如何明确线程等待的是哪把锁呢?既然我们是让当前线程去等待某个对象的锁,自然应该通过操作对象来实现,而不是操作线程。

wait/notify 和 sleep 方法的异同?
主要对比 wait 和 sleep 方法,我们先说相同点
1、它们都可以让线程阻塞。
2、它们都可以响应 interrupt 中断:在等待的过程中如果收到中断信号,都可以进行响应,并抛出 InterruptedException 异常。

但是它们也有很多的不同点
1、wait 方法必须在 synchronized 保护的代码中使用,而 sleep 方法并没有这个要求。
2、在同步代码中执行 sleep 方法时,并不会释放 monitor 锁,但执行 wait 方法时会主动释放 monitor 锁。
3、sleep 方法中会要求必须定义一个时间,时间到期后会主动恢复,而对于没有参数的 wait 方法而言,意味着永久等待,直到被中断或被唤醒才能恢复,它并不会主动恢复。
4、wait/notify 是 Object 类的方法,而 sleep 是 Thread 类的方法。

线程池

在这里插入图片描述

在这里插入图片描述
线程池的内部结构主要由四部分组成,如图所示。
第一部分是线程池管理器,它主要负责管理线程池的创建、销毁、添加任务等管理操作,它是整个线程池的管家。
第二部分是工作线程,也就是图中的线程 t0~t9,这些线程勤勤恳恳地从任务队列中获取任务并执行。
第三部分是任务队列,作为一种缓冲机制,线程池会把当下没有处理的任务放入任务队列中,由于多线程同时从任务队列中获取任务是并发场景,此时就需要任务队列满足线程安全的要求,所以线程池中任务队列采用 BlockingQueue 来保障线程安全。
第四部分是任务,任务要求实现统一的接口,以便工作线程可以处理和执行。

四种拒绝策略
在这里插入图片描述
第一种拒绝策略是 AbortPolicy,这种拒绝策略在拒绝任务时,会直接抛出一个类型为 RejectedExecutionException 的 RuntimeException,让你感知到任务被拒绝了,于是你便可以根据业务逻辑选择重试或者放弃提交等策略。
第二种拒绝策略是 DiscardPolicy,这种拒绝策略正如它的名字所描述的一样,当新任务被提交后直接被丢弃掉,也不会给你任何的通知,相对而言存在一定的风险,因为我们提交的时候根本不知道这个任务会被丢弃,可能造成数据丢失。
第三种拒绝策略是 DiscardOldestPolicy,如果线程池没被关闭且没有能力执行,则会丢弃任务队列中的头结点,通常是存活时间最长的任务,这种策略与第二种不同之处在于它丢弃的不是最新提交的,而是队列中存活时间最长的,这样就可以腾出空间给新提交的任务,但同理它也存在一定的数据丢失风险。
第四种拒绝策略是 CallerRunsPolicy,相对而言它就比较完善了,当有新任务提交后,如果线程池没被关闭且没有能力执行,则把这个任务交于提交任务的线程执行,也就是谁提交任务,谁就负责执行任务。这样做主要有两点好处。
第一点新提交的任务不会被丢弃,这样也就不会造成业务损失。
第二点好处是,由于谁提交任务谁就要负责执行任务,这样提交任务的线程就得负责执行任务,而执行任务又是比较耗时的,在这段期间,提交任务的线程被占用,也就不会再提交新的任务,减缓了任务提交的速度,相当于是一个负反馈。在此期间,线程池中的线程也可以充分利用这段时间来执行掉一部分任务,腾出一定的空间,相当于是给了线程池一定的缓冲期。

合适的线程数量,《Java并发编程实战》的作者 Brain Goetz 推荐的计算方法:
*线程数 = CPU 核心数 (1+平均等待时间/平均工作时间

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值