【Java并发基础】局部变量是线程安全的
前言
方法中的变量(即局部变量)是不存在数据竞争(Data Race)的,也是线程安全的。为了理解为什么,我们先来了一下方法是如何被执行的,然后再分析局部变量的安全性,最后再介绍利用局部变量不会共享的特点而产生的解决并发问题的一些技术。
方法是如何被执行的
int a = 7;
int[] b = fibonacci(a);
int[] c = b;
以上代码转换成CPU指令执行,方法的调用过程示意图如下:(图来自参考[1])
当调用fibonacci(a)
时,CPU要先找到方法fibonacci()
的地址(在CPU堆栈寄存器中),然后跳转到这个地址去执行代码(蓝色线),最后CPU执行完方法,再返回原来调用方法的下一条语句(红色线)。
CPU找调用方法的参数和返回地址,是通过堆栈寄存器。CPU支持一种线性结构,因为与方法调用有关,所以也称为调用栈。
再举个例子,有三个方法A、B、C。方法A中调用方法B,方法B中调用方法C。那么将会构建出如下调用栈。每个方法在调用栈里都有自己的独立空间,称为栈帧。每个栈帧都有对应方法需要的参数和返回地址。当调用新方法时,会创建新的栈帧,并压入调用栈;当方法返回时,对应的栈帧就会被自动弹出。即,栈帧和方法同生共死。
三个方法生成的调用栈如上图所示。
不同的编程语言虽定义方法虽各有所异,但是它们执行方法的原理却是一致的:都是依靠栈结构解决。Java语言虽然是靠虚拟机解释执行,但是方法的调用也是利用栈结构解决的。
局部变量的存放位置
局部变量是定义在方法内,作用域也是在方法内部。当方法运行结束后,局部变量也就失效了。那么我们可以得出,局部变量的存放位置应该在调用栈中。事实上,局部变量就是存放到调用栈中的。
调用栈与线程
两个线程可以同时用不同的参数调用相同的方法,那么调用栈和线程之间是什么关系呢?答案就是:每个线程都有自己独立的调用栈。
所以,Java方法里面的局部变量是不存在并发问题的。每个线程都有自己独立的调用栈,局部变量保存在各自的调用栈中,不会被共享,自然也就没有并发问题。
利用不共享解决并发问题的技术: 线程封闭
当多线程访问没有同步的可变共享变量时就会出现并发问题,而解决方案之一便是使变量不共享。变量不会和其他变量共享,也就不会存在并发问题。仅在单线程里访问数据,不需要同步,我们称之为线程封闭。当某个对象封闭在一个线程中时,这种用法将自动实现线程安全性,即使被封闭的对象本身不是线程安全的。
采用线程封闭技术的案例非常多。例如一种常见的应用便为JDBC的Connection
对象。从数据库连接池中获取一个Connection对象,在JDBC规范中并没有要求这个Connection一定是线程安全的。数据库连接池通过线程封闭技术,保证一个Connection对象一旦被一个线程获取之后,在这个Connection对象返回之前,连接池不会将它分配给其他线程,从而保证了Connection对象不会有并发问题。
线程封闭技术的一个具体实现是我们上面提到的局部变量的使用(栈封闭),还有一种需要提一下,即ThreadLocal
类。
ThreadLoacl类
维持线程封闭性一种更规范方法是使用ThreadLocal
,这个类能使线程中的某个值与保存值的对象相关联起来。ThreadLocal提供了get()
和set()
等访问接口,这些方法为每个使用该变量的线程都存有一份独立的副本,因此get()总是返回由当前执行线程在调用set()时设置的最新值。
ThreadLocal对象通常用于防止对可变的单实例变量(Singleton)或全局变量进行共享。
例如,在单线程应用程序中可能会维持一个全局的数据库连接,并在线程启动时初始化这个连接对象,从而避免在调用每个方法时都要传递一个Connection对象。由于JDBC的连接对象不一定线程安全的,因此,当多线程应用程序在没有协同的情况下使用全局变量时,就不是线程安全的。通过将JDBC的连接保存到ThreadLocal对象中,每个线程都会拥有属于自己的连接。
如以下代码所示,利用ThreadLocal来维持线程的封闭性:(代码来自参考[2])
public class ConnectionDispenser {
static String DB_URL = "jdbc:mysql://localhost/mydatabase";
private ThreadLocal<Connection> connectionHolder
= new ThreadLocal<Connection>() {
public Connection initialValue() {
try {
return DriverManager.getConnection(DB_URL);
} catch (SQLException e) {
throw new RuntimeException("Unable to acquire Connection, e");
}
};
};
public Connection getConnection() {
return connectionHolder.get();
}
}
当某个频繁执行的操作需要一个临时对象,例如一个缓冲区,而同时又希望避免在每次执行时都重新分配该临时对象,就可以使用这项技术。例如,在Java 5.0之前,Integer.toString()
方法使用ThreadLocal对象来保存一个12字节大小的缓冲区,用于对结果进行格式化,而不是使用共享的静态缓冲区(需要使用加锁机制)或者每次调用时都分配一个新的缓冲区。
小结
知道方法是如何调用的也就明白了局部变量为什么是线程安全的。方法调用会产生栈帧,局部变量会放在栈帧的工作内存中,线程之间不共享,故不存在线程安全问题。后面我们介绍了基于不共享解决并发问题的线程封闭技术,除了不共享这种思想可以解决并发问题,还有两种:使用不可变变量和正确使用同步机制。
参考:
[1]极客时间专栏王宝令《Java并发编程实战》
[2]Brian Goetz.Tim Peierls. et al.Java并发编程实战[M].北京:机械工业出版社,2016
【Java并发基础】局部变量是线程安全的的更多相关文章
- Java并发基础03. 传统线程互斥技术—synchronized
在多个线程同时操作相同资源的时候,就会遇到并发的问题,如银行转账啊.售票系统啊等.为了避免这些问题的出现,我们可以使用synchronized关键字来解决,下面针对synchronized常见的用法做 ...
- Java并发基础01. 传统线程技术中创建线程的两种方式
传统的线程技术中有两种创建线程的方式:一是继承Thread类,并重写run()方法:二是实现Runnable接口,覆盖接口中的run()方法,并把Runnable接口的实现扔给Thread.这两种方式 ...
- Java并发基础05. 传统线程同步通信技术
先看一个问题: 有两个线程,子线程先执行10次,然后主线程执行5次,然后再切换到子线程执行10,再主线程执行5次--如此往返执行50次. 看完这个问题,很明显要用到线程间的通信了, 先分析一下思路:首 ...
- Java并发基础02. 传统线程技术中的定时器技术
传统线程技术中有个定时器,定时器的类是Timer,我们使用定时器的目的就是给它安排任务,让它在指定的时间完成任务.所以先来看一下Timer类中的方法(主要看常用的TimerTask()方法): 前面两 ...
- java并发基础(五)--- 线程池的使用
第8章介绍的是线程池的使用,直接进入正题. 一.线程饥饿死锁和饱和策略 1.线程饥饿死锁 在线程池中,如果任务依赖其他任务,那么可能产生死锁.举个极端的例子,在单线程的Executor中,如果一个任务 ...
- Java 并发基础
Java 并发基础 标签 : Java基础 线程简述 线程是进程的执行部分,用来完成一定的任务; 线程拥有自己的堆栈,程序计数器和自己的局部变量,但不拥有系统资源, 他与其他线程共享父进程的共享资源及 ...
- 【搞定 Java 并发面试】面试最常问的 Java 并发基础常见面试题总结!
本文为 SnailClimb 的原创,目前已经收录自我开源的 JavaGuide 中(61.5 k Star![Java学习+面试指南] 一份涵盖大部分Java程序员所需要掌握的核心知识.欢迎 Sta ...
- java并发基础(二)
<java并发编程实战>终于读完4-7章了,感触很深,但是有些东西还没有吃透,先把已经理解的整理一下.java并发基础(一)是对前3章的总结.这里总结一下第4.5章的东西. 一.java监 ...
- Java并发基础概念
Java并发基础概念 线程和进程 线程和进程都能实现并发,在java编程领域,线程是实现并发的主要方式 每个进程都有独立的运行环境,内存空间.进程的通信需要通过,pipline或者socket 线程共 ...
- java并发基础及原理
java并发基础知识导图 一 java线程用法 1.1 线程使用方式 1.1.1 继承Thread类 继承Thread类的方式,无返回值,且由于java不支持多继承,继承Thread类后,无法再继 ...
随机推荐
- SpringBoot入门简易教程
使用SpringBoot来开发一个简单的restful api网关功能,目标:实现对SpringBoot的简单入门. 1. 创建SpringBoot项目 可以通过spring官网(https://st ...
- 配置一个简单的nfs
一. 服务端配置 1.1 安装包 服务端基本环境Centos6.5 [root@node1 ~]# yum -y install nfs-utils rpcbind [root@node1 ~]# r ...
- 009 Ceph RBD增量备份与恢复
一.RBD的导入导出介绍 Ceph存储可以利用快照做数据恢复,但是快照依赖于底层的存储系统没有被破坏 可以利用rbd的导入导出功能将快照导出备份 RBD导出功能可以基于快照实现增量导出 二.RBD导出 ...
- 原生JS数组方法实现(一)————push()、unshift()、pop()和shift()
push 向数组末尾添加一个或多个元素,并返回数组新的长度 ```javascript function push(){ for(let i=0;i<arguments.length;i++){ ...
- iOS定位权限请求时易犯的错误小结
起因 用户群反馈app可能请求了不合适的定位权限:始终定位. 看到这个截图,根据经验判断可能是后台定位功能导致可能不得不请求始终定位权限.再加上之前提交审核时,苹果要求在plist文件中新增NSLoc ...
- $POJ1742\ Coins$ 多重背包+贪心
Vjudge传送门 $Sol$ 首先发现这是一个多重背包,所以可以用多重背包的一般解法(直接拆分法,二进制拆分法...) 但事实是会TLE,只能另寻出路 本题仅关注“可行性”(面值能否拼成)而不是“最 ...
- linux solr 安装
1.官网下载solr任意版本 2.解压安装包 3.进入solr文件bin目录修改solr.in.sh中的SOLR_ULIMIT_CHECKS属性设置为false 4.启动solr 5.检查solr状态 ...
- Word List 1 part 1
inter- 在....之间 intermediate adj. 中间的 pro- 向前,在前;很多;赞同;亲... proportion n. 比例;部分 prim- 第一,主要的 prime ad ...
- js获取当前农历时间
<template> <div class="gaia-header"> <img alt="gaia_logo" src=&qu ...
- Educational Codeforces Round 53 (Rated for Div. 2) (前五题题解)
这场比赛没有打,后来补了一下,第五题数位dp好不容易才搞出来(我太菜啊). 比赛传送门:http://codeforces.com/contest/1073 A. Diverse Substring ...