对象及变量的并发访问

1 并发编程准备知识

1.1 三个概念

1.1原子性

一个操作或者多个操作,要么全部执行并且执行过程不会被任何因素打断,要么就都不执行。

1.2可见性

当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能立即看得到修改的值。

1.3有序性

程序执行的顺序按照代码的先后顺序执行。

1.2 指令重排序

一般来说,处理器为了提高程序的运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。

1.2.1 Java 重排序3种类型

1)编译器优化的重排序:编译器在不改变单线程程序的语义下,可以重新安排语句的执行顺序。

2)指令级并行的重排序:现代处理器采用啦指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对于机器指令的执行顺序。

3)内存系统的重排序。由于处理器使用了缓存和读/写缓冲区,这使得加载和存储操作看上去可能在乱序执行。

第一个属于编译器重排序,第二第三个属于处理器重排序。

这些重排序可能会导致多线程程序出现内存可见性问题。

图 Java重排序3个阶段

例子:处理器对内存的读/写操作的执行顺序,不一定与内存实际发生的读/写操作顺序一致,导致内存可见性问题。

初始状态:x=y=a=b=0;

ThreadA: a=1; x=b;

ThreadB: b=2; y=a;

可能会出现的结果: x=y=0;

图 内存系统的重排序的运行情况

JVM通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证。

1.2.2 happens-before(先行发生)原则

从JDK5开始,在JVM中如果一个操作的执行结果需要对另一个操作可见,那么两个操作之间必须要存在happens-before关系,这两个操作可以在一个线程内,也可以在不同线程之间。

程序次序规则:在一个线程内,按照控制流顺序,如果操作A先行发生于操作B,那么操作A锁产生的影响对于操作B是可见的。(这个是指程序看起来执行的顺序是按照代码顺序执行的,虽然会有重排序,但是执行结果是一致的。这个规则是用来保证程序在单线程中执行结果的正确性,但无法保证程序在多线程中执行的正确性)

管程锁定原则:对于同一个锁,如果一个unlock操作先行于一个lock操作,那么改unlock操作所产生的影响对于该lock操作是可见的。

volatile变量规则:对于同一个volatile变量,如果对于这个变量的写操作先行发生于对这个变量的读操作,那么对于这个变量的写操作所产生的影响对于这个变量的读操作是可见的。(适用于多线程)

线程启动规则:对于同一个Thread对象,该对象的start()方法先行发生于此线程的每一个动作。

线程终止原则:对于一个线程,线程中发生的所有操作先行发生于对此线程的终止检测。线程中所有操作所产生的影响对于调用线程Thread.join()方法或者Thread.isAlive()方法都是可见的。

线程中断规则:对于同一个线程,对线程interrupt()方法的调用先行发生于该线程检测到中断事件的发生。interrupt()方法调用所产生的影响对于该线程检测到中断事件是可见的。

对象终结规则:对于同一个对象,它的构造方法执行结束先行发生于它的finalize()方法的开始。

传递性:如果操作A先行发生于操作B,操作B先行发生于操作C,则操作A先行发生于操作C。

2 synchronized

关键字synchronized保障啦原子性、可见性和有序性。

2.1 同步方法

方法内的变量是线程安全的。

synchronized 方法将对象作为锁。

2.1.1 锁重入

1)同一对象下,各同步方法可锁重入。

2)继承环境下,同步方法可锁重入。

3)继承环境下,不使用synchronized重写方法,则不是同步方法。

public class LockReentry {

    public synchronized void parentSync() {
        System.out.println("parentSync()");
    }

    public synchronized void method0() {
        System.out.println( Thread.currentThread().getName() + ":"  + " parent.method0()");
    }

    private static class LockReentryChildren extends LockReentry {

        public synchronized void method1() {
            System.out.println(Thread.currentThread().getName() + ":" + "method1 begin");
            this.method2();
            System.out.println(Thread.currentThread().getName() + ":" + "method1 end");
        }

        public synchronized void method2() {
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println(Thread.currentThread().getName() + ":" + "method2 begin");
            super.method0();
            System.out.println(Thread.currentThread().getName() + ":" + "method2 end");
        }

        @Override
        public void parentSync() {
            System.out.println("重写了parentSync 非同步方法");
        }
    }

    public static void main(String[] args) throws InterruptedException {

        LockReentryChildren lockReentryChildren = new LockReentryChildren();

        Thread thread1 = new Thread(() -> {
            lockReentryChildren.method1();
        });
        thread1.setName("thread1");
        Thread thread2 = new Thread(() -> {
            lockReentryChildren.method2();
        });
        thread2.setName("thread2");
        Thread thread3 = new Thread(() -> {
            lockReentryChildren.parentSync();
        });
        thread3.setName("thread3");

        thread1.start();
        thread2.start();
        TimeUnit.SECONDS.sleep(1);
        thread3.start();
    }

}

图 锁重入实例代码运行结果

2.1.2 静态同步与非静态同步

非静态同步方法的锁是这个类的特定对象;

静态同步方法的锁是这个class;

两者不是同一把锁。

public class StaticSynchronized {

    public synchronized static void staticSyncMethod() {
        System.out.println( Thread.currentThread().getName() +" 进入静态同步方法");
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println( Thread.currentThread().getName() +" 结束静态同步方法");
    }

    public synchronized void  syncMethod() {
        System.out.println( Thread.currentThread().getName() + " 进入非静态同步方法");
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println( Thread.currentThread().getName() + " 结束非静态同步方法");
    }

    public static void main(String[] args) {
        StaticSynchronized staticSynchronized = new StaticSynchronized();
        Thread thread1 = new Thread(() -> {
//            staticSynchronized.staticSyncMethod(); //或者 StaticSynchronized.staticSyncMethod()
            staticSyncMethod();
        });
        thread1.setName("thread1");
        Thread thread2 = new Thread(() -> {
            staticSynchronized.syncMethod();
        });
        thread2.setName("thread2");

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

}

图 静态与非静态同步锁实例代码运行结果

每个*.java文件都对应一个类(Class)的实例,在内存中是单例的。这个实例用于描述类的基本信息,包括有多少个字段,有多少个构造方法,有多少个普通方法等,为了减少对内存的高占用,在内存中只需保持1份C lass类的对象就行。

public class ClassSingleCase {

    public static void main(String[] args) {

        ClassSingleCase cl1 = new ClassSingleCase();
        ClassSingleCase cl2 = new ClassSingleCase();
        ClassSingleCase cl3 = new ClassSingleCase();

        System.out.println(cl1.getClass() == cl2.getClass());
        System.out.println(cl2.getClass() == cl3.getClass());
    }
}

图 类对象的单例性

2.2 同步语句块

synchronized关键字申明方法在某些情况下是具有弊端的,比如线程A调用同步方法执行一个长时间的任务,那么B线程就要等较长时间。

可以用synchronized同步语句块来增加运行效率。

2.2.1 String常量池特性与同步问题

JVM 中有一个区域叫字符串常量池,它的作用是提高性能及节约空间,相当于是一个缓冲池。 我们通常用String = “字符串”;来定义一个字符串,如果常量池没有这个字符串,就先在常量池中生成一个字符串常量。

String str1 = “abc”;

String str2 = “abc”;

static String str3 = “abc”;

String str4 = new String(“abc”);

str1 = str2=str3 ≠ str4;

public class StringSync {

    public void method(String str) {
        synchronized (str) {
            System.out.println(Thread.currentThread().getName() + " 进入方法 str=" + str);
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println(Thread.currentThread().getName() + " 执行完方法");
        }
    }

    public static void main(String[] args) {
        StringSync stringSync = new StringSync();
        Thread thread1 = new Thread(() -> {
            stringSync.method("hello");
        });
        thread1.setName("thread1");
        Thread thread2 = new Thread(() -> {
            stringSync.method("hello");
        });
        thread2.setName("thread2");
        Thread thread3 = new Thread(() -> {
            stringSync.method(new String("hello"));
        });
        thread3.setName("thread3");
        Thread thread4 = new Thread(() -> {
            stringSync.method("java");
        });
        thread4.setName("thread4");

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

图 String同步快实例代码运行结果

2.3 其他

2.3.1 出现异常,锁自动释放

public class LockUnusual {

    static synchronized void syncMethod() {
        String threadName = Thread.currentThread().getName();
        System.out.println(Thread.currentThread().getName() + "进入方法");
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            System.out.println("sleep异常");
        }
        if (threadName.equals("thread1")) {
            System.out.println(Long.parseLong("a"));
        }
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            System.out.println("sleep异常");
        }
        System.out.println("结束任务");
    }

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            syncMethod();
        });
        thread1.setName("thread1");

        Thread thread2 = new Thread(() -> {
            syncMethod();
        });
        thread2.setName("thread2");
        Thread thread3 = new Thread(() -> {
            syncMethod();
        });
        thread3.setName("thread3");

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

图 出现异常,锁自动释放

2.3.2 检测死锁

可通过visualVM工具检测出死锁。

2.3.3 锁对象改变导致异步执行

public class LockObjChange {

    String name;

    public void setName(String name) {
        this.name = name;
    }

    public void printName() {
        if (this.name == null) throw new RuntimeException("锁对象不能为空");
        synchronized (this.name) {
            System.out.println(Thread.currentThread().getName() + " 进入方法:" + this.name);
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println(Thread.currentThread().getName() + " 执行完方法");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        LockObjChange lockObjChange = new LockObjChange();

        Thread thread1 = new Thread(() -> {
            lockObjChange.setName("java");
            lockObjChange.printName();
        });
        thread1.setName("thread1");

        Thread thread2 = new Thread(() -> {
            lockObjChange.setName("c++");
            lockObjChange.printName();
        });
        thread2.setName("thread2");

        thread1.start();
        TimeUnit.SECONDS.sleep(1);
        thread2.start();

    }
}

图 锁对象改变导致异步实例代码运行结果

2.3.4 holdsLock

Thread 类下 public static native boolean holdsLock(Object obj); 检测当前线程是否持有obj的锁。

2.3.5 临界区

临界区用来表示一种公共资源或者共享数据,可以被多个线程使用,但是每一个线程使用时,一旦临界区资源被一个线程占用,那么其他线程必须等待。

3 volatile

3.1 计算机内存模型

CPU执行过程中,涉及到数据的读取和写入,而程序运行过程中的临时数据是存放在主存(物理内存)中的,而内存读写数据的速度比CPU满得多,内存和CPU不直接交互,而是通过高速缓存。

图 CPU与内存多交互

3.1.1 缓存不一致问题

如果一个变量在多个CPU中都存在缓存(一般在多线程编程时才会出现),那么就可能存在缓存不一致问题。

问题说明:初始时,两个线程分别读取变量的值存入各自所在的CPU的高速缓存中,然后线程1对变量进行操作,然后把变量最新值写入到内存。此时线程2的高速缓存中变量的值还是初始值,然后对变量进行操作,最后线程2把变量的最新值写入内存。 最终的结果不是预期结果。

3.1.2 通过缓存一致性协议解决

最出名的是MESI协议,它的核心思想是:当CPU写数据时,如果发现操作变量是共享变量,即在其他CPU中也存在该变量的副本,就会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中该变量的缓存行是无效的,那它就会从内存重新读取。

3.2 Java内存模型

Java Memory Model,JMM,是Java虚拟机规范定义的内存模型。规定所有变量都存在于主存中(相当于物理内存),每个线程都有自己的工作内存(类似于高速缓存)。线程对变量的所有操作都必须在工作内存中进行,并且每个线程不能访问其他线程的工作内存。

图 Java内存模型

3.3 剖析volatile

volatile具有可见性、原子性、禁止代码重排序。

3.3.1 可见性

1)使用volatile关键字会强制将修改的值立即写入主存;

2)使用volatile关键字,当线程发现操作变量时,会导致其他线程该变量的缓存行无效;

3)当线程发现缓存变量的缓存行无效时,会再次从主存读取该变量。

3.3.2 有序性

1)当程序进行到volatile变量的读写操作时,前面的操作更改确保全部已经完成写入,且结果对后面的操作可见;在其后面的操作确保还没进行。

2)当进行指令优化时,不能将volatile变量前面的语句放在其后面执行,也不能将volatile变量后面的语句放到其前面执行。

3.3.2 非原子性操作

volatile int i = 0;

i++ 操作并不是一个原子操作,非线程安全的。其步骤如下:

1)从内存读取i的值;

2)计算i的值, i = i + 1;

3)将i的值写到内存中。

假设线程1在第一步后被阻塞了,并且还未对i进行修改。虽然volatile能保证其他线程对i值读取是从内存中读取的,但是线程i没有修改,所以其他线程根本不会看到修改后的值。所以最后得到的结果可能会不符合预期。

自增操作非原子操作,volatile无法保证对变量的任何操作都是原子性的。

3.4 使用场景

可见性,实现一个变量的值被更改,其他线程能取到最新的值。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值