关于多线程的一些知识点:基础概念和关键字原理

一、java线程的六种状态

java线程有六种状态,分别是new(创建),rennable(运行),time_waiting(超时等待),waiting(等待),blocked(阻塞),terminated(死亡)。
一个线程先被创建,然后进入运行(分为就绪,运行两种)状态,加锁(synchronized修饰)会进入阻塞状态,sleep(0),wait,join,park会进入等待状态,sleep(time),wait(time),join(time),park(time)(即加入唤醒时间的等待函数)会进入超时等待状态,notify(),notifyall()可以唤醒沉睡中的线程,使之进入运行(分为就绪,运行两种)状态,run函数运行结束后,线程进入死亡状态。

二、系统线程的五种状态

和java线程不同,系统线程除了新生和死亡,只有三种其他状态,分别是就绪(ready),运行(running)和等待(waiting)。

三、关于中断线程的方式

start可以启动线程,但要注意,run不可以,run是实例方法,只有start才能启动线程。stop毫无疑问是可以中断线程的,但过于粗暴,会导致未知的问题,比较合适的是interrupt()来设置中断位,可以向线程发起中断请求,但线程未必响应,需要增加判断条件isInterrupted()来判断线程是否中断,interrupted()方法也可以测试当前线程是否被中断,但会清除中断标志位。除此之外,也可以用抛错来终止线程

四、关键字synchronized的使用

synchronized是非公平锁,使用很简单,只分为两种,分别是类锁和对象锁,放在方法和实例里的就是对象锁,是只对共享此对象的线程共用锁。放在静态方法和类中的就是类锁,对于所有使用此类的实例化对象共用锁。

五、关键字synchronized的存储位置

锁的内容会存储在jvm对象头中,根据位数不同,32位和64位的对象头存储锁的结构也不同,但都包含了锁的标志位(2bit)

  • 无锁(32bit)有锁标志位(2bit),分代年龄(4bit),是否为偏向锁(1bit),hashcode(25bit)。
  • 无锁(64bit)有锁标志位(2bit),分代年龄(4bit),是否为偏向锁(1bit),hashcode(31bit),无用(25bit),cmsfree(1bit)。
  • 偏向锁(32bit)有锁标志位(2bit),分代年龄(4bit),是否为偏向锁(1bit),epoch(2bit),线程ID(23bit)。
  • 偏向锁(64bit)有锁标志位(2bit),分代年龄(4bit),是否为偏向锁(1bit),epoch(2bit),线程ID(54bit),cmsfree(1bit)。
  • 轻量级锁有锁标志位(2bit),栈帧中锁记录指针(剩余)。
  • 重量级锁有锁标志位(2bit),互斥量指针(剩余)。
  • GC标志有锁标志位(2bit),无(剩余)。

注意,对象头中的十六进制存储采用了大端存储和小端存储,所以要倒着看输出。

六、synchronized的底层原理

synchronized本身是关键字,实际上是调用了jvm的底层c++代码。
每一个java对象都有一个monitor,每一个线程都有一个监视器Monitor Record,当线程想要获取一个加锁资源时就必须获取到它的monitor,然后将所有权据为己有,直到线程运行完毕才会释放所有权,唤醒被阻塞的线程抢占该资源。

七、锁的分类和膨胀

正如上文,锁分为偏向锁,轻量级锁,重量级锁。
偏向锁(乐观):平时是关闭的,开启后,当一个线程访问一个加锁资源,会把线程id存入对象头,后续访问时,不需要操作锁,而是直接比对ID,如果一致就直接获得,其目的是消除无多线程竞争时轻量级锁对资源的同步,减少消耗。
第一次膨胀:是当偏向锁被关闭或者当前偏向锁内ID不是该访问线程时,锁会膨胀,成为轻量级锁
轻量级锁(乐观):目的是解决多线程在不同时段抢占同一把锁的情况,为了避免不必要的资源消耗,它不会让线程进入阻塞状态,而是会进行重试,即自旋锁,自旋锁会重试10次来抢占资源。
第二次膨胀:当自旋锁全部失败,抢占锁的线程将进入阻塞状态,锁膨胀为重量级锁
重量级锁(悲观):当多个线程抢占同一个锁时,未抢占到的线程将进入阻塞状态。当抢占到的线程释放锁,它会唤醒其他全部的阻塞线程,进行重新抢占。
锁一旦膨胀,将不会收缩,

八、synchronized的优化

jdk1.6中对锁的实现引入了大量的优化

  • 锁粗化:将紧紧连接在一起的lock指令合成一个
  • 锁消除:清除掉没有竞争资源的锁
  • 自旋和轻量级也是优化
  • 自适应自旋:从原本的十次自旋尝试变成了自适应,即上次等待时间长的将会缩短时间,上次时间短的可以放宽时间。

九、多线程的问题

多线程想要执行,有三个条件,分别是原子性,可见性和有序性。
原子性和操作本身有关,有些操作是原子性的,比如赋值,有些不是,比如自增,另外两个问题,可以通过volatile关键字和final关键字解决。

十、系统内存系统和java内存模型

在一个系统中,cpu资源运行速度最快,也最珍贵,IO读取是比较缓慢的,所以才会有并行。

可见性问题

为了解决读写速度问题,系统给每一个cpu配置了一个高速缓存,cpu会先读写高速缓存,当高速缓存中没有,才会读写主内存,高速缓存会在合适的时机将数据同步到主内存上,但一个cpu读写了自己的高速缓存,其他cpu是不知道的,这就造成了可见性问题。

高速缓存与缓冲区

为了解决可见性问题,增加了总线锁和缓存锁,缓存锁又增加了缓存一致性协议,其大致流程为当一个数据被更新,该cpu会告知其他cpu其修改数据在高速缓存中失效,强制其他cpu去主内存更新数据,但次过程需要等待cpu响应,浪费资源,因此增加异步通讯,store buffer,将数据失效的事情交给它去做异步,这样就可以解放cpu资源。但因为异步通讯,有时会造成程序执行顺序有误导致的明显错误,比如b = true要读取主内存,因此要进行缓存失效,但if(b)在高速缓存,cpu无需同步,判断时b为默认值false,判断为否,这就是指令重排序问题

有序性

为了解决指令重排序,又增加了新的概念,内存屏障,当有内存屏障时,数据会被强制同步,这样就避免了指令重排序的问题。

java内存模型

java并不存在这样一个实际的内存模型,而是仿照系统的内存模型,并且针对不同的体统有不同的处理。
简单来说,jvm定义了多线程情况下读写操作的行为规范,在虚拟机中将共享变量储存到内存以及从内存中取出共享变量的底层实现细节,通过这些细节来规范内存的读写操作从而保证指令的正确性,它解决了cpu多级缓存,处理器优化和指令重排序导致的内存问题,保证了并发条件下的可见性。

十一、volatile关键字

简单来说,volatile关键字就是启动了内存屏障,其内置四种内存屏障,loadload,loadstore,storeload,storestore,有效保证了数据的可见性和有序性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值