Java中几种保障线程安全的设计技术 利用HashSet解决无重复字符的最长子串问题 什么是计时攻击?Spring Boot 中该如何防御? 第三届江西省高校网络安全技能大赛 部分wp&Crypto的疑惑 Hashtable和hashMap有什么区别(美的2020面试题) 在线旅游领域迎来重要监管规章——还想大数据杀熟?没门! 为经济社会发展注智赋能 中国电信物联网用户突破2亿 中关村物联网产业联盟与国教台、金丝结联手共建国际物联网平台 在大数据产业生态大会上 百分点高体伟被评为“中国数据英雄” 数读丨沿“线”布局“三核引领”大数据看山东高端装备产业发展新格局 物联网走入寻常生活 中国电信物联网用户突破2亿 赛迪白皮书:《2020中国大数据产业生态地图暨中国大数据产业发展白皮书》(可免费获取) 瓴盛科技的AIoT SoC能否撬动万亿级智慧物联网市场 中国数字经济已步入人工智能时代,给大数据杀熟戴上“紧箍咒” 程序员离职后收到原公司 2400 元,被告违反竞业协议赔 18 万 专业篇丨网络工程、信息安全、物联网工程 物联网、微服务技术专家指点你线下实操?一场不可不来的技术沙龙 | Q推荐 晋安证券杨海:物联网和人工智能趋势对当今企业的影响 山东智能工厂大数据揭秘:新一代信息技术赋能传统产业转型升级 移动互联时代:给大数据杀熟戴上“紧箍咒” 除了 Coding,程序员获得收入的四大途径! RT-Thread推物联网操作系统!300毫秒启动安防摄像机 山东启动国家质量基础设施物联网线上平台 提升产品质量水平 美亚柏科发布2020年半年报:业绩逆势增长 大数据智能化产品发展迅猛 新民快评丨对“大数据杀熟”坚决说不 【行走自贸区】福州:树立物联网产业“马尾坐标” 【行走自贸区】福州:树立物联网产业“马尾坐标 福州:树立物联网产业“马尾坐标” Golang实践录:一个数据库迁移的代码记录 Apache Derby:一款基于 Java 的嵌入式关系型数据库 centos7 安装 jdk8 (复制粘贴系列) 堡垒机Jumpserver部署 linux-linux常用命令总结二&&Linux其他网络知识&&远程拷贝以&&远程登录服务器 Linux应用程序开发笔记:配置linuxptp开机启动(ubuntu gPTP) Janus WebRTC服务器部署 【Nginx】nginx 的三种反向代理方式 OpenLDAP源码安装及配置管理 ceph客户端安装配置访问rbd 把编译好的ko文件加载模块时出错:Error: could not insert module hello_world.ko: Invalid module format 如何安装pkg-config docker文件存放路径, 获取容器启动命令 修炼js 7 es6新特性2 网页三栏布局常用方法 wordpress必须禁用REST API和移除WP-JSON链接的方法 面试官让我用纯css做一个下拉菜单,一分钟搞定!! CSS常用样式(二):绘制双箭头 住建部重申“房子是用来住的”;商务部公布禁塑时间表;在线旅游网站不得大数据杀熟 给大数据杀熟戴上“紧箍咒” RT-Thread推物联网操作系统!300秒启动安防摄像机 如何用一句话激怒程序员!
您的位置:首页 >程序人生 >

Java中几种保障线程安全的设计技术


说明:以下我主要从面向对象设计的角度出发介绍几种保障线程安全的设计技术,这些技术可以
使得我们在不必借助同步锁的情况下保障线程安全,这就避免锁可能导致的问题及其资源的开销。

文章目录

一、变量定义为局部变量二、无状态(数据)对象三、不可变对象(final)四、构建线程私有对象五、装饰器模式六、总结五种方式实现线程安全

一、变量定义为局部变量

JVM里规定,Java运行数据区划分为以下五部分(对JVM不太了解的朋友,可以看看我的这系列文章):

线程私有:Java虚拟机栈、本地方法栈、程序计数器
线程共享:堆空间、方法区(非堆)

1、Java虚拟机栈:

栈空间(Stack Space)为线程的执行准备一段固定大小的存储空间,每个线程都有独立的线程栈空间,创建线程时就为线程分配栈空间。
在线程栈中每调用一个方法就给方法分配一个栈帧,栈帧用于存储方法的局部变量、操作数栈、方法返回地址、动态链接、还有一些附加信息。即局部变量存储在栈空间中,基本类型变量也是存储在栈空间中,引用类型变量值也是存储在栈空间中,引用的对象存储在堆中。由于Java虚拟机栈是相互独立的,一个线程不能访问另外一个线程的栈空间,因此线程对局部变量以及只能通过当前线程的局部变量才能访问的对象进行的操作具有固定的线程安全性。

2、堆空间

堆空间(Heap Space)用于存储对象的。是在JVM启动时分配的一段可以动态扩容的内存空间。创建对象时在堆空间中给对象分配存储空间,实例变量就是存储在堆空间中的,堆空间是多个线程之间可以共享的空间,因此实例变量可以被多个线程共享。多个线程同时操作实例变量可能存在线程安全问题。
但是堆空间也不是吃素的,JVM在堆空间中开辟了一共占堆空间 1% 的内存大小的TLAB区域,创建线程时就会给该线程分配一段TLAB这个区域的一小部分,这样每一个线程都私有了一份TLAB,这样在对堆空间变量的引用时就是独立的了,因为TLAB是私有的。

3、方法区(非堆)

方法区空间(Non-Heap Space)用于存储常量、类的元数据、JIT编译的热点代码等,非堆空间也是在JVM启动时分配的一段可以动态扩容的存储空间。类的元数据包括静态变量、类有哪些方法、属性及这些方法的元数据(方法名、参数、返回值等)。非堆空间也是多个线程可以共享的,因此访问非堆空间中的静态变量也可能存在线程安全问题。

总结:堆空间和方法区空间是线程共享的空间,即实例变量与静态变量是线程可以共享的,可能存在线程安全问题。栈空间是线程私有的存储空间,局部变量存储在栈空间中,局部变量具有固定的线程安全性。

所以我们在开发过程中定义变量时,能够定义为局部变量的变量就尽量定义为局部变量,而不要定义为全局变量
主要有以下两个角度去分析一下原因:

(1)就像上面所说的,局部变量是在方法内部定义的,方法在调用时是会被压入Java虚拟机栈的局部变量表中,而栈是不会涉及垃圾回收的,没有引用就是直接出栈了,这样就会提高性能,减少垃圾回收的成本。

(2)当定义为局部变量,JVM在进行逃逸分析后。假如是未逃逸状态的话,有可能就会采用栈上分配策略,栈上分配对象的话,用完就直接销毁,这样也不会涉及垃圾回收(注意:Java虚拟机栈是不用垃圾回收的,垃圾回收主要是在堆空间,方法区一般是不会进行垃圾回收的,因为它的回收效率是极低的)

之前写过一篇逃逸分析,栈上分配的文章,可以看看。

二、无状态(数据)对象

对象就是数据及对数据操作的封装,对象所包含的数据称为对象的状态(State),实例变量与静态变量称为状态变量。
如果一个类的同一个实例被多个线程共享并不会使这些线程存储共享的状态,那么该类的实例就称为无状态对象(Stateless Object)。反之如果一个类的实例被多个线程共享会使这些线程存在共享状态,那么该类的实例称为有状态对象。
实际上无状态对象就是不包含任何实例变量(数据)也不包含任何静态变量的对象。线程安全问题的前提是多个线程存在共享的数据,实现线程安全的一种办法就是避免在多个线程之间共享数据,使用无状态对象就是这种方法。

简单理解就是:创建对象,但是对象里面不去申明共享数据(无状态),那么就是不会存在线程安全问题。

三、不可变对象(final)

1、不可变对象是指一经创建它的状态就保持不变的对象,不可变对象具有固有的线程安全性。当不可变对象实体的状态发生变化时,系统会创建一个新的不可变对象,就如 String 字符串对象。所以 String 是不可以动态扩容和去修改的(只会去新建一个新的 String 对象)
在这里插入图片描述

2、自定义一个不可变对象需要满足以下条件:

(1)类本身使用final修饰,防止通过创建子类来改变它的定义;
(2)所有的字段都是 final 修饰的,final 字段在创建对象时必须显示初始化并且不能被修改;
(3)如果字段引用了其他状态可变的对象(集合、数组),则这些字段必须是private私有的。
在这里插入图片描述
3、关于final 关键字的拓展:

(1)被 final 修饰的对象,在类加载阶段是直接在链接阶段中准备阶段就是被显示初始化了,它与static 修饰的对象是在同一阶段被加载的。

(2)假如被final 修饰的变量,被频繁更改,那么就会不断地创建新的对象,这样就会额外的增加垃圾回收的频率,影响性能。

(3)但是从另一个层面来说,使用final 还会降低垃圾回收,怎么说呢?我们都知道堆中有年轻代和老年代,当用年轻代中 用final 修饰的变量被老年代所引用,这个引用时间一般都会很长,因为 final 修饰的对象是随着 JVM 启动而启动,销毁而销毁的。所以一直引用着老年代的对象,那么老年代的对象就不会被垃圾回收掉,这样就降低了垃圾回收的频率。但是又拿来了一个问题,垃圾一直回收不到,内存报 OOM 的概率就提升了。所以没有好坏之分,分析问题要全面均衡一下。

4、不可变对象主要的应用场景:

(1)被建模对象的状态变化不频繁,比如说,一个数字变量、一个字符串变量…
(2)同时对一组相关数据进行写操作(一次性写入),可以应用不可变对象,既可以保障原子性也可以避免锁的使用
(3)使用不可变对象作为安全可靠的 Map 的键(key),HashMap 键值对的存储位置与键的 hashCode() 有关,如果键的内部状态发生了变化会导致键的哈希码不同,可能会影响键值对的存储位置。如果HashMap的键是一个不可变对象,则 hashCode()方法的返回值恒定,存储位置是固定的。

四、构建线程私有对象

我们可以选择不共享非线程安全的对象,对于非线程安全的对象,每个线程都创建一个该对象的实例(也就是说,谁是不安全的对象,就给每一个线程都私有分配一份),各个线程线程访问各自创建的实例,一个线程不能访问另外一个线程创建的实例。
这种各个线程创建各自的实例,一个实例只能被一个线程访问的对象就称为线程特有对象。线程特有对象既保障了对非线程安全对象的访问的线程安全,又避免了锁的开销。线程特有对象也具有固有的线程安全性。

ThreadLocal < T > 类 相当于线程访问其特有对象的代理,即各个线程通过 ThreadLocal 对象可以创建并访问各自的线程特有对象,泛型T指定了线程特有对象的类型。一个线程可以使用不同的ThreadLocal实例来创建并访问不同的线程特有对象。ThreadLocal就是为每一线程创建一个特有的副本,以此来做到数据不共享,保证线程安全问题。

在这里插入图片描述
ThreadLocal实例为每个访问它的线程都关联了一个该线程特有的对象,ThreadLocal实例都有当前线程与特有实例之间的一个关联。相当于一个 Map 的键值对,key 是一个不一样的线程,value 是当前线程特有的一些对象。

五、装饰器模式

1、装饰器模式可以用来实现线程安全,基本思想是为非线程安全的对象创建一个相应的线程安全的外包装对象,客户端代码不直接访问非线程安全的对象而是访问它的外包装对象。外包装对象与非线程安全的对象具有相同的接口,即外包装对象的使用方式与非线程安全对象的使用方式相同,而外包装对象内部通常会借助,以线程安全的方式调用相应的非线程安全对象的方法。
相当于是使用外包装对象(加锁使其是线程安全的)去包裹非线程安全的对象。

2、装饰器模式举例:在java.util.Collections工具类中提供了一组synchronizedXXX(xxx)可以把非线程安全的xxx集合转换为线程安全的集合,它就是采用了这种装饰器模式。这个方法返回值就是指定集合的外包装对象,这类集合又称为同步集合。

3、另外说一下:Collection 是List 和Set 的父接口(和 Map 接口是同一级别的),Collections 是集合(List 、Set、Map)的一个工具类。

在这里插入图片描述
Collections 是一个工具类,调用非线程安全集合的方法,加锁实现这个集合的线程安全。

SynchronizedList 的部分源码如下:
在这里插入图片描述

3、使用装饰器模式的一个好处:就是实现关注点分离,在这种设计中,实现同一组功能的对象的两个版本:非线程安全的对象与线程安全的对象。对于非线程安全的在设计时只关注要实现的功能,对于线程安全的版本只关注线程安全性。

六、总结五种方式实现线程安全

1、定义局部变量:这样做到在栈(各线程独有一份)上使用;

2、使用无状态对象:这个更狠,直接在对象里不定义数据(状态),其实这样很多时候是很难做到的;

3、使用不可变对象(final关键字):定义为不可变对象,这样多个线程只是取数据,而不修改数据;

4、创建线程特有对象:多线程不是抢资源吗,那就每一个线程都自己拥有一份,谁也不抢谁的;

5、使用装饰者模式:用一个外包装对象去包装非线程安全的对象,这样就实现了共享的是线程安全的对象。

有用点个关注,手留余香!😗 😗 😗

郑重声明:本文版权归原作者所有,转载文章仅为传播更多信息之目的,如作者信息标记有误,请第一时间联系我们修改或删除,多谢。