多线程记录

多线程记录

线程

线程状态图

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做了很多优化,这两个性能上差不太多

  • synchronizedjvm实现的,ReentrantLockjdk实现的。
  • synchronized是不公平的,ReentrantLock可以设置公平。
  • synchronized等待不可中断,ReentrantLock等待时可中断。
  • ReentrantLock可绑定多个Condition

线程使用

  • 继承Thread

  • 实现Runnable接口

  • 实现Callable接口

区别
  • Callable通过FutureTask封装可实现返回值

  • CallableRunnanble不可直接执行,需要被作为任务放入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);
    }
    
说明:
  • FixedThreadPoolSingleThreadExecutor参数非常类似。他们的核心线程数与最大线程数都一致,所以闲置线程保持存活时间的值无效,队列都是一个无界的阻塞队列,也就是说使拒绝策略失效,能一直吃入线程,直到内存溢出。唯一的区别是,FixedThreadPool可以指定核心线程数的大小,而SingleThreadExecutor的核心线程数大小固定为1。

  • newCachedThreadPoolnewScheduledThreadPool最大线程数为Integer.MAX_VALUE也有可能会导致内存溢出。

  • ScheduledThreadPool 接受的是ScheduledFutureTask,可以延时执行task

  • 窃取线程池与其他不太一样,它使用的是ForkJoinPool,可以接受ForkJoinTask。它会按照指定参数把任务分配出去多个队列(不指定参数默认获取CPU内核数),每个队列执行完毕后会从其他的ForkJoinWorkerThreadworkQueue里拉出来一个任务执行,直到所有的队列都被执行完毕。

  • 线程池虽然阻塞队列使用的是LinkedBlockingQueue,但查看源码,在addWorker时会尝试获取可用线程,如果获取到,直接执行。也就是说为了保证执行的总效率并不保证线程先入先出的顺序。

  • 执行流程

    activeThreads小于corePoolSize
    activeThreads=corePoolSize
    Queue.size=settingMaxWorkQueue.size
    activeThreads小于maximumPoolSize
    activeThreads=maximumPoolSize
    addTask
    直接执行
    addQueue
    tryNewThread
    newThread
    RejectedExecutionHandler

自定义线程池参数解释

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 wordmark word中含有获取锁的线程信息,在该线程加锁解锁时,不去改变标志位,只要每次尝试获取锁时对比一下mark word中的线程信息,就可以确定是否可以执行
  • 如果偏向锁尝试被竞争获取,会查询持有偏向锁的线程是否还存在,如果偏向锁的线程不在活动状态,直接撤销标识位,偏向锁会尝试调整偏向。如果发生实际的竞争,锁会升级
  • jdk15之后关闭了偏向锁,jdk21无法使用偏向锁
  • jdk11测试发现,如果一开始加了biased,调用被锁类的hashcode方法,锁会升级为fat lock。如果先调用hashcode再加锁,会直接使用thin lock,只要对象头信息内存储了hashcode方法,就无法再偏向。
    hashcode示例图1
    hashcode示例图2
fat lock
  • 查看反编译的源码可见,对于加锁解锁操作是使用MonitorenterMonitorexit指令操作monitor,并且依靠该monitor的计数器实现可重入锁功能。
  • 如果对象的monitor获取失败,该线程进入BLOCKED,直到占有者释放后去队列中执行任务线程

ReentrantLock

ReentrantLock是Lock的一个独占锁实现,它主要是通过AQS框架实现(AbstractQueuedSynchronizer),AQS使用UnsafeCAS维护了用来同步信息的信号state,对于线程的调度通过LockSupport.park/unpark实现。

LockSupport

LockSupport是用来创建锁的基本线程阻塞原语。它内部调用的UNSAFE.park操作,可以添加一个blocker,这个blocker没理解有什么特殊作用,笔者只是当他做个线程变量。上面已经记录了关于sleepwaitawait的记录,这里简单区别一下park

  • 首先parksleep一样,不去释放锁资源,至于Lock里的await释放锁资源是做了另外的操作。
  • parksleep不一样的是可以被动唤醒。
  • park的特殊1:对于Interrupt中断不需要处理InterruptedException中断异常。
  • park的特殊2: unpark可以先于park有效执行,相当于一个信号量,但是最多只能存储一次。notify先于wait执行会报错。
AQS(AbstractQueuedSynchronizer)

AQS是一个用来构建锁和同步器的框架,基于模板方法模式设计。内部维护了state用来做线程同步竞争使用的资源,关于state的变更使用CAS。设计有加锁、解锁方法:tryAcquiretryReleasetryAcquireSharedtryReleaseSharedAQS对于锁的设计,并不只是简单的加锁、解锁。还可以以共享的方式使用锁,关于共享锁的应用可以查看CountDownLatchCyclicBarrierReadWriteLock

数据结构

AQS内部有个Node类,Node类内部维护了节点状态、对应的线程、共享模式下共享node地址。基于此Node类内部维护了一个虚拟的双向队列Sync queue做同步任务队列,还有一个非必须的单向的Condition queue做条件链表。

AQS内部类conditionObject实现了Condition,用来维护条件的信号通信。内部有signalawait方法。用来通知执行信号。signal是通知睡眠线程起床,但是并不释放资源。unlock是让出锁资源,但不提醒睡眠线程起床(synchronized与lock都是)。所以如果一个线程被显示调用无参的waitawait一定要记得唤醒睡眠线程

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());

调试截图

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值