当前位置: 移动技术网 > IT编程>开发语言>Java > 多角度简单解析synchronize关键字

多角度简单解析synchronize关键字

2020年07月29日  | 移动技术网IT编程  | 我要评论

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++;
    }
}

在这里插入图片描述

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

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

1、首先获取conut变量的值

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

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

这就导致了,当线程一获取到了conut值并且已经进行了+1的操作,但是并未写入到内存中,此时线程二进来了并读取了未修改的conut的值。所以在这样的情况下两个线程对变量进行了相同数值的操作。
在这里插入图片描述

两种用法

对象锁

​ 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()){}
    }
}

在这里插入图片描述

​ 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()){}
    }
}

同步代码块锁this.png

从输出结果我们可以看到,线程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()){}
    }
}

在这里插入图片描述

我们看结果可以知道,执行了两个任务锁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()){}
    }
}

在这里插入图片描述

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

类锁

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()+"结束任务");
    }
}

在这里插入图片描述

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

加入static后:
在这里插入图片描述

从结果我们可以看出,线程串行的执行了。即方法加入了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() + "结束任务");
        }
    }
}

在这里插入图片描述

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

而我们再看锁对象:
在这里插入图片描述
从结果中我们看出,锁对象的话不同线程之间就会进行互斥。可以保证在同一时间下只有一个线程执行同步代码块的内容。

多线程访问同步方法的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、可重入

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

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

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

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

可重入测试:
在这里插入图片描述

2、不可中断

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

Synchronized原理

加锁和释放锁的原理

​ 时机:内置锁

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

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

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

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

2、通过javac进行编译:
在这里插入图片描述

3、通过javap -verbose 编译的类查看所有内容;定位到同步代码块的方法:
在这里插入图片描述

此致,我们可以看到了加锁和释放锁的方法;当进入到同步代码块就必须要获得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关键字,其会在释放锁之前将本地内存的内容写入到主内存中,这样就可以保证在线程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

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网