• Java 多线程:锁(一)


    Java 多线程(三):锁(一)

    作者:Grey

    原文地址:

    博客园:Java 多线程(三):锁(一)

    CSDN:Java 多线程(三):锁(一)

    CAS

    比较与交换的意思

    举个例子,内存有个值是 3,如果用 Java 通过多线程去访问这个数,每个线程都要把这个值 +1。

    之前是需要加锁,即synchronized关键字来控制。但是 JUC 的包出现后,有了 CAS 操作,可以不需要加锁来处理,流程是:

    第一个线程:把 3 拿过来,线程本地区域做计算加 1,然后把 4 写回去。

    第二个线程:也把 3 这个数拿过来,线程本地区域做计算加 1 后,在回写回去的时候,会做一次比较,如果原来的值还是 3,那么说明这个值之前没有被打扰过,就可以把 4 写回去,如果这个值变了,假设变为了 4,那么说明这个值已经被其他线程修改过了,那么第二个线程需要重新执行一次,即把最新的 4 拿过来继续计算,回写回去的时候,继续做比较,如果内存中的值依然是 4,说明没有其他线程处理过,第二个线程就可以把 5 回写回去了。

    流程图如下

    image

    CAS 会出现一个 ABA 的问题,即在一个线程回写值的时候,其他线程其实动过那个原始值,只不过其他线程操作后这个值依然是原始值。

    如何来解决 ABA 问题呢?

    我们可以通过版本号或者时间戳来控制,比如数据原始的版本是 1.0,处理后,我们把这个数据的版本改成变成 2.0 版本, 时间戳来控制也一样。

    以 Java 为例,AtomicStampedReference这个类,它内部不仅维护了对象值,还维护了一个时间戳。当AtomicStampedReference对应的数值被修改时,除了更新数据本身外,还必须要更新时间戳。当AtomicStampedReference设置对象值时,对象值以及时间戳都必须满足期望值,写入才会成功。因此,即使对象值被反复读写,写回原值,只要时间戳发生变化,就能防止不恰当的写入。

    代码示例

    package git.snippets.juc;
    
    import java.util.concurrent.TimeUnit;
    import java.util.concurrent.atomic.AtomicStampedReference;
    
    /**
     * @author Grey
     * @date 2022/9/10
     * @since
     */
    public class ABATest {
        public static void main(String[] args) throws InterruptedException {
            abaCorrect();
        }
    
        private static void abaCorrect() throws InterruptedException {
            AtomicStampedReference<Integer> ref = new AtomicStampedReference<>(10, 0);
            Thread threadA = new Thread(() -> {
                try {
                    int[] stamp = new int[1];
                    Integer value = ref.get(stamp); //同时获取时间戳和数据,防止获取到数据和版本不是一致的
    
                    System.out.println(String.format("%s 启动,当前值是:%s,版本:%s", Thread.currentThread().getName(), ref.getReference(), stamp[0]));
                    TimeUnit.MILLISECONDS.sleep(1000);
    
                    int newValue = value + 1;
                    boolean writeOk = ref.compareAndSet(value, newValue, stamp[0], stamp[0] + 1);
    
                    System.out.println(String.format("%s:%s,%s", Thread.currentThread().getName(), "10->11", writeOk ? stamp[0] + 1 : stamp[0]));
                    stamp = new int[1];
                    value = ref.get(stamp); //同时获取时间戳和数据,防止获取到数据和版本不是一致的
                    newValue = value - 1;
                    writeOk = ref.compareAndSet(value, newValue, stamp[0], stamp[0] + 1);
                    System.out.println(String.format("%s:%s,%s", Thread.currentThread().getName(), "10->11->10", writeOk ? stamp[0] + 1 : stamp[0]));
                } catch (InterruptedException e) {
                }
            }, "线程A");
    
            Thread threadB = new Thread(() -> {
                try {
                    int[] stamp = new int[1];
                    Integer value = ref.get(stamp); //同时获取时间戳和数据,防止获取到数据和版本不是一致的
    
                    System.out.println(String.format("%s 启动,当前值是:%s,版本:%s", Thread.currentThread().getName(), ref.getReference(), stamp[0]));
                    TimeUnit.MILLISECONDS.sleep(2000);
    
                    int newValue = value + 2;
                    boolean writeOk = ref.compareAndSet(value, newValue, stamp[0], stamp[0] + 1);
    
                    System.out.println(String.format("%s: index是预期的10:%s,新值是:%s,版本:%s", Thread.currentThread().getName(), writeOk, ref.getReference(), writeOk ? stamp[0] + 1 : stamp[0]));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }, "线程B");
    
            threadA.start();
            threadB.start();
    
            threadA.join();
            threadB.join();
        }
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64

    CAS 的底层调用了汇编的 LOCK_IF_MP 方法:

    lock cmpxchg
    
    • 1

    虽然cmpxchg指令不是原子的,但是加了lock指令后,则cmpxhg被上锁,不允许被打断。 在单核 CPU 中,无须加lock,在多核 CPU 中,必须加lock,可以参考 stackoverflow 上的这个回答: is-x86-cmpxchg-atomic-if-so-why-does-it-need-lock

    使用 CAS 好处

    jdk 早期是重量级别锁 ,通过0x80中断 进行用户态和内核态转换,所以效率比较低,有了 CAS 操作,大大提升了效率。

    锁升级

    过程如下:

    image

    偏向锁

    synchronized 代码段多数时间是一个线程在运行,谁先来,这个就偏向谁,用当前线程标记一下。

    轻量级锁(自旋锁,无锁)

    1. 偏向锁撤销,然后竞争,每个线程在自己线程栈中存一个LR(lock record)锁记录

    2. 偏向锁和轻量级锁都是用户空间完成的,重量级锁需要向操作系统申请。

    3. 两个线程争抢的方式将lock record的指针,指针指向哪个线程的LR,哪个线程就拿到锁,另外的线程用 CAS 的方式继续竞争

    重量级锁

    JVM 的 ObjectMonitor 去操作系统申请。

    如果发生异常,synchronized会自动释放锁,

    示例代码如下:

    package git.snippets.juc;
    
    import java.util.concurrent.TimeUnit;
    
    public class ExceptionCauseUnLock {
        /*volatile */ boolean stop = false;
    
        public static void main(String[] args) {
            ExceptionCauseUnLock t = new ExceptionCauseUnLock();
            new Thread(t::m, "t1").start();
            try {
                TimeUnit.SECONDS.sleep(4);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (t.stop) {
                int m = 1 / 0;
            }
        }
    
        synchronized void m() {
            while (!stop) {
                stop = true;
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    其中

    int m = 1 / 0;
    
    • 1

    会抛出异常,锁会自动释放。

    锁重入

    synchronized是可重入锁, 可重入次数必须记录,因为解锁需要对应可重入次数的记录。

    偏向锁:记录在线程栈中,每重入一次,LR 加 1,备份原来的markword

    轻量级锁:类似偏向锁

    重量级锁:记录在ObjectMonitor的一个字段中

    自旋锁什么时候升级为重量级锁?

    • 有线程超过十次自旋

    • -XX:PreBlockSpin(jdk1.6之前)

    • 自旋的线程超过CPU核数一半

    • jdk1.6 以后,JVM自己控制

    为什么有偏向锁启动和偏向锁未启动?

    未启动:普通对象001 已启动:匿名偏向101

    为什么有自旋锁还需要重量级锁?

    因为自旋会占用 CPU 时间,消耗 CPU 资源,如果自旋的线程多,CPU 资源会被消耗,所以会升级成重量级锁(队列)例如:ObjectMonitor里面的WaitSet,重量级锁会把线程都丢到WaitSet中冻结, 不需要消耗 CPU 资源

    偏向锁是否一定比自旋锁效率高?

    明确知道多线程的情况下,不一定。 因为偏向锁在多线程情况下,会涉及到锁撤销,这个时候直接使用自旋锁,JVM 启动过程,会有很多线程竞争,比如启动的时候,肯定是多线程的,所以默认情况,启动时候不打开偏向锁,过一段时间再打开,JVM 有一个参数可以配置:BiasedLockingStartupDelay默认是4s

    synchronized

    锁定对象

    package git.snippets.juc;
    
    /**
     * synchronized锁定对象
     *
     * @author Grey
     * @date 2021/4/15
     * @since
     */
    public class SynchronizedObject implements Runnable {
        static SynchronizedObject instance = new SynchronizedObject();
        final Object object = new Object();
        static volatile int i = 0;
    
        @Override
        public void run() {
            for (int j = 0; j < 1000000; j++) {
                // 任何线程要执行下面的代码,必须先拿到object的锁
                synchronized (object) {
                    i++;
                }
            }
        }
        public static void main(String[] args) throws InterruptedException {
            Thread t1 = new Thread(instance);
            Thread t2 = new Thread(instance);
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            System.out.println(i);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    锁定方法

    锁定静态方法相当于锁定当前类

    package git.snippets.juc;
    
    /**
     * synchronized锁定静态方法,相当于锁定当前类
     *
     * @author Grey
     * @date 2021/4/15
     * @since
     */
    public class SynchronizedStatic implements Runnable {
        static SynchronizedStatic instance = new SynchronizedStatic();
        static volatile int i = 0;
    
        @Override
        public void run() {
            increase();
        }
    
        // 相当于synchronized(SynchronizedStatic.class)
        synchronized static void increase() {
            for (int j = 0; j < 1000000; j++) {
                i++;
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            Thread t1 = new Thread(instance);
            Thread t2 = new Thread(instance);
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            System.out.println(i);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    锁定非静态方法相当于锁定该对象的实例或synchronized(this)

    package git.snippets.juc;
    
    /**
     * synchronized锁定方法
     *
     * @author Grey
     * @date 2021/4/15
     * @since
     */
    public class SynchronizedMethod implements Runnable {
        static SynchronizedMethod instance = new SynchronizedMethod();
        static volatile int i = 0;
    
        @Override
        public void run() {
            increase();
        }
        void increase() {
            for (int j = 0; j < 1000000; j++) {
                synchronized (this) {
                    i++;
                }
            }
        }
        public static void main(String[] args) throws InterruptedException {
            Thread t1 = new Thread(instance);
            Thread t2 = new Thread(instance);
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            System.out.println(i);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    脏读

    package git.snippets.juc;
    
    import java.util.concurrent.TimeUnit;
    
    /**
     * 模拟脏读
     *
     * @author Grey
     * @date 2021/4/15
     * @since
     */
    public class DirtyRead {
        String name;
        double balance;
    
        public static void main(String[] args) {
            DirtyRead a = new DirtyRead();
            Thread thread = new Thread(() -> a.set("zhangsan", 100.0));
    
            thread.start();
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(a.getBalance("zhangsan"));
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(a.getBalance("zhangsan"));
        }
    
        public synchronized void set(String name, double balance) {
            this.name = name;
    
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
    
            this.balance = balance;
        }
    
        // 如果get方法不加synchronized关键字,就会出现脏读情况
        public /*synchronized*/ double getBalance(String name) {
            return this.balance;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53

    其中的getBalance方法,如果不加synchronized,就会产生脏读的问题。

    可重入锁

    一个同步方法可以调用另外一个同步方法,
    一个线程已经拥有某个对象的锁,再次申请的时候仍然会得到该对象的锁(可重入锁)
    子类synchronized,如果调用父类的synchronize方法:super.method(),如果不可重入,直接就会死锁。

    package git.snippets.juc;
    
    import java.io.IOException;
    
    /**
     * 一个同步方法可以调用另外一个同步方法,一个线程已经拥有某个对象的锁,再次申请的时候仍然会得到该对象的锁.
     *
     * @author Grey
     * @since
     */
    public class SynchronizedReentry implements Runnable {
    
    
        public static void main(String[] args) throws IOException {
            SynchronizedReentry myRun = new SynchronizedReentry();
            Thread thread = new Thread(myRun, "t1");
            Thread thread2 = new Thread(myRun, "t2");
            thread.start();
            thread2.start();
            System.in.read();
    
        }
    
        synchronized void m1(String content) {
            System.out.println(this);
            System.out.println("m1 get content is " + content);
            m2(content);
        }
    
        synchronized void m2(String content) {
            System.out.println(this);
            System.out.println("m2 get content is " + content);
    
        }
    
        @Override
        public void run() {
            m1(Thread.currentThread().getName());
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    程序在执行过程中,如果出现异常,默认情况锁会被释放 ,所以,在并发处理的过程中,有异常要多加小心,不然可能会发生不一致的情况。比如,在一个 web app 处理过程中,多个Servlet线程共同访问同一个资源,这时如果异常处理不合适,在第一个线程中抛出异常,其他线程就会进入同步代码区,有可能会访问到异常产生时的数据。因此要非常小心的处理同步业务逻辑中的异常。

    示例代码

    package git.snippets.juc;
    
    import java.io.IOException;
    import java.util.concurrent.TimeUnit;
    
    /**
     * 程序在执行过程中,如果出现异常,默认情况锁会被释放
     * 所以,在并发处理的过程中,有异常要多加小心,不然可能会发生不一致的情况。
     * 比如,在一个web app处理过程中,多个servlet线程共同访问同一个资源,这时如果异常处理不合适,
     * 在第一个线程中抛出异常,其他线程就会进入同步代码区,有可能会访问到异常产生时的数据。
     * 因此要非常小心的处理同步业务逻辑中的异常
     */
    public class SynchronizedException implements Runnable {
        int count = 0;
    
        public static void main(String[] args) throws IOException {
            SynchronizedException myRun = new SynchronizedException();
            Thread thread = new Thread(myRun, "t1");
            Thread thread2 = new Thread(myRun, "t2");
            thread.start();
            thread2.start();
            System.in.read();
    
        }
    
        @Override
        public void run() {
            synchronized (this) {
                while (true) {
                    try {
                        TimeUnit.SECONDS.sleep(2);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("current thread is " + Thread.currentThread().getName() + " count is " + count);
                    if (count == 5) {
                        count++;
                        int m = 1 / 0;
                    }
                    count++;
                }
            }
        }
    
        synchronized void m1(String content) {
            System.out.println(this);
            System.out.println("m1 get content is " + content);
            m2(content);
        }
    
        synchronized void m2(String content) {
            System.out.println(this);
            System.out.println("m2 get content is " + content);
    
        }
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58

    synchronized 的底层实现

    在早期的 JDK 使用的是操作系统级别的重量级锁

    后来的改进锁升级的概念:

    synchronized (Object)

    • markword 记录这个线程ID (使用偏向锁)

    • 如果线程争用:升级为 自旋锁

    • 10次自旋以后,升级为重量级锁 - OS

    所以,如果

    • 执行时间短(加锁代码),线程数少,用自旋。

    • 执行时间长,线程数多,用系统锁。

    注:synchronized不能锁定String常量,Integer,Long等基础类型

    代码示例如下

    package git.snippets.juc;
    
    /**
     * synchronized不能锁定String常量,Integer,Long等基础类型
     * 

    * 不要以字符串常量作为锁定对象 * 在下面的例子中,m1和m2其实锁定的是同一个对象 * 这种情况还会发生比较诡异的现象,比如你用到了一个类库,在该类库中代码锁定了字符串“Hello”, * 但是你读不到源码,所以你在自己的代码中也锁定了"Hello",这时候就有可能发生非常诡异的死锁阻塞, * 因为你的程序和你用到的类库不经意间使用了同一把锁 * * @author Grey * @since */ public class SynchronizedBasicType implements Runnable { public static Integer i = 0; static SynchronizedBasicType instance = new SynchronizedBasicType(); static final String lock = "this is a lock"; static final String lock1 = "this is a lock"; public static void main(String[] args) throws InterruptedException { m(); Thread t1 = new Thread(instance); Thread t2 = new Thread(instance); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(i); } public static void m() throws InterruptedException { Thread m1 = new Thread(new Runnable() { @Override public void run() { /*synchronized (this)*/ synchronized (lock) { System.out.println("locked ..."); try { Thread.sleep(10000); } catch (InterruptedException e) { } System.out.println("unlocked ..."); } } }); m1.start(); Thread.sleep(1000); Thread m2 = new Thread(new Runnable() { @Override public void run() { /*synchronized (this)*/ synchronized (lock1) { System.out.println("locked lock1 ..."); System.out.println("unlocked lock1 ..."); } } }); m2.start(); m1.join(); m2.join(); } @Override public void run() { for (int j = 0; j < 10000000; j++) { synchronized (i) { i++; } } } }

    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73

    锁定某对象 o,如果 o 的属性发生改变,不影响锁的使用; 但是如果 o 指向另外一个对象,则锁定的对象发生改变, 会影响锁的使用,所以应该避免将锁定对象的引用变成另外的对象。

    package git.snippets.juc;
    
    import java.util.concurrent.TimeUnit;
    
    /**
     * 锁定某对象o,如果o的属性发生改变,不影响锁的使用
     * 但是如果o变成另外一个对象,则锁定的对象发生改变
     * 应该避免将锁定对象的引用变成另外的对象
     */
    public class SyncSameObject {
        Object object = new Object();
    
        public static void main(String[] args) {
            SyncSameObject t = new SyncSameObject();
            new Thread(t::m).start();
            Thread t2 = new Thread(t::m, "t2");
            //锁对象发生改变,所以t2线程得以执行,如果注释掉这句话,线程2将永远得不到执行机会
            t.object = new Object();
            t2.start();
        }
    
        void m() {
            synchronized (object) {
                while (true) {
                    try {
                        TimeUnit.SECONDS.sleep(2);
                        System.out.println("current thread is " + Thread.currentThread().getName());
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    以上代码,如果不执行t.object=new Object()这句,m2 线程将永远得不到执行。

    死锁

    两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去,这就是死锁现象

    死锁产生的原因主要有如下几点

    1. 系统的资源竞争

    2. 程序在执行过程中申请和释放资源的顺序不当

    死锁产生的必要条件

    1. 互斥条件:进程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。

    2. 不剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放(只能是主动释放)。

    3. 请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。

    4. 循环等待条件:存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被链中下一个进程所请求。

    模拟死锁代码

    /**
     * 模拟死锁
     */
    public class DeadLock implements Runnable {
        int flag = 1;
        static Object o1 = new Object();
        static Object o2 = new Object();
    
        public static void main(String[] args) {
            DeadLock lock = new DeadLock();
            DeadLock lock2 = new DeadLock();
            lock.flag = 1;
            lock2.flag = 0;
            Thread t1 = new Thread(lock);
            Thread t2 = new Thread(lock2);
            t1.start();
            t2.start();
    
        }
    
        @Override
        public void run() {
            System.out.println("flag = " + flag);
            if (flag == 1) {
                synchronized (o2) {
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (o1) {
                        System.out.println("1");
                    }
                }
            }
            if (flag == 0) {
                synchronized (o1) {
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (o2) {
                        System.out.println("0");
                    }
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49

    如何避免死锁?

    1、让程序每次至多只能获得一个锁。当然,在多线程环境下,这种情况通常并不现实。

    2、设计时考虑清楚锁的顺序,尽量减少嵌在的加锁交互数量。

    3、增加时限,比如使用Lock类中的tryLock方法去尝试获取锁,这个方法可以指定一个超时时限,在等待超过该时限之后便会返回一个失败信息。

    volatile

    • 保持线程之间的可见性(不保证操作的原子性),依赖 MESI 协议

    • 防止指令重排序,CPU的load fencestore fence原语支持

    CPU 原来执行指令一步一步执行,现在是流水线执行,编译以后可能会产生指令的重排序,这样可以提高性能

    关于volatile不保证原子性的代码示例

    package git.snippets.juc;
    
    /**
     * Volatile保持线程之间的可见性(不保证操作的原子性)
     *
     * @author Grey
     * @date 2021/4/19
     * @since
     */
    public class VolatileNOTAtomic {
        volatile static Data data;
    
        public static void main(String[] args) {
            Thread writer = new Thread(() -> {
                for (int i = 0; i < 10000; i++) {
                    data = new Data(i, i);
                }
            });
    
            Thread reader = new Thread(() -> {
                while (data == null) {
                }
                int a = data.a;
                int b = data.b;
                if (a != b) {
                    // 会出现这种情况是因为new Data(i,i)非原子操作,会产生中间状态的对象,导致a和b的值会不一致
                    System.out.printf("a = %s, b=%s%n", a, b);
                }
            });
            writer.start();
            reader.start();
            try {
                writer.join();
                reader.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("end");
        }
    
        public static class Data {
            int a;
            int b;
    
            Data(int a, int b) {
                this.a = a;
                this.b = b;
            }
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51

    volatile并不能保证多个线程共同修改running变量时所带来的不一致问题,也就是说volatile不能替代synchronized,

    示例代码如下:

    package git.snippets.juc;
    
    import java.util.ArrayList;
    import java.util.List;
    
    /**
     * volatile并不能保证多个线程共同修改变量时所带来的不一致问题,也就是说volatile不能替代synchronized
     *
     * @author Grey
     * @date 2021/4/19
     * @since
     */
    public class VolatileCanNotReplaceSynchronized {
        volatile int count = 0;
        int count2 = 0;
    
        public static void main(String[] args) {
            VolatileCanNotReplaceSynchronized t = new VolatileCanNotReplaceSynchronized();
            List<Thread> threads = new ArrayList<>();
            List<Thread> threads2 = new ArrayList<>();
            for (int i = 0; i < 20; i++) {
                threads.add(new Thread(t::m));
                threads2.add(new Thread(t::m2));
            }
            threads.forEach(item -> item.start());
            threads2.forEach(item -> item.start());
            threads.forEach(item -> {
                try {
                    item.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            threads2.forEach(item -> {
                try {
                    item.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            System.out.println(t.count);
            System.out.println(t.count2);
        }
    
        void m() {
            for (int i = 0; i < 1000; i++) {
                count++;
            }
        }
    
        synchronized void m2() {
            for (int i = 0; i < 1000; i++) {
                count2++;
            }
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57

    DCL 为什么一定要加 volatile?

    什么是 DCL,请参考设计模式学习笔记中的单例模式说明。

    在New对象的时候,编译完实际上是分了三步

    1. 对象申请内存,成员变量会被赋初始值

    2. 成员变量设为真实值

    3. 成员变量赋给对象

    指令重排序可能会导致2和3进行指令重排,导致下一个线程拿到一个半初始化的对象,导致单例被破坏。所以 DCL 必须加volitile

    此外,被volatile关键字修饰的对象作为类变量或实例变量时,其对象中携带的类变量和实例变量也相当于被volatile关键字修饰了

    示例代码如下

    package git.snippets.juc;
    
    import java.util.concurrent.TimeUnit;
    
    
    /**
     * 被volatile关键字修饰的对象作为类变量或实例变量时,其对象中携带的类变量和实例变量也相当于被volatile关键字修饰了
     *
     * @author Grey
     * @since 1.8
     */
    public class VolatileRef {
        volatile M tag = new M();
    
        public static void main(String[] args) {
            VolatileRef t = new VolatileRef();
            new Thread(t::m, "t1").start();
            try {
                TimeUnit.SECONDS.sleep(4);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            t.tag.n.x.stop = new Boolean(true);
        }
    
        void m() {
            while (!tag.n.x.stop) {
            }
        }
    }
    
    class M {
        N n = new N();
    }
    
    class N {
        X x = new X();
    }
    
    class X {
        public Boolean stop = new Boolean(false);
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    说明

    本文涉及到的所有代码和图例

    图例

    代码

    更多内容见:Java 多线程

    参考资料

    实战Java高并发程序设计(第2版)

    深入浅出Java多线程

    多线程与高并发-马士兵

    Java并发编程实战

    【并发编程】MESI–CPU缓存一致性协议

    【并发编程】细说并发编程的三大特性

    设计模式学习笔记

    图解Java多线程设计模式

    Java多线程:死锁

  • 相关阅读:
    2022前端面试题整理
    [护网杯 2018]easy_tornado-1|SSTI注入
    【Flink 实战系列】Flink pipeline.operator-chaining 参数使用和解析
    9月10日,每日信息差
    数据结构与算法-顺序表
    Docker 制作镜像
    GPU驱动及CUDA安装流程介绍
    Netty
    java计算机毕业设计航空机票预订系统源码+mysql数据库+系统+LW文档+部署
    Mini Linux嵌入式设备服务器
  • 原文地址:https://blog.csdn.net/hotonyhui/article/details/126799227