Java中的线程安全问题

线程安全的本质:多个线程同时竞争性的访问同一个数据的时候(同时读写同一个数据的时候),可能会出现安全问题;只要有多线程出现的情况就一定存在线程安全问题。

不会出现线程安全的情况

每个线程执行自己的任务,没有线程安全问题

举个栗子:

现在有一个任务类MyTask

public class MyTask implements Runnable{
    private final  int Id;

    public MyTask(int id) {
        Id = id;
    }

    @Override
    public void run() {
        int n = 0;
        for (int i = 0; i < 100; i++) {
            n++;
            n++;
        }
        IO.println("第"+ Id +"号任务:"+n);

    }
}

测试类:

public class Test01 {
    static void main() {
        for (int i = 0; i < 5; i++) {
            MyTask task = new MyTask(i);
            Thread t1 = new Thread(task);
            t1.start();
        }
        IO.println("Main线程结束");
    }
}

运行后不会出现线程安全问题,因为每个线程执行的都是自己的任务

所以:即便在多线程环境下,也不一定出现线程安全问题

多个线程执行同一个任务,也不存在线程安全问题

保持MyTask类不变,将测试类中的new MyTask对象放到for循环外面:

public class Test01 {
    static void main() {
        MyTask task = new MyTask(100);
        for (int i = 0; i < 10; i++) {
            Thread t1 = new Thread(task);
            t1.start();
        }
        IO.println("Main线程结束");
    }
}

运行结果:

Main线程结束
第100号任务:200
第100号任务:200
第100号任务:200
第100号任务:200
第100号任务:200
第100号任务:200
第100号任务:200
第100号任务:200
第100号任务:200
第100号任务:200

每个线程执行run方法时都和别的线程没有任何关系,每个线程拥有自己的变量,变量存在一个独立的栈中,与其他线程互不影响,即局部变量永远不会出现竞争问题

出现线程安全问题的情况

将MyTask类中for循环中定义的n删除,将n定义为类的一个属性

public class MyTask implements Runnable{
    private final  int Id;
    private int n;

    public MyTask(int id) {
        Id = id;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            n++;
            n++;
        }
        IO.println("第"+ Id +"号任务:"+n);

    }
}

此时n是存放在堆里面的,不是放在栈里,不是局部变量。

测试类:

public class Test01 {
    static void main() {
        MyTask task = new MyTask(100);
        for (int i = 0; i < 10; i++) {
            Thread t1 = new Thread(task);
            t1.start();
        }
        IO.println("Main线程结束");
    }
}

此时,十个线程共同对n进行操作,执行结果:

Main线程结束
第100号任务:800
第100号任务:1200
第100号任务:1400
第100号任务:1600
第100号任务:2000
第100号任务:1000
第100号任务:1800
第100号任务:600
第100号任务:200
第100号任务:400

演示线程安全问题

修改MyTask类,添加一个本不应该出现的条件:

public class MyTask implements Runnable{
    private final  int Id;
    private int n;

    public MyTask(int id) {
        Id = id;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            n++;
            n++;

            if(n%2 == 1){
                IO.println("这应该是不可能发生的");
            }
        }

        IO.println("第"+ Id +"号任务:"+n);

    }
}

for循环内每次循环n都自增2,理论上if条件永远不可能成立。

测试类和上文保持不变,运行后有一定的概率会出现如下结果:

Main线程结束
这应该是不可能发生的
第100号任务:994
第100号任务:1594
第100号任务:1794
第100号任务:1194
第100号任务:200
第100号任务:794
第100号任务:604
第100号任务:1394
第100号任务:1994
第100号任务:400

在此时出现了本不应该出现的结果,此结果完全随机、不可预料、不可干预

如何避免线程安全问题

最基本的方法是不要出现共享资源,但在实际生产环境中难以做到。

解决线程安全可以加锁(同步锁)

同步锁

同步锁不是Java所独有的,是所有编程语言共同有的特性;

加锁是把安全放在第一位的,而不是把效率放在第一位的,加锁的方式效率并不高。

加锁使用的是synchronized关键字,

需要注意的是,在Java中几乎所有的对象都可以当锁,但是必须保证每个线程都能访问到这把锁。

示例1:以静态变量为锁,所有线程共⽤⼀把锁。

一般使用0个元素的byte数组当做锁:

private final byte[] lock = new byte[0];
//同步锁语法:同步块
synchronized(lock){

}

所以,修改上文中提到的MyTask类,为会产成线程冲突的代码块加锁:

public class MyTask implements Runnable{
    private final  int Id;
    private int n;//所有线程共享读写
    private final byte[] lock = new byte[0];//锁:所有线程可见

    public MyTask(int id) {
        Id = id;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {

            //同步锁语法:同步块
            synchronized(lock){
                n++;
                n++;

                if(n%2 == 1){
                    IO.println("这应该是不可能发生的");
                }
            }

        }

        IO.println("第"+ Id +"号任务:"+n);

    }
}

一旦加锁,当其中一个线程在执行加锁的代码块儿时,其他线程就只能阻塞等待。

THE END
文章版权归Tinsur.cn所有,不允许任何形式的转载
点赞0 分享