线程生命周期
- New:尚未启动;
- Runnable:可运行,等待cpu调度;
- Blocked:阻塞,处于synchronized同步代码或方法中被阻塞;
- Waiting:等待,Object.wait、Thread.join、LockSupport.park;
- Timed Waiting:具有指定等待时间的等待线程,Thread.sleep、Object.wait、Thread.join、LockSupport.parkNanos、LockSupport.parkUntil;
- Terminated:终止。
线程优先级
- 线程的优先级可以理解为线程抢占 CPU 时间片的概率,优先级越高的线程优先执行的概率就越大,但并不能保证优先级高的线程一定先执行
- 在 Thread 源码中和线程优先级相关的属性有 3 个
1 | // 线程可以拥有的最小优先级 |
默认情况下,每一个线程都会分配一个优先级 NORM_PRIORITY(5)。
- 在程序中我们可以通过 Thread.setPriority() 来设置优先级
1 | public final void setPriority(int newPriority) { |
线程中止
stop(弃用):强制中止线程,并且清除监控器锁monitor
的信息,因而可能造成这些对象处于不一致的状态,而且这个方法造成的 ThreadDeath
异常不像其他的检查期异常一样被捕获
interrupt:
- 通知线程停止,而不是强制停止,线程可以进行停止前的释放资源, 完成必要的处理任务
- 在线程内可通过
isInterrupted()
判断终端并进行相应处理 - 若线程处于等待或堵塞状态, 则会抛出
InterruptedException
创建线程
Java 提供了三种创建线程的方法:
- 通过实现 Runnable 接口;
- 通过继承 Thread 类本身;
- 通过 Callable 和 Future 创建线程。
通过实现 Runnable 接口来创建线程
创建一个线程,最简单的方法是创建一个实现 Runnable 接口的类。
为了实现 Runnable,一个类只需要执行一个方法调用 run(),声明如下:
1 | public void run() |
你可以重写该方法,重要的是理解的 run()
可以调用其他方法,使用其他类,并声明变量,就像主线程一样。
在创建一个实现 Runnable 接口的类之后,你可以在类中实例化一个线程对象。
Thread 定义了几个构造方法,下面的这个是我们经常使用的:
1 | Thread(Runnable threadOb,String threadName); |
这里,threadOb 是一个实现 Runnable 接口的类的实例,并且 threadName 指定新线程的名字。
新线程创建之后,你调用它的 start() 方法它才会运行。
实例:
1 | class RunnableDemo implements Runnable { |
通过继承Thread来创建线程
创建一个线程的第二种方法是创建一个新的类,该类继承 Thread 类,然后创建一个该类的实例。
继承类必须重写 run() 方法,该方法是新线程的入口点。它也必须调用 start() 方法才能执行。
该方法尽管被列为一种多线程实现方式,但是本质上也是实现了 Runnable 接口的一个实例。
通过 Callable 和 Future 创建线程
- 创建 Callable 接口的实现类,并实现 call() 方法,该 call() 方法将作为线程执行体,并且有返回值。
- 创建 Callable 实现类的实例,使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封装了该 Callable 对象的 call() 方法的返回值。
- 使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程。
- 调用 FutureTask 对象的 get() 方法来获得子线程执行结束后的返回值。
创建线程的三种方式的对比
- 采用实现 Runnable、Callable 接口的方式创建多线程时,线程类只是实现了 Runnable 接口或 Callable 接口,还可以继承其他类。
- 使用继承 Thread 类的方式创建多线程时,编写简单,如果需要访问当前线程,则无需使用 Thread.currentThread() 方法,直接使用 this 即可获得当前线程,但是在多线程时受到局限。
线程通信
文件共享、网络共享、共享变量、JDK提供的线程协调API。
线程API
suspend,resume(弃用)
死锁示例:
- 消费者和生产者使用同一把锁(同步);suspend不会释放
monitor
; - 生产者执行比消费者快;
wait/notify
只能由同一对象锁的持有者线程调用,也就是卸载同步块里面,否则会抛出IllegalMonitorStateException异常。
wait:使线程进入等待队列,并且释放对象锁;
notify/notifyAll:唤醒。
注意:虽然wait会自动解锁,但是对顺序有要求,要在notify之前。
1 | public class WaitNotifyCase { |
执行结果:
1 | thread A is waiting to get lock |
分析:
synchronized
代码块通过javap
生成的字节码中包含monitorenter
和monitorexit
指令, 执行monitorenter
指令可以获取对象的monitor
, 在wait()
接口注释中有标明The current thread must own this object's monitor
, 所以通过synchronized
该线程持有了对象的monitor
的情况下才能调用对象的wait()
方法wait()
接口注释中还提到调用wait()
后该线程会释放持有的monitor
进入等待状态直到被唤醒, 被唤醒的线程还要等到能重新持有monitor
才会继续执行- 线程状态变化:
- 调用
wait()
: RUNNABLE -> WAITING - 调用
notify()
:- WAITING -> BLOCKED -> RUNNABLE
- WAITING -> RUNNABLE
- 具体看 JVM 实现和策略配置
- 调用
monitor
在 HotSpot
虚拟机中 (1.7 版本),monitor
采用 ObjectMonitor
实现
1 | ObjectMonitor() { |
- 每个线程都有两个
ObjectMonitor
对象列表,分别为free
和used
列表,如果当前free
列表为空,线程将向全局global ListLock
请求分配ObjectMonitor
ObjectMonitor
对象中有两个队列:_WaitSet
和_EntryList
,用来保存ObjectWaiter
对象列表;_owner
指向获得ObjectMonitor
对象的线程- 每个等待锁的线程都会被封装成
ObjectWaiter
对象- ObjectWaiter 对象是双向链表结构,保存了_thread(当前线程)以及当前的状态 TState等数据
- ObjectWaiter 对象是双向链表结构,保存了_thread(当前线程)以及当前的状态 TState等数据
ObjectMonitor
获得锁是通过void ATTR enter(TRAPS)
方法
1 | void ATTR ObjectMonitor::enter(TRAPS) { |
ObjectMonitor
释放锁是通过void ATTR exit(TRAPS)
方法
1 | void ATTR ObjectMonitor::exit(TRAPS) { |
lock.wait()
方法最终通过ObjectMonitor
的void wait(jlong millis, bool interruptable, TRAPS)
实现:- 将当前线程封装成
ObjectWaiter
对象 node - 通过
ObjectMonitor::AddWaiter
方法将 node 添加到_WaitSet
列表中 - 通过
ObjectMonitor::exit
方法释放当前的ObjectMonitor
对象,这样其它竞争线程就可以获取该ObjectMonitor
对象 - 最终底层的
park
方法会挂起线程
- 将当前线程封装成
lock.notify()
方法最终通过ObjectMonitor
的void notify(TRAPS)
实现:- 如果当前
_WaitSet
为空,即没有正在等待的线程,则直接返回 - 通过
ObjectMonitor::DequeueWaiter
方法,获取_WaitSet
列表中的第一个ObjectWaiter
节点 - 根据不同的策略,将取出来的
ObjectWaiter
节点加入到_EntryList
或则通过Atomic::cmpxchg_ptr
指令进行自旋操作_cxq
- 如果当前
park/unpark
park:线程等待“许可”;
unpark:为指定线程提供“许可”。
不要求调用顺序。
多次调用unpark之后,再调用park,线程会直接运行,但不会叠加。
注意:同样有同步锁问题。
伪唤醒
为防止伪唤醒,需要用while循环来检查等待条件。
Thread常用方法
非静态方法
序号 | 方法描述 |
---|---|
1 | public void start() 使该线程开始执行;Java 虚拟机调用该线程的 run 方法。 |
2 | public void run() 如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回。 |
3 | public final void setName(String name) 改变线程名称,使之与参数 name 相同。 |
4 | public final void setPriority(int priority) 更改线程的优先级。 |
5 | public final void setDaemon(boolean on) 将该线程标记为守护线程或用户线程。 |
6 | public final void join(long millisec) 等待该线程终止的时间最长为 millis 毫秒。 |
7 | public void interrupt() 中断线程。 |
8 | public final boolean isAlive() 测试线程是否处于活动状态。 |
静态方法
序号 | 方法描述 |
---|---|
1 | public static void yield() 暂停当前正在执行的线程对象,并执行其他线程。 |
2 | public static void sleep(long millisec) 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。让线程进入到 TIMED_WAITING 状态,并停止占用 CPU 资源,但是不释放持有的 monitor ,直到规定事件后再执行,休眠期间如果被中断,会抛出异常并清除中断状态。TimeUnit.SECONDS.sleep() 比 Thread.sleep() 多了非负数判断 |
3 | public static boolean holdsLock(Object x) 当且仅当当前线程在指定的对象上保持监视器锁时,才返回 true。 |
4 | public static Thread currentThread() 返回对当前正在执行的线程对象的引用。 |
5 | public static void dumpStack() 将当前线程的堆栈跟踪打印至标准错误流。 |
相关问题
start与run
start()
方法属于Thread
自身的方法,并且使用了synchronized
来保证线程安全run()
方法为Runnable
的抽象方法,重写的run()
方法其实就是此线程要执行的业务方法- 调用
start()
方法是另起线程来运行run()
方法中的内容
join
1 | public final synchronized void join(long millis) |
- 本质是用
wait()
实现 - JVM 的 Thread 执行完毕会自动执行一次
notifyAll()
- 所以不建议在程序中对 Thread 对象调用 wait/notify, 可能会造成干扰
yield
- 状态依旧是 RUNNABLE, 不保证释放 CPU 资源。
yield
使当前线程从执行状态(运行状态)变为可执行态(就绪状态)。cpu会从众多的可执行态里选择,也就是说,当前也就是刚刚的那个线程还是有可能会被再次执行到的,并不是说一定会执行其他线程而该线程在下一次中不会执行到了。 Thread.sleep(0)
可以重新触发 CPU 的竞争, 而yield
不一定
线程封闭
数据被封闭在各自线程中,避免被同步。
ThreadLocal
线程级别变量,在每个线程都有独立的副本。
局部变量
局部变量固有属性,存储在执行线程的栈中。
CPU缓存
多级缓存
L1 Cache,L2 CPU外部放置高速存储器,L3 多核共享的内置缓存。CPU读取数据,L1->L2->L3->内存->外存储器。
缓存同步协议
MESI协议,对每条缓存有状态位:
- 修改态;
- 专有态;
- 共享态;
- 无效态。
CPU性能优化
运行时指令重排,可能将读缓存命令优先执行,因为写缓存时可能区块正被其他CPU占用。
内存屏障
写内存屏障:在指令后插入Store Barrier,能让写入缓存中的最新数据更新到主内存,让其他线程可见,CPU就不会因为性能考虑而去对指令重排。
读内存屏障:在指令前插入Load Barrier,可以让高速缓存中的数据失效,强制重新从主内存加载数据,让CPU缓存与主内存保持一致,避免了缓存导致的一致性问题。
线程安全
jvm内存模型
描述线程之间如何通过内存(memory)来进行交互,描述了java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取出变量这样的底层细节。
具体说来,JVM中存在一个主存区(Main Memory或Java Heap Memory),对于所有线程进行共享,但线程不能直接操作主内存中的变量,每个线程都有自己独立的工作内存(Working Memory),里面保存该线程使用到的变量的副本( 主内存中该变量的一份拷贝)
规定:线程对共享变量的读写都必须在自己的工作内存中进行,而不能直接在主内存中读写。不同线程不能直接访问其他线程的工作内存中的变量,线程间变量值的传递需要主内存作为桥梁。
可见性
可见性: 一个线程对共享变量值的修改,能够及时的被其他线程看到
线程可见性原理:
线程一对共享变量的改变想要被线程二看见,就必须执行下面两个步骤:
①将工作内存1中的共享变量的改变更新到主内存中
②将主内存中最新的共享变量的变化更新到工作内存2中。
指令重排序: 代码书写的顺序与实际执行的顺序不同,指令重排序是编译器或处理器为了提高程序性能而做的优化。
1.编译器优化的重排序(编译器优化)
2.指令级并行重排序(处理器优化)
3.内存系统的重排序(处理器优化)
指令重排序在多线程中会造成可见性问题。
导致共享变量在线程间不可见的原因:
- 线程的交叉执行
- 重排序结合线程交叉执行
- 共享变量更新后的值没有在工作内存与主内存间及时更新
查看重排序:
class->jit运行时编译->汇编指令->指令重排序
通过设置JVM参数,打印出jit编译的内容:-server -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:+LogCompilation -XX:LogFile=jit.log
汇编指令可以通过jitwatch
查看。
保证可见性,需要满足happens-before关系:
1.程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;
2.锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作;
3.volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;
4.传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;
5.线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作;
6.线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
7.线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;
8.对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始;
根据上面的happens-before规则,显然,一般只需要使用volatile关键字,或者使用锁的机制,就能实现内存的可见性了。
synchronized
JMM规定synchronized在线程解锁前,把共享变量的最新值刷新到主存,加锁时,清空工作内存(缓存)中共享变量的值,从主存重新读取。
volatile
volatile变量每次被线程访问时,都强迫从主内存中读取该变量的值,而当变量发生变化的时候都会强迫线程将最新的值刷新到主内存中。
深入来说:通过加入内存屏障和禁止重排序优化来实现的。
- 对volatile变量执行写操作时,会在写操作后加入一条store屏障指令
- store指令会在写操作后把最新的值强制刷新到主内存中。同时还会禁止cpu对代码进行重排序优化。这样就保证了值在主内存中是最新的。
- 对volatile变量执行读操作时,会在读操作前加入一条load屏障指令
- load指令会在读操作前把内存缓存中的值清空后,再从主内存中读取最新的值。
注意:volatile实现可见性有一个条件,就是对共享变量的操作必须具有原子性。例如num++这些复合操作是3步组成,不符合原子性。
- load指令会在读操作前把内存缓存中的值清空后,再从主内存中读取最新的值。
synchronized和volatile的比较
- synchronized锁住的是变量和变量的操作,而volatile锁住的只是变量,而且该变量的值不能依赖它本身的值,volatile算是一种轻量级的同步锁
- volatile不需要加锁,比synchronized更加轻量级,不会阻塞线程。
- 从内存可见性角度讲,volatile读相当于加锁,volatilexie相当于解锁。
- synchronized既能保证可见性,又能保证原子性,而volatile只能保证可见性,无法保证原子性。
注:由于voaltile比synchronized更加轻量级,所以执行的效率肯定是比synchroized更高。在可以保证原子性操作时,可以尽量的选择使用volatile。在其他不能保证其操作的原子性时,再去考虑使用synchronized。
有序性
重排序会影响有序性。
原子性
JDK 5之前使用synchronized保证同步,JDK 5新增了Atomic包。例如,AtomicInteger代替int,getAndIncrement()以及getAndDecrement()实现原子操作的加减。
锁机制存在以下问题:
(1)在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。
(2)一个线程持有锁会导致其它所有需要此锁的线程挂起。
(3)如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能风险。
volatile是不错的机制,但是volatile不能保证原子性。因此对于同步最终还是要回到锁机制上来。
锁
定义
悲观锁(Pessimistic Lock):
每次获取数据的时候,都会担心数据被修改,所以每次获取数据的时候都会进行加锁,确保在自己使用的过程中数据不会被别人修改,使用完成后进行数据解锁。由于数据进行加锁,期间对该数据进行读写的其他线程都会进行等待。
乐观锁(Optimistic Lock):
每次获取数据的时候,都不会担心数据被修改,所以每次获取数据的时候都不会进行加锁假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。利用CAS。
自旋锁:为了不放弃CPU执行事件,循环地使用CAS技术对数据尝试进行更新,直至成功。
CAS算法 即compare and swap(比较与交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。
适用场景
悲观锁:比较适合写入操作比较频繁的场景,如果出现大量的读取操作,每次读取的时候都会进行加锁,这样会增加大量的锁的开销,降低了系统的吞吐量。
乐观锁:比较适合读取操作比较频繁的场景,如果出现大量的写入操作,数据发生冲突的可能性就会增大,为了保证数据的一致性,应用层需要不断的重新获取数据,这样会增加大量的查询操作,降低了系统的吞吐量。
synchronized 关键字-监视器锁monitor lock
synchronized的底层是使用操作系统的mutex lock实现的。
synchronized 关键字的使用
1、同步代码块
1)可以定义一个常量作为锁对象
2)只要是同一个锁对象,同步代码块可以在不同的方法体中也能同步
2、同步实例方法
直接使用synchronized修饰实例方法
把整个方法体作为同步代码块,默认的锁对象就是this对象
3、同步静态方法
就是使用synchronized修饰静态方法
把整个方法体作为同步代码块,默认的锁对象是——当前类的运行时类对象,简单的理解为把当前类的字节码文件作为锁
synchronized用的锁是存在Java对象里的
锁的是SynchronizedDemo 对象
1 | public class SynchronizedDemo { |
谁调用的这个方法锁的就是哪个对象
1 | public class SynchronizedDemo { |