Synchronized

2020-03-31  本文已影响0人  凯玲之恋

1.Java的锁

1.1 锁的内存语义

1.2 锁的释放

1.3 锁的获取

锁的获取.jpg

1.4 锁的释放与获取

2.Synchronized的综述

3.Synchronized的使用

3.1 Synchronized的三种应用方式

QQ截图20170822164029.png

3.2 Synchronized的使用规则

/**
  * 先定义一个测试模板类
  *     这里补充一个知识点: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();
                }
            }
        }
    }
}

3.2.1 普通方法与同步方法调用互不关联

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
//分析:通过结果可知,普通方法和同步方法是非阻塞执行的

3.2.2 所有同步方法只能被一个线程访问

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执行完毕之后才能继续执行

3.2.3 同一个锁的同步代码块同一时刻只能被一个线程访问

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.chunkMethod2();
    });
    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
//分析可知:
//1.对比18行和19行可知,即使普通方法有同步代码块,但方法的访问是非阻塞的,任何线程都可以自由进入
//2.对比20行、22行以及25行和27行可知,对于同一个锁的同步代码块的访问一定是阻塞的

3.2.4 线程间同时访问同一个锁的多个同步代码的执行顺序不定

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对象一直被线程在内部把持住就没释放过,论把持住的重要性!

3.2.5 不同锁之间访问非阻塞

3.3 Synchronized的可重入性

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
//分析:对比16行和18行可知,在代码块中继续调用了当前实例对象的另外一个同步方法,再次请求当前实例锁时,将被允许,进而执行方法体代码,这就是重入锁最直接的体现

3.4 Synchronized与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线程根本不会运行
//原因:同步块中的锁是同一个字面量

3.5 Synchronized与不可变锁

public class SynchronizedDemo {
    static Integer i = 0;   //Integer持有final int value属性
    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作为对象锁时但还有计算操作就会出现并发问题
public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);  //每次都new一个新的锁有木有!!!
}

3.6 Synchronized与死锁

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锁,但之后由于两个线程还要获取对方已持有的锁,但已持有的锁都不会被双方释放,线程"假死",无法往下执行,从而形成死循环,即死锁,之后一直在做无用的死循环,严重浪费系统资源
死锁1.png

4.Synchronized实现原理

4.1 Synchronization

Synchronization in the Java Virtual Machine is implemented by monitor entry and exit, either explicitly (by use of the monitorenter and monitorexit instructions) or implicitly (by the method invocation and return instructions). 
For code written in the Java programming language, perhaps the most common form of synchronization is the synchronized method. A synchronized method is not normally implemented using monitorenter and monitorexit. Rather, it is simply distinguished in the run-time constant pool by the ACC_SYNCHRONIZED flag, which is checked by the method invocation instructions (§2.11.10).

4.2 反编译

4.2.1 预准备

package concurrent;
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();
    }
}

4.2.1 生成.class文件

javac SynchronizedDemo.java 
javac -encoding UTF-8 SynchronizedDemo.java

4.2.2 javap反编译

javap -v SynchronizedDemo

通过反编译我们会得到常量池、同步方法、同步代码块的不同编译结果,之后我们将基于这三个进行介绍

4.3 同步代码块同步原理

4.3.1 monitor监视器

4.3.2 monitorenter指令

Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows: 
• If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor. 
• If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count. 
• If another thread already owns the monitor associated with objectref, the thread blocks until the monitor's entry count is zero, then tries again to gain ownership.

4.3.3 monitorexit指令

The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref. 
The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.

4.3.4 实现原理

4.3.4 补充

4.4 同步方法同步原理

5.进阶原理

5.1 Monitor Obejct模式

5.1.1 Monitor Obejct模式综述

5.1.2 Monitor Obejct模式结构

在 Monitor Object 模式中,主要有四种类型的参与者:

QQ截图20170823110537.png

5.1.3 Monitor Obejct模式协作过程

5.2 对象头

5.2.1 JVM内存中的对象

5.2.2 对象头综述

Java对象头的长度.png

5.2.3 Mark Word的存储结构

32位JVM的Mark Word的默认存储结构(无锁状态)

32位MarkWord存储结构.png
在运行期间,Mark Word里存储的数据会随着锁标志位的变化而变化(32位)
QQ截图20170823114120.png
64位JVM的Mark Word的默认存储结构(对于32位无锁状态,有25bit没有使用)
64位MarkWord存储结构.png

5.3 Monitor Record

5.3.1 Monitor Record综述

5.3.2 Monitor Record结构

QQ截图20170823111424.png

5.3.3 Monitor Record工作机理

image_1bo6hdr131nraf2f1gb91odk1bnq1d.png

6.锁优化

6.1 自旋锁

6.2 自适应自旋锁

6.3 阻塞锁

6.3.1 阻塞锁

6.3.2 公平锁

公平锁就是获得锁的顺序按照先到先得的原则,从实现上说,要求当一个线程竞争某个对象锁时,只要这个锁的等待队列非空,就必须把这个线程阻塞并塞入队尾(插入队尾一般通过一个CAS操作保持插入过程中没有锁释放)

6.3.3 非公平锁

非公平锁场景下,每个线程都先要竞争锁,在竞争失败或当前已被加锁的前提下才会被塞入等待队列,在这种实现下,后到的线程有可能无需进入等待队列直接竞争到锁(随机性)

6.4 锁粗化

/**
  * StringBuffer是线程安全的字符串处理类
  * 每次调用stringBuffer.append方法都需要加锁和解锁,如果虚拟机检测到有一系列连串的对同一个对象加锁和解锁操作,就会将其合并成一次范围更大的加锁和解锁操作,即在第一次append方法时进行加锁,最后一次append方法结束后进行解锁
  */
StringBuffer stringBuffer = new StringBuffer();
public void append(){
    stringBuffer.append("kira");
    stringBuffer.append("sally");
    stringBuffer.append("mengmeng");
}

6.5 锁消除

/**
  * 比如执行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);
}

6.6 锁的升级

6.7 重量级锁

6.8 轻量级锁

6.8.1 轻量级锁综述

QQ截图20170823114120.png

6.8.2 轻量级锁流程图

6.8.3 轻量级锁加锁

6.8.4 轻量级锁解锁

6.8.5 轻量级锁注意事项

6.9 偏向锁

6.9.1 偏向锁综述

6.9.2 偏向锁流程图

6.9.3 偏向锁初始化

6.9.4 偏向锁撤销锁

6.9.5 偏向锁关闭锁

6.9.6 偏向锁注意事项

6.10 偏向锁 vs 轻量级锁 vs 重量级锁

QQ截图20170824161959.png

参考

并发番@Synchronized一文通(1.8版)
JVM源码分析之synchronized实现
深入JVM锁机制1-synchronized
浅谈Mutex (Lock)
聊聊并发(二)——Java SE1.6 中的 Synchronized

上一篇下一篇

猜你喜欢

热点阅读