1、Java 线程实现/创建方式
(1)继承Thread类
Thread类本质上是实现了Runnable接口的实例,代表一个线程的实例,通过start()
启动,自动执行run()
方法。
(2)实现Runnable接口
Runnable是一个没有返回值的线程任务类,Java有两种方式进行实现:
- 1、自定义线程类实现Runnable接口,覆写run方法;在主程序中利用Thread类构造器传入自定义线程,覆盖默认Thread实例
- 2、利用匿名类,在Thread构造器中传入匿名类,覆盖默认Thread实例
(3)ExcutorService、Callable、Future 含返回值的线程
- 若需要执行带有返回值的任务,必须实现Callable接口
- 执行Callable任务后,可以获取一个Future对象,等待结果返回,利用
get
方法获取返回值 - 利用ExcutorService,实现线程池执行任务,实现带有返回值的多线程
(4)线程池
线程池是为了解决线程的创建与销毁带来的系统资源消耗问题而实现的缓存策略,
Java提供了四种线程池:
利用 Excutors 的静态方法进行常见线程池,其实际上的顶级父类是ExcutorService接口
- newCachedThreadPool: 根据需要任务数量创建线程,并设立一个缓存时间
- 每调用一次execute,都会遍历一次线程池是否有可用线程,若没有可用线程,则创建一个新的线程执行任务
- 若一个线程超过60秒未使用,该线程将会被回收。
- newFixedThreadPool: 创建一个固定线程数的线程池,以共享的无界队列方式来运行这些线程
- 若全部线程都被任务占用,则新任务会保持在任务队列中等待调度执行。
- 若一个线程因为任务异常导致线程结束,则会重新创建一个线程调度任务队列的任务进行执行。
- newScheduledThreadPool: 创建一个具有定时功能的线程池,利用
schedule()
添加任务并设置执行周期 - newSingleThreadExcutor: 创建一个只有一个线程的线程池,当一个线程因为任务异常而结束,内部重新创建一个新线程替代旧线程执行新任务。
2、线程的生命周期:
线程的生命周期一共有五种:新建、就绪、运行、阻塞、死亡
- 新建状态(New): 当程序使用 new 关键字创建一个线程后,线程处于新建状态,此时JVM为其分配内存并初始化成员变量的值。
- 就绪状态(Runnable): 当线程对象调用 start() 之后,线程处于就绪状态,JVM为其创建方法调用栈和程序计数器,等待调度执行。
- 运行状态(Running): 处于就绪状态的线程获得了CPU,开始执行 run() 的线程方法体。
- 阻塞状态(Blocked): 线程因为某些原因放弃了CPU使用权,暂停运行,直到线程进入可运行状态。有三种阻塞情况
- 1、等待阻塞: 线程对象执行wait(),JVM 会把线程放入等待队列(waiting queue)中。
- 2、同步阻塞: 运行状态的线程在获取同步锁时,若该同步锁被其它线程占用,则JVM 会把线程放入锁池中。
- 3、其它阻塞: 运行状态的线程执行sleep()或join(),当发起IO请求时,JVM会把该线程置为阻塞状态,当sleep状态超时或join()等待线程终止或者超时,或IO处理完毕时,线程重新转入可运行状态。
- 线程死亡: 线程死亡有三种方式
- 1、run 或 call。方法执行完成,线程正常结束
- 2、线程抛出一个未捕获的 Exception 或 Error
- 3、调用线程对象的stop(),可能会导致死锁的发生
3、终止线程
终止线程的四种方式:
- 1、正常结束: 程序运行结束,线程自动结束。
- 2、使用退出标志退出线程: 当一个run()执行完成,线程就自动结束。当线程需要一个条件进行退出,则可以使用一个boolean类型的标志进行退出。(可以使用valatile,同步退出标志)
- 3、调用Interrupt():
- (1)当线程处于阻塞状态时,列入使用了sleep,同步锁的wait(),socket的receiver.accept(),使得线程处于阻塞状态,当方法体调用了interrupt()方法时,会抛出InterruptException异常,只有线程捕获到了该异常,才能让该线程正常结束。
- (2)当线程处于非阻塞状态时,使用
isInterrupt()
判断线程的中断标志来退出循环,当使用interrupt()
时,会将中断标志设为true。
- 4、调用stop(): 调用thread.stop()后,创建子线程的线程会抛出ThreadDeathError的错误,并且释放子线程持有的所有锁,这种操作可能会产生数据不一致,所以通常不会使用这种方式结束线程。
4、sleep 与 wait
- (1)sleep: 属于Thread类方法,线程对象调用该方法导致程序暂停执行指定的时间,并且让出CPU,但依旧持有对象锁并且保持监控状态,当时间过期后自动恢复运行状态。
- (2)wait: 属于Object类方法,线程对象调用该方法导致线程对象进入等待锁定池中并且释放对象锁,只有针对该对象调用 notify() 时,本线程才会进入对象锁定池准备获取对象锁进入运行状态。
5、start 与 run
- (1)start: 线程调用该方法,真正实现多线程执行,无需等待run方法执行完毕,继续执行代码,此时的线程处于就绪状态,并不运- 行。
- (2)run: 线程调用该方法,使得就绪状态的线程开始运行run函数中的代码,run方法结束,即线程结束。
6、Java后台线程
- (1)概念: 后台线程也叫**“守护线程”**,它是为用户线程提供公共服务的线程,在没有用户线程时自动离开。
- (2)优先级: 守护线程是一种公共线程,所以其优先级较其它线程低。
- (3)设置: 在用户线程对象创建之前,用线程对象的
setDaemon(true)
来设置线程为守护线程。 - (4)子线程创建: Daemon线程创建的子线程也是Daemon线程。
- (5)并发性: Daemon线程只存在于JVM,只有停止JVM的运行才能销毁Daemon线程。
- (6)案例: GC线程,是一个经典的守护线程,只有在有可回收对象时,才会执行服务方法,始终以低级别的状态运行。
- (7)生命周期: 与JVM生命周期挂钩,当所有的JVM线程都为守护线程时,JVM就可以退出了。
7、Java锁
(1)乐观锁: 获取数据不需要锁,而更新时先获取版本号,若数据版本与存储的版本相同,则获取锁进行更新;若数据版本与存储的版本不同,则重复 读 - 比较 - 写
的操作。Java中的乐观锁是通过CAS操作实现。
(2)悲观锁: 读写数据都需要获取锁才能进行。Java中的Synchroized 就是一种悲观锁。而AQS框架下的锁是先尝试CAS乐观锁获取锁,若获取不到则转为悲观锁获取,例如ReetrantLock。
(3)自旋锁: 是一种获取锁的机制,若持有锁的线程能够在短时间内释放锁资源,则等待竞争的线程不需要做内核态和用户态的切换进入阻塞挂起状态,只需要等待一点时间(自旋),等待持有锁的线程释放锁后立即可以获取锁。因为自旋是一种持续在CPU中不断尝试获取锁的方式,所以会持续占用CPU,导致CPU做无用功,所以通常需要设置一个自旋等待的最大时间,时间到达还未获取锁资源,则停止自旋并进入阻塞状态。
自旋锁的优缺点:
- 自旋锁不同场景的性能提升情况:
- 锁竞争不激烈的场景: 大幅度提升程序性能,因为自旋的损耗小于阻塞再唤醒的损耗
- 锁竞争激烈的场景或是持有锁的线程长期占用锁执行同步块: 不适用自旋锁,因为自旋随时间的拉长而损耗变大,所以需要关闭自旋锁。
自旋锁如何选择自旋执行时间? 在JDK 1.5 时,自旋时间是固定的,JDK 1.6 引入了适应性自旋锁,通过前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定,一个上下文切换的时间是一个最佳自旋时间。
自旋锁的开启:
- JDK 1.6 使用
-XX:+UseSpinning
开启 -XX:PreBlockSpin=10
设置自旋次数- JDK1.7后,由JVM控制开启自旋
(4)Sychronized 同步锁
sychronized 是一种可以将任意一个非NULL对象作为锁的实现方式,属于独占式的悲观锁,同时属于可重入锁。
- Sychronized 作用范围
- 1、作用于方法时,锁住的是对象的实例(this)
- 2、作用于静态方法时,锁住的是Class实例,由于Class的相关数据存储在永久代(元数据区),所以锁住静态方法,相当于锁住了所有调用该方法的线程
- 3、作用于一个对象实例时,锁住的是所有以该对象为锁的代码块,它有多个队列,当多个线程一起访问某个对象监视器时,对象监视器会将这些线程存储在不同的容器中。
- Sychronized 核心组件
- 1、Wait Set: 调用wait()的线程放置在这里。
- 2、Contention List:竞争队列,所有请求锁的线程首先放置在该队列。
- 3、Entry List:竞争队列中可竞争的线程移动到该队列。
- 4、OnDesk: 任意时刻,最多只有一个线程竞争锁资源,该线程被称为OnDesk
- 5、Owner: 当前获取锁的线程对象
- 6、!Owner: 当前释放锁的线程对象
- Sychronized 实现
- 1、JVM从Contention List的队尾取出一个对象作为OnDesk,在并发情况下,会从Contention List取出一部分移动如Entry List作为OnDesk候选线程队列
- 2、Owner线程在unlock时,会从Contention List 迁移出部分线程到Entry List,并指定一个线程为OnDesk(一般是第一个进入的线程)
- 3、Owner线程释放锁时,成为OnDesk的线程竞争锁的优势最大,牺牲一定的公平性换取提高系统吞吐量,这种行为被称为竞争切换
- 4、OnDesk获取锁后,成为Owner,而未获取锁的线程会留在Entry List 中,当Owner线程被wait阻塞,则自动释放锁,并且转入Wait Set,直到notify或notifyAll,转入Entry List
- 5、处于Wait Set、Contention List、Entry List的线程都处于阻塞状态,这种状态是由OS来完成的
- 6、Sychronized 是非公平锁,在线程进入Contention List 之前,会通过自旋来抢占竞争锁资源列表中线程对象的锁资源(包括OnDesk)
- 7、对象加锁是通过monitor对象进行的,
monitorenter和monitorexit
指令组成了加锁的范围,通过标记位来判断是否方法是否加锁 - 8、Sychronized 是重量级锁,需要调用OS相关接口,性能较低
- 9、Java 1.6 对sychronized 进行了优化:适应性自旋、锁清除、锁粗化、轻量级锁及偏向锁等;Java 1.7 和 1.8 对关键字实现机制进行优化
- 10、锁膨胀,锁可以从偏向锁升级到轻量级锁,再升级到重量级锁
- 11、JDK 1.6中默认开启偏向锁和轻量级锁,通过
-XX:-UseBiasedLocking
来禁用偏向锁
(5)ReetrantLock
ReentantLock 是一种可重入锁,继承并实现接口 Lock,除了能完成synchroni