Java Synchronized

539 阅读10分钟

这里有个概念一定要清楚对象和class对象(class type)。

//在jvm中普通对象是可以有多个的,class对象只有一个比较特殊。
public class TestClass {
    //TestClass的普通对象可以有多个。mTestClass1,2,3都是TestClass的对象
    TestClass mTestClass1 = new TestClass();
    TestClass mTestClass2 = new TestClass();
    TestClass mTestClass3 = new TestClass();
    //TestClass的class对象只有一个就是TestClass.class
    Class test = TestClass.class;
}

1:synchronized是什么?

它是 Java 53个关键字中的一员,一个并发编程时经常用到的关键字。

2:synchronized它有什么作用?

保证在同一时刻最多只有一个线程执行它所修饰的代码。

这句话里三个关键词“同一时刻”“最多只有一个”和“它所修饰的”。其中第三个关键词说明了它的作用域是{}包起来的代码。

3:synchronized的用法

synchronized我们在使用时一般分为两种类型,一种锁对象(实例),一种锁类(全局唯一),其中前者又包含方法锁和代码块锁,后者包含静态方法锁和class锁。

对象可以有多个,class和静态方法是全局唯一的。

锁对象聚焦的是对象(实例)。锁类聚焦的是类(全局唯一)。

3.1:锁对象

锁对象分为方法锁和代码块锁。锁对象的作用域是对象范围的,若使用的不是同一个对象则synchnorized无效。

方法锁

方法锁:synchronized修饰普通方法(非static方法),锁对象默认为this 。

例:

public synchronized void name(){
    ...
}
//方法锁,同时访问两个方法锁,其结果是不确定的。
// 方法锁默认锁的是this因此method1和method2是同一个锁,
// 当有个线程获得method1其他线程是不能访问method1和method2的。反之同理。
public class MethodBlockSync implements Runnable {

    static MethodBlockSync mMethodBlockSync = new MethodBlockSync();

    public static void main(String[] args) {
        Thread thread1 = new Thread(mMethodBlockSync);
        Thread thread2 = new Thread(mMethodBlockSync);
        thread1.start();
        thread2.start();
    }

    @Override
    public void run() {

        method1();
        method2();
    }

    public synchronized void method1(){
        System.out.println("锁对象#方法锁1,线程名:" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("线程"+Thread.currentThread().getName() + "结束");
    }

    public synchronized void method2(){
        System.out.println("锁对象#方法锁2,线程名:" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("线程"+Thread.currentThread().getName() + "结束");
    }
}

//锁对象#方法锁1,线程名:Thread-0
    //  3秒之后
//线程Thread-0结束
//锁对象#方法锁1,线程名:Thread-1
    //  3秒之后
//线程Thread-1结束
//锁对象#方法锁2,线程名:Thread-1
    //  3秒之后
//线程Thread-1结束
//锁对象#方法锁2,线程名:Thread-0
    //  3秒之后
//线程Thread-0结束

代码块锁

代码块锁:

synchronized(this){
    ...
}
//两个线程,使用同一个对象,访问同一个代码块锁。
public class CodeBlockSync implements Runnable {

    static CodeBlockSync mCodeBlockSync = new CodeBlockSync();

    public static void main(String[] args) {
        //两个线程,使用同一个对象。
        Thread thread1 = new Thread(mCodeBlockSync);
        Thread thread2 = new Thread(mCodeBlockSync);
        thread1.start();
        thread2.start();
    }
    
    @Override
    public void run() {
        synchronized (this) {
            System.out.println("锁对象#代码块,线程名:" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("线程"+Thread.currentThread().getName() + "结束");
        }
    }
}

//锁对象#代码块,线程名:Thread-0
//   3秒之后
//线程Thread-0结束
//锁对象#代码块,线程名:Thread-1
//   3秒之后
//线程Thread-1结束
//自己定义锁对象,同时访问两个相同代码块锁
public class CodeBlockSync2 implements Runnable {
    static CodeBlockSync2 mCodeBlockSync1 = new CodeBlockSync2();
    //锁对象
    Object mLock1 = new Object();

    public static void main(String[] args) {
        Thread thread1 = new Thread(mCodeBlockSync1);
        Thread thread2 = new Thread(mCodeBlockSync1);
        thread1.start();
        thread2.start();
    }
    @Override
    public void run() {
        synchronized (mLock1) {
            System.out.println("锁对象#代码块1,线程名:" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("线程"+Thread.currentThread().getName() + "结束");
        }
        synchronized (mLock1) {
            System.out.println("锁对象#代码块2,线程名:" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("线程"+Thread.currentThread().getName() + "结束");
        }
    }
}
//锁对象#代码块1,线程名:Thread-0
//    3秒之后
//线程Thread-0结束
//锁对象#代码块2,线程名:Thread-0
//   3秒之后
//线程Thread-0结束
//锁对象#代码块1,线程名:Thread-1
//   3秒之后
//线程Thread-1结束
//锁对象#代码块2,线程名:Thread-1
//   3秒之后
//线程Thread-1结束
//自定义两个锁对象,同时访问两个不相同的代码块锁
public class CodeBlockSync2 implements Runnable {

    static CodeBlockSync2 mCodeBlockSync1 = new CodeBlockSync2();

    //锁对象
    Object mLock1 = new Object();
    Object mLock2 = new Object();

    public static void main(String[] args) {
        Thread thread1 = new Thread(mCodeBlockSync1);
        Thread thread2 = new Thread(mCodeBlockSync1);
        thread1.start();
        thread2.start();
    }
    @Override
    public void run() {
        synchronized (mLock1) {
            System.out.println("锁对象#代码块1,线程名:" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("线程"+Thread.currentThread().getName() + "结束");
        }
        synchronized (mLock2) {
            System.out.println("锁对象#代码块2,线程名:" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("线程"+Thread.currentThread().getName() + "结束");
        }
    }
}
//锁对象#代码块1,线程名:Thread-0
//    3秒之后
//线程Thread-0结束
//锁对象#代码块2,线程名:Thread-0
//锁对象#代码块1,线程名:Thread-1
//    3秒之后
//线程Thread-0结束
//线程Thread-1结束
//锁对象#代码块2,线程名:Thread-1
//    3秒之后
//线程Thread-1结束
//自己定义两个锁对象,同时访问两个不同对象的代码块锁
public class CodeBlockSync3 implements Runnable {

    static CodeBlockSync3 mCodeBlockSync1 = new CodeBlockSync3();
    static CodeBlockSync3 mCodeBlockSync2 = new CodeBlockSync3();
    //锁对象
    Object mLock1 = new Object();
    Object mLock2 = new Object();

    public static void main(String[] args) {
        Thread thread1 = new Thread(mCodeBlockSync1);
        Thread thread2 = new Thread(mCodeBlockSync2);
        thread1.start();
        thread2.start();
    }
    @Override
    public void run() {
        synchronized (mLock1) {
            System.out.println("锁对象#代码块1,线程名:" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("线程"+Thread.currentThread().getName() + "结束");
        }
        synchronized (mLock2) {
            System.out.println("锁对象#代码块2,线程名:" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("线程"+Thread.currentThread().getName() + "结束");
        }
    }
}
//锁对象#代码块1,线程名:Thread-0
//锁对象#代码块1,线程名:Thread-1
//   3秒之后
//线程Thread-1结束
//线程Thread-0结束
//锁对象#代码块2,线程名:Thread-0
//锁对象#代码块2,线程名:Thread-1
//   3秒之后
//线程Thread-1结束
//线程Thread-0结束

通过上述实例可以看出来,锁对象方法是针对"对象(实例)"的。

3.2:锁类

锁类有两种形式,第一种是静态方法锁,第二种是锁class。

锁类的作用域是全局,当多个线程使用不同的对象访问锁类修饰的代码时同一时刻只能有一个线程得到这个锁。

3.2.1:静态方法锁

synchronized配合static一起修饰方法。例:

public synchronized static void method(){
    ...
}
//全局同步,锁对象作用对象层面,锁类作用全局
public class ClassStaticMethodSync implements Runnable {
    static ClassStaticMethodSync mStaticSync1 = new ClassStaticMethodSync();
    static ClassStaticMethodSync mStaticSync2 = new ClassStaticMethodSync();
    public static void main(String[] args) {
        Thread thread1 = new Thread(mStaticSync1);
        Thread thread2 = new Thread(mStaticSync2);
        thread1.start();
        thread2.start();
    }
    public synchronized static void method(){
        System.out.println("锁类#锁静态方法1,线程名:" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("线程"+Thread.currentThread().getName() + "结束");
    }
    @Override
    public void run() {
        method();
    }
}
//锁类#锁静态方法1,线程名:Thread-0
    //3秒之后
//线程Thread-0结束
//锁类#锁静态方法1,线程名:Thread-1
    //3秒之后
//线程Thread-1结束

3.2.2:锁类

形式:synchronized(*.class){},例:

synchronized (ClassSync.class) {
        System.out.println("锁类#锁class 1,线程名:" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("线程" + Thread.currentThread().getName()+ "结束");
}
public class ClassSync implements Runnable {

    static ClassSync mClassSync1 = new ClassSync();
    static ClassSync mClassSync2 = new ClassSync();

    public static void main(String[] args) {
        Thread thread1 = new Thread(mClassSync1);
        Thread thread2 = new Thread(mClassSync2);
        thread1.start();
        thread2.start();
    }

    public void method(){
        synchronized (ClassSync.class) {
            System.out.println("锁类#锁class 1,线程名:" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("线程" + Thread.currentThread().getName() + "结束");
        }
    }
    @Override
    public void run() {
        method();
    }
}

//锁类#锁class 1,线程名:Thread-0
        //3秒
//线程Thread-0结束
//锁类#锁class 1,线程名:Thread-1
        //3秒
//线程Thread-1结束

4:synchronized的特性

4.1:可重入

可重入指的是同一线程的外层函数获得锁之后,内层函数可以直接再次的到锁,synchronized的可重入作用域为线程。

//可重入
// 进入同一个同步方法。
public class RecursionSync implements Runnable{
    int i;
    static RecursionSync mRecursionSync = new RecursionSync();
    public static void main(String[] args) {
        Thread thread1 = new Thread(mRecursionSync);
        thread1.start();
    }
    @Override
    public void run() {
        method();
    }
    public synchronized void method(){
        System.out.println("可重入,线程名:" + Thread.currentThread().getName() + i);
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("线程"+Thread.currentThread().getName() + "结束");
        if (i == 0){
            i++;
            method();
        }
    }
}
//可重入
// 进入不同的同步方法。
public class RecursionSync2 implements Runnable{

    static RecursionSync2 mRecursionSync = new RecursionSync2();

    public static void main(String[] args) {
        Thread thread1 = new Thread(mRecursionSync);

        thread1.start();
    }

    @Override
    public void run() {
        method1();
    }
    public synchronized void method1(){
        System.out.println("可重入#方法1,线程名:" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("线程"+Thread.currentThread().getName() + "结束");
        method2();
    }
    public synchronized void method2(){
        System.out.println("可重入#方法2线程名:" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("线程"+Thread.currentThread().getName() + "结束");
    }
}

4.2:不可中断

一旦锁被某个线程获得了,则其它线程只能选择等待或者阻塞,直到这个锁被释放。如果这个锁一直不被释放,则其它线程只能永远的等待下去。

5:原理

synchronized 是基于 monitor机制实现的,monitor机制需要几个元素来配合,分别是:

临界区
monitor 对象
条件变量

monitor是java为了让我们开发人员更容易地编写出正确的并发程序而设计的。monitor属于编程语言的范畴,操作系统本身并不支持monitor。开发人员不用实现monitor机制,由编译器实现。monitor保证同一时刻,只有一个线程能进入monitor中的临界区(synchronized修饰的方法和代码块),这使得monitor能够达到互斥的效果。通过以上小节我们发现使用Synchronized的时候必须提供一个对象。例如:synchronized(this),synchronized如果修饰的是对象方法,那么其关联的对象实际上是this,如果修饰的是类方法,那么其关联的对象是this.class。总之,synchronzied需要关联一个对象。这个对象就是monitor对象。

5.1:加锁和释放锁

在java对象模型中,所有的对象头部都有锁状态标记,Monitor对象就在这个头部中。当进入一个 synchronized修饰的方法或者代码块时,会先获取与synchronized关键字绑定在一起的monitor对象,这个对象限定了其它线程无法进入与这个monitor对象相关的其它synchronized代码区域。java中使用moniterEnter代表加锁,moniterExit代表释放锁。

//syncMethod方法和lockMethod方法是等价的,
//当我们进入到sync修饰的方法时jvm会自动帮我们加锁,结束后解锁
public synchronized void syncMethod(){
    System.out.println("我是sync琐");
}
ReentrantLock lock = new ReentrantLock();
public void lockMethod(){
    lock.lock();
    try {
        System.out.println("我是lock琐");
    }finally {
        lock.unlock();
    }
}
//反汇编修饰代码块
public void syncMethod(){
    synchronized (this){
        System.out.println("我是sync琐");
    }
}
/*反汇编结果,monitorexit数量多于enter数量,jvm在异常时会自动释放锁(lock不会自动释放锁)
public void syncMethod();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter   加锁,monitor计数器加1
         4: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         7: ldc           #3                  // String 我是sync琐
         9: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        12: aload_1
        13: monitorexit   正常释放锁 monitor计数器减1
        14: goto          22
        17: astore_2
        18: aload_1
        19: monitorexit  异常时jvm自动释放锁  monitor计数器减1
        20: aload_2
        21: athrow
        22: return
* */

5.2:可重入

monitor对象有一个属性专门负责记录加锁次数的,线程第一次得到锁的时候,这个属性变为1,每重入一次这个属性加1。当这个属性的值为0时,表示没有线程获取到锁,不为0时表示此锁已被释放。

5.3:可见性

在多核CPU下,java为了提高效率,线程在拿值时,直接和CPU缓存进行交互,而不是主内存。因为CPU缓存执行速度比内存高。因此线程读操作永远都是取的CPU缓存中的值。

这时候就会产生一个问题,CPU缓存中的数据和内存中的数据并不是时刻同步的,导致不能及时加锁。这时候java中有个机制,让主内存主动通知CPU缓存,把当前被加锁的数据置为无效,并且需要等待锁释放才能重新获取。当释放锁时,JMM会把该线程对应的本地内存中的数据刷新到主内存中(并不是不释放锁就不刷新到主内存, 只是释放锁时把未刷新到主内存中的数据全部刷新到主内存).

6:缺陷

获取锁时不能设定超时,不能中断一个正在获得锁的线程,只能一条路走到黑。