java并发编程1.3线程间的共享——volatile,ThreadLocal

环境:

jdk1.8

摘要说明:

上一张介绍了synchronized关键字的使用;

本章节主要讲述java内存中的一些相关概念及volatile,ThreadLocal关键字的用法;

步骤:

1.基础概念

本章节我们主要介绍java内存模型的三大特征:原子性,可见性,有序性;

  • 原子性:原子是世界上的最小单位,具有不可分割性。比如 a=0;(a非long和double类型) 这个操作是不可分割的,那么我们说这个操作时原子操作。再比如:a++; 这个操作实际是a = a + 1;是可分割的,所以他不是一个原子操作。非原子操作都会存在线程安全问题,需要我们使用同步技术(sychronized)来让它变成一个原子操作。一个操作是原子操作,那么我们称它具有原子性。java的concurrent包下提供了一些原子类,我们可以通过阅读API来了解这些原子类的用法。比如:AtomicInteger、AtomicLong、AtomicReference等。

       在 Java 中 synchronized 和在 lock、unlock 中操作保证原子性。、

       基本类型数据的访问大都是原子操作,long 和double类型的变量是64位,但是在32位JVM中,32位的JVM会将64位数据的读写操作分为2次32位的读写操作来进行,这就导致了long、double类型的变量在32位虚拟机中是非原子操作,数据有可能会被破坏,也就意味着多个线程在并发访问的时候是线程非安全的。

  • 可见性:一个线程对共享变量做了修改之后,其他的线程立即能够看到(感知到)该变量这种修改(变化)。Java内存模型是通过将在工作内存中的变量修改后的值同步到主内存,在读取变量前从主内存刷新最新值到工作内存中,这种依赖主内存的方式来实现可见性的。

       无论是普通变量还是volatile变量都是如此,区别在于:volatile的特殊规则保证了volatile变量值修改后的新值立刻同步到主内存,每次使用volatile变量前立即从主内存中刷新,因此volatile保证了多线程之间的操作变量的可见性,而普通变量则不能保证这一点。

  除了volatile关键字能实现可见性之外,还有synchronized,Lock,final也是可以的。

  使用synchronized关键字,在同步方法/同步块开始时(Monitor Enter),使用共享变量时会从主内存中刷新变量值到工作内存中(即从主内存中读取最新值到线程私有的工作内存中),在同步方法/同步块结束时(Monitor Exit),会将工作内存中的变量值同步到主内存中去(即将线程私有的工作内存中的值写入到主内存进行同步)。

  使用Lock接口的最常用的实现ReentrantLock(重入锁)来实现可见性:当我们在方法的开始位置执行lock.lock()方法,这和synchronized开始位置(Monitor Enter)有相同的语义,即使用共享变量时会从主内存中刷新变量值到工作内存中(即从主内存中读取最新值到线程私有的工作内存中),在方法的最后finally块里执行lock.unlock()方法,和synchronized结束位置(Monitor Exit)有相同的语义,即会将工作内存中的变量值同步到主内存中去(即将线程私有的工作内存中的值写入到主内存进行同步)。

  final关键字的可见性是指:被final修饰的变量,在构造函数数一旦初始化完成,并且在构造函数中并没有把“this”的引用传递出去(“this”引用逃逸是很危险的,其他的线程很可能通过该引用访问到只“初始化一半”的对象),那么其他线程就可以看到final变量的值。

  • 有序性:对于一个线程的代码而言,我们总是以为代码的执行是从前往后的,依次执行的。这么说不能说完全不对,在单线程程序里,确实会这样执行;但是在多线程并发时,程序的执行就有可能出现乱序。用一句话可以总结为:在本线程内观察,操作都是有序的;如果在一个线程中观察另外一个线程,所有的操作都是无序的。前半句是指“线程内表现为串行语义(WithIn Thread As-if-Serial Semantics)”,后半句是指“指令重排”现象和“工作内存和主内存同步延迟”现象。

       Java提供了两个关键字volatile和synchronized来保证多线程之间操作的有序性,volatile关键字本身通过加入内存屏障来禁止指令的重排序,而synchronized关键字通过一个变量在同一时间只允许有一个线程对其进行加锁的规则来实现,

        在单线程程序中,不会发生“指令重排”和“工作内存和主内存同步延迟”现象,只在多线程程序中出现。

2.volatile

基础理解:        

       Java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。

  在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,不会引起线程上下文的切换和调度,因此volatile变量是一种比sychronized关键字更轻量级的同步机制。

       从上面图片我们可以看出,当对非 volatile 变量进行读写的时候,每个线程先从内存拷贝变量到CPU缓存中。如果计算机有多个CPU,每个线程可能在不同的CPU上被处理,这意味着每个线程将变量拷贝到不同的 CPU cache 中,这样变量不能及时更新。

      使用volatile声明变量后,JVM保证每次读写是从主内存中读取,跳过CPU cache这一步;

特性:

      同时使用volatile声明变量后将具有上述所说的两种特性:

  1. 保证此变量对所有的线程的可见性,这里的“可见性”,如上所述,当一个线程修改了这个变量的值,volatile 保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。但普通变量做不到这点,普通变量的值在线程间传递均需要通过主内存来完成。
  2. .禁止指令重排序优化。有volatile修饰的变量,赋值后多执行了一个“load addl $0x0, (%esp)”操作,这个操作相当于一个内存屏障(指令重排序时不能把后面的指令重排序到内存屏障之前的位置),只有一个CPU访问内存时,并不需要内存屏障;(什么是指令重排序:是指CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理)。

       上面所说的volatile关键字并不是线程安全的,这点主要体现在复合操作时,volatile只是传递变量到主内存中,但复合操作时不具有原子性,无法保证线程安全;

使用场景:

出于简易性或可伸缩性的考虑,您可能倾向于使用 volatile 变量而不是锁。此外,volatile 变量不会像锁那样造成线程阻塞,因此也很少造成可伸缩性问题。在某些情况下,如果读操作远远大于写操作,volatile 变量还可以提供优于锁的性能优势。

使用条件

您只能在有限的一些情形下使用 volatile 变量替代锁。要使 volatile 变量提供理想的线程安全,必须同时满足下面两个条件:

  • 对变量的写操作不依赖于当前值。
  • 该变量没有包含在具有其他变量的不变式中。

实际上,这些条件表明,可以被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。

第一个条件的限制使 volatile 变量不能用作线程安全计数器。虽然增量操作(x++)看上去类似一个单独操作,实际上它是一个由(读取-修改-写入)操作序列组成的组合操作,必须以原子方式执行,而 volatile 不能提供必须的原子特性。实现正确的操作需要使x 的值在操作期间保持不变,而 volatile 变量无法实现这点。(然而,如果只从单个线程写入,那么可以忽略第一个条件。)

如下列就很好的演示了volatile关键字在复合操作下不具备原子性,不是线程安全的;


package pers.cc.curriculum1.vol;

import pers.cc.tools.SleepTools;

/**
 * @模块名:study_1_thread_basic
 * @包名:pers.cc.curriculum1.vol
 * @类名称: VolatileThread 使用volatile关键字修饰的变量在复合操作下不具有原子性
 * @类描述:【类描述】
 * @版本:1.0
 * @创建人:cc
 * @创建时间:2019年1月21日下午5:45:25
 */

public class VolatileThread {
    private static volatile Integer i = 0;

    private static Integer j = 0;

    public static class SetRunnable implements Runnable {
        public void run() {
            // 使用volatile关键字修饰的变量在复合操作下不具有原子性
            i = i + 1;
            synchronized (j) {
                j = j + 1;
            }

        }
    }

    public static class ReadRunnable implements Runnable {
        public void run() {
            System.out.println(" ReadRunnable is run i:" + i);
            System.out.println(" ReadRunnable is run j:" + j);
        }
    }

    public static void main(String[] args) {
        for (int k = 0; k < 1000; k++) {
            SetRunnable setRunnable = new SetRunnable();
            new Thread(setRunnable).start();
        }
        SleepTools.second(10);
        ReadRunnable readRunnable1 = new ReadRunnable();
        new Thread(readRunnable1).start();

    }
}

演示的结果就是i的值会每次都不一样小于1000;

故 volatile最好适用于单线程写多形成读的场景;

3.ThreadLocal

       ThreadLocal叫做线程本地变量,也有些地方叫做线程本地存储;ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

       它采用采用空间来换取时间的方式,解决多线程中相同变量的访问冲突问题。


package pers.cc.curriculum1.tLocal;

/**
 * @模块名:study_1_thread_basic
 * @包名:pers.cc.curriculum1.tLocal
 * @类名称: UseThreadLocal
 * @类描述:【类描述】 ThreadLocal叫做线程本地变量,也有些地方叫做线程本地存储;ThreadLocal为变量在每个线程中都创建了一个副本,
 *            那么每个线程可以访问自己内部的副本变量。
 * @版本:1.0
 * @创建人:cc
 * @创建时间:2019年1月29日下午5:01:45
 */

public class UseThreadLocal {
    // 定义一个ThreadLocal并定义初始方法
    static ThreadLocal < Integer > threadLocal = new ThreadLocal < Integer >() {
        protected Integer initialValue() {
            return 1;
        }
    };

    public static class TestThread implements Runnable {
        int id;

        public TestThread(int id) {
            this.id = id;
        }

        @Override
        public void run() {
            id = id + threadLocal.get();
            threadLocal.set(id);
            System.out.println("id:" + id + "  threadLocal:"
                    + threadLocal.get());
        }

    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            Thread t = new Thread(new TestThread(i));
            t.start();
        }
    }
}

  上述代码运行结果如下,可以看出各个线程之间变量互不干扰:

id:3  threadLocal:3
id:1  threadLocal:1
id:5  threadLocal:5
id:8  threadLocal:8
id:7  threadLocal:7
id:6  threadLocal:6
id:9  threadLocal:9
id:4  threadLocal:4
id:10  threadLocal:10
id:2  threadLocal:2

最常见的ThreadLocal使用场景为 用来解决 数据库连接、Session管理等。

4.源码地址

https://github.com/cc6688211/concurrent-study.git

没有更多推荐了,返回首页