java 并发 线程安全

在回顾了处理并发程序时的主要风险(例如原子性或可见性 )之后,我们将通过一些类设计来帮助我们防止上述错误。 其中一些设计导致了线程安全对象的构造,从而使我们能够在线程之间安全地共享它们。 作为示例,我们将考虑不可变和无状态的对象。 其他设计将防止不同的线程修改相同的数据,例如线程局部变量。

您可以在github上查看所有源代码。

1.不可变的对象

不可变的对象具有状态(具有表示对象状态的数据),但是它是基于构造构建的,一旦实例化了对象,就无法修改状态。

尽管线程可以交错,但是对象只有一种可能的状态。 由于所有字段都是只读的,因此没有一个线程可以更改对象的数据。 因此,不可变对象本质上是线程安全的。

产品显示了一个不变类的示例。 它在构造期间建立所有数据,并且其任何字段均不可修改:

public final class Product {private final String id;private final String name;private final double price;public Product(String id, String name, double price) {this.id = id;this.name = name;this.price = price;}public String getId() {return this.id;}public String getName() {return this.name;}public double getPrice() {return this.price;}public String toString() {return new StringBuilder(this.id).append("-").append(this.name).append(" (").append(this.price).append(")").toString();}public boolean equals(Object x) {if (this == x) return true;if (x == null) return false;if (this.getClass() != x.getClass()) return false;Product that = (Product) x;if (!this.id.equals(that.id)) return false;if (!this.name.equals(that.name)) return false;if (this.price != that.price) return false;return true;}public int hashCode() {int hash = 17;hash = 31 * hash + this.getId().hashCode();hash = 31 * hash + this.getName().hashCode();hash = 31 * hash + ((Double) this.getPrice()).hashCode();return hash;}
}

在某些情况下,将字段定为最终值还不够。 例如,尽管所有字段都是final,但MutableProduct类不是不可变的:

public final class MutableProduct {private final String id;private final String name;private final double price;private final List<String> categories = new ArrayList<>();public MutableProduct(String id, String name, double price) {this.id = id;this.name = name;this.price = price;this.categories.add("A");this.categories.add("B");this.categories.add("C");}public String getId() {return this.id;}public String getName() {return this.name;}public double getPrice() {return this.price;}public List<String> getCategories() {return this.categories;}public List<String> getCategoriesUnmodifiable() {return Collections.unmodifiableList(categories);}public String toString() {return new StringBuilder(this.id).append("-").append(this.name).append(" (").append(this.price).append(")").toString();}
}

为什么以上类别不是一成不变的? 原因是我们让引用脱离了其类的范围。 字段“ category ”是一个可变的引用,因此在返回它之后,客户端可以对其进行修改。 为了显示此,请考虑以下程序:

public static void main(String[] args) {MutableProduct p = new MutableProduct("1", "a product", 43.00);System.out.println("Product categories");for (String c : p.getCategories()) System.out.println(c);p.getCategories().remove(0);System.out.println("\nModified Product categories");for (String c : p.getCategories()) System.out.println(c);
}

和控制台输出:

Product categoriesABC
Modified Product categoriesBC

由于类别字段是可变的,并且逃脱了对象的范围,因此客户端已修改类别列表。 该产品原本是一成不变的,但已经过修改,从而进入了新的状态。

如果要公开列表的内容,可以使用列表的不可修改视图:

public List<String> getCategoriesUnmodifiable() {return Collections.unmodifiableList(categories);
}

2.无状态对象

无状态对象类似于不可变对象,但是在这种情况下,它们没有状态,甚至没有一个状态。 当对象是无状态的时,它不必记住两次调用之间的任何数据。

由于没有修改状态,因此一个线程将无法影响另一线程调用对象操作的结果。 因此,无状态类本质上是线程安全的。

ProductHandler是此类对象的示例。 它包含对Product对象的多项操作,并且在两次调用之间不存储任何数据。 操作的结果不取决于先前的调用或任何存储的数据:

public class ProductHandler {private static final int DISCOUNT = 90;public Product applyDiscount(Product p) {double finalPrice = p.getPrice() * DISCOUNT / 100;return new Product(p.getId(), p.getName(), finalPrice);}public double sumCart(List<Product> cart) {double total = 0.0;for (Product p : cart.toArray(new Product[0])) total += p.getPrice();return total;}
}

在其sumCart方法,所述ProductHandler产品列表转换成一个阵列,因为for-each循环通过它的元件使用的迭代器内部进行迭代。 列表迭代器不是线程安全的,如果在迭代过程中进行了修改,则可能引发ConcurrentModificationException 。 根据您的需求,您可以选择其他策略 。

3.线程局部变量

线程局部变量是在线程范围内定义的那些变量。 没有其他线程会看到或修改它们。

第一种是局部变量。 在下面的示例中, total变量存储在线程的堆栈中:

public double sumCart(List<Product> cart) {double total = 0.0;for (Product p : cart.toArray(new Product[0])) total += p.getPrice();return total;
}

只要考虑一下,如果您定义引用并返回它,而不是原始方法,它将逃避其范围。 您可能不知道返回的引用存储在哪里。 调用sumCart方法的代码可以将其存储在静态字段中,并允许在不同线程之间共享。

第二种类型是ThreadLocal类。 此类为每个线程提供独立的存储。 可以从同一线程内的任何代码访问存储在ThreadLocal实例中的值。

ClientRequestId类显示ThreadLocal用法的示例:

public class ClientRequestId {private static final ThreadLocal<String> id = new ThreadLocal<String>() {@Overrideprotected String initialValue() {return UUID.randomUUID().toString();}};public static String get() {return id.get();}
}

ProductHandlerThreadLocal类使用ClientRequestId在同一线程内返回相同的生成ID:

public class ProductHandlerThreadLocal {//Same methods as in ProductHandler classpublic String generateOrderId() {return ClientRequestId.get();}
}

如果执行main方法,则控制台输出将为每个线程显示不同的ID。 举个例子:

T1 - 23dccaa2-8f34-43ec-bbfa-01cec5df3258T2 - 936d0d9d-b507-46c0-a264-4b51ac3f527dT2 - 936d0d9d-b507-46c0-a264-4b51ac3f527dT3 - 126b8359-3bcc-46b9-859a-d305aff22c7e...

如果要使用ThreadLocal,则应注意在线程池化时使用它的一些风险(例如在应用程序服务器中)。 您可能会在请求之间导致内存泄漏或信息泄漏。 自从“ 如何用ThreadLocals射杀自己”一文很好地解释了这种情况的发生之后,我将不再扩展本主题。

4.使用同步

提供对对象的线程安全访问的另一种方法是通过同步。 如果我们将对引用的所有访问同步,则在给定时间只有一个线程将访问它。 我们将在后续帖子中对此进行讨论。

5.结论

我们已经看到了几种技术,可以帮助我们构建可以在线程之间安全共享的更简单的对象。 如果一个对象可以具有多个状态,则防止并发错误要困难得多。 另一方面,如果一个对象只能有一个状态或没有一个状态,则不必担心不同的线程同时访问它。

翻译自: https://www.javacodegeeks.com/2014/08/java-concurrency-tutorial-thread-safe-designs.html

java 并发 线程安全

java 并发 线程安全_Java并发教程–线程安全设计相关推荐

  1. java线程安全性_Java并发-线程安全性

    1.什么是线程安全性? 在线程安全性的定义中,最核心的就是正确性.当多线程访问调用某个类时,线程之间不会出现错误的交互,不管运行时线程如何交替执行,并且在主调代码不需要任何同步或协同,这个类都能表现出 ...

  2. c++并发编程实战_Java 并发编程实战:JAVA中断线程几种基本方法

    一个多线程Java程序,只有当其全部线程执行结束时(更具体地说,是所有非守护线程结束或者某个线程调用system.exit()方法的时候) ,才会结束运行.有时,为了终止程序或者取消一个线程对象所执行 ...

  3. java 线程工厂_Java并发编程:Java的四种线程池的使用,以及自定义线程工厂

    引言 通过前面的文章,我们学习了Executor框架中的核心类ThreadPoolExecutor ,对于线程池的核心调度机制有了一定的了解,并且成功使用ThreadPoolExecutor 创建了线 ...

  4. java统计系统线程数_Java并发(八)计算线程池最佳线程数

    目录 一.理论分析 二.实际应用 为了加快程序处理速度,我们会将问题分解成若干个并发执行的任务.并且创建线程池,将任务委派给线程池中的线程,以便使它们可以并发地执行.在高并发的情况下采用线程池,可以有 ...

  5. java等待5秒_Java并发编程-主线程等待子线程解决方案

    主线程等待所有子线程执行完成之后,再继续往下执行的解决方案 public class TestThread extends Thread { public void run() { System.ou ...

  6. java线程池_Java 并发编程 线程池源码实战

    作者 | 马启航 杏仁后端工程师.「我头发还多,你们呢?」 一.概述 笔者在网上看了好多的关于线程池原理.源码分析相关的文章,但是说实话,没有一篇让我觉得读完之后豁然开朗,完完全全的明白线程池,要么写 ...

  7. java 一个线程运行_Java并发(基础知识)—— 创建、运行以及停止一个线程

    在计算机世界,当人们谈到并发时,它的意思是一系列的任务在计算机中同时执行.如果计算机有多个处理器或者多核处理器,那么这个同时性是真实发生的:如果计算机只有一个核心处理器那么就只是表面现象. 现代所有的 ...

  8. java 线程百科_Java并发——线程介绍

    前言: 互联网时代已经发展到了现在.从以前只考虑小流量到现在不得不去考虑高并发的问题.扯到了高并发的问题就要扯到线程的问题.你是否问过自己,你真正了解线程吗?还是你只知道一些其他博客里写的使用方法.下 ...

  9. java 对象 线程安全_JAVA并发编程学习:构造线程安全的对象

    设计线程安全的类 实例限制 当一个对象被另一个对象封装时,所有访问被被封装对象的代码路径就是全部可知的,这相比于让对象可被整个系统访问来说,更容易对代码路径进行分析.将数据封装在对象内部,把对数据的访 ...

  10. java线程属性_Java 并发 线程属性

    Java 并发 线程属性 @author ixenos 线程优先级 1.每当线程调度器有机会选择新线程时,首先选择具有较高优先级的线程 2.默认情况下,一个线程继承它的父线程的优先级 当在一个运行的线 ...

最新文章

  1. 项目需求(20-30万)|人体三维动作重构
  2. Windows删除文件时显示找不到该项目
  3. ESP32 各种时钟参数值设置
  4. 如何异步的处理restful服务(基础)
  5. vant toast 指定挂载到指定位置_docker卷挂载技术
  6. 01_SpringCoud 整合SpringCoud alibaba Nacos
  7. 让Json更懂中文(JSON_UNESCAPED_UNICODE)
  8. 建立一个全数据管理的分析平台,该如何落实?
  9. 使用adb命令结束android中的进程,两种方法 kill -9 和 am force-stop的相同与区别
  10. MongoDB CookBook读书笔记之备份与恢复
  11. GNSS RTK 北斗GPS接收机多径环境测试接收机自主完好性监测实验
  12. maven pom.lastupdated
  13. ethtool 开启网卡_技术|如何使用 ethtool 命令管理以太网卡
  14. Widows Server 2012上无法安装.net framework 3.5
  15. 2021-05-26SEO关键词KPI考核指标有哪些
  16. 拒酒词、与领导喝酒的诀窍、酒量不行的技巧
  17. oppo禁用android系统通知栏,OPPO全机型手机去除状态栏ROOT警告-安卓刷机教程
  18. 总结用过的几个视频同步分离电路--LM1881
  19. 婚纱照姿势怎么摆 三大技巧帮您解决烦恼
  20. php获取当前周的起止日期,使用PHP实现获取周的起始和结束日期

热门文章

  1. U86650-群鸡乱舞【矩阵乘法】
  2. nssl1248-B【点分治,平衡树】
  3. codeforces1440 D. Graph Subset Problem
  4. 工科数学分析无穷级数总结
  5. Summer Training day4 欧拉降幂
  6. 6、oracle数据库下查询操作
  7. Spark SQL(三)之视图与执行SQL
  8. Hadoop入门(二十四)Mapreduce的求TopK程序
  9. struts+hibernate+oracle+easyui实现lazyout组件的简单案例——Jsp页面
  10. Android中ImageView的scaleType 属性说明。