线程的六种状态
NEW: 初始状态,线程被创建出来但没有被调用 start() 。
RUNNABLE: 运行状态,线程被调用了 start()等待运行或正在运行的状态。
BLOCKED :阻塞状态,想要运行的代码被锁住了,需要等待锁释放。
WAITING:等待状态,表示该线程需要等待其他线程做出一些特定动作(通知或中断notify或inteurrpte)。调用wait、join方法后变成这种状态。
TIME_WAITING:调用wait、join方法时指定了时间。
TERMINATED:终止状态,表示该线程已经运行完毕。
线程常用API
Sleep:使线程进入TIME_WAITING状态
LockSupport.park()、LockSupport.unpark(要恢复的线程对象):使线程进入等待状态和唤醒等待线程,与wait不同的地方是与锁无关,wait必须在同步代码块中。线程若在park之前执行过unpark那么也不会等待,相当于提前解毒,但是不论unpark多少次只能解第一次。
Yield:让当前线程从运行状态进入到就绪状态(相当于刚刚start),可能马上又得到执行。
Join:当前线程A和其他线程等待调用join的线程(B.join())执行完之后再执行,join(Long timeout)在规定时间内还没执行完那就不等了。原理:通过wait和notif来实现,并没有显式地调用notifAll而是在JVM底层代码实现的。
interrupt :打断处于等待状态的线程会抛异常,打断状态不会变还是false;打断运行状态或者park的线程仍会继续运行只是标记更新为true,也就是调用isInterruptd返回为true。
无锁并发CAS和volatile
是一种乐观锁的思想,变量被其他线程修改了我多重试几次就行了。效率比Synchronized的悲观锁要高,因为它无锁就不会阻塞。但如果并发量特别大,可能大部分线程就一直在重试重试,反而效率会很低。
CAS
使用AtomicInteger、AtomicReference等原子操作类,底层使用compareAndSet()的方式来解决并发问题(底层使用了UnSafe类直接操作cpu所以它的比较和设置这两个操作是原子性的),也提供AtomicStampedReference用版本号的方式解决ABA问题。
compareAndSet:a = 10,线程A想要将a更新为11,更新前获取内存中的值是否和预期值相等也就是是否为10,如果更新前线程B将a更新为12了,线程A发现内存中的值12!=10那么他就会更新失败然后自旋重试,失败一定次数之后就会停止。
Volatile
保证变量在不同线程之间的可见性,一个线程获取一个变量的时候可能会从自己线程的高速缓存里获取,这样如果其他线程修改了这个变量那么就不能获取到最新的值了,使用volatile修饰变量就能保证线程获取变量的值是从主存中获取的,这样就保证了变量的可见性。
Synchronized关键字和Lock接口
- Synchronized底层使用objectMonitor,而lock接口的实现类使用AQS。
- ReentrantLock是可重入锁,同一个线程可以多次获取该锁。
- ReentrantLock可以实现公平锁和非公平锁,而synchronized只能实现非公平锁,公平锁是指多个线程按照申请锁的顺序获取锁(因为AQS底层维护了一个表示线程的等待队列),非公平锁就是多个线程去抢。公平锁是如果等待队列有线程在排队那就去排队,非公平锁是如果等待队列有线程先去和其他线程一起抢一次锁,万一抢成功了就不用阻塞就减少了一次线程唤醒,减少了cpu开支。但是一直这么抢,可能某个线程一直抢不到就会饿死,各有利弊。
- Synchronized临界区代码出现异常会释放锁,而ReentrantLock不会所以它需要手动unlock。
- 使用lockInterruptibly()代替lock()获取锁时,如果其他获取到锁的线程是因为被打断(interrupt)而结束进程的,那么使用lockInterruptibly()的线程就会抛出异常,在catch中可以写打断的逻辑。而使用synchronized的线程在其他线程被打断时只会一直傻傻的等,也就是说lockInterruptibly()多了一个监听打断的功能。
- wait和notify(Contition的await和signal)等待和唤醒的api不同。
- 并发量特别大的情况下Lock的性能才更强。
AQS
是一个用来构建锁和同步器的框架,可以用AQS构建出各种功能的同步器。基本原理是将每条请求共享资源的线程封装成一个CLH队列的节点(Node)来实现锁的分配,未获取到锁的线程放入到CLH队列中。
AQS三大重要参数head,tail,state,其中state表示同步状态,head为等待队列的头结点,tail为尾节点。
lock方法原理:调用tryAquire 方法线程尝试获取锁时先根据aqs的statue是否>0判断锁是否被占有,如果没被占有(statue=0),通过CAS去获取锁,如果被占有了判断占有的线程是不是就是自己,是自己则statue++。如果不是自己线程没有获得锁的情况下调用addWaiter方法将线程初始化为一个Node放入到CLH队列中,然后调用acquireQueued方法使用park将线程设置为等待状态。
unlock:调用unpark进行锁释放。
锁升级
对象头
每个对象都有对象头,主要包含一下两部分内容。
1 运行时元数据(Mark Word):哈希值、GC分代年龄、锁状态标志、线程持有的锁、偏向线程id、偏向时间戳。
2 类型指针(Klass Pointer):指向类元数据的InstanceKlass,确定该对象所属的类型。
Mark Word
锁的四种状态
无锁:
偏向锁:偏向于第一个获取到它的线程,这个线程执行完同步代码块中的代码之后先不释放锁,第二次执行到同步代码块的时候就会判断此时持有锁的线程是否就是自己,是就直接执行,这样一来就减少了锁释放和锁获取的时间效率极高。(检查Mark Word中的ThreadID是否是自己)
轻量级锁:一旦有第二个线程加入锁竞争,那么偏向锁就会升级为轻量级锁。(竞争的含义是当某线程尝试获取锁的时候,发现该锁已经被占用,只能等待其释放,这才叫发生了锁竞争)轻量级锁是通过CAS自旋的方式来获取锁的,比较Mark Word中的锁标志位是否为无锁,如果不是就一直自旋,如果是那就占用将它置为00。
重量级锁:CAS自旋到一定次数(默认允许循环10次,可以通过虚拟机参数更改)就说明锁竞争情况严重要升级为重量级锁,现在其他线程获取不到锁就不会自旋了而是阻塞进入BLOCKED状态等待唤醒。
轻量级锁和重量级锁的加锁原理:
轻量级:判断锁对象的锁标志位是不是无锁,如果是则在栈中建立一个Lock Record(锁记录),将锁对象的Mark Word复制到Lock Record的Displaced Mark Word中,将owner指向锁对象。
然后将锁对象的Mark Word更新为指向锁记录的指针,锁标志位更新为00告诉其他线程此对象已被轻量级锁锁定。
这时其他线程过来发现为00就会一直CAS自旋(默认10次)等待锁释放。
重入锁:当对象处于加锁状态时,会去检验Mark Word是否指向当前线程的栈帧,如果是则将刚刚建立的Lock Record中的Displaced Mark Word设置为null,记录线程重入锁。
参考->Synchronized原理(轻量级锁篇)-CSDN博客
重量级:执行到临界区代码时,查看锁对象的MarkWord是否绑定了Monitor
如果没有绑定则会先去与Monitor绑定,并且将owner设置为当前线程。
如果已经绑定,则去查这个Monitor是否已经有了owner
如果没有则owner与当前线程绑定
如果有,则放入EntryList进入阻塞状态(BLOCKED)
Owner线程执行完之后会让EntryList中的线程唤醒进行不公平竞争。