• [技术干货] 用Java创建和验证JWT
    本教程展示了如何使用Java JWT库(JJWT)轻松生成和验证JSON Web Tokens(JWT)。通过简单的代码示例,解释了JWT的创建、解码和验证过程,包括设置哈希算法、添加声明以及使用JUnit进行测试。 “我喜欢编写身份验证和授权代码。” 〜从来没有Java开发人员。 厌倦了一次又一次地建立相同的登录屏幕? 尝试使用Okta API进行托管身份验证,授权和多因素身份验证。 Java对JWT(JSON Web令牌)的支持过去需要进行大量工作:广泛的自定义,花费数小时来解决依赖项,以及仅用于组装简单JWT的代码页。 不再!  本教程将向您展示如何使用现有的JWT库做两件事: 生成一个JWT 解码并验证JWT 您会注意到本教程很短。 那是因为那很容易。 如果您想更深入地了解,请查看JWT规范或深入阅读有关在Spring Boot应用程序中使用JWT进行令牌身份验证的更长的文章 。 什么是JWT? JSON Web令牌是JSON对象,用于在各方之间以紧凑和安全的方式发送信息。 JSON规范 (或Javascript对象表示法)定义了一种使用键值对创建纯文本对象的方法。 这是一种构造基于原始类型(数字,字符串等)的数据的紧凑方法。 您可能已经非常熟悉JSON。 就像没有括号的XML。  令牌可用于在各方之间发送任意状态。 这里的“当事人”通常是指客户端Web应用程序和服务器。 JWT具有多种用途:身份验证机制,URL安全编码,安全共享私有数据,互操作性,数据过期等。 实际上,此信息通常与两件事有关:授权和会话状态。 服务器可以使用JWT告诉客户端应用程序允许用户执行哪些操作(或允许他们访问哪些数据)。 JWT通常还用于存储Web会话的状态相关用户数据。 因为JWT是在客户端应用程序和服务器之间来回传递的,这意味着状态数据不必存储在某个地方的数据库中(随后在每个请求中都可以检索到); 因此,它可以很好地扩展。  让我们看一个JWT示例(取自jsonwebtoken.io ) JWT具有三部分:标头,正文和签名。 标头包含有关JWT编码方式的信息。 主体是代币的肉 ( 索赔所在的地方)。 签名提供了安全性。 关于令牌的编码方式以及信息在体内的存储方式,我们这里不做很多详细介绍。 如果需要,请查看前面提到的教程 。 不要忘记:加密签名不​​提供保密性; 它们只是检测篡改JWT的一种方式,除非JWT经过专门加密,否则它们是公开可见的。 签名只是提供了一种验证内容的安全方法。 大。 得到它了? 现在,您需要使用JJWT制作令牌! 对于本教程,我们使用现有的JWT库。 Java JWT (又名JJWT)是由Les Hazlewood (Apache Shiro的主要提交人,Apache Shiro是Stormpath的前联合创始人兼CTO,现在是Okta自己的高级架构师)创建的,JJWT是一个简化JWT创建和验证的Java库。 它完全基于JWT , JWS , JWE , JWK和JWA RFC规范,并根据Apache 2.0许可的条款开源。 该库还为规范添加了一些不错的功能,例如JWT压缩和声明执行。  用Java生成令牌 这部分超级容易。 让我们看一些代码。 克隆GitHub存储库 :  git clone https://github.com/oktadeveloper/okta-java-jwt-example.git  cd okta-java-jwt-example 这个示例非常基础,并且包含一个src/main/java/JWTDemo.java类文件,其中包含两个静态方法: createJWT()和decodeJWT() 。 足够巧妙的是,这两种方法创建了一个JWT并对JWT进行解码。 看下面的第一种方法。 public static String createJWT(String id, String issuer, String subject, long ttlMillis) { //The JWT signature algorithm we will be using to sign the token SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; long nowMillis = System.currentTimeMillis(); Date now = new Date(nowMillis); //We will sign our JWT with our ApiKey secret byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(SECRET_KEY); Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName()); //Let's set the JWT Claims JwtBuilder builder = Jwts.builder().setId(id) .setIssuedAt(now) .setSubject(subject) .setIssuer(issuer) .signWith(signatureAlgorithm, signingKey); //if it has been specified, let's add the expiration if (ttlMillis > 0) { long expMillis = nowMillis + ttlMillis; Date exp = new Date(expMillis); builder.setExpiration(exp); }  //Builds the JWT and serializes it to a compact, URL-safe string return builder.compact(); } 总而言之, createJWT()方法执行以下操作:  设置哈希算法 获取签发日期索赔的当前日期 使用SECRET_KEY静态属性生成签名密钥 使用流畅的API添加声明并签署JWT 设置到期日期 可以根据您的需求进行定制。 例如,如果您想添加其他或自定义声明。  解码令牌 现在看一下更简单的decodeJWT()方法。 public static Claims decodeJWT(String jwt) { //This line will throw an exception if it is not a signed JWS (as expected) Claims claims = Jwts.parser() .setSigningKey(DatatypeConverter.parseBase64Binary(SECRET_KEY))  .parseClaimsJws(jwt).getBody();  return claims;  } 该方法再次使用静态SECRET_KEY属性生成签名密钥,并使用该方法来验证JWT是否未被篡改。 如果签名与令牌不匹配,则该方法将引发io.jsonwebtoken.SignatureException异常。 如果签名确实匹配,则该方法将索赔作为Claims对象返回。  差不多了!  运行JUnit测试 为了获得更多的荣誉,您可以在示例项目中运行JUnit测试。 有三个测试,它们演示了JJWT库的一些基本功能。 第一个测试显示了一条愉快的路,创建并成功解码了有效的JWT。 第二个测试显示了当您尝试将完全伪造的字符串解码为JWT时,JJWT库将如何失败。 最后一个测试显示了被JJWT篡改的方式将如何导致decodeJWT()方法引发SignatureException 。  您可以使用以下命令从命令行运行这些测试:  ./gradlew test -i AI助手 -i将Gradle的日志级别设置为Info以便我们看到测试的简单日志输出。  了解有关在Java应用程序中使用JWT的更多信息 JJWT库使创建和验证JWT非常容易。 只需指定一个秘密密钥和一些声明,您就会拥有一个JJWT。 以后,使用相同的密钥对JJWT进行解码并验证其内容。  现在,创建和使用JJWT非常简单,为什么不使用它们呢?  不要忘记SSL! 请记住,除非对JWT进行加密,否则它们中编码的信息通常仅是Base64编码的,任何小孩和一些宠物都可以读取。 因此,除非您希望中国,俄罗斯和FBI读取所有会话数据,否则请使用SSL对其进行加密。  Baeldung 在Java和JWT上有相当不错的深入教程 。  另外,这里还有Okta博客提供的更多链接,可帮助您继续前进:  Java应用程序的简单令牌认证 Spring Boot,OAuth 2.0和Okta入门 确保Spring Boot应用程序安全的10种绝佳方法 如果您的JWT被盗怎么办? JWT分析器和检查器Chrom插件 在线编码或解码JWT 如果您对此帖子有任何疑问,请在下面添加评论。 原文链接:https://blog.csdn.net/dnc8371/article/details/106701217 
  • [技术干货] 【Java web】JWTtoken登录校验
    JWTtoken登录校验 session用户认证的一般流程 JWTToken认证的流程 JWT token的组成 头部(header) payload 负载 signature签名 三个部分组合形成token java中使用JWTToken JWT (Json Web Token) 是为了网络应用环境间传递声明而执行的一种基于JSON的开放标准。  JWT可以校验用户的身份,传递用户的身份信息,一般用在用户登录上。  session用户认证的一般流程 用户输入账号密码, 向服务器发送请求 服务器验证账号密码是否正确, 如果正确则在该用户的session回话里保存相关的信息如id, 登录时间等 服务器向用户返回一个Cookie, 存着sessionId 用户再次请求服务器时带上sessionId.服务器根据sessionId从服务器保存的session中获取的信息. 这种方式在单服务器场景下没有太大问题 但在服务器集群下扩展性较差, session存在于不同的服务器, 则用户下次请求又必须分配到上次请求的服务器, 影响了负载均衡的能力.  CSRF: 跨站请求伪造, 如果截获了用户Cookie中的sessionId, 则很容易会受到跨站请求伪造的攻击  使用JWTToken则不会遇到该问题, 因为使用JWTToken服务器就不保存信息了, 而是token里保存着编码的信息, 并对附带一个加密的签名, 服务器接收到token可直接解密进行认证.  JWTToken认证的流程 用户输入账号密码, 向服务器发送请求. 服务器验证账号密码,返回一个加密的保存有用户信息的token 用户再次请求时带上token 服务器解析token, 取出token中的信息进行认证 JWT token的组成 三个部分 Header 头部 Payload 负载 Signature 签名 头部(header) {   "alg": "HS256",   "typ": "JWT" } alg : 加密类型 typ : JWT token的类型  alg    算法    介绍 HS256    HMAC256    HMAC with SHA-256 HS384    HMAC384    HMAC with SHA-384 HS512    HMAC512    HMAC with SHA-512 RS256    RSA256    RSASSA-PKCS1-v1_5 with SHA-256 RS384    RSA384    RSASSA-PKCS1-v1_5 with SHA-384 RS512    RSA512    RSASSA-PKCS1-v1_5 with SHA-512 ES256    ECDSA256    ECDSA with curve P-256 and SHA-256 ES384    ECDSA384    ECDSA with curve P-384 and SHA-384 ES512    ECDSA512    ECDSA with curve P-521 and SHA-512 payload 负载 负载主要是存放有效信息的地方 标准中注册的声明 建议但不强制使用  iss : jwt签发者 sub : jwt所面向的用户 aud : 接收jwt的一方 exp : jwt的过期时间, 要大于签发时间 nbf : 定义一个时间前, token都是不可用的 iat : jwt的签发时间 jti : jwt的唯一身份标识, 作为一次性token, 避免重放攻击  自定义数据  可以加入一些自己定义的数据, 用来进行用户认证  不建议存放敏感信息, 因为这部分内容不会进行加密, 而是采用base64进行编码.  id : 22 name : zhangs signature签名 签名为jwt的第三个部分 jwt的签名是根据headers头部和payload负载通过头部中声明的加密方式进行加盐secret组合加密 secret存在服务器中, 服务器通过这个签名验证token的合法性, 防止伪造的token.  三个部分组合形成token HMACSHA256(   base64UrlEncode(header) + "." +   base64UrlEncode(payload)+ "." +   your-256-bit-secret )  样例  eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MjIsImV4cCI6MTU1NzU4Mjc1NSwiaWF0IjoxNTU2OTc3OTU1fQ.icWwR1ysVb4p30XywCck6Fkzn6Oep45zG50NmRLc3BE 解析出来的headers  {   "alg": "HS256",   "typ": "JWT" } 解析出来的payload  {   "id": 22,   "exp": 1557582755,   "iat": 1556977955 } java中使用JWTToken 导入依赖     <dependency>       <groupId>com.auth0</groupId>       <artifactId>java-jwt</artifactId>       <version>3.5.0</version>     </dependency> 创建token      Map<String, Object> map = new HashMap<String, Object>();      map.put("alg", "HS256");     map.put("typ", "JWT");     String token = JWT.create()                 .withHeader(map)    // 添加头部                 .withClaim("id", id)   // 添加自定义数据                 .withExpiresAt(experiesDate) // 设置过期的日期                 .withIssuedAt(iatDate) // 签发时间                 .sign(Algorithm.HMAC256(SECRET)); // 加密 校验token          JWTVerifier verifier = JWT.require(Algorithm.HMAC256(SECRET)).build();         DecodedJWT jwt = null;         try {             jwt = verifier.verify(token);         } catch (Exception e) {             throw new Exception("登录过期");         }         return jwt.getClaims(); 在做登录的intercepter中加入验证逻辑    String token = request.getHeader("token");                               // 获取请求头里的token   if (token == null) {                                                               // 跳转返回未登录                 request.getRequestDispatcher("/user/need_login.do").forward(request, response);                 logger.info("未登录");             } else {                 try {                     Map<String, Claim> map = JWTUtil.verifyToken(token);    // 该方法验证失败会抛出异常                     int id = map.get("id").asInt();                         // 没有id也会抛出异常                     request.setAttribute("id", id);                         // 传递参数id                     return true;                                            // 验证成功放行                 } catch (Exception e) {                                     // 抛出异常进行跳转                     request.getRequestDispatcher("/user/need_login.do").forward(request, response);                     logger.info("登录过期");                 }             } ———————————————— 原文链接:https://blog.csdn.net/z944733142/article/details/89646877 
  • [技术干货] Java简单快速入门JWT(token生成与验证)
    一、Token简单介绍         简单来说,token就是一个将信息加密之后的密文,而jwt也是token的实现方式之一,用于服务器端进行身份验证和授权访问控制。由于是快速入门,这里简单介绍一下jwt的生成原理         jwt由三部分组成。分别是                 1.Header(标头),一般用于指明token的类型和加密算法                 2.PayLoad(载荷),存储token有效时间及各种自定义信息,如用户名,id、发行者等                 3.Signature(签名),是用标头提到的算法对前两部分进行加密,在签名认证时,防止止信息被修改         而Header和PayLoad最初都是json格式的键值对,格式如下         Header: {   "alg": "HS256",  #签名加密所用的算法   "typ": "JWT"     #表名token的格式 } AI助手         PayLoad:此段json中保存了用户的部分信息  {   "sub": "1234567890",  #主题   "name": "John Doe",  #用户名   "isVIP": true,     #是否为vip用户   "exp": 1677740800   #过期时间 } AI助手          Signature:签名,首先将Header和PayLoad的json字符串进行Base64Url加密后,得到两个加密后的字符串,然后把这两个字符串由‘.’拼接起来,然后再将拼接好的字符串再用之前 Header中提到的算法与一个密钥(一般为一个字符串)进行加密后得到一个新的字符串,最后把这个新字符串和之前使用Base64Url加密后的两段字符串进行连接,最终得到token字符串,然后就可以把它发送给客户端进行存储了。          一般jwt格式token格式如下 eyJ0eXAiOiJqd3QiLCJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiam9rZXIiLCJleHAiOjE2OTQxODE2NDUsInB3ZCI6IjEyMyJ9.vDrOQp-FkmRCQBzfaZsxzkef-iNjkAuAaqvT7Ns5Ab0 #(Header).(PayLoad).(Signature) 当然,作为快速上手jwt的文章,这里就不对jwt及token进行更加深入的讲解了,以上内容只需要做简单了解有个映像即可,总而言之:          token就是一个在服务器端生成,包含了部分用户信息,最后发送给客户端进行保管的加密字符串,当客户端向服务器再次发起请求时,请求需携带token,服务器端对token进行验证,以确定用户是否有权访问。(以此来实现,登录验证,权限校验,记住密码等功能)          接下来做一个简单的代码实现  二、代码实现 导入相关依赖jar包(此jwt框架功能比较齐全且简单,适合初学者)  <dependency>     <groupId>com.auth0</groupId>     <artifactId>java-jwt</artifactId>     <version>4.2.1</version> </dependency> jwt生成  首先需要一个字符串密钥进行加密,这个字符串可自定义,然后提前规划好你项目的jwt想要储存哪些自定义信息,例如用户名,密码,id,等等(这里为了方便演示,这里就把用户名以及密钥字符串设为常量了) package com.example.jwtdemo.Controller;   import com.auth0.jwt.JWT; import com.auth0.jwt.JWTCreator; import com.auth0.jwt.algorithms.Algorithm; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController;   import java.util.Date; import java.util.HashMap; import java.util.Map;   @RestController @Controller("/") public class TestController {     //用户的用户名     private static final String USERNAME = "admin";        //用于签名加密的密钥,为一个字符串(需严格保密)     private static final String KEY = "the_key";         @GetMapping("/jwt")     public String setToken() {                  //获取jwt生成器         JWTCreator.Builder jwtBuilder = JWT.create();           //由于该生成器设置Header的参数为一个<String, Object>的Map,         //所以我们提前准备好         Map<String, Object> headers = new HashMap<>();           headers.put("typ", "jwt");   //设置token的type为jwt         headers.put("alg", "hs256");  //表明加密的算法为HS256           //开始生成token         //我们将之前准备好的header设置进去         String token = jwtBuilder.withHeader(headers)                   //接下来为设置PayLoad,Claim中的键值对可自定义                 //设置用户名                 .withClaim("username", USERNAME)                                      //是否为VIP用户                 .withClaim("isVIP", true)                                  //设置用户id                 .withClaim("userId", 123)                   //token失效时间,这里为一天后失效                 .withExpiresAt(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24))                 //设置该jwt的发行时间,一般为当前系统时间                 .withIssuedAt(new Date(System.currentTimeMillis()))                   //token的发行者(可自定义)                 .withIssuer("issuer")                   //进行签名,选择加密算法,以一个字符串密钥为参数                 .sign(Algorithm.HMAC256(KEY));           //token生成完毕,可以发送给客户端了,前端可以使用         //localStorage.setItem("your_token", token)进行存储,在         //下次请求时携带发送给服务器端进行验证         System.out.println(token);         return token;     } } 最终生成的jwt  eyJ0eXAiOiJqd3QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJpc3N1ZXIiLCJleHAiOjE2OTQyNzA0OTMsImlhdCI6MTY5NDE4NDA5MywiaXNWSVAiOnRydWUsInVzZXJuYW1lIjoiYWRtaW4ifQ.JiTMn2jDaUTolPXB0TCBBOwSHG1l75W2oy2isdWhQIU AI助手 PS:以上代码中向jwt中注册的键值对不是都为必须的,需按照自己的情况进行设置  jwt验证          我们从客户端的请求中获取其携带的token进行验证,已确定其是否有权访问或进行其他的业务逻辑操作       @GetMapping("/verify")     public boolean verify(HttpServletRequest request) {           /*从请求头中获取token(具体要看你的token放在了请求的哪里,            这里以放在请求头举例)         */         String token = request.getHeader("token");           /*判断token是否存在,若不存在,验证失败,             并进行验证失败的逻辑操作(例如跳转到登录界面,             或拒绝访问等等)*/         if (token == null) return false;                  /*获取jwt的验证器对象,传入的算法参数以及密钥字符串(KEY)必须         和加密时的相同*/         var require = JWT.require(Algorithm.HMAC256(KEY)).build();           DecodedJWT decode;         try {                          /*开始进行验证,该函数会验证此token是否遭到修改,                 以及是否过期,验证成功会生成一个解码对象                 ,如果token遭到修改或已过期就会                 抛出异常,我们用try-catch抓一下*/             decode = require.verify(token);           } catch (Exception e) {               //抛出异常,验证失败             return false;         }           //若验证成功,就可获取其携带的信息进行其他操作                  //可以一次性获取所有的自定义参数,返回Map集合         Map<String, Claim> claims = decode.getClaims();         if (claims == null) return false;         claims.forEach((k, v) -> System.out.println(k + " " + v.asString()));           //也可以根据自定义参数的键值来获取         if (!decode.getClaim("isVIP").asBoolean()) return false;         System.out.println(decode.getClaim("username").asString());                  //获取发送者,没有设置则为空         System.out.println(decode.getIssuer());           //获取过期时间         System.out.println(decode.getExpiresAt());           //获取主题,没有设置则为空         System.out.println(decode.getSubject());         return true;     } ———————————————— 原文链接:https://blog.csdn.net/greatming666/article/details/132767798 
  • [技术干货] Token 令牌:原理、使用场景及操作指南
    Token 令牌:原理、使用场景及操作指南 一、引言 在当今的数字化时代,信息安全和用户权限管理是软件系统开发中的关键环节。Token 令牌作为一种有效的身份验证和授权机制,被广泛应用于各种网络应用、移动应用以及分布式系统中。它提供了一种安全、灵活且高效的方式来控制用户对资源的访问,同时也保护了系统的安全性。  二、Token 令牌的概念与原理 (一)什么是 Token 令牌 Token 令牌是一种包含用户相关信息的加密字符串。它由服务器生成并颁发给客户端,作为客户端身份的一种标识。Token 通常包含用户的身份信息(如用户 ID)、权限信息(如用户角色)以及一些用于验证的签名信息。这个加密字符串可以在不同的请求中传递,让服务器能够识别客户端的身份并验证其权限。  (二)工作原理 生成阶段 当用户成功登录或者完成某种认证流程后,服务器会根据用户的信息和系统设置的规则生成一个 Token。这个生成过程通常涉及到加密算法,以确保 Token 的安全性。例如,服务器可能会使用 JSON Web Token(JWT)标准,通过对包含用户信息的 JSON 对象进行签名和加密,生成一个 JWT 令牌。 传递阶段 客户端收到 Token 后,会将其存储起来,通常存储在浏览器的本地存储(Local Storage)、会话存储(Session Storage)或者作为 HTTP 请求头的一部分。在后续向服务器发送请求时,客户端会将 Token 一同发送给服务器。 验证阶段 服务器收到带有 Token 的请求后,会对 Token 进行验证。这包括检查 Token 的签名是否正确(用于验证 Token 是否被篡改)、验证 Token 是否过期等。如果 Token 验证通过,服务器就会根据 Token 中包含的权限信息来决定是否允许客户端访问请求的资源。 三、Token 令牌的使用场景 (一)身份认证 单页应用(SPA) 在单页应用中,传统的基于会话(Session)的身份认证可能会遇到一些问题,如跨域访问困难等。Token 令牌提供了一种更好的解决方案。例如,用户登录成功后,服务器颁发一个 JWT 令牌给客户端。客户端在后续的每一个 API 请求中都将这个令牌放在请求头中发送给服务器。服务器通过验证令牌来确认用户的身份,从而允许用户访问受保护的资源。 移动应用 对于移动应用来说,Token 令牌同样适用。移动应用在用户登录后获取令牌,并将其存储在本地安全的存储区域(如设备的加密存储)。在与服务器通信时,将令牌添加到请求中,方便服务器进行身份验证。这样可以确保只有经过认证的用户才能访问移动应用后端的服务,如获取用户个人信息、进行数据更新等。 (二)授权访问 多用户角色系统 在一个具有多种用户角色(如管理员、普通用户、访客)的系统中,Token 令牌可以携带用户的角色信息。服务器根据令牌中的角色信息来决定用户可以访问哪些资源。例如,管理员角色的用户可能拥有对系统所有功能的访问权限,而普通用户只能访问部分功能。当用户发送请求时,服务器通过验证令牌中的角色信息来授权或拒绝访问相应的资源。 第三方 API 集成 当一个应用需要访问第三方 API 时,第三方服务提供商可能会要求使用 Token 令牌进行授权。应用首先需要向第三方服务申请令牌,通常是通过注册应用并获取客户端 ID 和客户端秘密,然后使用这些信息进行认证以获取令牌。在后续访问第三方 API 时,将令牌包含在请求中,以获得授权访问。 四、如何使用 Token 令牌 (一)在 Web 应用中使用 JWT(JSON Web Token) 后端生成 JWT 令牌 首先,在后端应用(如使用 Node.js + Express)中,需要安装相关的 JWT 库,如 jsonwebtoken。当用户登录成功后,使用用户信息(如用户 ID、用户名等)生成 JWT 令牌。以下是一个简单的示例代码: const jwt = require('jsonwebtoken'); const secretKey ='my_secret_key';  // 假设这是从数据库中获取的用户信息 const user = {     id: 1,     username: 'example_user' };  // 生成JWT令牌 const token = jwt.sign(user, secretKey); 在这个示例中,我们定义了一个密钥(secretKey),并使用 jwt.sign 方法将用户信息(user)进行签名,生成一个 JWT 令牌(token)。 前端存储和传递 JWT 令牌 前端(如使用 JavaScript)在收到后端返回的 JWT 令牌后,可以将其存储在本地存储中。例如,使用 localStorage: localStorage.setItem('token', token); 在后续向服务器发送请求时,将令牌添加到请求头中。如果使用 Axios 库进行 HTTP 请求,可以这样设置: import axios from 'axios';  const token = localStorage.getItem('token'); axios.defaults.headers.common['Authorization'] = 'Bearer'+ token; 这样,每次发送请求时,Axios 都会将包含令牌的请求头发送给服务器。 后端验证 JWT 令牌 在后端,需要对收到的 JWT 令牌进行验证。同样使用 jsonwebtoken 库,在处理受保护的路由时,添加验证逻辑。例如: const jwt = require('jsonwebtoken'); const secretKey ='my_secret_key';  // 中间件函数用于验证JWT令牌 const authenticateToken = (req, res, next) => {     const authHeader = req.headers['authorization'];     const token = authHeader && authHeader.split(' ')[1];     if (token == null) return res.sendStatus(401);      jwt.verify(token, secretKey, (err, user) => {         if (err) return res.sendStatus(403);         req.user = user;         next();     }); };  // 在受保护的路由中使用中间件 app.get('/protected', authenticateToken, (req, res) => {     res.send('You have access to protected resource.'); }); 在这个示例中,我们定义了一个中间件函数(authenticateToken)来验证 JWT 令牌。首先从请求头中获取令牌,然后使用 jwt.verify 方法进行验证。如果验证通过,将用户信息(user)添加到请求对象(req)中,并调用 next 函数,允许请求继续处理;如果验证失败,根据情况返回 401(未授权)或 403(禁止访问)状态码。 (二)在移动应用中使用 Token 令牌(以 Android 为例) 获取 Token 令牌 在移动应用的登录模块,通过与后端服务器进行通信获取 Token 令牌。通常使用 HTTP 库(如 OkHttp)进行网络请求。假设后端返回的是一个 JSON 格式的响应,其中包含令牌,以下是一个简单的示例代码: import android.os.AsyncTask; import android.util.Log;  import org.json.JSONException; import org.json.JSONObject;  import java.io.IOException;  import okhttp3.MediaType; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response;  public class TokenFetcher extends AsyncTask<String, Void, String> {     private static final String TAG = "TokenFetcher";     private static final String BASE_URL = "https://your_backend_url/login";     private static final MediaType JSON = MediaType.get("application/json; charset=utf-8");      @Override     protected String doInBackground(String... params) {         String username = params[0];         String password = params[1];          OkHttpClient client = new OkHttpClient();         JSONObject json = new JSONObject();         try {             json.put("username", username);             json.put("password", password);         } catch (JSONException e) {             Log.e(TAG, "Error creating JSON object", e);         }         RequestBody body = RequestBody.create(json.toString(), JSON);         Request request = new Request.Builder()               .url(BASE_URL)               .post(body)               .build();         try {             Response response = client.newCall(request).execute();             if (response.isSuccessful()) {                 JSONObject responseJson = new JSONObject(response.body().string());                 return responseJson.getString("token");             } else {                 Log.e(TAG, "Login failed. Status code: " + response.code());             }         } catch (IOException | JSONException e) {             Log.e(TAG, "Error fetching token", e);         }         return null;     }      @Override     protected void onPostExecute(String token) {         if (token!= null) {             // 将令牌存储在安全的存储区域,如Android的SharedPreferences             // 这里只是示例,实际应用中需要考虑加密等安全措施             // 假设已经有一个名为TokenStorage的类用于存储令牌             TokenStorage.storeToken(token);         }     } } 在这个示例中,我们通过 AsyncTask 在后台线程中进行网络请求,向服务器发送用户名和密码,获取返回的令牌,并将其存储在本地(这里只是简单演示存储在一个假设的 TokenStorage 类中,实际应用中需要更好的安全存储方式)。 使用 Token 令牌进行请求 在移动应用需要访问受保护的后端资源时,将存储的 Token 令牌添加到请求中。同样使用 OkHttp 库,以下是一个示例代码: import android.util.Log;  import java.io.IOException;  import okhttp3.Interceptor; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response;  public class TokenInterceptor implements Interceptor {     @Override     public Response intercept(Chain chain) throws IOException {         Request originalRequest = chain.request();         String token = TokenStorage.getToken();         if (token!= null) {             Request newRequest = originalRequest.newBuilder()                   .header("Authorization", "Bearer " + token)                   .build();             return chain.proceed(newRequest);         } else {             Log.w("TokenInterceptor", "No token found.");             return chain.proceed(originalRequest);         }     }      public static OkHttpClient getClient() {         OkHttpClient client = new OkHttpClient.Builder()               .addInterceptor(new TokenInterceptor())               .build();         return client;     } } 在这里,我们定义了一个拦截器(TokenInterceptor),在每个请求中检查是否有令牌,如果有,则将其添加到请求头(“Authorization”)中,格式为 “Bearer <令牌内容>”,然后继续发送请求。通过这种方式,移动应用在访问后端资源时可以进行身份验证。 后端验证移动应用的 Token 令牌 后端验证移动应用发送的 Token 令牌的方式与 Web 应用类似。根据使用的技术栈,采用相应的验证逻辑,确保令牌的真实性和有效性,以及验证用户的权限等信息。 五、Token 令牌使用的注意事项 (一)安全性 令牌存储安全 在前端应用中,无论是 Web 应用还是移动应用,存储 Token 令牌时需要注意安全性。在 Web 应用中,避免将令牌存储在容易被跨站脚本攻击(XSS)获取的地方,如使用 Cookie 存储时要设置合适的安全属性(如 HttpOnly、Secure)。在移动应用中,要使用设备提供的安全存储机制,如 Android 的 KeyStore 或 iOS 的 Keychain。 传输安全 Token 令牌在网络传输过程中应该使用安全的协议,如 HTTPS。这样可以防止令牌被中间人拦截和窃取。 (二)有效期管理 设置合理的有效期 Token 令牌应该有合理的有效期设置。有效期过短可能会导致用户频繁登录,影响用户体验;有效期过长则增加了令牌被窃取后滥用的风险。根据应用的安全需求和用户使用场景,合理设置令牌的有效期,如几分钟到几天不等。 刷新机制 对于需要长时间使用的应用,应该建立令牌刷新机制。当令牌接近过期时,自动向服务器请求刷新令牌,以确保用户的持续访问权限。 (三)权限管理 精细的权限控制 根据应用的用户角色和功能需求,在 Token 令牌中设置精细的权限信息。确保服务器能够根据令牌准确地授权或拒绝用户对不同资源的访问。 权限更新 当用户的权限发生变化时,如用户角色升级或权限被回收,要及时更新 Token 令牌中的权限信息或者重新颁发令牌,以保证权限控制的准确性。 六、总结 Token 令牌是一种强大的身份验证和授权工具,在现代软件系统中发挥着重要的作用。通过了解其原理、使用场景以及正确的使用方法,开发者可以构建更加安全、灵活的应用程序。在使用过程中,要注意安全性、有效期管理和权限管理等重要事项,以确保 Token 令牌能够有效地保护系统和用户的权益。无论是 Web 应用还是移动应用,Token 令牌都为用户身份认证和授权访问提供了可靠的解决方案。 ————————————————    原文链接:https://blog.csdn.net/Andrew_Chenwq/article/details/143253009 
  • [技术干货] Java实现基于token认证
    随着互联网的不断发展,技术的迭代也非常之快。我们的用户认证也从刚开始的用户名密码转变到基于cookie的session认证,然而到了今天,这种认证已经不能满足与我们的业务需求了(分布式,微服务)。我们采用了另外一种认证方式:基于token的认证。 一、与cookie相比较的优势: 1、支持跨域访问,将token置于请求头中,而cookie是不支持跨域访问的; 2、无状态化,服务端无需存储token,只需要验证token信息是否正确即可,而session需要在服务端存储,一般是通过cookie中的sessionID在服务端查找对应的session; 3、无需绑定到一个特殊的身份验证方案(传统的用户名密码登陆),只需要生成的token是符合我们预期设定的即可; 4、更适用于移动端(Android,iOS,小程序等等),像这种原生平台不支持cookie,比如说微信小程序,每一次请求都是一次会话,当然我们可以每次去手动为他添加cookie,详情请查看博主另一篇博客; 5、避免CSRF跨站伪造攻击,还是因为不依赖cookie; 6、非常适用于RESTful API,这样可以轻易与各种后端(java,.net,python......)相结合,去耦合 还有一些优势这里就不一一列举了。 二、基于JWT的token认证实现 JWT:JSON  Web  Token,其实token就是一段字符串,由三部分组成:Header,Payload,Signature。详细情况请自行百度,现在,上代码。 1、引入依赖,这里选用java-jwt,选择其他的依赖也可以 2、实现签名方法 设置15分钟过期也是出于安全考虑,防止token被窃取,不过一般选择基于token认证,传输方式我们都应该选择https,这样别人无法抓取到我们的请求信息。这个私钥是非常重要的,加密解密都需要用到它,要设置的足够复杂并且不能被盗取,我这里选用的是一串uuid,加密方式是HMAC256。 3、认证 我这里演示的还是以传统的用户名密码验证,验证通过发放token。 4、配置拦截器 实现HandleInterceptor,重写preHandle方法,该方法是在每个请求之前触发执行,从request的头里面取出token,这里我们统一了存放token的键为accessToken,验证通过,放行,验证不通过,返回认证失败信息。 5、设置拦截器 这里使用的是Spring的xml配置拦截器,放过认证接口。 6、token解码方法 7、测试 访问携带token,请求成功。 未携带token或者token错误,过期,返回认证失败信息。 8、获取token里携带的信息 我们可以将一些常用的信息放入token中,比如用户登陆信息,可以方便我们的使用 至此,一个简单的基于token认证就实现了,下次我将shiro与JWT整合到一起。 ———————————————— 原文链接:https://blog.csdn.net/KKKun_Joe/article/details/81878231 
  • [技术干货] ConcurrentHashMap与HashMap的区别
    1.基本概念不同 ConcurrentHashMap是一个支持高并发更新与查询的哈希表。在保证安全的前提下,进行检索不需要锁定。 HashMap是基于哈希表的Map接口的实现,此实现提供所有可选的映射操作,并允许使用null值和null键。 2.底层数据结构不同 HashMap的底层数据结构主要是:数组+链表,确切的说是由链表为元素的数组。 ConcurrentHashMap的底层数据结构是:Segments数组+HashEntry数组+链表。 3.线程安全属性不同 ConcurrentHashMap是线程安全的数组,它采用分段锁保证安全性。容器中有多把锁,每一把锁锁一段数据,这样在多线程访问时不同段的数据时,就不会存在锁竞争了,这样便可以有效地提高并发效率。 而HashMap不是线程安全,没有锁机制,在多线程环境下会导致数据覆盖之类的问题,所以在多线程中使用HashMap是会抛出异常的。 4.对整个桶数组的处理方式不同 ConcurrentHashMap对整个桶数组进行了分段;而HashMap则没有对整个桶数组进行分段。 延伸阅读 HashTable是什么 HashTable也就是哈希表,是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。 HashTable继承于Dictionary,实现了Map、Cloneable、java.io.Serializable接口。HashTable的函数都是同步的,这意味着它是线程安全的,它的key、value都不可以为null。此外,HashTable中的映射不是有序的。 HashTable的成员变量主要有以下五个:  table:一个Entry[]数组类型,而Entry(在 HashMap 中有讲解过)就是一个单向链表。哈希表的”key-value键值对”都是存储在Entry数组中的 count:Hashtable的大小,它是Hashtable保存的键值对的数量 threshold:Hashtable的阈值,用于判断是否需要调整Hashtable的容量,threshold的值 = (容量 * 负载因子) loadFactor:负载因子 modCount:用来实现fail-fast机制的(也就是快速失败)。所谓快速失败就是在并发集合中,其进行迭代操作时,若有其他线程对其进行结构性的修改,这时迭代器会立马感知到,并且立即抛出ConcurrentModificationException异常,而不是等到迭代完成之后才告诉你已经出错了。 ————————————————        原文链接:https://blog.csdn.net/azybjbajzc/article/details/130626216 
  • [技术干货] Hashmap和ConcurrentHashmap的区别
    HashTable (1)底层数组+链表实现,无论key还是value都不能为null,线程安全,实现线程安全的方式是在修改数据时锁住整个HashTable,效率低,ConcurrentHashMap做了相关优化 (2)初始size为11,扩容:newsize = olesize*2+1 (3)计算index的方法:index = (hash & 0x7FFFFFFF) % tab.length  HashMap (1)底层数组+链表实现,可以存储null键和null值,线程不安全 (2)初始size为16,扩容:newsize = oldsize*2,size一定为2的n次幂 (3)扩容针对整个Map,每次扩容时,原来数组中的元素依次重新计算存放位置,并重新插入 (4)插入元素后才判断该不该扩容,有可能无效扩容(插入后如果扩容,如果没有再次插入,就会产生无效扩容) (5)当Map中元素总数超过Entry数组的75%,触发扩容操作,为了减少链表长度,元素分配更均匀 (6)计算index方法:index = hash & (tab.length – 1) HashMap的初始值还要考虑加载因子:  (7)哈希冲突:若干Key的哈希值按数组大小取模后,如果落在同一个数组下标上,将组成一条Entry链,对Key的查找需要遍历Entry链上的每个元素执行equals()比较。 (8)加载因子:为了降低哈希冲突的概率,默认当HashMap中的键值对达到数组大小的75%时,即会触发扩容。因此,如果预估容量是100,即需要设定100/0.75=134的数组大小。 (9)空间换时间:如果希望加快Key查找的时间,还可以进一步降低加载因子,加大初始大小,以降低哈希冲突的概率。  HashMap和Hashtable都是用hash算法来决定其元素的存储,因此HashMap和Hashtable的hash表包含如下属性:  (1)容量(capacity):hash表中桶的数量 (2)初始化容量(initial capacity):创建hash表时桶的数量,HashMap允许在构造器中指定初始化容量 (3)尺寸(size):当前hash表中记录的数量 (4)负载因子(load factor):负载因子等于“size/capacity”。负载因子为0,表示空的hash表,0.5表示半满的散列表,依此类推。轻负载的散列表具有冲突少、适宜插入与查询的特点(但是使用Iterator迭代元素时比较慢)  除此之外,hash表里还有一个“负载极限”,“负载极限”是一个0~1的数值,“负载极限”决定了hash表的最大填满程度。当hash表中的负载因子达到指定的“负载极限”时,hash表会自动成倍地增加容量(桶的数量),并将原有的对象重新分配,放入新的桶内,这称为rehashing。  HashMap和Hashtable的构造器允许指定一个负载极限,HashMap和Hashtable默认的“负载极限”为0.75,这表明当该hash表的3/4已经被填满时,hash表会发生rehashing。  “负载极限”的默认值(0.75)是时间和空间成本上的一种折中:  (1)较高的“负载极限”可以降低hash表所占用的内存空间,但会增加查询数据的时间开销,而查询是最频繁的操(HashMap的get()与put()方法都要用到查询) (2)较低的“负载极限”会提高查询数据的性能,但会增加hash表所占用的内存开销  程序猿可以根据实际情况来调整“负载极限”值。  ConcurrentHashMap (1)底层采用分段的数组+链表实现,线程安全 (2)通过把整个Map分为N个Segment,可以提供相同的线程安全,但是效率提升N倍,默认提升16倍。(读操作不加锁,由于HashEntry的value变量是 volatile的,也能保证读取到最新的值。) (3)Hashtable的synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术 (4)有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁 (5)扩容:段内扩容(段内元素超过该段对应Entry数组长度的75%触发扩容,不会对整个Map进行扩容),插入前检测需不需要扩容,有效避免无效扩容  Hashtable和HashMap都实现了Map接口,但是Hashtable的实现是基于Dictionary抽象类的。Java5提供了ConcurrentHashMap,它是HashTable的替代,比HashTable的扩展性更好。  HashMap基于哈希思想,实现对数据的读写。当我们将键值对传递给put()方法时,它调用键对象的hashCode()方法来计算hashcode,然后找到bucket位置来存储值对象。当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。HashMap使用链表来解决碰撞问题,当发生碰撞时,对象将会储存在链表的下一个节点中。HashMap在每个链表节点中储存键值对对象。当两个不同的键对象的hashcode相同时,它们会储存在同一个bucket位置的链表中,可通过键对象的equals()方法来找到键值对。如果链表大小超过阈值(TREEIFY_THRESHOLD,8),链表就会被改造为树形结构。  在HashMap中,null可以作为键,这样的键只有一个,但可以有一个或多个键所对应的值为null。当get()方法返回null值时,即可以表示HashMap中没有该key,也可以表示该key所对应的value为null。因此,在HashMap中不能由get()方法来判断HashMap中是否存在某个key,应该用containsKey()方法来判断。而在Hashtable中,无论是key还是value都不能为null。  Hashtable是线程安全的,它的方法是同步的,可以直接用在多线程环境中。而HashMap则不是线程安全的,在多线程环境中,需要手动实现同步机制。  Hashtable与HashMap另一个区别是HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。所以当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException,但迭代器本身的remove()方法移除元素则不会抛出ConcurrentModificationException异常。但这并不是一个一定发生的行为,要看JVM。  先看一下简单的类图:  从类图中可以看出来在存储结构中ConcurrentHashMap比HashMap多出了一个类Segment,而Segment是一个可重入锁。  ConcurrentHashMap是使用了锁分段技术来保证线程安全的。  锁分段技术:首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。  ConcurrentHashMap提供了与Hashtable和SynchronizedMap不同的锁机制。Hashtable中采用的锁机制是一次锁住整个hash表,从而在同一时刻只能由一个线程对其进行操作;而ConcurrentHashMap中则是一次锁住一个桶。  ConcurrentHashMap默认将hash表分为16个桶,诸如get、put、remove等常用操作只锁住当前需要用到的桶。这样,原来只能一个线程进入,现在却能同时有16个写线程执行,并发性能的提升是显而易见的。  HashMap和ConcurrentHashMap的区别总结 (1)HashMap不是线程安全的,而ConcurrentHashMap是线程安全的。  (2)ConcurrentHashMap采用锁分段技术,将整个Hash桶进行了分段segment,也就是将这个大的数组分成了几个小的片段segment,而且每个小的片段segment上面都有锁存在,那么在插入元素的时候就需要先找到应该插入到哪一个片段segment,然后再在这个片段上面进行插入,而且这里还需要获取segment锁。  (3)ConcurrentHashMap让锁的粒度更精细一些,并发性能更好。  原文链接:https://blog.csdn.net/g_ood_good_study/article/details/120438768 
  • [技术干货] HashMap 和 ConcurrentHashMap 的区别
    一、线程安全性 1. HashMap:非线程安全 非线程安全:HashMap 在设计时并没有考虑线程安全问题,因此在多线程环境下同时对 HashMap 进行读写操作可能会导致数据不一致、丢失或抛出 ConcurrentModificationException 异常。 需要外部同步:在多线程环境中使用 HashMap 时,需要使用外部同步来保护对 HashMap 的访问,例如使用 synchronized 块或通过 Collections.synchronizedMap(new HashMap<>()) 来创建线程安全的 Map。 Map<String, String> synchronizedMap = Collections.synchronizedMap(new HashMap<>()); 1 AI助手 2. ConcurrentHashMap:线程安全 线程安全:ConcurrentHashMap 是为并发环境设计的,支持多个线程同时进行读写操作而不会出现数据不一致的问题。它通过细粒度的锁机制实现线程安全,而不是像 Hashtable 那样对整个表进行锁定。 锁分段机制:ConcurrentHashMap 使用了一种分段锁定机制,即将整个哈希表分为多个段(Segment),每个段独立进行锁定操作,从而允许多个线程并发访问不同的段,极大地提高了并发性能。 二、锁机制与并发性能 1. HashMap:无锁 无锁机制:HashMap 没有内置锁机制,因此在多线程情况下,必须通过外部同步来确保线程安全。然而,外部同步通常需要对整个表进行锁定,这样会显著降低并发性能,特别是在写操作频繁的情况下。 2. ConcurrentHashMap:分段锁与 CAS 操作 分段锁(Segment Locking):ConcurrentHashMap 最初的实现中采用了分段锁,每个段相当于一个小的哈希表,多个线程可以同时访问不同的段,而不会发生冲突。Java 8 之后,ConcurrentHashMap 引入了更细粒度的锁(使用 CAS(Compare-And-Swap) 操作和 synchronized 块结合),进一步提高了并发性能。 锁的粒度:由于锁的粒度更细,ConcurrentHashMap 能够在高并发环境下表现出更好的性能,特别是在读操作多于写操作的场景中。 三、结构与实现 1. HashMap:哈希表 + 链表/红黑树 基本结构:HashMap 的内部结构是一个哈希表,使用链表法处理哈希冲突。在 Java 8 中,当链表长度超过一定阈值(默认8)时,链表会转换为红黑树,以提高性能。 扩容机制:HashMap 在插入元素时,如果元素数量超过 容量 * 负载因子,就会触发扩容(通常是容量加倍),并重新计算所有键的哈希值,分配到新的数组中。 2. ConcurrentHashMap:哈希表 + Node + CAS 分段锁(Java 7 之前):在 Java 7 及之前的版本中,ConcurrentHashMap 使用了分段锁,每个段(Segment)内部使用类似 HashMap 的结构。每个段独立维护哈希表,并且各个段之间是互相独立的,因此多个线程可以同时访问不同的段。 Node + CAS 操作(Java 8 之后):从 Java 8 开始,ConcurrentHashMap 放弃了分段锁的实现,改用更细粒度的锁和 CAS 操作。ConcurrentHashMap 内部使用 Node 类来存储键值对,并且通过 CAS 操作确保在并发写操作时的线程安全性。 扩容机制:ConcurrentHashMap 也支持动态扩容,但它的扩容过程是并发的,即多个线程可以同时进行扩容操作,而不会阻塞其他线程的读写操作。 四、Null 键和值的处理 1. HashMap:允许 null 键和值 允许一个 null 键:HashMap 允许一个 null 键和多个 null 值,这使得 HashMap 在某些情况下更为灵活。例如,可以使用 null 键表示缺少值的情况。 特殊处理 null:HashMap 内部对 null 键进行了特殊处理,存储在哈希表的第一个桶(index 为 0 )。 2. ConcurrentHashMap:不允许 null 键和值 不允许 null 键和值:ConcurrentHashMap 不允许 null 键和 null 值。如果尝试插入 null 键或值,会抛出 NullPointerException。 设计原因:这是出于并发环境下的设计考虑,因为 null 键或值可能会导致一些歧义或不可预期的行为,从而影响线程安全性。 五、迭代器的Fail-Fast机制 1. HashMap:Fail-Fast迭代器 Fail-Fast机制:HashMap 的迭代器是 Fail-Fast 的,这意味着如果在迭代过程中检测到集合被修改(除了通过迭代器自身的 remove() 方法),迭代器会抛出 ConcurrentModificationException 异常。 迭代中的修改:这种机制的实现依赖于 modCount 字段,HashMap 通过 modCount 来跟踪结构修改的次数。如果 modCount 在迭代过程中发生变化,意味着集合被并发修改,迭代器将无法继续安全地迭代。 2. ConcurrentHashMap:弱一致性迭代器 弱一致性:ConcurrentHashMap 的迭代器是弱一致性的,它不会抛出 ConcurrentModificationException。即使在迭代过程中集合被修改,ConcurrentHashMap 也能保证迭代器不会抛出异常,并且可以看到最新的元素。 并发修改下的安全性:ConcurrentHashMap 的弱一致性意味着迭代器在访问元素时可以看到部分修改,但不会抛出异常。这种设计在高并发环境下非常实用,因为它允许并发读写操作而不阻塞迭代过程。 六、使用场景 1. HashMap:适用于单线程或读多写少的场景 单线程环境:HashMap 最适合在单线程环境中使用,或者在无需担心并发修改的场景中使用。 读多写少的场景:在读操作明显多于写操作的场景中,HashMap 的性能优势更加明显。 2. ConcurrentHashMap:适用于多线程并发场景 高并发场景:ConcurrentHashMap 专为高并发环境设计,适用于需要频繁读写操作的多线程场景,例如缓存、计数器、会话管理等。 部分一致性要求:当需要在迭代过程中允许并发修改时,ConcurrentHashMap 提供了比 HashMap 更灵活和安全的处理方式。 七、扩展:ConcurrentHashMap 的性能优化 1. 分段锁与并发控制 Java 7 及之前:分段锁机制通过将哈希表划分为多个段来减少锁争用,每个段独立加锁,允许多个线程同时访问不同的段,从而提高并发性能。 Java 8 及之后:放弃分段锁机制,采用更细粒度的锁和 CAS 操作(如在树化和扩容时),提高了并发写操作的性能。 2. 扩容与 rehash 并发扩容:ConcurrentHashMap 支持并发扩容,这意味着多个线程可以同时进行 rehash 操作,而不会阻塞其他线程的读写操作。这种设计确保了高并发环境下的扩展性。 八、总结 HashMap 和 ConcurrentHashMap 是 Java 中两种常用的哈希表实现,但它们在设计  目标、线程安全性、性能优化等方面有显著差异:  线程安全性:HashMap 是非线程安全的,适合单线程环境;ConcurrentHashMap 是线程安全的,适合多线程并发场景。 锁机制:HashMap 没有内置锁机制,需要外部同步;ConcurrentHashMap 使用分段锁和 CAS 操作,优化了并发性能。 Null 键和值:HashMap 允许 null 键和值;ConcurrentHashMap 不允许 null 键和值。 迭代器机制:HashMap 的迭代器是 Fail-Fast 的,而 ConcurrentHashMap 的迭代器是弱一致性的,允许并发修改。 使用场景:HashMap 适用于单线程或读多写少的场景;ConcurrentHashMap 适用于高并发、多线程的环境。 ————————————————  原文链接:https://blog.csdn.net/Flying_Fish_roe/article/details/143200317 
  • [技术干货] 彻头彻尾理解 ConcurrentHashMap
    ConcurrentHashMap是J.U.C(java.util.concurrent包)的重要成员,它是HashMap的一个线程安全的、支持高效并发的版本。在默认理想状态下,ConcurrentHashMap可以支持16个线程执行并发写操作及任意数量线程的读操作。本文将结合Java内存模型,分析JDK源代码,探索ConcurrentHashMap高并发的具体实现机制,包括其在JDK中的定义和结构、并发存取、重哈希和跨段操作,并着重剖析了ConcurrentHashMap读操作不需要加锁和分段锁机制的内在奥秘和原理。   本文所有关于 ConcurrentHashMap 的源码都是基于 JDK 1.6 的,不同 JDK 版本之间会有些许差异,但不影响我们对 ConcurrentHashMap 的数据结构、原理等整体的把握和了解。   由于 ConcurrentHashMap 的源代码实现依赖于Java内存模型,所以阅读本文需要读者了解Java内存模型与Volatile语义,具体详见《Java 并发:volatile 关键字解析》一文。同时,ConcurrentHashMap的源代码会涉及到散列算法和链表数据结构,所以,读者需要对散列算法和基于链表的数据结构有所了解,特别是对HashMap的进一步了解和回顾。关于HashMap的详细介绍,请移步我的博文《Map 综述(一):彻头彻尾理解 HashMap》。 一. ConcurrentHashMap 概述   笔者曾在《Map 综述(一):彻头彻尾理解 HashMap》一文中提到,HashMap 是 Java Collection Framework 的重要成员,也是Map族(如下图所示)中我们最为常用的一种。不过遗憾的是,HashMap不是线程安全的。也就是说,在多线程环境下,操作HashMap会导致各种各样的线程安全问题,比如在HashMap扩容重哈希时出现的死循环问题,脏读问题等。HashMap的这一缺点往往会造成诸多不便,虽然在并发场景下HashTable和由同步包装器包装的HashMap(Collections.synchronizedMap(Map<K,V> m) )可以代替HashMap,但是它们都是通过使用一个全局的锁来同步不同线程间的并发访问,因此会带来不可忽视的性能问题。庆幸的是,JDK为我们解决了这个问题,它为HashMap提供了一个线程安全的高效版本 —— ConcurrentHashMap。在ConcurrentHashMap中,无论是读操作还是写操作都能保证很高的性能:在进行读操作时(几乎)不需要加锁,而在写操作时通过锁分段技术只对所操作的段加锁而不影响客户端对其它段的访问。特别地,在理想状态下,ConcurrentHashMap 可以支持 16 个线程执行并发写操作(如果并发级别设为16),及任意数量线程的读操作。   如下图所示,ConcurrentHashMap本质上是一个Segment数组,而一个Segment实例又包含若干个桶,每个桶中都包含一条由若干个 HashEntry 对象链接起来的链表。总的来说,ConcurrentHashMap的高效并发机制是通过以下三方面来保证的(具体细节见后文阐述): 通过锁分段技术保证并发环境下的写操作; 通过 HashEntry的不变性、Volatile变量的内存可见性和加锁重读机制保证高效、安全的读操作;  通过不加锁和加锁两种方案控制跨段操作的的安全性。 二. HashMap 线程不安全的典型表现   我们先回顾一下HashMap。HashMap是一个数组链表,当一个key/Value对被加入时,首先会通过Hash算法定位出这个键值对要被放入的桶,然后就把它插到相应桶中。如果这个桶中已经有元素了,那么发生了碰撞,这样会在这个桶中形成一个链表。一般来说,当有数据要插入HashMap时,都会检查容量有没有超过设定的thredhold,如果超过,需要增大HashMap的尺寸,但是这样一来,就需要对整个HashMap里的节点进行重哈希操作。关于HashMap的重哈希操作本文不再详述,读者可以参考《Map 综述(一):彻头彻尾理解 HashMap》一文。在此,笔者借助陈皓的《疫苗:JAVA HASHMAP的死循环》一文说明HashMap线程不安全的典型表现 —— 死循环。    HashMap重哈希的关键源码如下:   /**      * Transfers all entries from current table to newTable.      */     void transfer(Entry[] newTable) {          // 将原数组 table 赋给数组 src         Entry[] src = table;         int newCapacity = newTable.length;          // 将数组 src 中的每条链重新添加到 newTable 中         for (int j = 0; j < src.length; j++) {             Entry<K,V> e = src[j];             if (e != null) {                 src[j] = null;   // src 回收                  // 将每条链的每个元素依次添加到 newTable 中相应的桶中                 do {                     Entry<K,V> next = e.next;                      // e.hash指的是 hash(key.hashCode())的返回值;                     // 计算在newTable中的位置,注意原来在同一条子链上的元素可能被分配到不同的桶中                     int i = indexFor(e.hash, newCapacity);                        e.next = newTable[i];                     newTable[i] = e;                     e = next;                 } while (e != null);             }         }     } 1、单线程环境下的重哈希过程演示   单线程情况下,rehash 不会出现任何问题,如上图所示。假设hash算法就是最简单的 key mod table.length(也就是桶的个数)。最上面的是old hash表,其中的Hash表桶的个数为2, 所以对于 key = 3、7、5 的键值对在 mod 2以后都冲突在table[1]这里了。接下来的三个步骤是,Hash表resize成4,然后对所有的键值对重哈希的过程。  2、多线程环境下的重哈希过程演示   假设我们有两个线程,我用红色和浅蓝色标注了一下,被这两个线程共享的资源正是要被重哈希的原来1号桶中的Entry链。我们再回头看一下我们的transfer代码中的这个细节:  do {     Entry<K,V> next = e.next;       // <--假设线程一执行到这里就被调度挂起了     int i = indexFor(e.hash, newCapacity);     e.next = newTable[i];     newTable[i] = e;     e = next; } while (e != null);   而我们的线程二执行完成了,于是我们有下面的这个样子:   注意,在Thread2重哈希后,Thread1的指针e和指针next分别指向了Thread2重组后的链表(e指向了key(3),而next指向了key(7))。此时,Thread1被调度回来执行:Thread1先是执行 newTalbe[i] = e;然后是e = next,导致了e指向了key(7),而下一次循环的next = e.next导致了next指向了key(3),如下图所示:   这时,一切安好。Thread1有条不紊的工作着:把key(7)摘下来,放到newTable[i]的第一个,然后把e和next往下移,如下图所示:   在此时,特别需要注意的是,当执行e.next = newTable[i]后,会导致 key(3).next 指向了 key(7),而此时的key(7).next 已经指向了key(3),环形链表就这样出现了,如下图所示。于是,当我们的Thread1调用HashMap.get(11)时,悲剧就出现了 —— Infinite Loop。   这是HashMap在并发环境下使用中最为典型的一个问题,就是在HashMap进行扩容重哈希时导致Entry链形成环。一旦Entry链中有环,势必会导致在同一个桶中进行插入、查询、删除等操作时陷入死循环。  三. ConcurrentHashMap 在 JDK 中的定义   为了更好的理解 ConcurrentHashMap 高并发的具体实现,我们先来了解它在JDK中的定义。ConcurrentHashMap类中包含两个静态内部类 HashEntry 和 Segment,其中 HashEntry 用来封装具体的K/V对,是个典型的四元组;Segment 用来充当锁的角色,每个 Segment 对象守护整个ConcurrentHashMap的若干个桶 (可以把Segment看作是一个小型的哈希表),其中每个桶是由若干个 HashEntry 对象链接起来的链表。总的来说,一个ConcurrentHashMap实例中包含由若干个Segment实例组成的数组,而一个Segment实例又包含由若干个桶,每个桶中都包含一条由若干个 HashEntry 对象链接起来的链表。特别地,ConcurrentHashMap 在默认并发级别下会创建16个Segment对象的数组,如果键能均匀散列,每个 Segment 大约守护整个散列表中桶总数的 1/16。  1、类结构定义    ConcurrentHashMap 继承了AbstractMap并实现了ConcurrentMap接口,其在JDK中的定义为:  public class ConcurrentHashMap<K, V> extends AbstractMap<K, V>         implements ConcurrentMap<K, V>, Serializable {      ... } 四. ConcurrentHashMap 的构造函数   ConcurrentHashMap 一共提供了五个构造函数,其中默认无参的构造函数和参数为Map的构造函数 为 Java Collection Framework 规范的推荐实现,其余三个构造函数则是 ConcurrentHashMap 专门提供的。  1、ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel)    该构造函数意在构造一个具有指定容量、指定负载因子和指定段数目/并发级别(若不是2的幂次方,则会调整为2的幂次方)的空ConcurrentHashMap,其相关源码如下:      /**      * Creates a new, empty map with the specified initial      * capacity, load factor and concurrency level.      *      * @param initialCapacity the initial capacity. The implementation      * performs internal sizing to accommodate this many elements.      * @param loadFactor  the load factor threshold, used to control resizing.      * Resizing may be performed when the average number of elements per      * bin exceeds this threshold.      * @param concurrencyLevel the estimated number of concurrently      * updating threads. The implementation performs internal sizing      * to try to accommodate this many threads.      * @throws IllegalArgumentException if the initial capacity is      * negative or the load factor or concurrencyLevel are      * nonpositive.      */     public ConcurrentHashMap(int initialCapacity,                              float loadFactor, int concurrencyLevel) {         if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)             throw new IllegalArgumentException();          if (concurrencyLevel > MAX_SEGMENTS)                           concurrencyLevel = MAX_SEGMENTS;          // Find power-of-two sizes best matching arguments         int sshift = 0;            // 大小为 lg(ssize)          int ssize = 1;            // 段的数目,segments数组的大小(2的幂次方)         while (ssize < concurrencyLevel) {             ++sshift;             ssize <<= 1;         }         segmentShift = 32 - sshift;      // 用于定位段         segmentMask = ssize - 1;      // 用于定位段         this.segments = Segment.newArray(ssize);   // 创建segments数组          if (initialCapacity > MAXIMUM_CAPACITY)             initialCapacity = MAXIMUM_CAPACITY;         int c = initialCapacity / ssize;    // 总的桶数/总的段数         if (c * ssize < initialCapacity)             ++c;         int cap = 1;     // 每个段所拥有的桶的数目(2的幂次方)         while (cap < c)             cap <<= 1;          for (int i = 0; i < this.segments.length; ++i)      // 初始化segments数组             this.segments[i] = new Segment<K,V>(cap, loadFactor);     }   从源码中首先可以知道,ConcurrentHashMap对Segment的put操作是加锁完成的。在第二节我们已经知道,Segment是ReentrantLock的子类,因此Segment本身就是一种可重入的Lock,所以我们可以直接调用其继承而来的lock()方法和unlock()方法对代码进行上锁/解锁。需要注意的是,这里的加锁操作是针对某个具体的Segment,锁定的也是该Segment而不是整个ConcurrentHashMap。因为插入键/值对操作只是在这个Segment包含的某个桶中完成,不需要锁定整个ConcurrentHashMap。因此,其他写线程对另外15个Segment的加锁并不会因为当前线程对这个Segment的加锁而阻塞。故而 相比较于 HashTable 和由同步包装器包装的HashMap每次只能有一个线程执行读或写操作,ConcurrentHashMap 在并发访问性能上有了质的提高。在理想状态下,ConcurrentHashMap 可以支持 16 个线程执行并发写操作(如果并发级别设置为 16),及任意数量线程的读操作。    在将Key/Value对插入到Segment之前,首先会检查本次插入会不会导致Segment中元素的数量超过阈值threshold,如果会,那么就先对Segment进行扩容和重哈希操作,然后再进行插入。重哈希操作暂且不表,稍后详述。第8和第9行的操作就是定位到段中特定的桶并确定链表头部的位置。第12行的while循环用于检查该桶中是否存在相同key的结点,如果存在,就直接更新value值;如果没有找到,则进入21行生成一个新的HashEntry并且把它链到该桶中链表的表头,然后再更新count的值(由于count是volatile变量,所以count值的更新一定要放在最后一步)。   到此为止,除了重哈希操作,ConcurrentHashMap的put操作已经介绍完了。此外,在ConcurrentHashMap中,修改操作还包括putAll()和replace()。其中,putAll()操作就是多次调用put方法,而replace()操作实现要比put()操作简单得多,此不赘述。  2、ConcurrentHashMap 的重哈希操作 : rehash()    上面叙述到,在ConcurrentHashMap中使用put操作插入Key/Value对之前,首先会检查本次插入会不会导致Segment中节点数量超过阈值threshold,如果会,那么就先对Segment进行扩容和重哈希操作。特别需要注意的是,ConcurrentHashMap的重哈希实际上是对ConcurrentHashMap的某个段的重哈希,因此ConcurrentHashMap的每个段所包含的桶位自然也就不尽相同。针对段进行rehash()操作的源码如下:       void rehash() {             HashEntry<K,V>[] oldTable = table;    // 扩容前的table             int oldCapacity = oldTable.length;             if (oldCapacity >= MAXIMUM_CAPACITY)   // 已经扩到最大容量,直接返回                 return;              /*              * Reclassify nodes in each list to new Map.  Because we are              * using power-of-two expansion, the elements from each bin              * must either stay at same index, or move with a power of two              * offset. We eliminate unnecessary node creation by catching              * cases where old nodes can be reused because their next              * fields won't change. Statistically, at the default              * threshold, only about one-sixth of them need cloning when              * a table doubles. The nodes they replace will be garbage              * collectable as soon as they are no longer referenced by any              * reader thread that may be in the midst of traversing table              * right now.              */              // 新创建一个table,其容量是原来的2倍             HashEntry<K,V>[] newTable = HashEntry.newArray(oldCapacity<<1);                threshold = (int)(newTable.length * loadFactor);   // 新的阈值             int sizeMask = newTable.length - 1;     // 用于定位桶             for (int i = 0; i < oldCapacity ; i++) {                 // We need to guarantee that any existing reads of old Map can                 //  proceed. So we cannot yet null out each bin.                 HashEntry<K,V> e = oldTable[i];  // 依次指向旧table中的每个桶的链表表头                  if (e != null) {    // 旧table的该桶中链表不为空                     HashEntry<K,V> next = e.next;                     int idx = e.hash & sizeMask;   // 重哈希已定位到新桶                     if (next == null)    //  旧table的该桶中只有一个节点                         newTable[idx] = e;                     else {                             // Reuse trailing consecutive sequence at same slot                         HashEntry<K,V> lastRun = e;                         int lastIdx = idx;                         for (HashEntry<K,V> last = next;                              last != null;                              last = last.next) {                             int k = last.hash & sizeMask;                             // 寻找k值相同的子链,该子链尾节点与父链的尾节点必须是同一个                             if (k != lastIdx) {                                 lastIdx = k;                                 lastRun = last;                             }                         }                          // JDK直接将子链lastRun放到newTable[lastIdx]桶中                         newTable[lastIdx] = lastRun;                          // 对该子链之前的结点,JDK会挨个遍历并把它们复制到新桶中                         for (HashEntry<K,V> p = e; p != lastRun; p = p.next) {                             int k = p.hash & sizeMask;                             HashEntry<K,V> n = newTable[k];                             newTable[k] = new HashEntry<K,V>(p.key, p.hash,                                                              n, p.value);                         }                     }                 }             }             table = newTable;   // 扩容完成         }   其实JDK官方的注释已经解释的很清楚了。由于扩容是按照2的幂次方进行的,所以扩展前在同一个桶中的元素,现在要么还是在原来的序号的桶里,或者就是原来的序号再加上一个2的幂次方,就这两种选择。根据本文前面对HashEntry的介绍,我们知道链接指针next是final的,因此看起来我们好像只能把该桶的HashEntry链中的每个节点复制到新的桶中(这意味着我们要重新创建每个节点),但事实上JDK对其做了一定的优化。因为在理论上原桶里的HashEntry链可能存在一条子链,这条子链上的节点都会被重哈希到同一个新的桶中,这样我们只要拿到该子链的头结点就可以直接把该子链放到新的桶中,从而避免了一些节点不必要的创建,提升了一定的效率。因此,JDK为了提高效率,它会首先去查找这样的一个子链,而且这个子链的尾节点必须与原hash链的尾节点是同一个,那么就只需要把这个子链的头结点放到新的桶中,其后面跟的一串子节点自然也就连接上了。对于这个子链头结点之前的结点,JDK会挨个遍历并把它们复制到新桶的链头(只能在表头插入元素)中。特别地,我们注意这段代码:  for (HashEntry<K,V> last = next;      last != null;      last = last.next) {     int k = last.hash & sizeMask;     if (k != lastIdx) {         lastIdx = k;         lastRun = last;     } } newTable[lastIdx] = lastRun;   在该代码段中,JDK直接将子链lastRun放到newTable[lastIdx]桶中,难道这个操作不会覆盖掉newTable[lastIdx]桶中原有的元素么?事实上,这种情形时不可能出现的,因为桶newTable[lastIdx]在子链添加进去之前压根就不会有节点存在,这还是因为table的大小是按照2的幂次方的方式去扩展的。假设原来table的大小是2^k大小,那么现在新table的大小是2^(k+1)大小,而定位桶的方式是:  // sizeMask = newTable.length - 1,即 sizeMask = 11...1,共k+1个1。 int idx = e.hash & sizeMask;   因此这样得到的idx实际上就是key的hash值的低k+1位的值,而原table的sizeMask也全是1的二进制,不过总共是k位,那么原table的idx就是key的hash值的低k位的值。所以,如果元素的hashcode的第k+1位是0,那么元素在新桶的序号就是和原桶的序号是相等的;如果第k+1位的值是1,那么元素在新桶的序号就是原桶的序号加上2^k。因此,JDK直接将子链lastRun放到newTable[lastIdx]桶中就没问题了,因为newTable中新序号处此时肯定是空的。  3、ConcurrentHashMap 的读取实现 :get(Object key)   与put操作类似,当我们从ConcurrentHashMap中查询一个指定Key的键值对时,首先会定位其应该存在的段,然后查询请求委托给这个段进行处理,源码如下: /**      * Returns the value to which the specified key is mapped,      * or {@code null} if this map contains no mapping for the key.      *      * <p>More formally, if this map contains a mapping from a key      * {@code k} to a value {@code v} such that {@code key.equals(k)},      * then this method returns {@code v}; otherwise it returns      * {@code null}.  (There can be at most one such mapping.)      *      * @throws NullPointerException if the specified key is null      */     public V get(Object key) {         int hash = hash(key.hashCode());         return segmentFor(hash).get(key, hash);     }   我们紧接着研读Segment中get操作的源码:      V get(Object key, int hash) {             if (count != 0) {            // read-volatile,首先读 count 变量                 HashEntry<K,V> e = getFirst(hash);   // 获取桶中链表头结点                 while (e != null) {                     if (e.hash == hash && key.equals(e.key)) {    // 查找链中是否存在指定Key的键值对                         V v = e.value;                         if (v != null)  // 如果读到value域不为 null,直接返回                             return v;                            // 如果读到value域为null,说明发生了重排序,加锁后重新读取                         return readValueUnderLock(e); // recheck                     }                     e = e.next;                 }             }             return null;  // 如果不存在,直接返回null         }   了解了ConcurrentHashMap的put操作后,上述源码就很好理解了。但是有一个情况需要特别注意,就是链中存在指定Key的键值对并且其对应的Value值为null的情况。在剖析ConcurrentHashMap的put操作时,我们就知道ConcurrentHashMap不同于HashMap,它既不允许key值为null,也不允许value值为null。但是,此处怎么会存在键值对存在且的Value值为null的情形呢?JDK官方给出的解释是,这种情形发生的场景是:初始化HashEntry时发生的指令重排序导致的,也就是在HashEntry初始化完成之前便返回了它的引用。这时,JDK给出的解决之道就是加锁重读,源码如下:   /**          * Reads value field of an entry under lock. Called if value          * field ever appears to be null. This is possible only if a          * compiler happens to reorder a HashEntry initialization with          * its table assignment, which is legal under memory model          * but is not known to ever occur.          */         V readValueUnderLock(HashEntry<K,V> e) {             lock();             try {                 return e.value;             } finally {                 unlock();             }         } 4、ConcurrentHashMap 存取小结    在ConcurrentHashMap进行存取时,首先会定位到具体的段,然后通过对具体段的存取来完成对整个ConcurrentHashMap的存取。特别地,无论是ConcurrentHashMap的读操作还是写操作都具有很高的性能:在进行读操作时不需要加锁,而在写操作时通过锁分段技术只对所操作的段加锁而不影响客户端对其它段的访问。  七. ConcurrentHashMap 读操作不需要加锁的奥秘   在本文第二节,我们介绍到HashEntry对象几乎是不可变的(只能改变Value的值),因为HashEntry中的key、hash和next指针都是final的。这意味着,我们不能把节点添加到链表的中间和尾部,也不能在链表的中间和尾部删除节点。这个特性可以保证:在访问某个节点时,这个节点之后的链接不会被改变,这个特性可以大大降低处理链表时的复杂性。与此同时,由于HashEntry类的value字段被声明是Volatile的,因此Java的内存模型就可以保证:某个写线程对value字段的写入马上就可以被后续的某个读线程看到。此外,由于在ConcurrentHashMap中不允许用null作为键和值,所以当读线程读到某个HashEntry的value为null时,便知道产生了冲突 —— 发生了重排序现象,此时便会加锁重新读入这个value值。这些特性互相配合,使得读线程即使在不加锁状态下,也能正确访问 ConcurrentHashMap。总的来说,ConcurrentHashMap读操作不需要加锁的奥秘在于以下三点:  用HashEntery对象的不变性来降低读操作对加锁的需求;  用Volatile变量协调读写线程间的内存可见性;  若读时发生指令重排序现象,则加锁重读;    由于我们在介绍ConcurrentHashMap的get操作时,已经介绍到了第三点,此不赘述。下面我们结合前两点分别从线程写入的两种角度 —— 对散列表做非结构性修改的操作和对散列表做结构性修改的操作来分析ConcurrentHashMap是如何保证高效读操作的。  1、用HashEntery对象的不变性来降低读操作对加锁的需求   非结构性修改操作只是更改某个HashEntry的value字段的值。由于对Volatile变量的写入操作将与随后对这个变量的读操作进行同步,所以当一个写线程修改了某个HashEntry的value字段后,Java内存模型能够保证读线程一定能读取到这个字段更新后的值。所以,写线程对链表的非结构性修改能够被后续不加锁的读线程看到。   对ConcurrentHashMap做结构性修改时,实质上是对某个桶指向的链表做结构性修改。如果能够确保在读线程遍历一个链表期间,写线程对这个链表所做的结构性修改不影响读线程继续正常遍历这个链表,那么读/写线程之间就可以安全并发访问这个ConcurrentHashMap。在ConcurrentHashMap中,结构性修改操作包括put操作、remove操作和clear操作,下面我们分别分析这三个操作:  clear操作只是把ConcurrentHashMap中所有的桶置空,每个桶之前引用的链表依然存在,只是桶不再引用这些链表而已,而链表本身的结构并没有发生任何修改。因此,正在遍历某个链表的读线程依然可以正常执行对该链表的遍历。  关于put操作的细节我们在上文已经单独介绍过,我们知道put操作如果需要插入一个新节点到链表中时会在链表头部插入这个新节点,此时链表中的原有节点的链接并没有被修改。也就是说,插入新的健/值对到链表中的操作不会影响读线程正常遍历这个链表。    下面来分析 remove 操作,先让我们来看看 remove 操作的源代码实现:      /**      * Removes the key (and its corresponding value) from this map.      * This method does nothing if the key is not in the map.      *      * @param  key the key that needs to be removed      * @return the previous value associated with <tt>key</tt>, or      *         <tt>null</tt> if there was no mapping for <tt>key</tt>      * @throws NullPointerException if the specified key is null      */     public V remove(Object key) {     int hash = hash(key.hashCode());         return segmentFor(hash).remove(key, hash, null);     }   同样地,在ConcurrentHashMap中删除一个键值对时,首先需要定位到特定的段并将删除操作委派给该段。Segment的remove操作如下所示:          /**          * Remove; match on key only if value null, else match both.          */         V remove(Object key, int hash, Object value) {             lock();     // 加锁             try {                 int c = count - 1;                       HashEntry<K,V>[] tab = table;                 int index = hash & (tab.length - 1);        // 定位桶                 HashEntry<K,V> first = tab[index];                 HashEntry<K,V> e = first;                 while (e != null && (e.hash != hash || !key.equals(e.key)))  // 查找待删除的键值对                     e = e.next;                  V oldValue = null;                 if (e != null) {    // 找到                     V v = e.value;                     if (value == null || value.equals(v)) {                         oldValue = v;                         // All entries following removed node can stay                         // in list, but all preceding ones need to be                         // cloned.                         ++modCount;                         // 所有处于待删除节点之后的节点原样保留在链表中                         HashEntry<K,V> newFirst = e.next;                         // 所有处于待删除节点之前的节点被克隆到新链表中                         for (HashEntry<K,V> p = first; p != e; p = p.next)                             newFirst = new HashEntry<K,V>(p.key, p.hash,newFirst, p.value);                           tab[index] = newFirst;   // 将删除指定节点并重组后的链重新放到桶中                         count = c;      // write-volatile,更新Volatile变量count                     }                 }                 return oldValue;             } finally {                 unlock();          // finally子句解锁             }         }   Segment的remove操作和前面提到的get操作类似,首先根据散列码找到具体的链表,然后遍历这个链表找到要删除的节点,最后把待删除节点之后的所有节点原样保留在新链表中,把待删除节点之前的每个节点克隆到新链表中。假设写线程执行remove操作,要删除链表的C节点,另一个读线程同时正在遍历这个链表,如下图所示:   我们可以看出,删除节点C之后的所有节点原样保留到新链表中;删除节点C之前的每个节点被克隆到新链表中(它们在新链表中的链接顺序被反转了)。因此,在执行remove操作时,原始链表并没有被修改,也就是说,读线程不会受同时执行 remove 操作的并发写线程的干扰。   综合上面的分析我们可以知道,无论写线程对某个链表进行结构性修改还是非结构性修改,都不会影响其他的并发读线程对这个链表的访问。 2、用 Volatile 变量协调读写线程间的内存可见性   一般地,由于内存可见性问题,在未正确同步的情况下,对于写线程写入的值读线程可能并不能及时读到。下面以写线程M和读线程N来说明ConcurrentHashMap如何协调读/写线程间的内存可见性问题,如下图所示:   假设线程M在写入了volatile变量count后,线程N读取了这个volatile变量。根据 happens-before 关系法则中的程序次序法则,A appens-before 于 B,C happens-before D。根据 Volatile法则,B happens-before C。结合传递性,则可得到:A appens-before 于 B; B appens-before C;C happens-before D。也就是说,写线程M对链表做的结构性修改对读线程N是可见的。虽然线程N是在未加锁的情况下访问链表,但Java的内存模型可以保证:只要之前对链表做结构性修改操作的写线程M在退出写方法前写volatile变量count,读线程N就能读取到这个volatile变量count的最新值。    事实上,ConcurrentHashMap就是一个Segment数组,而每个Segment都有一个volatile变量count去统计Segment中的HashEntry的个数。并且,在ConcurrentHashMap中,所有不加锁读方法在进入读方法时,首先都会去读这个count变量。比如我们在上一节提到的get方法:      V get(Object key, int hash) {             if (count != 0) {            // read-volatile,首先读 count 变量                 HashEntry<K,V> e = getFirst(hash);   // 获取桶中链表头结点                 while (e != null) {                     if (e.hash == hash && key.equals(e.key)) {    // 查找链中是否存在指定Key的键值对                         V v = e.value;                         if (v != null)  // 如果读到value域不为 null,直接返回                             return v;                            // 如果读到value域为null,说明发生了重排序,加锁后重新读取                         return readValueUnderLock(e); // recheck                     }                     e = e.next;                 }             }             return null;  // 如果不存在,直接返回null         } 3、小结    在ConcurrentHashMap中,所有执行写操作的方法(put、remove和clear)在对链表做结构性修改之后,在退出写方法前都会去写这个count变量;所有未加锁的读操作(get、contains和containsKey)在读方法中,都会首先去读取这个count变量。根据 Java 内存模型,对同一个 volatile 变量的写/读操作可以确保:写线程写入的值,能够被之后未加锁的读线程“看到”。这个特性和前面介绍的HashEntry对象的不变性相结合,使得在ConcurrentHashMap中读线程进行读取操作时基本不需要加锁就能成功获得需要的值。这两个特性以及加锁重读机制的互相配合,不仅减少了请求同一个锁的频率(读操作一般不需要加锁就能够成功获得值),也减少了持有同一个锁的时间(只有读到 value 域的值为 null 时 , 读线程才需要加锁后重读)。  八. ConcurrentHashMap 的跨段操作   在ConcurrentHashMap中,有些操作需要涉及到多个段,比如说size操作、containsValaue操作等。以size操作为例,如果我们要统计整个ConcurrentHashMap里元素的大小,那么就必须统计所有Segment里元素的大小后求和。我们知道,Segment里的全局变量count是一个volatile变量,那么在多线程场景下,我们是不是直接把所有Segment的count相加就可以得到整个ConcurrentHashMap大小了呢?显然不能,虽然相加时可以获取每个Segment的count的最新值,但是拿到之后可能累加前使用的count发生了变化,那么统计结果就不准了。所以最安全的做法,是在统计size的时候把所有Segment的put,remove和clean方法全部锁住,但是这种做法显然非常低效。那么,我们还是看一下JDK是如何实现size()方法的吧:  /**      * Returns the number of key-value mappings in this map.  If the      * map contains more than <tt>Integer.MAX_VALUE</tt> elements, returns      * <tt>Integer.MAX_VALUE</tt>.      *      * @return the number of key-value mappings in this map      */     public int size() {         final Segment<K,V>[] segments = this.segments;         long sum = 0;         long check = 0;         int[] mc = new int[segments.length];         // Try a few times to get accurate count. On failure due to         // continuous async changes in table, resort to locking.         for (int k = 0; k < RETRIES_BEFORE_LOCK; ++k) {             check = 0;             sum = 0;             int mcsum = 0;             for (int i = 0; i < segments.length; ++i) {                 sum += segments[i].count;                    mcsum += mc[i] = segments[i].modCount;  // 在统计size时记录modCount             }             if (mcsum != 0) {                 for (int i = 0; i < segments.length; ++i) {                     check += segments[i].count;                     if (mc[i] != segments[i].modCount) {  // 统计size后比较各段的modCount是否发生变化                         check = -1; // force retry                         break;                     }                 }             }             if (check == sum)// 如果统计size前后各段的modCount没变,且两次得到的总数一致,直接返回                 break;         }         if (check != sum) { // Resort to locking all segments  // 加锁统计             sum = 0;             for (int i = 0; i < segments.length; ++i)                 segments[i].lock();             for (int i = 0; i < segments.length; ++i)                 sum += segments[i].count;             for (int i = 0; i < segments.length; ++i)                 segments[i].unlock();         }         if (sum > Integer.MAX_VALUE)             return Integer.MAX_VALUE;         else             return (int)sum;     }   size方法主要思路是先在没有锁的情况下对所有段大小求和,这种求和策略最多执行RETRIES_BEFORE_LOCK次(默认是两次):在没有达到RETRIES_BEFORE_LOCK之前,求和操作会不断尝试执行(这是因为遍历过程中可能有其它线程正在对已经遍历过的段进行结构性更新);在超过RETRIES_BEFORE_LOCK之后,如果还不成功就在持有所有段锁的情况下再对所有段大小求和。事实上,在累加count操作过程中,之前累加过的count发生变化的几率非常小,所以ConcurrentHashMap的做法是先尝试RETRIES_BEFORE_LOCK次通过不锁住Segment的方式来统计各个Segment大小,如果统计的过程中,容器的count发生了变化,则再采用加锁的方式来统计所有Segment的大小。    那么,ConcurrentHashMap是如何判断在统计的时候容器的段发生了结构性更新了呢?我们在前文中已经知道,Segment包含一个modCount成员变量,在会引起段发生结构性改变的所有操作(put操作、 remove操作和clean操作)里,都会将变量modCount进行加1,因此,JDK只需要在统计size前后比较modCount是否发生变化就可以得知容器的大小是否发生变化。    至于ConcurrentHashMap的跨其他跨段操作,比如contains操作、containsValaue操作等,其与size操作的实现原理相类似,此不赘述。  九. 更多  如果读者需要要深入了解 HashMap,请移步我的另一篇博文《Map 综述(一):彻头彻尾理解 HashMap》。   更多关于哈希(Hash)和equals方法的介绍,请移步我的博文《Java 中的 ==, equals 与 hashCode 的区别与联系》。   更多关于Java内存模型与Volatile语义的介绍,请移步我的博文《Java 并发:volatile 关键字解析》。   更多关于Java Collection Framework各成员类的底层实现和原理的介绍,请见我的专栏《Java Collection Framework 源码剖析》。本专栏主要从源码角度剖析Java Collection Framework各成员类的底层实现原理和技巧,包括但不限于List、Map等经典容器类、ConcurrentHashMap等并发容器类,并说明各容器类间的区别、联系以及应用场景,以便不断加深对Java容器框架的理解。 ———————————————— 原文链接:https://blog.csdn.net/justloveyou_/article/details/72783008/ 
  • [技术干货] Hashtable与ConcurrentHashMap区别
    ConcurrentHashMap融合了hashtable和hashmap二者的优势。hashtable是做了同步的,hashmap未考虑同步。所以hashmap在单线程情况下效率较高。hashtable在的多线程情况下,同步操作能保证程序执行的正确性。但是hashtable每次同步执行的时候都要锁住整个结构。看下图:图左侧清晰的标注出来,lock每次都要锁住整个结构。ConcurrentHashMap正是为了解决这个问题而诞生的。ConcurrentHashMap锁的方式是稍微细粒度的。 ConcurrentHashMap将hash表分为16个桶(默认值),诸如get,put,remove等常用操作只锁当前需要用到的桶。试想,原来 只能一个线程进入,现在却能同时16个写线程进入(写线程才需要锁定,而读线程几乎不受限制,之后会提到),并发性的提升是显而易见的。更令人惊讶的是ConcurrentHashMap的读取并发,因为在读取的大多数时候都没有用到锁定,所以读取操作几乎是完全的并发操作,而写操作锁定的粒度又非常细,比起之前又更加快速(这一点在桶更多时表现得更明显些)。只有在求size等操作时才需要锁定整个表。而在迭代时,ConcurrentHashMap使用了不同于传统集合的快速失败迭代器的另一种迭代方式,我们称为弱一致迭代器。在这种迭代方式中,当iterator被创建后集合再发生改变就不再是抛出 ConcurrentModificationException,取而代之的是在改变时new新的数据从而不影响原有的数 据,iterator完成后再将头指针替换为新的数据,这样iterator线程可以使用原来老的数据,而写线程也可以并发的完成改变,更重要的,这保证了多个线程并发执行的连续性和扩展性,是性能提升的关键。下面分析ConcurrentHashMap的源码。主要是分析其中的Segment。因为操作基本上都是在Segment上的。先看Segment内部数据的定义。从上图可以看出,很重要的一个是table变量。是一个HashEntry的数组。Segment就是把数据存放在这个数组中的。除了这个量,还有诸如loadfactor、modcount等变量。看segment的get 函数的实现:加上hashentry的代码:可以看出,hashentry是一个链表型的数据结构。在segment的get函数中,通过getFirst函数得到第一个值,然后就是通过这个值的next,一路找到想要的那个对象。如果不空,则返回。如果为空,则可能是其他线程正在修改节点。比如上面说的弱一致迭代器在将指针更改为新值的过程。而之前的 get操作都未进行锁定,根据bernstein条件,读后写或写后读都会引起数据的不一致,所以这里要对这个e重新上锁再读一遍,以保证得到的是正确值。readValueUnderLock中就是用了lock()进行加锁。put操作已开始就锁住了整个segment。这是因为修改操作时不能并发的。同样,remove操作也是如此(类似put,一开始就锁住真个segment)。但要注意一点区别,中间那个for循环是做什么用的呢?(截图未完全,可以自己找找代码查看一下)。从代码来看,就是将定位之后的所有entry克隆并拼回前面去,但有必要吗?每次删除一个元素就要将那之前的元素克隆一遍?这点其实是由entry的不变性来决定的,仔细观察entry定义,发现除了value,其他 所有属性都是用final来修饰的,这意味着在第一次设置了next域之后便不能再改变它,取而代之的是将它之前的节点全都克隆一次。至于entry为什么要设置为不变性,这跟不变性的访问不需要同步从而节省时间有关。原文链接:https://blog.csdn.net/csdn_ds/article/details/72528283
  • [技术干货] 浅谈java集合中线程安全的类
    vector:就比arraylist多了个同步化机制(线程安全),因为效率较低,现在已经不太建议使用。在web应用中,特别是前台页面,往往效率(页面响应速度)是优先考虑的。 statck:堆栈类,先进后出 hashtable:就比hashmap多了个线程安全 Collections的synchronizedXxxx()方法包装的集合 ConcurrentXxxx:从jdk1.5提供,通过分段锁实现线程安全,使用这类方法既可以不怎么影响效率,又可以保证安全,建议使用。 具体原理可参照:http://blog.csdn.net/csdn_ds/article/details/72528283 除了这些之外,其他的都是非线程安全的类和接口。 线程安全的类其方法是同步的,每次只能一个访问。是重量级对象,效率较低,一般用的不多,只有在特殊情况下会考虑使用。 1、Vector、ArrayList、LinkList之间的区别 Vector : 基于Array的List,其实就是封装了Array所不具备的一些功能方便我们使用,它不可能走出Array的限制。性能也就不可能超越Array。所以,在可能的情况下,我们要多运用Array。另外很重要的一点就是Vector“synchronized”的,这个也是Vector和ArrayList的唯一的区别。 ArrayList:同Vector一样是一个基于数组实现的,但是不同的是ArrayList不是同步的。所以在性能上要比Vector优越一些,但是当运行到多线程环境中时,可需要自己在管理线程的同步问题。 LinkedList:LinkedList不同于前面两种List,它不是基于Array的,所以不受Array性能的限制。它每一个节点(Node)都包含两方面的内容:1.节点本身的数据(data);2.下一个节点的信息(nextNode)。所以当对LinkedList做添加,删除动作的时候就不用像基于Array的List一样,必须进行大量的数据移动。只要更改nextNode的相关信息就可以实现了。这就是LinkedList的优势。 2、HashTable跟HashMap的区别  HashTable是线程安全的,即HashTable的方法都提供了同步机制;HashMap不是线程安全的,即不提供同步机制 ;HashTable不允许插入空值,HashMap允许! 3、StringBuffer和StringBuilder的区别 StringBuffer是线程安全的,StringBuilder是线程不安全的。 题外:此处记录一下java中8中基本的数据类型:  byte、int、short、long、float、double、boolean、char ————————————————                     原文链接:https://blog.csdn.net/csdn_ds/article/details/72528300 
  • [技术干货] 2 JVM的内存管理(堆内存)
    我用思维导图对JVM的内存结构做简单的划分,如下图所示:下面我们对各个区进行说明。堆:也称heap堆区。堆是jvm内存中占用空间最大的一个区域。主要分为新生代、老年代、永久代(jdk1.8以后叫元空间,到1.9以后又被移除)新生代:在new一个对象时,会把堆新生代的内存空间进行判断,如果内存空间够则放入新生代(如果是大对象,例如数据很多的容器对象,有可能直接放入老年代)。如果内存空间不够放入该对象,则触发young gc,如果触发15次新生代空间还不够则把之前使用的数据迁移到老年代并释放新生代所有的空间。如果老年代的空间不够用,则进行full gc。如果老年代内存也不够用则抛出OOM并结束线程。gc的种类:minor GC(又叫young GC):用于收集年轻代中的非存活对象,在新生代空间不足时触发,young gc 可能会触发线程暂停。所以在一些并发比较高或者集中处理一些行为中java会出现卡顿的现象就是出于这个原因major gc:在老年代空间不足时触发。full gc的效率比较低,应尽量减少full gc的发生。目前只有CMS收集器有单独收集老年代的行为mixed GC(混合收集):主要收集年轻代和部分老年代的非存活对象。目前G1收集器采用这种行为。full gc:主要收集整个堆(新生代和老年代)中的非存活对象。System.gc()调用时候会触发full gc 。我们可以通过-XX:DisableExplicitGC来禁止System.gc()的行为。在多次young gc后把新生代存活的对象迁移到老年代(在一定条件下才会触发。更加详细的信息可参阅《hotspot实战》由于篇幅有限不在这里详述),如果老年代空间不足时会触发full gc。full gc后老年代内存还是不够用则OOM我们看下新生代的内存分配,如下图:对象的分配过程:新生成的对象在年轻代Eden区中分配内存,当Eden空间已满时,触发Minor GC,将不再被其他对象所引用的对象进行回收,存活下来的对象被转移到Survivor0区。Survivor0区满后触发Minor GC,将Survivor0区存活下来的对象转移到Survivor1区,同时,清空Survivor0区,保证总有一个Survivor区为空。经过多次Minor GC后,仍然存活的对象被转移到老年代,进入老年代的Minor GC次数可以通过参数-XX:MaxTenuringThreshold=<N>进行设置,默认为15次。当老年代已满时会触发Major GC(即:Full GC,因此执行Major GC时会先执行Minor GC)。分代收集的原因:将对象按照存活概率进行分类,主要是为了减少扫描范围和执行GC的频率,同时,对不同区域采用不同的回收算法,提高回收效率。年轻代中存在两块相同大小的Survivor区的原因:解决内存碎片化,即:保证分配对象(如:大对象)时有足够的连续内存空间。对象进入老年代的触发条件:对象的年龄达到15岁时。默认的情况下,对象经过15次Minor GC后会被转移到老年代中。对象进入老年代的Minor GC次数可以通过JVM参数:-XX:MaxTenuringThreshold进行设置,默认为15次。动态年龄判断。当一批存活对象的总大小超过Survivor区内存大小的50%时,按照年龄的大小(年龄大的存活对象优先转移)将部分存活对象转移到老年代中。大对象直接进入老年代 。当需要创建一个大于年轻代剩余空间的对象(如:一个超大数组)时,该对象会被直接存放到老年代中,可以通过参数-XX:PretenureSizeThreshold(默认值是0,即:任何对象都会先在年轻代分配内存)进行设置。Minor GC后的存活对象太多无法放入Survivor区时, 会将这些对象直接转移到老年代中。字符串常量池:字符串常量池是Java中的一个特殊的存储区域,用于存储字符串常量。在Java中,字符串常量是不可变的,因此可以被共享。这样可以减少内存的使用,提高程序的性能。在JDK8中,字符串常量池存储在堆中。静态变量:静态变量是指在类中定义的变量,它们的值在整个程序运行期间都不会改变。在JDK8中取消了永久代,方法区变成了一个逻辑上的区域,因此,静态变量的内存在堆中进行分配(JDK7及以前,静态变量的内存在永久代中进行分配)。它们的生命周期与类的生命周期相同。线程本地缓冲区:tlabTLAB(Thread Local Allocation Buffer)是Java虚拟机中的一个优化技术,主要用于提高对象的分配效率。每个线程都有自己的TLAB,用于分配对象。当一个线程需要分配对象时,它会先在自己的TLAB中分配,如果TLAB中的空间不足,则会向堆中申请空间。上面对内存的堆区进行了阐述。由于不同的jdk版本处理内存的方式不一样,会有些出入敬请谅解
  • [技术干货] 初识JVM
    想要对java虚拟机更深入的了解,可以查看《HotSpot实战》。需要电子版的请扫我头像关注我的个人号,发送000006领取电子书我们知道java程序是把java源文件编译成字节码.class文件,然后交给JVM执行。那么java到底是解释执行还是编译执行的语言呢?这个没有固定的答案,具体要要看用什么样的JVM。JVM把class文件编译成机器码执行那就是编译执行,如果JVM对class加载后由JVM解释执行就是解释执行。有的JVM即有yo解释执行也有编译执行。JVM的种类:hotspot jvm:这是最常用的JVM实现,由Oracle开发。HotSpot JVM提供了高效的执行引擎和垃圾回收机制,支持多种垃圾回收算法,如Parallel GC、CMS GC、G1 GC等。它广泛应用于服务器和桌面应用程序中‌openJ9 jvm:由IBM开发,专注于高性能和低内存消耗。OpenJ9 JVM在性能和资源利用方面表现出色,适用于需要高性能和资源优化的应用场景‌graalm: 由Oracle开发,是一个通用虚拟机,支持多种编程语言。GraalVM不仅支持Java,还支持其他语言如JavaScript、Python等,适用于需要多语言支持和高性能的应用‌zing jvm: 由Azul Systems开发,专注于低延迟和高吞吐量。Zing JVM在金融交易等对延迟要求极高的场景中表现出色‌dalvik jvm: 用于Android平台,是Android特有的JVM变体。Dalvik JVM优化了移动设备的资源利用,适用于Android应用程序的运行‌我们后面重点讲hotspot jvm也是应用最广泛的hotspot源码下载:cid:link_0犹豫篇幅限制,关于源码编译这里不再熬述。1    java虚拟机与程序的生命周期:    1.1  执行了System.exit()方法    1.2  程序正常执行结束    1.3 程序在执行过程中遇到了异常或错误异常中止(一般是主线程的异常中止)    1.4 由于操作系统的错误而导致java虚拟机进程的中止2    类加载,链接,初始化    2.1 加载:查找并加载类的二进制数据JVM规范规定类加载器在预料类将要被使用时预先加载它,有个预热的过程。类加载器在程序主动使用某一个类时才报告错误。加载完成以后进入到连接阶段    2.2 连接将已经读入到内存的二进制数据合并到虚拟机的运行环境中,然后进行一系列的验证,确保被加载类的正确性。    2.3 准备阶段在该阶段,java虚拟机为类的静态变量分配内存,并设置默认的初始值    2.4 解析该阶段java虚拟机会把类的二进制数据中的符号引用替换为直接引用    2.5 初始化为类的静态成员变量赋予正确的初始值3    java程序对类的使用主要分为两种    3.1 主动使用创建类的实例。例如:new Test()访问某个类或接口的静态变量调用类的静态方法反射初始化一个类的子类虚拟机启动时被表明为启动的类    3.2 被动使用除掉以上的情况属于被动使用,不会导致类的初始化。虚拟机实现必须在每个类或接口被java程序首次使用时才初始化。类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其方法存进运行时数据区的方法区内。然后在堆区创建一个Java.lang.Class对象,用来封装在类在方法区内的数据结构。
  • [互动交流] obs上传对象报错
    com.amazonaws.SdkClientException: Unable to execute HTTP request: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target https上传到obs报这个错,是因为我服务端没有安装obs的https证书吗
  • [技术干货] Java中的设计模式有哪些
    Java中的设计模式是一种被广泛应用于软件开发的解决方案,旨在解决常见的软件设计问题。设计模式分为三大类:创建型模式、结构型模式和行为型模式。以下是一些常见的设计模式及其原理:  1. 创建型模式 单例模式(Singleton Pattern)  原理:单例模式确保一个类只有一个实例,并提供一个全局访问点来获取这个实例。这通常用于需要全局唯一实例的场景,比如配置管理、数据库连接池等。  实现方式:  私有构造函数:防止外部直接实例化。 静态实例:类中持有一个私有的静态实例。 公共静态方法:提供一个全局访问点来获取实例,并确保线程安全(使用 synchronized 或双重检查锁定等技术)。 示例代码:  public class Singleton {     private static Singleton instance;          private Singleton() {         // 私有构造函数,防止外部实例化     }          public static synchronized Singleton getInstance() {         if (instance == null) {             instance = new Singleton(); // 创建实例         }         return instance;     } } 工厂方法模式(Factory Method Pattern)  原理:工厂方法模式定义一个用于创建对象的接口,但由子类决定实例化哪个类。工厂方法模式将对象的创建推迟到子类,从而实现了创建和使用的解耦。  实现方式:  抽象工厂类:定义一个工厂方法,返回抽象产品。 具体工厂类:实现工厂方法,创建具体产品。 抽象产品类:定义产品的公共接口。 具体产品类:实现产品接口。 示例代码:  // 抽象产品 interface Product {     void use(); }  // 具体产品 class ConcreteProductA implements Product {     public void use() {         System.out.println("Using Product A");     } }  class ConcreteProductB implements Product {     public void use() {         System.out.println("Using Product B");     } }  // 抽象工厂 abstract class Creator {     public abstract Product factoryMethod(); }  // 具体工厂 class ConcreteCreatorA extends Creator {     public Product factoryMethod() {         return new ConcreteProductA();     } }  class ConcreteCreatorB extends Creator {     public Product factoryMethod() {         return new ConcreteProductB();     } }  抽象工厂模式(Abstract Factory Pattern)  原理:抽象工厂模式提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们的具体类。它创建多个产品对象,每个产品属于一个产品族。  实现方式:  抽象工厂:定义创建产品对象的接口。 具体工厂:实现抽象工厂接口,生成具体的产品。 抽象产品:定义产品的接口。 具体产品:实现抽象产品接口。 示例代码:  // 抽象产品A interface AbstractProductA {     void operationA(); }  // 具体产品A1 class ProductA1 implements AbstractProductA {     public void operationA() {         System.out.println("Operation A1");     } }  // 具体产品A2 class ProductA2 implements AbstractProductA {     public void operationA() {         System.out.println("Operation A2");     } }  // 抽象工厂 interface AbstractFactory {     AbstractProductA createProductA(); }  // 具体工厂1 class ConcreteFactory1 implements AbstractFactory {     public AbstractProductA createProductA() {         return new ProductA1();     } }  // 具体工厂2 class ConcreteFactory2 implements AbstractFactory {     public AbstractProductA createProductA() {         return new ProductA2();     } } 2. 结构型模式 适配器模式(Adapter Pattern)  原理:适配器模式将一个类的接口转换成客户端所期望的另一种接口,使得原本因接口不匹配而不能一起工作的类可以一起工作。它通过包装一个对象,使其符合目标接口。  实现方式:  目标接口:客户端期望的接口。 适配者类:需要适配的类,具有目标接口所不具备的接口。 适配器类:实现目标接口,并将请求转发到适配者类。 示例代码:  // 目标接口 interface Target {     void request(); }  // 适配者类 class Adaptee {     public void specificRequest() {         System.out.println("Specific request");     } }  // 适配器类 class Adapter implements Target {     private Adaptee adaptee;      public Adapter(Adaptee adaptee) {         this.adaptee = adaptee;     }      public void request() {         adaptee.specificRequest();     } } 装饰者模式(Decorator Pattern)  原理:装饰者模式动态地给一个对象添加一些额外的职责。它比生成子类更灵活,允许在运行时为对象添加功能。  实现方式:  组件接口:定义对象的接口。 具体组件:实现组件接口的类。 装饰者抽象类:持有一个组件对象,并实现组件接口。 具体装饰者:扩展装饰者抽象类,为对象添加新功能。 示例代码:  // 组件接口 interface Component {     void operation(); }  // 具体组件 class ConcreteComponent implements Component {     public void operation() {         System.out.println("Concrete Component Operation");     } }  // 装饰者抽象类 abstract class Decorator implements Component {     protected Component component;      public Decorator(Component component) {         this.component = component;     }      public void operation() {         component.operation();     } }  // 具体装饰者 class ConcreteDecorator extends Decorator {     public ConcreteDecorator(Component component) {         super(component);     }      public void operation() {         super.operation();         addedBehavior();     }      private void addedBehavior() {         System.out.println("Added Behavior");     } } 代理模式(Proxy Pattern) 原理:代理模式为其他对象提供一种代理以控制对这个对象的访问。代理对象可以在客户端和真实对象之间起到中介作用,可以增加额外的功能或控制访问权限。 实现方式:  主题接口:定义真实对象和代理对象的共同接口。 真实主题:实现主题接口,定义实际的业务逻辑。 代理对象:实现主题接口,控制对真实主题的访问。 示例代码:  // 主题接口 interface Subject {     void request(); }  // 真实主题 class RealSubject implements Subject {     public void request() {         System.out.println("Real Subject Request");     } }  // 代理对象 class Proxy implements Subject {     private RealSubject realSubject;      public void request() {         if (realSubject == null) {             realSubject = new RealSubject();         }         realSubject.request();     } } 3. 行为型模式 策略模式(Strategy Pattern) 原理:策略模式定义一系列算法,将每一个算法封装起来,并使它们可以互换。策略模式让算法独立于使用它的客户端独立变化。 实现方式:  策略接口:定义所有支持的算法的共同接口。 具体策略:实现策略接口的具体算法。 上下文:使用策略对象,调用策略的方法。 示例代码:  // 策略接口 interface Strategy {     void execute(); }  // 具体策略A class ConcreteStrategyA implements Strategy {     public void execute() {         System.out.println("Strategy A");     } }  // 具体策略B class ConcreteStrategyB implements Strategy {     public void execute() {         System.out.println("Strategy B");     } }  // 上下文 class Context {     private Strategy strategy;      public Context(Strategy strategy) {         this.strategy = strategy;     }      public void executeStrategy() {         strategy.execute();     } } **观察者模式(Observer Pattern ———————————————— 原文链接:https://blog.csdn.net/dataiyangu/article/details/140572071 
总条数:739 到第
上滑加载中