synchronized 关键字

synchronized通过对象内部的一个叫做监视器锁的东西来实现的。

监视器锁本身依赖于操作系统底层的互斥锁Mutex Lock来实现的。

操作系统实现线程之间的切换需要从用户态转换到核心态,成本很高,状态转换要花费比较长的时间。

这就是synchronized效率低的原因。这种依赖于操作系统MutexLock实现的锁成为重量级锁

synchronized v.s. volatile

Volatile:内存可见(不保证原子性,不保证互斥性);防止CPU指令重排(内存屏障)

扩展:

如何保证内存可见?

JMM通过控制主内存与每个线程的本地内存之间的交互,来为Java程序员提供内存可见性保证。

什么是内存屏障?

MM属于语言级的内存模型,它确保在不同的编译器和不同的处理器平台之上,通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证

happens-before规则

happens-before表示的是前一个操作的结果对于后续操作是可见的,它是一种表达多个线程之间对于内存的可见性。所以我们可以认为在 JMM 中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作必须要存在happens-before关系。这两个操作可以是同一个线程,也可以是不同的线程。

Synchronized v.s. Lock

Synchronized 对象头的偏向锁标志位?

java中的锁

对象头偏向锁

锁的状态由低到高:

无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁

只能升级,不能降级。

锁状态 存储内容 标志位
无锁 对象的hashCode,对象分代年龄,是否是偏向锁(0) 01
偏向锁 偏向线程ID,偏向时间戳,对象分代年龄,是否偏向锁(1) 01
轻量级锁 指向栈中锁记录的指针 00
重量级锁 指向互斥量的指针 11

锁的对比

优点 缺点 场景
偏向锁 加解锁不需要额外消耗,和执行非异步方法相比仅存在纳秒级差距。 若线程之间存在竞争,会代带来额外的锁撤销的消耗(撤销偏向锁会Stop The World) 使用于只有一个线程访问同步代码场景
轻量级锁 竞争的线程不会阻塞,提高了系统的响应速度 始终得不到锁的竞争线程,不断自旋会消耗CPU 追求响应速度,同步代码块执行速度非常快
重量级锁 线程竞争不使用自旋,不会消耗CPU 线程阻塞,响应时间慢 追求吞吐量,同步块执行速度较慢

锁如何升级

  • 无锁

    无锁是指线程通过无限循环来执行更新操作,如果执行成功就退出循环,如果执行失败(有其他线程更新了值),则继续执行,直到成功为止。CAS操作就属于无锁。如果从性能的角度来看,无锁状态的性能是非常高的。

  • 偏向锁

    偏向锁的前提假设是当一个线程获取锁,后面还有大概率该线程还会需要继续持有这把锁

    当有另外一个线程区尝试获取这个锁的时候,偏向模式就宣告结束。

    虚拟机启用偏向锁的参数-XX:UseBiasedLocking。如果当前偏向锁已启动,当锁对象第一次被线程获取的时候,虚拟机将会把对象头中的标志位设为01,即偏向模式。同时使用CAS操作把获取到这个锁的线程的ID记录在对象的Mark Word之中,如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁的同步块时,虚拟机都可以不用再进行同步操作了。

  • 轻量级锁

    当开始有锁的竞争了,那么偏向锁就会升级到轻量级锁;

    当获取锁冲突多,时间越长的时候,线程肯定无法继续在这里死等了,所以只好先挂起,然后等前面获取锁的线程释放了锁之后,再开启下一轮的锁竞争,而这种形式就是我们的重量级锁。

    在代码进入同步块的时候,如果此同步对象没有被锁定(锁标志位为01状态),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝。

    然后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针。如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位(Mark Word的最后2bit)将转变为00,即表示此对象处于轻量级锁定状态。 轻量级锁的前提假设是对于绝大部分的锁,在整个同步周期内都是不存在竞争的,通过CAS操作来避免时候互斥锁的开销

    关于自旋锁:

    自旋锁是一种通过让线程不释放当前的CPU执行一个忙循环,来尝试获取锁的方式。 自旋锁的前提假设是锁被其它线程占用的时间很短 。如果其它线程占用锁的时间很长,那么自旋的线程只会白白消耗处理器资源,而不会做任何有用的工作,反而带来性能上的浪费。自旋次数的默认值是10次,用户可以通过使用参数-XX:PreBlockSpin来更改。

  • 重量级锁

    当有两个及以上的线程争用同一个锁,那么轻量级锁就不再有效,要膨胀为重量级锁。锁标志的状态值变为10,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。

    • 偏向锁只有设置了-XX:UseBiasedLocking参数才会存在
    • 假设启用了偏向锁,对象头的锁标志位是01(和未锁定状态一样),但是存储的内容是偏向线程ID、偏向时间戳
    • 当线程获取偏向锁是通过CAS操作将对象头中存储的偏向线程ID更新为当前线程的ID
    • 对象是否被锁定是指对象头是否指向线程的锁记录(Lock Record)
    • 只有是轻量级锁或者重量级锁时对象才会被锁定

callable vs runnable

  • callable可与Future结合拿到线程结束后的返回值
  • callable的Callable的抽象方法有返回值。返回值类型就是泛型类型。
  • Callable接口带泛型
  • call()方法还能抛出异常。

线程池

为什么要用线程池?

为了减少在创建和销毁线程上所花的时间和对系统资源的消耗,解决资源不足的问题。

如果不适用线程池,有可能造成系统创建大量同类线程而消耗内存挥着”过度切换“问题。

四种线程池

  • SingleThreadPool:单个线程线程池
  • FixedThreadPool:指定大小的固定大小线程池
  • CachedThreadPool
  • ScheduledThreadPool

问题

  • FixedThreadPoolSingleThreadPool:允许的请求队列长度为Interger.MAX_VALUE,可能会堆积大量请求,造成OOM;
  • CachedThreadPoolScheduledThreadPool:允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量线程,造成OOM。

线程池参数?

  • corePoolSize:核心线程数量,定义了最小可以同时运行的线程数量,没有任务也不会被回收。
  • maximumPoolSize:当线程数量大于核心线程数,且队列中存放的任务达到队列容量的时候,继续创建线程。
  • keepalivetime:超过corePoolSize之后”临时线程“的存活时间。
  • Time_unit:单位
  • workQueue:当新任务来的时候,保存未执行的任务。大于corePoolSize时进入阻塞队列等待。
  • threadFactory:线程工厂,用来创建线程,默认就行
  • RejectdExecutionHandler handler:拒绝策略,线程数量达到maximumPoolSize大小,且workQueue也塞满的情况下,线程池调用handler拒绝策略来处理请求。

拒绝策略:

  • AbortPolicy:默认拒绝策略,直接抛异常

  • DiscardPolicy:直接抛弃不处理。

  • DiscardOldestPolicy:丢弃队列中最老的任务。

  • CallerRunsPolicy:将任务分配给当前执行execute方法的线程来执行

    自定义拒绝策略:实现RejectedExecutionHandler接口,可以实现友好的拒绝策略。包括但不限于:将数据存入数据库,用日志处理等。