热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

并发编程11线程安全策略之线程封闭

该方法中的局部变量都会被拷贝一份儿到线程栈中;方法中的局部变量都会被拷贝一份到线程的栈中(Java内存模型);Step4.Controller层调用通过RequestHolder.


文章目录


  • 脑图
  • 概述
  • 线程封闭的三种方式
  • 示例

    • 堆栈封闭
    • ThreadLocal
    • Step1. ThreadLocal操作类
    • Step2. 自定义过滤器
    • Step3. 注册拦截器,配置拦截规则
    • Step4. Controller层调用
    • Step5. 测试
  • 代码





脑图



概述

在上篇博文并发编程-10线程安全策略之不可变对象 ,我们通过介绍使用线程安全的不可变对象可以保证线程安全。

除了上述方法,还有一种办法就是:线程封闭。


线程封闭的三种方式


  • Ad-hoc 线程封闭 ,完全由程序控制实现,不可控,不要使用

  • 堆栈封闭 方法中定义局部变量。不存在并发问题

堆栈封闭其实就是方法中定义局部变量。不存在并发问题。


多个线程访问一个方法的时候,方法中的局部变量都会被拷贝一份到线程的栈中(Java内存模型),所以局部变量是不会被多个线程所共享的。


局部变量的固有属性之一就是封闭在线程中。它们位于执行线程的栈中,其他线程无法访问这个栈。

Java虚拟机栈 请参考以前的博文 地址如下: https://blog.csdn.net/yangshangwei/article/details/52833342#java%E8%99%9A%E6%8B%9F%E6%9C%BA%E6%A0%88-java-virtual-machine-stacks


  • ThreadLocal 线程封闭 将可变数据通过每个线程有自己的独立副本从而实现线程封闭的机制

ThreadLocal类:线程本地变量。如果将变量使用ThreadLocal来包装,那么每个线程往这个ThreadLocal中读写都是线程隔离的,互相之间不会影响。


它提供了一种将可变数据通过每个线程有自己的独立副本从而实现线程封闭的机制。


Thread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,也就是说每个线程有一个自己的ThreadLocalMap。


ThreadLocalMap有自己的独立实现,可以简单地将它的key视作ThreadLocal,value为代码中放入的值(实际上key并不是ThreadLocal本身,而是它的一个弱引用)。


每个线程在往某个ThreadLocal里set值的时候,都会往自己的ThreadLocalMap里存,get也是以某个ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。


示例


堆栈封闭

多个线程访问一个方法,该方法中的局部变量都会被拷贝一份儿到线程栈中。所以局部变量是不被多个线程所共享的,也就不会出现并发问题。所以能用局部变量就别用全局的变量,全局变量容易引起并发问题。

局部变量,没啥好说的 ,直接看ThreadLocal实现线程安全吧


ThreadLocal

假设我们将用户信息放到ThreadLocal中,然后从ThreadLocal中获取该用户信息。 这个例子中的场景不是很严谨,仅仅仅是为了演示ThreadLocal的用法

这里我们通过拦截器(过滤器也行) ,【如果过滤器和拦截器不清楚的话,建议先看下我之前写的博文: Spring Boot2.x-12 Spring Boot2.1.2中Filter和Interceptor 的使用 】在调用Controller之前 ,重写拦截器的preHandle方法,通常情况下在该方法中从session中获取user信息,将写入到ThreadLocal, 重写afterCompletion方法不管是方法执行正常还是异常都会执行该方法,在该方法中移除threadlocal中的值,否则累计太多容易造成内溢出。


Step1. ThreadLocal操作类

通常情况下都要具备三个方法 add get remove 。特别是remove,否则容易造成内存溢出

package com.artisan.example.threadLocal;
import lombok.extern.slf4j.Slf4j;
/** * 通常情况下都要具备三个方法 add get remove * 特别是remove,否则容易造成内存泄漏 * @author yangshangwei * */
@Slf4j
public class RequestHolder {
private final static ThreadLocal USER_HOLDER = new ThreadLocal();

public static void addCurrentUser(ArtisanUser artisanUser) {
// 将当前线程作为key, artisanUser作为value 存入ThreadLocal类的ThreadLocalMap中
USER_HOLDER.set(artisanUser);
log.info("将artisanUser:{} 写入到ThreadLocal",artisanUser.toString());
}

public static ArtisanUser getCurrentUser() {
// 通过当前线程这个key ,获取存放在当前线程的ThreadLocalMap变量中的value
ArtisanUser artisanUser = USER_HOLDER.get();
log.info("从ThreadLocal中获取artisanUser:{}",artisanUser.toString());
return artisanUser;

}

public static void removeCurrentUser() {
log.info("从ThreadLocal中移除artisanUser:{}", getCurrentUser());
// 通过当前线程这个key获取当前线程的ThreadLocalMap,从中移除value
USER_HOLDER.remove();
}

}

Step2. 自定义过滤器

在过滤器中 ,重写对应的方法 添加 和 移除 threadLocal

package com.artisan.interceptors;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.checkerframework.checker.index.qual.LengthOf;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import com.artisan.example.threadLocal.ArtisanUser;
import com.artisan.example.threadLocal.RequestHolder;
import lombok.extern.slf4j.Slf4j;
/** * 实现 Handlerlnterceptor接口,覆盖其对应的方法即完成了拦截器的开发 * * @author yangshangwei * */
@Slf4j
public class MyInterceptor implements HandlerInterceptor {
/** * preHandle在执行Controller之前执行 * 返回true:继续执行处理器逻辑,包含Controller的功能 * 返回false:中断请求 * * 处理器执行前方法 */
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
log.info("MyInterceptor-处理器执行前方法preHandle,返回true则不拦截后续的处理");

// 模拟user存在session中
ArtisanUser user = new ArtisanUser();
user.setName("artisan");
user.setAge(20);
request.getSession().setAttribute("user", user);

// 将用户信息添加到ThreadLocal中
RequestHolder.addCurrentUser((ArtisanUser)request.getSession().getAttribute("user"));

return true;
}
/** * postHandle在请求执行完之后渲染ModelAndView返回之前执行 * * 处理器处理后方法 */
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {

}
/** * afterCompletion在整个请求执行完毕后执行,无论是否发生异常都会执行 * * 处理器完成后方法 * * * 在这个方法中移除当前用户 */
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
log.info("MyInterceptor-处理器完成后方法afterCompletion");
RequestHolder.removeCurrentUser();
}
}

Step3. 注册拦截器,配置拦截规则

注册拦截器,配置拦截规则

package com.artisan.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import com.artisan.interceptors.MyInterceptor;
/** * 实现 WebMvcConfigurer 接 口, 最后覆盖其addInterceptors方法进行注册拦截器 * @author yangshangwei * */
// 标注为配置类
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册拦截器到 Spring MVC 机制, 然后 它会返 回 一个拦截器注册
InterceptorRegistration regist = registry.addInterceptor(new MyInterceptor());
// 指定拦截匹配模式,限制拦截器拦截请求
regist.addPathPatterns("/artisan/threadLocal/*");

}
}

Step4. Controller层调用

通过RequestHolder.getCurrentUser() 获取存到ThreadLocal中的user信息

package com.artisan.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.artisan.example.threadLocal.ArtisanUser;
import com.artisan.example.threadLocal.RequestHolder;
@RestController
@RequestMapping("/artisan/threadLocal")
public class ThreadLocalTestController {

@GetMapping("/getCurrentUser")
public ArtisanUser getCurrentUser() {
return RequestHolder.getCurrentUser();
}
}

Step5. 测试

启动Spring Boot 工程,打开postman,请求

http://localhost:8080/artisan/threadLocal/getCurrentUser

postman 或者浏览器

控制层可以直接通过RequestHold这个threalocal封装类直接获取到存放在ThreadLocal中的变量信息,说明OK。

观察后台日志:


代码

https://github.com/yangshangwei/ConcurrencyMaster


推荐阅读
  • 本文介绍了在Python3中如何使用选择文件对话框的格式打开和保存图片的方法。通过使用tkinter库中的filedialog模块的asksaveasfilename和askopenfilename函数,可以方便地选择要打开或保存的图片文件,并进行相关操作。具体的代码示例和操作步骤也被提供。 ... [详细]
  • 本文介绍了如何在给定的有序字符序列中插入新字符,并保持序列的有序性。通过示例代码演示了插入过程,以及插入后的字符序列。 ... [详细]
  • 关键词:Golang, Cookie, 跟踪位置, net/http/cookiejar, package main, golang.org/x/net/publicsuffix, io/ioutil, log, net/http, net/http/cookiejar ... [详细]
  • 本文介绍了在多平台下进行条件编译的必要性,以及具体的实现方法。通过示例代码展示了如何使用条件编译来实现不同平台的功能。最后总结了只要接口相同,不同平台下的编译运行结果也会相同。 ... [详细]
  • Go GUIlxn/walk 学习3.菜单栏和工具栏的具体实现
    本文介绍了使用Go语言的GUI库lxn/walk实现菜单栏和工具栏的具体方法,包括消息窗口的产生、文件放置动作响应和提示框的应用。部分代码来自上一篇博客和lxn/walk官方示例。文章提供了学习GUI开发的实际案例和代码示例。 ... [详细]
  • Go Cobra命令行工具入门教程
    本文介绍了Go语言实现的命令行工具Cobra的基本概念、安装方法和入门实践。Cobra被广泛应用于各种项目中,如Kubernetes、Hugo和Github CLI等。通过使用Cobra,我们可以快速创建命令行工具,适用于写测试脚本和各种服务的Admin CLI。文章还通过一个简单的demo演示了Cobra的使用方法。 ... [详细]
  • r2dbc配置多数据源
    R2dbc配置多数据源问题根据官网配置r2dbc连接mysql多数据源所遇到的问题pom配置可以参考官网,不过我这样配置会报错我并没有这样配置将以下内容添加到pom.xml文件d ... [详细]
  • 在springmvc框架中,前台ajax调用方法,对图片批量下载,如何弹出提示保存位置选框?Controller方法 ... [详细]
  • Spring学习(4):Spring管理对象之间的关联关系
    本文是关于Spring学习的第四篇文章,讲述了Spring框架中管理对象之间的关联关系。文章介绍了MessageService类和MessagePrinter类的实现,并解释了它们之间的关联关系。通过学习本文,读者可以了解Spring框架中对象之间的关联关系的概念和实现方式。 ... [详细]
  • 在Kubernetes上部署JupyterHub的步骤和实验依赖
    本文介绍了在Kubernetes上部署JupyterHub的步骤和实验所需的依赖,包括安装Docker和K8s,使用kubeadm进行安装,以及更新下载的镜像等。 ... [详细]
  • 在Android开发中,使用Picasso库可以实现对网络图片的等比例缩放。本文介绍了使用Picasso库进行图片缩放的方法,并提供了具体的代码实现。通过获取图片的宽高,计算目标宽度和高度,并创建新图实现等比例缩放。 ... [详细]
  • 本文介绍了在开发Android新闻App时,搭建本地服务器的步骤。通过使用XAMPP软件,可以一键式搭建起开发环境,包括Apache、MySQL、PHP、PERL。在本地服务器上新建数据库和表,并设置相应的属性。最后,给出了创建new表的SQL语句。这个教程适合初学者参考。 ... [详细]
  • 云原生边缘计算之KubeEdge简介及功能特点
    本文介绍了云原生边缘计算中的KubeEdge系统,该系统是一个开源系统,用于将容器化应用程序编排功能扩展到Edge的主机。它基于Kubernetes构建,并为网络应用程序提供基础架构支持。同时,KubeEdge具有离线模式、基于Kubernetes的节点、群集、应用程序和设备管理、资源优化等特点。此外,KubeEdge还支持跨平台工作,在私有、公共和混合云中都可以运行。同时,KubeEdge还提供数据管理和数据分析管道引擎的支持。最后,本文还介绍了KubeEdge系统生成证书的方法。 ... [详细]
  • 本文介绍了Android 7的学习笔记总结,包括最新的移动架构视频、大厂安卓面试真题和项目实战源码讲义。同时还分享了开源的完整内容,并提醒读者在使用FileProvider适配时要注意不同模块的AndroidManfiest.xml中配置的xml文件名必须不同,否则会出现问题。 ... [详细]
  • 本文介绍了包的基础知识,包是一种模块,本质上是一个文件夹,与普通文件夹的区别在于包含一个init文件。包的作用是从文件夹级别组织代码,提高代码的维护性。当代码抽取到模块中后,如果模块较多,结构仍然混乱,可以使用包来组织代码。创建包的方法是右键新建Python包,使用方式与模块一样,使用import来导入包。init文件的使用是将文件夹变成一个模块的方法,通过执行init文件来导入包。一个包中通常包含多个模块。 ... [详细]
author-avatar
加肥的猫miao_115
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有