Day06

1. synchronized和Lock有什么区别

  1. 使用方式:synchronized是隐式锁,自动释放锁,在这种同步方式下,我们需要依赖Monitor(同步监视器)来实现线程通信。作用在静态方法上,类的Class对象;实例方法上,当前实例(this);代码块上,则需要在关键字后面的小括号里显式地指定一个对象作为Monitor;而Lock接口是显式锁,手动释放,Lock接口具备更大的灵活性
  2. 功能特性:Lock弥补了synchronized的不足,它新增的特性包括:可中断地获取锁,非阻塞地获取锁,可超时地获取锁
  3. 实现机制:synchronized的底层是采用java对象头来存储锁的信息,lock实现类是基于AQS实现的。早期synchronized的性能很差,只有无锁和有锁两种状态,但是后期synchronized引入了锁升级机制,有了很大改善。锁现在有四种状态:无锁,偏向锁,轻量级锁,重量级锁,减少了获取锁和释放锁带来的性能消耗

2. 说说synchronize的用法及原理

  1. synchronized可以修饰普通方法,静态方法和同步代码块

  2. 偏向锁原理:当线程第一次访问同步代码块并获取锁时,检测Mark Word是否为可偏向状态,即是否是偏向锁1,锁标识为01的偏向锁,若为可偏向状态,则判断Mark Word中的线程ID是否为当前线程ID,如果是,执行同步代码块,否则通过CAS操作将Mark Word的线程ID替换为当前线程ID,执行同步代码块。持有偏向锁的线程以后每次进入这个锁相关的同步块时,jvm都不需要进行任何同步操作只需要判断id是否一致,偏向锁效率高。

  3. 轻量级锁原理:1.判断当前对象是否处于无锁状态,如果是,则JVM首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,将对象的Mark Word复制到栈帧中的Lock Record中,将Lock Record中的owner指向当前对象。2.JVM利用CAS操作尝试将对象的Mark Word存储指向Lock Record的指针,如果成功表示竞争到锁,则将锁标志变成00变为轻量级锁,执行同步操作。3.如果失败,则判断当前对象的Mark Word是否指向了当前线程的栈帧,如果是则表示当前线程已经持有当前对象的锁,则直接执行同步代码块;否则只能说明该锁对象已经被其他线程抢占了,这时轻量级锁会膨胀到重量级锁,锁标志位变为10,后面等待的线程将会进入阻塞状态。

3. 说说你对AQS的理解

  1. AQS队列同步器,用来构建锁的基础框架,Lock实现类都是基于AQS实现的
  2. AQS是基于模版方法模式进行设计的,所以锁的实现需要继承AQS并重写它指定的方法
  3. AQS内部定义了一个FIFO的队列来实现线程同步,同时还定义了同步状态记录锁的信息
  4. AQS的模版方法,将管理同步状态的逻辑提炼出来形成标准流程,这些方法主要包括:独占式获取同步状态、独占式释放同步状态、共享式获取同步状态、共享式释放同步状态
  5. AQS通过设置一个由volatile修饰的Int类型的互斥变量state来实现互斥同步,state=0时表示该锁可以被获取,state>=1时表明该锁已经被获取。当当前锁为空闲时,多个线程同时使用CAS方式去修改state的值(保证了原子性,可见性),最后只有一个线程修改成功并获得锁

4.说说你对线程池的理解

  1. 线程池可以有效管理线程:它可以管理线程的数量,可以避免无节制的创建线程,导致超出系统负荷直至崩溃。它还可以让线程复用,可以大大减少创建和销毁线程所带来的开销。
  2. 线程池需要依赖一些参数来控制任务的执行流程,其中最重要的参数有:corePoolSize(核心线程数)、workQueue(等待队列)、maximumPoolSize(最大线程数)、handler(拒绝策略)、keepAliveTime(空闲线程存活时间)。当我们
  3. 当我们向线程池提交一个任务后,线程池按照如下步骤处理这个任务:1.判断线程数是否达到corePoolSize,如果没有则新建线程执行该任务,否则进入下一步;2.判断等待队列是否已满,若没有则将任务放入等待队列,否则进入下一步;3.判断线程数是否达到了maximumPoolSize,如果没有则新建线程执行该任务,否则进入下一步;4.采用初始化线程池指定的拒绝策略,拒绝执行该任务;5.新建的线程处理完当前任务后,不会立刻关闭,而是继续处理等待队列中的任务。如果线程的空闲时间达到了keepAliveTime,则线程池会销毁一部分线程,将线程数量收缩至corePoolSize。
  4. 等待队列可以有界也可以无界。若指定了无界的队列,则线程池永远无法进入第三步,相当于废弃了maximumPoolSize参数。这种用法是十分危险的,如果任务在队列中产生大量的堆积,就很容易造成内存溢出。JDK为我们提供了一个名为Executors的线程池创建工具,该工具创建出来的就是带有无界队列的线程池,所以一般在工作中我们是不建议使用这个类来创建线程池的
  5. 拒绝策略主要有4个:让调用者自己执行任务、直接抛出异常、丢弃任务不做任何处理、删除队列中最老的任务并把当前任务加入队列,我们也可以基于接口实现自己的拒绝策略
  6. 线程池的生命周期包含5个状态:RUNNING、SHUTDOWN、STOP、TIDING、TERMINATED。这5种生命状态的状态值分别是:-1,0,1,2,3。在线程池的生命周期中,它的状态只能由小到大迁移,是不可逆的。1.RUNNING:表示线程池正在运行。2.SHUNDOWN:执行shutdown()时进入该状态,此时队列不会被清空,线程池会等待任务执行完毕。3.STOP:执行shutdownNow()时进入该状态,此时线程池会清空队列,不再等待任务的执行。4.TIDING:当线程池及队列为空时进入该状态,此时线程池会执行钩子函数,目前该函数是一个空的实现。5.TERMINATED:钩子函数执行完毕后,线程进入该状态,表示线程池已经死亡

5.说说volatile的用法及原理

  1. volatile是一个Java内存的关键字,主要用来解决内存可见性问题
  2. 当多个线程操作共享变量时,会存在数据不一致的问题。因为在JMM中,有工作内存和主内存的区分,每个线程都有自己的工作内存,也叫本地内存,里边存储了变量的副本,当对一个普通变量进行写操作时,不会立即同步到主内存,因此其他线程不能立即看到最新的值,这就是内存可见性问题
  3. 通过volatile关键字修饰的变量可以解决这个问题,JVM底层是通过内存屏障来实现,当对一个变量进行读取时,插入读屏障,表示先从主内存读取,当对一个变量进行写入后,插入写屏障,表示立即同步到主内存,这样就解决了可见性问题,保证了数据一致性
  4. 但volatile不能解决原子性问题,因为每个线程都有工作内存,各自计算完后同步到主内存会存在覆盖问题
  5. volatile的使用场景一般用于一个线程读,一个线程写的情况下,例如标志位这类。但同时读写一个volatile的场景不适合,因为会出现原子性问题
  6. volatile也能保证变量有序性:在java中由于寄存器和内存处理速度存在巨大差异,所以java为了提升运行速度会将编译好的代码进行一定程度的重排,但不影响在独立线程环境下的运行结果,但是在多线程环境中有可能出错。volatile则可以避免这种指令重排从而实现即使是多线程也能达到顺序一致的效果
最后修改:2024 年 06 月 08 日
如果觉得我的文章对你有用,请随意赞赏