东莞医疗网站建设报价,建设网站如入什么费,网站建设仟金手指六六14,flash 网站作为一个Java开发#xff0c;对于Synchronized这个关键字并不会陌生#xff0c;无论是并发编程#xff0c;还是与面试官对线#xff0c;Synchronized可以说是必不可少。
在JDK1.6之前#xff0c;都认为Synchronized是一个非常笨重的锁#xff0c;就是在之前的《谈谈Java…作为一个Java开发对于Synchronized这个关键字并不会陌生无论是并发编程还是与面试官对线Synchronized可以说是必不可少。
在JDK1.6之前都认为Synchronized是一个非常笨重的锁就是在之前的《谈谈Java中的锁》中提到的重量级锁。但是在JDK1.6对Synchronized进行优化后Synchronized的性能已经得到了巨大提升也算是脱下了重量级锁这一包袱。本文就来看看Synchronized的使用与原理。
JDK1.6后优化点
锁粗化Lock Coarsening、锁消除Lock Elimination、轻量级锁Lightweight Locking、偏向锁Biased Locking、适应性自旋Adaptive Spinning等技术来减少锁操作的开销synchronized的并发性能已经基本与J U C包提供的Lock持平
一、Synchronized的使用
在Java中synchronized有【修饰实例方法】、【修饰静态方法】、【修饰代码块】三种使用方式分别锁住不同的对象。这三种方式获取不同的锁锁定共享资源代码段达到互斥mutualexclusion效果以此保证线程安全。 共享资源代码段又被称之为临界区锁的作用就是保证临界区互斥即同一时间临界区的只能有一个线程执行其他线程阻塞等待排队等待前一个线程释放锁。 1.1 修饰实例方法
作用于当前对象实例加锁进入同步代码前要获得 当前对象实例的锁
public synchronized void methodA() {System.out.println(作用于当前对象实例加锁进入同步代码前要获得 当前对象实例的锁);
}1.2 修饰静态方法
给当前类加锁作用于当前类的所有对象实例,进入同步代码前要获得 当前 class 的锁。
当被static修饰时表明被修饰的代码块或者变量是整个类的一个静态资源属于类成员不属于任何一个实例对象也就是说不管 new 了多少个对象都只有一份
public synchronized static void methodB() {System.out.println(给当前类加锁作用于当前类的所有对象实例,进入同步代码前要获得 **当前 class 的锁**。);
}如果一个线程 A 调用一个实例对象的非静态 synchronized 方法而线程 B 需要调用这个实例对象所属类的静态 synchronized 方法是不会发生互斥现象因为访问静态 synchronized 方法占用的锁是当前类的锁而访问非静态 synchronized 方法占用的锁是当前实例对象锁。
1.3 修饰代码块
指定加锁对象对给定对象/类加锁。
synchronized(this|object) 表示进入同步代码库前要获得给定对象的锁。
public void methodc() {synchronized(this) {System.out.println(锁住的是当前对象对象锁);}
}synchronized(类.class) 表示进入同步代码前要获得 当前 class 的锁
javapublic void methodd() {synchronized(SynchronizedDome.class) {System.out.println(给当前类加锁SynchronizedDome class 的锁);}
}二、Synchronized案例说明
了解了Synchronized的使用方法以后接下来结合案例的方式来详细看看Synchronized的加锁多线程下是怎么执行的我这里将按照上面三个使用方法来分别使用案例描述
2.1 修饰实例方法案例
案例说明两个线程同时对一个共享变量sum进行累加3000输出其最终结果我们期望的结果最终应该是6000接下来看看不加Synchronized修饰和加Synchronized修饰的情况下分别输出什么。
2.1.1 案例
不加Synchronized修饰
package com.upup.edwin.sync;public class SynchronizedDome {//定义来了一个共享变量public static int sum 0;// 进行累加3000次public void add() {for (int i 0; i 3000; i) {sum sum 1;}}public static void main(String[] args) throws InterruptedException {SynchronizedDome dome new SynchronizedDome();Thread thread1 new Thread(() - dome.add());Thread thread2 new Thread(() - dome.add());thread1.start();thread2.start();//join() 方法是让main线程等待线程 thread1 和thread2 都执行完成之后在继续执行下面的输出thread1.join();thread2.join();System.out.println(两个线程执行完成之后的累加结果sum sum);}} 从结果上看当我们不加synchronized修饰的时候输出结果并不是我们锁期待的6000这说明两个线程之间在执行的时候相互干扰了也就是线程不安全。
加Synchronized修饰
我们对上面的add方法进行改造在方法上加上synchronized关键字也就是加上了锁来看看它的执行结果
// 进行累加3000次
public synchronized void add() {for (int i 0; i 3000; i) {sum sum 1;}
}加上synchronized修饰后发现输出结果与我们预期的是一致的说明加上锁两个线程是排队顺序执行的。
2.1.2 案例执行过程
通过以上的两个案例对比可以发现在synchronized修饰方法的时候能够让结果正常输出保证了线程安全那么它是怎么做到的吗两个线程的执行过程是怎么样的呢
在前面我们提到了当synchronized修饰实例方法的时候获取的是当前对象实例的锁我们在代码中new出了一个SynchronizedDome对象因此本质上锁住的是这个对象
SynchronizedDome dome new SynchronizedDome();所有线程要执行同步函数都要先获取锁synchronized里面叫做监视器锁获取到锁的线程才能执行同步函数没有获取到的线程只能等待抢到锁的线程执行完同步函数后会释放锁并通知唤醒其他等待的线程再次获取锁。流程如下 2.2 修饰静态方法案例
修饰静态方法与修饰实例方法基本一致唯一的区别就是锁的不是当前对象而是整个Class对象。我们只需要把上述案例中同步函数改成静态的就可以了
package com.upup.edwin.sync;public class SynchronizedDome {//定义来了一个共享变量public static int sum 0;// 进行累加3000次public synchronized static void add() {for (int i 0; i 3000; i) {sum sum 1;}}public static void main(String[] args) throws InterruptedException {Thread thread1 new Thread(() - add());Thread thread2 new Thread(() - add());thread1.start();thread2.start();//join() 方法是让main线程等待线程 thread1 和thread2 都执行完成之后在继续执行下面的输出thread1.join();thread2.join();System.out.println(两个线程执行完成之后的累加结果sum sum);}} 可以看到当我们改成静态方法之后就不需要在main方法中new SynchronizedDome()了直接调用add即可这也说明锁的不是当前对象了 我们知道在Java中静态资源是属于Class的不属于任何一个实例对象而每个Class对象在Jvm中都是唯一的所以我们锁住Class对象后其他线程无法获取其静态资源了从而进入等待阶段本质上锁住静态资源的执行过程与锁住实例方法的执行过程是一致的只是锁的对象不一样而已。
2.3 修饰代码块案例
静态资源锁Class实例方法锁对象还有一种就是锁住一个方法的某一段代码也就是代码块。比如我们在上述的add 方法中调用了一个print方法 public static void print(){try {Thread.sleep(5000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println(这是一个不需要加锁的方法当前执行的线程是 Thread.currentThread().getName());}如上print就是睡眠了5秒钟后输出一句话不涉及到线程安全问题如果使用synchronized修饰整个Add方法并且在add中调用 print()如下
public synchronized static void add() {print();for (int i 0; i 3000; i) {sum sum 1;}
}这种方式synchronized就会把add()整个包裹使整个程序执行时间变长完整案例如下
package com.upup.edwin.sync;public class SynchronizedDome {//定义来了一个共享变量public static int sum 0;// 进行累加3000次public synchronized static void add() {print();for (int i 0; i 3000; i) {sum sum 1;}}// 可以异步执行的方法public static void print(){try {Thread.sleep(5000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println(这是一个不需要加锁的方法当前执行的线程是 Thread.currentThread().getName());}public static void main(String[] args) throws InterruptedException {long l1 System.currentTimeMillis();Thread thread1 new Thread(() - add());Thread thread2 new Thread(() - add());thread1.start();thread2.start();//join() 方法是让main线程等待线程 thread1 和thread2 都执行完成之后在继续执行下面的输出thread1.join();thread2.join();long l2 System.currentTimeMillis();System.out.println(两个线程执行完成的时间是l2 - l1 (l2 - l1) 毫秒);System.out.println(两个线程执行完成之后的累加结果sum sum);}}以上案例执行结果如下 很明显两个线程在排队执行Add方法时连print方法一起等待但是实际上print是一个线程安全的方法不需要获取锁并且print方法还比较耗时这就拖慢了整个程序的执行总时长其执行过程如下 这种方式会将线程安全的方法也锁住导致排队执行代码变多时间变长其本质就是synchronized锁住的是整个Add方法粒度比较大我们可以对add进行改造一下让它只锁累计的那一段代码
public static void add() {print();synchronized(SynchronizedDome.class){for (int i 0; i 3000; i) {sum sum 1;}}
}如上synchronized只锁了这for循环段代码print()是可以并行执行的这样就可以提升整个方法的执行效率完整代码如下
package com.upup.edwin.sync;public class SynchronizedDome {//定义来了一个共享变量public static int sum 0;// 进行累加3000次public static void add() {print();synchronized(SynchronizedDome.class){for (int i 0; i 3000; i) {sum sum 1;}}}// 可以异步执行的方法public static void print(){try {Thread.sleep(5000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println(这是一个不需要加锁的方法当前执行的线程是 Thread.currentThread().getName());}public static void main(String[] args) throws InterruptedException {long l1 System.currentTimeMillis();Thread thread1 new Thread(() - add());Thread thread2 new Thread(() - add());thread1.start();thread2.start();//join() 方法是让main线程等待线程 thread1 和thread2 都执行完成之后在继续执行下面的输出thread1.join();thread2.join();long l2 System.currentTimeMillis();System.out.println(两个线程执行完成的时间是l2 - l1 (l2 - l1) 毫秒);System.out.println(两个线程执行完成之后的累加结果sum sum);}}修改之后整个方法的执行时间只有5秒多我们休眠的时间也是5秒说明两个线程是一起进入的休眠并不是排队的其执行过程如下 在上述案例中我使用的是锁住整个class的方法‘synchronized(SynchronizedDome.class)’,如果要改成锁住对象只需要改成’synchronized(this)即可。其他执行流程都是一样的只是获取的锁不一样
三、Synchronized原理剖析
以Hotspot(是Jvm的一种实现)为例在Jvm中每个Class都有一个对象对象又由 【对象头 实例数据 对齐填充(java对象必须是8byte的倍数)】三部分组成每个对象都有一个对象头synchronized的锁就是存在对象头中的。
3.1 对象头
既然synchronized的锁是存在对象头中的那就先来了解一下对象头Hotspot 有两种对象头
数组类型如果对象是数组类型则虚拟机用3字节存储对象头非数组类型如果对象是非数组类型则用2字节存储对象头 一般对象头由两部分组成
Mark Word
存储自身的运行时数据比如对象的HashCode分代年龄和锁标志位信息。
Mark Word存储的信息与对象自身定义无关所以Mark Word是一个一个非固定的数据结构Mark Word里存储的数据会在运行期间随着锁标志位的变化而变化。
Klass Pointer类型指针指向它的类元数据的指针。
Mark Word在不同的虚拟机下的bit位不一样以下是32位与64位虚拟机的对比图 3.2 Monitor
在了解Monitor之前先思考一个问题前面我们说synchronized修饰方法和代码块的时候加锁业务流程的执行过程是一样的那么他们内部加锁实现是不是一样的呢
其实加锁过程肯定是不一样的不然加锁过程一样锁一样加锁业务流程的执行过程是一样那就没必要分成方法和代码块了
我们可以找到上述案例中的SynchronizedDome类的Class文件然后再命令行中执行javap -c -s -v -l SynchronizedDome.class就可以看到编译后指令集。可以分别查看synchronized修饰方法和代码块指令集的区别
synchronized代码块 上图中的指令集中有monitorenter、monitorexit两个指令当synchronized修饰代码块时JVM就是使用monitorenter和monitorexit两个指令实现同步的当线程执行到monitorenter的时候要先获得monitor锁才能执行后面的方法。当线程执到monitorexit的时候则要释放锁。
synchronized修饰方法 上图中的指令集中有一个ACCSYNCHRONIZED标记当synchronized修饰方法时,JVM通过在方法访问标识符(flags)中加入ACCSYNCHRONIZED来实现同步功能当线程执行有ACCSYNCHRONIZED标志的方法需要获得monitor锁。每个对象都与一个monitor相关联线程可以占有或者释放monitor。
3.1什么是Monitor锁
从上面的描述无论是修饰代码块还是修饰方法都要获取一个Monitor锁那么什么是Monitor锁呢
Monitor即监视器可以理解为一个同步工具或一种同步机制通常被描述为一个对象。每一个Java对象就有一把看不见的锁称为内部锁或者Monitor锁。
Monitor锁与对象的关系图 任何一个对象都有一个Monitor与之关联当且一个Monitor被持有后它将处于锁定状态。Synchronized在JVM中基于进入和退出Monitor对象通过成对的MonitorEnter和MonitorExit指令来实现方法同步和代码块同步。
MonitorEnter插入在同步代码块的开始位置当代码执行到该指令时将会尝试获取该对象Monitor锁MonitorExit插入在方法结束处和异常处JVM保证每个MonitorEnter必须有对应的MonitorExit释放Monitor锁
3.2 Monitor锁的工作原理
每一个对象都会有一个monitor锁Monitor锁的MarkWord锁标识位为10其中指针指向的是Monitor对象的起始地址。
在Java虚拟机HotSpot中Monitor是由ObjectMonitor实现的ObjectMonitor中维护了一个锁池EntryList和等待池WaitSet。
ObjectMonitor工作模型图如下 ObjectMonitor工作模型图大致描述了以下几个步骤
所有新的线程都会进入(①号入口)EntryList中去竞争锁
当有线程通过CAS把monitor的owner字段设置为自己时说明这个线程获取到了锁也就是进入图中的②号入口owner区域其他线程进入阻塞状态
如果当前线程是第一次进入该monitor将recursions由0设置为1_owner为当前线程该线程成功获得锁并返回
如果当前线程不是第一次进入该monitor说明当前线程再次进入monitor即重入锁执行recursions 记录重入的次数
如果获取到锁的线程owner执行了wait等方法就会释放锁并进入③号入口waitset中于此同时通知waitset中其他线程重新竞争锁获取到锁(④号入口)进入owner区域
当线程执行完同步代码会释放锁由⑤号口出于此同时通知waitset和EntryList中其他线程重新竞争锁
释放锁线程执行monitorexitmonitor的进入数-1执行过多少次monitorenter最终要执行对应次数的monitorexit 四、Synchronized锁优化
接下来看看Synchronized的锁优化。锁优化主要包含锁粗化、锁消除、锁升级三部分。
4.1 锁粗化
同步代码块要求我们将同步代码的范围尽量缩小这样可以使同步的操作数量尽可能缩小缩短阻塞时间如果存在锁竞争那么等待锁的线程也能尽快拿到锁。
比如上述案例add的循环中如果将Synchronized防止for循环里面不是范围更小吗
for (int i 0; i 3000; i) {synchronized(SynchronizedDome.class){sum sum 1;}
}这样虽然缩小了范围但是未必缩短了时间因为在加锁过程中也会消耗资源如果频繁的加锁释放锁可能会导致性能损耗。
基于此JVM会对这种情况进行锁粗化锁粗化就是将【多个连续的加锁、解锁操作连接在一起】扩展成一个范围更大的锁避免频繁的加锁解锁操作。
J V M在检测到上述for循环再频繁获取同一把锁的到时候就会将加锁的范围粗化到循环操作的外部使其只需要获取一次锁就可以减小加锁释放锁的开销。 4.2 锁消除
Java虚拟机在JIT编译时会进行逃逸分析对象在函数中被使用也可能被外部函数所引用称为函数逃逸通过对运行上下文的扫描分析synchronized锁对象是不是只被一个线程加锁不存在其他线程来竞争加锁的情况。这样就可以消除该锁了提升执行效率。
锁消除的经典案例就是StringBuffer 了StringBuffer 是线程安全的其内部的append方法就是通过synchronized加锁的源码如下
Override
public synchronized StringBuffer append(String str) {toStringCache null;super.append(str);return this;
}当我们调用StringBuffer 的append时就会加锁但是当我们使用的对象经过逃逸分析后认为该对象不会被其他线程共享的时候就会将append方法的synchronized去掉编译不加入monitorenter和monitorexit指令。比如下面这个方法
public static String appendStr(String str, int i) {StringBuffer sb new StringBuffer();sb.append(str);sb.append(i);return sb.toString();
}StringBuffer的append虽然是同步方法。但appendStr中的sb对象没有传递到方法外不会被其他线程引用不存在锁竞争的情况因此可以进行锁消除。
五、Synchronized锁升级
我们常说的锁升级其实就是这几种锁的升级跃迁。其中有无锁、偏向锁 、轻量级锁、 重量级锁等几种锁的实现。锁升级过程【无锁】—【偏向锁】—【轻量级锁】—【 重量级锁】。 而锁的变化其实就是一个标志位的变化在前面提到的对象头中Mark World时有提到它存储的就是对象的HashCode分代年龄和锁标志位信息。因此锁的升级变化本质上就是Mark World中锁标志位的变化。以上几种锁的标志位信息如下
锁状态存储内容存储内容无锁对象的hashCode、对象分代年龄、是否是偏向锁001偏向锁偏向线程ID、偏向时间戳、对象分代年龄、是否是偏向锁101轻量级锁指向栈中锁记录的指针00重量级锁指向互斥量重量级锁的指针10
注意
锁可以升级不可以降级但是偏向锁状态可以被重置为无锁状态
锁可以升级不可以降级但是偏向锁状态可以被重置为无锁状态
锁可以升级不可以降级但是偏向锁状态可以被重置为无锁状态
5.1 无锁升级为偏向锁
为啥要有偏向锁
大多数情况下是一个线程多次获得同一个锁不存在锁竞争的而竞争锁会增大资源消耗为了降低获取锁的代价才引入的偏向锁。
当线程第一次执行到同步代码块的时候锁对象变成就会偏向锁通过CAS修改对象头里的锁标志位其目标就是在只有一个线程执行同步代码块时降低获取锁带来的消耗提高性能。
偏向锁是默认开启的而且开始时间一般是比应用程序启动慢几秒可以通过JVM配置成没有延迟
-XX:BiasedLockingStartUpDelay0可以通过J V M参数关闭偏向锁关闭之后程序默认会进入轻量级锁状态
-XX:-UseBiasedLockingfalse无锁升级为偏向锁其本质是判断对象头的Mark Word中线程ID与当前线程ID是否一致以及偏向锁标识如果一致直接执行同步代码或方法具体流程如下 无锁状态存储内容「是否为偏向锁0」锁标识位01
CAS设置当前线程ID到Mark Word存储内容中并且将是否为偏向锁0 修改为 是否为偏向锁1
在Mark Word和栈帧中记录获取到偏向的锁的threadID
执行同步代码或方法
偏向锁状态存储内容「是否为偏向锁1、线程ID」锁标识位01
对比线程ID是否一致如果一致无需使用CAS来加锁、解锁直接执行同步代码或方法
因为偏向锁不会自动释放锁因此后续线程A再次获取锁的时候需要比较当前线程的threadID和Java对象头中的threadID是否一致
如果不一致CAS将Mark Word的线程ID设置为当前线程ID设置成功执行同步代码或方法
其他线程如线程B要竞争锁对象而偏向锁不会主动释放因此Mark Word还是存储的线程A的threadID
此时会检查Mark Word的线程A是否存活如果没有存活那么锁对象被重置为无锁状态其它线程线程B可以竞争将其设置为偏向锁
CAS设置失败证明存在多线程竞争情况触发撤销偏向锁当到达全局安全点偏向锁的线程被挂起偏向锁升级为轻量级锁然后在安全点的位置恢复继续往下执行。
如果Mark Word的线程A是存活则线程B的CAS会失败此时会暂停线程A撤销偏向锁升级为轻量级锁
5.2 偏向锁升级为轻量级锁
轻量级锁又称自旋锁一般在竞争锁对象的线程比较少持有锁时间也不长的场景中由于阻塞线程、唤醒线程需要C P U从用户态转到内核态时间比较长如果同步代码块执行的时间比这更时间短那就本末倒置了所以这种情况一般不阻塞线程让其自旋一段时间等待锁其他线程释放锁通过自旋换取线程在用户态和内核态之间切换的开销。
锁竞争
如果多个线程轮流获取一个锁但是每次获取锁的时候没有发生阻塞就不存在锁竞争。只有当某线程尝试获取锁的时候发现该锁已经被占用只能等待其释放这才发生了锁竞争。
当前线程持有的锁是偏向锁的时候被另外的线程所访问偏向锁就会升级为轻量级锁其他线程会通过自旋的形式尝试获取锁不会阻塞从而提高性能。
升级为轻量级锁有两种情况
当关闭偏向锁功能时会由无锁直接升级为轻量级锁
多个线程竞争偏向锁导致偏向锁升级为轻量级锁
这两种情况下偏向锁升级为轻量级锁过程如下 无锁状态存储内容「是否为偏向锁0」锁标识位01
关闭偏向锁功能时 CAS设置当前线程栈中锁记录的指针到Mark Word存储内容锁标识位设置为00执行同步代码或方法
轻量级锁状态存储内容「线程栈中锁记录的指针」锁标识位00
CAS设置当前线程栈中锁记录的指针到Mark Word存储内容设置成功获取轻量级锁执行同步块代码或方法设置失败证明多线程存在一定竞争线程自旋上一步的操作自旋一定次数后还是失败轻量级锁升级为重量级锁Mark Word存储内容替换成重量级锁指针锁标记位10
5.3 轻量级锁升级为重量级锁
轻量级锁在自旋一定次数之后还没获取到锁就升级为重量级锁重量级锁是依赖操作系统的MutexLock互斥锁来实现的需要从用户态转到内核态成本非常高等待锁的线程都会进入阻塞状态防止CPU空转。 计数器记录自旋次数默认允许循环10次可以通过虚拟机参数更改