文章目录
多线程记录
线程
线程状态图
graph LR
subgraph B[Runnable]
B1((Ready))
B2((Runnable))
B1 --获取时间片--> B2
end
subgraph C[Waiting]
C1((Blocked))
C2((Time Wating))
C3((Waiting))
end
A((new))
B((Runnable))
C((Waiting))
D((Terminated))
A--Thread.start()-->B
B<--synchronized/lock--> C1
B<--Thread.sleep()--> C2
B<--Object.wait()/Object.notify()--> C3
B--finish-->D
C--Exception-->D
synchronized与lock的区别
新版本对synchronized做了很多优化,这两个性能上差不太多
synchronized是jvm实现的,ReentrantLock是jdk实现的。synchronized是不公平的,ReentrantLock可以设置公平。synchronized等待不可中断,ReentrantLock等待时可中断。ReentrantLock可绑定多个Condition。
线程使用
-
继承
Thread类 -
实现
Runnable接口 -
实现
Callable接口
区别
-
Callable通过FutureTask封装可实现返回值 -
Callable与Runnanble不可直接执行,需要被作为任务放入Thread中 -
Thread在执行时要执行start方法,对于run接口的直接调用将被视为线程内同步接口调用
线程协作
-
Thread.sleep(millisec)
-
Thread.yield()
-
Thread.join()
-
Object.wait()/Object.notify()/Object.notifyAll()
-
Condition.await()/Condition.signal()/Condition.signalAll()
区别
- Thread.yield()只是对线程调度器提醒可被切换时间片,执行线程未必一定会被切换
- sleep()/wait()/await() 可被中断,会提醒
InterruptedException异常,请在本线程内对该异常进行处理 - Thread的方法不会释放锁,wait()会释放Object锁,await()会释放Condition锁
线程池
创建线程池的方式
线程池的创建主要依靠Executors类,内置了一些线程池的创建方法,查看源码可发现他们都是根据new ThreadPoolExecutor创建,同理,也能用这个类创建自定义的线程池。可使用的预制参数创建线程池如下:
-
newFixedThreadPool(int corePoolSize) - 固定线程池
public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); } -
newSingleThreadExecutor - 单个线程池
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) { return new AutoShutdownDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(), threadFactory)); } -
newCachedThreadPool - 缓存线程池
return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); -
newScheduledThreadPool(int corePoolSize) - 延时调度线程池
public ScheduledThreadPoolExecutor(int corePoolSize,
ThreadFactory threadFactory) {
super(corePoolSize, Integer.MAX_VALUE,
DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
new DelayedWorkQueue(), threadFactory);
}
-
newWorkStealingPool(int parallelism) - 窃取线程池
public static ExecutorService newWorkStealingPool() { return new ForkJoinPool (Runtime.getRuntime().availableProcessors(), ForkJoinPool.defaultForkJoinWorkerThreadFactory, null, true); }
说明:
-
FixedThreadPool与SingleThreadExecutor参数非常类似。他们的核心线程数与最大线程数都一致,所以闲置线程保持存活时间的值无效,队列都是一个无界的阻塞队列,也就是说使拒绝策略失效,能一直吃入线程,直到内存溢出。唯一的区别是,FixedThreadPool可以指定核心线程数的大小,而SingleThreadExecutor的核心线程数大小固定为1。 -
newCachedThreadPool与newScheduledThreadPool最大线程数为Integer.MAX_VALUE也有可能会导致内存溢出。 -
ScheduledThreadPool接受的是ScheduledFutureTask,可以延时执行task。 -
窃取线程池与其他不太一样,它使用的是
ForkJoinPool,可以接受ForkJoinTask。它会按照指定参数把任务分配出去多个队列(不指定参数默认获取CPU内核数),每个队列执行完毕后会从其他的ForkJoinWorkerThread的workQueue里拉出来一个任务执行,直到所有的队列都被执行完毕。 -
线程池虽然阻塞队列使用的是
LinkedBlockingQueue,但查看源码,在addWorker时会尝试获取可用线程,如果获取到,直接执行。也就是说为了保证执行的总效率并不保证线程先入先出的顺序。 -
执行流程
自定义线程池参数解释
public ThreadPoolExecutor(int corePoolSize, // 常驻的核心线程数
int maximumPoolSize, // 该线程池容忍最大的线程数
long keepAliveTime, // 空闲线程保持存活状态时间
TimeUnit unit, // 空闲线程保持存活状态时间单位
BlockingQueue<Runnable> workQueue // 存放任务的阻塞队列
ThreadFactory threadFactory, // 线程工厂
RejectedExecutionHandler handler // 拒绝策略
)
锁
sychronized
thin lock
-
轻量级锁可以减少重量级锁导致的线程阻塞与内核态切换,实现是使用CAS自旋,默认为10次,可以指定
-XX:PreBlockSpin。关于CAS的ABA问题,内部维护了一个AtomicStampedReference.Pair,此Pair内放入的是对象引用与版本号。 -
1.6引入了一种
自适应自旋锁,意味着自旋时间不固定。根据之前同一个锁的自旋时间与拥有者的状态来判断。 -
锁的信息放入在对象头中,锁的标志位在无锁时值为
01,轻量级锁锁定后,标志位应为00,先尝试更新,如果更新失败检查记录的mark word中是否为当前线程信息,如果两个线程竞争,升级为重量级锁。
biased
- 由于实际环境中,锁总是由同一个线程多次获取,所以为了防止锁竞争带来的消耗,加入了偏向锁的概念。
- 偏向锁的实现依靠上述的
mark word,mark word中含有获取锁的线程信息,在该线程加锁解锁时,不去改变标志位,只要每次尝试获取锁时对比一下mark word中的线程信息,就可以确定是否可以执行 - 如果偏向锁尝试被竞争获取,会查询持有偏向锁的线程是否还存在,如果偏向锁的线程不在活动状态,直接撤销标识位,偏向锁会尝试调整偏向。如果发生实际的竞争,锁会升级
- jdk15之后关闭了偏向锁,jdk21无法使用偏向锁
- jdk11测试发现,如果一开始加了biased,调用被锁类的hashcode方法,锁会升级为fat lock。如果先调用hashcode再加锁,会直接使用thin lock,只要对象头信息内存储了hashcode方法,就无法再偏向。


fat lock
- 查看反编译的源码可见,对于加锁解锁操作是使用
Monitorenter和Monitorexit指令操作monitor,并且依靠该monitor的计数器实现可重入锁功能。 - 如果对象的
monitor获取失败,该线程进入BLOCKED,直到占有者释放后去队列中执行任务线程
ReentrantLock
ReentrantLock是Lock的一个独占锁实现,它主要是通过AQS框架实现(AbstractQueuedSynchronizer),AQS使用Unsafe的CAS维护了用来同步信息的信号state,对于线程的调度通过LockSupport.park/unpark实现。
LockSupport
LockSupport是用来创建锁的基本线程阻塞原语。它内部调用的UNSAFE.park操作,可以添加一个blocker,这个blocker没理解有什么特殊作用,笔者只是当他做个线程变量。上面已经记录了关于sleep、wait、await的记录,这里简单区别一下park。
- 首先
park与sleep一样,不去释放锁资源,至于Lock里的await释放锁资源是做了另外的操作。 park与sleep不一样的是可以被动唤醒。park的特殊1:对于Interrupt中断不需要处理InterruptedException中断异常。park的特殊2:unpark可以先于park有效执行,相当于一个信号量,但是最多只能存储一次。notify先于wait执行会报错。
AQS(AbstractQueuedSynchronizer)
AQS是一个用来构建锁和同步器的框架,基于模板方法模式设计。内部维护了state用来做线程同步竞争使用的资源,关于state的变更使用CAS。设计有加锁、解锁方法:tryAcquire、tryRelease、tryAcquireShared、tryReleaseShared。AQS对于锁的设计,并不只是简单的加锁、解锁。还可以以共享的方式使用锁,关于共享锁的应用可以查看CountDownLatch、CyclicBarrier、ReadWriteLock 。
数据结构
AQS内部有个Node类,Node类内部维护了节点状态、对应的线程、共享模式下共享node地址。基于此Node类内部维护了一个虚拟的双向队列Sync queue做同步任务队列,还有一个非必须的单向的Condition queue做条件链表。
AQS内部类conditionObject实现了Condition,用来维护条件的信号通信。内部有signal、await方法。用来通知执行信号。signal是通知睡眠线程起床,但是并不释放资源。unlock是让出锁资源,但不提醒睡眠线程起床(synchronized与lock都是)。所以如果一个线程被显示调用无参的wait、await一定要记得唤醒睡眠线程。
AQS的condition链表存储condition信息,一旦调用signal方法,会将满足的conditionNode转移到Sync Queue。等待获取锁资源并执行后续任务。如果调用await,会将Sync Queue中执行的当前任务挂起到Condition Queue中,直到被唤醒后才重新进入Sync Queue
其他记录
查看锁信息的工具
jol-core
可以在代码中打印对象头的信息,旧版本打印实际值,需要自行了解对象头的锁标识位置。在笔者是用的0.16及以上版本中,mark word打印信息内直接标识锁类型。
引入和使用方式如下:
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.16</version>
</dependency>
// 打印对象锁信息
System.out.println(ClassLayout.parseClass(xxx).toPrintable());
// 打印类锁信息
System.out.println(ClassLayout.parseInstance(XXX.class).toPrintable());


被折叠的 条评论
为什么被折叠?



