Java多线程理解,可以吊打面试官

进程和线程

  1. 一个Java程序(进程),就是一个大工场,一个线程就是一个工人
  2. 单核CPU:工厂只有老板(main线程)一人干活;
  3. 单核多线程:老板这一分钟模拟工人A干脏活,下一分钟模拟工人B干累活;
  4. 多核CPU:老板雇人了,有很多工人干活
  5. Java线程:JVM中使用操作系统分配的线程,也就是说开多少线程需要考虑机器性能
  6. ThreadLocal:Java线程的私有存储空间,相当于工人的口袋
  7. 锁:工人A和工人B被老板分配干同一个活,先来后到,只有把这个活加上锁,谁先来,谁拿到锁,谁就可以干活

Java有两套锁机制

  • Synchronized锁机制
  • JUC包的锁机制
  • 分布式锁机制(第三方工具实现,比如redis实现,zookeeper等)

Synchronized锁机制

Synchronized是JVM提供的一套线程互斥机制,加了Synchronized的方法或代码块只能被一个线程进入;
Synchronized中的锁,JVM中把任何对象都当作锁,获取锁就是获取对象的Monitor,任何一个对象都有一个Monitor对象监视器(一种数据结构),锁记志位是用于记录该锁是否被某个线程拿到,锁标记位和线程ID存放在对象头中;

Synchronized加在方法上:锁默认是当前实例对象;
Synchronized加在静态方法上:锁默认是当前类的Class对象;
Synchronized使用代码块,()括号里设置锁,可以用任何可用的对象当作锁;

锁升级

Synchronized锁(存放在对象头中的标记位)有三种状态

  • 偏向锁:用了synchronized,在大多数情况下锁不存在多线程竞争,总是由同一个线程多次获得,为了降低上下文切换开销,当一个线程第一次获取这个锁时,会在对象头和栈帧中的锁记录里存储线程ID,以后该线程进入和退出同步块时不需要CAS操作来加锁和解锁,可直接获取到。
    偏向锁是默认开启的,在程序启动的第4秒开启,也可以手动关闭偏向锁,关闭或升级后进入轻量级锁状态

  • 轻量级锁:在偏向锁状态时,如果有其他线程来竞争锁,则立刻转换成轻量级锁状态;轻量级解锁时,发现此时有别的线程在竞争锁,也就是线程CAS获取锁失败后进入自旋状态(ps: 为了避免无用的自旋引入了自适应自旋锁)这时升级为重量级锁。

  • 重量级锁:这时就是线程阻塞状态了,属于系统线程的阻塞。
    锁只会从无锁,一步步升级到重量级锁,而不会降级,一切都是隐式进行

使用Synchronized有5个重要的配套方法,都只能在Synchronized块里面使用

wait()假如线程A先获取了对象的锁,然后调用锁对象的wait()方法,从而释放了锁并进入对象的等待队列中,使线程A进入等待状态;
notify()由于A释放了锁,线程B随后竞争获取到锁,执行完业务,并调用对象的notify()方法,此方法会将线程A从等待队列中移到SynchronizedQueue同步队列中去竞争锁,此时A变为阻塞状态,当线程B真正执行完释放了锁之后, 线程A才可获取到锁并从wait()方法返回,继续执行;
notifyAll()把等待队列中的所有线程移到同步队列去竞争,但只会有一个线程从wait()返回,其余的阻塞;
join()线程A执行了线程B.join()语句,此时线程A等待线程B结束之后才从B.join()返回;底层也是运用上面的方法;
interrupt()B.interrupt(),通知线程B应该中断了,当对一个线程调用interrupt()时,设置它的中断标志为true,线程B自行决定是否真正的中断;
ThreadLocal线程的私有内存空间,是一个以ThreadLocal对象为键,任意对象为值的map存储结构;如果在一个线程中创建子线程,ThreadLocal无法传递值给子线程,用InheritableThreadLocal就可以了;(建议用volatile);

重点知识1:重排序

因为虚拟机或cpu或指令集都有重排序优化执行顺序,但是又怕结果错乱,所以有以下规则

as-if-serial规则:不管怎么重排序,单线程的执行结果不会被改变
happens-before规则:不管怎么重排序,正确同步的多线程程序执行结果不会改变

重点知识2:volatile

用volatile修饰的变量的3个特性
1.可见性:保证了共享变量的可见性,当一个线程修改一个共享变量时,另一个线程能读到这个修改的值
2.半原子性:对单个volatile变量的读/写具有原子性(i = 1),但对于i++这种复合操作不具有原子性
3.禁止读写重排序:是通过内存屏障来实现的



在多核处理器中,处理器不直接和内存进行数据交互,而是先将内存中数据读到缓存后对缓存操作,但操作完不知何时会写回内存,
如果用volatile修饰变量,进行写操作时JVM就会向处理器发送一条带Lock的前缀指令,将变量所在的缓存行数据写回到内存,
但,就算写回到内存,如果其他处理器缓存的值还是旧的,其他处理器执行计算操作就会出问题;
所以在多核心处理器下,
为了各个处理器的缓存是一致的,就会加总线锁或者实现缓存一致性协议MESI的方式来实现一致性:
每个处理器通过嗅探总线上传播的数据来检查自己缓存的值是否过期,
当处理器发现自己缓存行对应的内存地址被修改,就会将自己处理器的缓存行设置为无效状态,
当处理器要对这个数据进行读取或修改操作时,将会重新从内存中把数据读到缓存里再操作。



有2条JMM内存规则,保证可见性
当写一个volatile变量时,JMM会把该线程对应的本地缓存中的所有可见的volatile共享变量值刷新到主内存,包括其他共享变量。
当读一个volatile变量时,JMM会把该线程对应的本地缓存的volatile变量值设为无效,其他线程操作时将从主内存中读取,包括其他共享变量。
有3条禁止重排序规则,防止多线程下读写变量结果错乱
当第二个操作是对 volatile 写时,不管第一个操作是什么,都不能重排序。这个规则确保 volatile 写之前的操作不会被编译器重排序到 volatile 写之后。
当第一个操作是对 volatile 读时,不管第二个操作是什么,都不能重排序。这个规则确保 volatile 读之后的操作不会被编译器重排序到 volatile 读之前。
当第一个操作是对 volatile 写,第二个操作是 volatile 读时,不能重排序。

重点知识:CAS

原子操作:有两种办法
1.加锁(会阻塞)
2.使用CAS机制 (在系统层面CAS还是会加锁的,通过实现的MESI协议锁定总线或者缓存实现原子操作)

CAS:比较并交换
线程A,需要修改变量x的值1改为2,这时线程A拿到变量i的值判断要修改的1是否是1,如果是1则修改为2
线程B也会这么判断,所以线程B修改不了就放弃,只能被某一个线程成功修改(先来的线程),其他均失败

ABA问题:
如果修改成功的线程A又把值改为1,这时刚好另一个线程C也在判断是1,以为没有被修改过,
这时线程C会修改成功,
解决的办法就是值加上版本号or时间戳,去判断,让每次修改的值的版本都不一样

Java里面用Unsafe类中的CAS方法,Unsafe类需要反射才能使用
在这里插入图片描述

AQS锁机制

队列同步器AbstractQueuedSynchronizer:用来构建锁或其他同步组件的基础类
同步队列是一个虚拟(CLH)队列,是一个FIFO双向队列;
不存在队列实例对象,仅存在结点之间的关联关系;

如果用一个普通的LinkedList来维护节点之间的关系,那么当一个线程获取了同步状态(锁),
其他线程获取同步状态(锁)失败,从而被并发的添加到LinkedList时,将难以保证节点的正确添加
AQS加锁逻辑
在AQS源码中你会发现很多用volatile修饰的变量,比如:
state是锁标志变量
head是当前持有锁的线程节点引用
原理是借助volatile的内存语义(禁止重排,可见性)
在加锁解锁的一些方法中对变量的读取或修改,在多线程竞争下,保证逻辑的正确性

在这里插入图片描述

(下次更新)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值