Java——多线程高并发系列之synchronized关键字

Posted 张起灵-小哥

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java——多线程高并发系列之synchronized关键字相关的知识,希望对你有一定的参考价值。

文章目录:

写在前面

Demo1(synchronized面对同一个实例对象)

Demo2(synchronized面对多个实例对象)

Demo3(synchronized面对一个 public static final 常量)

Demo4(synchronized同步代码块分别位于实例方法、静态方法中)

Demo5(synchronized同步实例方法体,默认的锁对象是this)

Demo6(synchronized同步静态方法体,默认的锁对象是当前类的运行时对象Test06.class)

Demo7(同步方法与同步代码块如何选择)

Demo8(synchronized解决脏读问题)

Demo9(synchronized同步过程中线程出现异常, 会自动释放锁对象)

Demo10(使用synchronized实现死锁)


写在前面

Java 中的每个对象都有一个与之关联的内部锁(Intrinsic lock)。这种锁也称为监视器(Monitor),这种内部锁是一种排他锁,可以保障原子性,可见性与有序性。内部锁是通过 synchronized 关键字实现的,synchronized 关键字修饰代码块,修饰该方法。

修饰实例方法就称为同步实例方法
修饰静态方法称称为同步静态方法

下面,我用10个Demo带大家彻底搞明白synchronized关键字的使用!!!


Demo1(synchronized面对同一个实例对象)

synchronized关键字当前修饰的是一个代码块,其中的this可以这样理解,目前,synchronized这个“东西”目前处在Test01类中,而我们程序的第一句就是 Test01 obj = new Test01(); 那么synchronized中的this代表的就是obj对象,obj对象是属于Test01类的,那这不就对应上this了吗?

package com.szh.synchronizedtest;

/**
 * synchronized同步代码块
 *  this表示的是obj锁对象
 */
public class Test01 {

    public static void main(String[] args) {
        //先创建Test01对象
        Test01 obj=new Test01();

        //创建两个线程,分别调用mm方法
        new Thread(new Runnable() { //Thread-0
            @Override
            public void run() {
                obj.mm(); //使用的锁对象this就是obj
            }
        }).start();

        new Thread(new Runnable() { //Thread-1
            @Override
            public void run() {
                obj.mm(); //使用的锁对象this也是obj
            }
        }).start();
    }

    public void mm() {
        synchronized (this) {
            for (int i = 1; i <= 10; i++) {
                System.out.println(Thread.currentThread().getName() + " ---> " + i);
            }
        }
    }
}

从程序的运行结果可以看出来,第一个线程这里先抢到CPU执行权,然后启动,进入mm方法后,对其上锁,此时第二个线程就进不来了,必须等待第一个线程执行完毕同步代码块之后才可以执行。

也就是Thread-0先打印了10行,之后Thread-1才打印。(我这里只是面对这个执行结果才这样说,线程的执行结果也可能是先Thread-1、再Thread-0)


Demo2(synchronized面对多个实例对象)

这个例子中,synchronized仍然修饰了同步代码块,但是这里Test02类new了两个实例对象,那这就不一样了。

new第一个Thread的时候,我们使用obj1实例对象去调用mm方法,这时候Thread-0线程进入同步代码块,对obj1对象上锁,执行for循环。

new第二个Thread的时候,我们使用obj2实例对象去调用mm方法,这时候Thread-1线程能不能进入同步代码块呢?(不能吧,你Thread-0线程不是已经上锁了吗?)答案当然能!!!Thread-0线程是对obj1对象上的锁,我先Thread-1面对的是obj2对象啊,这个对象还没有上锁呢,我这里为什么不能进入同步代码块呢???所以这里Thread-1完全可以进入同步代码块执行,此时对obj2对象上锁。

也就是说,synchronized关键字要想实现线程同步,这些线程必须面对同一个锁对象!!!

package com.szh.synchronizedtest;

/**
 * synchronized同步代码块
 *  this在这个代码案例中表示了两个不同的锁对象
 *  要想实现同步,必须是同一个锁对象
 */
public class Test02 {

    public static void main(String[] args) {
        //先创建Test01对象
        Test02 obj1=new Test02();
        Test02 obj2=new Test02();

        //创建两个线程,分别调用mm方法
        new Thread(new Runnable() { //Thread-0
            @Override
            public void run() {
                obj1.mm(); //使用的锁对象this就是obj1
            }
        }).start();

        new Thread(new Runnable() { //Thread-1
            @Override
            public void run() {
                obj2.mm(); //使用的锁对象this就是obj2
            }
        }).start();
    }

    public void mm() {
        synchronized (this) {
            for (int i = 1; i <= 10; i++) {
                System.out.println(Thread.currentThread().getName() + " ---> " + i);
            }
        }
    }
}

执行结果中可以看到,两个线程不同步,一会Thread-0、一会Thread-1。


Demo3(synchronized面对一个 public static final 常量)

这个例子中,synchronized中传入了一个 public static final 修饰的常量,这个自然也是可以实现线程同步的。

因为在Test03这个类中,不管你new多少个对象,在某个线程中,使用哪个实例对象去调同步代码块对应的方法,对于我们 public static final 修饰的常量来说,在这个类中都是可见的!!!

当第一个线程(Thread-0)run启动成功之后,synchronized将这个常量给上了锁,然后线程Thread-0进去执行for循环。

这个时候第二个线程(Thread-1)run也启动成功了,它也想进入同步代码块执行,但是同步代码块面对的是常量,而常量此时已经被Thread-0给锁上了,那么你Thread-1肯定就进不去了呀,它就必须等待Thread-0执行完毕才可以进入同步代码块执行。

(我这里只是面对这个执行结果才这样说,线程的执行结果也可能是先Thread-1、再Thread-0)

package com.szh.synchronizedtest;

/**
 * synchronized同步代码块
 *  使用一个常量对象作为锁对象
 *  this表示的是OBJ
 */
public class Test03 {

    //定义一个常量
    public static final Object OBJ=new Object();

    public static void main(String[] args) {
        //先创建Test01对象
        Test03 obj1=new Test03();
        Test03 obj2=new Test03();

        //创建两个线程,分别调用mm方法
        new Thread(new Runnable() { //Thread-0
            @Override
            public void run() {
                obj1.mm(); //使用的锁对象this就是OBJ常量,这与使用哪个对象调用mm方法无关,这个常量是大家共有的
            }
        }).start();

        new Thread(new Runnable() { //Thread-1
            @Override
            public void run() {
                obj2.mm(); //使用的锁对象this就是OBJ常量,这与使用哪个对象调用mm方法无关
            }
        }).start();
    }

    public void mm() {
        synchronized (OBJ) {
            for (int i = 1; i <= 10; i++) {
                System.out.println(Thread.currentThread().getName() + " ---> " + i);
            }
        }
    }
}


Demo4(synchronized同步代码块分别位于实例方法、静态方法中)

这个例子中,我分别将synchronized修饰的同步代码块放在了实例方法和静态方法中,下面,我来说一下我对执行结果的理解:

实例方法中的synchronized理解可以参考Demo3。而这个静态方法中,为什么也实现了线程同步呢?因为我们都知道,static 修饰的方法被称为静态方法,而静态方法一般是由它的所属类直接调用的,而在这个例子中它是属于Test04类的,所以

public static void sm() {
    synchronized (OBJ) {
        for (int i = 1; i <= 10; i++) {
            System.out.println(Thread.currentThread().getName() + " ---> " + i);
        }
    }
}

这个代码块就是由Test04直接调用的,而它的同步代码块锁的是OBJ这个常量,这个常量不正好也是Test04类所有的吗?所以说,这三个子线程中的同步代码块面对的都是Test04类所拥有的OBJ常量(就这一把锁)!!!

package com.szh.synchronizedtest;

/**
 * synchronized同步代码块
 *  这里使用一个常量作为锁对象,这个常量是所有对象都共享的,也就是同一个锁对象
 *  不管是实例方法还是静态方法,只要是同一个锁对象,就可以实现同步
 */
public class Test04 {

    //定义一个常量
    public static final Object OBJ=new Object();

    public static void main(String[] args) {
        //先创建Test01对象
        Test04 obj1=new Test04();
        Test04 obj2=new Test04();

        //创建两个线程,分别调用mm方法
        new Thread(new Runnable() { //Thread-0
            @Override
            public void run() {
                obj1.mm(); //使用的锁对象this就是OBJ常量,这与使用哪个对象调用mm方法无关,这个常量是大家共有的
            }
        }).start();

        new Thread(new Runnable() { //Thread-1
            @Override
            public void run() {
                obj2.mm(); //使用的锁对象this就是OBJ常量,这与使用哪个对象调用mm方法无关
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                sm(); //使用的锁对象this就是OBJ常量
            }
        }).start();
    }

    public void mm() {
        synchronized (OBJ) {
            for (int i = 1; i <= 10; i++) {
                System.out.println(Thread.currentThread().getName() + " ---> " + i);
            }
        }
    }

    public static void sm() {
        synchronized (OBJ) {
            for (int i = 1; i <= 10; i++) {
                System.out.println(Thread.currentThread().getName() + " ---> " + i);
            }
        }
    }
}

这里可以看到,虽然我们new线程的顺序是Thread-0、Thread-1、Thread-2,但是执行顺序却是0、1、2的线程顺序,所以多线程的执行结果并不是一定按照代码顺序来的。


Demo5(synchronized同步实例方法体,默认的锁对象是this)

这个例子中,synchronized(this)就不再多说了,它锁的就是Test05类的那个实例对象obj。

而 public synchronized void mm2()这个实例方法中,大家不要被迷惑,这里它修饰的是一个实例方法,而实例方法不就是由类的实例对象调用的吗?那说白了,它锁的不还是本类中new的那个obj对象吗?是的吧!!!

package com.szh.synchronizedtest;

/**
 * synchronized同步实例方法体,默认的锁对象是this
 *  this表示的obj锁对象
 */
public class Test05 {

    public static void main(String[] args) {
        //先创建Test01对象
        Test05 obj=new Test05();

        //创建两个线程,分别调用mm方法
        new Thread(new Runnable() { //Thread-0
            @Override
            public void run() {
                obj.mm(); //使用的锁对象this就是obj
            }
        }).start();

        new Thread(new Runnable() { //Thread-1
            @Override
            public void run() {
                obj.mm2(); //使用的锁对象this也是obj
            }
        }).start();
    }

    public void mm() {
        synchronized (this) {
            for (int i = 1; i <= 10; i++) {
                System.out.println(Thread.currentThread().getName() + " ---> " + i);
            }
        }
    }

    public synchronized void mm2() {
        for (int i = 1; i <= 10; i++) {
            System.out.println(Thread.currentThread().getName() + " ---> " + i);
        }
    }
}


Demo6(synchronized同步静态方法体,默认的锁对象是当前类的运行时对象Test06.class)

如果这样写:synchronized(XXX.class)则表示的是当前类的运行时类对象作为锁对象,而在反射中简单的理解,XXX.class不就是这个类的字节码文件吗?

而 public synchronized static void mm2() {} 这样写,则说明synchronized修饰了静态方法体,而静态方法是属于类的,所以它这里锁的也是当前类的运行时对象。也就是说synchronized在这两种方式中面对的都是类锁!!!

package com.szh.synchronizedtest;

/**
 * synchronized同步静态方法体,默认的锁对象是当前类的运行时对象Test06.class
 *  这个也可以称为类锁
 */
public class Test06 {

    public static void main(String[] args) {
        //先创建Test01对象
        Test06 obj=new Test06();

        //创建两个线程,分别调用mm方法
        new Thread(new Runnable() { //Thread-0
            @Override
            public void run() {
                obj.mm(); //使用的锁对象是Test06.class
            }
        }).start();

        new Thread(new Runnable() { //Thread-1
            @Override
            public void run() {
                Test06.mm2(); //使用的锁对象是Test06.class
            }
        }).start();
    }

    public void mm() {
        //使用当前类的运行时类对象作为锁对象,简单的理解为将Test06类的字节码文件作为锁对象
        synchronized (Test06.class) {
            for (int i = 1; i <= 10; i++) {
                System.out.println(Thread.currentThread().getName() + " ---> " + i);
            }
        }
    }

    //synchronized修饰静态方法,同步静态方法,默认的锁对象为运行时类对象Test06.class
    public synchronized static void mm2() {
        for (int i = 1; i <= 10; i++) {
            System.out.println(Thread.currentThread().getName() + " ---> " + i);
        }
    }
}


Demo7(同步方法与同步代码块如何选择)

这个就不再给出例子了,就一句话:同步代码块,锁的粒度细,执行效率高。同步方法,锁的粒度粗, 执行效率低。


Demo8(synchronized解决脏读问题)

这个例子中,就是说如果我们在子线程中设置了用户名和密码,我们对写方法的代码块进行了同步,如果不对读方法的代码块进行同步了话,那么这个时候,子线程设置了用户名和密码,而main主线程并不知道你子线程设置的用户名和密码,它此时是可以从getValue中读取到静态内部类PublicValue中提前定义好的 root、12345678(因为读方法并没有同步,子线程对setValue方法上了锁,但是getValue还可以顺利的读)。这显然不对啊,我们希望的是子线程设置完新的用户名密码之后,在main主线程中也可以读取到新的值,而不是读取到的脏数据。(将getValue方法前的synchronized删掉,main主线程输出结果将会是 root、12345678)

这个时候,我们就需要将读方法的代码块也定义为同步的!!!这个时候,子线程一旦对setValue写方法上锁之后,那么main主线程此时再想从getValue方法中读取就不行了,因为getValue方法也上了锁,而这两个方法的synchronized都是面对的实例方法上的锁,而同步实例方法默认的锁对象就是this( PublicValue publicValue=new PublicValue(); )。所以这样等到子线程设置完新值之后,main主线程就可以读取到了。

package com.szh.synchronizedtest;

/**
 * 脏读
 *  出现读取属性值出现了一些意外, 读取的是中间值,而不是修改之后的值
 *  出现脏读的原因是: 对共享数据的修改 与 对共享数据的读取 不同步
 * 解决方法:
 *  不仅对修改数据的代码块进行同步, 还要对读取数据的代码块同步
 */
public class Test08 {

    public static void main(String[] args) throws InterruptedException {
        //开启子线程设置用户名和密码
        PublicValue publicValue=new PublicValue();
        SubThread t1=new SubThread(publicValue);
        t1.start();

        //为了确定设置成功
        Thread.sleep(100);
        //在main线程中读取用户名和密码
        publicValue.getValue();
    }

    static class SubThread extends Thread {
        private PublicValue publicValue;

        public SubThread(PublicValue publicValue) {
            this.publicValue=publicValue;
        }

        @Override
        public void run() {
            publicValue.setValue("admin","666");
        }
    }

    static class PublicValue {
        private String name="root";
        private String pwd="12345678";

        public synchronized void getValue() {
            System.out.println(Thread.currentThread().getName() + ", getter -- name: "
                            + name + ", pwd: " + pwd);
        }

        public synchronized void setValue(String name,String pwd) {
            this.name=name;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.pwd=pwd;
            System.out.println(Thread.currentThread().getName() + ", setter -- name: "
                    + name + ", pwd: " + pwd);
        }
    }
}


Demo9(synchronized同步过程中线程出现异常, 会自动释放锁对象)

这个例子说的是,同步过程中,如果发生了运行时异常,则占有锁的那个线程会自动释放占有的锁对象。

Thread-0在启动之后,执行同步代码块,它锁的是当前类的运行时对象(Test09.class),也就是当前类的字节码文件(类锁),所以这个时候,Thread-1启动后,它调用mm2()方法时,是无法执行mm2()方法的,因为这个方法是静态方法,默认的锁对象也是当前类的运行时对象(Test09.class),所以这两个子线程面对的是同一个锁!!!

当 i=5 时,Integer.parseInt("abc");这段代码会抛出字符串转换为数字异常,这个时候发生了异常,那么Thread-0子线程就会释放它所占有的Test09类锁,那么此时Thread-1就可以顺序执行了!!!(详情见运行结果图)

package com.szh.synchronizedtest;

/**
 * synchronized同步静态方法体,默认的锁对象是当前类的运行时对象Test09.class
 *  这个也可以称为类锁
 *  同步过程中线程出现异常, 会自动释放锁对象
 */
public class Test09 {

    public static void main(String[] args) {
        //先创建Test01对象
        Test09 obj=new Test09();

        //创建两个线程,分别调用mm方法
        new Thread(new Runnable() { //Thread-0
            @Override
            public void run() {
                obj.mm(); //使用的锁对象是Test06.class
            }
        }).start();

        new Thread(new Runnable() { //Thread-1
            @Override
            public void run() {
                Test09.mm2(); //使用的锁对象是Test06.class
            }
        }).start();
    }

    public void mm() {
        //使用当前类的运行时类对象作为锁对象,简单的理解为将Test06类的字节码文件作为锁对象
        synchronized (Test09.class) {
            for (int i = 1; i <= 10; i++) {
                System.out.println(Thread.currentThread().getName() + " ---> " + i);
                if (i==5) {
                    //把字符串转换为 int 类型时,如果字符串不符合 数字格式会产生异常
                    Integer.parseInt("abc");
                }
            }
        }
    }

    //synchronized修饰静态方法,同步静态方法,默认的锁对象为运行时类对象Test06.class
    public synchronized static void mm2() {
        for (int i = 1; i <= 10; i++) {
            System.out.println(Thread.currentThread().getName() + " ---> " + i);
        }
    }
}


Demo10(使用synchronized实现死锁)

这个例子,首先在子线程中有两个常量 lock1、lock2,那么你SubThread无论new多少个子线程对象,它们都是共享这两个常量的。这里对两个线程分别命名为了 a、b,然后启动这两个子线程,它们两个会抢夺CPU资源去执行自己的run方法,假如说 t1 线程先抢到了,那么它首先对 lock1 上了锁,这个时候,t2线程抢到了,那么它首先对 lock2 上了锁,这个时候,假如t1又抢到了,那么它想对 lock2 上锁,此时 lock2 已经被 t2 上过锁了,所以 t1 会在这里等待,一直等到 t2 释放 lock2 的锁;此时 t2 获得了 CPU 的运行权,那么它继续执行run方法,它此时相对 lock1 上锁,而 lock1 已经被 t1 上了锁,所以 t2 会在这里等待,一直等到 t1 释放 lock1 的锁;但是我们前面说了呀,t1在等待 t2 释放 lock2 的锁,t2在等待 t1 释放 lock1 的锁,那我等你,你等我,谁也退出不了,谁也前进不成,那不就死到这里了吗?这就是死锁问题!!!

package com.szh.synchronizedtest;

/**
 * 死锁
 */
public class Test10 {

    public static void main(String[] args) {
        SubThread t1=new SubThread();
        SubThread t2=new SubThread();

        t1.setName("a");
        t2.setName("b");

        t1.start();
        t2.start();
    }

    static class SubThread extends Thread {
        private static final Object lock1=new Object();
        private static final Object lock2=new Object();

        @Override
        public void run() {
            if ("a".equals(Thread.currentThread().getName())) {
                synchronized (lock1) {
                    System.out.println("a线程获得了lock1锁,还需要获得lock2锁");
                    synchronized (lock2) {
                        System.out.println("a线程又获得了lock2锁???");
                    }
                }
            }
            if ("b".equals(Thread.currentThread().getName())) {
                synchronized (lock2) {
                    System.out.println("b线程获得了lock2锁,还需要获得lock1锁");
                    synchronized (lock1) {
                        System.out.println("b线程又获得了lock1锁???");
                    }
                }
            }
        }
    }
}

 

以上是关于Java——多线程高并发系列之synchronized关键字的主要内容,如果未能解决你的问题,请参考以下文章

Java——多线程高并发系列之线程池(Executor)的理解与使用

Java——多线程高并发系列之线程池(Executor)的理解与使用

Java——多线程高并发系列之ThreadLocal的使用

Java——多线程高并发系列之ThreadLocal的使用

Java——多线程高并发系列之LockReentrantLock

Java——多线程高并发系列之LockReentrantLock