前景提要: 本篇文章只是入门,目的在于在脑海中构建一个Java运行的模型,然后可以在平时写代码时对是否引发线程安全问题有感知。
文章目录
引入
了解辨别线程安全问题之前先来构建一个计算机运行模型
可以将计算机简单看作两部分:
- CPU
- 存放数据和指令的地方
计算机运行
:
CPU 执行一条条指令
,每条指令完成对数据的操控例如move(移动,一个数据移动到另一个地方)…
这样计算机成功运行
当计算机有多个任务怎么处理
?
- 首先假设是一个一个处理,在一个任务中有普通指令和IO指令,IO指令是非常慢的,因为要去磁盘读取数据,执行IO时CPU是不工作的在等待,第一个任务遇到IO指令等待,第二个任务也等待…如果将等待IO的时间去执行其他任务这样会快很多。
- 实际上同时有多任务时,并不是将一个任务执行完,再去执行另一个,而是
有一个调度系统,对每个任务分一个时间片,在时间内执行这个任务,然后再执行其他任务
,这其中有一个任务切换叫做上下文切换,上下文切换切换是由需要消耗时间的
了解线程安全问题怎么能不知道线程和进程
了解进程和线程的概念
什么是进程?
进程,顾名思义是一段进行中的程序
,上述提到CPU是不断切换执行任务的,对于这种执行到一定部分的程序叫做进程。- 进程,是上述计算机中执行的一个个任务,由于CPU有多核,可以同时运行多个任务,也就是多个进程。
什么是线程?
- 进程是一个大任务,这个大任务想要完成就要一点点的去做,分成一个个小任务,比如要建一个房子,先要打地基,然后一层层盖,最后刷漆装修等。
进程这个大任务包含的一个个小任务就是是线程
。
线程和进程的抽象化理解
进程可以看作一家公司
,将任务分配给公司,公司是死的没办法执行任务,具体做事情的是一个个员工,这些员工就是线程
。线程是调度的基本单位
,当分给公司一个任务的时候,同时也会分完成任务所需要的资源,所以进程是资源分配的基本单位,同时各个线 程去共享进程的资源
。员工完成各自的任务需要公司的资源,比如说打印机,员工之间共享使用。- 总的来说,
进程可以看做是一个大的线程组,进程是不做事的,事情交给进程去做
。
Java代码运行的模型JVM–Java的平台无关性
了解CPU不同种类有不同指令集
-
CPU 有很多种类如AMD和Intel,这两种CPU在指令集方面是有不同的,不同的CPU厂商有一天套不是很相同的指令集,相同指令集在硬件上有不同实现
。 -
CPU就是去执行一条条指令(
指令是机器码是一串二进制数字。指令就是我们常说的机器语言,机器语言
(machine language)是一种指令集的体系。这种指令集,称机器码(machine code),是电脑的CPU可直接解读的数据)。 -
计算机去识别机器语言或汇编语言去执行一条条指令。汇编语言是人类看的懂的语言来描述指令集,汇编语言操作起来还是非常困难的,人类又发明了高级语言比较贴合人类的语言更容易理解,如C,C++,java,但是计算机看不懂高级语言的,c,c++要转化(通过编译器编译)成机器语言。
-
机器语言、汇编语言、高级语言的联系:
高级语言要通过编译程序翻译成汇编代码,汇编语言要通过汇编得到机器语言,计算机才能执行
。 -
C或C++语言可能会因为CPU等的差异会运行成不同的结果
最常见的例子是c语言中int在32,与64位下表示的范围不同。所以c,c++依赖于硬件,在不同计算机运行结果可能不同。
原因是因为C或C++会转化成机器语言,通过CPU执行,由于机器语言在不同CPU会有差异。导致C或C++在不同计算机上有差异。
java的平台无关性
Java并不是直接编译成机器语言,java通过编译器编译成字节码
。
通过虚拟器执行字节码
,这样就会不依赖于硬件,依赖于虚拟机
。
不同的计算机,不同的操作系统装不同的虚拟机,具体的硬件差异通过虚拟机来解决
,从而达到相同的执行效果,这就是java的平台无关性
分析并发性问题
java代码执行所依赖的核心区域–通过区域来分析并发问题
JVM内存区域
- J
VM虚拟机
运行起来的时候是一个进程
- JVM内存主要分为5部分,堆和方法区是共享的,其他的三部分是线程私有的
栈
,栈对应了方法,执行一个方法时,会形成一个栈帧放进栈中,栈帧里存放的是方法中的局部变量数据。方法执行开始到执行完毕对应了栈帧入栈到出栈的过程。递归和方法调用也是借助了栈。栈是线程私有的,其中方法内的数据也是私有的
。堆
,存放对象数据的地方。这个是共享的
- 大多数对象的数据放在堆里,对象的引用,如果是在方法内是放在栈里,方法外放在堆里
栈是私有的如果放在方法里这个对象是线程安全的
(排除对象作为参数和返回值引发的线程安全问题),如果放在方法外可能不是线程安全的- 引用放在什么地方,放在方法里,就是将以用放在私有的栈中,这就是线程安全的,在方法内是线程安全的,除非将这个引用交给其他方法,或从其他地方拿过来的,不能保证在其他地方线程安全,对应了引用为参数和返回值的情况。这是局部变量和方法内地引用
总的来说,线程安不安全主要看是引用放在了什么位置,放在栈是线程安全的,放在堆是线程不安全的
。
基本数据类型和引用数据类型–基本和引用数据类型的线程安全问题
分析基本数据类型和引用数据类型的传参
基本数据类型存放的是本身的值
例如,i这个变量中放的就是10
int i = 10;
-
对于
引用数据类型,放的是对象数据的地址
,通过这个地址来访问对象数据
-
基本数据类型
传参
通过调用swap(i,j)方法并不会让i,j值产生交换
,
原因是传参是复制的值,复制完以后成为了两个独立个体。
public void swap(int a,int b )
{
int t=a;
a=b;
b=t;
}
- 对于
引用数据类型
是将地址复制给对方。对方可以通过地址访问对象数据,会真正改变值
public void swap(Integer a,Integer b )
{
int t=a;
a=b;
b=t;
}
基本数据类型和引用数据类型的线程安全分析
-
对于基本数据,不管是在方法内部还是作为参数,都是线程安全的
。作为参数时,在传参的过程中,直接将值赋值给参数,方法的参数丝毫不会影响外部的变量
-
对于
引用数据类型
,作为传参和返回值的时候,传递的是地址,每个拿到地址的变量,都可随意更改。总的来说,作为传参和返回值时,线程安全取决于拿到地址的那些变量是否有线程安全问题,如果只在方法内部使用,一定线程安全
。
-
也就是说把基本数据类型,放在方法里是绝对安全的,但是要区分传参使用,和直接使用成员变量这两种情况。,
通过对象的使用–分析产生的并发问题
-
上面通过对私有区域栈和共享区域堆的分析可以确定如果放在方法里这个对象是线程安全的(排除对象作为参数和返回值引发的线程安全问题),现在通过对对象的引用来进一步分析线程安全问题
-
对一个对象的抽象理解
-
从
房子的角度
来考虑,我是房屋主人我只有一把钥匙,谁也不给,这个房子是只有我自己能进,这很明显是安全
的
如果我将钥匙复制了很多份
给了其他人,或者我的钥匙被坏人偷偷复制了一份,那么此时我的房子是不安全的
-
分辨是否会引发线程安全问题主要是看引用
,这个对象的引用是否是被其他人拿到,是那就不安全,否就是私有就是安全
举个例子:
public List test(List list)
{
方法内的具体逻辑操作
return list;
}
如果引用放在堆里,堆是共享的,所以是不安全的。这是类变量(静态),和成员变量的情况。
基本数据类型存放的是值,引用数据类型存放的是地址(根据这个地址可以找到对象数据),对象绝大多数放在堆里。
对应java是不能直接操作地址的。对于方法之间的传参,引用数据类型,相当于是一把钥匙,通过参数形式传入方法,相当于是复制了一把钥匙交给对方,对方也有了操作的权限。对于基本数据类型,方法传参,像当于直接克隆了一个崭新的给对方。在方法内操作基本数据类型是不会影响到方法外的基本数据类型的。
对于方法内的基本数据类型,可以说都是线程安全的,但是要区别下面两种情况
提一点:
对于成员变量和类变量是不分基本数据类型和引用数据类型的,这两个是否引发线程安全问题情况是一样的
对于成员变量的线程安全分析–依赖于父元素
Class Main{
private int i;
public void test(int a)
{
a++;
}
}
调用test(i)是线程安全的
Class Main{
private int i;
public void test()
{
i++;
}
}
这个是线程不安全的
从引用数据类型类比为钥匙的角度谈线程安全性:
对于方法内的局部变量,这个局部变量是内部定义的,又 不会当作返回值,这就是线程安全的。如果局部变量是参数的话,或者是返回值,相当于从别人手中拿到钥匙,或把钥匙给了别人,这就依赖于其他地方是不是线程安全的。
对于成员变量,引用是放在堆里的,他的线程安全性依赖于引用他的父元素,对于成员变量实例化后才能操作,这个线程安全性使用它的父元素,如果这个父元素只是自己使用是没有线程安全的,如果把这个引用交给多个人,就可能引发线程安全问题
举个例子: 经典的 模拟抢票
Class Tacket
{
//票的数量
private int tacket=0;
//抢票方法
public void qTacket()
{
if(tacket>=0)
tacket--;
}
}
// class MyThread extend Thread
{
private Tacker tacket;
public MyThread ()
{
super();
}
public MyThread(Tacket tacket)
{
this .tacket=tacker;
}
//重写run方法,run()方法内部是执行抢票的动作
public void run()
{
tacket.qTacket();
}
}
//开始模拟抢票过程
Class Main{
public static void main(String []args)
{
//拿到操作票的钥匙
Tacket tacket=new Tacket();
//开启10个线程去抢票,相当于把钥匙给了很多人,造成线程不安全
for(int i=0;i<10;i++)
{
new MyThread(tacket).start();
}
}
}
解释:
- 对于Tacket这个类来说,成员变量tacket无法看出是否线程安全,但是在Main类中,将tacket对象交给多个线程,在每个线程中对成员进行了读写操作,引发了线程安全问题。
- 所以,
对于成员变量的线程安全性分析,主要看父类元素是否线程安全
。
对于静态变量(类变量)
- 静态变量,不管在哪个线程,在哪个地方,可以通过方法名.变量名来访问静态变量
- 这种情况下,如果只读这并不会引发线程安全问题,
- 但是再实际过程中,绝大部分都是读写操作,这就会引发线程安全问题了
- 所以,
对于静态变量,在多线程开发环境中,一定是需要去手动保证线程安全的
。
总结
总之,线程安全问题最终还是会回到是否共享的,但是是否共享是一个很宽泛的概念,包含了很多情况,在很多时候可能并不会注意到,通过这篇文章呢可以帮助我们在平时写程序的时候对于线程安全问题有明显的感知。