Java线程实现/创建方式
线程实现方式包括继承Thread类与实现Runnable接口
线程启动的唯一方式是Thread::start,start()是一个本地方法,执行之后将启动一个新线程并等待CPU调度运行run()方法
如果涉及有返回值的线程方法,则需要实现callable接口,并以ExecutorService方式调用
4种线程池
Java线程池顶级接口为Executor,仅包含execute(Runnable command)一个方法,真正实现线程池功能的为ExecutorService接口。线程池主要实现类有:
ThreadPoolExecutor:
ScheduledThreadPoolExecutor:
线程池工具类Executors提供构造4中具体含义的线程池:
newCachedThreadPool:提供一个带60s缓冲的线程池,设定初始线程数与最大线程数
newFixedThreadPool:固定大小线程池,线程执行结束如果无新任务立即销毁
newScheduledThreadPool:定时调度线程池
newSingleThreadExecutor:单线程池
线程生命周期与终止线程的4种方式
新建:使用new关键字创建线程之后,处于新建状态JVM分配内存并初始化属性
就绪:调用start()方法之后,JVM为其创建虚拟机栈和程序计数器并等待CPU调度
运行:获得CPU调度,执行run()方法体
阻塞:
等待阻塞(o.wait->等待对列):运行的线程执行 o.wait()方法,JVM 把该线程放入等待队列中
同步阻塞(lock->锁池):运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线程放入锁池(lock pool)中。
其他阻塞(sleep/join):运行的线程执行 Thread.sleep(long ms)或 t.join()方法,或者发出了 I/O 请求时,JVM 会把该线程置为阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时或者 I/O处理完毕时,线程重新转入可运行(runnable)状态。
死亡:
正常结束:run()或 call()方法执行完成,线程正常结束。
异常结束:线程抛出一个未捕获的 Exception 或 Error。
调用 stop:直接调用该线程的 stop()方法来结束该线程—该方法通常容易导致死锁,不推荐使用。
线程终止方式:
正常运行结束:线程运行run方法完毕,或设置终止状态,在线程运行过程中更改状态完成循环跳出
interrupt方法终止:
调用线程的interrupt方法,会设置线程的终止状态为true,并产生InterruptedException异常,可以通过isInterrupt()方法能取得终止状态。但设置interrupt后在catch中捕获异常,并不会直接终止线程,需要在异常补货之后使用break终止循环状态;另外,使用ExecutorService启动的线程对interrupt不生效
stop方法终止:废弃,会发生不可预知的异常情况
sleep,wait,start,run,join
Java锁机制
CAS
Compare and Swap,更改前先与原来的值比对,如果不一致则更新失败。多应用于Unsafe类中,采用自旋锁+CAS机制更新
AQS
AbstractQueuedSynchronizer,一个抽象类,定义了一套多线程下访问共享资源的方案。内部主要包含一个volatile int state的属性标记当前状态,和一个FIFO的阻塞队列用来存储等待资源的线程。state状态更新是采用自旋锁+CAS机制。AQS中设置了3中共享方式:独享,共享,独享+共享,主要取决于各个实现类内部的实现方案。主要实现类有:ReentrantLock(独享),CountDownLatch(共享),ReentrantReadWriteLock(独享+共享)
Java锁类型
乐观锁
读多写少,读取的时候不加锁,写入是通过CAS机制比较版本,加锁写入。
悲观锁
悲观锁在写多读少的场景下,任务每次读写前其他线程都可能更新了数据所以要加锁读写,典型如synchronized。
自旋锁
如果持有锁的线程可能在很短的时间内释放锁,则等待竞争锁的线程就不必进入阻塞状态,而是自旋等待锁释放后获取锁,避免线程状态切换的损耗。
优点:合适场景下,避免线程切换上下文带来的消耗
缺点:如果线程持锁时间长,或锁竞争激烈则CPU消耗更大。
自适应自旋锁:JDK6开始JVM对于自旋执行时间根据状态判断确定,而非之前的固定值。
Synchronized同步锁
Synchronized关键字将任意一个非null对象当作锁。属于独占式悲观锁,也属于可重入锁,非公平锁
Synchronized作用范围:
作用于方法上,则将当前对象this加锁,访问当前对象的同步方法时会阻塞
作用于静态方法上,则将Class对象加锁,由于Class对象存储于方法区,属于全局共享对象,因此静态方法锁相当于一个类的一个全局锁,对于此类的所有对象调用静态同步方法的线程都会加锁。
作用于属性对象,锁住的是以此特殊对象为锁的代码块。
Synchronized实现:
ContentionList:竞争队列,一个非具体的队列存放竞争线程,从头部放入尾部取出
EntryList:为了增加性能,将有资格竞争锁资源的一部分线程从CotentionList中取出
WaitSet:阻塞线程存放集合
OnDeck:当前竞争锁资源的线程,每次只有一个线程可以竞争资源
Owner:当前持有锁的线程
!Owner:释放锁的线程
当一个线程开始竞争当前锁资源时,先自旋尝试获取锁资源(非公平性),如果超过自旋时间阀值还未获取到资源,则从头部插入ContentionList;由于ContetionList中太多线程不断通过CAS并发访问会导致资源消耗,因此JVM会将一部分线程转移至EntryList中;Owner线程Unlock时,JVM从ContentionList尾部将一部分线程转移至EntryList,并指定某一个线程为OnDeck(一般是最先进来的),OnDeck线程并不直接获得锁,而是获得竞争权(与自旋的线程一并竞争),虽然失去公平性,但是增加了吞吐量;OnDeck线程获得锁资源后变成Owner,如果出发wait后阻塞进入WaitSet,直到notify再次唤醒会重新进入EntryList,ContentionList,EntryList,WaitSet中的线程都属于阻塞线程;
锁升级:从偏向锁升级到轻量锁再到重量锁,这也是JDK8开始对synchronized的优化
Image.png
ReetrantLock
ReetrantLoc是一种可重入锁,与Synchronized功能基本相同,只不过
额外提供了诸如可响应中断锁、可轮询锁请求、定时锁,指定唤醒等功能,避免多线程死锁的方法。
需要手动加锁与解锁
private final Lock lock = new ReetrantLock(false);// 默认非公平锁,可设置为true,即公平锁
private final Condition condition = lock.newCondition();
try{
lock.lock();// 锁定
...
condition.await();// 等待,等同于Object中的wait()方法
...
condition.signal();// 唤醒,等同于Object中的notify(),ReentrantLock可以指定唤醒条件,Object只能随机唤醒
condition.sinalAll()// 唤醒所有等待线程,等同于Object中的notifyAll()
}finally{
lock.unlock(); // 解锁,需要手动操作否则导致死锁
}
Samaphore信号量
AtomicInteger
自旋锁+CAS机制,属于乐观锁,独占锁
volatile关键字
保证线程可见性
多线程读取volatile修饰的变量时不再加载到线程缓冲区,而是直接读取主内存的变量值
多线程写入volatile修饰的变量时,会主动通知其他线程刷新
不保证原子性
volatile修饰的变量不能保证原子性,例如i++在编译后的字节码指令分为多个步骤,volatile不能保证这一组指令运行不会被打断,因此多个线程同时计算i++时通常不能得到预期结果。java使用锁机制来保证原子性,如synchronized或lock。
禁止指令重排序
jvm在编译和运行期会优化字节码指令,对于不影响运行结果的字节码顺序可能有变化,例如实例化对象时分为:
申请内存空间
初始化变量
变量关联
但步骤2和3没有关联性因此可能会被重排序为1,3,2这时当外部线程判断对象是否为空时返回false,然而此时的对象还未创建完成导致无法使用。
如果使用volatile关键字修饰后,虚拟机会保证上述步骤按顺序完成这时获取到的对象才能保证可用。
注:
双重校验的线程同步单例模式变量必须使用volatile修饰
当线程调用了synchronized修饰的方法时,会主动从主内存刷新变量值