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中几乎所有的对象都可以当锁,但是必须保证每个线程都能访问到这把锁。

以成员变量为锁,所有线程共⽤⼀把锁。

一般使用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);

    }
}

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

以静态变量为锁

依旧修改MyTask类

synchronized(MyTask.class){
                n++;
                n++;

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

自增操作并非原子操作

形如n++不是原子操作,所以会被进程打断,形如i+=3也不是原子操作

赋值操作原子操作,不会被打断

synchronized的另一种用法

synchronized还可以放在方法名前的修饰符上,此时同步代码块为整个方法体(即为方法内的所有的代码都加锁):

@Override
    //synchronized如果修饰普通成员方法,以this为锁,this指代当前对象
    //如果synchronized修饰类方法,以类.class为锁
    public synchronized void run() {
        for (int i = 0; i < 100; i++) {
            
                n++;
                n++;

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

        }

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

    }

synchronized如果修饰普通成员方法,以this为锁,this指代当前对象

如果synchronized修饰类方法,以 类.class为锁

常用的线程安全的类

线程安全的类

  1. 内部状态不可改变(或禁止访问)的类:String、Integer、Long、LocalDate、LocalTime、Math…
  2. 使用粗暴同步锁的类(性能损失严重):StringBuffer、Vector、Hashtable、
    Collections.synchronizedList、Collections.synchronizedSet、
    Collections.synchronizedMap…
  3. 使用特殊机制或复合机制保证线程安全的类:juc 包下的 ConcurrentHashMap、
    CopyOnWriteArrayList、ConcurrentLinkedQueue…
  4. 原子操作类(CAS 锁):juc 包下的 AtomicInteger、AtomicLong、AtomicBoolean、
    AtomicReference…

注意事项

  1. 除非遇到性能瓶颈,或者不得已的需要,否则尽量不要使用多线程。
  2. 如果一定要使用多线程,尽可能地不要共享读写数据(单纯只读是可以的,同时读写或同时写都可能会出现线程安全问题)。
  3. 如果一定要共享读写数据,要么加锁,要么使用线程安全的类。

 

THE END
文章版权归Tinsur.cn所有,转载分享请标注原链接
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容