线程A释放锁后,会将共享变动操做刷新到主内存中java
线程B获取锁时,JMM会将该线程的本地内存置为无效,被监视器保护的临界区代码必须从主内存中读取共享变量数组
/** * 先定义一个测试模板类 * 这里补充一个知识点:Thread.sleep(long)不会释放锁 * 读者可参见笔者的`并发番@Thread一文通` */
public class SynchronizedDemo {
public static synchronized void staticMethod(){
System.out.println(Thread.currentThread().getName() + "访问了静态同步方法staticMethod");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "结束访问静态同步方法staticMethod");
}
public static void staticMethod2(){
System.out.println(Thread.currentThread().getName() + "访问了静态同步方法staticMethod2");
synchronized (SynchronizedDemo.class){
System.out.println(Thread.currentThread().getName() + "在staticMethod2方法中获取了SynchronizedDemo.class");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void synMethod(){
System.out.println(Thread.currentThread().getName() + "访问了同步方法synMethod");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "结束访问同步方法synMethod");
}
public synchronized void synMethod2(){
System.out.println(Thread.currentThread().getName() + "访问了同步方法synMethod2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "结束访问同步方法synMethod2");
}
public void method(){
System.out.println(Thread.currentThread().getName() + "访问了普通方法method");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "结束访问普通方法method");
}
private Object lock = new Object();
public void chunkMethod(){
System.out.println(Thread.currentThread().getName() + "访问了chunkMethod方法");
synchronized (lock){
System.out.println(Thread.currentThread().getName() + "在chunkMethod方法中获取了lock");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void chunkMethod2(){
System.out.println(Thread.currentThread().getName() + "访问了chunkMethod2方法");
synchronized (lock){
System.out.println(Thread.currentThread().getName() + "在chunkMethod2方法中获取了lock");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void chunkMethod3(){
System.out.println(Thread.currentThread().getName() + "访问了chunkMethod3方法");
//同步代码块
synchronized (this){
System.out.println(Thread.currentThread().getName() + "在chunkMethod3方法中获取了this");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void stringMethod(String lock){
synchronized (lock){
while (true){
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
复制代码
当一个线程进入同步方法时,其余线程能够正常访问其余非同步方法缓存
public static void main(String[] args) {
SynchronizedDemo synDemo = new SynchronizedDemo();
Thread thread1 = new Thread(() -> {
//调用普通方法
synDemo.method();
});
Thread thread2 = new Thread(() -> {
//调用同步方法
synDemo.synMethod();
});
thread1.start();
thread2.start();
}
复制代码
输出:安全
Thread-1访问了同步方法synMethod
Thread-0访问了普通方法method
Thread-0结束访问普通方法method
Thread-1结束访问同步方法synMethod
复制代码
分析:经过结果可知,普通方法和同步方法是非阻塞执行的数据结构
当一个线程执行同步方法时,其余线程不能访问任何同步方法多线程
public static void main(String[] args) {
SynchronizedDemo synDemo = new SynchronizedDemo();
Thread thread1 = new Thread(() -> {
synDemo.synMethod();
synDemo.synMethod2();
});
Thread thread2 = new Thread(() -> {
synDemo.synMethod2();
synDemo.synMethod();
});
thread1.start();
thread2.start();
}
复制代码
输出:并发
Thread-0访问了同步方法synMethod
Thread-0结束访问同步方法synMethod
Thread-0访问了同步方法synMethod2
Thread-0结束访问同步方法synMethod2
Thread-1访问了同步方法synMethod2
Thread-1结束访问同步方法synMethod2
Thread-1访问了同步方法synMethod
Thread-1结束访问同步方法synMethod
复制代码
分析:经过结果可知,任务的执行是阻塞的,显然Thread-1必须等待Thread-0执行完毕以后才能继续执行app
当同步代码块都是同一个锁时,方法能够被全部线程访问,但同一个锁的同步代码块同一时刻只能被一个线程访问ide
public static void main(String[] args) {
SynchronizedDemo synDemo = new SynchronizedDemo();
Thread thread1 = new Thread(() -> {
//调用同步块方法
synDemo.chunkMethod();
synDemo.chunkMethod2();
});
Thread thread2 = new Thread(() -> {
//调用同步块方法
synDemo.chunkMethod();
synDemo.synMethod2();
});
thread1.start();
thread2.start();
}
复制代码
输出:工具
Thread-0访问了chunkMethod方法
Thread-1访问了chunkMethod方法
Thread-0在chunkMethod方法中获取了lock
...停顿等待...
Thread-1在chunkMethod方法中获取了lock
...停顿等待...
Thread-0访问了chunkMethod2方法
Thread-0在chunkMethod2方法中获取了lock
...停顿等待...
Thread-1访问了chunkMethod2方法
Thread-1在chunkMethod2方法中获取了lock
复制代码
分析可知:
线程间同时访问同一个锁多个同步代码的执行顺序不定,即便是使用同一个对象锁
public static void main(String[] args) {
SynchronizedDemo synDemo = new SynchronizedDemo();
Thread thread1 = new Thread(() -> {
//调用同步块方法
synDemo.chunkMethod();
synDemo.chunkMethod2();
});
Thread thread2 = new Thread(() -> {
//调用同步块方法
synDemo.chunkMethod2();
synDemo.chunkMethod();
});
thread1.start();
thread2.start();
}
---------------------
//输出:
Thread-0访问了chunkMethod方法
Thread-1访问了chunkMethod2方法
Thread-0在chunkMethod方法中获取了lock
...停顿等待...
Thread-0访问了chunkMethod2方法
Thread-1在chunkMethod2方法中获取了lock
...停顿等待...
Thread-1访问了chunkMethod方法
Thread-0在chunkMethod2方法中获取了lock
...停顿等待...
Thread-1在chunkMethod方法中获取了lock
//分析可知:
//现象:对比20行、22行和24行、25行可知,虽然是同一个lock对象,但其不一样代码块的访问是非阻塞的
//缘由:根源在于锁的释放和从新竞争,当Thread-0访问完chunkMethod方法后会先释放锁,这时Thread-1就有机会能获取到锁从而优先执行,依次类推到24行、25行时,Thread-0又从新获取到锁优先执行了
//注意:但有一点是必须的,对于同一个锁的同步代码块的访问必定是阻塞的
//补充:同步方法之因此会被所有阻塞,是由于synDemo对象一直被线程在内部把持住就没释放过
复制代码
因为三种使用方式的锁对象都不同,所以相互之间不会有任何影响但有两种状况除外:
public static void main(String[] args) {
SynchronizedDemo synDemo = new SynchronizedDemo();
Thread thread1 = new Thread(() -> synDemo.chunkMethod() );
Thread thread2 = new Thread(() -> synDemo.chunkMethod3());
Thread thread3 = new Thread(() -> staticMethod());
Thread thread4 = new Thread(() -> staticMethod2());
thread1.start();
thread2.start();
thread3.start();
thread4.start();
}
---------------------
//输出:
Thread-1访问了chunkMethod3方法
Thread-1在chunkMethod3方法中获取了this
Thread-2访问了静态同步方法staticMethod
Thread-0访问了chunkMethod方法
Thread-0在chunkMethod方法中获取了lock
Thread-3访问了静态同步方法staticMethod2
...停顿等待...
Thread-2结束访问静态同步方法staticMethod
Thread-3在staticMethod2方法中获取了SynchronizedDemo.class
//分析可知:
//现象:虽然是同一个lock对象,但其不一样代码块的访问是非阻塞的
//缘由:根源在于锁的释放和从新竞争,当Thread-0访问完chunkMethod方法后会先释放锁,这时Thread-1就有机会能获取到锁从而优先执行,,Thread-0又从新获取到锁优先执行了
复制代码
重入锁:当一个线程再次请求本身持有对象锁的临界资源时,这种状况属于重入锁,请求将会成功实现:一个线程获得一个对象锁后再次请求该对象锁,是容许的,每重入一次,monitor进入次数+1
public static void main(String[] args) {
SynchronizedDemo synDemo = new SynchronizedDemo();
Thread thread1 = new Thread(() -> {
synDemo.synMethod();
synDemo.synMethod2();
});
Thread thread2 = new Thread(() -> {
synDemo.synMethod2();
synDemo.synMethod();
});
thread1.start();
thread2.start();
}
---------------------
//输出:
Thread-0访问了同步方法synMethod
Thread-0结束访问同步方法synMethod
Thread-0访问了同步方法synMethod2
Thread-0结束访问同步方法synMethod2
Thread-1访问了同步方法synMethod2
Thread-1结束访问同步方法synMethod2
Thread-1访问了同步方法synMethod
Thread-1结束访问同步方法synMethod
//分析:在代码块中继续调用了当前实例对象的另一个同步方法,再次请求当前实例锁时,将被容许,进而执行方法体代码,这就是重入锁最直接的体现
复制代码
隐患:因为在JVM中具备String常量池缓存的功能,所以相同字面量是同一个锁!!!注意:严重不推荐将String做为锁对象,而应该改用其余非缓存对象提示:对字面量有疑问的话请先回顾一下String的基础
public static void main(String[] args) {
SynchronizedDemo synDemo = new SynchronizedDemo();
Thread thread1 = new Thread(() -> synDemo.stringMethod("sally"));
Thread thread2 = new Thread(() -> synDemo.stringMethod("sally"));
thread1.start();
thread2.start();
}
---------------------
//输出:
Thread-0
Thread-0
Thread-0
Thread-0
...死循环...
//分析:输出结果永远都是Thread-0的死循环,也就是说另外一个线程,即Thread-1线程根本不会运行
//缘由:同步块中的锁是同一个字面量
复制代码
隐患:当使用不可变类对象(finalClass)做为对象锁时,使用synchronized一样会有并发问题缘由:因为不可变特性,看成为锁但同步块内部仍然有计算操做,会生成一个新的锁对象注意:严重不推荐将final Class做为锁对象时仍对其有计算操做补充:虽然String也是final Class,但它的缘由倒是字面量常量池
public class SynchronizedDemo {
static Integer i = 0; //Integer是final Class
public static void main(String[] args) throws InterruptedException {
Runnable runnable = new Runnable() {
@Override
public void run() {
for (int j = 0;j<10000;j++){
synchronized (i){
i++;
}
}
}
};
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(i);
}
}
---------------------
//输出:
14134
//分析:跟预想中的20000不一致,当使用Integer做为对象锁时但还有计算操做就会出现并发问题
复制代码咱们经过反编译发现执行i++操做至关于执行了i = Integer.valueOf(i.intValue()+1)经过查看Integer的valueOf方法实现可知,其每次都new了一个新的Integer对象,锁变了有木有!!!
复制代码
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i); //每次都new一个新的锁有木有!!!
}
复制代码
死锁:当线程间须要相互等待对方已持有的锁时,就造成死锁,进而产生死循环
public static void main(String[] args) {
Object lock = new Object();
Object lock2 = new Object();
Thread thread1 = new Thread(() -> {
synchronized (lock){
System.out.println(Thread.currentThread().getName() + "获取到lock锁");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2){
System.out.println(Thread.currentThread().getName() + "获取到lock2锁");
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (lock2){
System.out.println(Thread.currentThread().getName() + "获取到lock2锁");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock){
System.out.println(Thread.currentThread().getName() + "获取到lock锁");
}
}
});
thread1.start();
thread2.start();
}
---------------------
//输出:
Thread-1获取到lock2锁
Thread-0获取到lock锁
.....
//分析:线程0得到lock锁,线程1得到lock2锁,但以后因为两个线程还要获取对方已持有的锁,但已持有的锁都不会被双方释放,线程"假死",没法往下执行,从而造成死循环,即死锁,以后一直在作无用的死循环,严重浪费系统资源
复制代码
咱们用 jstack 查看一下这个任务的各个线程运行状况,能够发现两个线程都被阻塞 BLOCKED
咱们很明显的发现,Java-level=deadlock,即死锁,两个线程相互等待对方的锁
为了能直观了解Synchronized的工做原理,咱们经过反编译SynchronizedDeme类的class文件的方式看看都发生了什么
public class SynchronizedDemo {
public static synchronized void staticMethod() throws InterruptedException {
System.out.println("静态同步方法开始");
Thread.sleep(1000);
System.out.println("静态同步方法结束");
}
public synchronized void method() throws InterruptedException {
System.out.println("实例同步方法开始");
Thread.sleep(1000);
System.out.println("实例同步方法结束");
}
public synchronized void method2() throws InterruptedException {
System.out.println("实例同步方法2开始");
Thread.sleep(3000);
System.out.println("实例同步方法2结束");
}
public static void main(String[] args) {
final SynchronizedDemo synDemo = new SynchronizedDemo();
Thread thread1 = new Thread(() -> {
try {
synDemo.method();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread thread2 = new Thread(() -> {
try {
synDemo.method2();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread1.start();
thread2.start();
}
}
复制代码
javac -encoding UTF-8 SynchronizedDemo.java
最终咱们将获得一个 .class 文件,即 SynchronizedDemo.class
javap -v SynchronizedDemo
复制代码经过反编译咱们会获得常量池、同步方法、同步代码块的不一样编译结果
常量池除了会包含基本类型和字符串及数组的常量值外,还包含以文本形式出现的符号引用:
类和接口的全限定名
字段的名称和描述符
方法和名称和描述符
复制代码
同步方法会包含一个ACC_SYNCHCRONIZED标记符
同步代码块会在代码中插入 monitorenter 和 monitorexist 指令
每一个对象都有一个监视器。当该监视器被占用时便是锁定状态(或者说获取监视器便是得到同步锁)。线程执行monitorenter指令时会尝试获取监视器的全部权,过程以下:
执行monitorexit指令将遵循如下步骤:
因为 wait/notify 等方法底层实现是基于监视器,所以只有在同步方法(块)中才能调用wait/notify等方法,不然会抛出 java.lang.IllegalMonitorStateException 的异常的缘由
区别于同步代码块的监视器实现,同步方法经过使用 ACC_SYNCHRONIZED 标记符隐示的实现原理是经过方法调用指令检查该方法在常量池中是否包含 ACC_SYNCHRONIZED 标记符,若是有,JVM 要求线程在调用以前请求锁
Monitor实际上是一种同步工具,也能够说是一种同步机制,它一般被描述为一个对象,主要特色是互斥和信号机制
在 Monitor Object 模式中,主要有四种类型的参与者:
同步方法的调用和串行化:
同步方法线程挂起:
监视条件通知:
同步方法线程恢复:
在JVM中,对象在内存中的布局分红三块区域:对象头、示例数据和对齐填充
对象头: 对象头主要存储 Mark Word(对象的hashCode、锁信息)、类型指针、数组长度(如果数组的话)等信息
示例数据:存放类的属性数据信息,包括父类的属性信息,若是是数组的实例部分还包括数组长度,这部份内存按4字节对齐
填充数据:因为JVM要求对象起始地址必须是8字节的整数倍,当不知足8字节时会自动填充(所以填充数据并非必须的,仅仅是为了字节对齐)
synchcronized的锁是存放在Java对象头中的
若是对象是数组类型,JVM用3个字宽(Word)存储对象头,不然是用2个子宽在32位虚拟机中,1字宽等于4个字节,即32bit;64位的话就是8个字节,即64bit
32位JVM的Mark Word的默认存储结构(无锁状态)
在运行期间,Mark Word里存储的数据会随着锁标志位的变化而变化(32位)
64位JVM的Mark Word的默认存储结构(对于32位无锁状态,有25bit没有使用)
公平锁就是得到锁的顺序按照先到先得的原则,从实现上说,要求当一个线程竞争某个对象锁时,只要这个锁的等待队列非空,就必须把这个线程阻塞并塞入队尾(插入队尾通常经过一个CAS操做保持插入过程当中没有锁释放)
相对的,非公平锁场景下,每一个线程都先要竞争锁,在竞争失败或当前已被加锁的前提下才会被塞入等待队列,在这种实现下,后到的线程有可能无需进入等待队列直接竞争到锁(随机性)
/** * StringBuffer是线程安全的字符串处理类 * 每次调用stringBuffer.append方法都须要加锁和解锁,若是虚拟机检测到有一系列连串的对同一个对象加锁和解锁操做,就会将其合并成一次范围更大的加锁和解锁操做,即在第一次append方法时进行加锁,最后一次append方法结束后进行解锁 */
StringBuffer stringBuffer = new StringBuffer();
public void append(){
stringBuffer.append("kira");
stringBuffer.append("sally");
stringBuffer.append("mengmeng");
}
复制代码
/** * 好比执行10000次字符串的拼接 */
public static void main(String[] args) {
SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
for (int i = 0 ; i < 10000 ; i++){
synchronizedDemo.append("kira","sally");
}
}
public void append(String str1,String str2){
//因为StringBuffer对象被封装在方法内部,不可能存在共享资源竞争的状况
//所以JVM会认为该加锁是无心义的,会在编译期就删除相关的加锁操做
//还有一点特别要注明:明知道不会有线程安全问题,代码阶段就应该使用StringBuilder
//不然在没有开启锁消除的状况下,StringBuffer不会被优化,性能可能只有StringBuilder的1/3
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append(str1).append(str2);
}/** 复制代码
从JDK1.6开始,锁一共有四种状态:无锁状态、偏向锁状态、轻量锁状态、重量锁状态
锁的状态会随着竞争状况逐渐升级,锁容许升级但不容许降级
不容许降级的目的是提升得到锁和释放锁的效率
后面会经过倒序的方式,即重量级锁->轻量级锁->偏向锁进行讲解,由于一般后者是前者的优化
锁的升级过程
线程1和线程2同时争夺锁,并致使锁膨胀成重量级锁
隐患:对于轻量级锁有个使用前提是"没有多线程竞争环境",一旦越过这个前提,除了互斥开销外,还会增长额外的CAS操做的开销,在多线程竞争环境下,轻量级锁甚至比重量级锁还要慢
偏向锁使用一种等到竞争出现才释放锁的机制,只有当其余线程竞争锁时,持有偏向锁的线程才会释放锁
偏向锁的撤销须要等待全局安全点(该时间点上没有字节码正在执行)
偏向锁的撤销须要遵循如下步骤: -
要么从新偏向于其余线程(即将偏向锁交给其余线程,至关于当前线程"被"释放了锁)
要么恢复到无锁或者标记锁对象不适合做为偏向锁(此时锁会被升级为轻量级锁)
最后唤醒暂停的线程,被阻塞在安全点的线程继续往下执行同步代码块