高并发编程:线程安全和ThreadLocal

版权声明:本文为张仕宗原创文章,允许转载,转载时请务必标明文章原始出处 。 https://blog.csdn.net/zhang5476499/article/details/83099667

线程安全的概念:当多个线程访问某一个类(对象或方法)时,这个类始终都能表现出正确的行为,那么这个类(对象或方法)就是线程安全的。

线程安全

说的可能比较抽象,下面就以一个简单的例子来看看什么是线程安全问题。

public class MyThread implements Runnable {
    private int number = 5;

    @Override
    public void run() {
        number--;
        System.out.println("线程 : " + Thread.currentThread().getName() + "获取到了公共资源,number = " + number);
    }

    public static void main(String[] args) {
        MyThread mt = new MyThread();
        Thread t1 = new Thread(mt, "t1");
        Thread t2 = new Thread(mt, "t2");
        Thread t3 = new Thread(mt, "t3");
        Thread t4 = new Thread(mt, "t4");
        Thread t5 = new Thread(mt, "t5");

        t1.start();
        t2.start();
        t3.start();
        t4.start();
        t5.start();
    }
}

Java中定一个线程有两种方式:一是继承Thread方法,二是实现Runnable接口,MyThread使用的是实现Runnable的方式来定义一个线程类。该类中有一个类变量number,初始值是5。在我new出的5个线程开启start()方法的时候,线程执行到run方法就把number减一次。代码在控制台的输出结果如下:

线程 : t1获取到了公共资源,number = 3
线程 : t3获取到了公共资源,number = 2
线程 : t2获取到了公共资源,number = 3
线程 : t4获取到了公共资源,number = 1
线程 : t5获取到了公共资源,number = 0

再次执行,得到以下结果:

线程 : t2获取到了公共资源,number = 3
线程 : t1获取到了公共资源,number = 3
线程 : t3获取到了公共资源,number = 2
线程 : t4获取到了公共资源,number = 1
线程 : t5获取到了公共资源,number = 0

从上面两个输出结果可以看出,先执行到那个线程是不确定的,而number的值更为奇怪,并不是按照5到0依次递减的。已第一次运行结果为例子,究竟是什么原因导致了程序出现数据不一致问题的可能性?下面给出了一个可能的情景,如图所示:
在这里插入图片描述
代码中创建了5个线程,t1线程启动做number–操作时,这时候t3线程抢占到CPU的执行权,t1中断,t3启动,这时候number的值等于4,t3线程在number等于4的基础上做number–操作,当t3执行完number–操作时,t1又抢到了CPU的执行权,于是对number进行输出,此时的number等于3,输出结束之后t3抢到了CPU执行权,于是t3也对number进行打印输出,于是t3线程输出的结果也是等于3。
这是多线程程序中的一个普遍问题,称为竞争状态,如果一个类的对象在多线程程序中没有导致竞争状态,则称这样的类为线程安全的。上诉的MyThread类不是线程安全的。解决的办法是给代码加锁,加锁的关键字为synchronized,synchronized可以在任意对象及方法上加锁,而加锁的这段代码称为“互斥区”或“临界区”

public class MyThread implements Runnable {
    private int number = 5;

    @Override
    public synchronized void run() {
        number--;
        System.out.println("线程 : " + Thread.currentThread().getName() + "获取到了公共资源,number = " + number);
    }

    public static void main(String[] args) {
        MyThread mt = new MyThread();
        Thread t1 = new Thread(mt, "t1");
        Thread t2 = new Thread(mt, "t2");
        Thread t3 = new Thread(mt, "t3");
        Thread t4 = new Thread(mt, "t4");
        Thread t5 = new Thread(mt, "t5");

        t1.start();
        t2.start();
        t3.start();
        t4.start();
        t5.start();
    }
}

本例为一个线程安全的线程类,无论运行多少次,都是同样的输出结果:

线程 : t1获取到了公共资源,number = 4
线程 : t2获取到了公共资源,number = 3
线程 : t4获取到了公共资源,number = 2
线程 : t3获取到了公共资源,number = 1
线程 : t5获取到了公共资源,number = 0

当多个线程访问myThread的run方法时,以排队的方式进行处理,这里的排队是按照CPU分配的先后顺序给定的,而不是按照代码的先后顺序或者线程的启动先后顺序来执行的。一个线程想要执行synchronized修改的方法里面的代码,首先是尝试获得锁,如果拿到锁,执行synchronized代码体内容;拿不到锁,这个线程就会不断的尝试获得这把锁,直到拿到为止。而且多个线程会同时去竞争这把锁,也就是会有锁竞争问题。

ThreadLocal

ThreadLocal是线程局部变量,是一种多线程间并发访问量的解决方案。与其synchronized等加锁的方式不同,ThreadLocal完全不提供锁,而使用以空间换时间的手段,为每个线程提供变量的独立副本,以保障线程安全。

public class UseThreadLocal {

    public static ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public void setThreadLocal(String value) {
        threadLocal.set(value);
    }

    public String getThreadLocal(){
        return threadLocal.get();
    }

    public static void main(String[] args) {

        UseThreadLocal utl = new UseThreadLocal();

        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                utl.setThreadLocal("张三");
                System.err.println("当前t1线程拿到的值 : " + utl.getThreadLocal());
            }
        }, "t1");


        Thread t2 = new Thread(new Runnable() {

            @Override
            public void run() {
                utl.setThreadLocal("李四");
                System.err.println("当前t2线程拿到的值 : " + utl.getThreadLocal());
            }
        }, "t2");

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t1.start();
        t2.start();
        System.err.println("主线程拿到的值 : " + utl.getThreadLocal());
    }
}

上述代码创建了3个线程,线程1向ThreadLocal里面设置值"张三",线程2向ThreadLocal里面设置值"李四"。程序的代码输出如下:

当前t1线程拿到的值 : 张三
当前t2线程拿到的值 : 李四
主线程拿到的值 : null

从程序的输出可以看出,每个线程只能打印出本线程设置的变量值。该程序存在一个共享变量threadLocal,当t1向threadLocal设置“张三”之后,取出的值自然是“张三”,接下来t2线程向threadLocal设置值“李四”之后,取出来的值自然是“李四”。有的同学可能会有疑问,说t2也许将t1之前设置的值覆盖掉了,那么请看主线程的输出,其结果为null,主线程取出的结果为空。这说明了用了ThreadLocal里面的值只存在与线程的局部变量,对其他线程具有不可见性。
那么ThreadLocal是如何实现其功能的?阅读其源码发现它用到了ThreadLocalMap,该类和HashMap一样是键值对的一种数据结构,值得注意的是虽然该类和HashMap功能类似,当时该类并没有继续自Map。

		private Entry[] table;

        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

ThreadLocal的set方法源码

    public void set(T value) {
        Thread t = Thread.currentThread(); //获取当前线程
        ThreadLocalMap map = getMap(t); /以当前线程作为key获得map容器
        if (map != null)//判断map是否为空
            map.set(this, value); //非空则把当前线程作为key,当前value作为值放进map里面
        else
            createMap(t, value);//为空则创建map
    }

下面我们再来看ThreadLocal应用场景的另一个例子,任务的同时提交。

public class MessageHolder {

    private List<String> messages = new ArrayList<>();

    private static final ThreadLocal<MessageHolder> holder = new ThreadLocal<MessageHolder>(){
        @Override
        protected MessageHolder initialValue() {
            return new MessageHolder();
        }
    };

    public static void add(String value) {
        holder.get().messages.add(value);
    }

    /**
     * 清空list,并返回删掉的list里面的值
     * @return
     */
    public static List<String> clear() {
        List<String> list = holder.get().messages;
        holder.remove();
        return list;
    }

    public static void main(String[] args) {
        MessageHolder.add("A");
        MessageHolder.add("B");
        List<String> cleared = MessageHolder.clear(); //已经被清除的list

        System.out.println("被清空掉的元素:" + cleared);
    }
}

MessageHolder类定义了add和clear方法,add方法是添加元素,clear是清空元素的方法,并返回被清楚的list集合。应用场景如下图,funtion1可能return 1,2,function2可能返回3,4,function3返回5,6,而之前的做法可能是对这三个function()累加的代码段进行加锁,这样造成A线程在访问的时候B线程只能处于等待,只有当这三个方法都执行完毕,向前端返回1,2,3,4,5,6的时候,A线程释放索,B线程才能继续使用,这样系统解决并发性就很低。

在这里插入图片描述
从性能上说,ThreadLocal不具有绝对的优势,在并发不是很高的时候,加锁的性能会更好,但作为一套与锁完全无关的线程安全解决方案,在高并发量或者竞争激烈的场景,使用ThreadLocal可以在一定程度上减少锁竞争。

猜你喜欢

转载自blog.csdn.net/zhang5476499/article/details/83099667
今日推荐