zoukankan      html  css  js  c++  java
  • synchronized详解

    synchronized

    在学习synchroinzed前,我们首先需要了解什么是线程安全性?

    当多个线程操作共享资源时,如果最终的结果与我们预想的一致,那么就是线程安全的,否则就是线程不安全的。

    看下面代码:

    /**
     * @author 赵帅
     * @date 2021/1/6
     */
    public class ThreadDemo {
    
        private int num = 0;
    
        public void fun() {
            for (int i = 0; i < 1000; i++) {
                num++;
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            ThreadDemo demo = new ThreadDemo();
    
            for (int i = 0; i < 10; i++) {
                new Thread(demo::fun).start();
            }
    
            TimeUnit.SECONDS.sleep(5);
            System.out.println("demo.num = " + demo.num);
        }
    }
    

    我们定义了一个num值为0;在fun方法中我们对这个num自增1000次;在main方法中我们启动了十个线程来调用这个方法,那么最终num的值应该是10*1000=10000。期望值是10000,但是执行方法后,无论执行几次最终的结果都不是期望值,因此这个类是线程不安全的。

    总结造成线程安全问题的主要原因:

    1. 存在共享资源。
    2. 存在多个线程同时操作共享资源。

    上面的问题如何解决?

    为了解决这个问题,我们需要保证在一个线程操作共享数据时,其他的线程不能操作这个数据。也就是保证同一时刻有且只有一个线程可以操作共享数据,其他线程必须等待这个线程处理完后再进行,这中方式叫做互斥锁。synchroinzed关键字可以实现这个操作。synchronized可以保证在同一时刻只有一个线程执行某个方法或某个代码块。

    synchronized的使用

    使用synchronzed解决上面的问题:

    import java.util.concurrent.TimeUnit;
    
    /**
     * @author 赵帅
     * @date 2021/1/6
     */
    public class ThreadDemo {
    
        private int num = 0;
        private final Object lock = new Object();
    
        public void fun() {
            synchronized (lock) {
            for (int i = 0; i < 1000; i++) {
                    num++;
                }
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            ThreadDemo demo = new ThreadDemo();
    
            for (int i = 0; i < 10; i++) {
                new Thread(demo::fun).start();
            }
    
            TimeUnit.SECONDS.sleep(5);
            System.out.println("demo.num = " + demo.num);
        }
    }
    

    运行结果与期望值一致。synchronized在使用时必须锁定一个对象,可以像上面代码一样自己定义锁的对象,也可以使用如下方式:

    /**
     * @author 赵帅
     * @date 2021/1/7
     */
    public class SynchronizedDemo {
    
        /**
         * 加锁锁定的对象
         */
        private final Object lock = new Object();
        private static final Object STATIC_LOCK = new Object();
    
        /**
         * 方式1: 使用this关键字,锁定当前对象
         */
        public void fun1() {
            synchronized (this) {
                // do something
            }
    
            synchronized (lock) {
                // do something
            }
        }
    
        /**
         * 方式2:锁定方法
         * 在方法上加锁,这种方式与上面一样,都是锁定的当前对象
         */
        public synchronized void fun2(){}
    
        /**
         * 方式3:静态方法内加锁
         * 静态方法时无法使用this,只能锁定static修饰的对象,或者使用 类对象。
         * Synchronized.class 是Class对象
         */
        public static void fun3() {
            synchronized (SynchronizedDemo.class) {
                // do something
            }
    
            synchronized (STATIC_LOCK) {
                // do something
            }
        }
    
        /**
         * 方式4:锁定静态方法 
         * 锁定静态方法时,与上面方式一样,锁定的是当前类对象
         */
        public static synchronized void fun4() {}
    }
    
    

    多个线程必须竞争同一把锁,也就是说锁对象必须相同,下面这种方式是错误的:

        public void fun1() {
            final Object lock = new Object();
            synchronized (lock) {
                // do something
            }
        }
    

    每个线程进来后都会创建一个新的锁对象,线程之间不存在锁竞争,那么锁就失去了作用,因此必须保证锁定同一个对象,多个线程竞争同一把锁。

    synchronized锁定的对象不能是基本类型和String类型

    使用如下代码做解释:

    package com.xiazhi.thread;
    
    import java.util.concurrent.TimeUnit;
    
    /**
     * @author 赵帅
     * @date 2021/1/7
     */
    public class SynchronizedDemo2 {
    
        public Integer lock = 1;
    
        public void fun1() {
            synchronized (lock) {
                System.out.println(Thread.currentThread().getName() + "得到锁");
                try {
                    // 模拟执行业务代码耗时1秒
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "释放锁");
            }
        }
    
        public static void main(String[] args) {
            SynchronizedDemo2 demo = new SynchronizedDemo2();
            for (int i = 0; i < 10; i++) {
                new Thread(demo::fun1).start();
                // demo.lock++; //1
            }
    
        }
    }
    
    
    

    上面代码中定义了一个Integer类型的锁,并启动了十个线程,来调用fun1方法。我们期待的输出是这样的。

    Thread-0得到锁
    Thread-0释放锁
    Thread-9得到锁
    Thread-9释放锁
    Thread-8得到锁
    Thread-8释放锁
    Thread-7得到锁
    Thread-7释放锁
    Thread-6得到锁
    Thread-6释放锁
    Thread-5得到锁
    Thread-5释放锁
    Thread-4得到锁
    Thread-4释放锁
    Thread-3得到锁
    Thread-3释放锁
    Thread-2得到锁
    Thread-2释放锁
    Thread-1得到锁
    Thread-1释放锁
    

    线程之间因为竞争同一把锁有序执行,此时程序是可以正常运行的。但是一旦我们打开 demo.lock++这个注释,那么程序的结果就会变成这样:

    Thread-0得到锁
    Thread-2得到锁
    Thread-1得到锁
    Thread-3得到锁
    Thread-4得到锁
    Thread-6得到锁
    Thread-7得到锁
    Thread-8得到锁
    Thread-9得到锁
    Thread-1释放锁
    Thread-4释放锁
    Thread-6释放锁
    Thread-0释放锁
    Thread-5得到锁
    Thread-2释放锁
    Thread-3释放锁
    Thread-7释放锁
    Thread-8释放锁
    Thread-9释放锁
    Thread-5释放锁
    

    每一个线程都能拿到锁,这说明线程之间并不是在竞争同一把锁了。这是因为demo.lock++实际上执行的是`demo.lock = new Integer(demo.lock+1)。可以看到,创建了一个新的对象。我们打印一下锁对象的内存地址:

    package com.xiazhi.thread;
    
    import java.util.concurrent.TimeUnit;
    
    /**
     * @author 赵帅
     * @date 2021/1/7
     */
    public class SynchronizedDemo2 {
    
        public Integer lock = 1;
    
        public void fun1() {
            synchronized (lock) {
                System.out.println(Thread.currentThread().getName() + "得到锁");
                // 打印当前锁对象的内存地址
                System.out.println("当前锁对象:" + System.identityHashCode(lock));
                try {
                    // 模拟执行业务代码耗时1秒
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "释放锁");
            }
        }
    
        public static void main(String[] args) {
            SynchronizedDemo2 demo = new SynchronizedDemo2();
            for (int i = 0; i < 10; i++) {
                new Thread(demo::fun1).start();
                demo.lock++;
            }
    
        }
    }
    

    查看运行结果:

    Thread-1得到锁
    Thread-2得到锁
    当前锁对象:245887817
    Thread-0得到锁
    当前锁对象:1443225064
    当前锁对象:245887817
    Thread-3得到锁
    当前锁对象:1443225064
    Thread-4得到锁
    当前锁对象:1468479040
    Thread-5得到锁
    当前锁对象:774263507
    Thread-6得到锁
    当前锁对象:1022687942
    Thread-7得到锁
    当前锁对象:166950465
    Thread-8得到锁
    当前锁对象:1694292106
    Thread-9得到锁
    当前锁对象:1694292106
    Thread-2释放锁
    Thread-0释放锁
    Thread-4释放锁
    Thread-3释放锁
    Thread-1释放锁
    Thread-6释放锁
    Thread-5释放锁
    Thread-8释放锁
    Thread-7释放锁
    Thread-9释放锁
    

    可以很明显的看到锁的对象一直在变化,而我们加锁的目的就是为了保证多个线程竞争同一把锁,现在是在竞争多把锁。线程之间就不存在竞争关系,都可以得到锁。所以不能使用Integer,其他的基本类型包装类型也是跟这个一样。所以说锁对象不能是基本类型包装类型。

    如果只是因为i++这个原因的话,或许我们会想如果用final修饰为不可变对象不就可以了么。例如下面这样:

        public final Integer lock = 1;
    

    这样的话,就保证了lock对象是不可变的。这样是不是就可以了?

    仍然不行。因为再Integer类内部维护着一个缓存池,缓存-128~127之间的值。

    /**
     * @author 赵帅
     * @date 2021/1/9
     */
    public class IntegerTest {
        public static void main(String[] args) {
            Integer var1 = 127;
            Integer var2 = 127;
    
            System.out.println(var1 == var2);// true
    
            Integer var3 = 128;
            Integer var4 = 128;
            System.out.println(var3 == var4);// false
        }
    }
    

    可以看到如果Integer的值在 -128~127之间的话,无论创建多少次,实际上使用的都会是一个对象。那么再使用中就会造成如下问题:

    我们首先来看不使用Integer做锁的时候, 程序的运行结果:

    package com.xiazhi.thread;
    
    import java.util.concurrent.TimeUnit;
    
    /**
     * @author 赵帅
     * @date 2021/1/9
     */
    public class SynchronizedDemo3 {
    
        static class A{
            private final Object lock = new Object();
    
            public void fun1() {
                synchronized (lock) {
                    System.out.println(Thread.currentThread().getName() + "获取锁");
                    // do something
                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "释放锁");
                }
            }
    
        }
    
        static class B{
            private final Object lock = new Object();
    
            public void fun1() {
                synchronized (lock) {
                    System.out.println(Thread.currentThread().getName() + "获取锁");
                    // do something
                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "释放锁");
                }
            }
        }
    
        public static void main(String[] args) {
            A a = new A();
            B b = new B();
            for (int i = 0; i < 5; i++) {
                new Thread(a::fun1, "Class A" + i).start();
                new Thread(b::fun1, "Class B" + i).start();
            }
        }
    }
    

    此时程序的运行结果是这样的:

    Class A0获取锁
    Class B0获取锁
    Class A0释放锁
    Class A4获取锁
    Class B0释放锁
    Class B4获取锁
    Class A4释放锁
    Class B4释放锁
    Class A3获取锁
    Class B3获取锁
    Class A3释放锁
    Class B3释放锁
    Class A2获取锁
    Class B2获取锁
    Class A2释放锁
    Class B2释放锁
    Class A1获取锁
    Class B1获取锁
    Class A1释放锁
    Class B1释放锁
    

    可以看到,ClassA的和ClassB之间是没有锁竞争的,类A的lock和类B的lock是两把锁,这样的话,这也类关联的线程其实是两个并行的线程。A和B之间互不影响。但是如果我们将类A和类B的锁对象修改:

    package com.xiazhi.thread;
    
    import java.util.concurrent.TimeUnit;
    
    /**
     * @author 赵帅
     * @date 2021/1/9
     */
    public class SynchronizedDemo3 {
    
        static class A{
            private final Integer lock = 1;
    
            public void fun1() {
                synchronized (lock) {
                    System.out.println(Thread.currentThread().getName() + "获取锁");
                    // do something
                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "释放锁");
                }
            }
    
        }
    
        static class B{
            private final Integer lock = 1;
    
            public void fun1() {
                synchronized (lock) {
                    System.out.println(Thread.currentThread().getName() + "获取锁");
                    // do something
                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "释放锁");
                }
            }
        }
    
        public static void main(String[] args) {
            A a = new A();
            B b = new B();
            for (int i = 0; i < 5; i++) {
                new Thread(a::fun1, "Class A" + i).start();
                new Thread(b::fun1, "Class B" + i).start();
            }
        }
    }
    

    此时再次运行程序,会发现A和B之间变成串形了,因为A和B都是用了Integer做锁,而且值一样,就变成了一把锁了。

    通过上面的分析,我们知道了为什么不允许使用基本包装类型来做锁对象。那么为什么也不允许String呢?

    原因与Integer缓存池一样,String创建的对象会进入常量池缓存。

    synchronized保证了可见性、原子性、有序性

    上面我们在讲volatile的可见性时的代码,如果我们讲代码这样更改:

    import java.util.concurrent.TimeUnit;
    
    /**
     * 证明JMM模型中,线程对共享资源的操作,操作的是副本。
     * @author 赵帅
     * @date 2021/1/4
     */
    public class JMMTest {
    
        /**
         * 线程是否继续循环
         */
        private static boolean running = true;  //0
    
        public static void main(String[] args) throws InterruptedException {
    
            new Thread(() -> {
                while (running) {
                    System.out.println("hello"); //1
                }
            }, "thread-0").start();
    
            TimeUnit.SECONDS.sleep(1);
            running = false;
            System.out.println("main线程结束");
        }
    
    }
    
    

    我们在1处添加代码,发现0处即使没有添加volatile,代码也是能正常结束的。为什么?

    查看 System.out.println的源码:

    		public void println(String x) {
            synchronized (this) {
                print(x);
                newLine();
            }
        }
    

    发现使用了synchronized。synchronized是保证了可见性、原子性和有序性。

    synchroinzed是如何保证可见性的?

    synchronized获得锁之后会执行以下内容:

    1. 清空工作内存中共享变量的值
    2. 从主内存重新拷贝需要使用的共享变量的值
    3. 执行代码
    4. 将共享变量的最新值刷新到主内存数据
    5. 释放锁

    从上面步骤可以看出,synchroinzed保证了线程可见性。

    synchronized是如何保证原子性的?

    什么是原子性?

    原子性是指操作是不可分的,要么全部一起执行,要么都不执行。

    synchronized如何保证原子性

    查看synchronized的字节码原语,synchronized是通过monitorenter和monitorexit两个命令来操作的。线程1在执行monitorenter指令的时候,会对Monitor进行加锁,加锁后其他线程无法获得锁,除非线程1主动释放锁。即使在执行过程中,由于时间片用完,线程1放弃cpu,但是它并没有解锁,由于synchronized是可重入的,下一个时间片还是只能被他自己获取到,还是会继续执行代码,直到所有代码执行完,这就保证了原子性。

    synchronized是可重入锁,什么是可重入锁?

    查看下面一段代码:

    package com.xiazhi.thread;
    
    /**
     * @author 赵帅
     * @date 2021/1/9
     */
    public class SynchronizedDemo4 {
        
        public synchronized void fun1() {
            fun2();
        }
        
        public synchronized void fun2() {
            // do something
        }
    
        public static void main(String[] args) {
            SynchronizedDemo4 demo = new SynchronizedDemo4();
            demo.fun1();
        }
    }
    

    方法fun1和fun2都被synchronized修饰了,也就是说这两个方法都需要获得锁才可以执行,但是在fun1中调用了fun2方法,程序进入fun1时说明已经获得到this的锁了,之前我们说了,当锁被占用时,其他线程只有等待当前线程释放锁才可以拿到锁,但是现在线程已经拿到锁了,那么再次调用fun2是否能够调用成功?如果可以调用成功就说明这是个可重入锁。也就是说可重入锁就是指一个线程是否可以重复多次获得锁。

    synchronized保证有序性

    有序性是指程序执行的顺序按照代码的先后顺序执行。在并发时,程序的执行可能会出现乱序。给人的直观感觉就是:写在前面的代码,会在后面执行。但是synchronized提供了有序性保证,这其实和as-if-serial语义有关。
    as-if-serial语义是指不管怎么重排序(编译器和处理器为了提高并行度),单线程程序的执行结果都不能被改变。编译器和处理器无论如何优化,都必须遵守as-if-serial语义。只要编译器和处理器都遵守了这个语义,那么就可以认为单线程程序是按照顺序执行的,由于synchronized修饰的代码,同一时间只能被同一线程访问。那么可以认为是单线程执行的。所以可以保证其有序性。

    synchronized锁升级过程

    synchronized在jdk早期是重量级锁。

    什么是重量级锁?

    要解释重量级锁和轻量级锁的概念首先需要理解用户态和内核态的。

    一个对象的内容

    在java中,一个对象的内容主要分为以下几个部分:

    类型 jvm 32位长度 jvm64位长度
    markword 64位,8字节 64位,8字节
    class类指针长度 32位 64位,开启类指针压缩后为32位,默认开启
    属性长度 32位 64位,开启属性指针压缩后为32位,默认开启
    补齐 - -

    java内存地址按照8字节对齐,因此当对象的长度不足8的倍数是,会补齐到8的倍数。例如:

    Object obj = new Object();
    

    obj对象的大小 = markword(8字节)+Object类指针长度(8字节)+属性指针长度(object无属性,0字节)==16字节,16为8的倍数,所以不需要补齐。

    当开启类指针压缩时:

    obj对象的大小 = markword(8字节)+Object类指针长度(4字节,开启指针压缩)+属性指针长度(object无属性,0字节)==12字节。12不是8的倍数,所以补齐4个字节,最后类大小仍为16字节。

    markword

    我们之前说synchronized必须锁定一个对象,那么多个线程如何判断这个对象是否已经被占用了呢?当锁定这个对象时,会对这个对象添加一个标记,标记这个对象是否加锁。这个标记就放在markword中。

    锁升级

    因为早期的synchronized太重,每次都要调用内核态进行操作,效率太低了,因此为了提升效率,在后来的版本中对synchronized进行了优化,添加了锁升级的过程,锁升级过程中锁的状态就记录在锁对象的markword中。整个锁升级过程如下:

    • 无锁态:对象刚创建,还没有线程进来加锁。
    • 偏向锁:第一个线程进来后,升级为偏向锁。
    • 轻量级锁(自旋锁):当多个线程竞争这把锁时,升级为自旋锁。
    • 重量级锁:当线程自旋超过15次或等待线程数超过10,升级为重量级锁。

    锁升级过程与markword中内容对应关系如下:

    锁状态 25bit 4bit 1bit 2bit
    23bit 2bit 是否偏向锁 锁标志位
    无锁态 对象的hashcode 分代年龄 0 01
    偏向锁 线程ID epoch 分代年龄 1 01
    轻量级锁 指向栈中锁记录的指针 00
    重量级锁 指向重量级锁的指针 10
    GC标记 11
  • 相关阅读:
    .NET写的Email可以群发邮件的实用函数
    動網中用到的幾個Function和一個JS[base64encode,base64decode,md5,sendmail,js]
    HTML在线编辑器
    IIS虚拟目录控制类
    实用正则表达式(实用篇)
    IIS站点管理类
    精巧sql语句
    圖片滾動代碼
    c# 添加图片水印,可以指定水印位置+生成缩略图
    JavaScript旋转图片
  • 原文地址:https://www.cnblogs.com/Zs-book1/p/14257054.html
Copyright © 2011-2022 走看看