java线程三之安全与不安全

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”的异常。

猜你喜欢

转载自blog.csdn.net/wangzx19851228/article/details/80968741