如何实现线程安全

  1. 什么是线程安全
  2. 为什么会造成线程不安全
  3. 如何实现线程安全
  4. 总结

什么是线程安全

个人理解: 多个线程同时读写某实例对象中同一数据, 可能会造成数据的不正确结果, 这就是线程不安全.
在操作数据时, 避免同一数据同一时刻被多个线程共享, 就不会造成数据的混乱, 这就是线程安全.

//线程不安全简单示例
public class ThreadSafeDemo {
    int index = 0;
    List<String> testList = new ArrayList<String>();
    public static void main(String[] args) throws InterruptedException {
        new ThreadSafeDemo().test();
    }

    private void test() throws InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 100000; i++) {
            executorService.submit(() -> {
                index++;
            });
            if (i < 1000) {
                executorService.submit(new TestThread(testList));
            }
        }
        executorService.shutdown();
        while (!executorService.isTerminated()) {
            Thread.sleep(100);
        }
        System.out.println("index: " + index);
        System.out.println("testList : " + testList.size());
    }
}

class TestThread implements Runnable {

    public List<String> list;

    public TestThread(List<String> list) {
        this.list = list;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            list.add("");
        }
    }
}
// 多次测试, 两个输出很大几率都不能输出100000

为什么会造成线程不安全

一个变量的赋值(i++, list 的add方法)看似是原子操作, 其实是分为三个步骤执行:

  1. 复制源数据, 生成一个数据副本
  2. 操作数据副本
  3. 将副本数据写入源数据
    当两个线程同时读取 index 时, 都读取到0, 执行index++ 之后, 又写回源数据, 这样本来应该是2 的源数据就变成了1.

如何实现线程安全

从上面的示例可以看出, 如果将共享的资源index加锁, 一次只有一个线程访问, 那么就可以解决这个问题.
从这个思路出发 可以得出:
1. 直接使用java的 synchronized, lock 等加锁 或者 AtomicInteger 等原子操作类, 让共享资源的访问从并行化变成串行化.
2. 如果是集合类型的并发操作时, 直接使用concurrent包下的 并发集合, 也能达到1的效果.

public class ThreadSafeDemo {
    int index = 0;
    ArrayBlockingQueue<String> testList = new ArrayBlockingQueue<String>(100000);
    private static final Object objLock = new Object();
    public static void main(String[] args) throws InterruptedException {
        new ThreadSafeDemo().test();
    }

    private void test() throws InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 100000; i++) {
            executorService.submit(() -> {
                synchronized (objLock) {
                    index++;
                }
            });
            if (i < 1000) {
                executorService.submit(new TestThread(testList));
            }
        }
        executorService.shutdown();
        while (!executorService.isTerminated()) {
            Thread.sleep(100);
        }
        System.out.println("index: " + index);
        System.out.println("testList : " + testList.size());
    }
}

class TestThread implements Runnable {

    public ArrayBlockingQueue<String> list;

    public TestThread(ArrayBlockingQueue<String> list) {
        this.list = list;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            list.add("");
        }
    }
}

// 输出都是正确的 100000

还有另外一种思路, 就是为每个执行的线程单独准备一份源数据, 不去一个线程中共享同一个源数据.
1. 对应的就是 ThreadLocal 类, 将每个线程的数据独立, 分别计算, 排除共享变量.

public class ThreadLocalTest {
    ThreadLocal<String> localString = new ThreadLocal<String>();

    public void set() {
        localString.set(Thread.currentThread().getName());
    }

    public String getString() {
        return localString.get();
    }

    public static void main(String[] args) throws InterruptedException {
        final ThreadLocalTest test = new ThreadLocalTest();

        test.set();
        System.out.println(test.getString());

        ExecutorService executorService = Executors.newFixedThreadPool(2);
        for (int i = 0; i < 2; i++) {
            executorService.submit(() -> {
                test.set();
                System.out.println(test.getString());
            });

        }
        executorService.shutdown();
        while (!executorService.isTerminated()) {
            Thread.sleep(100);
        }
        System.out.println(test.getString());
    }
}
// 输出: 
// main
// pool-1-thread-1
// pool-1-thread-2
// main 

总结

多线程编程中很容易引入线程安全问题, 在实际开发中必须注意.
实际开发中SimpleDataFormat不是一个线程安全的类, 所以不推荐使用静态方法的方式在多线程下访问其 parse() 和 format() 方法, 建议使用 ThreadLocal 或者加锁的方式调用.


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 zhao4xi@126.com

文章标题:如何实现线程安全

文章字数:868

本文作者:Zhaoxi

发布时间:2018-12-17, 15:01:54

最后更新:2019-09-21, 15:04:57

原始链接:http://zhao4xi.github.io/2018/12/17/如何实现线程安全/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。

目录