欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

线程安全性问题

程序员文章站 2022-05-17 18:54:18
...

什么是线程安全?

当多个线程访问某个类,不管运行时环境采用何种调度方式或者这些线程如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类为线程安全的。-----《并发编程实战》

什么是线程不安全?

多线程并发得不到预期结果。

例:

import java.util.concurrent.CountDownLatch;
public class UnsafeThread {

    private static int num = 0;

    private static CountDownLatch countDownLatch = new CountDownLatch(10);
    /**
     * 自增
     */
    public static void inCreate(){
        num++;
    }
    
    public static void main(String[] args) {

        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                for (int j = 0; j < 100; j++) {
                    inCreate();
                    try {
                        Thread.sleep(20L);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                //每次执行完调用countDown
                countDownLatch.countDown();
            }).start();
        }

        while (true){
            if (countDownLatch.getCount() == 0){
                System.out.println(num);
                break;
            }
        }

    }

}

运行结果:

线程安全性问题

理想结果是1000,但每次运行的结果都是1000以下的其他值

线程不安全原因

num++非原子操作,

从字节码角度剖析线程不安全操作
javac -encoding UTF-8 UnsafeThread.java 编译成.class
javap -c UnsafeThread.class 进行反编译,得到相应的字节码指令

   0: getstatic     #2               获取指定类的静态域,并将其押入栈顶
   3: iconst_1						 将int型1押入栈顶
   4: iadd							 将栈顶两个int型相加,将结果押入栈顶
   5: putstatic     #2               为指定类静态域赋值
   8: return

num++被拆分成多条指令,多线程并发下可能导致同时读到了相同的num值,然后各自+1.

原子性操作

一个操作或者多个操作要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。和事务相似,要么执行完成提交更改,要么就像什么事情都没发生过一样。

操作要成功就一起成功,要失败就一起失败。

原子类有 AtomicInteger, AtomicLong等。像AtomicInteger在他的源码中有相应的get,set方法用于取值改值。

synchronized关键字使得操作具有原子性。

Synchronized关键字

内置锁

​ 每个java对象都可以用做一个实现同步的锁,这些锁称为内置锁。线程进入同步代码块或方法的时候会自动获得该锁,在退出同步代码块或方法时会释放该锁。获得内置锁的唯一途径就是进入这个锁的保护的同步代码块或方法。

互斥锁

​ 内置锁是一个互斥锁,这就是意味着最多只有一个线程能够获得该锁,当线程A尝试去获得线程B持有的内置锁时,线程A必须等待或者阻塞,直到线程B释放这个锁,如果B线程不释放这个锁,那么A线程将永远等待下去。

修饰普通方法:锁住对象的实例

修饰静态方法:锁住整个类

修饰代码块:锁住一个对象synchronized(obj)锁住括号内的内容。

Volatile关键字

只能修饰变量

保证该变量的可见性,volatile关键字仅仅保证可见性,并不保证原子性;禁止指令重排序

例子


int a,b,x,y = 0;

线程1 线程2
x=a; y=b;
b=1; a=2;
x=0;y=0;

因为上面的代码不存在数据的依赖性,因此编译器可能对指令进行重排序


线程1 线程2
b=1; a=2;
x=a; y=b;
x=2;y=1;

这样就和预想的上面写的结果不一致了,这就是导致重拍后的结果,为防止这种结果出现,volatile就规定禁止指令重排序,为保证数据的一致性。

单例与线程安全

饿汉式—本身线程安全

在类加载的时候就已经实例化,无论之后有没有用到。如果该类比较占内存,而之后有没有用到就白白的浪费了一定资源。

package com.example.single;

public class HungerSingleton {

    public static HungerSingleton outInstance = new HungerSingleton();

    public static HungerSingleton getInstance(){
        return outInstance;
    }

    private HungerSingleton(){

    }


    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                System.out.println(HungerSingleton.getInstance());
            }).start();
        }
    }
}

懒汉式单例-最简单的写法是非线程安全的

只在需要的时候才会进行实例化

package com.example.single;

public class LazySingleton {

    private static LazySingleton lazySingleton = null ;

    private LazySingleton(){

    }

    public static LazySingleton getInstance(){
        //判断实例是否为空,为空就实例化创建
        if (null == lazySingleton) {
            try {
                Thread.sleep(100L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            lazySingleton = new LazySingleton();
        }
        //不为空就直接返回
        return lazySingleton;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                System.out.println(LazySingleton.getInstance());
            }).start();
        }
    }
}

这种线程不安全,即便使用双重检查锁也依旧会有问题,对象实例化过程中jvm会进行很多操作,这时就要防止指令重排序,就要写成 private static volatile LazySingleton lazySingleton 加上volatile关键字来防止指令重排序。

这里为什么是使用双重检查锁呢?

因为在判断为null时并没有第一时间创建单例对象,而是进行了睡眠0.1秒来模拟实例化耗时,这时会有其他线程进来去判断null同样为true,这样就会new多个实例,即便把new 实例的代码使用synchronized也还是一样,因为都过了null的判断,所以要在synchronized锁住的的new实例的代码外再包一层null判断再过滤掉前面漏进来的线程。双重检查锁代码如下:

package com.example.single;

public class LazySingleton {

    private static volatile LazySingleton lazySingleton = null ;

    private LazySingleton(){

    }

    public static LazySingleton getInstance(){
        //判断实例是否为空,为空就实例化创建
        if (null == lazySingleton){
            try {
                Thread.sleep(100L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (LazySingleton.class){
                if (null == lazySingleton) {
                    lazySingleton = new LazySingleton();
                }
            }
        }
        //不为空就直接返回
        return lazySingleton;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                System.out.println(LazySingleton.getInstance());
            }).start();
        }
    }
}

如何避免线程安全性问题

线程安全问题的原因:

​ 1.多线程环境------------改为单线程

​ 2.多个线程操作同一共享资源(例.成员变量)-----------------不共享资源

​ 3.对该共享资源进行了非原子性操作----------------非原子性操作改成原子性操作(加锁、使用JDK自带的原子性操作的类、JUC提供的相应的并发工具类)

在不使用线程池每次都new线程的情况下,高并发导致总会有一些时间点获取不到线程资源导致线程饥饿,这种情况就要升级机器,或者牺牲一定的TPS改为单线程。

个人自学总结的一些东西,如有不正确的地方请大家指出,共同进步。