Java并发之synchronized深入

2019-06-15 00:00:00 synchronized java 并发

一句话总结synchronized

JVM会自动通过使用monitor来加锁和解锁,保证了同时只有一个线程可以执行指定代码,从而保证了线程安全,同时具有可重入和不可中断的性质。        

一.synchronized的作用

使用synchronized修饰方法或者代码块时,它能够保证在同一时刻最多只有一个线程执行该段代码,以达到保证并发安全的效果。

二.synchronized的地位

1.Synchronized是Java的关键字,被Java语言原生支持

2.是最基本的互斥同步手段

3.是并发编程中的元老级角色,是并发编程的主要内容

三.synchronized的性质

synchronized的具有可重入性和不可中断性

1.可重入性(synchronized区别于其他锁的一个很重要的性质)

什么是可重入:指的是同一线程的外层函数获得锁之后,内层函数可以直接再次获取该锁。也叫做递归锁。Java中两大递归锁:Synchronized和ReentrantLock。

锁的不可重入性:线程拿到一把锁了,如果想再次使用这把锁,必须先将锁释放,与其他线程再次进行竞争。

锁的可重入性:如果线程已经拿到了锁,试图去请求这个已经获得到的锁,而无需提前释放,直接就可以使用手里的这把锁,这就叫做可重入性

synchronized是可重入锁

可重入锁的好处:

1 避免死锁 

2 提升封装性

粒度:

可重入的特性是线程级别的,不是调用级别的。

情况1:证明同一个方法是可重入的

实例代码:

public class SynchronizedRecursion {
    int a = 0;

    public static void main(String[] args) {
        SynchronizedRecursion sr = new SynchronizedRecursion();
        sr.method1();
    }

    private synchronized void method1(){
        System.out.println("synchronized修饰的方法,a = " + a);
        if(a==0){
            a++;
            method1();
        }
    }
}

情况2:证明可重入不要求是同一个方法

实例代码:

public class SynchronizedOtherMethod {
    public synchronized void method1(){
        System.out.println("我是method1");
        method2();
    }

    public synchronized void method2(){
        System.out.println("我是method2");
    }

    public static void main(String[] args) {
        SynchronizedOtherMethod som = new SynchronizedOtherMethod();
        som.method1();
    }
}

情况3:证明可重入不要求是同一个类中的

实例代码:

public class SynchronizedSuperClass {

    public synchronized void doSomething(){
        System.out.println("我是父类方法");
    }
}

class TestClass extends SynchronizedSuperClass{
    public synchronized void doSomething(){
        System.out.println("我是子类方法");
        super.doSomething();
    }

    public static void main(String[] args) {
        TestClass tc = new TestClass();
        tc.doSomething();
    }
}

2.不可中断(相比于其他有的锁可以中断,这个性质是synchronized的一个劣势所在)

一旦这个锁已经被别的线程获得了,如果当前线程还想获得,只能选择等待或者阻塞,直到别的线程释放这个锁。如果别的线程 永远不释放锁,那么线程只能永远地等下去。

相比之下,Lock类,可以拥有中断的能力。

第一点,如果我觉得我等的时候太长了,有权中断现在已经获取到锁的线程的执行; 第二点,如果我觉得我等待的时间太长了不想再等了,也可以退出。

Lock比synchronized灵活很多,但是编码易出错。

四.synchronized的原理

1.加解锁原理:现象,时机,深入JVM看字节码

现象:每一个类的实例对应一把锁,而每一个synchronized方法都必须先获得调用该方法的类的实例的锁方能执行,否则线程会阻塞。而方法一旦执行,它就独占了这把锁,直到该方法返回或者是抛出异常,才将锁释放。一旦锁释放之后,之前被阻塞的线程才能获得这把锁,从被阻塞的状态重新进入到可执行的状态。当一个对象中有synchronized修饰的方法或者代码块的时候,要想执行这段代码,就必须先获得这个对象锁,如果此对象的对象锁已经被其他调用者所占用,就必须等待它被释放。所有的Java对象都含有一个互斥锁,这个锁由JVM自动去获取和释放,我们只需要指定这个对象就行了,至于锁的释放和获取不需要我们操心。

获取和释放锁的时机:内置锁

我们知道每一个Java对象都可以用作一个同步的锁,这个锁叫做内部锁,或者叫做监视器锁–monitor lock。线程在进入到同步代码块之前,会自动获得这个锁,并且在退出同步代码块的时候会自动的释放这个锁,无论是正常途径退出还是抛出异常退出。获得这个内置锁的唯一途径就是进入这个锁保护的同步代码块或者同步方法中。

实例代码:

public class SynchronizedToLock {

    Lock lock = new ReentrantLock();

    public synchronized void method1(){
        System.out.println("我是synchronized锁");
    }

    public void method2(){
        lock.lock();  //加锁
        try{
            System.out.println("我是lock锁");
        }finally {
            lock.unlock();  //释放锁
        }
    }

    public static void main(String[] args) {
        SynchronizedToLock stl = new SynchronizedToLock();
        stl.method1();
        stl.method2();
    }
}

method1()和method2()方法等价

深入JVM看字节码:

实例代码:

public class Decompilation {

    private Object object = new Object();

    //同步代码块
    public void insert(Thread thread){
        synchronized (object){

        }
    }
}

1、将上面的Java代码编译为 .class文件,命令窗口执行命令:javac xxx.java  进行编译。

2、通过反编译查看字节码,执行命令:javap -verbose xxx.class 进行反编译

3、synchronized如何实现的,有个加锁monitorenter和解锁monitorexit

读到该指令,会让monitor计数器+1或-1

具体如图:

《Java并发之synchronized深入》

《Java并发之synchronized深入》

线程既可以在方法完成之后退出,也可以在抛出异常后退出,因此monitorexit数量多于monitorenter。

monitorenter和monitorexit指令

monitorenter:线程每次进入时,计数器+1。如果重入,继续加

monitorexit:线程退出时,计数器-1.变为0时候,其他线程可以获取锁。

2.可重入原理:加锁次数计数器

1、JVM负责跟踪对象被加锁的次数; 
2、有个monitor计数器,线程第一次给对象加锁的时候,计数变为1.每当这个相同的线程在此对象上再次获得锁时,计数会递增。
3、任务结束离开,则会执行monitorexit,计数递减,直到完全释放

3.可见性原理:Java内存模型

《Java并发之synchronized深入》

五.synchronized的缺陷

1.效率低:

·        锁的释放情况少

·        试图获得锁时不能设定超时

·        不能中断一个正在试图获得锁的线程

2.不够灵活(读写锁更灵活:读操作的时候不会加锁,写操作的时候才会加锁):

·        加锁和释放的时机单一

·        每个锁仅有单一的条件(某个对象),可能是不够的

3.无法知道是否成功获取到锁,而使用Lock则不同。

Lock可以,如果尝试成功了做一些逻辑判断,如果没有成功做另外一些逻辑判断.

Lock类:

lock.lock();lock.unlock();通过这两个方法,可以手动加锁和解锁。

lock.tryLock();lock.tryLock(10, TimeUnit.MINUTES);可以判断是否加锁,以及设置超时时间,返回类型为boolean

六.synchronized的用法

   1、对象锁

        a、方法锁(默认锁对象为this当前实例对象):synchronized修饰普通方法,锁对象默认为this

实例代码:

public synchronized void run(){
     for(int j = 0; j < 100000; j++){
         i++;
     }
 }

        b、同步代码块锁: 自己手动指定锁对象

实例代码:

public synchronized void run(){
    synchronized (this){
        for(int j = 0; j < 100000; j++){
            i++;
        }
    }
}

    2、类锁

       Java类可能有很多个对象,但只要一个class对象。类锁只不过是Class对象的锁而已。类锁只能在同一时刻被一个对象拥有。

        a、使用关键字synchronized修饰静态static的方法

实例代码:

public  void run(){
   method();
}

public static synchronized void method(){
    for(int j = 0; j < 100000; j++){
        i++;
    }
}

        b、使用关键字synchronized指定锁为Class对象: synchronized(*.class)

实例代码:

public synchronized void run(){
    synchronized (DisappearRequest2.class){
        for(int j = 0; j < 100000; j++){
            i++;
        }
    }
}
synchronized使用的注意点:   

锁的信息是保存在对象头中的、作用域不易过大,影响性能、避免死锁

七.不使用并发情况会带来的后果以及解决办法

问题:两个线程同时a++,最后结果会比预计的少

原因

count++,它看上去只是一个操作,实际上包含了三个动作:

1.读取count

2.将count加一

3.将count的值写入到内存中

这三个操作,如果不按照原子去执行,就会带来并发问题

解决之前:

public class DisappearRequest1 implements Runnable{
    static DisappearRequest1 instance = new DisappearRequest1();

    static int i = 0;

    @Override
    public void run(){
        for(int j = 0; j < 100000; j++){
            i++;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);
        t1.start();
        t2.start();

        t1.join();  //等待线程结束
        t2.join();

        System.out.println("i=" + i);
    }
}

解决:

public class DisappearRequest2 implements Runnable{
    static DisappearRequest2 instance = new DisappearRequest2();

    static int i = 0;

    //方法1:直接把synchronized加到方法上
   /* public synchronized void run(){
        for(int j = 0; j < 100000; j++){
            i++;
        }
    }*/

    //方法2:对象锁的同步代码块
    /*public synchronized void run(){
        synchronized (this){
            for(int j = 0; j < 100000; j++){
                i++;
            }
        }
    }*/

    //第三种:类锁:synchronized(*.class)
    public synchronized void run(){
        synchronized (DisappearRequest2.class){
            for(int j = 0; j < 100000; j++){
                i++;
            }
        }
    }

    //第四种:类锁
    /*public  void run(){
       method();
    }

    public static synchronized void method(){
        for(int j = 0; j < 100000; j++){
            i++;
        }
    }*/

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);
        t1.start();
        t2.start();

        t1.join();  //等待线程结束
        t2.join();

        System.out.println("i=" + i);
    }
}


八.关于多线程访问同步方法的七种情况的总结

1.两个线程同时访问一个对象的同步方法(对象锁):串行执行

这种情况就是对象锁的方法锁情况。会相互等待,只能有一个线程持有锁。

2.两个线程访问的是两个对象的同步方法:两个锁对象互不干扰,并行执行

不会加锁,因为访问的是不同的实例

3.两个线程访问的是synchronized的静态方法:两个锁对象是同一个锁,串行执行

这种情况就是类锁的静态方法锁。

4.同时访问同步方法与非同步方法:同步(并行)执行

synchronized关键字只作用于当前方法,不会影响其他未加关键字的方法的并发行为。因此非同步方法不受到影响,还是会并发执行。

5.访问同一个对象的不同的普通同步方法(对象锁):串行执行

synchronized关键字虽然没有指定所要的那个锁对象,但是本质上是指定了this这个对象作为它的锁。所以对于同一个实例来讲,两个方法拿到的是同一把锁,因此会出现串行的情况。

6.同时访问静态synchronized和非静态synchronized方法:同步(并行)执行

前者为类锁,锁为Class类;后者为对象锁,锁为this对象。因此两者的锁不同,会并行执行。

7.方法抛异常后,会释放锁

关于synchronized的相关实例代码见github: 

https://github.com/JavaCodeMood/JavaSynchronizedDemo.git

相关文章