可见性

  所谓可见性,指的是当一个线程修改了对象的状态后,其他线程能够看到该对象发生的变化。在单线程环境下,向某个变量写入值,然后在后面的操作再读取,在这个过程中该变量的值对该线程来说总是可见。但是,在多线程环境下,可见性就不一定等到保证,例如,对于一个共享变量 share = 0 来说,线程1和线程2都进行share++ 操作,但是最终share 的结果并不一定是2。先看看一段代码

public class NoVisibility {

   private static boolean ready;

   private static int number;

   private static class ReaderThread extends Thread{
public void run() {
while (!ready) {
Thread.yield(); //当前线程从运行态->就绪态,重新竞争cpu
}
System.out.println(number);
}
} public static void main(String[] args) {
new ReaderThread().start();
number = 42;
ready = true;
}
}

  上面的 NoVisibility 可能会一直循环下去(虽然这种情况发生的概率很小),因为 ReaderThread 线程一直看不到主线程对ready的更新;还有另一种情况是输出结果可能是0,有人会问有输出说明 ready 已经被更新为 true,那么 number 也应该被更新了42,看起来是这样,但在jvm执行指令时会出现“指令重排序”的现象。

  “指令重排序”指的是处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。对于上面的代码,readynumber 谁先赋值对最终的程序结果并没有影响(对于main线程来说),故在实际执行可能先对 ready 赋值。但是要注意的是如果后面的语句对前面的语句存在依赖关系时,则不会发生“指令重排序”,例如

int a = 2;    //语句1
int r = 2; //语句2
a = a + 3; //语句3
r = a*a; //语句4

语句4 的 r 依赖于语句3的 a 的处理结果,故执行时语句4不能在语句3之前执行

  在上面 NoVisibility 产生错误的原因是缺乏同步使得 ReadTread 线程读取了失效数据。另外有一点,对于绝大多数基本变量的读取和写入都是原子操作,但是64位的数值变量除外(如long,double),这是因为对于64位的变量,jvm允许将64位的读或写操作分解为两个32位操作,当对该变量读和写操作在不同线程进行,有可能会读取到当前值的高32位和重新赋值后的低32位,如线程1去读 long num 这个变量,还未开始读取,这时线程2对num这个变量重新赋值,先对低32位进行更新,还未更新高32位,这时线程1继续执行读取操作,于是线程1就读取了原来的高32位和更新后的低32位。

  那么怎么使得不同线程不会读取到失效数据呢?一种简单的方式是加上内置锁,这样使得某一线程在尚未执行完同步代码块前,其他的线程无法执行同步代码块,这样就保证了每个线程都能看到共享变量的最新值;另外的一种方式是把共享变量用 volatile 修饰,线程在读取volatile 变量时总是会返回最新写入的值,关于volatile更多详细请看下面参考链接https://www.cnblogs.com/dolphin0520/p/3920373.html

不过这里有一个点要注意,volatile 虽然保证了变量的可见性,但并不保证原子性,这也是为什么用 volatile 修饰的 int i 变量执行 i++ 操作仍不能保证线程安全,其实这要从i++ 这个操作的原理来讲,i++包括三个操作:1、取 i 值;2、将 i+1 存入 tmp;3、i = tmp。在多线程对 valotile 修饰的 i 进行++操作时,先假设线程1执行完第2步后堵塞这时线程2对 i 进行了更新,通知其他线程内存中的 i 已经被更新,你们应该重新取 i 值,但线程1在第3步并不需要取 i 值,而是将 tmp 值存入i。所以在使用volatile关键字是应该要记住它并不保证原子性。

线程封闭

  什么是线程封闭?当我们访问共享变量时,通常要使用同步,一种避免使用同步的方式就是不共享数据。很显然仅在单线程内访问数据,就不需要同步,这种避免共享数据的技术就是线程封闭。在java中,较常用到的线程封闭技术是栈封闭和使用 ThreadLocal 类。

  栈封闭:我的理解就是使用线程内部的局部变量。

  ThreadLocal 类:这个要仔细讲讲,ThreadLocal 类为每个线程保存了一份独立的副本,每次线程执行 ThreadLocalgetset 方法都是每次以当前线程为参数去取当前线程对象里的 ThreadLocalMap,而 ThreadLocalMap 保存着以 ThreadLocal 对象为 key 的键值对,这样就使得每个线程访问 ThreadLocal 变量互不干扰。先来看看ThreadLocal类怎么使用。

public class Test {
private static ThreadLocal<Connection> conn
= new ThreadLocal<Connection>() {
// 重写 ThreadLocal类里的initialValue()方法
public Connection initialValue() {
try {
return DriverManager.getConnection("DB_URL");//取得某一数据库连接
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
}; public static Connection getConnection() {
// 调用conn对象的get方法
return conn.get();
}
}

下面看看ThreadLocal类的源码

先看看 get 方法

public ThreadLocal() {

}
public T get() {
Thread t = Thread.currentThread(); /*
* 查看当前线程t有没有相应的map,注意,该方法传入的参数为当前线程,
* 返回的是线程t的静态变量 threadLocals,该变量初始值为null,故对不同
* 线程来说,每个线程都有自己的threadLocals
*/
ThreadLocalMap map = getMap(t);
/*
*     ThreadLocalMap getMap(Thread t) {
*      return t.threadLocals;
*     }
*/
if (map != null) {
// 获取当前线程下对象的value,注意,每个线程都存有当前对象的value
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 如果线程是第一次调用ThreadLocal的get方法,那么返回值从重写的初始化方法得到
return setInitialValue();
}

去看看 setInitialValue() 方法

private T setInitialValue() {
// 初始化value
T value = initialValue();
Thread t = Thread.currentThread();
// 取当前线程的map
ThreadLocalMap map = getMap(t);
if (map != null)
// 不为空,更新当前线程下该对象的value
map.set(this, value);
else
// map为空,创建map
createMap(t, value);
return value;
}

看看 ThreadLocal 怎么创建 ThreadLocalMap

// 这个方法是使得ThreadLocal类保存线程本地变量的关键,它新建的ThreadLocalMap是以当前ThreadLocal对象为key,然后是存在该线程的静态变量threadLocals里。

void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
} ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
// 对于table里的引用,每次都是new出来了,故不会和其他线程指向同一个当前对象
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}

看到这里,相信大家就已经大概明白ThreadLocal类线程封闭的具体原理了。

以上内容如有不当之处,请指出。谢谢!

参考链接:

https://www.cnblogs.com/dolphin0520/p/3920407.html

https://www.cnblogs.com/dolphin0520/p/3920373.html

java并发编程可见性与线程封闭的更多相关文章

  1. Java并发编程系列-(2) 线程的并发工具类

    2.线程的并发工具类 2.1 Fork-Join JDK 7中引入了fork-join框架,专门来解决计算密集型的任务.可以将一个大任务,拆分成若干个小任务,如下图所示: Fork-Join框架利用了 ...

  2. 【java并发编程实战】-----线程基本概念

    学习Java并发已经有一个多月了,感觉有些东西学习一会儿了就会忘记,做了一些笔记但是不系统,对于Java并发这么大的"系统",需要自己好好总结.整理才能征服它.希望同仁们一起来学习 ...

  3. 【Java并发编程一】线程安全和共享对象

    一.什么是线程安全 当多个线程访问一个类时,如果不用考虑这些线程在运行时环境下的调度和交替执行,并且不需要额外的同步及在调用代码代码不必作其他的协调,这个类的行为仍然是正确的,那么称这个类是线程安全的 ...

  4. Java并发编程:进程和线程的由来(转)

    Java多线程基础:进程和线程之由来 在前面,已经介绍了Java的基础知识,现在我们来讨论一点稍微难一点的问题:Java并发编程.当然,Java并发编程涉及到很多方面的内容,不是一朝一夕就能够融会贯通 ...

  5. 【Java并发编程六】线程池

    一.概述 在执行并发任务时,我们可以把任务传递给一个线程池,来替代为每个并发执行的任务都启动一个新的线程,只要池里有空闲的线程,任务就会分配一个线程执行.在线程池的内部,任务被插入一个阻塞队列(Blo ...

  6. Java并发编程扩展(线程通信、线程池)

    之前我说过,实现多线程的方式有4种,但是之前的文章中,我只介绍了两种,那么下面这两种,可以了解了解,不懂没关系. 之前的文章-->Java并发编程之多线程 使用ExecutorService.C ...

  7. Java并发编程(01):线程的创建方式,状态周期管理

    本文源码:GitHub·点这里 || GitEE·点这里 一.并发编程简介 1.基础概念 程序 与计算机系统操作有关的计算机程序.规程.规则,以及可能有的文件.文档及数据. 进程 进程是计算机中的程序 ...

  8. Java并发编程(02):线程核心机制,基础概念扩展

    本文源码:GitHub·点这里 || GitEE·点这里 一.线程基本机制 1.概念描述 并发编程的特点是:可以将程序划分为多个分离且独立运行的任务,通过线程来驱动这些独立的任务执行,从而提升整体的效 ...

  9. java并发编程实战之线程安全性(一)

    1.1什么是线程安全性 要对线程安全性给出一个确切的定义是非常复杂的.最核心的概念就是正确性.正确性:某个类的行为与其规范完全一致.在良好的规范中通常会定义各种不变性条件来约束对象的状态,以及定义各种 ...

随机推荐

  1. CTF--web 攻防世界web题 robots backup

    攻防世界web题 robots https://adworld.xctf.org.cn/task/answer?type=web&number=3&grade=0&id=506 ...

  2. HTTP协议7之Cookie--转

    Cookie是HTTP协议中非常重要的东西, 之前拜读了Fish Li 写的[细说Cookie], 让我学到了很多东西.Fish的这篇文章写得太经典了. 所以我这篇文章就没有太多内容了. 最近我打算写 ...

  3. Java虚拟机—Java8内存模型(整理版)

    1.概述 对于Java程序员来说,在虚拟机自动内存管理机制的帮助下,不再需要手动释放内存,不容易出现内存泄露和内存溢出问题.一旦出现内存泄露和溢出方面的问题,如果不了解虚拟机是怎样使用内存的,排查错误 ...

  4. nginx 容器反向代理网址的设置

    先讲一下场景:  nginx 容器要和SpringBoot 容器部署在一台机器上, nginx 为 SpringBoot 提供反向代理, 需要在 nginx.conf 中写上 SpringBoot 的 ...

  5. UOJ #450「集训队作业2018」复读机

    UOJ #450 题意 有$ k$台复读机,每时每刻有且只有一台复读机进行复读 求$ n$时刻后每台复读机的复读次数都是$ d$的倍数的方案数 $ 1\leq d \leq 3,k \leq 5·10 ...

  6. 在NOARCHIVELOG和ARCHIVELOG模式之间选择

    本节介绍在选择以NOARCHIVELOG或ARCHIVELOG模式运行数据库时必须考虑的问题,并包含以下主题: 在NOARCHIVELOG模式下运行数据库 在ARCHIVELOG模式下运行数据库 是否 ...

  7. 2018-2019-2 网络对抗技术 20165325 Exp3 免杀原理与实践

    2018-2019-2 网络对抗技术 20165325 Exp3 免杀原理与实践 实验内容(概要) 一.正确使用msf编码器,msfvenom生成如jar之类的其他文件,veil-evasion,自己 ...

  8. springboo+nginx测试反向代理01

    操作环境:centos7,springboot2.1,nginx1.8.1 boot程序链接地址 : https://github.com/zgq7/nginxDemo nginx下载地址: http ...

  9. VMware workstation 上克隆CentOS 6.x 系统后网卡无法启动的问题

    在日常学习中,我们往往没有足够的物理机资源来搭建多节点的实验环境,一个比较好的解决方案就是利用虚拟机来模拟物理机完成实验. 这样一来,多节点操作系统的部署就可以利用VMware 自带的系统“克隆”功能 ...

  10. 主席树——求区间[l,r]不同数字个数的模板(向左密集 D-query)

    主席树的另一种用途,,(还有一种是求区间第k大,区间<=k的个数) 事实上:每个版本的主席树维护了每个值最后出现的位置 这种主席树不是以权值线段树为基础,而是以普通的线段树为下标的 /* 无修改 ...