线程和进程
1. 一个Java程序(进程) 就是一个大工场,一个线程就是一个工人;
2. 单核CPU:工厂只有老板一人干活;单核多线程:老板这一分钟模拟工人a干A活,下一分钟模拟工人b干B活;
3. 多核CPU:老板雇人了,很多工人干活;
4. java的线程是操作系统分配的线程,也就是说开辟多少线程需要考虑机器性能;
5. 锁:工人a和工人b都要抢一个活,先来后到,只有把活加上锁,谁先来谁拿到锁,谁就可以干活;
锁
Java里有两套锁机制
- Synchronize锁机制
- JUC包里的锁机制
- 分布式锁机制(不是java的,有redis实现,zk实现等)
Synchronize锁机制
Synchronize是JVM提供的一套线程互斥机制,加了Synchronize的方法或代码块,只能被一个线程进入;Synchronize的锁:fJVM中把任何对象都当成锁,锁标志位存放在对象头中,获取锁就是获取对象的Monitor,任何一个对象都有一个Monitor对象监视器(一种数据结构);
Synchronize加在方法上,锁默认是当前实例对象,加在静态方法上,锁默认是当前类的class对象,加在Synchronize()括号里,可以用任何可用的对象;
Synchronize的锁(存放在对象头中的标记位)有三种状态
-
偏向锁:在大多数情况下,锁不存在多线程竞争,总是由同一个线程多次获得,为了降低开销,当一个线程第一次获取锁时,会在对象头和栈帧中的锁记录里存储偏向的线程ID,以后该线程在进入退出同步块时不需要CAS操作来加锁和解锁。
偏向锁是默认开启的,在程序启动的第4秒开启,也可以手动关闭偏向锁,关闭后程序入轻量级锁状态 -
轻量级锁:偏向锁状态时,如果发生线程竞争锁的情况,则立刻转换成轻量级锁状态;轻量级解锁时,发现还有别的线程在竞争锁,也就是(cas获取锁失败则线程进入自旋状态,为了避免无用的自旋,引入了自适应自旋锁)这时升级为重量级锁
-
重量级锁:这时就是线程阻塞状态了;锁只会从无锁,一步步升级到重量级锁,而不会降级,一切都是隐式进行
使用了Synchronize,那么配套的有5个重要方法,都只能在Synchronize块里面使用
wait() | 线程A先获取了对象的锁,然后调用对象wait方法,从而释放了锁并进入了对象的等待队列中,进入等待状态; |
notify() | 由于A释放了锁,线程B随后获取了锁,并调用对象的notify方法,将线程A从等待队列移到;SynchronizedQueue同步队列中去竞争,此时A变为阻塞状态;B释放了锁之后, A再次获取到锁并从wait()方法返回继续执行; |
notifyAll() | 把所有等待队列的线程全部移到同步队列去竞争,但只会有一个线程从wait()返回,其余的阻塞; |
join() | 线程A执行了线程B.join()语句: A线程等待B线程终止之后才从B.join()返回; |
interrupt() | 通知该线程应该中断了,当对一个线程调用interrupt()时,设置中断标志为true,线程自行决定中断; |
ThreadLocal | 是线程私有变量,是一个以 ThreadLocal 对象为键、任意对象为值的存储结构;如果在一个线程中,创建子线程,那么ThreadLocal也无法传递值,换成InheritableThreadLocal就可以了,不建议用 |
知识1:重排序
因为虚拟机或cpu或指令集都有重排序优化执行顺序,但是又怕结果错乱,所以有以下规则
as-if-serial规则:不管怎么重排序,单线程的执行结果不会被改变
happens-before规则:不管怎么重排序,正确同步的多线程程序执行结果不会改变
知识2:volatile
1.可见性,保证了共享变量的可见性,当某个线程修改一个共享变量时,另外一个线程能读到这个修改的值
2.半原子性:对单个volatile变量的读/写具有原子性(i = 1),但对于i++这种复合操作不具有原子性
3.禁止volatile读写重排序:是通过内存屏障来实现的处理器不直接和内存进行通信,而是先将内存的数据读到缓存后再对缓存操作,但操作完不知道何时会写回内存
如果对volatile变量进行写操作,JVM就会向处理器发送一条Lock前缀指令,将这个变量所在缓存行的数据写回到内存但,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题
所以在多处理器下,为了各个处理器的缓存是一致的,就会实现缓存一致性协议MESI:每个处理器通过嗅探总线上传播的数据来检查自己缓存值是不是过期了,
当处理器发现自己缓存行对应的内存地址被修改,就会将自己处理器的缓存行设置成无效状态,
当处理器对这个数据进行修改操作的时候,会重新从内存中把数据读到缓存里。有以下5条规则:
当写一个volatile变量时,JMM会把该线程对应的本地缓存中的所有可见的共享变量值刷新到主内存,同时其他共享变量也会被刷到主内存。
当读一个volatile变量时,JMM会把该线程对应的本地缓存变量值设为无效。线程接下来将从主内存中读取包括其他变量。
当第二个操作是 volatile 写时,不管第一个操作是什么,都不能重排序。这个规则确保 volatile写之前的操作不会被编译器重排序到 volatile写之后。
当第一个操作是 volatile 读时,不管第二个操作是什么,都不能重排序。这个规则确保 volatile读之后的操作不会被编译器重排序到 volatile读之前。
当第一个操作是 volatile 写,第二个操作是 volatile读时,不能重排序。
以上这些保证了可见性
知识3:CAS
java原子操作:有两种办法
1.加锁
2.使用CAS机制 (在系统层面CAS还是会加锁的,通过锁定总线或锁缓存实现原子操作)CAS是比较并交换
线程A,需要修改某个变量的值1改为2,这时要拿到变量的值判断要修改的1是否是还1,如果是则修改为2,线程B也会这么判断,所以线程B修改不了就放弃,只能被一个线程修改成功。
先来的那个线程修改成功,这就是CAS;其他均失败
但是如果修改成功的线程又把值改为1,这时刚好另一个线程C在判断:怎么是1,这时线程C就修改成功了,
这就是ABA问题,要解决的办法就是值加上版本号,让每次修改的值都不一样。
AQS锁机制(下次更新)
AbstractQueuedSynchronizer队列同步器:用来构建锁或者其他同步组件的基础类
同步队列是虚拟(CLH)队列,是一个虚拟的FIFO双向队列,即不存在队列实例,仅存在结点之间的关联关系;
如果使用一个普通的 LinkedList 来维护节点之间的关系,那么当一个线程获取了同步状态,
而其他多个线程获取同步状态失败而被并发地添加到LinkedList时,LinkedList将难以保证节点的正确添加;