目录
1.4 synchronized 原理 (重量级锁 JDK1.6以前)
1.6 synconised(重量级锁)内部(锁升级的过程)
一.计算机的组成
1.CPU:计算
内存:存储数据和程序
2.缓存行填充的编成技巧
cpu和内存之间,一般分为三级缓存L1、L2、L3,比如一个双核的cpu,那么L1和L2在cpu内核里,L3的数量与cpu数量一样,多个cpu共享内存
二.缓存行对齐,伪共享
三.缓存行填充的编成技巧
缓存一致性协议(硬件级别协议):当cpu中得一个值改了之后,要通知其他cpu
当创建两个x的时候,这两个x永远不可能与其他有效数据位于同一行,所以当一个cpu的值修改的时候,并不需要通知其他cpu,这时候大大提高cpu 的运行速度。
四.有序性带来的this溢出问题
3.并发编程的三大特性:可见性(线程内部数据修改完值,如何让其他人可见)、有序性、原子性
有序性:
(1)Object ob=new Object();
问题一:创建对象的步骤:申请内存空间,赋默认值,调用构造方法,建立关联
DCL:double check lock
双重检查:1.如果把第一个if去掉,那么必须先加锁,才知道对象是否创建,如果有1w个线程,那么会非常消耗资源,加上的话,第一个线程为空,然后等暂停,如果这时候第二个线程来了,加锁,然后创建对象,这时候第一个线程的第二个if不通过,不会创建第二个对象。
五.有序性带来的半程对象问题
问题二:DCL 单例模式需不需要volatile?需要,禁止指令重排
volatile只能保证有序性、可见性、不能保证原子性,也就不能保证线程安全。
jvm有四种内存屏障:LoadLoad(读读之间有LoadLoad不可以指令重排),StoreStore,LoadStore,StoreLoad
下列图中,7和4 乱序执行了,正常应该先4后7,发生了指令重排
在单线程最终结果一致的时候,会发生指令重排
第3题:
第7题:可以答16个字节
第8个问题:虚函数表
class对象既在堆,又在方法区
六.线程和锁
任何对象都可以当成锁来用
1.Synchnoized
1.1 特性
-
原子性:确保线程互斥的访问同步代码。synchronized保证只有一个线程拿到锁,进入同步代码块操作共享资源,因此具有原子性。
-
可见性: 保证共享变量的修改能够及时可见。执行 synchronized时,会对应执行 lock 、unlock原子操作。lock操作,就会清空工作空间该变量的值;执行unlock操作之前,必须先把变量同步回主内存中。
-
有序性: synchronized内的代码和外部的代码禁止排序,至于内部的代码,则不会禁止排序,但是由于只有一个线程进入同步代码块,因此在同步代码块中相当于是单线程的,根据 as-if-serial 语义,即使代码块内发生了重排序,也不会影响程序执行的结果。
-
悲观锁: synchronized是悲观锁。每次使用共享资源时都认为会和其他线程产生竞争,所以每次使用共享资源都会上锁。
-
独占锁(排他锁):synchronized是独占锁(排他锁)。该锁一次只能被一个线程所持有,其他线程被阻塞。
-
非公平锁: synchronized是非公平锁。线程获取锁的顺序可以不按照线程的阻塞顺序。允许线程发出请求后立即尝试获取锁。
-
可重入锁: synchronized是可重入锁。持锁线程可以再次获取自己的内部的锁。
为啥synchronized无法禁止指令重排,但可以保证有序性?
加了锁之后,只能有一个线程获得到了锁,获得不到锁的线程就要阻塞。所以同一时间只有一个线程执行,相当于单线程,而单线程的指令重排是没有问题的。
1.2 注意事项
- 普通同步方法,锁是当前实例对象this:public synchronized void fun(){ }
- 静态同步方法,锁是当前类的Class对象:public synchronized static void fun(){ }
- 同步代码块中,锁为syncronized括号中配置的对象:synchronized (SyncTest.class) { }
- 静态同步方法的锁对象是当前类的Class对象,类被装载后,在内存中有且仅有一个该类的Class对象,因此 可以保证静态同步方法的锁对象是同一个
- 只有同步代码块与同步方法的锁对象是同一个对象时,才能保证同一时刻,只有一个线程访问共享资源。
Tips:
按锁的性质可以将锁分为以下几种类别:
-
悲观锁 or 乐观锁:是否一定要锁
-
共享锁 or 独占锁(排他锁):是否可以有多个线程同时拿锁
-
公平锁 or 非公平锁:是否按阻塞顺序拿锁
-
可重入锁 or 不可重入锁:拿锁线程是否可以多次拿锁
1.3 同步代码块与同步方法的使用注意事项
(1) 只有同步代码块与同步方法的锁对象是同一个对象时,才能保证同一时刻,只有一个线程访问共享资源。
class SyncWrong implements Runnable {
private static int i = 0;
private synchronized void add() {
i++;
}
@Override
public void run() {
for (int j = 0; j < 1000000; j++) {
add();
}
}
public static void main(String[] args) throws Exception {
Thread t1 = new Thread(new SyncWrong());
Thread t2 = new Thread(new SyncWrong());
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
可以看到,虽然我们对 add方法加了锁,但依然不能保证输出结果的正确性。
这段代码违反了上述规则,add方法是普通同步方法,因此该方法的锁对象是当前实例对象this,主函数中的 t1、t2 线程传入了不同的SyncWrong 对象,因此,synchronized 锁对象不是同一个,自然就无法达到同步的目的。解决办法就是让 synchronized 锁对象是同一个。
解决方式一:只创建一个SyncWrong对象
//此时,synchronized 锁对象 就是 wrong 这个对象了
public static void main(String[] args) throws Exception {
SyncWrong wrong = new SyncWrong();
Thread t1 = new Thread(wrong);
Thread t2 = new Thread(wrong);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
解决方式二:将add方法改为静态方法
//静态同步方法的锁对象是当前类的Class对象,类被装载后,在内存中有且仅有一个该类的Class对象,
//因此 可以保证静态同步方法的锁对象是同一个
private synchronized static void add() {
i++;
}
(2) 每个访问共享资源的代码都必须被同步
class Resource{
private static int i = 0;
public synchronized void add() {
i++;
}
public void reduce() {
i--;
}
}
对于上述代码,add 和 reduce 都对共享资源 i 进行了处理,你必须同步这两个方法(将 reduce 改造为同步方法)。如果不这样做,那么reduce()方法就会完全忽略这个对象锁,也就是在当前线程获得到锁,执行add操作时,其他线程可以随意的调用 reduce方法,从而导致并发修改的问题。
(3) 私有化共享资源
共享资源一般以对象形式存在于内存中,因此要控制共享资源的访问
class Resource{
public static int i = 0;
public synchronized void add() {
i++;
}
public synchronized void get() {
return i;
}
}
上述代码的问题在于任何线程都可以通过 Resource.i 来获取共享变量的值,若当前线程获得锁,正在执行add同步方法的内容,其他线程通过 Resource.i 则会忽略锁,可能获取到中间结果。正确的方法是将 共享资源 i 私有化(用 private 修饰), 然后提供一个被同步的获取方式。
1.4 synchronized 原理 (重量级锁 JDK1.6以前)
1.4.1 对象头
在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头( Header )、实例数据( Instance Data ) 和对齐填充( Padding )。
HotSpot虚拟机的一般对象头(Header)包括两部分信息:“Mark Word”、“Class Pointer”,数组类对象还包括“Array Length”。
- Mark Word:存储对象自身的运行时数据
- Class Pointer:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
- Array Length:如果是数组对象,则 Array Length 存储数组的长度。
Tip:
- 对齐填充( Padding ):Java对象占用空间是8字节对齐的,即所有Java对象占用bytes数必须是8的整数倍。若Java对象的bytes数不是8的整数倍,则会填充一些字节,让Java对象占用的空间是8的整数倍。
1.4.2 Mark Word
synchronized底层实现与Java对象头中的Mark Word密不可分的。Java对象头里的Mark Word存储对象自身的运行时数据,如 HashCode、分代年龄和锁标记位。在运行期间,Mark Word里存储的数据会随着锁标志位的变化而变化。
32位JVM在不同状态下Mark Word的组成如下表:
64位JVM在不同状态下Mark Word的组成如下表:
-
unused:就是表示没有使用的区域,在64位虚拟机的对象头中会出现。
-
hash:对象的hashcode,如果对象没有重写hashcode()方法,那么通过System.identityHashCode()方法获取。采用延迟加载技术,不会主动计算,但是一旦生成了hashcode,JVM会将其记录在 Mark Word 中。
-
age:4位的Java对象GC年龄。对象在Survivor区复制一次,年龄增加1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC的年龄阈值为15。最大值也是15。
-
biased_lock:1位的偏向锁标记,为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。
-
lock:2位的锁状态标记位,其中无锁和偏向锁的锁标志位都是01,只是在前面的1 bit的biased_lock区分了这是无锁状态还是偏向锁状态。lock和biased_lock共同表示对象处于什么状态:
-
thread Id:持有偏向锁的线程ID。
-
epoch:偏向锁的时间戳。
-
Lock record address:轻量级锁状态下,指向栈中锁记录的指针。
-
Monitor address:重量级锁状态下,指向对象监视器Monitor的指针。
1.4.3 重量级锁JVM层面的实现原理:monitor
Monitor是一种用来实现同步的工具,又称为对象监视器/管程。每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁(重量级)之后,该对象头的Mark Word中就被设置指向Monitor对象的指针。
注意:JDK1.6之后对synchronized 进行了优化,在重量级锁的基础上,又引入了偏向锁和轻量级锁,但只有重量级锁的实现是和monitor相关的。
(1)monitor结构
在HotSpot虚拟机中,最终采用C++的ObjectMonitor类实现monitor。
ObjectMonitor C++ 结构:
ObjectMonitor() {
//成员变量简单的初始化
_header = NULL; //markOop对象头,重量级锁储存锁对象头信息的地方
_count = 0;
_waiters = 0, //等待线程数
_recursions = 0; //锁的重入次数,作用于可重入锁
_object = NULL; //监视器锁寄生的对象。锁不是平白出现的,而是寄托存储于对象中。
_owner = NULL; //指向持有ObjectMonitor对象的线程地址
_WaitSet = NULL; //处于wait状态的线程,会被包装成ObjectWaiter,加入到_WaitSet集合(调用wait方法)
_WaitSetLock = 0 ; // 保护等待队列,作用于自旋锁。
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ; //阻塞在EntryList上的最近可达的的线程列表
FreeNext = NULL ;
_EntryList = NULL ; //处于等待锁block阻塞状态的线程,会被包装成ObjectWaiter,加入到该列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;// 监视器前一个拥有者线程的ID
}
ObjectMonitor 中 几个比较重要的字段:
- _WaitSet :处于wait状态的线程,会被加入到_WaitSet。
- _EntryList:处于等待锁block状态的线程,会被加入到_EntryList。
- _owner:指向持有ObjectMonitor对象的线程。
- _count:锁计数器,获取锁则 count+1,释放锁 - - count-1,count直到为0,才可以被其他线程持有。
- _recursions:记录锁的重入次数。
(2)monitor的获取和释放(重量级锁的获取和释放)
对于重量级锁,获取锁的过程实际上就是获取monitor的过程,释放锁的过程实际上就是释放monitor的过程。 monitor的竞争获取是在ObjectMonitor的enter方法中,而释放则是在exit方法中。
-
通过CAS尝试把monitor的_owner字段设置为当前线程,然后对_count++,若在设置_owner为当前线程时,发现原来的_owner指向当前线程,则说明当前线程再次进入monitor,还应让_recursions++;
-
如果获取锁失败,则当前线程加入_EntryList,等待锁的释放;
已获取锁的线程执行wait操作,则 _count–,_recursions–,设置_owner 为 null,然后当前线程加入 _WaitSet中,等待被唤醒。
已获取锁的线程通过 notify 唤醒 _WaitSet中的线程,被唤醒的线程继续竞争锁。
已获取锁的线程执行完同步代码块时,释放锁(ObjectMonitor.exit),_count–,_recursions–,若 _count = 0 ,则设置 _owner 为 null,然后唤醒 _EntryList竞争锁(竞争是非公平的)。
Tip:
-
Java中的wait、notify、notifyAll等方法依赖于ObjectMonitor对象内部方法来完成,这也就是为什么要在同步方法或同步代码块中调用他们的原因(需要先获取对象的锁),否则就会抛出 IllegalMonitorException 。
-
由于每个Java对象都可以关联一个Monitor对象,所以任何对象都是可以是锁对象,而基本数据类型不是对象,所以不能作为锁。
(3)同步方法和同步代码块的实现原理
同步代码块原理
public class SyncTest {
public static void main(String[] args) {
synchronized (SyncTest.class) {
}
}
}
对上面同步代码块编译:javac -encoding UTF-8 SyncTest.java
然后反汇编:javap -v SyncTest.class
得到字节码文件如下:
同步语句块的实现使用的是monitorenter 和 monitorexit指令:
monitorenter指令指向同步代码块的开始位置
monitorexit指令则指明同步代码块的结束位置。
为了保证方法异常结束时正确释放锁,编译器会自动产生一个异常处理器表,当发生异常时进行处理,确保该线程能正确的执行monitorexit指令。
同步方法原理
public class SyncTest {
public synchronized void fun(){
}
}
反汇编后得到的字节码文件内容:
同步方法通过ACC_SYNCHRONIZED来标志一个方法是同步方法。
当方法调用时,调用指令将会 检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有monitor, 然后再执行方法,最后在无论方法是否正常完成,都会释放monitor。
1.4.4 重量级锁OS层面的实现原理:互斥锁
重量级锁是依赖对象内部的monitor锁来实现的,而monitor又是依赖操作系统的MutexLock(互斥锁、互斥量)来实现的。
(1)互斥锁
互斥锁(互斥量)是一个二元变量,其状态为0/1,0表示未加锁,1表示已加锁。
-
访问公共资源前,必须申请该互斥锁,若处于未加锁状态,则申请到锁对象,并立即占有该锁。
-
如果该互斥锁处于锁定状态,则阻塞当前线程。
-
持锁线程使用完共享资源,释放共享资源,将互斥锁置0,并唤醒被阻塞的线程。
(2)重量级锁开销大的原因
在JDK1.6之前,synchronized属于重量级锁,效率低下,因为Monitor是依赖于底层的操作系统的互斥原语mutex来实现,JDK1.6之前实际上加锁时会调用Monitor的enter方法,解锁时会调用Monitor的exit方法。
多个线程竞争monitor时,被阻塞的线程不会消耗CPU,但是阻塞或者唤醒一个线程时,都需要操作系统来帮忙完成,这会导致线程在 “用户态 和 内核态” 之间来回切换,这个状态之间的转换需要相对比较长的时间,对性能有较大影响。
1.5 synchronized 优化(JDK1.6开始)
JDK1.6开始,对synchronized 进行了很多方面的优化,如:适应性自旋、锁消除、锁粗化、轻量级锁、偏向锁等,这些技术都是为了在线程之间更高效地共享数据及解决竞争问题,从而提高程序的执行效率。
1.5.1 锁升级
JDK1.6为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”,在JDK 1.6中,锁对象一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。
**锁可以升级但不能降级,**这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。
Tip:实际上锁也是可以发生降级的,只不过锁降级的条件比较苛刻。而且锁升降级效率较低,如果频繁升降级的话对性能就会造成很大影响。
1.5.1.1 偏向锁
偏向锁指,锁偏向于第一个获取他的线程,若接下来的执行过程中,该锁一直没有被其他线程获取,则持有偏向锁的线程永远不需要再进行同步。
目的:消除数据在无竞争情况下的同步,也就是说优化了只有一个线程执行 同步代码块 时的同步操作。
原理:CAS
(1)偏向锁的获取
- 检测锁对象的MarkWord是否为可偏向状态,即是否为偏向锁标识1,锁标识位为01;
- 如果为可偏向状态,判断线程ID是否指向当前线程,如果是进入步骤(5),否则进入步骤(3)。若为不可偏状态,直接升级为轻量级锁,进入轻量级锁逻辑。
- 如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行(5);如果竞争失败,执行(4)。
- 如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。
- 执行同步代码。
(2)偏向锁的释放
偏向锁使用了一种等到竞争出现才释放偏向锁的机制:偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。
(3)偏向锁的撤销(升级)
偏向锁释放失败(其他线程通过CAS替换原来线程的ThreadID失败),说明当前存在多线程竞争锁的情况,则开始进行偏向锁的撤销(升级)。
-
对持有偏向锁的线程进行撤销时,需要等待到达全局安全点。当到达全局安全点(safepoint,代表了一个状态,在该状态下所有线程都是暂停的)时,获得偏向锁的线程被挂起。
-
如果获得偏向锁的线程如果已经退出了临界区,也就是同步代码块执行完了,不存在锁竞争,那么这个时候争抢锁的线程可以基于 CAS 重新偏向当前线程,此过程称为重偏向。
-
如果获得偏向锁的线程的同步代码块还没执行完,处于临界区之内,存在锁竞争,这个时候会把原获得偏向锁的线程升级为轻量级锁(标志位为“00”),并将指向当前线程的锁记录地址的指针放入对象头Mark Word,后唤醒持有锁的当前线程,进入轻量级锁的竞争模式。
注意:其他情况下的偏向锁撤销
- 当对象计算过一致性哈希吗后(Object::hashCode() 或 System::identityHashCode(Object)方法的调用,重写的hashCode方法则不会),立即撤销偏向锁。
- 调用 wait/notify 方法
(4)批量重偏向、批量撤销
-
批量重偏向:当一个锁对象类的撤销次数达到20次时,虚拟机会认为这个锁不适合再偏向于原线程,于是会在偏向锁撤销达到20次时让这一类锁尝试偏向于其他线程。
-
批量撤销:当一个锁对象类的撤销次数达到40次时,虚拟机会认为这个锁根本就不适合作为偏向锁使用,因此会将类的偏向标记关闭(0),之后现存对象加锁时会升级为轻量级锁,锁定中的偏向锁对象会被撤销,新创建的对象默认为无锁状态。
(5)偏向锁的关闭
偏向锁在JDK1.6、JDK1.7中是默认启用的,并且它在应用程序启动后几秒钟才被激活。
-
JVM参数关闭延迟:-XX:BiasedLockingStartupDelay=0
如果你确定应用程序里所有的锁通常情况下处于竞争状态,则可以用以下参数关闭偏向锁,那么程序默认会进入轻量级锁状态: -
JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false
1.5.1.2 轻量级锁
轻量级锁是JDK1.6时加入的新型锁机制,“轻量级” 是相对于使用操作系统的 互斥量 实现的传统锁而言的。
- 目的:同步周期内,没有多线程竞争的前提下(多线程之间交替执行同步代码块,没有发生竞争),避免使用互斥量带来的性能损耗。
- 原理:CAS + 自旋
- 特点:非阻塞同步、乐观锁
(1)轻量级锁的获取
1.在代码进入同步块时,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝。
2.拷贝对象头中的Mark Word复制到锁记录中。
3.拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock Record里的owner指针指向object mark word。如果更新成功,则执行步骤(4),否则执行步骤(5)
4.如果这个更新动作成功,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态。
5.如果这个更新操作失败,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧。如果是就说明当前线程已经拥有了这个对象的锁,说明是一次重入,创建锁记录并设置Displaced Mark Word为null,起到了一个重入计数器的作用,然后进入同步块继续执行。否则执行步骤(6)。
6.若Mark Word不指向当前线程的栈帧,则说明多个线程竞争锁,此时强锁失败的线程通过自旋,不断重试步骤(3),自旋次数超过10次,则进行锁膨胀,即轻量级锁膨胀为重量级锁。
执行完同步代码块代码,退出同步代码块,使用CAS开始轻量级锁解锁,线程会使用CAS操作把对象当前的Mark Word和线程中复制的Displaced Mark Word替换回来。
- CAS 替换成功,成功释放锁,恢复到无锁的状态(01)。
- CAS 替换失败,则释放锁,唤醒被挂起阻塞的线程。
当超过自旋阈值,竞争的线程就会把锁升级为重量级锁,因此CAS 替换可能会失败,由于此时已经是重量级锁,则唤醒被挂起阻塞的线程,之后开始重量级锁的竞争逻辑了。
锁升级的整个流程:
1.5.1.3 自旋锁
自旋,就是指当有另外一个线程来竞争锁时,这个线程会在原地循环等待(让该线程执行一段无意义的忙循环,CPU忙等待),而不是把该线程给阻塞,直到获得锁的线程释放锁之后,这个线程就可以马上获得到锁。
轻量级锁的竞争就是采用的自旋锁机制。
-
引入自旋锁的原因:互斥同步对性能最大的影响是阻塞的实现,因为阻塞和唤醒线程需要用户态和内核态的相互转换,这些操作给系统的并发性能带来很大的影响。
-
自旋锁在JDK 1.4.2中引入,默认关闭,但是可以使用-XX:+UseSpinning开启;在JDK1.6中默认开启。
-
自旋锁的缺点:自旋等待不能替代阻塞,虽然它可以避免线程切换带来的开销,但是它占用了处理器的时间。如果持有锁的线程很快就释放了锁,那么自旋的效率就非常好;反之,自旋的线程就会白白消耗掉处理器的资源,它不会做任何有意义的工作,这样反而会带来性能上的浪费。
-
自旋等待的时间(自旋的次数)必须要有一个限度,如果自旋超过了定义的时间仍然没有获取到锁,则应该进入阻塞状态。通过参数-XX:PreBlockSpin可以调整自旋次数,默认的自旋次数为10。
-
自适应自旋锁:JDK1.6引入自适应的自旋锁,自适应就意味着自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定: 在同一锁对象上,若线程刚刚的一次自旋操作成功过,那么JVM会认为这次自旋成功的可能性会很高,就多自旋几次;反之,就少自旋甚至不自旋。
-
适用范围:适用于临界区代码少,执行快、锁竞争频率较低的情况。
1.5.1.4 偏向锁、轻量级锁、重量级锁的比较
锁 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
偏 向 锁 | 加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。 | 加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。 | 加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。 |
轻量级锁 | 竞争的线程不会阻塞,提高了程序的响应速度。 | 如果始终得不到锁竞争的线程使用自旋会消耗CPU。 | 追求响应时间。同步块执行速度非常快。 |
重量级锁 | 线程竞争不使用自旋,不会消耗CPU。 | 线程阻塞,响应时间缓慢。 | 追求吞吐量。同步块执行速度较长。 |
1.5.1.5 对象信息存在哪
-
重量级锁:对象信息存储在ObjectMonitor的_header成员变量中;
-
轻量级锁的实现中,会通过线程栈帧的锁记录存储Displaced Mark Word;
-
当一个对象当前正处于偏向锁状态,并且需要计算其identity hash code的话,则它的偏向锁会被撤销,并且锁会膨胀为轻量级锁或者重量锁;
-
当一个对象已经计算过identity hash code,它就无法进入偏向锁状态;
1.5.2 锁消除
**锁消除指的就是虚拟机即使编译器在运行时,如果检测到那些共享数据不可能存在竞争,那么就执行锁消除。**锁消除可以节省毫无意义的请求锁的时间。
JVM参数禁止锁消除优化: -XX:-EliminateLocks
StringBuffer类的append()方法:
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
package cu.edu.xupt.acat.sync;
public class EliminateLocksTest {
public static void fun() {
StringBuffer buffer = new StringBuffer();
buffer.append("hello");
buffer.append("world");
}
public static void main(String[] args) {
long start = System.currentTimeMillis();
for (int i = 0; i < 10000000; i++) {
fun();
}
long end = System.currentTimeMillis();
System.out.println(end - start);
}
}
注:因为buffer是局部变量,所以不会发生线程安全问题,虽然buffer的内部加了锁,但是实际上锁并没有被使用,因为JVM对锁进行了消除。
StringBuffer作为局部变量,作用于方法内部,不可能逃逸出该方法,因此他就不可能被多个线程同时访问,也就没有资源的竞争,但是StringBuffer的append操作却需要执行同步操作,因此Java即时编译器会进行锁消除的优化。
1.5.3 锁粗化
通常情况下,为了保证多线程间的有效并发,应该将临界区范围限制的尽量小,但是某些情况下,如果一系列的连续操作都对 同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中的,那即使没有出现线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。
- 如果虚拟机检测到有一串零碎的操作都是对同一对象的加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部。
频繁加锁、解锁:for(int i=0;i<size;i++){ synchronized(lock){ } }
锁粗化后
synchronized(lock){ for(int i=0;i<size;i++){ } }
1.6 synconised(重量级锁)内部(锁升级的过程)
锁升级的过程:new-偏向锁-轻量级锁(无锁,自旋锁,自适应自旋)-重量级锁
synchronized锁升级常见问题:
(1)偏向锁和轻量级锁是用户空间完成,重量级锁需要像内核申请(这时候markword记录的是ObjectMonitor)
(2)锁升级的过程:当第一个线程来的时候,把线程id写到markword里(偏向锁),当第二个线程来的时候,先把偏向锁撤销,线程1和线程2自旋竞争,这两个线程在自己的线程栈内部生成LR(Lock Record),用自旋的方式,当竞争的锁的指针指向哪个线程的LR,那么哪个线程就持有了这把锁,另一个线程继续CAS等待。
(3)自旋锁什么时候升级为重量级锁
答:竞争加剧或自旋线程数超过CPU核数的一半
(4)synchronized优化的过程和markword息息相关
(5)什么叫偏向锁启动和偏向锁未启动
答:偏向锁未启动,new出来的对象markword是001,偏向锁启动new出来的对象是101
(6)为什么有自旋锁还需要重量级锁?
答:自旋是消耗CPU资源的,如果锁的时间长,或者自旋线程多,CPU会被大量消耗,重量级锁有等待队列,所有拿不到锁的进入等待队列,不需要消耗CPU资源。
(7)偏向锁是否一定比自旋锁效率高?
答:不一定,在明确知道会有多线程竞争的情况下,偏向锁肯定会涉及锁撤销,这时候直接使用自旋锁。JVM启动过程,会有很对线程竞争(明确),所以默认情况启动时不打开偏向锁,过一段时间再打开。
(8)匿名偏向锁加锁,一定是偏向锁
(10)
(11)
(12)synchroize(this)与synchroise锁这个方法(非静态)是等价的
synchroize(T.Class)与synchroise锁这个方法(静态)是等价的
synchroize保证原子性和可见性
synchronize锁的是对象,不是代码
锁定方法和非锁定方法,同时执行
synchronized(object)->不能用String常量,Integer,Long等基础数据类型
(13)synchronize可重入锁
(14)异常跟锁
(15)
(15)sync(object)
markword记录这个线程的id(偏向锁)
如果线程争用,升级为自旋锁->10次以后->升级为重量级锁-os
执行时间短(加锁代码),线程数少,用自旋锁
执行时间长,线程数多,用系统锁
(16)锁优化
锁细化:
下面的问题需要给Object 加final,就可以保证这个对象不变
eg:
2.CAS
CAS(自旋锁是一个乐观锁、轻量级锁):读取一个值,修改它,再往回写的时候,判断这个值是不是当初读的那个值,如果不是拿到这个值,修改它,再往回写的时候,再次判断,直到新值与原值相等
ABA问题:读取一个值,修改它,再往回写的时候,这个值是当初读的那个值,但是只是值相等,中间这个值可能被改过,然后又改回来了。如果是基础类型,那么无所谓,如果是引用类型,就有所谓。
解决ABA问题:利用版本号解决,如果有修改,版本号就加一,如果用布尔类型,如果有修改,就变为false
如果在判断完原值和新值一致的时候,被其他线程打断了,那么最终的值就会有问题,这时候就要保证原子性
原子性:执行一段代码,不可以被其他线程打断
最终实现:
JVM能处理的锁叫轻量级锁,JVM不能处理,交给操作系统处理的锁叫重量级锁。
轻量级锁:自旋锁(也叫无锁)
轻量级锁自旋等
重量级锁在等待队列等
调用wait()方法,轻量级锁转化成重量级锁
偏向锁:不是一把锁,只是简单标记
3.锁重入
(1)sychronized:可重入锁(可重入锁必须记录,因为要解锁几次必须得对应)
偏向锁、自旋锁->线程锁->LR+1
重量级锁->?Object Monitor字段上
4.下面这个类等同于c->malloc free c++->new delete,直接操作内存
七.对象的内存布局
上锁在hotspot的实现就是修改markword,所以锁信息被记录在markword里面
markword里面还记录了JC和hashcode的信息
注:如果偏向锁打开,默认是匿名偏向状态
markword占8个字节,所以前8位是markword,接下来四位是类型指针class pointer ,接下来是成员变量instance status (不是所有变量都有,object就没有)具体这个变量是多少字节就是多少字节,如果是long就是8位,如果加一起不够被8整除,那么需要添加字节
八.锁降级的过程
这把锁不被其他任何线程锁定,也就是GC的时候才会降级,所以可以简单认为锁降级不存在。
https://www.zhihu.com/question/63859501
九.Volatile
1.Volatile保证线程的可见性
(1)MESI-缓存一致性协议
2.禁止指令重排序(有序性)
(1)DCL单例
下列线程不安全
加锁-可以解决线程不安全问题
一上来就加锁不太好,先检查,后加锁(锁细化),但是这种写法不对,第一个请求来的时候通过了第一层检查,还没有执行下面代码的时候,这时候第二个请求也来了,也通过了第一层检查
双重检查:先不加volatile,双重检查基本上可以保证线程安全,但是会有小概率出现指令重排序
加上volatile,防止指令重排序
创建对象有3步:1.申请内存,变量初始化(赋初值) 2.把真正的值赋值给赋初值的变量 3.把创建变量的地址指向这个变量,这三步可能会出现指令重排,加了volatile,就不能指令重排了
3.volatile不能保证原子性,正常执行加一,第一个线程编成2,第二个线程和第三个线程虽然对这个变量可见,但是他们各自加一,就会导致这个变量编成3,并没有执行加一再加一。
十.线程
1. 进程:程序,任务或者进程,进程是静态概念,分配资源,进程是系统资源分配的基本单元
2. 线程:动态概念,共享资源,线程是系统执行任务的基本单元
3. 协程:轻量级线程
1.线程安全
当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那么这个对象就是线程安全的。
Java语言中得各种操作共享数据可以分成五类,按安全程度由强到弱,来排序:
1. 不可变: 不可变的对象一定是线程安全的,无论对象的方法实现还是方法的调用者都不需要在采取安全措施。
如果共享数据是一个基本数据类型,那么只要在定义的时候使用final关键字修饰就可以保证它不可变;如果是共享数据是一个对象,需要对象自行保证其行为不会对其状态产生任何影响。
2. 不可变: 绝对线程安全是不管运行是环境如何,调用者都不需要任何额外的同步措施。通常需要付出很大的甚至不切实际的代价。
3. 相对线程安全: 就是通常意义的线程安全,确保这个对象单独的操作是操作安全的。Java语言中,大部分声称线程安全的类都属于这种类型,如:Vector、Hashtable等
3. 线程对立: 无论调用端是否采取了同步措施,都无法在多线程环境中并发使用代码。
2.互斥同步
互斥和同步是一种最常见、最主要的实现并发正确性(相对线程安全)的保障手段。
- 同步:指的是在多个线程并发访问共享数据时,保证共享数据在同一个时刻只能被一个线程使用。
- 互斥指的是实现同步的一种手段,临界区互斥量和信号量都是主要的互斥实现方式。
在 Java 中,最基本的互斥同步手段就是synchronized关键字。synchronized可以保证线程竞争共享资源的正确性(多个线程并发访问共享数据时,保证共享数据在同一个时刻只能被一个线程使用)
十一.java中得四种引用类型,强软弱虚
1.强:当有任何一个引用指向一个对象的时候,这个对象叫有用的对象,引用没有了,对象就变成了没用的对象,就被回收了
2.弱引用
(1)只要遇到垃圾回收,就当弱引用不存在(只有弱引用指向的对象直接被回收),强引用的对象继续存在
(2)应用常景:ThreadLocal,weakHashmap,这两个都是为了防止内存泄漏
3.软引用
(1)m是强引用,SoftReference指向new byte是软引用
(2)如果一个内存空间只能放两个对象,其实一个对象是强引用指向,另一个是软引用指向,这时候来了一个新对象,发现空间不够了,把软引用干掉,如果空间够,就不干掉软引用指向的对象。
图解:
(3)垃圾回收器遇到软引用时,不会回收
(4)做备用,缓存
4.虚引用
(1) 通知垃圾回收器进行特殊回收
十二.ThreadLocal
1.相当于一个容器,在这里放的东西,只有本线程可以读出来,其他线程读不出来
应用:@transaction(Spring声明式事务)
图解:从数据库连接拿多个连接出来,注入bean,connect连接是一个,才能保证事物
原理:拿第一个connection连接的时候,注入到ThreadLocal里,当第二次拿连接的时候,从TheadLocal里拿,就可以保证是同一个连接。
ThreadLocal原理:弱引用
十三:云相关提纲
十四. 各种锁
1.乐观锁:坏事未必会发生,所以事后补偿
-----自旋锁:一种常见的乐观锁实现
(1)ABA问题(加版本号或者布尔类型)
(2)保障CAS操作的原子性问题(lock指令)
2.悲观锁:坏事一定会发生,所以先做预防(上锁)
3.读写锁:读锁和写锁
(1)读锁:读的时候,不允许写,但是允许同时读
(2)写锁:写的时候,不允许写,也不允许读
4.排他锁:只有一个线程能访问代码(sychronized)
5.共享锁:可以允许有多个线程访问代码
6.统一锁:大粒度的锁
(1)锁定A等待B,锁定B等待A 这种可能会产生死锁,这时候A+B统一成为大锁
7.分段锁:分成一段一段的小粒度锁
(1)JDK1.7 CHM ConcurrentHashMap
8.间隙锁
十五.线程池
1.线程池:
2.线程池7个参数:
(1)核心线程数
(2)最大线程数
(3)生存空间
(4)时间单位
(5)任务队列
(6)线程工厂
(7)拒绝策略
3.什么是纤程或者协程?和普通的的java纤程有啥不同?
为什么它能提高效率?是不是M:N永远优于1:1?
4.线程:
启动线程的三种方式:(1)Thread (2)Runnable (3)Excutors.newCachedThread
线程的3个方法:sleep,yeild,join
线程的状态:
十六.JUC同步锁
1.ReentranLock 可重入锁,可以替代synchronized
(1)trylock
(2)底层是CAS
(3)lockinterupptibly和公平及非公平
2.countDownLatch 门栓
也可以用join方法:
3.CycleBarrier
JUC 包中提供类似的工具,可以设置一个简单的集合点,等所有成员到齐了之后,再执行下一步操作。CycleBarrier 它就相当于是一个栅栏,所有线程在到达栅栏后都需要等待其他线程,等所有线程都到达后,再一起通过。(线程到了parties数量的时候,继续执行)
CyclicBarrier是回环屏障的意思,可以让一组线程全部达到一个状态后再全部同时执行。
这里之所以叫作回环是因为当所有等待线程执行完毕,并重置CyclicBarrier的状态后它可以被重用。之所以叫作屏障是因为线程调用await方法后就会被阻塞,这个阻塞点就称为屏障点,等所有线程都调用了await 方法后,线程就会冲破屏障,继续向下运行
————————————————
注:(1)Semaphore信号量机制,CountDownLatch、循环栅栏Cycle barrier并称为三大队列式同步器
(2) Sychronised和ReentrantLock并称为两大重量级锁,ReentrantReadWriteLock是 ReentrantLock的子类。
(3)CyclicBarrier阻塞一组线程,直至某个状态之后再全部同时执行,并且所有线程都被释放。和之前说的CountdownLatch功能相反,CountdownLatch阻塞主线程,等所有子线程完结了再继续下去。
上节介绍的CountDownLatch在解决多个线程同步方面相对于调用线程的join方法已经有了不少优化,但是CountDownLatch的计数器是一次性的,也就是等到计数器值变为0后,再调用CountDownIatch的await和countdown方法都会立刻返回,这就起不到线程同步的效果了。所以为了满足计数器可以重置的需要,JDK开发组提供了CyclicBarrier类,并且CyclicBarrier类的功能并不限于CountDownLatch的功能。从字面意思理解,CyclicBarrier是回环屏障的意思,可以让一组线程全部达到一个状态后再全部同时执行。
这里之所以叫作回环是因为当所有等待线程执行完毕,并重置CyclicBarrier的状态后它可以被重用。之所以叫作屏障是因为线程调用await方法后就会被阻塞,这个阻塞点就称为屏障点,等所有线程都调用了await 方法后,线程就会冲破屏障,继续向下运行
(4)从javadoc的描述可以得出:
- CountDownLatch:一个或者多个线程,等待其他多个线程完成某件事情之后才能执行;
- CyclicBarrier:多个线程互相等待,直到到达同一个同步点,再继续一起执行。
-
对于CountDownLatch来说,重点是“一个线程(多个线程)等待”,而其他的N个线程在完成“某件事情”之后,可以终止,也可以等待。而对于CyclicBarrier,重点是多个线程,在任意一个线程没有完成,所有的线程都必须互相等待,然后继续一起执行。
CountDownLatch是计数器,线程完成一个记录一个,只不过计数不是递增而是递减,而CyclicBarrier更像是一个阀门,需要所有线程都到达,阀门才能打开,然后继续执行
4.Phaser
先指定N个线程等N个线程到齐了开始干第一阶段的活,等第一阶段所有线程干完活了,接着这N个线程继续开始干下一阶段的活,以此类推直至干完业务逻辑了所有阶段的活(当然每个阶段可以干完进入下一个阶段可以踢掉一下不需要的线程,当然也可以全部留下来,不剔除)
5.ReadWriteLock
共享锁(读)和排他锁(写)
ReadWriteLock的升级态:StampedLock
6.Semaphore信号量
应用:限流,车道和收费站
7.Exchanger(交换器)
8.LockSupport
可以实现当前线程阻塞,但是不需要加锁,unpark可以在park之前使用
以下解答问题的程序是否正确?
结果:
答:程序有问题,看结果可知,并没有在5停住,第一个原因:在集合里加元素的时候,sise还没有加一,第二个线程就读了,可能会有错误输出,需要加同步synchnised(给add方法和size方法加)。第二个原因:两个线程之间对变量不是实时可见的,所以停止不了,需要加valatile,改进:在lists之前加volatile,容器变成同步容器
结果是对的,但是把睡眠1s去掉之后就不好用了。原因:volatile修饰的是lists,lists是一个引用,但是实际上引用并没有变化,变化的是引用中得值,volatile检测不到引用中得值得变化,所以在实际使用中,轻易不要用volatile,并且volatile修饰的变量越简单越好。
另一种实现:
还是不行,wait释放锁,notify不释放锁,所以必须t1线程执行完了,t2才可以继续执行,
解决方法1:notify之后,执行wait方法,让线程t1阻塞,这时候线程t2就可以获得锁,继续执行了,不用notifyAll的原因是因为只有两个线程,没必要
解决方法2:
如果注释掉t1线程里的睡眠1s,就会发现t2结束在add6之后,所以上述程序也有问题,解决方法,用两个countDown
解决方法3:
去掉t1里面的睡眠1s之后,也会
也是不对的,与countDown问题是一样的,需要两个LockSupport.park才能解决
解决方法3的改进:
结果没问题。t2线程的if(c.size!=5)这句话可以去掉,直接执行t2线程的LockSupport.park(),这样也是没问题的
解决方法4:
t1到5之后,release掉资源,这时候不能保证是t2 acquire到,还是t1再次acquire到,所以程序还是有点问题,改进:
在t1 release之后,修改为如下代码:
这时候由于t2先启动,所以可能会出现t2 acquire到资源,然后直接release掉了
修改方法:在t2.join()之前,加上t2.start(),t2线程在这里启动,结果就对了
题干:要求A线程打印A~Z,B线程打印1~26,出现A1B2C3这样的结果
如果用if,唤醒之后就往下执行了,而用while会在判断一遍,看看是不是还是等于max,所以必须用Max
上述写法有个小瑕疵,如果是生产者被阻塞,notifyAll会叫醒所有线程,但是生产者阻塞的话,是没有必要叫醒生产者,只要叫醒消费者就可以
eg:
4.高并发三大解决策略:缓存,限流,降级
如有侵权,请联系我删除
参考博客:
Java并发:synchronized关键字如何保证线程安全_synchronized是如何保证线程安全-CSDN博客
参考笔记:B站马士兵视频及笔记(建议大家去看视频)