多线程
线程的实现
在 Java 中,实现多线程的主要有以下四种
- 继承
Thread
类,重写run()
方法; - 实现
Runnable
接口,实现run()
方法,并将Runnable
实现类的实例作为Thread
构造函数的参数 target; - 实现
Callable
接口,实现call()
方法,然后通过FutureTask
包装器来创建Thread
线程; - 通过
ThreadPoolExecutor
创建线程池,并从线程池中获取线程用于执行任务;
不使用Executors创建,而是用ThreadPoolExecutor创建
通过工厂模式,将创建产品实例的权利移交工厂,我们不再通过new来创建我们所需的对象,而是通过工厂来获取我们需要的产品。降低了产品使用者与使用者之间的耦合关系;创建线程池没有显式new,而是通过Executors这个静态方法newCaChedThreadPool来完成的;
Thread
类与Runnable
接口比较:
实现一个自定义的线程类,可以有继承Thread
类或者实现Runnable
接口的方式,由于java单继承、多实现的特性,Runnable
接口使用比Thread
更加灵活
Callable
接口:
通常来说,我们使用Runnable
和Thread
来创建一个新的线程。但是它们有一个弊端,就是run
方法是没有返回值的。而有时候我们希望开启一个线程去执行一个任务,并且这个任务执行完成后有一个返回值。
JDK提供了Callable
接口与Future
类为我们解决这个问题,这也是所谓的“异步”模型。
线程的六种状态:
// Thread.State 源码
public enum State {NEW,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING,TERMINATED;
}
- NEW:
- 不能反复调用同一个线程的
start()
方法 - 处于TERMINATED状态的线程不能再次调用
start()
方法
- RUNNABLE:
运行状态。处于RUNNABLE状态的线程在Java虚拟机中运行,也有可能在等待其他系统资源(比如I/O)。
- BLOCKED:
阻塞状态。处于BLOCKED状态的线程正等待锁的释放以进入同步区。
- WAITING
等待状态。处于等待状态的线程变成RUNNABLE状态需要其他线程唤醒。
以下方法使进程进入等待状态:
- TIMED_WAITING
超时等待状态。线程等待一个具体的时间,时间到后会被自动唤醒。
- TERMINATED
终止状态。此时线程已执行完毕。
转换成TERMINATED状态的三种方法:
Object.wait()
Thread.join()
LockSupport.park()
Java线程间的通信:
锁与同步
在Java中,锁的概念都是基于对象的,所以我们又经常称它为对象锁。线程和锁的关系,我们可以用婚姻关系来理解。一个锁同一时间只能被一个线程持有。也就是说,一个锁如果和一个线程“结婚”(持有),那其他线程如果需要得到这个锁,就得等这个线程和这个锁“离婚”(释放)。
等待/通知机制
Java多线程的等待/通知机制是基于Object
类的wait()
方法和notify()
, notifyAll()
方法来实现的。
信号量
使用volitile
关键字实现信号量。volitile
关键字能够保证内存的可见性,如果用volitile
关键字声明了一个变量,在一个线程里面改变了这个变量的值,那其它线程是立马可见更改后的值的。
其他通信相关
join方法
join()方法是Thread类的一个实例方法。它的作用是让当前线程陷入“等待”状态,等join的这个线程执行完成后,再继续执行当前线程。
sleep方法
sleep方法是Thread类的一个静态方法。它的作用是让当前线程睡眠一段时间。
wait方法与sleep方法的区别:
sleep仅仅释放cpu资源,不会释放锁,所以易死锁。而wait方法会释放cpu资源,也会释放当前锁。
原理篇
volatitle
valatitle
保证内存可见性且禁止重排序
synchronized与锁
四种状态:无锁、偏向锁、轻量级锁、重量级锁
无锁->偏向锁->轻量级锁->重量级锁
- 偏向锁:
顾名思义,它会偏向于第一个访问锁的线程,如果在接下来的运行过程中,该锁没有被其他的线程访问,则持有偏向锁的线程将永远不需要触发同步。
如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会尝试消除它身上的偏向锁,将锁恢复到标准的轻量级锁。
- 轻量级锁:
是指当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。
若当前只有一个等待线程,则该线程通过自旋进行等待。但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。
- 重量级锁:
在轻量级锁状态下,如果有第三个来访时,就会自动升级成重量级锁
锁 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
偏向锁 | 加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。 | 如果线程间存在锁竞争,会带来额外的锁撤销的消耗。 | 适用于只有一个线程访问同步块场景。 |
轻量级锁 | 竞争的线程不会阻塞,提高了程序的响应速度。 | 如果始终得不到锁竞争的线程使用自旋会消耗CPU。 | 追求响应时间。同步块执行速度非常快。 |
重量级锁 | 线程竞争不使用自旋,不会消耗CPU。 | 线程阻塞,响应时间缓慢。 | 追求吞吐量。同步块执行速度较长。 |
cas与原子操作
乐观锁与悲观锁
悲观锁:
它总是认为每次访问共享资源时会发生冲突,所以必须对每次数据操作加上锁,以保证临界区的程序同一时间只能有一个线程在执行。
乐观锁:
乐观锁总是假设对共享资源的访问没有冲突,线程可以不停地执行,无需加锁也无需等待。而一旦多个线程发生冲突,乐观锁通常是使用一种称为CAS的技术来保证线程执行的安全性。乐观锁天生免疫死锁。
乐观锁多用于“读多写少“的环境,避免频繁加锁影响性能;而悲观锁多用于”写多读少“的环境,避免频繁失败和重试影响性能。
CAS
如果有一个多个线程共享的变量
i
原本等于5,我现在在线程A中,想把它设置为新的值6;
CAS的全称是:比较并交换(Compare And Swap)。在CAS中,有这样三个值:
- V:要更新的变量(var),指代变量(i)
- E:预期值(expected),指旧值(5)
- N:新值(new),指要设置的值(6)
比较并交换的过程如下:
判断V是否等于E,如果等于,将V的值设置为N;如果不等,说明已经有其它线程更新了V,则当前线程放弃更新,什么都不做。
线程池原理
主要的任务处理流程:
如何做到线程复用
ThreadPoolExecutor在创建线程时,会将线程封装成工作线程worker,并放入工作线程组中,然后这个worker反复从阻塞队列中拿任务去执行。
首先去执行创建这个worker时就有的任务,当执行完这个任务后,worker的生命周期并没有结束,在while
循环中,worker会不断地调用getTask
方法从阻塞队列中获取任务然后调用task.run()
执行任务,从而达到复用线程的目的。只要getTask
方法不返回null
,此线程就不会退出。