如何实现线程安全
什么是线程安全
个人理解: 多个线程同时读写某实例对象中同一数据, 可能会造成数据的不正确结果, 这就是线程不安全.
在操作数据时, 避免同一数据同一时刻被多个线程共享, 就不会造成数据的混乱, 这就是线程安全.
//线程不安全简单示例
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方法)看似是原子操作, 其实是分为三个步骤执行:
- 复制源数据, 生成一个数据副本
- 操作数据副本
- 将副本数据写入源数据
当两个线程同时读取 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" 转载请保留原文链接及作者。