欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

多角度简单解析synchronize关键字

程序员文章站 2022-03-06 16:17:15
这里写自定义目录标题Synchronized简介作用不使用synchronize的后果两种用法对象锁类锁多线程访问同步方法的7种情况Synchronized的性质Synchronized原理加锁和释放锁的原理可重入原理保证可见性原理Synchronized的缺陷常见面试问题Synchronized简介作用保证在同一时刻最多只有一个线程执行该段代码,以达到并发安全的效果。其主要通过拿到锁来保证执行代码块的线程唯一性,只有拿到锁的线程才能执行synchronize中的代码。不使用synchronize的...

Synchronized简介

作用

保证在同一时刻最多只有一个线程执行该段代码,以达到并发安全的效果。其主要通过拿到锁来保证执行代码块的线程唯一性,只有拿到锁的线程才能执行synchronize中的代码。

不使用synchronize的后果

我们可以启动两个线程对同一个变量进行自增的操作:

public class HowToUseSyn implements Runnable{
    static HowToUseSyn howToUseSyn = new HowToUseSyn();
    // 计数器
    static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread threadOne = new Thread(howToUseSyn);
        Thread threadTwo = new Thread(howToUseSyn);
        threadOne.start();
        threadTwo.start();
        // 保证线程1,2都能执行完在打印结果
        Thread.sleep(2000);
        System.out.println(count);
    }
    @Override
    public void run() {
        for (int i = 0; i < 100000; i++) {
            add();
        }
    }

    // 提供自增的方法
    public static void add(){
        count++;
    }
}

多角度简单解析synchronize关键字

结果我们可以看到,所得的值并不是我们期望的加了20W次(因为每个线程都加了10W次)。

原因:conut++看上去是一个操作,其实其包含了三个操作:

1、首先获取conut变量的值

2、然后对count变量的值进行+1的操作

3、最后在将count的值写到内存中。

这就导致了,当线程一获取到了conut值并且已经进行了+1的操作,但是并未写入到内存中,此时线程二进来了并读取了未修改的conut的值。所以在这样的情况下两个线程对变量进行了相同数值的操作。
多角度简单解析synchronize关键字

两种用法

对象锁

​ 1.方法锁(默认锁对象为this当前实例对象)

​ synchronize修饰普通方法,锁对象默认为this

public class DuiXiangSuoTwo implements Runnable{
    static DuiXiangSuoTwo duiXiangSuo = new DuiXiangSuoTwo();
    @Override
    public void run() {
        method();
    }
    // 加了synchronize的同步方法
    public synchronized void method(){
        System.out.println("线程"+Thread.currentThread().getName()+"进入代码块");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("线程"+Thread.currentThread().getName()+"结束任务");
    }

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

多角度简单解析synchronize关键字

​ 2.同步代码块锁(自己指定锁对象)

​ 手动指定锁对象

​ 锁this对象代码实例:

public class DuiXiangSuo implements Runnable{

    static  DuiXiangSuo duiXiangSuo = new DuiXiangSuo();

    @Override
    public void run() {
        synchronized (this){
            System.out.println("线程"+Thread.currentThread().getName()+"进入代码块");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("线程"+Thread.currentThread().getName()+"结束任务");
        }
    }

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

多角度简单解析synchronize关键字

从输出结果我们可以看到,线程0和线程1都是串行的进行代码块并执行其中的内容。这就很好的保证了在synchronize代码块中最多只有一个线程执行其中的代码。

不同的线程拿不同的锁去执行不同的任务:

当我们需要多个线程去协作不同的任务的时候,我们就不能锁住this对象,而是自己创建一个对象锁,让synchronize去锁我们创建的对象。

代码实例(使用this锁):

public class DuiXiangSuo implements Runnable{
    static  DuiXiangSuo duiXiangSuo = new DuiXiangSuo();
    // 锁1
    Object lock1 = new Object();
    // 锁2
    Object lock2 = new Object();
    @Override
    public void run() {
        synchronized (this){
            System.out.println("线程"+Thread.currentThread().getName()+"拿到锁1");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("线程"+Thread.currentThread().getName()+"结束任务锁1");
        }

        synchronized (this){
            System.out.println("线程"+Thread.currentThread().getName()+"拿到锁2");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("线程"+Thread.currentThread().getName()+"结束任务锁2");
        }
    }

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

多角度简单解析synchronize关键字

我们看结果可以知道,执行了两个任务锁this的话,会让另一个任务暂时的停止。不能让两个线程同时开始任务,而再看,我们更换了不同对象锁以后:

public class DuiXiangSuo implements Runnable{
    static  DuiXiangSuo duiXiangSuo = new DuiXiangSuo();
    // 锁1
    Object lock1 = new Object();
    // 锁2
    Object lock2 = new Object();
    @Override
    public void run() {
        synchronized (lock1){
            System.out.println("线程"+Thread.currentThread().getName()+"拿到锁1");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("线程"+Thread.currentThread().getName()+"结束任务锁1");
        }

        synchronized (lock2){
            System.out.println("线程"+Thread.currentThread().getName()+"拿到锁2");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("线程"+Thread.currentThread().getName()+"结束任务锁2");
        }
    }

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

多角度简单解析synchronize关键字

从结果我们可以看到,两个线程可以同时拿不同的锁去执行不同的同步代码块,这就大大加快的线程之间协作的效率。

类锁

Java类有可能会有多个对象,但是Class类对象只有一个;所以所谓类锁,不过是Class对象的锁而已,但是Class对象只有一个,所以就能保证同一时刻只有一个线程获取该Class类对象的锁。

​ 1、synchronize修饰静态的方法

​ 我们来看看不同对象不加synchronize的情况:

public class ClassSuoStaticMethod implements Runnable{
    static ClassSuoStaticMethod classSuoStaticMethod = new ClassSuoStaticMethod();
    static ClassSuoStaticMethod classSuoStaticMethod2 = new ClassSuoStaticMethod();

    public static void main(String[] args) {
        System.out.println("classSuoStaticMethod=>>"+classSuoStaticMethod);
        System.out.println("classSuoStaticMethod2=>>"+classSuoStaticMethod2);
        Thread thread1 = new Thread(classSuoStaticMethod);
        Thread thread2 = new Thread(classSuoStaticMethod2);
        thread1.start();
        thread2.start();
        while(thread1.isAlive() || thread2.isAlive()){}
        System.out.println("finish!");
    }

    @Override
    public void run() {
        method();
    }

    public synchronized void method(){
        System.out.println("线程"+Thread.currentThread().getName()+"进入代码块");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("线程"+Thread.currentThread().getName()+"结束任务");
    }
}

多角度简单解析synchronize关键字

可以看到,即使是加了锁,两个线程是都可以并行执行的。因为加的是不同对象的锁,两个线程获取的对象锁都是不同的,所以他们可以并行的执行。

加入static后:
多角度简单解析synchronize关键字

从结果我们可以看出,线程串行的执行了。即方法加入了static后,锁住的便是该类对象,所以即使两个的实例对象不一样,但是其实有类对象new出来的,所以还是会进行互斥,从而达到了串行执行的效果。

​ 2、指定锁为Class对象

同样的,我们先来看锁this的情况(也就是锁当前对象实例)

public class ClassSuoStaticClass implements Runnable{
    static ClassSuoStaticClass classSuoStaticMethod = new ClassSuoStaticClass();
    static ClassSuoStaticClass classSuoStaticMethod2 = new ClassSuoStaticClass();

    public static void main(String[] args) {
        System.out.println("classSuoStaticMethod=>>"+classSuoStaticMethod);
        System.out.println("classSuoStaticMethod2=>>"+classSuoStaticMethod2);
        Thread thread1 = new Thread(classSuoStaticMethod);
        Thread thread2 = new Thread(classSuoStaticMethod2);
        thread1.start();
        thread2.start();
        while(thread1.isAlive() || thread2.isAlive()){}
        System.out.println("finish!");
    }

    @Override
    public void run() {
        method();
    }

    public void method(){
        // 每个线程都会执行到这个方法,锁this就是锁这个线程实例对象,对另一个线程不构成任何干扰
        synchronized(this) {
            System.out.println("线程" + Thread.currentThread().getName() + "进入代码块");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("线程" + Thread.currentThread().getName() + "结束任务");
        }
    }
}

多角度简单解析synchronize关键字

我们可以看到,锁this的话,不同线程之间不会进行互斥,都可以拿到锁进入到同步代码块中。

而我们再看锁对象:
多角度简单解析synchronize关键字
从结果中我们看出,锁对象的话不同线程之间就会进行互斥。可以保证在同一时间下只有一个线程执行同步代码块的内容。

多线程访问同步方法的7种情况

1、两个线程访问同一个对象的同步方法:

​ 争抢同一把锁,只有一个线程能拿到锁去执行。

2、两个线程访问两个对象的同步方法:

​ synchronize锁的是不同的对象实例,所以两个线程不会产生互斥,并行的执行代码。

3、两个线程访问synchronize的静态方法:

​ 争抢同一把类锁,只有一个线程能拿到锁去执行。

4、同时访问同步方法和非同步方法:

​ 非同步方法不会受到影响,synchronize对哪个方法加锁那么只针对哪个方法有效,其他的方法不会产生影响。

5、访问同一个对象的两个不同的同步方法

​ 既然是同一个对象,那么synchronize加锁指向的this也是指向同一个,所以会导致程序串行的执行。

6、同时访问静态synchronize和非静态synchronize

​ 非静态synchronize锁的是this对象实例,而静态synchronize锁的是Class这个唯一的对象。这两个锁的不是同一个,所以会让程序并行的执行。

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

​ JVM会帮忙释放锁。

还有一种特殊的情况:在同步方法中调用非同步方法,此时已经线程不安全。该非同步方法其他的线程也可以进行直接访问,所以最终结果是多个线程并行访问该非同步方法。

七种情况小结:

1、一把锁只能同时被一个线程获取,没有拿到锁的线程只能进行等待

2、每个实例都对应自己的一把锁,不同实例之间互不影响。但是,如果是类锁的话,那么只有一把,*.class或者是静态synchronize方法都使用同一把类锁。

3、不管是正常执行完还是抛出异常,都会释放锁。

Synchronized的性质

1、可重入

​ 含义:外层函数获得锁后,内层函数可以直接再次获得该锁。

​ 不可重入:当需要拿另一把锁的时候,需要释放掉该锁并且重新竞争才可以。

​ 好处:避免死锁,提升封装性

​ 粒度:线程范围而非调用范围。

可重入测试:
多角度简单解析synchronize关键字

2、不可中断

​ 含义:当锁被别的线程获得后,如果我还想获得那么只能进行等待或者阻塞,直到别的线程释放锁。如果别的线程不释放,我将一直等待阻塞。

Synchronized原理

加锁和释放锁的原理

​ 时机:内置锁

​ 从字节码反编译文件看出synchronize:

1、首先,写一个简单的带有synchronize代码块的类:

public class LockSync {
    private Object object = new Object();

    public void add(){
        synchronized (object){
            
        }
    }
}

2、通过javac进行编译:
多角度简单解析synchronize关键字

3、通过javap -verbose 编译的类查看所有内容;定位到同步代码块的方法:
多角度简单解析synchronize关键字

此致,我们可以看到了加锁和释放锁的方法;当进入到同步代码块就必须要获得monitor锁的对象,但是释放锁可以让线程执行完也可以是抛出异常。所以,获取锁和释放锁并不是一一配对的。

解读Monditorenter和Monditorexit指令:

Monditorenter和Monditorexit会在执行的时候让对象的锁计数进行+1或者-1的操作,每个对象与一个Monditorenter相关联,而一个Monditorenter的lock锁只能在同一时间被同一个线程获得;一个线程在尝试获得与一个对象关联的Monditorenter所有权的时候,只会发生以下三种情况。1、当前计数器为0,表示并未有线程获得,此时该线程会马上获得并把计数器+1;2、如果重入的话,计数器就会累加;3、Monditorenter被其他线程持有,我去获取他只能进入阻塞状态进行等待,直到Monditorenter计数器变为0。Monditorexit,拥有锁的话,释放其所有权;释放的过程:直接将计数器-1即可。

可重入原理

依靠加锁次数计数器。JVM负责跟踪对象被加锁的次数:线程第一次给对象加锁的时候,计数变为1。每当这个线程在此对象上再次获得锁时,计数会递增。当任务离开时,计数递减,当计数为0的时候,锁被完全释放。

保证可见性原理

简单了解Java内存模型:

线程之间的简单通信:
多角度简单解析synchronize关键字

当一个线程要与另一个线程通信的时候,首先他会将该线程中的本地内存里面的变量写入到主内存中;接着另一个线程在从主内存中读取。使用synchronize关键字,其会在释放锁之前将本地内存的内容写入到主内存中,这样就可以保证在线程A释放锁后和线程B拿到锁之前的这段时间,本地内存和主内存的数据都是一致的。

Synchronized的缺陷

1、效率低

​ 锁的释放情况少:只有代码执行完和抛出异常才能释放。

​ 尝试获取锁不能设置时间

​ 不能中断正在尝试获取锁的线程

2、不够灵活

​ 加锁和释放的时机单一,每个锁只有单一的条件;而读写锁就比较灵活,读数据就不需要加锁,写数据就加上写锁。

3、无法知道是否成功获取锁

​ Lock接口可以对是否成功获取锁进行下一步业务处理。

常见面试问题

1、synchronize使用注意点:

​ 锁对象不能为空:锁的信息存在对象头中,如果对象都不存在,那么锁信息无法放置。

​ 作用域不宜过大:

​ 避免死锁。

2、Lock和synchronize如何选择:

​ 如果可以,使用JUC包下的类;其次优先使用synchronize;最后,根据业务要写Lock或者使用其方法的时候才用Lock。

3、多线程访问同步方法的具体情况(就是上面那七种)

本文地址:https://blog.csdn.net/a760352276/article/details/107620307