线程
线程创建与运行
创建线程的三种方式:
- 继承Thread类,重写run方法
- 通过Runable接口创建线程类
- 通过Callable和FutureTask创建线程
1 | public class MyTest { |
线程状态
Java定义了5中线程状态(新建、运行、等待(包括无限期等待和限期等待)、阻塞、结束):
- 新建(New):创建后尚未启动的线程处于此状态。
- 运行(Runable):包括了操作系统中的Running和Ready,线程可能正在执行,也可能在等待时间片。
- 等待
- 无限期等待(Waiting):处于这种状态的线程不会被分配CUP时间,它们等待被其他线程显示唤醒。
- 没有设置Timeout的Object.wait()方法。
- 没有设置TImeout参数的Thread.join方法
- LockSupport.park()方法
- 限期等待(Timed Waiting):处于这种状态的线程不会被分配CUP时间,一定时间后由系统自动唤醒。
- 设置Timeout的Object.wait()方法。
- 设置TImeout参数的Thread.join方法
- Thread.sleep()方法
- LockSupport.parkNanos()方法
- LockSupport.parkUntil()方法
- 无限期等待(Waiting):处于这种状态的线程不会被分配CUP时间,它们等待被其他线程显示唤醒。
- 阻塞(Blocked):线程等待着一个排它锁。
- 结束(Terminated):已经终止的线程状态。

用户线程与守护线程
Java中的线程分为两类,分别为daemon线程(守护线程)和user线程(用户线程)。
区别之一是当最后一个非守护线程结束时,JVM会正常退出,而不管当前是否有守护线程,也就是说守护线程是否结束并不影响JVM的退出。
线程相关方法
wait()
当一个线程调用一个共享变量的wait()方法时,该调用线程会被阻塞挂起,直到发生下面几件事情之一才返回:
- 其他线程调用了该共享对象的notify()或者notifyAll()方法
- 其他线程调用了该线程的interrupt()方法,该线程抛出InterruptedException异常返回。
调用wait()方法 的线程需要先获取该对象的监视器锁,否则调用wait()方法时调用线程会抛出IllegalMonitorStateException异常。
获取对象的监视器锁:
执行 synchronized 同步代码块时 ,使用该共享变量作为参数。
1
2
3synchronized (obj){
//do something
}调用该共享变量的方法,并且该方法使用了synchronized修饰。
1
2
3synchronized void sync(int a,int b){
//do something
}
调用wait()会释放锁
当线程调用共享对象的wait()方法时,当前线程只会释放当前共享对象的锁,当前线程持有的其他共享对象的监视器锁并不会被释放。
notify()/notifyAll()
notify()函数
一个线程调用共享对象的notify()方法后,会唤醒一个在该共享变量上调用 wait系列方法后被挂起的线程。 一个共享变量上可能会有多个线程在等待,具体唤醒哪个等待的线程是随机的。
notifyAll()函数
notifyAll()方法则会唤醒所有在该共享变量上由于调用 wait 系列方法而被挂起的线程。
被唤醒的线程,不会直接从wait方法返回并执行,而是需要去竞争锁,获取到对象的锁以后才能继续执行。
虚假唤醒
一个线程可以从挂起状态变为可以运行状态(也就是被唤醒), 即使该线程没有被其他线程调用 notify()、 notifyAll()方法进行通知, 或者被中断, 或者等待超时, 这就是所谓的虚假唤醒 。
虽然虚假唤醒在应用实践中很少发生, 但要防患于未然, 做法就是不停地去测试该线 程被唤醒的条件是否满足, 不满足则继续等待, 也就是说在一个循环中调用 wait()方法进行防范。退出循环的条件是满足了唤醒该线程的条件 。
1 | synchronized(obj){ |
join()方法
threadObj.join()
threadObj.join(long millis)
1 | public static void main(String[] args) throws InterruptedException { |
sleep()方法
Thread.sleep(long millis)
当 一个执行中的线程调用了Thread的sleep方法后,调用线程会暂时让出指定时间的执行权,也就是在这期间不参与 CPU 的调度, 但是该线程所拥有的锁是持有不让出的。(不会释放锁)
yield()方法
Thread.yield()
当一个线程调用 yield 方法时,当前线程会让出 CPU 使用权,然后处于就绪状态,线程调度器会从线程就绪队列里面获取一个线程优先级最高的线程,也有可能会调度到刚刚让出CPU的那个线程来获取CPU执行权。
interrupt()方法(线程中断)
通过设置线程的中断标志并不能直接终止该线程的执行 ,而是被中断的线程根据中断状态自行处理。
interrupt()方法:
- 设置线程状态,
- 如果线程处于wait、sleep、join而被阻塞挂起,则会抛出InterruptedException
1 | public static void testInterrupt() throws InterruptedException { |
正确的线程退出方法
1 | ... |
线程组
Java中使用ThreadGroup来表示线程组,它可以对一批线程进行分类管理,Java允许程序直接对线程组进行控制。默认情况下,所有的线程都属于主线程组。
中断线程组,线程组会去中断组里的所有线程。
1 | public class MyTest { |
ThreadLocal
Thread类中有一个threadLocals和一个inheritableThreadLocals,它们都是ThreadLocalMap类型的变量,而 ThreadLocalMap 是一个定制化的Hashmap。 在默认情况下,每个线程中的这两个变量都为 null,只有当前线程第一次调用ThreadLocal的set或者get方法时才会创建它们。
InheritableThreadLocal继承自 ThreadLocal , 其提供了一个特性,就是让子线程可 以访问在父线程中设置的本地变量 。
1 |
|
结合源码去看,实际上set是把threadLocal作为key,“abc”作为value,加入到线程的hashMap里面。
ThreadLocal使用不当可能导致内存泄漏
当一个线程调用ThreadLocal的 set 方法设置变量时,当前线程的ThreadLocalMap里就会存放一个记录, 这个记录的 key 为 ThreadLocal 的弱引用, value则为设置的值 。
如果当前线程一直存在且没有调用 ThreadLocal 的 remove 方法,并且这时候在其他地方还有对 ThreadLocal 的引用 ,则当前线程的 ThreadLocalMap 变量里面会存在对ThreadLocal 变量的引用和对 value 对象的引用 ,它们是不会被释放的,这就会造成内存泄漏。
如上述代码所示,存在静态的threadLocal强引用,所以不会被释放。
锁概念
乐观锁与悲观锁
乐观锁和悲观锁是在数据库中引入的名词,但是在并发包锁里面也 引入了 类似的思想。
悲观锁指对数据被外界修改持保守态度 ,认为数据很容易就会被其他线程修改 ,所以在数据被处理前先对数据进行加锁,并在整个数据处理过程中,使数据处于锁定状态 。
乐观锁是相对悲观锁来说的,它认为数据在一般情况下不会造成冲 突 ,所以在访问记录前不会加排它锁,而是在进行数据提交更新时,才会正式对数据冲突与否进行检测 。
乐观锁并不会使用数据库提供的锁机制,一般在表 中 添加 version 宇段或者使用业务状态来实现 。乐观锁直到提交时才锁定,所以不会产生任何死锁 。
公平锁与非公平锁
根据线程获取锁的抢占机制,锁可以分为公平锁和非公平锁。
- 公平锁:线程获取锁的顺序是按照线程请求锁的时间早晚来决定的,也就是最早请求锁的线程将最早获取到锁 。
- 非公平锁:在运行时闯入,也就是先来不一定先得。
ReentrantLock 提供了公平和非公平锁的实现。
非公平锁下,后来的线程会先去尝试获取一次锁,获取不到才会挂起。
公平锁,后来的线程先判断AQS队列里面是否有在排队的线程,没有的话去获取锁,有的话直接排队挂起。
在没有公平性需求的前提下尽量使用非公平锁,因为公平锁会带来性能开销 。
独占锁和共享锁
读锁、写锁。
自旋锁
自旋锁是:
当前线程在获取锁时,如果发现锁已经被其他线程占有,它不马上阻塞自己,在不放弃 CPU 使用权的情况下,多次尝试获取,很有可能在后面几次尝试中其他线程己经释放了锁。
死锁概念
