20210531 Synchronized三种用法

Posted 陈如水

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了20210531 Synchronized三种用法相关的知识,希望对你有一定的参考价值。

类锁,对象锁,普通同步方法,静态同步方法,同步代码块,单例模式(懒汉式(线程是否安全),饿汉式)。

先来看下利用synchronized实现同步的基础:Java中的每一个对象都可以作为锁,即一个对象一把锁。具体表现为以下3种形式。

1,对于普通同步方法,锁是当前实例对象;进入同步代码前要获得当前实例的锁;

2,对于静态同步方法,锁是当前类的Class对象;进去同步代码前要获得当前类对象的锁;

3,对于同步方法块,锁是Synchonized括号里配置的对象。这需要指定加锁的对象,进入同步代码前要获得指定对象的锁。

 

Synchonized实现原理

1)从JVM规范中可以看到Synchonized在JVM里的实现原理,JVM基于进入和退出Monitor对

象来实现方法同步和代码块同步,但两者的实现细节不一样。代码块同步是使用monitor enter

和monitor exit指令实现的,而方法同步是使用另外一种方式实现的,细节在JVM规范里并没有

详细说明。但是,方法的同步同样可以使用这两个指令来实现

2)monitor enter指令是在编译后插入到同步代码块的开始位置,而monitor exit是插入到方法结

束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之配对。任何对象都有

一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter

指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁。

 

一,普通同步方法,synchronized修饰实例方法(实例锁)

使用时,作用范围为整个函数,这里所谓的实例锁就是调用该同步方法(不包括静态方法)的对象。

/**
 * 测试三种同步状态
 */
public class SynchronizedDemo implements Runnable {

    //共享资源变量
    int count = 0;

    @Override
    public synchronized void run() {
        for (int i = 0; i < 5; i++) {
            increaseCount();
            System.out.println(Thread.currentThread().getName() + ":" + count);
        }
    }

    private void increaseCount() {
        count++;
    }

    public static void main(String[] args) {
        //两个线程,同一个对象(其实是同一个对象锁)
        SynchronizedDemo synchronizedDemo1 = new SynchronizedDemo();

        Thread thread1 = new Thread(synchronizedDemo1, "thread1");
        Thread thread2 = new Thread(synchronizedDemo1, "thread2");

        thread1.start();
        thread2.start();
    }
}

代码中开启了两个线程去操作一个变量(共享变量),count++是先读取值,再写回一个新值。我们想一下,如果第一个线程执行这一过程中,第二个线程拿到写回之前的count值,做count++操作,那么这就造成了线程不安全。所以这里在run方法加上synchronized,获取一个对象锁,代码中的实例锁就是syncTest1了。同时我们从输出结果看出:当一个线程正在访问一个对象synchronized实例方法时,别的线程是访问不了的。一个对象一把锁说的就是这个,当线程获取了该对象的锁后,其他线程无法获取该对象的锁,当然就访问不了该对象的synchronized方法,但是,可以访问该对象的其他未被synchronized修饰的方法。   

如果是一个线程 A 需要访问实例对象 obj1 的 synchronized 方法 f1(当前对象锁是obj1),另一个线程 B 需要访问实例对象 obj2 的 synchronized 方法 f2(当前对象锁是obj2),这样是允许的,因为两个实例对象锁并不同相同,此时如果两个线程操作数据并非共享的,线程安全是有保障的(两个普通同步方法,两个实例访问互不影响),遗憾的是如果两个线程操作的是共享数据,那么线程安全就有可能无法保证了。线程不安全的现象:

/**
 * 测试三种同步状态
 */
public class SynchronizedDemo implements Runnable {

    //共享资源变量
    int count = 0;

    @Override
    public synchronized void run() {
        for (int i = 0; i < 5; i++) {
            increaseCount();
            System.out.println(Thread.currentThread().getName() + ":" + count);
        }
    }

    private void increaseCount() {
        count++;
    }

    public static void main(String[] args) {
        //两个线程,两个个对象(两把不同的锁)
        SynchronizedDemo synchronizedDemo1 = new SynchronizedDemo();
        SynchronizedDemo synchronizedDemo2 = new SynchronizedDemo();

        Thread thread1 = new Thread(synchronizedDemo1, "thread1");
        Thread thread2 = new Thread(synchronizedDemo2, "thread2");

        thread1.start();
        thread2.start();
    }
}

从输出结果来看,两个线程可能同时拿到共享变量去做count++操作(两个线程同时去修改共享变量)。上述操作中虽然我们的run方法还是使用synchronized修饰,但是我们new了两个实例。这就意味存在了两个不同的实例锁,thread1和thread2分别进入了syncTest1和syncTest2的实例锁,当然保证不了线程安全。但是我们也有解决方案:使用synchronized修饰静态方法。

二、synchronized修饰静态方法

静态方法是不属于当前实例的,而是属性类的,那么这个锁就是类的class对象锁,上述问题引刃而解。

/**
 * 测试三种同步状态
 */
public class SynchronizedDemo implements Runnable {

    //共享资源变量
   static int count = 0;

    @Override
    public synchronized void run() {
        for (int i = 0; i < 5; i++) {
            increaseCount();
            System.out.println(Thread.currentThread().getName() + ":" + count);
        }
    }

    //静态同步方法
    private synchronized static void increaseCount() {
        count++;
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + count++);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        //两个线程,两个个对象(两把不同的锁)
        SynchronizedDemo synchronizedDemo1 = new SynchronizedDemo();
        SynchronizedDemo synchronizedDemo2 = new SynchronizedDemo();

        Thread thread1 = new Thread(synchronizedDemo1, "thread1");
        Thread thread2 = new Thread(synchronizedDemo2, "thread2");

        thread1.start();
        thread2.start();
    }
}

同样是new了两个不同实例,却保持了线程同步。那是我们synchronizd修饰的是静态方法,run方法中调用这个静态方法,再说一次 静态方法不属于当前实例,而是属于类。所以这个方案其实是用的一个把锁,而这个锁就是这个类的class对象锁。   需要注意的是如果一个线程A调用一个实例对象的非static synchronized方法,而线程B需要调用这个实例对象所属类的静态 synchronized方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的class对象,而访问非静态 synchronized 方法占用的锁是当前实例对象锁(结合demo2,demo3)。

静态同步方法,普通同步方法,一个占用类锁,一个占用同步锁

三、synchronized修饰代码块(只对一小部分代码加锁)

在某些情况下,我们编写的方法体可能比较大,同时存在一些比较耗时的操作,而需要同步的代码又只有一小部分,如果直接对整个方法进行同步操作,可能会得不偿失,此时我们可以使用同步代码块的方式对需要同步的代码进行包裹,这样就无需对整个方法进行同步操作了。所以他的作用范围为synchronizd(obj){}的这个大括号中。

/**
 * 测试三种同步状态
 */
public class SynchronizedDemo implements Runnable {

    //共享资源变量
    static int count = 0;
    //指定对象当作锁
    private byte[] mBytes = new byte[0];

    @Override
    public synchronized void run() {
        increaseCount();
    }

    //同步代码块
    private void increaseCount() {
        synchronized (this) {
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + ":" + count++);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) {
        //两个线程,两个个对象(两把不同的锁)
        SynchronizedDemo synchronizedDemo1 = new SynchronizedDemo();
        SynchronizedDemo synchronizedDemo2 = new SynchronizedDemo();

        Thread thread1 = new Thread(synchronizedDemo1, "thread1");
        Thread thread2 = new Thread(synchronizedDemo2, "thread2");

        thread1.start();
        thread2.start();
    }
}

从输出结果看出,这个demo并没有保证线程安全,因为我们指定锁为this,指的就是调用这个方法的实例对象。这里我们new了两个不同的实例对象syncTest1,syncTest2,所以有两个锁,thread1与thread2分别进入自己传入的对象锁的线程执行increaseCount方法,导致线程不安全。

四、指定任意对象充当锁

    private void increaseCount() {
        //任意对象当作锁
        synchronized (mBytes) {
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + ":" + count++);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

如果把demo4的成员变量注释放开,并将mBytes传入synchronized后面的括号中,也是线程不安全的结果。这里之所以加上mBytes这个对象是为了说明synchronized后面的括号中是可以指定任意对象充当锁的,而零长度的byte数组对象创建起来将比任何对象都经济。当然,如果要使用这个经济实惠的锁并保证线程安全,那就不能new出多个不同实例对象出来啦。如果你非要想new两个不同对象出来,又想保证线程同步的话,那么synchronized后面的括号中可以填入SyncTest.class,表示这个类对象作为锁,自然就能保证线程同步了。

使用类锁可以解决这个问题,代码如下:

    private void increaseCount() {
        //使用类锁
        synchronized (SynchronizedDemo.class) {
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + ":" + count++);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

总结

1,普通同步方法。一个对象中的加锁方法只允许一个线程访问。但要注意这种情况下锁的是访问该方法的实例对象, 如果多个线程不同对象访问该方法,则无法保证同步。

2,静态同步方法。由于静态方法是类方法, 所以这种情况下锁的是包含这个方法的类,也就是类对象;这样如果多个线程不同对象访问该静态方法,也是可以保证同步的。

3,同步代码块。其中普通代码块 如Synchronized(obj) 这里的obj 可以为类中的一个属性、也可以是当前的对象,它的同步效果和修饰普通方法一样;Synchronized方法 (obj.class)静态代码块它的同步效果和修饰静态方法类似。

以上是关于20210531 Synchronized三种用法的主要内容,如果未能解决你的问题,请参考以下文章

synchronized 基本用法

天天用Synchronized,底层原理是个啥?

天天用Synchronized,底层原理是个啥?

20210531-C++面试

20210531-C++面试

20210531-C++面试