1.线程模型

1.1什么是线程模型?

Java字节码文件运行在JVM中,JVM运行在各个系统上。所以当JVM想要进行线程创建回收等操作的时候,势必会调用操作系统相关的接口。也就是说JVM线程与操作系统的线程存在着某种映射关系,这两种不同维度的线程之间的协议和规范,就是线程模型。
JVM线程对不同的操作系统上的原生线程进行了高级抽象,使我们在开发的时候不必关注底层原理,只需要专注于java程序本身。

1.2JVM线程模型

JVM的三种线程模型,一对一,一对多,多对多
一对一
image.png
优点:每个线程都是独立的调度单元,直接利用操作系统内核提供的调度功能
缺点:用户线程的阻塞唤醒,会直接映射到内核线程上,容易引起频繁切换,降低性能。但是一些语言引入了CAS来避免一部分的内核调用,比如Java引入了AQS这种函数级别的锁,减少使用内核级别的锁,就能提升性能。
Linux内核能够创建的资源是有限的的,所以这在一定程度上会限制并发量。
目前大部分主流虚拟机采用的都是这种线程模型
UT=用户线程,KLT=内核线程,LWP=轻量级线程

多对一
多个用户线程映射到一个内核线程上,用户线程的调度需要用户空间来完成
image.png
优点:
提升并发量上限,大部分调度和同步操作都在用户空间完成,减少线程状态的切换,能够提升性能
缺点:
当一个用户线程进行了内核调用并阻塞了,其他所有的线程也会阻塞
java早期就是这种模式后来已经被抛弃了

多对多
image.png

2.什么是锁?

在并发环境下,多个线程会对同一个资源进行争抢,可能会导致数据不一致的问题,为了解决这个问题,许多编程语言都引入了锁机制,通过抽象的锁来对资源进行锁定。

3.Java中的锁

3.1锁

  1. Java中每个Object对象都带有一把锁,锁中记录了当前对象被哪一把锁占用,锁是存放在对象头中的。

3.2 对象,对象头的结构

Java对象的结构,如下图所示
image.png

  1. # Object 的各个结构
  2. - 对齐填充字节
  3. - 为了满足java对象大小必须是8bit的倍数,为了对象的大小填充的一些无用字节
  4. - 实例数据
  5. - 对象中定义的方法,属性
  6. - 对象头
  7. - 对象本身的运行时信息,包含两部分
  8. markwordclasspoint(指针,指向当前类型所在方法区中的位置)
  9. markword存储的数据如下图所示

image.png
对象中包含的锁的状态就存放在对象头的markword中,锁标志位代表了当前对象中存放的不同的锁的类型,我们主要研究的就是不同的锁的类型

4.Synchronized关键词

java中的锁关键词就是synchronized,那synchronized关键词通过javac编译以后会生成两条字节码指令,分别是monitorenter和monitorexit,依赖这两个字节码指令来进行线程同步
image.png
Monitore 监视器(监视程序的状态)
image.png

4.1Synchronized的同步机制image.png

entryset中聚集了一些想要进入monitre的进程
假设现在A线程成功进入monitre中执行任务,那么他的状态就会由waiting变成active,假设此时有一个语句,是A的状态改变,则A进入WaitSet重新编程waiting状态,也就是我们常说的等待池,因为A释放掉了锁,则线程B可以进入monitre中执行任务,在B执行完成以后,可以通过notify来唤醒线程A,然后再执行完成以后退出monitre

4.2Synchronized的性能问题

  1. # synchronized被编译后实际上是moniterenter和monitorexit
  2. - monitor 是依赖于操作系统的mutexlock实现的,简单来说,每次切换线程有课程切换线程
  3. 的时间大于程序本身的执行时间,所以在java6以后对synchronized进行了优化,
  4. 引入了偏向锁和轻量级锁
  5. 所以锁分别有四种状态
  6. 无锁,偏向锁,轻量级锁,重量级锁
  7. 这也对应了markword中锁标记位的四种状态
  8. 锁只能升级不能降级

4.3锁的四种状态

在java6以后,为了优化synchronized的性能,引入了偏向锁和轻量级锁,所以锁的四种状态分别是
无锁,偏向锁,轻量级锁,重量级锁
这也对应了markword中锁标记位的四种状态
锁只能升级不能降级

4.3.1 无锁

即对象不加锁,所有的线程都能访问到该资源。则会导致两种情况的出现,一种是该资源出现在多线程环境下,或者出现在多线程环境下,也不需要进行保护。第二种就是资源会竞争,但是不想锁定,还是想通过一些机制保护资源
那么我们可以使用其他的手段,使同时只有一个线程能够修改成功其他修改失败的线程会循环等待,直到成功为止,这就是CAS

  1. CAS:
  2. compare and swap
  3. CAS在操作系统中是通过一条指令来实现的,所以就可以保证操作的原子性,所以我们可以通过
  4. 无锁编程的方式实现线程安全

因为synchronized是通过操作系统的monitor指令来完成的,所以性能比较低下,通过对比我们可知,无锁的性能是非常高的,但并不意味之无锁可以代替有锁

4.3.2 偏向锁

假如一个对象被加锁了,但在实际运行时,只有一个线程能获得这个对象锁,那么我们最理想的方式就是不通过线程状态切换,因为线程切换也需要消耗资源也不要CAS做,我们希望的是最好对象能够认识这个线程,只要是这个线程过来,对象就直接把锁交出去,我们就认为对象偏爱这个线程,就成为偏向锁。

  1. # 偏向锁的实现原理
  2. - markword中,如果锁标志位为01,那么判断倒数第三个bit是否为1,如果是1,那么
  3. 代表当前对象的锁状态是偏向锁,否则就是无锁
  4. 如果锁为偏向锁,那么再去markword读前23bit,这23bit就是线程id,也就是偏向的
  5. 线程id

image.png

4.3.3轻量级锁

当锁还是偏向锁的时候,通过判断线程的id来判断给不给锁,那么升级成轻量级锁呢?
image.png

  1. # 轻量级锁如何判断线程
  2. - 当看到锁标记位为00的时候,那么就知道他是轻量级锁