前景提要: 本篇文章只是入门,目的在于在脑海中构建一个Java运行的模型,然后可以在平时写代码时对是否引发线程安全问题有感知。
可以将计算机简单看作两部分:
计算机运行
:
CPU 执行一条条指令
,每条指令完成对数据的操控例如move(移动,一个数据移动到另一个地方)…当计算机有多个任务怎么处理
?
有一个调度系统,对每个任务分一个时间片,在时间内执行这个任务,然后再执行其他任务
,这其中有一个任务切换叫做上下文切换,上下文切换切换是由需要消耗时间的什么是进程?
进程,顾名思义是一段进行中的程序
,上述提到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的平台无关性
VM虚拟机
运行起来的时候是一个进程
栈
,栈对应了方法,执行一个方法时,会形成一个栈帧放进栈中,栈帧里存放的是方法中的局部变量数据。方法执行开始到执行完毕对应了栈帧入栈到出栈的过程。递归和方法调用也是借助了栈。栈是线程私有的,其中方法内的数据也是私有的
。堆
,存放对象数据的地方。这个是共享的
栈是私有的如果放在方法里这个对象是线程安全的
(排除对象作为参数和返回值引发的线程安全问题),如果放在方法外可能不是线程安全的线程安不安全主要看是引用放在了什么位置,放在栈是线程安全的,放在堆是线程不安全的
。基本数据类型存放的是本身的值
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();
}
}
}
解释:
对于成员变量的线程安全性分析,主要看父类元素是否线程安全
。对于静态变量,在多线程开发环境中,一定是需要去手动保证线程安全的
。总之,线程安全问题最终还是会回到是否共享的,但是是否共享是一个很宽泛的概念,包含了很多情况,在很多时候可能并不会注意到,通过这篇文章呢可以帮助我们在平时写程序的时候对于线程安全问题有明显的感知。