2

Java 多线程:锁(一) - Grey Zeng

 2 years ago
source link: https://www.cnblogs.com/greyzeng/p/16678272.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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 <a href="mailto:[email protected]">Grey</a>
 * @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();
    }

}

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

lock cmpxchg

虽然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;
        }
    }
}
int m = 1 / 0;

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

锁重入#

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 <a href="mailto:[email protected]">Grey</a>
 * @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);
    }
}

锁定方法#

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

package git.snippets.juc;

/**
 * synchronized锁定静态方法,相当于锁定当前类
 *
 * @author <a href="mailto:[email protected]">Grey</a>
 * @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);
    }
}

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

package git.snippets.juc;

/**
 * synchronized锁定方法
 *
 * @author <a href="mailto:[email protected]">Grey</a>
 * @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);
    }
}

脏读#

package git.snippets.juc;

import java.util.concurrent.TimeUnit;

/**
 * 模拟脏读
 *
 * @author <a href="mailto:[email protected]">Grey</a>
 * @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;
    }
}

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

可重入锁#

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

package git.snippets.juc;

import java.io.IOException;

/**
 * 一个同步方法可以调用另外一个同步方法,一个线程已经拥有某个对象的锁,再次申请的时候仍然会得到该对象的锁.
 *
 * @author <a href="mailto:[email protected]">Grey</a>
 * @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());
    }
}

程序在执行过程中,如果出现异常,默认情况锁会被释放 ,所以,在并发处理的过程中,有异常要多加小心,不然可能会发生不一致的情况。比如,在一个 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);

    }

}

synchronized 的底层实现#

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

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

synchronized (Object)

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

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

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

所以,如果

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

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

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

代码示例如下

package git.snippets.juc;

/**
 * synchronized不能锁定String常量,Integer,Long等基础类型
 * <p>
 * 不要以字符串常量作为锁定对象
 * 在下面的例子中,m1和m2其实锁定的是同一个对象
 * 这种情况还会发生比较诡异的现象,比如你用到了一个类库,在该类库中代码锁定了字符串“Hello”,
 * 但是你读不到源码,所以你在自己的代码中也锁定了"Hello",这时候就有可能发生非常诡异的死锁阻塞,
 * 因为你的程序和你用到的类库不经意间使用了同一把锁
 *
 * @author <a href="mailto:[email protected]">Grey</a>
 * @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++;
            }
        }
    }
}

锁定某对象 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();
                }
            }
        }
    }
}

以上代码,如果不执行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、增加时限,比如使用Lock类中的tryLock方法去尝试获取锁,这个方法可以指定一个超时时限,在等待超过该时限之后便会返回一个失败信息。

volatile#

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

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

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

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

package git.snippets.juc;

/**
 * Volatile保持线程之间的可见性(不保证操作的原子性)
 *
 * @author <a href="mailto:[email protected]">Grey</a>
 * @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;
        }
    }
}

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

示例代码如下:

package git.snippets.juc;

import java.util.ArrayList;
import java.util.List;

/**
 * volatile并不能保证多个线程共同修改变量时所带来的不一致问题,也就是说volatile不能替代synchronized
 *
 * @author <a href="mailto:[email protected]">Grey</a>
 * @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++;
        }
    }
}

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 <a href="mailto:[email protected]">Grey</a>
 * @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);
}

说明#

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

图例

代码

更多内容见:Java 多线程

参考资料#

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

深入浅出Java多线程

多线程与高并发-马士兵

Java并发编程实战

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

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

设计模式学习笔记

图解Java多线程设计模式

Java多线程:死锁


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK