1. 线程不安全的情况
a) 竞态条件,也就是先检查后执行、查询-修改-更新等复合操作
举例如下:
public class Test {
private int i=0;
public void test()throws Exception{
Thread.sleep(1000);
i=i+1;
System.out.println("i值:"+i);
}
public static void main(String[]args){
final Testt=new Test();
for(int j=0;j<50;j++){
new Thread(){
public void run(){
try{
t.test();
}catch (Exception e){
e.printStackTrace();
}
}
}.start();
}
}
}
执行结果:
i值:4
i值:7
i值:6
i值:8
i值:10
i值:10
i值:8
i值:8
i值:12
i值:15
i值:15
本来期望的是每执行一次i能+1,但是结果却显示了相同的数据。i=i+1就是竞态条件,它这边是先取得i值,然后再为i值加1,然后再为i赋值。
b) 各线程调用共享的实例变量或静态变量
上面竞态条件的举例说明中其实也包含了调用共享的实例变量,在没有同步的情况,各个线程调用共享变量,会造成变量的不确定性。
c) 构造时的this逸出
在这儿我们得要先了解发布和逸出的概念。
发布:指的是将对象能够被当前范围之外的代码所使用。比如把对象当参数传递,方法return返回该对象,这些操作都能使该对象被外部使用。
逸出:指的是错误的发布情况。对象的创建过程还未结束,就被发布出去使用。
逸出举例如下:
public class Test {
private int i=0;
public Test(){
new Thread(new TestRunnable()).start();
//这儿模拟构造函数长时间执行
for(int j=0;j<=1000000;j++){
}
i=1;
}
public class TestRunnable implements Runnable{
@Override
public void run(){
System.out.println(Test.this.i);
}
}
public static void main(String[] args){
new Test();
}
}
这儿的结果,i可能是1也可能是0,这样就造成线程不安全。
2. 线程安全的情况
a) 无状态的对象
没有状态的对象,也就是没有共享变量的对象,不共享变量,线程始终都是安全的。
b) 线程封闭
所有的变量都在线程或者方法中,其它的线程无法使用这个变量,这种情况线程始终都是安全的。
c) 不可变对象
是指创建对象实例后,不可更改其变量。它的设计原则如下:
(一) 类添加final修饰符,保证类不被继承
(二) 保证所有变量必须私有,并且加上final修饰符
(三) 不提供改变变量的方法,包括setter
(四) 在初始化构造变量时,进行深拷贝
看下面的例子:
/**
* Created by
* Date : 2018/7/9 11:40
* 构造引用变量时,要进地深拷贝
*/
public class Test {
private final int[] intArray;
public Test(int[] paramArray){
this.intArray=paramArray;
}
public static voidmain(String[] args){
int[]paramArray=new int[]{1,2,3};
Test t=new Test(paramArray);
int[] testArray=t.getIntArray();
for(int param:testArray){
System.out.println("变更paramArray之前的值:"+param);
}
for(int i=0;i<paramArray.length;i++){
paramArray[i]=4;
}
for(int param:testArray){
System.out.println("变更paramArray之后的值:"+param);
}
}
public int[] getIntArray() {
return intArray;
}
}
打印的结果:
变更paramArray之前的值:1
变更paramArray之前的值:2
变更paramArray之前的值:3
变更paramArray之后的值:4
变更paramArray之后的值:4
变更paramArray之后的值:4
上面的对象的变量已经被改变了。所以我们要进行深拷贝,将构造方法中的
this.intArray=paramArray;改为this.intArray=paramArray.clone();
执行后的结果为:
变更paramArray之前的值:1
变更paramArray之前的值:2
变更paramArray之前的值:3
变更paramArray之后的值:1
变更paramArray之后的值:2
变更paramArray之后的值:3
(五) 在将象发布到外部时,比如getter方法,进行深拷贝,原理同第(四)条
特别要提醒的是虽然通过以上的原则可以设计出不可变对象,但是如果通过反射的话还是能让对象的变量变为可变的。
比如下面的例子:
/**
* Created by
* Date : 2018/7/9 11:55
* String不可变对象通过反射使其可变
*/
public class Test1 {
public static void main(String[]args)throws Exception{
String s="xiao xin";
System.out.println("s="+s);
Field strField=String.class.getDeclaredField("value");
strField.setAccessible(true);
char[] sChar=(char[])strField.get(s);
sChar[4]='_';
System.out.println("s="+s);
}
}
打印的结果为:
s=xiaoxin
s=xiao_xin
d) 将对象封装在线程安全的对象中
比如封装在Vector等线程安全的对象中,但是这种如果出现竞态条件的话也是不安全的。
举例如下:
/**
* Created by
* Date : 2018/7/9 10:33
* 线程安全的类出现竞态条件也是不安全的
*/
public class Test {
final static Vector<Integer> vector=new Vector<Integer>();
public Test(){
vector.add(1);
vector.add(2);
vector.add(3);
vector.add(4);
vector.add(5);
vector.add(6);
vector.add(7);
vector.add(8);
vector.add(9);
vector.add(10);
}
public static void main(String[]args){
final Testt=new Test();
new Thread(){
public void run(){
try{
t.test();
}catch(Exception e){
e.printStackTrace();
}
}
}.start();
for(int j=0;j<9;j++){
final int k=j;
new Thread(){
public void run(){
vector.remove(k);
}
}.start();
}
}
public void test()throws Exception{
int size=vector.size();
for(int i=0;i<size;i++){
TimeUnit.SECONDS.sleep(1);
doSometing(vector.get(i));
}
}
public void doSometing(Integer i){
System.out.println(i);
}
}
上面一个线程在跑Vector的循环,另一个线程在删除Vector的元素,这样就会造成”Array index out of range”的异常。