带鱼
盘点认证框架 : SpringSecurity 基础篇
一 . 前言SpringSecurity 应该是最常见的认证框架了 , 处于Spring体系中使他能快速的上手 , 这一篇开始作为入门级开篇作 , 来浅浅的讲一下SpringSecurity 的整体结构.关于Security 的使用 , 官方文档已经太详细了, 建议直接查看 官方文档 , 作为开篇 , 不深入源码 , 只做体系的介绍~~后续会陆陆续续将笔记中的源码梳理出来 ,笔记很乱, 但愿等整理出体系!二 . Spring Security 知识体系Security中需要我们操作的成员大概可以分为以下几种 , 但是涉及的类远远不止他们Filter : 对请求拦截处理Provider : 对用户进行认证Token : 用户认证主体2.1 Spring Security 主要包结构spring-security-remoting : 提供与 Spring Remoting 的集成spring-security-web : 包含过滤器和相关的网络安全基础设施代码。spring-security-config : 包含安全命名空间解析代码和 Java 配置代码?- 使用 Spring Security xml 命名空间进行配置或 Srping Security 的 Java Configuration 支持spring-security-ldap : 该模块提供 LDAP 身份验证和配置代码spring-security-oauth2-core : 包含支持 OAuth 2.0授权框架和 OpenID Connect Core 1.0的核心类和接口spring-security-oauth2-client : 包含 Spring Security 对 OAuth 2.0授权框架和 OpenID Connect Core 1.0的客户端支持spring-security-oauth2-jose : 包含 Spring Security 对 JOSE (Javascript 对象签名和加密)框架的支持JSON Web Token (JWT)JSON Web Signature (JWS)JSON Web Encryption (JWE)JSON Web Key (JWK)spring-security-oauth2-resource-server : 包含 Spring Security 对 OAuth 2.0资源服务器的支持。它通过 OAuth 2.0承载令牌来保护 apispring-security-acl : 此模块包含专门的域对象 ACL 实现。它用于对应用程序中的特定域对象实例应用安全性spring-security-cas : 该模块包含 Spring Security 的 CAS 客户端集成spring-security-openid : 此模块包含 OpenID web 身份验证支持。它用于根据外部 OpenID 服务器对用户进行身份验证。spring-security-testspring-secuity-taglibs2.2 Spring Security 核心体系 FilterSpringSecurity 中存在很多Filter , 抛开一些底层的 , 一般业务中的Filter主要是为了控制以何种方式进行认证 . 一般的体系结构里面 , 都会循环处理 , 例如 CAS 中 , 就是通过 HandlerManager 进行 for each 循环 , 而 SpirngSecurity 中 , 同样通过 SecurityFilterChain 进行循环.SecurityFilterChain 在后期源码梳理的时候在详细介绍 , 这里先看张图 :FilterChainProxy 使用 SecurityFilterChain 来确定应该为此请求调用哪个 Spring 安全过滤器 ,FilterChainProxy 决定应该使用哪个 SecurityFilterChain。会调用第一个被匹配的SecurityFilterChain ,即匹配是有序的如果请求/api/messages/ 的URL,它将首先匹配 SecurityFilterChain0的 /api/** 模式,因此只会调用 SecurityFilterChain0,即使它也匹配 SecurityFilterChainn。如果请求/messages/ 的URL,它将不匹配 SecurityFilterChain0的 /api/** 模式,因此 FilterChainProxy 将继续尝试每个 SecurityFilterChain。假设没有其他的 SecurityFilterChai n实例匹配 SecurityFilterChainn 将被调用。 (即无匹配调用最后一个)已知的 Filter 类ChannelProcessingFilter
WebAsyncManagerIntegrationFilter
SecurityContextPersistenceFilter
HeaderWriterFilter
CorsFilter
CsrfFilter
LogoutFilter
OAuth2AuthorizationRequestRedirectFilter
Saml2WebSsoAuthenticationRequestFilter
X509AuthenticationFilter
AbstractPreAuthenticatedProcessingFilter
CasAuthenticationFilter
OAuth2LoginAuthenticationFilter
Saml2WebSsoAuthenticationFilter
UsernamePasswordAuthenticationFilter
OpenIDAuthenticationFilter
DefaultLoginPageGeneratingFilter
DefaultLogoutPageGeneratingFilter
ConcurrentSessionFilter
DigestAuthenticationFilter
BearerTokenAuthenticationFilter
BasicAuthenticationFilter
RequestCacheAwareFilter
SecurityContextHolderAwareRequestFilter
JaasApiIntegrationFilter
RememberMeAuthenticationFilter
AnonymousAuthenticationFilter
OAuth2AuthorizationCodeGrantFilter
SessionManagementFilter
ExceptionTranslationFilter
FilterSecurityInterceptor
SwitchUserFilter
2.3 Authentication 体系结构认证体系是核心的处理体系 , 包含以下主要类 :SecurityContextHolder : Spring Security 存储被验证者的详细信息的地方。SecurityContext : 从 SecurityContextHolder 获得,并包含当前经过身份验证的用户的身份验证。。Authentication : 可以作为 AuthenticationManager 的输入,以提供用户为身份验证或来自 SecurityContext 的当前用户提供的凭据。。GrantedAuthority : 在身份验证上授予主体的权限(即角色、范围等)。AuthenticationManager : 定义 Spring Security 的过滤器如何执行身份验证的 API。。ProviderManager : AuthenticationManager 最常用的实现。。Providationprovider : 由 ProviderManager 用于执行特定类型的身份验证。。AuthenticationEntryPoint : 用于从客户机请求凭证(即重定向到登录页面,发送 www 认证响应等)。AbstractAuthenticationProcessingFilter : 用作验证用户凭据的基本筛选器AccessDecisionManager : 由 AbstractSecurityInterceptor 调用,负责做出最终的访问控制决策后续我们会围绕以上类进行源码梳理:三 . SpringSeurity 案例以下是一个很简单的 Security 案例 : >>>>项目源码<<<<3.1 SpringSecurity 依赖和配置<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private AuthenticationSuccessHandler myAuthenticationSuccessHandler;
@Autowired
private AuthenticationFailureHandler myAuthenctiationFailureHandler;
@Bean
public UserService CustomerUserService() {
System.out.print("step1============");
return new UserService();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//BCryptPasswordEncoder().encode("123456")).roles("ADMIN");
auth.userDetailsService(CustomerUserService()).passwordEncoder(new BCryptPasswordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//此方法中进行了请求授权,用来规定对哪些请求进行拦截
//其中:antMatchers--使用ant风格的路径匹配
//regexMatchers--使用正则表达式匹配
http.authorizeRequests()
.antMatchers("/test/**").permitAll()
.antMatchers("/before/**").permitAll()
.antMatchers("/index").permitAll()
.antMatchers("/").permitAll()
.anyRequest().authenticated() //其它请求都需要校验才能访问
.and()
.formLogin()
.loginPage("/login") //定义登录的页面"/login",允许访问
.defaultSuccessUrl("/home") //登录成功后默认跳转到"list"
.successHandler(myAuthenticationSuccessHandler).failureHandler(myAuthenctiationFailureHandler).permitAll().and()
.logout() //默认的"/logout", 允许访问
.logoutSuccessUrl("/index")
.permitAll();
http.addFilterBefore(new BeforeFilter(), UsernamePasswordAuthenticationFilter.class);
}
@Override
public void configure(WebSecurity web) throws Exception {
//解决静态资源被拦截的问题
web.ignoring().antMatchers("/**/*.js", "/lang/*.json", "/**/*.css", "/**/*.js", "/**/*.map", "/**/*.html", "/**/*.png");
}
}
其中有几个主要的地方 :@EnableWebSecurity 干了什么 ?该配置将创建一个 servlet Filter,作为名为 springSecurityFilterChain 的 bean创建一个具有用户用户名和随机生成的登录到控制台的密码的 UserDetailsService bean为每个请求向 Servlet 容器注册名为 springSecurityFilterChain 的 bean 的 FilterTODO3.2 准备一个 UserDetailsServicepublic class UserService implements UserDetailsService {
//......
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Users user = userRepository.findByUsername(username);
// ....
return user;
}
一个基本的 Demo 就完成了 , 案例能怎么简单 , 其实主要是因为我们复用了以下的类 :UsernamePasswordAuthenticationFilterDaoAuthenticationProviderUsernamePasswordAuthenticationToken四 . 定制案例我们把整个结构再定制一下 , 满足我们本身的功能 :4.1 Step 1 : 定制一个 Filter// 我们复用 UsernamePasswordAuthenticationFilter , 将其进行部分定制
public class DatabaseAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
// 修改用户名为 account
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "account";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
private boolean postOnly = true;
public DatabaseAuthenticationFilter() {
super(new AntPathRequestMatcher("/database/login", "POST"));
}
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
// username 从下方方法获取
String username = obtainUsername(request);
String password = obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
// 核心 : 这里替换了 DatabaseUserToken
DatabaseUserToken authRequest = new DatabaseUserToken(username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
protected String obtainPassword(HttpServletRequest request) {
return request.getParameter(passwordParameter);
}
protected String obtainUsername(HttpServletRequest request) {
return request.getParameter(usernameParameter);
}
protected void setDetails(HttpServletRequest request,
DatabaseUserToken authRequest) {
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
}
public void setUsernameParameter(String usernameParameter) {
Assert.hasText(usernameParameter, "Username parameter must not be empty or null");
this.usernameParameter = usernameParameter;
}
public void setPasswordParameter(String passwordParameter) {
Assert.hasText(passwordParameter, "Password parameter must not be empty or null");
this.passwordParameter = passwordParameter;
}
public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}
public final String getUsernameParameter() {
return usernameParameter;
}
public final String getPasswordParameter() {
return passwordParameter;
}
}
4.2 Step 2 : 准备一个 TokenToken 是在Authentication 中传递的核心 , 它用于后续进行票据的认证public class DatabaseUserToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private final Object principal;
private String credentials;
private String type;
private Collection<? extends GrantedAuthority> authorities;
public DatabaseUserToken(Object principal, String credentials) {
super(null);
this.principal = principal;
this.credentials = credentials;
this.type = "common";
setAuthenticated(false);
}
public DatabaseUserToken(Object principal, String credentials, String type) {
super(null);
this.principal = principal;
this.credentials = credentials;
this.type = StringUtils.isEmpty(type) ? "common" : type;
setAuthenticated(false);
}
public DatabaseUserToken(Object principal, String credentials, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
setAuthenticated(false);
}
public DatabaseUserToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = null;
super.setAuthenticated(true); // must use super, as we override
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
/**
* @param isAuthenticated
* @throws IllegalArgumentException
*/
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
super.setAuthenticated(true);
}
@Override
public Object getCredentials() {
return credentials;
}
@Override
public Object getPrincipal() {
return principal;
}
}
4.3 Step 3 : 准备一个 Provider这里通过 supports 方法判断 token 是否符合 ,从而发起认证过程 (PS : 和 CAS 简直一个思路)
public class DatabaseAuthenticationProvider implements AuthenticationProvider {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private UserInfoService userInfoService;
@Autowired
private AntSSOConfiguration antSSOConfiguration;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
logger.info("------> auth database <-------");
String username = (authentication.getPrincipal() == null)
? "NONE_PROVIDED" : String.valueOf(authentication.getPrincipal());
String password = (String) authentication.getCredentials();
if (StringUtils.isEmpty(password)) {
throw new BadCredentialsException("密码不能为空");
}
UserInfo user = userInfoService.searchUserInfo(new UserInfoSearchTO<String>(username));
logger.info("------> this is [{}] user :{}<-------", username, String.valueOf(user));
if (null == user) {
logger.error("E----> error :{} --user not fount ", username);
throw new BadCredentialsException("用户不存在");
}
String encodePwd = "";
if (password.length() != 32) {
encodePwd = PwdUtils.AESencode(password, AlgorithmConfig.getAlgorithmKey());
logger.info("------> {} encode password is :{} <-------", password, encodePwd);
}
if (!encodePwd.equals(user.getPassword())) {
logger.error("E----> user check error");
throw new BadCredentialsException("用户名或密码不正确");
} else {
logger.info("user check success");
}
DatabaseUserToken result = new DatabaseUserToken(
username,
new BCryptPasswordEncoder().encode(password),
listUserGrantedAuthorities(user.getUserid()));
result.setDetails(authentication.getDetails());
logger.info("------> auth database result :{} <-------", JSONObject.toJSONString(result));
return result;
}
@Override
public boolean supports(Class<?> authentication) {
return (DatabaseUserToken.class.isAssignableFrom(authentication));
}
private Set<GrantedAuthority> listUserGrantedAuthorities(String uid) {
Set<GrantedAuthority> authorities = new HashSet<GrantedAuthority>();
if (StringUtils.isEmpty(uid)) {
return authorities;
}
authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
return authorities;
}
}
4.4 隐藏环节// 将 Provider 注入体系
auth.authenticationProvider(reflectionUtils.springClassLoad(item.getProvider()));
// 将 Filter 注入体系
AbstractAuthenticationProcessingFilter filter = reflectionUtils.classLoadReflect(item.getFilter());
filter.setAuthenticationManager(authenticationManager);
http.addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class);
以上的流程可以看到已经不需要 继承 UserService类了 . 重写这些足够我们去实现大部分业务逻辑 , 使用时在Provider 完成对应的认证方式即可五 . 总结Security 很好用, 在我的个人实践中 , 正在尝试将常见的协议进行整合 , 做成一个开源脚手架 , 个人感觉SpringSecuity 体系应该可以轻松的完成 .开篇比较简单 , 正在构思怎样才能从实践的角度将他讲清楚 , 笔记也在陆陆续续整理 , 争取下个月将整套文章发出来 !附录HttpSecurity 常用方法
带鱼
盘点认证框架 : 简单过一下 Shiro
一 . 前言之前说了 SpringSecurity , 也说了 Pac4j , 后续准备把 Shiro 和 CAS 也完善进来 , Shiro 整个框架结构比较简单 , 这一篇也只是简单过一下 , 不深入太多.1.1 基础知识Shiro 的基础知识推荐看官方文档 Shiro Doc , 这里就简单的罗列一下Shiro 具有很简单的体系结构 (Subject,SecurityManager 和 Realms) , 按照流程大概就是这样ApplicationCode --> Subject (Current User)
|
SecurityManager (Managers all Subject)
|
Realms
Shiro 的基石Shiro 自己内部定义了4个功能基石 , 分为身份验证、授权、会话管理和密码学Authentication : 身份认证 , 证明用户身份的行为Authorization : 访问控制的过程,即确定谁可以访问什么Session Management : 管理特定于用户的会话,即使是在非 web 或 EJB 应用程序中Cryptography : 使用加密算法来保证数据的安全,同时仍然易于使用以及一些额外的功能点:Web Support : Shiro 的 Web 支持 api 帮助简单地保护 Web 应用程序Caching : 缓存是 Apache Shiro API 中的第一层,用于确保安全操作保持快速和高效Concurrency : Apache Shiro 支持多线程应用程序及其并发特性Run As : 允许用户假设另一个用户的身份的特性 (我理解这就是代办)Remember Me : 记住我功能补充隐藏概念:Permission : 许可Role : 角色二 . 基本使用Shiro 的使用对我而言第一感觉就是干净 , 你不需要像 SpringSecurity 一样去关注很多配置 ,关注很多Filter , 也不需要像 CAS 源码一样走了很多 WebFlow , 所有的认证都是由你自己去完成的.2.1 配置类@Configuration
public class shiroConfig {
/**
* 配置 Realm
*
* @return
*/
@Bean
public CustomRealm myShiroRealm() {
CustomRealm customRealm = new CustomRealm();
return customRealm;
}
/**
* 权限管理,配置主要是Realm的管理认证
* @return
*/
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myShiroRealm());
return securityManager;
}
//Filter工厂,设置对应的过滤条件和跳转条件
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map<String, String> map = new HashMap<>();
// logout url
map.put("/logout", "logout");
//对所有用户认证
map.put("/**", "authc");
//登录
shiroFilterFactoryBean.setLoginUrl("/login");
//首页
shiroFilterFactoryBean.setSuccessUrl("/index");
//错误页面,认证不通过跳转
shiroFilterFactoryBean.setUnauthorizedUrl("/error");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}
/**
* 注册 SecurityManager
*
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
/**
* AOP 注解冲突解决方式
*
* @return
*/
@Bean
@ConditionalOnMissingBean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAAP = new DefaultAdvisorAutoProxyCreator();
defaultAAP.setProxyTargetClass(true);
return defaultAAP;
}
}
2.2 发起认证Shiro 发起认证很简答 , 完全是手动发起 Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(
user.getUserName(),
user.getPassword()
);
try {
//进行验证,这里可以捕获异常,然后返回对应信息
subject.login(usernamePasswordToken);
// subject.checkRole("admin");
// subject.checkPermissions("query", "add");
} catch (UnknownAccountException e) {
log.error("用户名不存在!", e);
return "用户名不存在!";
} catch (AuthenticationException e) {
log.error("账号或密码错误!", e);
return "账号或密码错误!";
} catch (AuthorizationException e) {
log.error("没有权限!", e);
return "没有权限";
}
因为是完全手动发起的 , 所以在集成 Shiro 的时候毫无压力 , 可以自行在外层封装任何的接口 , 也可以在接口中做任何的事情.2.3 校验逻辑public class CustomRealm extends AuthorizingRealm {
@Autowired
private LoginService loginService;
/**
* @MethodName doGetAuthorizationInfo
* @Description 权限配置类
* @Param [principalCollection]
* @Return AuthorizationInfo
* @Author WangShiLin
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//获取登录用户名
String name = (String) principalCollection.getPrimaryPrincipal();
//查询用户名称
User user = loginService.getUserByName(name);
//添加角色和权限
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
for (Role role : user.getRoles()) {
//添加角色
simpleAuthorizationInfo.addRole(role.getRoleName());
//添加权限
for (Permissions permissions : role.getPermissions()) {
simpleAuthorizationInfo.addStringPermission(permissions.getPermissionsName());
}
}
return simpleAuthorizationInfo;
}
/**
* @MethodName doGetAuthenticationInfo
* @Description 认证配置类
* @Param [authenticationToken]
* @Return AuthenticationInfo
* @Author WangShiLin
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
if (StringUtils.isEmpty(authenticationToken.getPrincipal())) {
return null;
}
//获取用户信息
String name = authenticationToken.getPrincipal().toString();
User user = loginService.getUserByName(name);
if (user == null) {
//这里返回后会报出对应异常
return null;
} else {
//这里验证authenticationToken和simpleAuthenticationInfo的信息
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(name, user.getPassword().toString(), getName());
return simpleAuthenticationInfo;
}
}
}
LoginServiceImpl 也简单贴一下 , 就是从数据源中获取用户而已@Service
public class LoginServiceImpl implements LoginService {
@Autowired
private PermissionServiceImpl permissionService;
@Override
public User getUserByName(String getMapByName) {
return getMapByName(getMapByName);
}
/**
* 模拟数据库查询
*
* @param userName 用户名
* @return User
*/
private User getMapByName(String userName) {
// 构建 Role 1
Role role = new Role("1", "admin", getAllPermission());
Set<Role> roleSet = new HashSet<>();
roleSet.add(role);
// 构建 Role 2
Role role1 = new Role("2", "user", getSinglePermission());
Set<Role> roleSet1 = new HashSet<>();
roleSet1.add(role1);
User user = new User("1", "root", "123456", roleSet);
Map<String, User> map = new HashMap<>();
map.put(user.getUserName(), user);
User user1 = new User("2", "zhangsan", "123456", roleSet1);
map.put(user1.getUserName(), user1);
return map.get(userName);
}
/**
* 权限类型一
*/
private Set<Permissions> getAllPermission() {
Set<Permissions> permissionsSet = new HashSet<>();
permissionsSet.add(permissionService.getPermsByUserId("1"));
permissionsSet.add(permissionService.getPermsByUserId("2"));
return permissionsSet;
}
/**
* 权限类型二
*/
private Set<Permissions> getSinglePermission() {
Set<Permissions> permissionsSet1 = new HashSet<>();
permissionsSet1.add(permissionService.getPermsByUserId("1"));
return permissionsSet1;
}
}
LoginServiceImpl其实都可以不算是 Shiro 整个认证体系的一员 ,它只是做一个 User 管理的业务而已 , 那么剩下了干了什么?写了一个 API 接口准备了一个 Realm通过 Subject 发起认证在接口上标注相关的注解整套流程下来 , 就是简单 , 便捷 , 很轻松的就集成了认证的功能.三 . 源码按照惯例 , 还是看一遍源码吧 ,我们按照四个维度来分析 :请求的拦截请求的校验认证的过程退出的过程3.1 请求的拦截这要从 ShiroFilterFactoryBean 这个类开始 @Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//....
//登录
shiroFilterFactoryBean.setLoginUrl("/login");
//首页
shiroFilterFactoryBean.setSuccessUrl("/index");
//错误页面,认证不通过跳转
shiroFilterFactoryBean.setUnauthorizedUrl("/error");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}
C- ShiroFilterFactoryBean
?- 构建 ShiroFilterFactoryBean 时会为其配置一个 login 地址
M- applyGlobalPropertiesIfNecessary : 配置全局属性
- applyLoginUrlIfNecessary(filter);
- applySuccessUrlIfNecessary(filter);
- applyUnauthorizedUrlIfNecessary(filter);
M- applyLoginUrlIfNecessary
?- 为 Filter 配置 loginUrl
- String existingLoginUrl = acFilter.getLoginUrl();
- acFilter.setLoginUrl(loginUrl)
// 这里对所有的 地址做了拦截
C01- PathMatchingFilter
F- protected Map<String, Object> appliedPaths = new LinkedHashMap<String, Object>();
?- 所有的path均会在这里处理
- 拦截成功了会调用 isFilterChainContinued , 最终会调用 onAccessDenied -> M2_01
C02- FormAuthenticationFilter
M2_01- onAccessDenied
-
// 判断是否需要重定向
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
if (isLoginRequest(request, response)) {
if (isLoginSubmission(request, response)) {
return executeLogin(request, response);
} else {
return true;
}
} else {
// 重定向到 login 页
saveRequestAndRedirectToLogin(request, response);
return false;
}
}
// 当然还有已登录得逻辑 , 已登录是在上面之前判断得
C- AccessControlFilter
M- onPreHandle
?- 该方法中会调用其他得 Filter 判断是否登录
// 例如这里就是 AuthenticationFilter 获取 Subject
C- AuthenticationFilter
M- isAccessAllowed
- Subject subject = getSubject(request, response);
- return subject.isAuthenticated() && subject.getPrincipal() != null;
整体的调用链大概是OncePerRequestFilter # doFilterAbstractShiroFilter # callAbstractShiroFilter # executeChainProxiedFilterChain # doFilterAdviceFilter # doFilterInternalPathMatchingFilter # preHandle最终会因为Filter 链 , 最终由 FormAuthenticationFilter 重定向出去3.2 拦截的方式
按照我们的常规思路 , 拦截仍然是通过 Filter 来完成
C- AbstractShiroFilter
M- doFilterInternal
- final Subject subject = createSubject(request, response) : 通过 请求构建了一个 Subject
- 调用 Subject 的 Callable 回调
- updateSessionLastAccessTime(request, response);
- executeChain(request, response, chain);
M- executeChain
- 执行 FilterChain 判断
-
// 这里往上追溯 , 可以看到实际上是一个 AOP 操作 : ReflectiveMethodInvocation
// 再往上就是 AopAllianceAnnotationsAuthorizingMethodInterceptor , 注意这里面是懒加载的
C03- AnnotationsAuthorizingMethodInterceptor : 通过 Interceptor 对方法进行拦截
M3_01- assertAuthorized : 断言认证信息
- 获取一个集合 Collection<AuthorizingAnnotationMethodInterceptor>
FOR- 循环 AuthorizingAnnotationMethodInterceptor -> PS301
- assertAuthorized -> M3_05
C- AuthorizingAnnotationMethodInterceptor
M3_05- assertAuthorized(MethodInvocation mi)
- ((AuthorizingAnnotationHandler)getHandler()).assertAuthorized(getAnnotation(mi))
- 这里是获取 Method 上面的 Annotation , 再调用 assertAuthorized 验证 -> M5_01
// 补充 : PS301
TODO
C05- RoleAnnotationHandler
M5_01- assertAuthorized(Annotation a)
- 如果不是 RequiresRoles , 则直接返回
- getSubject().checkRole(roles[0]) -> M6_02
- getSubject().checkRoles(Arrays.asList(roles));
?- 注意 , 这里是区别 And 和 Or 将 roles 分别处理
请求的逻辑 :3.3 一个完整的认证过程Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(user.getUserName(),user.getPassword());
subject.login(usernamePasswordToken);
// 来看一下 , 整个流程中做了什么
C06- DelegatingSubject
M6_01- login(AuthenticationToken token)
- Subject subject = securityManager.login(this, token) : 调用 securityManager 完成认证 :M8_01
M6_02- checkRole(String role)
- securityManager.checkRole(getPrincipals(), role) -> M10_04
C07- DefaultWebSecurityManager
M7_01-
C08- DefaultSecurityManager
M8_01- login(Subject subject, AuthenticationToken token)
- 底层调用 AuthorizingRealm 完成认证 , 此处的认证类为自定义的 CustomRealm
C09- AbstractAuthenticator
M9_01- authenticate(AuthenticationToken token)
M9_02-
C10- ModularRealmAuthenticator
M10_01- doAuthenticate(AuthenticationToken authenticationToken)
- 根据 Realm 数量 , 选择不同的认证类
- doSingleRealmAuthentication(realms.iterator().next(), authenticationToken) -> M10_02
- doMultiRealmAuthentication(realms, authenticationToken) -> M10_03
M10_02- doSingleRealmAuthentication
- AuthenticationInfo info = realm.getAuthenticationInfo(token)
M10_03- doMultiRealmAuthentication
M10_04- checkRole
- hasRole(principals, role) 判断是否 -> M10_05
M10_05- hasRole
FOR- getRealms() : 获取当前的 realms 类
- ((Authorizer) realm).hasRole(principals, roleIdentifier) : 调用 Reamlm 判断是否有 Role -> M11_04
C11- AuthenticatingRealm
M11_01- getAuthenticationInfo(AuthenticationToken token)
- getCachedAuthenticationInfo(token) : 获取缓存的 Authentication
- 调用 doGetAuthenticationInfo 进行实际的认证 : M12_02
- cacheAuthenticationInfoIfPossible 缓存认证信息
M11_02- cacheAuthenticationInfoIfPossible
- getAvailableAuthenticationCache() : 获取缓存集合
- getAuthenticationCacheKey(token) : 获取缓存 key
- cache.put(key, info) : 添加缓存
M11_03- assertCredentialsMatch
- CredentialsMatcher cm = getCredentialsMatcher();
- cm.doCredentialsMatch(token, info)
M11_04- hasRole
- AuthorizationInfo info = getAuthorizationInfo(principal)
-> M11_05
- hasRole(roleIdentifier, info)
M11_05- getAuthorizationInfo(PrincipalCollection principals)
?- 注意这里和 M11_01 的参数是不一样的
- getAvailableAuthorizationCache() 获取 Cache<Object, AuthorizationInfo>
- 如果 Cache 不为空 , getAuthorizationCacheKey 获取 key 后通过该key 从 Cache 里面获取 AuthorizationInfo
- 如果 缓存获取失败 , 则调用 doGetAuthorizationInfo(PrincipalCollection principals) 获取对象
?- 注意 , 这里要为其添加 Role -> M12_01
C12- CustomRealm
M12_01- AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection)
M12_02- AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken)
3.4 Logout 的流程logout 中做了哪些事 ?logout 最终会调用 subject logout 操作 public void logout() {
try {
// 本质上是从 Session 中移除 .RUN_AS_PRINCIPALS_SESSION_KEY
clearRunAsIdentitiesInternal();
this.securityManager.logout(this);
} finally {
// 把 Subject
this.session = null;
this.principals = null;
this.authenticated = false;
}
}
C- DefaultSecurityManager
M- logout
- beforeLogout(subject)
- subject.getPrincipals() 获取一个 PrincipalCollection
- 再获取一个 Authenticator , 通过它来 onLogout PrincipalCollection
?- ((LogoutAware) authc).onLogout(principals)
?- 需要这一步是因为可能存在 缓存和多模块登录 , 需要同时退出
// 最后移除 session , 删除 subject
- delete(subject);
- this.subjectDAO.delete(subject)
- stopSession(subject);
Shiro 的 logout 看起来也很清晰 , session 一关 , subject 一删 , 完毕 .甚至于都不用考虑是否需要重定向 , 一切都是业务自己决定.3.5 补充一 : Shiro 的异常体系很清晰 , Shiro 认证失败均会有响应的异常 , 由异常处理就可以决定业务的走向3.6 补充二 : 细说 DefaultSubjectDAODefaultSubjectDAO 是其中一个比较重要的逻辑 , 它负责处理 Subject 的相关持久化 , 当然使用者中我们可以做一个自己的实现类来处理Subject 的操作
private SessionStorageEvaluator sessionStorageEvaluator;
// 主要看一下其中的 CURD 操作
M- saveToSession(Subject subject)
- mergePrincipals(subject);
- mergeAuthenticationState(subject);
M- mergePrincipals
?- 将主体当前的Subject#getPrincipals()与可能在其中的任何元素合并到任何可用的会话
- currentPrincipals = subject.getPrincipals();
- Session session = subject.getSession(false);
- session.removeAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
- session.setAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY, currentPrincipals)
// 不用多说什么 , 很清晰的就能看到 , 将 currentPrincipals 设置到了可用的 Session 中 , 也就是说 , Principals 其实是在 Session 中流转
M- mergeAuthenticationState
- session = subject.getSession();
- session.setAttribute(DefaultSubjectContext.AUTHENTICATED_SESSION_KEY, Boolean.TRUE);
- session.removeAttribute(DefaultSubjectContext.AUTHENTICATED_SESSION_KEY);
// 一样的 , 通过 Session 控制
3.7 Subject 的管理逻辑之前看到 Subject 获取时 ,是通过 getSubject 获取的 , 看看这个类
// 看了这个类大概就知道 , 为什么 shiro 支持多线程
public static Subject getSubject() {
Subject subject = ThreadContext.getSubject();
if (subject == null) {
subject = (new Subject.Builder()).buildSubject();
ThreadContext.bind(subject);
}
return subject;
}
// PS : ThreadContext 是 Shiro 自己的工具类
// TODO : 这里先留一个小坑 , 多线程的处理逻辑还没有专门分析 , 后续进行补充
四 . 扩展- 自行定义一个 OAuth 流程因为Shiro 的特性 , 所以 OAuth 模式实际上是集成了其他的包参考自 @ www.e-learn.cn/topic/15938… , 这一节不全 , 建议参考原文Maven : 不要求一定是他们 , 其他的OAuth 实现均可<dependency>
<groupId>org.apache.oltu.oauth2</groupId>
<artifactId>org.apache.oltu.oauth2.authzserver</artifactId>
<version>1.0.2</version>
</dependency>
<dependency>
<groupId>org.apache.oltu.oauth2</groupId>
<artifactId>org.apache.oltu.oauth2.resourceserver</artifactId>
<version>1.0.2</version>
</dependency>
其他的整体而言多的就是2个接口 @RequestMapping("/authorize")
public Object authorize(Model model, HttpServletRequest request)
throws URISyntaxException, OAuthSystemException {
logger.info("------ > 第一步 进入验证申请", request.toString());
try {
logger.info("------ > 第二步 生成 OAuthAuthzRequest", request.toString());
OAuthAuthzRequest oauthRequest = new OAuthAuthzRequest(request);
//检查传入的客户端id是否正确
if (!oAuthService.checkClientId(oauthRequest.getClientId())) {
OAuthResponse response = OAuthASResponse
.errorResponse(HttpServletResponse.SC_BAD_REQUEST)
.setError(OAuthError.TokenResponse.INVALID_CLIENT)
.setErrorDescription(Constants.INVALID_CLIENT_DESCRIPTION)
.buildJSONMessage();
return new ResponseEntity(
response.getBody(), HttpStatus.valueOf(response.getResponseStatus()));
}
logger.info("step 3 获取 subject---:{}", SecurityUtils.getSubject().toString());
Subject subject = SecurityUtils.getSubject();
//如果用户没有登录,跳转到登陆页面
if (!subject.isAuthenticated()) {
if (!login(subject, request)) {//登录失败时跳转到登陆页面
model.addAttribute("client",
clientService.findByClientId(oauthRequest.getClientId()));
return "oauth2login";
}
}
logger.info("step 4 获取 username---:{}", (String) subject.getPrincipal());
String username = (String) subject.getPrincipal();
//生成授权码
String authorizationCode = null;
//responseType目前仅支持CODE,另外还有TOKEN
String responseType = oauthRequest.getParam(OAuth.OAUTH_RESPONSE_TYPE);
if (responseType.equals(ResponseType.CODE.toString())) {
OAuthIssuerImpl oauthIssuerImpl = new OAuthIssuerImpl(new MD5Generator());
authorizationCode = oauthIssuerImpl.authorizationCode();
logger.info("step 5 step -- authorizationCode :{}", authorizationCode);
oAuthService.addAuthCode(authorizationCode, username);
}
//进行OAuth响应构建
OAuthASResponse.OAuthAuthorizationResponseBuilder builder =
OAuthASResponse.authorizationResponse(request,
HttpServletResponse.SC_FOUND);
logger.info("step 5 step -- OAuthAuthorizationResponseBuilder :{}", builder);
//设置授权码
builder.setCode(authorizationCode);
//得到到客户端重定向地址
String redirectURI = oauthRequest.getParam(OAuth.OAUTH_REDIRECT_URI);
//构建响应
final OAuthResponse response = builder.location(redirectURI).buildQueryMessage();
//根据OAuthResponse返回ResponseEntity响应
HttpHeaders headers = new HttpHeaders();
headers.setLocation(new URI(response.getLocationUri()));
return new ResponseEntity(headers, HttpStatus.valueOf(response.getResponseStatus()));
} catch (OAuthProblemException e) {
//出错处理
logger.info("step 2 进入authorize OAuthAuthzRequest---:{}", request.toString());
String redirectUri = e.getRedirectUri();
if (OAuthUtils.isEmpty(redirectUri)) {
//告诉客户端没有传入redirectUri直接报错
return new ResponseEntity(
"OAuth callback url needs to be provided by client!!!", HttpStatus.NOT_FOUND);
}
//返回错误消息(如?error=)
final OAuthResponse response =
OAuthASResponse.errorResponse(HttpServletResponse.SC_FOUND)
.error(e).location(redirectUri).buildQueryMessage();
HttpHeaders headers = new HttpHeaders();
headers.setLocation(new URI(response.getLocationUri()));
return new ResponseEntity(headers, HttpStatus.valueOf(response.getResponseStatus()));
}
}
private boolean login(Subject subject, HttpServletRequest request) {
if ("get".equalsIgnoreCase(request.getMethod())) {
return false;
}
String username = request.getParameter("username");
String password = request.getParameter("password");
if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) {
return false;
}
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
try {
subject.login(token);
return true;
} catch (Exception e) {
request.setAttribute("error", "登录失败:" + e.getClass().getName());
return false;
}
}
AccessToken@RequestMapping("/accessToken")
public HttpEntity token(HttpServletRequest request)
throws URISyntaxException, OAuthSystemException {
try {
//构建OAuth请求
OAuthTokenRequest oauthRequest = new OAuthTokenRequest(request);
logger.info("step 1 OAuthTokenRequest request---:{}", oauthRequest.toString());
//检查提交的客户端id是否正确
if (!oAuthService.checkClientId(oauthRequest.getClientId())) {
OAuthResponse response = OAuthASResponse
.errorResponse(HttpServletResponse.SC_BAD_REQUEST)
.setError(OAuthError.TokenResponse.INVALID_CLIENT)
.setErrorDescription(Constants.INVALID_CLIENT_DESCRIPTION)
.buildJSONMessage();
return new ResponseEntity(
response.getBody(), HttpStatus.valueOf(response.getResponseStatus()));
}
// 检查客户端安全KEY是否正确
if (!oAuthService.checkClientSecret(oauthRequest.getClientSecret())) {
OAuthResponse response = OAuthASResponse
.errorResponse(HttpServletResponse.SC_UNAUTHORIZED)
.setError(OAuthError.TokenResponse.UNAUTHORIZED_CLIENT)
.setErrorDescription(Constants.INVALID_CLIENT_DESCRIPTION)
.buildJSONMessage();
return new ResponseEntity(
response.getBody(), HttpStatus.valueOf(response.getResponseStatus()));
}
logger.info("step 1 authCode request---:{}",oauthRequest.getParam(OAuth.OAUTH_CODE));
String authCode = oauthRequest.getParam(OAuth.OAUTH_CODE);
// 检查验证类型,此处只检查AUTHORIZATION_CODE类型,其他的还有PASSWORD或REFRESH_TOKEN
if (oauthRequest.getParam(OAuth.OAUTH_GRANT_TYPE).equals(
GrantType.AUTHORIZATION_CODE.toString())) {
if (!oAuthService.checkAuthCode(authCode)) {
OAuthResponse response = OAuthASResponse
.errorResponse(HttpServletResponse.SC_BAD_REQUEST)
.setError(OAuthError.TokenResponse.INVALID_GRANT)
.setErrorDescription("错误的授权码")
.buildJSONMessage();
return new ResponseEntity(
response.getBody(), HttpStatus.valueOf(response.getResponseStatus()));
}
}
//生成Access Token
OAuthIssuer oauthIssuerImpl = new OAuthIssuerImpl(new MD5Generator());
final String accessToken = oauthIssuerImpl.accessToken();
logger.info("step 1 accessToken request---:{}",accessToken);
logger.info("step 1 username data---:{}", oAuthService.getUsernameByAuthCode(authCode));
oAuthService.addAccessToken(accessToken,
oAuthService.getUsernameByAuthCode(authCode));
//生成OAuth响应
OAuthResponse response = OAuthASResponse
.tokenResponse(HttpServletResponse.SC_OK)
.setAccessToken(accessToken)
.setExpiresIn(String.valueOf(oAuthService.getExpireIn()))
.buildJSONMessage();
//根据OAuthResponse生成ResponseEntity
return new ResponseEntity(
response.getBody(), HttpStatus.valueOf(response.getResponseStatus()));
} catch (OAuthProblemException e) {
//构建错误响应
OAuthResponse res = OAuthASResponse
.errorResponse(HttpServletResponse.SC_BAD_REQUEST).error(e)
.buildJSONMessage();
return new ResponseEntity(res.getBody(), HttpStatus.valueOf(res.getResponseStatus()));
}
}
总结一下简单点说 , 就是 Shiro 对 OAuth 没有支持 ,而想要获得 OAuth 能力 , 就自己定制 , 只是把 Shiro 当成一个内部 SSO , 获取用户信息即可核心代码Subject subject = SecurityUtils.getSubject();
String username = (String) subject.getPrincipal();
总结Shiro 这一篇也完了 , 真的很浅 ,没讲什么深入的东西, 一大原因是 Shiro 的定位就是大道至简 .他只给你提供认证的能力 , 你也只需要把他当成一个内部 SSO , 通过相关方法认证 和 获取用户即可.同时 ,他提供了细粒度的支持 , 与其他项目耦合低 , 我们曾经就在存在一个 认证框架的时候去集成他的 细粒度能力 , 因为它通过手动登录 , 基本上没什么冲突 , 也很好用.
带鱼
盘点认证框架 : Pac4j 认证工具
一 . 前言这一篇来说说 Pac4j , 为什么说他是认证工具呢 ,因为它真的提供了满满的封装类 ,可以让大部分应用快速的集成完成 ,使用者不需要关系认证协议的流程 , 只需要请求和获取用户即可需要注意的是 , Pac4j 中多个不同的版本其实现差距较大 ,我的源码以 3.8.0 为主 ,分析其思想 , 然后再单独对比一下后续版本的优化 , 就不过多的深入源码细节了二 . 基础使用Pac4j 的一大特点就是为不同供应商提供了很完善的 Client , 基本上无需定制就可以实现认证的处理 , 但是这里我们尽量定制一个自己的流程 , 来看看 Pac4j 的一个定制流程是怎样的以OAuth 为例 :2.1 构建 Authoriza 请求我们先构建一个 Client ,用来发起请求 :OAuth20Configuration : 最原生的 OAuth 配置类 , 可以自行定制符合特定规范的配置类OAuth20Client : 最原生的客户端调用类 , 后面可以看到 pac4j 有很多定制的client 类public class OAuthService extends BasePac4jService {
private Logger logger = LoggerFactory.getLogger(this.getClass());
private final static String CLIENT_ID = "b45e4-41c0-demo";
private final static String CLIENT_SECRET = "0407581-ef15-f773-demo";
private final static String CALLBACK_URL = "http://127.0.0.1:8088/oauth/callback";
/**
* 执行 Authorization 请求
*
* @return
*/
public void doOAuthRequest(HttpServletRequest request, HttpServletResponse response) {
// Step 1 :构建请求 Client
OAuth20Configuration config = new OAuth20Configuration();
config.setApi(new DefaultOAuthAPI());
config.setProfileDefinition(new DefaultOAuthDefinition());
config.setScope("user");
config.setKey(CLIENT_ID);
config.setSecret(CLIENT_SECRET);
// Step 2 : 构建一个 Client
OAuth20Client client = new OAuth20Client();
// 补充完善属性
client.setConfiguration(config);
client.setCallbackUrl(CALLBACK_URL);
// Step 3 : 构建请求 , 这里通过 302 重定向
J2EContext context = new J2EContext(request, response);
client.redirect(context);
// Step 4 : 缓存数据
request.getSession().setAttribute("client", client);
}
}
注意 , 这里有个 DefaultOAuthAPI 和 DefaultOAuthDefinition , 定义的是 SSO 路径和 Profile 声明DefaultOAuthAPIDefaultOAuthAPI 中主要包含了请求的地址 , DefaultApi20 有2个抽象接口 , 我额外添加了一个自己的接口DefaultOAuthAPI 不做任何限制 , 可以把任何需要的接口都放进去 , 用于后续取用.public class DefaultOAuthAPI extends DefaultApi20 {
public String getRootEndpoint() {
return "http://127.0.0.1/sso/oauth2.0/";
}
@Override
public String getAccessTokenEndpoint() {
return getRootEndpoint() + "accessToken";
}
@Override
protected String getAuthorizationBaseUrl() {
return getRootEndpoint() + "authorize";
}
}
DefaultOAuthDefinition该声明相当于一个字典 , 用于翻译 profile 返回的数据整个类中做了下面这些事 :定义了 user profile 会返回的属性定义了各种转换类和映射定义了 profile 请求的地址定义了 转换数据的实际实现
public class DefaultOAuthDefinition extends OAuth20ProfileDefinition<DefaultOAuhtProfile, OAuth20Configuration> {
public static final String IS_FROM_NEW_LOGIN = "isFromNewLogin";
public static final String AUTHENTICATION_DATE = "authenticationDate";
public static final String AUTHENTICATION_METHOD = "authenticationMethod";
public static final String SUCCESSFUL_AUTHENTICATION_HANDLERS = "successfulAuthenticationHandlers";
public static final String LONG_TERM_AUTHENTICATION_REQUEST_TOKEN_USED = "longTermAuthenticationRequestTokenUsed";
public DefaultOAuthDefinition() {
super(x -> new DefaultOAuhtProfile());
primary(IS_FROM_NEW_LOGIN, Converters.BOOLEAN);
primary(AUTHENTICATION_DATE, new DefaultDateConverter());
primary(AUTHENTICATION_METHOD, Converters.STRING);
primary(SUCCESSFUL_AUTHENTICATION_HANDLERS, Converters.STRING);
primary(LONG_TERM_AUTHENTICATION_REQUEST_TOKEN_USED, Converters.BOOLEAN);
}
@Override
public String getProfileUrl(final OAuth2AccessToken accessToken, final OAuth20Configuration configuration) {
return ((DefaultOAuthAPI) configuration.getApi()).getRootEndpoint() + "/profile";
}
@Override
public DefaultOAuhtProfile extractUserProfile(final String body) {
final DefaultOAuhtProfile profile = newProfile();
// 参数从 attributes 中获取
final String attributesNode = "attributes 中获取";
JsonNode json = JsonHelper.getFirstNode(body);
if (json != null) {
profile.setId(ProfileHelper.sanitizeIdentifier(profile, JsonHelper.getElement(json, "id")));
json = json.get(attributesNode);
if (json != null) {
// 这里以 CAS 的返回做了不同的处理
if (json instanceof ArrayNode) {
final Iterator<JsonNode> nodes = json.iterator();
while (nodes.hasNext()) {
json = nodes.next();
final String attribute = json.fieldNames().next();
convertAndAdd(profile, PROFILE_ATTRIBUTE, attribute, JsonHelper.getElement(json, attribute));
}
} else if (json instanceof ObjectNode) {
final Iterator<String> keys = json.fieldNames();
while (keys.hasNext()) {
final String key = keys.next();
convertAndAdd(profile, PROFILE_ATTRIBUTE, key, JsonHelper.getElement(json, key));
}
}
} else {
raiseProfileExtractionJsonError(body, attributesNode);
}
} else {
raiseProfileExtractionJsonError(body);
}
return profile;
}
}
DefaultDateConverter该对象用于解析数据 , 例如此处解析时间类型
public class DefaultDateConverter extends DateConverter {
public DefaultDateConverter() {
super("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
}
@Override
public Date convert(final Object attribute) {
Object a = attribute;
if (a instanceof String) {
String s = (String) a;
int pos = s.lastIndexOf("[");
if (pos > 0) {
s = s.substring(0, pos);
pos = s.lastIndexOf(":");
if (pos > 0) {
s = s.substring(0, pos) + s.substring(pos + 1, s.length());
}
a = s;
}
}
return super.convert(a);
}
}
DefaultOAuhtProfile可以理解为一个 TO , 用于接收数据public class DefaultOAuhtProfile extends OAuth20Profile {
private static final long serialVersionUID = 1347249873352825528L;
public Boolean isFromNewLogin() {
return (Boolean) getAttribute(DefaultOAuthDefinition.IS_FROM_NEW_LOGIN);
}
public Date getAuthenticationDate() {
return (Date) getAttribute(DefaultOAuthDefinition.AUTHENTICATION_DATE);
}
public String getAuthenticationMethod() {
return (String) getAttribute(DefaultOAuthDefinition.AUTHENTICATION_METHOD);
}
public String getSuccessfulAuthenticationHandlers() {
return (String) getAttribute(DefaultOAuthDefinition.SUCCESSFUL_AUTHENTICATION_HANDLERS);
}
public Boolean isLongTermAuthenticationRequestTokenUsed() {
return (Boolean) getAttribute(DefaultOAuthDefinition.LONG_TERM_AUTHENTICATION_REQUEST_TOKEN_USED);
}
}
2.2 构建一个接收对象 @GetMapping("callback")
public void callBack(final HttpServletRequest request, final HttpServletResponse response) throws IOException {
logger.info("------> [SSO 回调 pac4j OAuth 逻辑] <-------");
// 从 Session 中获取缓存的对象
OAuth20Client client = (OAuth20Client) request.getSession().getAttribute("client");
J2EContext context = new J2EContext(request, response);
// 获取 AccessToken 对应的Credentials
final Credentials credentials = client.getCredentials(context);
// 通过 Profile 获取 Profile
final CommonProfile profile = client.getUserProfile(credentials, context);
// Web 返回数据信息
logger.info("------> Pac4j Demo 获取用户信息 :[{}] <-------", profile.toString());
response.getWriter().println(profile.toString());
}
总结一下就是 :DefaultOAuthAPI : 作为 metadata , 来标识请求的路径DefaultOAuthDefinition : 解释器用于解释返回的含义DefaultDateConverter : 用于转换数据DefaultOAuhtProfile : to 对象用于承载数据很简单的一个定制 , 可以适配多种不同的 OAuth 供应商三 . 源码一览3.1 OAuth 请求篇3.1.1 Authoriza 流程Authoriza 中核心类为 IndirectClient , 我们来简单看一下 IndirectClient的逻辑发起 Authoriza 认证C01- IndirectClient
M101- redirect(WebContext context)
?- 之前可以看到 , 我们调用 redirect 发起了请求
M102- getRedirectAction
- 如果请求类型是 ajaxRequest , 则由 ajaxRequestResolver 进行额外处理
-
// M101 伪代码
public final HttpAction redirect(WebContext context) {
RedirectAction action = this.getRedirectAction(context);
return action.perform(context);
}
// M102 伪代码
public RedirectAction getRedirectAction(WebContext context) {
this.init();
if (this.ajaxRequestResolver.isAjax(context)) {
RedirectAction action = this.redirectActionBuilder.redirect(context);
this.cleanRequestedUrl(context);
return this.ajaxRequestResolver.buildAjaxResponse(action.getLocation(), context);
} else {
String attemptedAuth = (String)context.getSessionStore().get(context, this.getName() + "$attemptedAuthentication");
if (CommonHelper.isNotBlank(attemptedAuth)) {
this.cleanAttemptedAuthentication(context);
this.cleanRequestedUrl(context);
throw HttpAction.unauthorized(context);
} else {
return this.redirectActionBuilder.redirect(context);
}
}
}
C02- RedirectActionBuilder (OAuth20RedirectActionBuilder)
M201- redirect
- 生成 state 且放入 session
2- this.configuration.buildService : 构建一个 OAuth20Service
3- 通过 param 属性获取一个 authorizationUrl
- RedirectAction.redirect(authorizationUrl) : 发起认证
// M201 伪代码
public RedirectAction redirect(WebContext context) {
//伪代码 , 通过 generateState 生成 state ,且会放入 session
String state=this.configuration.isWithState()?generateState : null;
// M201-2 : OAuth20Service 是 OAuth 的业务类
OAuth20Service service = (OAuth20Service)this.configuration.buildService(context, this.client, state);
// M201-3 : 设置了认证的地址
String authorizationUrl = service.getAuthorizationUrl(this.configuration.getCustomParams());
return RedirectAction.redirect(authorizationUrl);
}
// 到这里还没有看到实际请求的情况 ,我们再往底层看看
我们回到 M101 方法的 perform 中
C- RedirectAction
M- perform(WebContext context)
- this.type == RedirectAction.RedirectType.REDIRECT ? HttpAction.redirect(context, this.location) : HttpAction.ok(context, this.content);
// 再深入一层 , 真相大白了
public static HttpAction redirect(WebContext context, String url) {
context.setResponseHeader("Location", url);
context.setResponseStatus(302);
return new HttpAction(302);
}
他使用的是302 重定向的状态码 , 由浏览器完成重定向 , 这里的充电关系地址是
http://127.0.0.1/sso/oauth2.0/authorize?response_type=code&client_id=b7a8cc2a-5dec-4a78&redirect_uri=http%3A%2F%2F127.0.0.1%3A9081%2Fmfa-client%2Foauth%2Fcallback%3Fclient_name%3DCasOAuthWrapperClient
补充一 : OAuth20ServiceOAuth20Service 是一个 OAuth 业务类 , 其中包含常用的 OAuth 操作3.1.2 AccessToken 流程在上文中 ,我们为 OAuth 请求构建了一个 CallBack 方法 , SSO 认证完成后会回调该方法 , 我们来看看其中的一些有趣的点 : public void oauthCallBack(final HttpServletRequest request, final HttpServletResponse response) throws IOException {
// 这里可以和之前构建 Context 进行对比
// 当时将多个属性放在了session 中 , 这里就形成了一个循环 , 将状态发了回来
CasOAuthWrapperClient client = (CasOAuthWrapperClient) request.getSession().getAttribute("oauthClient");
// 第二步 : 获取 AccessToken
J2EContext context = new J2EContext(request, response);
final OAuth20Credentials credentials = client.getCredentials(context);
final CommonProfile profile = client.getUserProfile(credentials, context);
response.getWriter().println(profile.toString());
}
来看一看 getCredentials 方法干了什么C01- IndirectClient
M103- getCredentials(WebContext context)
- this.init() : OAuth 这一块主要是断言
- CommonHelper.assertNotBlank("key", this.key);
- CommonHelper.assertNotBlank("secret", this.secret);
- CommonHelper.assertNotNull("api", this.api);
- CommonHelper.assertNotNull("hasBeenCancelledFactory", this.hasBeenCancelledFactory);
- CommonHelper.assertNotNull("profileDefinition", this.profileDefinition);
public final C getCredentials(WebContext context) {
this.init();
C credentials = this.retrieveCredentials(context);
if (credentials == null) {
context.getSessionStore().set(context, this.getName() + "$attemptedAuthentication", "true");
} else {
this.cleanAttemptedAuthentication(context);
}
return credentials;
}
// 继续索引 , 可以看到更复杂得
C03- BaseClient
M301- retrieveCredentials
- this.credentialsExtractor.extract(context) : 获取一个 credentials 对象
?- 这个对象是之前 Authoriza 完成后返回的 Code 对象 :PS001
- this.authenticator.validate(credentials, context) : 发起校验
?- 这里是 OAuth20Authenticator , 详见
// 补充 PS001
#OAuth20Credentials# | code: OC-1-wVu2cc3p33ChsQshKd1rUabk6lggPB1QhWh | accessToken: null |
C04- OAuth20Authenticator
M401- retrieveAccessToken
- 从 OAuth20Credentials 获得 code
- 通过 OAuth20Configuration 构建 OAuth20Service , 调用 getAccessToken
// M401 伪代码 : 这里就很清楚了
protected void retrieveAccessToken(WebContext context, OAuthCredentials credentials) {
OAuth20Credentials oAuth20Credentials = (OAuth20Credentials)credentials;
String code = oAuth20Credentials.getCode();
this.logger.debug("code: {}", code);
OAuth2AccessToken accessToken;
try {
accessToken = ((OAuth20Service)((OAuth20Configuration)this.configuration).buildService(context, this.client, (String)null)).getAccessToken(code);
} catch (InterruptedException | ExecutionException | IOException var7) {
throw new HttpCommunicationException("Error getting token:" + var7.getMessage());
}
this.logger.debug("accessToken: {}", accessToken);
oAuth20Credentials.setAccessToken(accessToken);
}
C05- OAuth20Service
M501- getAccessToken
- OAuthRequest request = this.createAccessTokenRequest(code, pkceCodeVerifier);
- this.sendAccessTokenRequestSync(request);
M502- sendAccessTokenRequestSync
- (OAuth2AccessToken)this.api.getAccessTokenExtractor().extract(this.execute(request));
?- 点进去可以发现 ,其底层实际上是一个 HttpClient 调用
- httpClient.execute(userAgent, request.getHeaders(), request.getVerb(), request.getCompleteUrl(),request.getByteArrayPayload());
?- PS002
// PS002 补充 : 参数详见下图
http://127.0.0.1/sso/oauth2.0/accessToken?
3.1.3 UserInfo看了 AccessToken 的获取 , 再看看怎么换取 Userinfo
// Step 1 :起点方法
final CommonProfile profile = client.getUserProfile(credentials, context);
C03- BaseClient
M302- getUserProfile
- U profile = retrieveUserProfile(credentials, context);
M303- retrieveUserProfile
- this.profileCreator.create(credentials, context);
?- 这里是 OAuth20ProfileCreator : M601
C06- OAuthProfileCreator
M601- create
- T token = this.getAccessToken(credentials) : 获取了 Token
- return this.retrieveUserProfileFromToken(context, token);
M602- retrieveUserProfileFromToken
- final OAuthProfileDefinition<U, T, O> profileDefinition = configuration.getProfileDefinition();
?- OAuthProfileDefinition 用于构建请求 , 包括发送的类型等
- final String profileUrl = profileDefinition.getProfileUrl(accessToken, configuration);
?- profile 地址
- final S service = this.configuration.buildService(context, client, null);
?- 构建一个 Service
- final String body = sendRequestForData(service, accessToken, profileUrl, profileDefinition.getProfileVerb());
?- 请求 Profile , 这里实际上就已经调用拿到数据了
- final U profile = (U) configuration.getProfileDefinition().extractUserProfile(body);
?- 解析成 Profile 对象
- addAccessTokenToProfile(profile, accessToken);
?- 构建最后的对象
3.2 SAML 篇3.2.1 发起请求// Step 1 : 发起请求
- 构建一个 Configuration
- 构建一个 Client
- 因为 saml 的 API 都在 metadata 中 , 所以这里没有注入 API 的需求
--> 发起调用
RedirectAction action = client.getRedirectAction(context);
action.perform(context);
- return redirectActionBuilder.redirect(context);
?- 一样的套路 , 这里的 builder 是 SAML2RedirectActionBuilder
// 最后还是一样构建了一个 SAML 的 302 请求
看一下请求的结果3.2.2 接收数据后面仍然是一模一样的 , 只不过 Authenticator 变成了 SAML2Authenticator
final SAML2Client client = (SAML2Client) request.getSession().getAttribute("samlclient");
// 获取 J2EContext 对象
J2EContext context=new J2EContext(request,response);
final SAML2Credentials credentials = client.getCredentials(context);
// 获取 profile 数据
final CommonProfile profile = client.getUserProfile(credentials, context);
response.getWriter().println(profile.toString());
C- SAML2Authenticator
M- validate
- final SAML2Profile profile = getProfileDefinition().newProfile();
?- 获取返回的 Profile
- profile.addAuthenticationAttribute(SESSION_INDEX, credentials.getSessionIndex());
- profile.addAuthenticationAttribute(SAML_NAME_ID_FORMAT, nameId.getFormat());
- profile.addAuthenticationAttribute(SAML_NAME_ID_NAME_QUALIFIER, nameId.getNameQualifier());
- profile.addAuthenticationAttribute(SAML_NAME_ID_SP_NAME_QUALIFIER, nameId.getSpNameQualifier());
- profile.addAuthenticationAttribute(SAML_NAME_ID_SP_PROVIDED_ID, nameId.getSpProviderId());
?- 配置相关属性
// final CommonProfile profile = client.getUserProfile(credentials, context);
四 . 深入分析Pac4j 是一个很好的开源项目 , 从流程上讲 , 他拥有非常好的扩展性 (PS: 个人写产品很喜欢扩展性 , 什么都可配) , 这在开源里面是一个很大的优势 , 它的整体流程基本上可以看成以下几个部分Configuration 体系Client 体系Credentials 体系Profile 体系在这么多体系的情况下 ,通过 Context 完成整体容器的协调 , 在通过 RedirectAction 做统一的 请求重定向 .为什么专门提 RedirectAction 呢 ?因为我认为 pac4j 把所有的请求都抽象成了2个部分 , 一个是发起认证 , 一个的 callback 返回 ,以这2个直观的操作为边界 , 再在其中加入认证信息获取等操作 , 用户基本上对请求的调用是不可见的.五 . 开源分析那么 , 从 pac4j 里面 , 能学到哪些优点呢?首先 , pac4j 的定位是什么?pac4j 是一个认证工具 , 或者说 SDK , 他解决了认证过程的复杂性 , 使用者进行简单的调用就可以直接拿到用户信息.而他的第一个优点 , 就是兼容性和可配置性 , 我提供了了这么多 client , 你可以省事直接调 , 也可以自己定制 ,都没关系.从代码结构上说 , pac4j 的第二个优点就是结构清晰 .我们从上面的分析中 , 就能感觉到 , 能做什么 , 怎么做 , 怎么命名 ,其实都规约好了 , 进行简单的实现就可以满足要求.而我认为第三个优点 , 就是耦合性低.pac4j 采用的使 maven 聚合方式 , 想实现什么协议 , 就只引用相关包即可 . 代码与代码间的依赖度也低 , 这同样对定制有很大的好处, 值得学习.六. 总结pac4j 这工具 , 如果为了快速集成上线 , 确实是一个很理想的工具 ,个人在写demo 的时候 , 也经常用他做基础测试 , 别说 , 真挺好用代码已经发在 git 上面 , case 4.6.2 , 可以直接看.
带鱼
盘点认证框架 : SpringSecurity OAuth 篇
一 . 前言这一篇我们继续深入 SpringSecurity , 看看其 OAuth2.0 的流程逻辑.二 . 简易使用2.1 Maven 依赖<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- OAuth 包 -->
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.3.5.RELEASE</version>
</dependency>
2.2 配置项@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private List<AuthorizationServerConfigurer> configurers = Collections.emptyList();
@Autowired
private AuthenticationSuccessHandler myAuthenticationSuccessHandler;
@Autowired
private AuthenticationFailureHandler myAuthenctiationFailureHandler;
@Autowired
private AuthorizationServerEndpointsConfiguration endpoints;
@Autowired
private UserService userService;
@Autowired
private ClientDetailsService clientDetailsService;
@Bean
public AuthenticationManager authenticationManagerBean(DataSource dataSource) throws Exception {
OAuth2AuthenticationManager authenticationManager = new OAuth2AuthenticationManager();
authenticationManager.setTokenServices(new DefaultTokenServices());
authenticationManager.setClientDetailsService(new JdbcClientDetailsService(dataSource));
return authenticationManager;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//该方法用于用户认证,此处添加内存用户,并且指定了权限
auth.userDetailsService(userService).passwordEncoder(new BCryptPasswordEncoder());
}
@Autowired
public void configure(ClientDetailsServiceConfigurer clientDetails) throws Exception {
for (AuthorizationServerConfigurer configurer : configurers) {
configurer.configure(clientDetails);
}
}
@Override
protected void configure(HttpSecurity http) throws Exception {
AuthorizationServerSecurityConfigurer configurer = new AuthorizationServerSecurityConfigurer();
FrameworkEndpointHandlerMapping handlerMapping = endpoints.oauth2EndpointHandlerMapping();
http.setSharedObject(FrameworkEndpointHandlerMapping.class, handlerMapping);
configure(configurer);
http.apply(configurer);
String tokenEndpointPath = handlerMapping.getServletPath("/oauth/token");
String authorizeEndpointPath = handlerMapping.getServletPath("/oauth/authorize");
String tokenKeyPath = handlerMapping.getServletPath("/oauth/token_key");
String checkTokenPath = handlerMapping.getServletPath("/oauth/check_token");
if (!endpoints.getEndpointsConfigurer().isUserDetailsServiceOverride()) {
UserDetailsService userDetailsService = http.getSharedObject(UserDetailsService.class);
endpoints.getEndpointsConfigurer().userDetailsService(userDetailsService);
}
// PS : 注意 , OAuth 本身有一个 WebSecurityConfigurerAdapter ,我这里选择覆盖自定义
http.authorizeRequests()
.antMatchers("/test/**").permitAll()
.antMatchers("/before/**").permitAll()
.antMatchers("/index").permitAll()
.antMatchers(authorizeEndpointPath).authenticated()
.antMatchers(tokenEndpointPath).fullyAuthenticated()
.antMatchers(tokenKeyPath).access(configurer.getTokenKeyAccess())
.antMatchers(checkTokenPath).access(configurer.getCheckTokenAccess())
.anyRequest().authenticated() //其它请求都需要校验才能访问
.and()
.requestMatchers()
// .antMatchers(tokenEndpointPath, tokenKeyPath, checkTokenPath)
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER)
.and()
.formLogin()
.loginPage("/login") //定义登录的页面"/login",允许访问
.defaultSuccessUrl("/home") //登录成功后默认跳转到"list"
.successHandler(myAuthenticationSuccessHandler).failureHandler(myAuthenctiationFailureHandler).permitAll().and()
.logout() //默认的"/logout", 允许访问
.logoutSuccessUrl("/index")
.permitAll();
http.addFilterBefore(new BeforeFilter(), UsernamePasswordAuthenticationFilter.class);
http.setSharedObject(ClientDetailsService.class, clientDetailsService);
}
@Override
public void configure(WebSecurity web) throws Exception {
//解决静态资源被拦截的问题
web.ignoring().antMatchers("/**/*.js", "/lang/*.json", "/**/*.css", "/**/*.js", "/**/*.map", "/**/*.html", "/**/*.png");
}
protected void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
for (AuthorizationServerConfigurer configurer : configurers) {
configurer.configure(oauthServer);
}
}
}
Resource 资源配置@Configuration
@EnableResourceServer
public class ResServerConfig extends ResourceServerConfigurerAdapter {
@Autowired
private TokenStore tokenStore;
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources
.tokenStore(tokenStore).resourceId("resourceId");
}
@Override
public void configure(HttpSecurity http) throws Exception {
http
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.and()
.requestMatchers()
.antMatchers("/user", "/res/**")
.and()
.authorizeRequests()
.antMatchers("/user", "/res/**")
.authenticated();
}
}
OAuthConfig 专属属性@Configuration
@EnableAuthorizationServer
@Order(2)
public class OAuthConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Autowired
@Lazy
private AuthenticationManager authenticationManager;
@Bean
public TokenStore tokenStore() {
return new JdbcTokenStore(dataSource);
}
@Bean
public JdbcClientDetailsService jdbcClientDetailsService() {
return new JdbcClientDetailsService(dataSource);
}
@Bean
public ApprovalStore approvalStore() {
return new JdbcApprovalStore(dataSource);
}
@Bean
public AuthorizationCodeServices authorizationCodeServices() {
return new JdbcAuthorizationCodeServices(dataSource);
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(jdbcClientDetailsService());
}
//检查token的策略
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.allowFormAuthenticationForClients();
security.tokenKeyAccess("isAuthenticated()");
security.checkTokenAccess("permitAll()");
}
//OAuth2的主配置信息
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
// .approvalStore(approvalStore())
.authenticationManager(authenticationManager)
.authorizationCodeServices(authorizationCodeServices())
.tokenStore(tokenStore());
}
}
2.3 数据库详见项目2.4 使用方式请求方式http://localhost:8080/security/oauth/authorize?response_type=code&client_id=client&redirect_uri=http://www.baidu.com&scope=app
https://www.baidu.com/?code=jYgDO3
AccessTokenvar settings = {
"url": "http://localhost:8080/security/oauth/token",
"method": "POST",
"timeout": 0,
"headers": {
"Content-Type": "application/x-www-form-urlencoded"
},
"data": {
"grant_type": "authorization_code",
"client_id": "client",
"client_secret": "secret",
"code": "CFUFok",
"redirect_uri": "http://www.baidu.com"
}
};
$.ajax(settings).done(function (response) {
console.log(response);
});
// 失败
{
"error": "invalid_grant",
"error_description": "Invalid authorization code: CFUFok"
}
// 成功
{
"access_token": "c0955d7f-23fb-4ca3-8a52-c715867cbef2",
"token_type": "bearer",
"refresh_token": "55f53af0-1133-46dc-a32d-fbb9968e5938",
"expires_in": 7199,
"scope": "app"
}
check Tokenvar settings = {
"url": "http://localhost:8080/security/oauth/check_token?token=c0955d7f-23fb-4ca3-8a52-c715867cbef2",
"method": "GET",
"timeout": 0,
};
$.ajax(settings).done(function (response) {
console.log(response);
});
// 返回
{
"aud": [
"resourceId"
],
"exp": 1618241690,
"user_name": "gang",
"client_id": "client",
"scope": [
"app"
]
}
三 . 源码解析3.1 基础类TokenStore TokenStore 是一个接口 , 既然是一个接口 , 就意味着使用中是可以完全定制的public interface TokenStore {
// 通过 OAuth2AccessToken 对象获取一个 OAuth2Authentication
OAuth2Authentication readAuthentication(OAuth2AccessToken token);
OAuth2Authentication readAuthentication(String token);
// 持久化关联 OAuth2AccessToken 和 OAuth2Authentication
void storeAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication);
// OAuth2AccessToken 的获取和移除
OAuth2AccessToken readAccessToken(String tokenValue);
void removeAccessToken(OAuth2AccessToken token);
OAuth2AccessToken getAccessToken(OAuth2Authentication authentication);
// OAuth2RefreshToken 的直接操作
void storeRefreshToken(OAuth2RefreshToken refreshToken, OAuth2Authentication authentication);
OAuth2RefreshToken readRefreshToken(String tokenValue);
OAuth2Authentication readAuthenticationForRefreshToken(OAuth2RefreshToken token);
void removeRefreshToken(OAuth2RefreshToken token);
// 使用刷新令牌删除访问令牌 , 该方法会被用于控制令牌数量
void removeAccessTokenUsingRefreshToken(OAuth2RefreshToken refreshToken);
// Client ID 查询令牌
Collection<OAuth2AccessToken> findTokensByClientIdAndUserName(String clientId, String userName);
Collection<OAuth2AccessToken> findTokensByClientId(String clientId);
}
我们再来看看 TokenStore 主要的实现类 , 默认提供了 以下几种实现 :InMemoryTokenStore从名字就能看到 , 该类是将 Token 放在内存中 ,点进去就能看到 , 类中准备了大量的 ConcurrentHashMap , 保证多线程访问的安全性// 整体其实没什么看的 , 唯一有点特殊的就是 , 里面不是一个集合 , 而是每个业务一个集合 , 这其实相当于分库分表的处理思路
C- InMemoryTokenStore
- private final ConcurrentHashMap<String, OAuth2AccessToken> accessTokenStore
- private final ConcurrentHashMap<String, OAuth2AccessToken> authenticationToAccessTokenStore
- private final ConcurrentHashMap<String, Collection<OAuth2AccessToken>> userNameToAccessTokenStore
// 内部类 TokenExpiry
PSC- TokenExpiry implements Delayed
?- Delayed 是延迟处理的接口 , 用于判断 Token 是否过期
- private final long expiry;
- private final String value;
JdbcTokenStoreJdbcTokenStore 是可行的处理方式 , 但是并不是最优解 , 数据库处理对高并发 , 高性能会带来不小的挑战// 关键点一 : SQL 写死了 , 点开就能看到 , sql 是定死的 , 但是提供了 Set 方法 , 即可定制
private static final String DEFAULT_ACCESS_TOKEN_INSERT_STATEMENT = "insert into oauth_access_token (toke....."
private String insertAccessTokenSql = DEFAULT_ACCESS_TOKEN_INSERT_STATEMENT;
// 关键点二 : 使用 JDBCTemplate , 意味着常规Spring 配置即可
private final JdbcTemplate jdbcTemplate;
RedisTokenStoreRedis 存储 Token , 比较常见的存储方式 , 一般是首选方案 , 环境影响不能使用才会次选 JDBC冒号区分文件夹RedisConnectionFactory 需要 redis 包JdkSerializationStrategy 序列化策略后面说一下它的另外2个特别的实现类 , 他们不是一种持久化的方式JwkTokenStore提供了对使用JSON Web密钥(JWK)验证JSON Web令牌(JWT)的JSON Web签名(JWS)的支持令牌库实现专门用于资源服务器 , 唯一责任是解码JWT并使用相应的JWK验证其签名(JWS)从这个介绍大概就知道了 , 他是用于资源服务器的 , 他的主要目的是转换 , 所以点开后不难发现 , 里面有一个对象用于底层调用private final TokenStore delegate : 通过该对象再去处理底层的方式
// 常见的构造器
public JwkTokenStore(String jwkSetUrl)
public JwkTokenStore(List<String> jwkSetUrls)
public JwkTokenStore(String jwkSetUrl, AccessTokenConverter accessTokenConverter)
public JwkTokenStore(String jwkSetUrl, JwtClaimsSetVerifier jwtClaimsSetVerifier)
public JwkTokenStore(String jwkSetUrl, AccessTokenConverter accessTokenConverter,JwtClaimsSetVerifier jwtClaimsSetVerifier)
public JwkTokenStore(List<String> jwkSetUrls, AccessTokenConverter accessTokenConverter,JwtClaimsSetVerifier jwtClaimsSetVerifier)
扩展资料 :JWK RFC : tools.ietf.org/html/rfc751…JWT RFC : tools.ietf.org/html/rfc751…JWS RFC : tools.ietf.org/html/rfc751…JwtTokenStore这个对象其实是一个全新的体系 , 是 Token 的 JWT 实现 , 而不仅仅只是一种存储方式- private JwtAccessTokenConverter jwtTokenEnhancer;
- private ApprovalStore approvalStore;
- JdbcApprovalStore
- TokenApprovalStore
- InMemoryApprovalStore
可以看到提供了一个 转换的成员变量和一个 存储的 store 对象这里要注意的是 , 他只从令牌本身读取数据。不是真正的存储,它从不持久化任何东西.3.2 事件类型和处理事件用于推送 , 主要使用的有 DefaultAuthenticationEventPublisher ,我们来看看他DefaultAuthenticationEventPublisher从构造器里面可以看到大概的事件类型public DefaultAuthenticationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.applicationEventPublisher = applicationEventPublisher;
addMapping(BadCredentialsException.class.getName(),AuthenticationFailureBadCredentialsEvent.class);
addMapping(UsernameNotFoundException.class.getName(),AuthenticationFailureBadCredentialsEvent.class);
addMapping(AccountExpiredException.class.getName(),AuthenticationFailureExpiredEvent.class);
addMapping(ProviderNotFoundException.class.getName(),AuthenticationFailureProviderNotFoundEvent.class);
addMapping(DisabledException.class.getName(),AuthenticationFailureDisabledEvent.class);
addMapping(LockedException.class.getName(),AuthenticationFailureLockedEvent.class);
addMapping(AuthenticationServiceException.class.getName(),AuthenticationFailureServiceExceptionEvent.class);
addMapping(CredentialsExpiredException.class.getName(),AuthenticationFailureCredentialsExpiredEvent.class);
addMapping( "org.springframework.security.authentication.cas.ProxyUntrustedException",
AuthenticationFailureProxyUntrustedEvent.class);
}
M- publishAuthenticationSuccess
?- 发布认证成功事件
M- publishAuthenticationFailure
- AbstractAuthenticationEvent event = constructor.newInstance(authentication, exception);
?- 构建一个 AbstractAuthenticationEvent
- applicationEventPublisher.publishEvent(event)
?- 发布事件
M- setAdditionalExceptionMappings
?- 将额外的异常设置为事件映射。它们会自动与ProviderManager定义的事件映射的默认异常合并
3.3 Service 处理类ResourceServerTokenServices 接口public interface ResourceServerTokenServices {
OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException;
OAuth2AccessToken readAccessToken(String accessToken);
}
DefaultTokenServices 默认的 Token 处理类 , 没看到特别的东西, 主要是对 TokenStore 的调用RemoteTokenServices查询/check_token端点以获取访问令牌的内容。如果端点返回400响应,这表明令牌无效。C- RemoteTokenServices
F- private RestOperations restTemplate;
- 简单点说就是通过这个对象调用 check_token 接口查询 token 信息
- 注意 , 区别于本地类 , 这种方式目的应该是当前 OAuth 服务作为一个 SP 的情况
3.4 Token 管理体系TokenGranter 是一个接口 , 他有很多实现类,其中最常见的应该是 AuthorizationCodeTokenGranter 和 ImplicitTokenGranter , RefreshTokenGranterC- AuthorizationCodeTokenGranter
M- getOAuth2Authentication
- Paramters 中获取 Code , 并且判空 -> InvalidRequestException
- authorizationCodeServices.consumeAuthorizationCode(authorizationCode) : 通过 Code 获取 OAuth2Authentication
- 判断 redirectUri 和 clientId 是否存在 -> RedirectMismatchException/InvalidClientException
3.5 Token Conversion 体系C- DefaultAccessTokenConverter
?- 默认 Token 处理体系 ,我们来看一下主要做了什么
M- convertAccessToken
?- 可以看到 , 整个转换逻辑中会通过不同的开关 , 决定显示哪些
3.6 配置类详情OAuth 中除了原本的 User 概念 ,同时还有个 Client 概念 ,每个 Client 都可以看成一类待认证的对象 , **Spring OAuth 中提供了 OAuth 协议的自动配置 **, 主要包含2个类 :实现 User 的认证AuthorizationServerSecurityConfiguration extends WebSecurityConfigurerAdapter实现 Resource 的认证ResourceServerConfiguration extends WebSecurityConfigurerAdapter implements Ordered
// ResourceServerConfiguration
F- private TokenStore tokenStore; // token 管理实现
F- private AuthenticationEventPublisher eventPublisher; // 事件发布
F- private Map<String, ResourceServerTokenServices> tokenServices; // Token Service 集合
F- private ApplicationContext context;
F- private List<ResourceServerConfigurer> configurers = Collections.emptyList();
?- 这里的集合可以用于自己定制 ResourceServerConfigurer 类
F- private AuthorizationServerEndpointsConfiguration endpoints;
?- 对 EndPoint 接口做一个初始化操作
PSC- NotOAuthRequestMatcher
M- configure(HttpSecurity http)
?- 核心配置方法 , 主要生成了一个 ResourceServerSecurityConfigurer 放在 HttpSecurity 中
?- 这里实际上是克隆了一个当前对象给 HttpSecurity ,而不是一个引用
- 前面几步分别是 : 配置 tokenServices + tokenStore + eventPublisher
- 然后发现一个有意思的地方 : 从结构上讲 , 这应该算是装饰器的应用
for (ResourceServerConfigurer configurer : configurers) {
configurer.configure(resources);
}
- 后面几步开始对 HttpSecurity 本身做配置 , 分别是
- authenticationProvider : AnonymousAuthenticationProvider
- exceptionHandling
- accessDeniedHandler
- sessionManagement : session 管理
- sessionCreationPolicy
- 跨域处理 csrf
- 添加 requestMatcher
- 然后又发现了一个有趣的地方 , 双方互相持有对象
for (ResourceServerConfigurer configurer : configurers) {
configurer.configure(http);
}
// AuthorizationServerSecurityConfiguration
protected void configure(HttpSecurity http) throws Exception {
// 看样子和上面一样 , 构建一个新得 AuthorizationServerSecurityConfigurer 放入 HttpSecurity 中
AuthorizationServerSecurityConfigurer configurer = new AuthorizationServerSecurityConfigurer();
//
FrameworkEndpointHandlerMapping handlerMapping = endpoints.oauth2EndpointHandlerMapping();
http.setSharedObject(FrameworkEndpointHandlerMapping.class, handlerMapping);
// 装饰器持有对象 , 获得更多的扩展功能
configure(configurer);
http.apply(configurer);
// 此处就是获取 OAuth 的相关接口 , 并且在下面为其配置对应的权限要求
String tokenEndpointPath = handlerMapping.getServletPath("/oauth/token");
String tokenKeyPath = handlerMapping.getServletPath("/oauth/token_key");
String checkTokenPath = handlerMapping.getServletPath("/oauth/check_token");
if (!endpoints.getEndpointsConfigurer().isUserDetailsServiceOverride()) {
UserDetailsService userDetailsService = http.getSharedObject(UserDetailsService.class);
endpoints.getEndpointsConfigurer().userDetailsService(userDetailsService);
}
// 略.... 没什么关键的 , 都是通用的东西
http
.authorizeRequests()
.antMatchers(tokenEndpointPath).fullyAuthenticated()
.antMatchers(tokenKeyPath).access(configurer.getTokenKeyAccess())
.antMatchers(checkTokenPath).access(configurer.getCheckTokenAccess())
.and()
.requestMatchers()
.antMatchers(tokenEndpointPath, tokenKeyPath, checkTokenPath)
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER);
http.setSharedObject(ClientDetailsService.class, clientDetailsService);
}
四 . 运行逻辑看完了配置逻辑 , 我们来看一下主要的运行逻辑 :4.1 请求入口我们从请求的 入口 authorize 来开始 , 看看请求经过了哪些途径 , 来到了这里流程开始跳过一系列 Invoke 和 MVC 的流程 , 找到了 FilterChainProxy 类 , 大概就能知道 , OAuth 协议是在Filter 整体流程里面对请求进行的过滤下面来找一下是哪个过滤类讲请求拦截到登录页的 :Step 1 : SecurityContextPersistenceFilter这里主要是执行 Filter 链后在 finally 中处理Step 2 : BasicAuthenticationFilter可以看到 , 代码中做了这些事情C01- BasicAuthenticationFilter
String header = request.getHeader("Authorization");
?- Basic Z2FuZzoxMjM0NTY=
// 如果 header 为空 , 则继续执行 Filter
if (header == null || !header.toLowerCase().startsWith("basic ")) {
chain.doFilter(request, response);
return;
}
// 如果认证信息存在
- String[] tokens = extractAndDecodeHeader(header, request);
- String username = tokens[0];
- new UsernamePasswordAuthenticationToken(username, tokens[1]);
?- 构建一个 UsernamePasswordAuthenticationToken
- .... (PS : 这里的逻辑 Filter 详细说过了 , 就不反复说了)
- SecurityContextHolder.getContext().setAuthentication(authResult);
Filter 的逻辑其实之前就已经讲了 , 这里也就不太深入了其他扩展 FilterTokenEndpointAuthenticationFilterTokenEndpoint的可选身份验证过滤器。它位于客户端的另一个过滤器(通常是BasicAuthenticationFilter)的下游,如果请求也包含用户凭证,它就会为Spring SecurityContext创建一个OAuth2Authentication .如果使用这个过滤器,Spring安全上下文将包含一个OAuth2Authentication封装(作为授权请求)、进入过滤器的表单参数和来自已经经过身份验证的客户端身份验证的客户端id,以及从请求中提取并使用身份验证管理器验证的已验证用户令牌。OAuth2AuthenticationProcessingFilter针对OAuth2受保护资源的认证前过滤器。从传入请求提取一个OAuth2令牌,并使用它用OAuth2Authentication(如果与OAuth2AuthenticationManager一起使用)填充Spring安全上下文。4.2 接口详情注意 , 到这个接口时候 ,认证其实已经完成了 , 拦截的过程详见上文 Filter , 这一部分只分析内部的流程接口一 : authorizehttp://localhost:8080/security/oauth/authorize
C06- AuthorizationEndpoint
M601- authorize
P- Map<String, Object> model
P- Map<String, String> parameters : 传入的参数
P- SessionStatus sessionStatus
P- Principal principal : 因为实际上已经认证完了 , 所以能拿到 Principal
- getOAuth2RequestFactory().createAuthorizationRequest(parameters);
?- 通过 parameters 生成了一个 AuthorizationRequest , 该对象为认证过程中的流转对象
- authorizationRequest.getResponseTypes() : 获取 tResponseTypes 的Set<String>
?- 如果集合类型正确 -> UnsupportedResponseTypeException
?- TODO : 为什么是集合 ?
- authorizationRequest.getClientId() : 校验 ClientId 是否存在 -> InvalidClientException
- principal.isAuthenticated() : 校验是否认证 -> InsufficientAuthenticationException
- authorizationRequest.setRedirectUri(resolvedRedirect) : 生成并且设置重定向地址
?- 注意 , 这个地址此时还不带 Code
- oauth2RequestValidator.validateScope(authorizationRequest, client)
?- 校验当前 client 的作用域是否包含当前请求
- userApprovalHandler.checkForPreApproval(authorizationRequest,(Authentication) principal)
?- TODO
- userApprovalHandler.isApproved(authorizationRequest, (Authentication) principal)
?- 请求是否已被最终用户(或其他流程)批准
- getAuthorizationCodeResponse(authorizationRequest,(Authentication) principal)
?- ResponseType = code 时的最终处理逻辑 :M602
?- ResponseType = token 时的最终处理逻辑 :M605
M602- getAuthorizationCodeResponse
- getSuccessfulRedirect(authorizationRequest,generateCode(authorizationRequest, authUser)):M603
M603- getSuccessfulRedirect
- Map<String, String> query = new LinkedHashMap<String, String>();
- query.put("code", authorizationCode);
?- 插入 Code
- String state = authorizationRequest.getState();
- if (state != null) query.put("state", state);
?- 插入 State
M604- generateCode
- OAuth2Request storedOAuth2Request = getOAuth2RequestFactory().createOAuth2Request(authorizationRequest);
- OAuth2Authentication combinedAuth = new OAuth2Authentication(storedOAuth2Request, authentication);
- String code = authorizationCodeServices.createAuthorizationCode(combinedAuth);
?- 核心方法 , 注意 这里的 AuthorizationCodeServices 是一个接口 , 意味着该对象是可以自定义实现的
?- 这里的生成类是 RandomValueStringGenerator
// 补充 Token 模式
当使用 Implicit 模式进行认证的时候 , 这里是怎么处理的呢 ?
M605- getImplicitGrantResponse(authorizationRequest)
- TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(authorizationRequest, "implicit");
- OAuth2Request storedOAuth2Request = getOAuth2RequestFactory().createOAuth2Request(authorizationRequest);
- OAuth2AccessToken accessToken = getAccessTokenForImplicitGrant(tokenRequest, storedOAuth2Request);
?- 此方法中生成最后的 Token , 如果为空会抛出异常
- getTokenGranter().grant("implicit",new ImplicitTokenRequest(tokenRequest, storedOAuth2Request));
接口二 : AccessToken 接口 /oauth/tokenC07- TokenEndpoint
?- Token 的处理主要集中在该类中 , 该类中提供了 POST 和 GET 两种请求能力 , 这2种无明显区别
M701- postAccessToken(Principal principal,Map<String, String> parameters)
- 判断是否已经认证
- getClientDetailsService().loadClientByClientId(clientId)
?- 先获取 clientId , 再获取一个 ClientDetails
- getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient)
?- 构建出一个 TokenRequest
- 校验 Client ID , 再校验 ClientDetails 的 Scope 域
- 校验 GrantType 是否合理 , 不能为 空 , 不能为 implicit
- tokenRequest.setScope(OAuth2Utils.parseParameterList(parameters.get(OAuth2Utils.SCOPE)));
?-对 RefreshToken 类型 进行处理
- getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest)
?- 核心 , 生成 AccessToken , 详见上文 CodeToken 生成逻辑
- getResponse(token) : 生成一个 Response 对象
接口三 : CheckTokenEndpoint - /oauth/check_tokenC08- CheckTokenEndpoint
M801- checkToken
- OAuth2AccessToken token = resourceServerTokenServices.readAccessToken(value);
?- 通过 Token 获取 OAuth2AccessToken 对象
- OAuth2Authentication authentication = resourceServerTokenServices.loadAuthentication(token.getValue());
?- 如果 OAuth2AccessToken 存在且没过期 , 获取 OAuth2Authentication
- accessTokenConverter.convertAccessToken(token, authentication)
?- 返回用户信息
Client 核心处理Client 也是 OAuth 中一个非常核心的概念 , 毫无意外 , Client 的校验仍然是通过 Filter 处理的C- ClientCredentialsTokenEndpointFilter
M- attemptAuthentication
- String clientId = request.getParameter("client_id");
- String clientSecret = request.getParameter("client_secret");
- Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
?- 如果认证过了 , 则直接返回 (PS : 这里是 Client 认证)
- UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(clientId,clientSecret);
?- 构建一个 UsernamePasswordAuthenticationToken , 用于认证 Client
- his.getAuthenticationManager().authenticate(authRequest)
// 后面会调用 ProviderManager 用于认证处理
C- ProviderManager
M- authenticate
FOR - getProviders()
- result = provider.authenticate(authentication) : 完成认证
// DaoAuthenticationProvider
可以看到 , 这里完全是将 Client 当成一个用户在认证 , 整个体系都得到了通用业务处理之 : OAuth2ErrorHandler 异常处理TODOAuthenticationProvider 体系结构AuthenticationProvider 的整个体系结构异常庞大 , 他是认证的主体TODO4.3 其他重要工具类DefaultStateKeyGenerator默认state 构建工具private RandomValueStringGenerator generator = new RandomValueStringGenerator();
?- 使用的是随机数
4.4 异常类OAuth2AccessDeniedException当访问被拒绝时,我们通常想要一个403,但是我们想要与所有其他OAuth2Exception类型一样的处理UserApprovalRequiredException许可异常UserRedirectRequiredException抛出该异常 , 许可令牌重定向AccessTokenRequiredExceptionJwkException4.5 补充类OAuth2RestTemplateOAuth2 的定制 RestTemplate 使用所提供资源的凭据发出oauth2认证的Rest请求// 其中包含了一些和 OAuth 相关的定制
- appendQueryParameter : 构建token 请求的 parameter
- acquireAccessToken : 构建一个 OAuth2AccessToken ??
- getAccessToken : 必要情况下获取或更新当前上下文的访问令牌
- createRequest : 创建一个请求 , 会调用 DefaultOAuth2RequestAuthenticator 生成一个 Token
C- DefaultOAuth2RequestAuthenticator
?- 通过 AccessToken 生成一个 OAuth2Request
- Authorization Bearer ....
ProviderDiscoveryClient看这代码 , OAuth2 应该还支持 OIDC 呢 , 该类用于发现 OIDC 规范配置的提供者的客户端public ProviderConfiguration discover() {
// 发起请求
Map responseAttributes = this.restTemplate.getForObject(this.providerLocation, Map.class);
ProviderConfiguration.Builder builder = new ProviderConfiguration.Builder();
// 获取 OIDC 信息
builder.issuer((String)responseAttributes.get(ISSUER_ATTR_NAME));
builder.authorizationEndpoint((String)responseAttributes.get(AUTHORIZATION_ENDPOINT_ATTR_NAME));
if (responseAttributes.containsKey(TOKEN_ENDPOINT_ATTR_NAME)) {
builder.tokenEndpoint((String)responseAttributes.get(TOKEN_ENDPOINT_ATTR_NAME));
}
if (responseAttributes.containsKey(USERINFO_ENDPOINT_ATTR_NAME)) {
builder.userInfoEndpoint((String)responseAttributes.get(USERINFO_ENDPOINT_ATTR_NAME));
}
if (responseAttributes.containsKey(JWK_SET_URI_ATTR_NAME)) {
builder.jwkSetUri((String)responseAttributes.get(JWK_SET_URI_ATTR_NAME));
}
return builder.build();
}
// 基本上能看到这些 OIDC 的属性
private static final String PROVIDER_END_PATH = "/.well-known/openid-configuration";
private static final String ISSUER_ATTR_NAME = "issuer";
private static final String AUTHORIZATION_ENDPOINT_ATTR_NAME = "authorization_endpoint";
private static final String TOKEN_ENDPOINT_ATTR_NAME = "token_endpoint";
private static final String USERINFO_ENDPOINT_ATTR_NAME = "userinfo_endpoint";
private static final String JWK_SET_URI_ATTR_NAME = "jwks_uri";
4.6 业务的扩展定制我们最终的目的是为了知道哪些节点可以扩展 :Security 可以扩展的地方主要有这几类 :使用接口的地方TokenStoreResourceServerTokenServicesClientDetailsServiceAuthorizationServerConfigurer使用开关的地方扩展 Filter , 监听 AccessTokenOAuth2AuthenticationFailureEventOAuth2ClientAuthenticationProcessingFilter提供Set 方法的地方TODO : 这个光说没用 , 后续会尝试做个 Demo 出来
带鱼
盘点认证框架 : SpringSecurity Filter 篇
一 . 前言上一篇聊了聊 Secutity 的基础 , 这一篇我们聊一聊 Securiy Filter , 基本上 Security 常见得功能都能通过 Filter 找到相关的痕迹 .二 . 一个基本的 Filter 案例先来看看我们之前这么注册 Filter 的 >>>AbstractAuthenticationProcessingFilter filter = new DatabaseAuthenticationFilter();
filter.setAuthenticationManager(authenticationManagerBean());
http.addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class);
我们来追溯一下 Filter 怎么被加载进去的 :Step First : 将 Filter 加载到 Security 体系中// 添加到 HttpSecurity
C- HttpSecurity
- this.filters.add(filter) : 这里的 filters 仅仅是一个 List 集合
// 追溯代码可以看到这个集合会用于创建一个 DefaultSecurityFilterChain
protected DefaultSecurityFilterChain performBuild() throws Exception {
Collections.sort(this.filters, this.comparator);
return new DefaultSecurityFilterChain(this.requestMatcher, this.filters);
}
HttpSecurity 这个类我们后面会详细说说 , 这里了解到他其中维护了一个 Filter 集合即可 , 这个集合会被加载到 FilterChain 中Step 2 : Filter Chain 的使用方式// 成功标注断点后 , 可以追溯到整个的加载流程 :
// Step 1 : 要构建一个 springSecurityFilterChain
@Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
public Filter springSecurityFilterChain()throws Exception {
boolean hasConfigurers = webSecurityConfigurers != null
&& !webSecurityConfigurers.isEmpty();
if (!hasConfigurers) {
WebSecurityConfigurerAdapter adapter = objectObjectPostProcessor.postProcess(new WebSecurityConfigurerAdapter() {});
webSecurity.apply(adapter);
}
return webSecurity.build();
}
// Step 2 : webSecurity.build() 执行构建
public final O build() throws Exception {
// 居然还可以看到 CAS 操作 , 这里应该是设置绑定状态
if (this.building.compareAndSet(false, true)) {
// 执行 Build
this.object = doBuild();
return this.object;
}
throw new AlreadyBuiltException("This object has already been built");
}
// Step 3 : AbstractConfiguredSecurityBuilder 中
protected final O doBuild() throws Exception {
synchronized (configurers) {
buildState = BuildState.INITIALIZING;
// 为子类挂载钩子
beforeInit();
init();
buildState = BuildState.CONFIGURING;
// 在调用每个SecurityConfigurer#configure(SecurityBuilder)方法之前调用。
// 子类可以在不需要使用SecurityConfigurer时 , 覆盖这个方法来挂载到生命周期中
beforeConfigure();
configure();
buildState = BuildState.BUILDING;
// 实际构建对象
O result = performBuild();
buildState = BuildState.BUILT;
return result;
}
}
// Step 4 : 至此反射获取到 Filters 链
@Override
protected DefaultSecurityFilterChain performBuild() throws Exception {
Collections.sort(filters, comparator);
return new DefaultSecurityFilterChain(requestMatcher, filters);
}
可以看到 , 最开始添加到 Filter 集合的 Filter ,最终会用于构建 springSecurityFilterChain , 那么 springSecurityFilterChain 又是干什么的呢?三 . Security Filter ChainSecurity 的 Filter 和 WebFilter 本质是一样的 , 只是为了实现 Seccurity 的功能>>> 来看一下 FilterChain 的调用链 :3.1 FilterChain 的创建过程FilterChain 的核心是一个 VirtualFilterChain , 每个请求过来都会有一个VirtualFilterChain 生成VirtualFilterChain 是 FilterChainProxy 的内部类.// 补充 : VirtualFilterChain : 内部过滤器链实现,用于通过与请求匹配的额外内部过滤器列表传递请求
C- VirtualFilterChain
?- 每次运行的时候都会创建 , 来链表调用所有的 Filter
P- currentPosition : 当前运行 Filter 的下标
P- FirewalledRequest : 可用于拒绝潜在危险的请求和/或包装它们来控制它们的行为
P- List<Filter> additionalFilters : 包含所有的 Filter 对象
M- doFilter(ServletRequest request, ServletResponse response)
?- 这个方法会从2个维度来处理
1- currentPosition == size : 当执行最后一个的时候 , 先重置 FirewalledRequest , 再调用 originalChain
2- currentPosition != size : 在此之前依次执行 Filter 集合中的 doFilter
VirtualFilterChain 的创建流程 :
C- FilterChainProxy extends GenericFilterBean
?- GenericFilterBean 继承了 Filter 接口 , 其最终会由 SpringWeb 的 Filter 进行调用
M- doFilterInternal
- FirewalledRequest 的相关处理
- 创建了一个 VirtualFilterChain ,执行 Filter 链
// implements Filter
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
boolean clearContext = request.getAttribute(FILTER_APPLIED) == null;
if (clearContext) {
try {
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
doFilterInternal(request, response, chain);
}finally {
SecurityContextHolder.clearContext();
request.removeAttribute(FILTER_APPLIED);
}
}else {
doFilterInternal(request, response, chain);
}
}
private void doFilterInternal(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
FirewalledRequest fwRequest = firewall.getFirewalledRequest((HttpServletRequest) request);
HttpServletResponse fwResponse = firewall.getFirewalledResponse((HttpServletResponse) response);
List<Filter> filters = getFilters(fwRequest);
if (filters == null || filters.size() == 0) {
// 日志略...
fwRequest.reset();
chain.doFilter(fwRequest, fwResponse);
return;
}
// 这里创建了一个内部类 VirtualFilterChain
VirtualFilterChain vfc = new VirtualFilterChain(fwRequest, chain, filters);
// 类似于链表的方式 , 依次来调用
vfc.doFilter(fwRequest, fwResponse);
}
// 扩展 : FirewalledRequest
C- FirewalledRequest :
?- 这是一个配合防火墙功能的Request 实现类 , 其通常配合 HttpFirewall 来实现
M- reset : 重置方法,该方法允许在请求离开安全过滤器链时由FilterChainProxy重置部分或全部状态
C- StrictHttpFirewall
M- getFirewalledRequest
- rejectForbiddenHttpMethod(request) : 拒绝禁止的 HttpMethod
- rejectedBlacklistedUrls(request) : 拒绝黑名单 URL
- return new FirewalledRequest(request) : 这里创建了一个 FirewalledRequest , 不过 reset 是空实现
可以看到 , 这里每次执行 doFilterInternal 时都会创建一个 VirtualFilterChain .主要抽象类 AbstractAuthenticationProcessingFilterC- AbstractAuthenticationProcessingFilter
- !requiresAuthentication(request, response) : 确定是否匹配该请求
?- 注意 ,我们构建 DatabaseAuthenticationFilter 的时候其实是传入了一个Matcher匹配器的
- requiresAuthenticationRequestMatcher.matches(request);
- 不匹配则继续执行 FilterChain
- 匹配后继续执行
- Authentication authResult = attemptAuthentication(request, response);
// 这个方法是需要实现类复写的 , 在实现类中我们做了下面的事情
- 将 Request 中的验证信息 (账户密码, 如果需要扩展 ,可以是更多信息 , Cookie , Header 等等) 取出
- 构建了一个 Token (DatabaseUserToken)
- 将 Token 放入 Details 中
- 通过 AuthenticationManager 调用 ProviderManager 完成认证
// 具体的认证方式我们后续在详细分析
我之前以为 Security 的方式是把 所有的 Filter 走一遍后再执行 Provider , 从这里看来他采用的是Filter 适配后就直接执行 Provider3.2 WebAsyncManagerIntegrationFilter
C- WebAsyncManagerIntegrationFilter
?- 提供SecurityContext和Spring Web的webbasyncmanager之间的集成
?- SecurityContextCallableProcessingInterceptor#beforeConcurrentHandling 用于填充SecurityContext
C- 创建一个WebAsyncManager
?- 用于管理异步请求处理的中心类,主要用作SPI,通常不直接由应用程序类使用。
@Override
protected void doFilterInternal(HttpServletRequest request,HttpServletResponse response, FilterChain filterChain)throws ServletException, IOException {
// 创建一个WebAsyncManager
// 用于管理异步请求处理的中心类,主要用作SPI,通常不直接由应用程序类使用。
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
SecurityContextCallableProcessingInterceptor securityProcessingInterceptor = (SecurityContextCallableProcessingInterceptor) asyncManager
.getCallableInterceptor(CALLABLE_INTERCEPTOR_KEY);
// 如果没有 SecurityContextCallableProcessingInterceptor , 则创建一个注入 WebAsyncManager
if (securityProcessingInterceptor == null) {
asyncManager.registerCallableInterceptor(CALLABLE_INTERCEPTOR_KEY,new SecurityContextCallableProcessingInterceptor());
}
filterChain.doFilter(request, response);
}
3.3 SecurityContextPersistenceFilter 体系注意 , SecurityContext 是整个认证的核心 , 拥有 SecurityContext 即表示认证成功C- SecurityContextPersistenceFilter
?- 这是一个必选的Filter , 其目的是为了往 SecurityContextHolder 中插入一个 SecurityContext , SecurityContext 是最核心的认证容器
- SecurityContext contextBeforeChainExecution = repo.loadContext(holder);
?- 注意 , 这里会尝试获取Sesssion 的 Context , 用于验证
- SecurityContextHolder.setContext(contextBeforeChainExecution)
?- 设置一个 Context
- chain.doFilter(holder.getRequest(), holder.getResponse());
- finally 中会在所有filter 完成后 , 往 SecurityContextHolder 插入一个 contextAfterChainExecution
?- 注意前面是 contextBeforeChainExecution
// finally 代码一览
finally {
SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
// 清除 Context
SecurityContextHolder.clearContext();
// 重新保存新得 Context
repo.saveContext(contextAfterChainExecution, holder.getRequest(),holder.getResponse());
request.removeAttribute(FILTER_APPLIED);
}
3.4 HeaderWriterFilter 体系List<HeaderWriter> headerWriters : 构造对象的时候会写入一个list核心方法 : doFilterInternal准备了2个 HeaderWriterResponse , HeaderWriterRequest , 他们支持对Header 的二次封装 (原本的 Servlet 是不支持的)filterChain 完成后会执行 headerWriterResponse.writeHeaders();// eaderWriterResponse.writeHeaders() 是从 HeadersConfigurer 中获取的
C- HeadersConfigurer
M- private List<HeaderWriter> getHeaderWriters() {
List<HeaderWriter> writers = new ArrayList<>();
addIfNotNull(writers, contentTypeOptions.writer);
addIfNotNull(writers, xssProtection.writer);
addIfNotNull(writers, cacheControl.writer);
addIfNotNull(writers, hsts.writer);
addIfNotNull(writers, frameOptions.writer);
addIfNotNull(writers, hpkp.writer);
addIfNotNull(writers, contentSecurityPolicy.writer);
addIfNotNull(writers, referrerPolicy.writer);
addIfNotNull(writers, featurePolicy.writer);
writers.addAll(headerWriters);
return writers;
}
3.5 LogoutFilterLogoutFilter 允许定制LogoutHandler , 这一点在构造函数里面就能看到可以看到 , 默认使用 logout 地址作为拦截请求public LogoutFilter(LogoutSuccessHandler logoutSuccessHandler,
LogoutHandler... handlers) {
this.handler = new CompositeLogoutHandler(handlers);
Assert.notNull(logoutSuccessHandler, "logoutSuccessHandler cannot be null");
this.logoutSuccessHandler = logoutSuccessHandler;
setFilterProcessesUrl("/logout");
}
// LogoutFilter doFilter 逻辑
M- doFilter
?- 只要的操作就是调用handler 执行 logout 逻辑 , 并且调用 LogoutSuccess 逻辑
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
if (requiresLogout(request, response)) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
// 执行 LogoutHandler 的实现类
this.handler.logout(request, response, auth);
// 执行 LogoutSuccessHandler 的实现类
logoutSuccessHandler.onLogoutSuccess(request, response, auth);
return;
}
chain.doFilter(request, response);
}
// PS : 这个 Filter 有一定局限性 , 无法处理多个 Handler , 可以考虑定制一个 Filter
// Handler 实现类我们后续再深入
3.6 CsrfFilter之前了解到 , 为了使同步器令牌模式能够防止 CSRF 攻击,必须在 HTTP 请求中包含实际的 CSRF 令牌。这必须包含在浏览器不会自动包含在 HTTP 请求中的请求的一部分(即表单参数、 HTTP 头等)中。Spring Security 的 CsrfFilter 将一个 CsrfToken 作为一个名为 _csrf 的 HttpServletRequest 公开属性
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
request.setAttribute(HttpServletResponse.class.getName(), response);
// 加载 CsrfToken
CsrfToken csrfToken = this.tokenRepository.loadToken(request);
final boolean missingToken = csrfToken == null;
if (missingToken) {
// 缺失则重新创建一个
csrfToken = this.tokenRepository.generateToken(request);
this.tokenRepository.saveToken(csrfToken, request, response);
}
request.setAttribute(CsrfToken.class.getName(), csrfToken);
request.setAttribute(csrfToken.getParameterName(), csrfToken);
if (!this.requireCsrfProtectionMatcher.matches(request)) {
filterChain.doFilter(request, response);
return;
}
// 跨域后比对实际Token
String actualToken = request.getHeader(csrfToken.getHeaderName());
if (actualToken == null) {
actualToken = request.getParameter(csrfToken.getParameterName());
}
if (!csrfToken.getToken().equals(actualToken)) {
if (missingToken) {
this.accessDeniedHandler.handle(request, response,new MissingCsrfTokenException(actualToken));
} else {
this.accessDeniedHandler.handle(request, response,new InvalidCsrfTokenException(csrfToken, actualToken));
}
return;
}
filterChain.doFilter(request, response);
}
3.9 其他零散Token如果缓存的请求与当前请求匹配,则负责重新构造已保存的请求整个核心代码主要是 2句话:其中主要是封装了一个新得 wrappedSavedRequestHttpServletRequest wrappedSavedRequest = requestCache.getMatchingRequest(
(HttpServletRequest) request, (HttpServletResponse) response);
chain.doFilter(wrappedSavedRequest == null ? request : wrappedSavedRequest,response);
SecurityContextHolderAwareRequestFilter 一个过滤器,它使用实现servlet API安全方法的请求包装器填充ServletRequest 简单点说 , 就是一个封装 Request 的 Filter , 封装的 HttpServletRequest 提供了很多额外的功能HttpServletRequest.authenticate() - 允许用户确定他们是否被验证,如果没有,则将用户发送到登录页面HttpServletRequest.login() - 允许用户使用AuthenticationManager进行身份验证HttpServletRequest.logout() - 允许用户使用Spring Security中配置的LogoutHandlers注销SessionManagementFilter SessionManagementFilter 中提供了多个对象用于在用户已经认证后进行 Session 会话活动 ,** 激活会话固定保护机制或检查多个并发登录**SecurityContextRepository securityContextRepository;SessionAuthenticationStrategy sessionAuthenticationStrategy;3.8 ExceptionTranslationFilter作用 : 处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException。如果检测到AuthenticationException,过滤器将启动authenticationEntryPoint。这允许通用地处理来自AbstractSecurityInterceptor的任何子类的身份验证失败。sendStartAuthentication(request, response, chain,(AuthenticationException) exception);如果检测到AccessDeniedException,筛选器将确定该用户是否是匿名用户。如果它们是匿名用户,则将启动authenticationEntryPoint。如果它们不是匿名用户,则筛选器将委托给AccessDeniedHandler。默认情况下,过滤器将使用AccessDeniedHandlerImpl。sendStartAuthentication(request,response,chain,new InsufficientAuthenticationException(
messages.getMessage("ExceptionTranslationFilter.insufficientAuthentication","Full authentication is required to access this resource")));
(PS : 因为是链式结构 , 所以他作为最后一个 , 也是处在最外层的)核心是通过一个 catch 来处理try {
chain.doFilter(request, response);
}catch (Exception ex) {
Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
RuntimeException ase = (AuthenticationException) throwableAnalyzer.getFirstThrowableOfType(AuthenticationException.class, causeChain);
// 获取依次类型
if (ase == null) {
ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType(
AccessDeniedException.class, causeChain);
}
if (ase != null) {
if (response.isCommitted()) {
throw new ServletException("Unable to handle the Spring Security Exception because the response is already committed.", ex);
}
// 专属处理 SpringSecurityException
handleSpringSecurityException(request, response, chain, ase);
}else {
if (ex instanceof ServletException) {
throw (ServletException) ex;
}else if (ex instanceof RuntimeException) {
throw (RuntimeException) ex;
}
throw new RuntimeException(ex);
}
}
首先,ExceptionTranslationFilter 调用 FilterChain.doFilter (请求、响应)来调用应用程序的其余部分。如果用户没有经过身份验证,或者它是 AuthenticationException,那么启动身份验证。否则,如果它是一个 AccessDeniedException,那么 Access Denied如果应用程序没有抛出 AccessDeniedException 或 AuthenticationException,那么 ExceptionTranslationFilter 不会做任何事情。四 . 业务流谈到了 Filter , 肯定就要细聊 Filter 对应的业务 , 上面说了一些简单的 Filter 业务 , 这一段我们来说一说比较大的业务流程 :4.1 HttpSecurity 的业务匹配我们在配置 Security 的时候 , 一般都会配置 Request Match 等参数 , 例如 : http.authorizeRequests()
.antMatchers("/test/**").permitAll()
.antMatchers("/before/**").permitAll()
.antMatchers("/index").permitAll()
.antMatchers("/").permitAll()
.anyRequest().authenticated() //其它请求都需要校验才能访问
.and()
.formLogin()
.loginPage("/login") //定义登录的页面"/login",允许访问
.defaultSuccessUrl("/home") //登录成功后默认跳转到"list"
.successHandler(myAuthenticationSuccessHandler).failureHandler(myAuthenctiationFailureHandler).permitAll().and()
.logout() //默认的"/logout", 允许访问
.logoutSuccessUrl("/index")
.permitAll();
那么这些参数是怎么生效的呢 ?Step End : 最终匹配对象我们来根据整个业务流程逆推 , 其最终对象是一个 RequestMatcher 实现类注意 , 我们其上的 antMatchers 类型会生成多种不同的实现类 :AndRequestMatcher : 请求路径IpAddressMatcher : IP地址MediaTypeRequestMatcher : 媒体类型.... 等等其他的就不详细说了拿到实现类后 ,调用 实现类的matches 方法返回最终结果public boolean matches(HttpServletRequest request) {
return requestMatcher.matches(request);
}
Step Start : 看看请求的起点找到了最终的匹配点 , 后面就好说了 , 打个断点 , 整个调用链就清清楚楚了
// Step1 : FilterChainProxy
private void doFilterInternal(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
FirewalledRequest fwRequest = firewall.getFirewalledRequest((HttpServletRequest) request);
HttpServletResponse fwResponse = firewall.getFirewalledResponse((HttpServletResponse) response);
List<Filter> filters = getFilters(fwRequest);
//...... 省略
// 执行 Filter 链 , 如果没有相当于直接进去
if (filters == null || filters.size() == 0) {
chain.doFilter(fwRequest, fwResponse);
}
// Step2 : getFilters 过滤 Filter Chain
for (SecurityFilterChain chain : filterChains) {
// 如果地址匹配 , 则执行对象 Filter 链
if (chain.matches(request)) {
return chain.getFilters();
}
}
这里可以看到 , 如果没有被拦截成功的 ,最终应该就直接运行了 , 所以 Security 一切的起点都是 Filter五.补充5.1 DelegatingFilterProxy 补充Security 通过 DelegatingFilterProxy 将 Security 融入到 WebFilter 的体系中 ,其主要流程为 :C- DelegatingFilterProxy # doFilterC- DelegatingFilterProxy # invokeDelegateC- FilterChainProxy # doFilterpublic void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// -> PIC51001 : delegate 对象结构
Filter delegateToUse = this.delegate;
if (delegateToUse == null) {
synchronized (this.delegateMonitor) {
delegateToUse = this.delegate;
if (delegateToUse == null) {
WebApplicationContext wac = findWebApplicationContext();
if (wac == null) {
throw new IllegalStateException("...");
}
delegateToUse = initDelegate(wac);
}
this.delegate = delegateToUse;
}
}
// Let the delegate perform the actual doFilter operation.
invokeDelegate(delegateToUse, request, response, filterChain);
}
protected void invokeDelegate(
Filter delegate, ServletRequest request, ServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// 会调用到 FilterChainProxy , 正式进入 Security 体系
delegate.doFilter(request, response, filterChain);
}
PIC51001 : delegate 对象结构继续补充 : delegate 的初始化 , 获取 FilterChainprotected Filter initDelegate(WebApplicationContext wac) throws ServletException {
String targetBeanName = getTargetBeanName();
// 这里的 TargetBeanName 为 springSecurityFilterChain
Filter delegate = wac.getBean(targetBeanName, Filter.class);
if (isTargetFilterLifecycle()) {
delegate.init(getFilterConfig());
}
return delegate;
}
总结Spring Security 的起点就是 Filter ,常用的功能都能通过 Filter 找到相关的痕迹 , 后续我们会继续分析更底层的东西, 来开枝散叶的看看底下经历了什么Security 的 Filter 和 WebFilter 的本质一样 , Security 通过一个 DelegatingFilterProxy 将 SecurityFilterChain 集中到 Filter 体系中FilterChain 的核心是一个 VirtualFilterChain , 每个请求过来都会有一个VirtualFilterChain 生成 ,其中会添加所有的 Filter 类的 Filter 包括 :SecurityContextPersistenceFilter : 对 SecurityContext 进行持久化操作HeaderWriterFilter : 对 Header 进行二次处理 , 因为很多认证信息会放在 Header 中 , 这也是一个极其重要的类LogoutFilter : 拦截 logout 请求 , 并且退出ExceptionTranslationFilter : 对流程中的异常进行处理 (AccessDeniedException和AuthenticationException)Filter 会通过 matches 进行拦截 , 判断是否要执行 Filters 逻辑
带鱼
盘点认证框架 : SpringSecurity 认证流程篇
一 . 前言上一篇讲了 Security 的 Filter 是怎么运行的 , 这一篇我们来看看 Security 的认证流程 .二 . 认证信息的流转2.1 SecurityContext 基本对象信息Security 核心信息就是 SecurityContext , 我们来看看认证信息是怎么确定和流转的SecurityContextHolder 是 Spring Security 存储被验证者的详细信息的地方。Spring Security 不关心 SecurityContextHolder 是如何填充的。如果它包含一个值,则将其用作当前经过身份验证的用户。生成一个 SecurityContext// 表明用户已通过身份验证的最简单方法是直接设置 SecurityContextHolder
SecurityContext context = SecurityContextHolder.createEmptyContext();
// 生成 Authentication 认证对象
Authentication authentication =new TestingAuthenticationToken("username", "password", "ROLE_USER");
context.setAuthentication(authentication);
// SecurityContextHolder 中设置 context
SecurityContextHolder.setContext(context);
获得已经认证的用户// Step 1 : 获取 SecurityContext
SecurityContext context = SecurityContextHolder.getContext();
// Step 2 : 获取 Authentication 及其相关信息
Authentication authentication = context.getAuthentication();
String username = authentication.getName();
Object principal = authentication.getPrincipal();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
SecurityContextHolder 的相关逻辑默认情况下 SecurityContextHolder 使用 ThreadLocal 来存储这些细节 (PS : 也可以通过 SecurityContextHolder.MODE global 配置)Spring Security 的 FilterChainProxy 确保 SecurityContext 总是被清除// SecurityContextHolder 提供了以下的参数
// 提供了三种不同的Mode
public static final String MODE_THREADLOCAL = "MODE_THREADLOCAL";
public static final String MODE_INHERITABLETHREADLOCAL = "MODE_INHERITABLETHREADLOCAL";
public static final String MODE_GLOBAL = "MODE_GLOBAL";
public static final String SYSTEM_PROPERTY = "spring.security.strategy";
// 策略类型名
private static String strategyName = System.getProperty(SYSTEM_PROPERTY);
private static SecurityContextHolderStrategy strategy;
private static int initializeCount = 0;
// Node 1 : 类初始化 , 这里因为有静态初始化块 , 所以上面从才可以总结通过静态类来设置
static {
initialize();
}
// 进行了初始化操作
private static void initialize() {
if (!StringUtils.hasText(strategyName)) {
strategyName = MODE_THREADLOCAL;
}
// 这里可以看到 , 有三种不同的Mode , 分别对应三种不同的策略
if (strategyName.equals(MODE_THREADLOCAL)) {
strategy = new ThreadLocalSecurityContextHolderStrategy();
}else if (strategyName.equals(MODE_INHERITABLETHREADLOCAL)) {
strategy = new InheritableThreadLocalSecurityContextHolderStrategy();
}else if (strategyName.equals(MODE_GLOBAL)) {
strategy = new GlobalSecurityContextHolderStrategy();
}else {
try {
// 反色获取
Class<?> clazz = Class.forName(strategyName);
Constructor<?> customStrategy = clazz.getConstructor();
strategy = (SecurityContextHolderStrategy) customStrategy.newInstance();
} catch (Exception ex) {
ReflectionUtils.handleReflectionException(ex);
}
}
initializeCount++;
}
// Node 2 : setContext 逻辑 , 通过策略调用
public static void setContext(SecurityContext context) {
strategy.setContext(context);
}
GlobalSecurityContextHolderStrategy : 其中 SecurityContext 就是个静态变量InheritableThreadLocalSecurityContextHolderStrategy : 其中包含一个 ThreadLocal<SecurityContext>ThreadLocalSecurityContextHolderStrategy : 和上一个没什么区别2.2 SecurityContext 流程Step 1 : 调用 Provider 处理情况 , 这里认证完成后返回了一个 Authentication// 回忆一下 , 之前 Filter 中 , 调用 AuthenticationManager 开始了 Provider 的流程
DatabaseUserToken authRequest = new DatabaseUserToken(username, password);
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
// 往外层追溯一下 , 可以看到 , 其核心被调用的是抽象类 AbstractAuthenticationProcessingFilter
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
Step 2 : Provider 处理这里的 AuthenticationManager 主要是 ProviderManager 主要是这些 ,我们仅保留其中比较重要的逻辑 :public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
Authentication result = null;
Authentication parentResult = null;
boolean debug = logger.isDebugEnabled();
for (AuthenticationProvider provider : getProviders()) {
// 每个 Provider 都会重写 supports , 此处判断是否支持该 Provider
if (!provider.supports(toTest)) {
continue;
}
try {
// 此处调用具体的 Provider 执行
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}catch (AccountStatusException|InternalAuthenticationServiceException e)
prepareException(e, authentication);
throw e;
}catch (AuthenticationException e) {
lastException = e;
}
}
// 这里还有个补偿策略 ,如果当前 AuthenticationManager 处理不了 , 会由 父类处理
// 暂时没想清楚具体的使用场景 , 可能适用于细粒度权限这种
if (result == null && parent != null) {
try {
result = parentResult = parent.authenticate(authentication);
}catch (AuthenticationException e) {
lastException = e;
}
}
if (result != null) {
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {
((CredentialsContainer) result).eraseCredentials();
}
// 发布认证成功的时间
if (parentResult == null) {
eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}
if (lastException == null) {
lastException = new ProviderNotFoundException(messages.getMessage(
"ProviderManager.providerNotFound",
new Object[] { toTest.getName() },
"No AuthenticationProvider found for {0}"));
}
prepareException(lastException, authentication);
throw lastException;
}
可以看到 ,到这一步 Provider 返回了一个 Authentication 回去Step 3 : AbstractAuthenticationProcessingFilter 处理从第二步 Prodiver 返回了 Authentication , 他最终被传递到 AbstractAuthenticationProcessingFilter 中// AbstractAuthenticationProcessingFilter 伪代码 :
// Step 1 : 进行预认证
authResult = attemptAuthentication(request, response);
// Step 2 : Session 策略处理 , 这里的 sessionStrategy 是 NullAuthenticatedSessionStrategy , 其里面是空实现
sessionStrategy.onAuthentication(authResult, request, response)
// Step 3 : 成功后执行容器处理
successfulAuthentication(request, response, chain, authResult)
|- SecurityContextHolder.getContext().setAuthentication(authResult) // 果然来了 , 把 authResult 放入 SecurityContext
|- rememberMeServices.loginSuccess(request, response, authResult) // 记住我功能的处理 , RememberFilter 会对这个进行处理
|- successHandler.onAuthenticationSuccess(request, response, authResult) // SavedRequestAwareAuthenticationSuccessHandler
至此 , Provider 产生的 Authentication 成功放入 容器中扩展 SavedRequestAwareAuthenticationSuccessHandler 处理 Success 结果总结一下就是定制缓存和跳转关系 >>>C- SavedRequestAwareAuthenticationSuccessHandler
P- RequestCache requestCache : Request 缓存工具 , 用户获取缓存的 Request 对象
M- onAuthenticationSuccess
- SavedRequest savedRequest = requestCache.getRequest(request, response) : 先获取缓存的对象
- String targetUrlParameter = getTargetUrlParameter() : 这里是看看有没有成功的跳转地址
?- 如果想实现不同用户不同跳转 ,定制这里
- clearAuthenticationAttributes(request) : 删除与身份验证相关的临时数据,这些数据可能在身份验证过程中存储在会话中 , 避免敏感信息泄露
- String targetUrl = savedRequest.getRedirectUrl();
- getRedirectStrategy().sendRedirect(request, response, targetUrl);
?- 重定向出去
以上是认证从和认证失败的流程图 , 可以看到具体的处理类 :总结一下认证成功和认证失败分别干了什么 :如果认证失败:Security contextholder 被清空了。调用 RememberMeServices.loginFail。如果没有配置 rememberme,这是一个 no-op调用 AuthenticationFailureHandler。同时对比一下认证成功:会在新登录时通知 SessionAuthenticationStrategy在 SecurityContextHolder 上设置身份验证,然后 SecurityContextPersistenceFilter 将 SecurityContext 保存到 HttpSession 中调用 RememberMeServices.loginSuccess。如果没有配置 remember me,这是一个 no-opApplicationEventPublisher 发布交互式身份验证连接调用 AuthenticationSuccessHandler三 . 再次访问和退出上面说了一个认证过程中发生了什么 , 这里我们看下认证完成后再次访问>>>3.1 认证后访问// 前面说了 , 认证完成后会写入 SecurityContextHolder , Security 通过判断 SecurityContext 来校验用户
// 同理 , 下次访问的时候同样通过该方式 :
// Step 1 : SecurityContextPersistenceFilter 拦截到请求
// Step 2 : 从请求中获取 SecurityContext
SecurityContext contextBeforeChainExecution = repo.loadContext(holder);
// 从 HttpSessionSecurityContextRepository 中获取
C- HttpSessionSecurityContextRepository
SecurityContext context = readSecurityContextFromSession(httpSession);
// 此处断点可以看到认证信息 :
org.springframework.security.core.context.SecurityContextImpl@45eed422:
Authentication: com.security.demo.token.DatabaseUserToken@45eed422:
Principal: gang;
Credentials: [PROTECTED];
Authenticated: true;
Details: org.springframework.security.web.authentication.WebAuthenticationDetails@fffdaa08: RemoteIpAddress: 127.0.0.1;
SessionId: A58D946FBFCB17743E2E0A44DBAB7A76;
Granted Authorities: ROLE_USER
// Step 3 : finally 此处将 SecurityContext 进行了设置
SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
SecurityContextHolder.clearContext();
repo.saveContext(contextAfterChainExecution, holder.getRequest(),holder.getResponse());
PS : 因为是基于 Session 管理 , 所以过一会就过期了当然这是基于 Session 的模式 ,生命周期和 Session 等同 ,但是通常会常用更长的生命周期方案 ,比如 AccessToken , Cookie 等等 ,而 Session 只是为了维持一个认证的临时状态3.2 Logout 退出Logout 相关类 :PersistentTokenBasedRememberMeServicesTokenBasedRememberMeServicesCookieClearingLogoutHandlerCsrfLogoutHandlerSecurityContextLogoutHandlerHeaderWriterLogoutHandler同样的 , Logout 也有 Filter 和 HandlerLogoutFilterSimpleUrlLogoutSuccessHandlerHttpStatusReturningLogoutSuccessHandler和前面分析 Filter 一样 , 其核心还是通过 LogoutFilter 来进行 :this.handler.logout(request, response, auth);
logoutSuccessHandler.onLogoutSuccess(request, response, auth);
C- SecurityContextLogoutHandler : 核心类 , 处理 Context
public void logout(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) {
Assert.notNull(request, "HttpServletRequest required");
if (invalidateHttpSession) {
HttpSession session = request.getSession(false);
if (session != null) {
logger.debug("Invalidating session: " + session.getId());
session.invalidate();
}
}
if (clearAuthentication) {
// 此处将 SecurityContext 设置为了 null
SecurityContext context = SecurityContextHolder.getContext();
context.setAuthentication(null);
}
SecurityContextHolder.clearContext();
}
总结 :至此 , 一个完整的 Security 生命周期就看完了, 其实很简单 , 总结起来就是 :Filter 做业务决定AuthenticationManager 决定校验方式Provider 进行认证校验Handler做结果处理已经外部跳转
带鱼
盘点认证框架
对常用的认证框架进行使用说明及源码分析 <br> 文档特点 : 持续迭代 , 用于快速开发及底层分析
带鱼
聊聊 Java 21 中的结构化并发(预览版)
结构化编程在开始聊结构化并发之前,我们先简单聊聊一下结构化编程:Goto Statement Considered Harmful在计算机发展的早期,程序员使用汇编语言进行编程,在之后的一段时期,诞生了比汇编略微高级的编程语言,如 FORTRAN、FLOW-MATIC 等。这些语言虽然在一定程度上提高了可读性,但是仍然存在很大的局限性。如下所示就是一段 FLOW-MATIC 代码:由于当时块语法还没有发明,因此 FLOW-MATIC 不支持 if 块、循环块、函数调用、块修饰符等现代语言必备的基础特性。整段代码就是一系列按顺序排列并打平的命令。关于控制流,程序支持两种方式,分别是:顺序执行、跳转执行,即 GOTO 语句。顺序执行的逻辑非常简单,它总是能够找到执行入口与出口。与之相反,跳转执行则充满了不确定性。如果程序中存在 GOTO 语句,那么它可以在 任何时候跳转至任何指令位置。一旦程序大量使用了 GOTO 语句,那么最终将变成 面条式代码(Spaghetti code)。如下图所示:结构化编程在发表 《Goto Statement Considered Harmful》 之后,Dijkstra 又发表了 《Notes on Structured Programming》 表达了其理想的编程范式,提出了 结构化编程 的概念。结构化编程在现在看来是理所当然的,但是在当时并不是。结构化编程的核心是 基于块语句,实现代码逻辑的抽象与封装,从而保证控制流具有单一入口和单一出口。现代编程语言中的条件语句、循环语句、函数定义与调用都是结构化编程的体现。相比 GOTO 语句,基于块的控制流有一个显著的特征:控制流从程序入口进入,中途可能会经历条件、循环、函数调用等控制流转换,但是最终控制流都会从程序出口退出。这种编程范式使得代码结构变得更加结构化,思维模型变得更加简单,也为编译器在低层级提供了优化的可能。因此,完全禁用 GOTO 语句已经成为了大部分现代编程语言的选择。虽然,少部分编程语言仍然支持 GOTO,但是它们大都支持高德纳(Donald Ervin Knuth)所提出的前进分支和后退分支不得交叉的理论。类似 break、continue 等控制流命令,依然遵循结构化的基本原则:控制流拥有单一的入口与出口。非结构化并发在开始了解结构化并发前,我们先回顾一下 Java 中非结构化并发的写法。 ExecutorService executorService= Executors.newFixedThreadPool(3);
Future<String> user = executorService.submit(() -> getUser());
Future<Integer> order = executorService.submit(() -> getOrder());
String theUser = user.get(); // 加入 getUser
int theOrder = order.get(); // 加入 getOrder
非结构化并发存在的一些问题线程泄漏当 getUser 或者 getOrder 抛出异常时,另外一个任务并不会停止执行,一方面会导致线程资源的浪费,另一方面可能干扰其它任务。又或者其中一个线程已经执行失败,继续执行的线程执行时间很长,这时候需要阻塞等待线程的完成,同样造成资源的浪费。代码本身不会体现任务间的关系上面的各种情况其实都是在开发人员的脑海中,程序逻辑本身并不会体现出来,这样不仅会产生更多的错误空间,而且会使错误排查更加困难。排查错误困难多线程编程中一个比较大的难点就是对错误的追踪,任务运行在不同的线程上,当然我们现在有跨线程追踪的方案,但是远远没有我们使用非并发编程时的简单和方便。结构化并发在单线程编程模型中,编程语言 通过代码块避免控制流随意跳转,从而实现程序的结构化。但在多线程编程(并发编程)模型中,线程之间控制和归属关系仍然存在很多问题,其面临的问题与 GOTO 的问题非常相似,这也是结构化并发所要解决的问题。什么是结构化并发呢?结构化并发的核心是 在并发模型下,也要保证控制流的单一入口和单一出口。程序可以产生多个控制流来实现并发,但是所有并发控制流在出口时都应该处于完成或取消状态,控制流最终在出口处完成合并。下面是非结构化并发(图一)和结构化并发(图二)的运行示例图:Java 结构化并发示例public class Test {
public static void main(String[] args) {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Future<String> userFuture = scope.fork(() -> getUser());
Future<Integer> orderFuture = scope.fork(() -> getOrder());
scope.join() // Join both subtasks
.throwIfFailed(); // ... and propagate errors
System.out.println("User: " + userFuture.get());
System.out.println("Order: " + orderFuture.get());
} catch (ExecutionException | InterruptedException e) {
throw new RuntimeException(e);
}
}
private static int getOrder() throws Exception {
// throw new Exception("test");
return 1;
}
private static String getUser() {
return "user";
}
}
结构化并发带来的好处下面我们看看结构化并发如何解决非结构化并发中可能存在的一些问题:短路处理如果一个getOrder()或getUser()一个子任务失败,则另一个尚未完成的任务将被取消。(这是由实施的关闭策略管理的ShutdownOnFailure;其他策略也是可能的,同时支持自定义策略)。避免了线程资源浪费以及可能的无意义阻塞。取消传播如果线程在调用期间被中断join(),则当线程退出作用域时,两个子任务都会自动取消。避免了线程资源浪费。清晰性上面的代码有一个清晰的结构:设置子任务,等待它们完成或被取消,然后决定是成功(并处理已经完成的子任务的结果)还是失败(没有什么需要清理的)。可观察性线程转储 - 线程堆栈信息可以清楚的显示任务层次结构:Exception in thread "main" java.lang.RuntimeException: java.util.concurrent.ExecutionException: java.lang.Exception: test
at Test.main(Test.java:21)
Caused by: java.util.concurrent.ExecutionException: java.lang.Exception: test
at jdk.incubator.concurrent/jdk.incubator.concurrent.StructuredTaskScope$ShutdownOnFailure.throwIfFailed(StructuredTaskScope.java:1188)
at Test.main(Test.java:17)
Caused by: java.lang.Exception: test
at Test.getOrder(Test.java:26)
at Test.lambda$main$1(Test.java:15)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317)
at java.base/java.lang.VirtualThread.run(VirtualThread.java:305)
at java.base/java.lang.VirtualThread$VThreadContinuation.lambda$new$0(VirtualThread.java:177)
at java.base/jdk.internal.vm.Continuation.enter0(Continuation.java:327)
at java.base/jdk.internal.vm.Continuation.enter(Continuation.java:320)
总结目前结构化并发的目标推广一种并发编程风格,可以消除因取消和关闭而产生的常见风险,例如线程泄漏和取消延迟。提高并发代码的可观察性。以下不是目前非结构化并发的目标不会替换现有的任务并发结构。(java.util.concurrent package, such as ExecutorService and Future)为Java平台定义明确的结构化并发API并不是我们的目标。其他结构化并发结构可以由第三方库或在未来的JDK版本中定义。
带鱼
Java多线程(一) : 快查手册
快查手册// 乐观锁/悲观锁
java悲观锁:synchronized、lock的实现类
java乐观锁:乐观锁在Java中是通过使用无锁编程来实现,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。
// 独享锁/共享锁
synchronized、ReentrantLock是独享锁。
ReadWriteLock其读锁是共享锁,其写锁是独享锁。
// 可重入锁
synchronized、ReentrantLock
// 公平锁/非公平锁
synchronized是非公平锁
ReetrantLock(通过构造函数指定该锁是否是公平锁,默认是非公平锁)
JVM 参数变量> User user = new User()
- new User 会创建到 Heap 中
- User user 为对象得引用 ,放在方法栈中
JVM 多线程的变量同步
带鱼
Java 21正式发布 小小使用一下期待已久的虚拟线程
前言hello,大家好,我是Lorin,2023年9月19号,Java 21 通用版本正式发布,具体的版本内容可以参考下面这篇文章:Java 21 版本特性一览:mp.weixin.qq.com/s/46VM8uRe6…其中也有一些比较好的特性,其中最让我感兴趣的就是虚拟线程,虚拟线程这个名字大家也许有点陌生,但是协程这个名字大家可能耳熟能详,从 Java 19 开始加入预览版,今天终于成为了正式版,我们来看看它究竟能给我们带来什么?虚拟线程(协程)虚拟线程是一种轻量级的并发编程机制,它在代码中提供了一种顺序执行的感觉,同时允许在需要时挂起和恢复执行。虚拟线程可以看作是一种用户级线程,与操作系统的线程或进程不同,它是由编程语言或库提供的,而不是由操作系统管理的。看下面的图大家也许更容易理解:优点非常轻量级:可以在单个线程中创建成百上千个虚拟线程而不会导致过多的线程创建和上下文切换。简化异步编程: 虚拟线程可以简化异步编程,使代码更易于理解和维护。它可以将异步代码编写得更像同步代码,避免了回调地狱(Callback Hell)。减少资源开销: 相比于操作系统线程,虚拟线程的资源开销更小。本质上是提高了线程的执行效率,从而减少线程资源的创建和上下文切换。缺点不适用于计算密集型任务: 虚拟线程适用于I/O密集型任务,但不适用于计算密集型任务,因为它们在同一线程中运行,可能会阻塞其他虚拟线程。依赖于语言或库的支持: 协程需要编程语言或库提供支持。不是所有编程语言都原生支持协程。比如 Java 实现的虚拟线程。使用 Java 虚拟线程Java 20 已经支持虚拟线程,大家可以在官网下载使用,在使用上官方为了降低使用门槛,尽量复用原有的 Thread 类,让大家可以更加平滑的使用。如何创建虚拟线程官方提供了以下几种方式:使用Thread.startVirtualThread()创建public class VirtualThreadTest {
public static void main(String[] args) {
CustomThread customThread = new CustomThread();
Thread.startVirtualThread(customThread);
}
}
static class CustomThread implements Runnable {
@Override
public void run() {
System.out.println("CustomThread run");
}
}
使用Thread.ofVirtual()创建public class VirtualThreadTest {
public static void main(String[] args) {
CustomThread customThread = new CustomThread();
// 创建不启动
Thread unStarted = Thread.ofVirtual().unstarted(customThread);
unStarted.start();
// 创建直接启动
Thread.ofVirtual().start(customThread);
}
}
static class CustomThread implements Runnable {
@Override
public void run() {
System.out.println("CustomThread run");
}
}
使用ThreadFactory创建public class VirtualThreadTest {
public static void main(String[] args) {
CustomThread customThread = new CustomThread();
ThreadFactory factory = Thread.ofVirtual().factory();
Thread thread = factory.newThread(customThread);
thread.start();
}
}
static class CustomThread implements Runnable {
@Override
public void run() {
System.out.println("CustomThread run");
}
}
使用Executors.newVirtualThreadPerTaskExecutor()创建public class VirtualThreadTest {
public static void main(String[] args) {
CustomThread customThread = new CustomThread();
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
executor.submit(customThread);
}
}
static class CustomThread implements Runnable {
@Override
public void run() {
System.out.println("CustomThread run");
}
}
性能对比通过多线程和虚拟线程的方式处理相同的任务,对比创建的系统线程数和处理耗时:注:统计创建的系统线程中部分为后台线程(比如 GC 线程),两种场景下都一样,所以并不影响对比。public class VirtualThreadTest {
static List<Integer> list = new ArrayList<>();
public static void main(String[] args) {
// 开启线程 统计平台线程数
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
scheduledExecutorService.scheduleAtFixedRate(() -> {
ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();
ThreadInfo[] threadInfo = threadBean.dumpAllThreads(false, false);
updateMaxThreadNum(threadInfo.length);
}, 10, 10, TimeUnit.MILLISECONDS);
long start = System.currentTimeMillis();
// 虚拟线程
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
// 使用平台线程
// ExecutorService executor = Executors.newFixedThreadPool(200);
for (int i = 0; i < 10000; i++) {
executor.submit(() -> {
try {
// 线程睡眠 0.5 s,模拟业务处理
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException ignored) {
}
});
}
executor.close();
System.out.println("max:" + list.get(0) + " platform thread/os thread");
System.out.printf("totalMillis:%dms\n", System.currentTimeMillis() - start);
}
// 更新创建的平台最大线程数
private static void updateMaxThreadNum(int num) {
if (list.isEmpty()) {
list.add(num);
} else {
Integer integer = list.get(0);
if (num > integer) {
list.add(0, num);
}
}
}
}
请求数10000 单请求耗时 1s// Virtual Thread
max:22 platform thread/os thread
totalMillis:1806ms
// Platform Thread 线程数200
max:209 platform thread/os thread
totalMillis:50578ms
// Platform Thread 线程数500
max:509 platform thread/os thread
totalMillis:20254ms
// Platform Thread 线程数1000
max:1009 platform thread/os thread
totalMillis:10214ms
// Platform Thread 线程数2000
max:2009 platform thread/os thread
totalMillis:5358ms
请求数10000 单请求耗时 0.5s// Virtual Thread
max:22 platform thread/os thread
totalMillis:1316ms
// Platform Thread 线程数200
max:209 platform thread/os thread
totalMillis:25619ms
// Platform Thread 线程数500
max:509 platform thread/os thread
totalMillis:10277ms
// Platform Thread 线程数1000
max:1009 platform thread/os thread
totalMillis:5197ms
// Platform Thread 线程数2000
max:2009 platform thread/os thread
totalMillis:2865ms
可以看到在密集 IO 的场景下,需要创建大量的平台线程异步处理才能达到虚拟线程的处理速度。因此,在密集 IO 的场景,虚拟线程可以大幅提高线程的执行效率,减少线程资源的创建以及上下文切换。吐槽:虽然虚拟线程我很想用,但是我 Java8 有机会升级到 Java21 吗?呜呜注:有段时间 JDK 一直致力于 Reactor 响应式编程来提高Java性能,但响应式编程难以理解、调试、使用,
最终又回到了同步编程,最终虚拟线程诞生。
带鱼
Java 多线程 : 勉强弄懂了AQS
一 . AQS 基础一句话概括AQS :一个叫 AbstractQueuedSynchronizer 的抽象类包含2个重要概念 : 以Node为节点实现的链表的队列(CHL队列) + STATE标志支持2种锁 : 独占锁和共享锁 ,1.1 什么是 AQS ?java.util.concurrent.locks.AbstractQueuedSynchronizer 抽象类,简称 AQS , 队列同步器作用 : 用于构建锁和同步容器的同步器 原理 : 使用一个 FIFO 的队列表示排队等待锁的线程队列头节点称作“哨兵节点”或者“哑节点”,不与任何线程关联。其他的节点与等待线程关联,每个节点维护一个等待状态 waitStatus场景 : AQS解决了实现同步器时涉及当的大量细节问题,例如获取同步状态、FIFO同步队列1.2 AQS 的 status 表示了什么 ?AQS 使用一个int 的 status 来表示同步状态, 同步状态重要目的是用于跟踪线程是否应该阻塞 , 当它的前身释放时,一个节点被通知。否则,队列中的每个节点都充当一个特定通知样式的监视器,该监视器持有单个等待线程.status > 0 : 获取了锁status = 0 : 释放了锁status < 0 :1.3 常用方法:状态处理getState():返回同步状态的当前值。setState(int newState):设置当前同步状态。compareAndSetState(int expect, int update):使用 CAS 设置当前状态,该方法能够保证状态设置的原子性。独占锁相关方法【可重写】#tryAcquire(int arg):独占式获取同步状态,获取同步状态成功后,其他线程需要等待该线程释放同步状态才能获取同步状态。【可重写】#tryRelease(int arg):独占式释放同步状态。共享锁相关方法【可重写】#tryAcquireShared(int arg):共享式获取同步状态,返回值大于等于 0 ,则表示获取成功;否则,获取失败。【可重写】#tryReleaseShared(int arg):共享式释放同步状态。【可重写】#isHeldExclusively():当前同步器是否在独占式模式下被线程占用,一般该方法表示是否被当前线程所独占。独占式获取同步状态acquire(int arg):独占式获取同步状态。 如果当前线程获取同步状态成功,则由该方法返回;否则,将会进入同步队列等待。该方法将会调用可重写的 #tryAcquire(int arg) 方法;acquireInterruptibly(int arg):与 #acquire(int arg) 相同,但是该方法响应中断。 当前线程为获取到同步状态而进入到同步队列中 如果当前线程被中断,则抛出 InterruptedException() 如果未中断 ,将尝试调用 tryAcquire , 调用失败线程将进入队列,可能会反复阻塞和解除阻塞tryAcquireNanos(int arg, long nanos):超时获取同步状态。 如果当前线程被中断,则抛出 InterruptedException() 如果当前线程在 nanos 时间内没有获取到同步状态,那么将会返回 false ,已经获取则返回 true 。 未超时未获取会一只排队 ,反复阻塞共享式获取同步状态acquireShared(int arg):共享式获取同步状态 如果当前线程未获取到同步状态,将会进入同步队列等待,与独占式的主要区别是在同一时刻可以有多个线程获取到同步状态;acquireSharedInterruptibly(int arg):共享式获取同步状态,响应中断。tryAcquireSharedNanos(int arg, long nanosTimeout):共享式获取同步状态,增加超时限制。释放同步状态release(int arg):独占式释放同步状态 该方法会在释放同步状态之后,将同步队列中第一个节点包含的线程唤醒。releaseShared(int arg):共享式释放同步状态。public class SimpleLock extends AbstractQueuedSynchronizer {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Override
protected boolean tryAcquire(int unused) {
logger.info("------> try tryAcquire :{} <-------", unused);
//使用compareAndSetState控制AQS中的同步变量
if (compareAndSetState(0, 1)) {
logger.info("------> cas success ");
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
@Override
protected boolean tryRelease(int unused) {
logger.info("------> try tryRelease :{} <-------", unused);
setExclusiveOwnerThread(null);
//使用setState控制AQS中的同步变量
setState(0);
return true;
}
public void lock() {
acquire(1);
}
public boolean tryLock() {
return tryAcquire(1);
}
public void unlock() {
release(1);
}
}
其他关联知识点 : 1 AQS的所有子类中,要么使用了它的独占锁,要么使用了它的共享锁,不会同时使用它的两个锁。二 . AQS 原理2.1 基本原理AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。AQS使用一个int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。AQS使用CAS对该同步状态进行原子操作实现对其值的修改。在await之后,一个节点被插入到条件队列中(可见后面代码)。收到信号后,节点被转移到主队列CLH(Craig,Landin,and Hagersten)队列 该队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。AQS定义两种资源共享方式 Exclusive(独占):只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁:公平锁:按照线程在队列中的排队顺序,先到者先拿到锁非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的Share(共享):多个线程可同时执行,如Semaphore/CountDownLatch。Semaphore、CountDownLatCh、 CyclicBarrier、ReadWriteLock 我们都会在后面讲到。2.1 AQS底层使用了模板方法模式2.1.1 模板方法详情同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样(模板方法模式很经典的一个应用):使用者继承AbstractQueuedSynchronizer并重写指定的方法。(这些重写方法很简单,无非是对于共享资源state的获取和释放)将AQS组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。这和我们以往通过实现接口的方式有很大区别,这是模板方法模式很经典的一个运用。AQS使用了模板方法模式,自定义同步器时需要重写下面几个AQS提供的模板方法:以下方法未重写抛出 UnsupportedOperationExceptionisHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。2.1.2 常见的实现案例Semaphore(信号量)功能 : 允许多个线程同时访问synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。CountDownLatch (倒计时器) 功能 : CountDownLatch是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行。CyclicBarrier(循环栅栏)功能 : CyclicBarrier 和 CountDownLatch 非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch 更加复杂和强大。应用场景 : 和 CountDownLatch 类似。CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。作用 : 让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。三 . AQS 同步状态的获取和释放独占式获取和释放同步状态同一时刻,仅有一个线程持有同步状态acquire(int arg) : 该方法对中断不敏感 , 即后续对该线程进行中断操作时,线程不会从 CLH 同步队列中移除tryAcquire(int arg) : 去尝试获取同步状态 true : 获取成功 , 设置锁状态 , 直接返回不用线程阻塞 , 自旋直到获得同步状态成功 false : 获取失败 , 用#addWaiter(Node mode) 方法,将当前线程加入到 CLH 同步队列尾部,mode 方法参数为Node.EXCLUSIVEacquireQueued : 自旋直到获得同步状态成功
public final void acquire(int arg) {
if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
共享式获取和释放同步状态
// 首先调用至少一次tryacquisharered
//
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
// 上述调用失败 , 线程可能会进入队列反复阻塞和解除阻塞
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
// shouldParkAfterFailedAcquire 检查并更新未能获取的节点的状态。如果线程阻塞,则返回true
// parkAndCheckInterrupt : 中断线程
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
shouldParkAfterFailedAcquire 的作用等待状态为 Node.SIGNAL 时,表示 pred 的下一个节点 node 的线程需要阻塞等待。在pred 的线程释放同步状态时,会对 node 的线程进行唤醒通知等待状态为 0 或者 Node.PROPAGATE 时,通过 CAS 设置,将状态修改为 Node.SIGNAL等待状态为 NODE.CANCELLED 时,则表明该线程的前一个节点已经等待超时或者被中断了,则需要从 CLH 队列中将该前一个节点删除掉,循环回溯,直到前一个节点状态 <= 0 查询同步队列中的等待线程情况 // 自旋处理流程:
for (;;) {
// 获取当前线程的前驱节点
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
// 当前线程的前驱节点是头结点,且同步状态成功
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 获取失败,线程等待
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
四. 阻塞和唤醒线程AQS 在处理中有2处值得深入的过程 : 阻塞和唤醒
// 阻塞发生在获取对应同步方法同步失败后 ,流程为 :
Step Start : 获取同步状态 -> 获取失败 -> 检查该状态 shouldParkAfterFailedAcquire(Node pred, Node node)
Step 2 : 返回true -> 当前线程应该被柱塞
Step 3 : parkAndCheckInterrupt() 阻塞线程
- 调用 LockSupport#park(Object blocker) 方法,将当前线程挂起,此时就进入阻塞等待唤醒的状态
// 后续将进行线程的唤醒操作 , 唤醒分为2种
第一种,当前节点(线程)的前序节点释放同步状态时,唤醒了该线程
第二种,当前线程被打断导致唤醒。
Step Start : 当线程释放同步状态后 , 唤醒该线程的后继节点 (unparkSuccessor)
Step 2 : 后继节点存在 , 唤醒后继节点 LockSupport.unpark(s.thread)
Step 3 : 如果后继节点为null (超时 , 中断) , 通过 tail 回溯的方式找到最前序的可用的线程
// 补充 :
> park() : 阻塞当前线程
> park(Object blocker) : 为了线程调度 , 在许可可用之前兼用当前线程
> unpark : 如果给定线程的许可尚不可用 , 则使其可用
> parkNanos(long nanos) :为了线程调度禁用当前线程,最多等待指定的等待时间,除非许可可用
- park 方法和 unpark(Thread thread) 方法,都是成对出现的
- unpark(Thread thread) 方法,必须要在 park 方法执行之后执行
五 . CLH 同步队列> 简介 : CLH 同步队列是一个 FIFO 双向队列,AQS 依赖它来完成同步状态的管理
> 2种状态 :
• 当线程如果获取同步状态失败时,AQS则会将当前线程已经等待状态等信息构造成一个节点(Node)并将其加入到CLH同步队列,同时会阻塞当前线程
• 当同步状态释放时,会把首节点唤醒(公平锁),使其再次尝试获取同步状态。
C- Node : AbstractQueuedSynchronizer 的内部静态类
SF- Node SHARED = new Node();
SF- Node EXCLUSIVE = null;
SF- int CANCELLED = 1;
SF- int SIGNAL = -1;
SF- int CONDITION = -2;
SF- int PROPAGATE = -3;
F- volatile int waitStatus -- CANCELLED SIGNAL CONDITION PROPAGATE INITAL 总共 5 种状态 , 其中 INITAL 是初始状态
F- volatile Node prev; -- 指向前一个节点
F- volatile Node next; -- 指向后一个节点
F- volatile Thread thread; -- Node 节点对应的线程 Thread
F- Node nextWaiter; -- Node 节点获取同步状态的模型( Mode )
M- tryAcquire : 独占式获取同步状态
M- tryAcquireShared : 共享式获取同步状态
M- addWaiter : 入队
M- isShared : 判断是否为共享式获取同步状态
M- predecessor : 获得 Node 节点的前一个 Node 节点
// 属性详情 :
Node 中包含链表常用的2个概念 : prev , next , 同步器中包含2个属性 head , tail 分别指向队列的头和尾 ,
> 入列 :
M- addWaiter
- 准备新节点 -> 记录尾节点 -> 将新节点放入尾节点 -> CAS 设置新的尾节点
- 失败反复尝试 , 直到成功
> 出列 :
- 首节点的线程释放同步状态后,将会唤醒它的下一个节点(Node.next)。后继节点将会在获取同步状态成功时,将自己设置为首节点( head )
- setHead
- 该操作为单线程操作
// 原理简述 :
六. AQS 源码9.6.1 问题一 : CLH 的形式AQS 有2个属性private transient volatile Node head; 等待队列的头,懒加载。初始化后只能通过sehead方法进行修改。 注意:如果head存在,它的waitStatus保证不存在private transient volatile Node tail; 尾部的等待队列,懒加载AQS 有几个重要的方法private Node addWaiter(Node mode) 为当前线程和给定模式创建并进入节点队列private void setHead(Node node)设置队列头为节点,退出队列。 仅通过acquire方法调用。 将未使用的字段置空 (GC 及效率)private Node enq(final Node node) 将节点插入队列,必要时进行初始化9.6.2 问题二 : Node 节点AQS 中有个内部类 Node , 他是节点对象 , 它其中有四个属性表示状态SIGNAL :该节点的后继节点被阻塞(或即将被阻塞)(通过park),因此当前节点在释放或取消后继节点时必须解除它的后继节点的阻塞CANCELLED : 该节点因超时或中断而被取消CONDITION : 该节点当前在条件队列中。它将不会被用作同步队列节点,直到传输时,状态将被设置为0PROPAGATE : 这个 releaseShared 应该传播到其他节点它还有如下几个重要的属性volatile Node prev : 前一个节点volatile Node next : 下一个节点volatile Thread thread : 当前线程9.6.3 AQS 状态private volatile int state;9.6.5 AQS 流程图七 . AQS 使用@ https://github.com/black-ant/case/tree/master/case%20Module%20Thread/case%20AQS
public class SimpleLock extends AbstractQueuedSynchronizer {
@Override
protected boolean tryAcquire(int unused) {
//使用compareAndSetState控制AQS中的同步变量
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
@Override
protected boolean tryRelease(int unused) {
setExclusiveOwnerThread(null);
//使用setState控制AQS中的同步变量
setState(0);
return true;
}
public void lock() {
acquire(1);
}
public boolean tryLock() {
return tryAcquire(1);
}
public void unlock() {
release(1);
}
}
// 分析
1 [ main] try tryAcquire :1 <-------
2 [ main] cas success
3 [ Thread-52] try tryAcquire :1 <-------
4 [ Thread-52] try tryAcquire :1 <-------
5 [ Thread-52] try tryAcquire :1 <-------
6 [ Thread-53] try tryAcquire :1 <-------
16 [ main] try tryRelease :1 <-------
17 [ Thread-52] try tryAcquire :1 <-------
18 [ Thread-52] cas success
19 [ Thread-52] c.g.s.thread.aqs.demo.logic.StartLogic : ------> acquired the lock! <-------
20 [ Thread-52] try tryRelease :1 <-------
21 [ Thread-53] try tryAcquire :1 <-------
22 [ Thread-53] cas success
23 [ Thread-53] c.g.s.thread.aqs.demo.logic.StartLogic : ------> acquired the lock! <-------
24 [ Thread-53] try tryRelease :1 <-------
// 第2行 : main 线程获取了独占锁 , 导致后续3-6行的线程全部无法获取锁 , 排在队列中
// 第16行 : main 释放了锁 , 所以从 17-20 行 ,是 Thread-52 的操作流程 (后面可以看到 53 的队列流)
// AQS 使用如上文所示 , 通常要实现 tryAcquire 和 tryRelease
TODO
带鱼
【多线程系列】终于理解了多线程中不得不谈的并发三大性质
环境及版本运行版本:JDK 1.8并发三大性质并发是计算机科学领域的重要概念,它涉及到多个任务或操作在同一时间段内执行的能力。并发有三大性质,分别是:原子性、有序性、可见性。下面我们谈一起来看看它们到底是什么:原子性原子性(Atomicity):原子性是指一个操作是不可分割的整体,要么完全执行,要么完全不执行,不存在中间状态。不同于数据库事务原子性,在并发编程中,我们把一个或者多个操作在 CPU 执行的过程中不被中断的特性称为原子性。即保证一系列操作,不可以被拆分执行,执行过程中,需要互斥排它,不能有其他线程执行这块临界区。常见的原子操作包括加锁、解锁、读取、写入等。Java内存模型(Java Memory Model,JMM)定义了8种原子操作,它们用于定义多线程环境下的内存访问行为。这些操作包括以下8种:lock(锁定):作用于主内存,它把一个变量标记为一条线程独占状态;
read(读取):作用于主内存,它把变量值从主内存传送到线程的工作内存中,以便随后的load动作使用;
load(载入):作用于工作内存,它把read操作的值放入工作内存中的变量副本中;
use(使用):作用于工作内存,它把工作内存中的值传递给执行引擎,每当虚拟机遇到一个需要使用这个变量的指令时候,将会执行这个动作;
assign(赋值):作用于工作内存,它把从执行引擎获取的值赋值给工作内存中的变量,每当虚拟机遇到一个给变量赋值的指令时候,执行该操作;
store(存储):作用于工作内存,它把工作内存中的一个变量传送给主内存中,以备随后的write操作使用;
write(写入):作用于主内存,它把store传送值放到主内存中的变量中。
unlock(解锁):作用于主内存,它将一个处于锁定状态的变量释放出来,释放后的变量才能够被其他线程锁定;
除了上述的8种原子操作,还可以使用Atomic 类、使用锁(例如 Synchronized 关键字或 Lock)实现原子操作。如何理解 Synchronized 原子性:
由于只有一个线程能够执行临界区中的代码,synchronized 关键字确保了这段代码的原子性、有序性,临界区内的操作可以被视为一个整体,要么完全执行,要么不执行,不会出现中间的不一致状态。
有序性有序性(Ordering):有序性是指程序执行的结果按照一定的规则,符合预期的顺序。指令重排序可以保证单线程串行语义一致(实际执行顺序不一定和代码顺序相同),但是没有义务保证多线程间的语义也一致,因此多线程环境中,由于指令重排序和线程的交替执行,程序的执行顺序可能与代码的编写顺序不完全一致。为了保证有序性,需要使用同步机制(如锁、volatile关键字)或者使用 happens-before 原则来建立线程操作之间的先后关系。volatile和synchronized具有不同的含义:
volatile 禁止了指令重排序。
synchronized 提供了互斥的含义,保证了多线程下临界区的有序执行,但临界区内部执行过程中可能会发生指令重排序。
可见性可见性(Visibility):可见性是指当一个线程修改了共享变量的值后,其他线程能够立即看到这个修改。在多线程环境中,每个线程都有自己的工作内存,对共享变量的修改可能在一个线程的工作内存中进行,其他线程并不立即感知到这个修改。为了确保可见性,需要使用同步机制(如锁、volatile关键字)来保证共享变量的值在多个线程之间的可见性。示例public class CurrencyCharacter {
/**
* 当 线程 对 count 进行修改时,会将主内存的数据读拷贝到工作内存进行操作
* 不同的线程都有自己的副本,线程之间对副本的修改可见性不会被保证
*/
int cunt;
/**
* except return value 20000
*
* @return
* @throws InterruptedException
*/
public int addCurrency() throws InterruptedException {
int loop = 10000;
Thread thread1 = new Thread(getRunnable(loop));
Thread thread2 = new Thread(getRunnable(loop));
thread1.start();
thread2.start();
thread1.join();
thread2.join();
return cunt;
}
private Runnable getRunnable(int loop) {
return () -> {
for (int i = 0; i < loop; i++) {
// count ++ 并不是原子性 简单可以分为使用、赋值+1、存储三步操作
this.cunt++;
}
};
}
}
class CurrencyCharacterTest {
@Test
void addCurrency() throws InterruptedException {
CurrencyCharacter currencyCharacter = new CurrencyCharacter();
Assertions.assertNotEquals(20000, currencyCharacter.addCurrency());
}
@Test
void order() {
int a = 1;
int b = 2; // 前面两行的执行顺序并不会影响执行结果,因此可以进行指令重排序
int c = a + b;
System.out.println(c);
}
}
总结从上述我们可以看到,缓存带来了可见性问题,多线程带来了原子性问题,编译优化(编译器、CPU)、缓存带了有序性问题,最简单的方式我们可以静止缓存,编译器、处理器指令重排优化,但是程序的性能将会大大降低。为了解决这个问题,JMM提供了一些规范,一方面让开发者可以更加方便的进行多线程编程,而不需要了解过多的了解计算机底层的实现,一方面基于这些规则要求编译器、CPU进行相关的处理,比如常见的禁止指令重排、可见性保证。【多线程系列】高效的 CAS (Compare and Swap)【多线程系列】CAS 常见的两个升级版本 CLH、MCS【多线程系列】JUC 中的另一重要大杀器 AQS 抽象队列同步器
带鱼
盘点认证协议 : 普及篇之LTPA
纯约束型协议 : OAuth , SAML , OIDC , CAS ,LTPA服务器类协议 : RADIUS , Kerberos , ADFS认证方式类 : OTP , 生物认证 (人脸 , 声纹 , 指纹)认证服务器(附带) : AD , LDAP , ADFS这一篇聊一聊 LTPA 这个协议 , 这个算是一个很少见的协议 , 专属于 IBM , 我们只是简单的说说它...一 . 前言ltpa 全称 Lightweight Third-Party Authentication , 即轻量级第三方认证 . 这个协议看起来很复杂 , 其实使用的时候会感觉很简单 , 用法和 JWT 很类似.LTPA 是 一项 IBM 协议 ,用于 在 WebSphere®Application Server 中提供基于 cookie 或二进制安全性令牌的认证机制 ,其支持 单点登录 SSO .整个流程中包括多个服务器 , 例如WebSphere® 和 DataPower®。要对其中一个或多个服务器实现单点登录解决方案,您可以将 WebSEAL 配置为支持 LTPA 认证 .目的 : LTPA 令牌认证的目的是将 LTPA 令牌从第一个 Web Service(其认证生成客户机)流动到下游 Web Service , 简单点说就是 IDP Server 生成令牌 , 下发到下游服务 .Cookie 作用 : 具有有效 LTPA cookie 的用户可以访问与第一个服务器属于同一身份验证域的服务器,并将自动进行身份验证。Cookie 本身包含有关已经验证的用户、用户要验证的领域(例如 LDAP 服务器)和时间戳的信息。所有这些信息之二用共享的3DES 密钥加密,并由公/私密密钥对签名。这一切都很好,直到您试图执行一些故障排除,并意识到无法查看这些 cookie 的内部。二 . 深入知识点2.1 流程分析宏观流程: User ---> ISAM SSO (WebSeal) - LTPA goes here ----> backend server WAS宏观解释 :后端WAS服务器本身不做任何身份验证。用户对SSO服务器进行身份验证,SSO服务器将加密的LTPA令牌发送到后端服务器,该服务器包含用户名(通常只有用 户名、组成员,但从不包含密码)。后端服务器解密LTPA并将其作为LTPA并信任它。然后后端应用服务器(WAS)将此身份验证详细信息传递给应用程序。流程详情:当未经认证的用户对 WebSEAL 受保护资源发出请求时,WebSEAL 将首先确定是否提供了 LTPA Cookie。完成认证操作后,将在 HTTP 响应中插入新的 LTPA Cookie,并将其传递回客户机以供其他支持 LTPA 的认证服务器使用。2.2 LTPA 架构LTPA 协议是基于 WebSphere 实现的 !WebSphere 是什么 : WebSphere 是一个IBM 产品 , 它支持在一个因特网域中的一组web 服务器间使用单一登录的认证策略 ,通过密码术 可支持分布式环境的安全性 ,web 用户只需对 WebSphere Application Server 或 Domino 服务器认证一次 ,认证将会通过服务器进行共享.**PS: 和联合认证有相同的思想 .. **一个通过有效的LTPA Cookie能够在同一个认证域中所有服务器被自动认证。此Cookie中包含认证信息和时间戳。这些信息通过共享的3DES Key进行了bis 加密。使用公共密钥/私有密钥进行签名。2.3 LTPA Token 令牌2.3.1 令牌的属性LTPA 令牌包含如下属性 :title : 必须 ,策略的标题 ,字符串description : 非必须 , 对策略的描述 ,字符串key : LTPA 秘钥 ,必须 ,用于生成 LTPA 令牌 的LTPA 秘钥名称 - 包括 :2.3.2 令牌的案例首先,这个 cookie 由以下部分组成,以%进行分隔:
- 用户信息,格式为u:user\:<RealmName>/<UserDN>,
如:u:user\:VGOLiveRealm/CN=squallzhong,O=VGOLive Technology
- 过期时间
- 签名信息,如:
u:user\:VGOLiveRealm/CN=squallzhong,O=VGOLive Technology%1301558320666%Cy2CAeru5kEElGj0hrvYsKW2ZVsvvcu6Un573aeX55OO4G3EMYWc0e/ZbqDp1z7MS+dLzniuUH4sYWCMpnKdm7ZGabwmV+WcraBl+y+yzwcl722gHVMOnDZAW7U3jEay9Tk2yG4yXkMWU+617xndpVxke2jtS5wIyVVM3q7UDPw=
2.3.3 令牌的区别LTPA (Version 1): www.ibm.com/websphere/a… LTPA2: www.ibm.com/websphere/a…LtpaToken LtpaToken 用于与 WebSphere Application Server 的前发行版进行互操作。此令牌仅包含认证身份属性。 LtpaToken 针对 WebSphere Application Server V5.1.0.2 之前的发行版(对于 z/OS®)或 V5.1.1(对于分布式系统)生成。LtpaToken2 LtpaToken2 包含更强的加密功能,并且您能够向令牌添加多个属性。此令牌包含认证身份和其他信息(例如,属性)。属性用于联系原始登录服务器和唯一高速缓存密钥。如果在确定唯一性时要考虑除身份以外的其他内容,还将使用属性来查找主题。注意 : 为了允许运行不同版本WebSphere Application Server的服务器之间的互操作性,默认情况下,在将绑定配置为期望LTPA2令牌时,Version 7.0及更高版本的JAX-WS web服务安全运行时可以成功地使用LTPA Version 1令牌。但是,您可以将JAX-WS运行时的绑定配置为只接受LTPA2令牌。有关更多信息,请参阅有关身份验证生成器或使用者令牌设置的文档。IBM LTPA 文档支持2.4 LTPA CookieCookie 加密方式LTPA cookie 在 DESede/ECB/PKCS5Padding 模式下使用3DES 密钥进行加密。真正的密钥也是在 DESede/ECB/PKCS5Padding 模式下使用3DES 加密的,其中使用用0X0最多24字节填充的所提供密码的 SHA 散列。要解密实际令牌,可以获取密码,生成一个3DES 密钥,解密加密密钥,然后解密 cookie 数据。还有一个公钥/私钥对用于对 cookie 进行签名。使用 3DES 秘钥 进行 DESede/ECB/PKCS5P 加密秘钥采用 DESede/ECB/PKCS5P进行加密通过提供的密码进行 SHA Hash对生成的24字节秘钥 进行 Base64 编码Cookie 的元素 @ my.oschina.net/psuyun/blog…LTPA token 版本(4字节)创建时间(8字节)过期时间(8字节)用户名(可变长度)Domino LTPA 密钥(20字节)在与 Domino 做 SSO 的时候,会使用 LTPA Token的认证方式,本文描述它的生成原理,通过它我们可以自己编码生成身份认证的 cookie,实现 SSO。首先,这个 cookie 由以下部分组成LTPA token 版本(4字节)创建时间(8字节)过期时间(8字节)用户名(可变长度)Domino LTPA 密钥(20字节)接下来分别说明各部分的具体内容:LTPA token 版本目前 Domino 只有一种值:0x0001创建时间为以十六进制方式表示的Unix time,例如:2009-04-09 13:52:42 (GMT +8) = 1239256362 = 49DD8D2A。过期时间=创建时间 + SSO 配置文档的过期时间(LTPA_TokenExpiration域)用户名为 Names 中用户文档的FullName域值;如:Squall Zhong/DigiwinDomino LTPA 密钥通过 Base64编码后,保存在 SSO 配置文档的LTPA_DominoSecret域中当然不能将密钥直接发送给浏览器,所以将上述部分合并起来(如上图),计算 SHA-1 校验和然后用 SHA-1 校验和替换掉 Domino LTPA 密钥,最后再将内容通过 Base64 编码,形成最终的 cookie 发送给浏览器。这样如果 cookie 中的任何内容被修改,校验和就不对了,达到了防篡改的效果。所以最终LTPA Cookie所得到的值为以下公式组成:SHA-1=LTPA版本号+创建时间+过期时间+用户名+Domino LTPA 密钥LTPA Cookie= Base64(LTPA版本号+创建时间+过期时间+用户名+SHA-1)三 . 实践3.1 一个 LTPA 的格式[token] = BASE64([header][creation time][expiration time][username][SHA-1 hash])
Header: LtpaToken 版本(长度4),Domino的固定为[0x00][0x01][0x02][0x03]
Creation time: 创建时间戳(长度8),格式为Unix time比如[2010-03-12 00:21:49]为4B99189D
expiration time:过期时间戳(长度8) 同上
username: 用户名(长度不定)
SHA-1 hash:SHA-1校验和(长度20)
- 由前面所说的密钥和其余的Token资料合并而成,合成公式如下
- [SHA-1 hash] = SHA-1([header][creation time][expiration time][username][shared secret])
3.2 Domain 解析 TokenBase64解码LtpaToken。截取最前面20字节,最后面20字节,中间部分就是用户名。如果用户名在本系统中不正确,返回无效的LtpaToken。截取最后面20字节,是SHA-1校验和。用Token中的其余部分和本系统中的密钥生成新的SHA-1校验和,如果2个校验和不匹配。返回无效的LtpaToken。当前服务器时间必须大于创建时间,小于失效时间。否则返回无效的LtpaToken。最后解析通过了,完成用户的登录3.3 Java 实现 LTPA Cookie 解析转载自 @ www.cnblogs.com/cmt/p/14580…解析方法
// LTPA 3DES 密钥
String ltpa3DESKey = "7dH4i81YepbVe+gF9XVUzE4C1Ca5g6A4Q69OFobJV9g=";
// LTPA 密钥密码
String ltpaPassword = "Passw0rd";
try {
// 获得加密key
byte[] secretKey = getSecretKey(ltpa3DESKey, ltpaPassword);
// 使用加密key解密ltpa Cookie
String ltpaPlaintext = new String(decryptLtpaToken(tokenCipher,
secretKey));
displayTokenData(ltpaPlaintext);
} catch (Exception e) {
System.out.println("Caught inner: " + e);
}
//获得安全Key
private static byte[] getSecretKey(String ltpa3DESKey, String password)
throws Exception {
// 使用SHA获得key密码的hash值
MessageDigest md = MessageDigest.getInstance("SHA");
md.update(password.getBytes());
byte[] hash3DES = new byte[24];
System.arraycopy(md.digest(), 0, hash3DES, 0, 20);
// 使用0替换后4个字节
Arrays.fill(hash3DES, 20, 24, (byte) 0);
// BASE64解码 ltpa3DESKey
byte[] decode3DES = Base64.decodeBase64(ltpa3DESKey.getBytes());
// 使用key密码hash值解密已Base64解码的ltpa3DESKey
return decrypt(decode3DES, hash3DES);
}
//解密LtpaToken
public static byte[] decryptLtpaToken(String encryptedLtpaToken, byte[] key)
throws Exception {
// Base64解码LTPAToken
final byte[] ltpaByteArray = Base64.decodeBase64(encryptedLtpaToken
.getBytes());
// 使用key解密已Base64解码的LTPAToken
return decrypt(ltpaByteArray, key);
}
// DESede/ECB/PKC5Padding解方法
public static byte[] decrypt(byte[] ciphertext, byte[] key)
throws Exception {
final Cipher cipher = Cipher.getInstance("DESede/ECB/PKCS5Padding");
final KeySpec keySpec = new DESedeKeySpec(key);
final Key secretKey = SecretKeyFactory.getInstance("TripleDES")
.generateSecret(keySpec);
cipher.init(Cipher.DECRYPT_MODE, secretKey);
return cipher.doFinal(ciphertext);
}
生成 LTPA Token
/**
02 * 为指定用户创建有效的LTPA Token.创建时间为<tt>now</tt>.
03 *
04 * @param username
05 * - 用户名,注:使用用户全称,如:CN=SquallZhong/O=VGOLive Technology
06 * @param creationTime
07 * - 创建时间
08 * @param durationMinutes
09 * - 到期时间,单位:分钟
10@param ltpaSecretStr
11 * - Domino Ltpa 加密字符串
12 * @return - 返回已Base64编码的Ltpa Cookie.
13 * @throws NoSuchAlgorithmException
14 * @throws Base64DecodeException
15 */
16 public static String createLtpaToken(String username,
17 GregorianCalendar creationTime, int durationMinutes,
18 String ltpaSecretStr) throws NoSuchAlgorithmException {
19 // Base64解码ltpaSecretStr
20 byte[] ltpaSecret = Base64.decodeBase64(ltpaSecretStr.getBytes());
21 // 用户名字节数组
22 byte[] usernameArray = username.getBytes();
23 byte[] workingBuffer = new byte[preUserDataLength
24 + usernameArray.length + ltpaSecret.length];
25
26 // 设置ltpaToken版本至workingBuffer
27 System.arraycopy(ltpaTokenVersion, 0, workingBuffer, 0,
28 ltpaTokenVersion.length);
29 // 获得过期时间,过期时间=当前时间+到期时间(分钟)
30 GregorianCalendar expirationDate = (GregorianCalendar) creationTime
31 .clone();
32 expirationDate.add(Calendar.MINUTE, durationMinutes);
33
34 // 转换创建时间至16进制字符串
35 String hex = dateStringFiller
36 + Integer.toHexString(
37 (int) (creationTime.getTimeInMillis() / 1000))
38 .toUpperCase();
39 // 设置创建时间至workingBuffer
40 System.arraycopy(hex.getBytes(), hex.getBytes().length
41 - dateStringLength, workingBuffer, creationDatePosition,
42 dateStringLength);
43
44 // 转换过期时间至16进制字符串
45 hex = dateStringFiller
46 + Integer.toHexString(
47 (int) (expirationDate.getTimeInMillis() / 1000))
48 .toUpperCase();
49 // 设置过期时间至workingBuffer
50 System.arraycopy(hex.getBytes(), hex.getBytes().length
51 - dateStringLength, workingBuffer, expirationDatePosition,
52 dateStringLength);
53
54 // 设置用户全称至workingBuffer
55 System.arraycopy(usernameArray, 0, workingBuffer, preUserDataLength,
56 usernameArray.length);
57
58 // 设置已Base64解码ltpaSecret至workingBuffer
59 System.arraycopy(ltpaSecret, 0, workingBuffer, preUserDataLength
60 + usernameArray.length, ltpaSecret.length);
61 // 创建Hash字符串
62 byte[] hash = createHash(workingBuffer);
63
64 // ltpaToken版本+开始时间(16进制)+到期时间(16进制)+用户全名+SHA-1(ltpaToken版本+开始时间(16进制)+到期时间(16进制)+用户全名)
65 byte[] outputBuffer = new byte[preUserDataLength + usernameArray.length
66 + hashLength];
67 System.arraycopy(workingBuffer, 0, outputBuffer, 0, preUserDataLength
68 + usernameArray.length);
69 System.arraycopy(hash, 0, outputBuffer, preUserDataLength
70 + usernameArray.length, hashLength);
71 // 返回已Base64编码的outputBuffer
72 return new String(Base64.encodeBase64(outputBuffer));
73 }
74…...
通过F5 BIG-IP创建Domino LTPAToken
when RULE_INIT {
set cookie_name "LtpaToken" # Don't change this
set ltpa_version "\x00\x01\x02\x03" # Don't change this
set ltpa_secret "b64encodedsecretkey" # Set this to the LTPA secrey key from your Lotus Domino LTPA configuration
set ltpa_timeout "1800" # Set this to the timeout value from your Lotus Domino LTPA configuration
}
when HTTP_REQUEST {
#
# Do your usual F5 HTTP authentication here
#
# Initial values
set creation_time_temp [clock seconds]
set creation_time [format %X $creation_time_temp]
set expr_time_temp [expr { $creation_time_temp + $::ltpa_timeout}]
set expr_time [format %X $expr_time_temp]
set username [HTTP::username]
set ltpa_secret_decode [b64decode $::ltpa_secret]
# First part of token
set cookie_data_raw {}
append cookie_data_raw $::ltpa_version
append cookie_data_raw $creation_time
append cookie_data_raw $expr_time
append cookie_data_raw $username
append cookie_data_raw $ltpa_secret_decode
# SHA1 of first part of token
set sha_cookie_raw [sha1 $cookie_data_raw]
# Final not yet encoded token
set ltpa_token_raw {}
append ltpa_token_raw $::ltpa_version
append ltpa_token_raw $creation_time
append ltpa_token_raw $expr_time
append ltpa_token_raw $username
append ltpa_token_raw $sha_cookie_raw
# Final Base64 encoded token
set ltpa_token_final [b64encode $ltpa_token_raw]
# Insert the cookie
HTTP::cookie insert name $::cookie_name value $ltpa_token_final
}
# Remove Authorization HTTP header to avoid using basic authentication
if { [HTTP::header exists "Authorization"] } {
HTTP::header remove "Authorization"
}
}
总结LTPA 是一个企业痕迹很重的协议 ,它基本上归属于 IBM , 这就意味着使用如果有困难 , 文档不一定能解决 , 而请求服务支持可不是一个简单的事情 .当然 ,在使用中 , ltpa cookie 也可以被当成一种 JWT Token 的一种生成方式 , 将其放在Cookie 中 ,再基于 SSO 认证 , 这不算一个 LTPA 体系 , 仅仅是使用了 LTPA 的 Cookie 生成方式 .
带鱼
Java 多线程 : 真想聊清楚线程池
一 . 线程池简介1 线程池的元素 线程池主要由两个概念组成,一个是任务队列,另一个是工作者线程。任务队列是一个阻塞队列,保存待执行的任务。工作者线程主体就是一个循环,循环从队列中接受任务并执行。2 为什么要用线程池降低资源消耗。 通过重复利用已创建的线程降低线程创建和销毁造成的消耗。提高响应速度。 当任务到达时,任务可以不需要的等到线程创建就能立即执行。提高线程的可管理性。 线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。3 线程池中的核心概念BlockingQueue workQueue : 用于保留任务并移交给工作线程的队列HashSet workers : 线程池中所有的工作线程4 线程池的原理定义 : 线程池通过一个叫 ctl 的 AtomicInteger 决定运行情况 , 通过 ThreadFactory 创建线程 , 并且把等待的线程放入 workQueue , 等待移交给工作线程二. 常见的线程池// 基本对象
ThreadPoolExecutor
// 可重用固定线程数的线程池
FixedThreadPool
- ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(2);
// 使用单个worker线程的Executor
SingleThreadExecutor
// 会根据需要创建新线程的线程池
CachedThreadPool
三. 线程池的创建线程池创建可以通过 ThreadPoolExecutor 和 工具类 Executors 实现3.1 通过构造方法实现(推荐)通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则3.2 通过Executor 框架的工具类Executors来实现 (个人demo 可以考虑)3.2.1 FixedThreadPoolreturn new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());
该方法返回一个固定线程数量的线程池。 (corePoolSize == maximumPoolSize)使用LinkedBlockingQuene作为阻塞队列当线程池没有可执行任务时,也不会释放线程该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。3.2.2 SingleThreadExecutorreturn new FinalizableDelegatedExecutorService (
new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>()));
方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。3.2.3 CachedThreadPool:return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,new SynchronousQueue<Runnable>());
该方法返回一个可根据实际情况调整线程数量的线程池。(默认缓存60s , 线程池的线程数可达到Integer.MAX_VALUE,即2147483647)内部使用SynchronousQueue作为阻塞队列线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。在没有任务执行时,当线程的空闲时间超过keepAliveTime,会自动释放线程资源3.2.4 ScheduledExecutorService :return new DelegatedScheduledExecutorService(new ScheduledThreadPoolExecutor(1));
初始化的线程池可以在指定的时间内周期性的执行所提交的任务,在实际的业务场景中可以使用该线程池定期的同步数据四. Fork/Join1 > Fork / Join 的核心是 ForkJoinPool , 用于来管理工作线程
: 工作线程一次只能执行一个任务 ,
: 不会根据任务创建线程,而是将任务存储到工作线程的双端队列中
2 > Fork / join 的思路是分而治之
- Fork 递归的将任务分为较小的子任务
- Join : 将子任务递归的串联成单个结果
3 > 工作窃取算法 : 空闲的线程试图从繁忙的线程(他们的双端队列)中窃取工作
// Fork/Join 依赖于 ForkJoinPool , 此处仅简单介绍 , 详情参考十六章
五. ThreadPoolExecutorThreadPoolExecutor实现了生产者/消费者模式,
- 工作者线程就是消费者
- 任务提交者就是生产者,线程池自己维护任务队列。
> ThreadPoolExecutor
- AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
: 此变量 记录了 “线程池中的任务数量”和“线程池的状态”两个信息
: 高3位表示"线程池状态",低29位表示"线程池中的任务数量"
- RUNNING : 111 : 该线程池能接收新任务 ,且能对新任务进行处理
- SHUTDOWN : 000 : 不能接收新任务 ,但是可以对任务进行处理
- STOP : 001 : 不添加新任务 , 不对任务进行处理 , 会中断正在执行的任务
- TIDYING : 010 : 当所有的任务已终止,ctl记录的"任务数量"为0,线程池会变为TIDYING状态
- 当所有的任务已终止,ctl记录的"任务数量"为0,线程池会变为TIDYING状态
- TERMINATED : 011 : 线程池彻底终止的状态
----------------------------------------
> ThreadPoolExecutor 的参数
- corePoolSize : 线程池中核心线程的数量
- maximumPoolSize : 线程池中允许的最大线程数
- keepAliveTime : 线程空闲的时间
- unit : keepAliveTime的单位
- workQueue : 用来保存等待执行的任务的阻塞队列,等待的任务必须实现Runnable接口
- threadFactory : 用于设置创建线程的工厂
- allowCoreThreadTimeOut : 允许核心线程过期
- Handler : 处理器
- defaultHandler : 任务拒绝处理器
六 . 线程池的饱和和动态调整// 线程池的饱和策略 , 当线程池满了. 会通过对应的策略
1、AbortPolicy:直接抛出异常,默认策略;
2、CallerRunsPolicy:用调用者所在的线程来执行任务;
3、DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
4、DiscardPolicy:直接丢弃任务;
// ThreadPoolExecutor 提供了动态调整线程池容量大小的方法:
• setCorePoolSize:设置核心池大小。
• setMaximumPoolSize:设置线程池最大能创建的线程数目大小。
当上述参数从小变大时,ThreadPoolExecutor 进行线程赋值,还可能立即创建新的线程来执行任务。
// 动态调整源码核心 :
七. 线程池执行任务的过程刚创建时,里面没有线程调用 execute() 方法,添加任务时:完成一个任务,继续取下一个任务处理。 没有任务继续处理,线程被中断或者线程池被关闭时,线程退出执行,如果线程池被关闭,线程结束。 否则,判断线程池正在运行的线程数量是否大于核心线程数,如果是,线程结束,否则线程阻塞。因此线程池任务全部执行完成后,继续留存的线程池大小为 corePoolSize 。八. 线程池中 submit 和 execute 方法有什么区别两个方法都可以向线程池提交任务。#execute(...) 方法,返回类型是 void ,它定义在 Executor 接口中 , 必须实现Runnable接口 。#submit(...) 方法,可以返回持有计算结果的 Future 对象,它定义在 ExecutorService 接口中,它扩展了 Executor 接口,其它线程池类像 ThreadPoolExecutor 和 ScheduledThreadPoolExecutor 都有这些方法。九 . 如果你提交任务时,线程池队列已满,这时会发生什么重点在于线程池的队列是有界还是无界的。> 如果你使用的 LinkedBlockingQueue,也就是无界队列的话,没关系,继续添加任务到阻塞队列中等待执行,因为 LinkedBlockingQueue 可以近乎认为是一个无穷大的队列,可以无限存放任务。> 如果你使用的是有界队列比方说 ArrayBlockingQueue 的话,任务首先会被添加到 ArrayBlockingQueue 中,ArrayBlockingQueue满了,则会使用拒绝策略 RejectedExecutionHandler 处理满了的任务,默认是 AbortPolicy 。十 . 线程池的底层逻辑要想弄清楚这一部分 , 首先得理解 Queue , Worker , Task , Thread 等多个概念Queue :Worker :Task :Thread :10.1 线程池的物理结构Worker 对象Worker 对象是 ThreadPoolExecutor 中的一个内部类 , 他是一个包装类 , 是一个线程单元 , 同时提供线程的中断等功能// 问题一 : Worker 结构
private final class Worker extends AbstractQueuedSynchronizer implements Runnable {
// 被封装的线程
final Thread thread;
// 初始任务
Runnable firstTask;
// 线程任务计数器
volatile long completedTasks;
// 可以看到 , 把 worker 都行构建成了 Thread
Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
public void run() {
runWorker(this);
}
protected boolean isHeldExclusively() {....}
// 获取同步状态
protected boolean tryAcquire(int unused) {....}
// 释放同步状态
protected boolean tryRelease(int unused) {....}
//锁操作
public void lock() { acquire(1); }
public boolean tryLock() { return tryAcquire(1); }
public void unlock() { release(1); }
public boolean isLocked() { return isHeldExclusively(); }
// 暂停操作
void interruptIfStarted(){....}
}
// 问题二 : ThreadPoolExecutor中的线程包装
- 线程被封装成一个对象Worker
- 通过调用 runWorker(Worker w) 获取任务并执行的死循环
- 如果任务的运行出了什么问题 ,调用 processWorkerExit() 处理
C- ThreadPoolExecutor
PVC- Worker
M- run : public void run() {runWorker(this); }
拒绝策略部分 PolicyThreadPoolExecutor 中提供了4 个拒绝策略内部类 , 具体的类型详见上文 , 这里来看一下结构 :public interface RejectedExecutionHandler {
void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}
10.2 主要流程Step 1 : 进入的起点 - 线程的封装// task 的构建 : 匿名传进来的线程会构建成一个 FutureTask
RunnableFuture<Void> ftask = newTaskFor(task, null);
protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
return new FutureTask<T>(runnable, value);
}
Step 2 : 运行的起点 - executeThreadPoolExecutor 中 excutor 方法是执行的起点 , 其中会进行三种操作当线程池未满时 , 直接 addWorker 运行当线程池满了且正在运行时 , 将线程加入 workQueue 中当上述均失败后 , 就会调用 reject 来处理异常情况 (RejectedExecutionHandler)// 问题一 : execute 中线程池处理任务的逻辑
1 int c = ctl.get();
2 if (workerCountOf(c) < corePoolSize) {
3 if (addWorker(command, true))
4 return;
5 c = ctl.get();
6 }
7 if (isRunning(c) && workQueue.offer(command)) {
8 int recheck = ctl.get();
9 if (! isRunning(recheck) && remove(command))
10 reject(command);
11 else if (workerCountOf(recheck) == 0)
12 addWorker(null, false);
13 }
14 else if (!addWorker(command, false))
15 reject(command);
// 2 : workerCountOf 判断当前线程数是否小于corePoolSize 从而决定是否通过 addWorker 创建线程
// 7 : 如果线程池已满 ,且状态为 running , 尝试把任务添加到 workQueue
// 14 : 如果 7 步处理失败 , 尝试 addWorker , 失败则通过 reject 处理
//补充 : addWork 作用
- 检查是否可以根据当前池状态和给定边界(核心或最大)添加新的工作者
- 创建并启动新的worker,运行firstTask作为它的第一个任务
// 问题二 : 线程池运行 Work 详情 (简述一下就是核心的四步)
1 addWorker(Runnable firstTask, boolean core) : 可以看到addWorker 添加的是一个 Runnable
2 new Worker(firstTask) :如果状态符合 ,会创建一个 Worker 对象
Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
3 final Thread t = w.thread;
?- 这里将Thread 取了出来
4 后文将会 t.start()运行
// 补充 : 期间还会进行锁的处理 , 省略一些的主要流程如下
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// 核心一 : 状态判断
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
// 核心二 : 容量满了的处理 , 退出或者重试 (可以看到 c 语言的影子)
for (;;) {
int wc = workerCountOf(c);
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); // Re-read ctl
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
// 核心三 : 处理开始
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
int rs = runStateOf(ctl.get());
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
workers.add(w);
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock();
}
if (workerAdded) {
// 核心四 : 启动线程
t.start();
workerStarted = true;
}
}
} finally {
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
Step 3 : 工厂的创建// 问题四 : 创建工厂
从上文说到 , 线程池通过 ThreadFactory 创建线程 (newThread()) ,
Step 4 : 线程的复用在上文 Step 1 问题一 中 , 将 线程加入到 workQueue 中了isRunning(c) && workQueue.offer(command) , 这里就是取出来的步骤 :// 这个问题涉及到的方法主要包括 getTask ()
M- runWorker
while (task != null || (task = getTask()) != null) : 死循环 , 只要还有 task 就会执行
- task.run() : 获取到 task 后 通过 task run 执行
M- getTask()
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
// 补充 : 这里可以看到 , worker 可以理解为一个工作线程 ,他通过 while 不停的从 queue 中获取 task 执行
// 这里很有趣 , worker 更像一个加工工厂 , 我一开始还以为迭代的是 worker , 现在发现是在 worker 上锁后在里面迭代
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
// task 实际上不是 worker 的内部属性
while (task != null || (task = getTask()) != null) {
// 上锁
w.lock();
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
beforeExecute(wt, task);
// 线程执行
task.run();
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}
Step 5 : 拒绝策略// 瞅一瞅拒绝策略 :
当你的线程池满了后 , 通常这个异常就爆出来了
java.util.concurrent.RejectedExecutionException:
Task java.util.concurrent.FutureTask@523424b5 rejected from
java.util.concurrent.ThreadPoolExecutor@319dead1
[Running, pool size = 6, active threads = 6, queued tasks = 3, completed tasks = 0]
- 尽管我们可以通过拒绝策略有很多种 ,但是超高并发的时候哪一种都不靠谱 , 所以我们先看下 , 这个拒绝策略怎么来的
1 从问题三代码的第七行我们就能看到 workQueue.offer(command) , queue 已经 offer 失败了 , 说明Queue 也满了
2 到14行 , 再次通过 addWork 直接运行 , 失败了
3 执行了 reject 方法 , handler.rejectedExecution(command, this);
?- handler 是接口 ,他有四个实现类 , 具体含义可以见上文拒绝策略
4 例如 AbortPolicy 就是 throw new RejectedExecutionException , CallerRunsPolicy 就是再次run
(主线程慢慢跑 , 肯定慢的)
- 所以部分业务我们要改 , 怎么改 ?
1 spring 里面可以自定义你的拒绝策略 , 可以参考这一篇的用法
@ https://blog.csdn.net/tanglei6636/article/details/90721801
2 ThreadPoolExecutor 构造器里面也有
- 改的思路 ?
前提一是集群已经无法解决 (基本上现阶段集群都能满足) ,且你无法节流
1 放到消息队列
2 入库
3 写盘
4 放集合 , 单独一个线程 , 用一个取一个
Step 6 : 线程的关闭1 checkShutdownAccess 校验是否可以关闭
2 RunState 改为 STOP
3 ReentrantLock and isInterrupted
4 drainQueue : remove queue 队列
Step 7 : 如何实现回调 ?submit 回调
- <T> Future<T> submit(Callable<T> task)
?- 很明显 , submit 返回的是 Future , 这就意味着主线程能阻塞等待
- RunnableFuture<T> ftask = newTaskFor(task);
10.2 底层复杂分析ctl 到底怎么玩的 ?> private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
?- 线程池里面通过 ctl 来判断线程的状态 , 前面说了线程池高3位表示 "线程池状态",低29位表示线程池中的任务数量-
?- 以 STOP 状态为例 , 在运行的时候 ,他的十进制值为 536870912
- 首先 ,我们将他转换为二进制 -> 10000 00000 00000 00000 00000 00000
- 获取后面的29位 ,然后前面补齐 , 最后的高三位即为 001
?- 而 TIDYING 对应的就是 00001 00000 00000 00000 00000 00000 00000 -> 010
?- RUNNING 为 -1 , 按照为数不多的一点残留知识 , 这里说成111是因为负数按照补码表示的原因
?- 众所周知 , 二进制处理的效率最高 ,所以这么玩合情合理
线程池公式> 计算密集型 :Ncpu + 1
> 包含了 I/O和其他阻塞操作的任务 : Nthreads = Ncpu x Ucpu x (1 + W/C)
Ncpu = CPU的数量
Ucpu = 目标CPU的使用率, 0 <= Ucpu <= 1
W/C = 等待时间与计算时间的比率
> IO密集型 = 2Ncpu
比较当前线程容量的方法 workerCountOf(c) 为什么要 & 一个 CAPACITY ?1 > public static final int SIZE = 32;
?- 用二进制补码形式表示int值的位数
2 > private static final int COUNT_BITS = Integer.SIZE - 3;
3 > private static final int CAPACITY = (1 << COUNT_BITS) - 1;
4 > private static int workerCountOf(int c) { return c & CAPACITY; }
// 原因一 : 还是状态的原因 , 低 29 位才是 线程数量 , 加上这个参数才能包装低 29 二进制时最开始为 0
十一. 线程池使用
// 构造一个线程池 , 推荐用法
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(5, 6, 3, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(3));
// 除了测试 , 尽量避免使用以下方法构建线程池
// 线程创建
// 1 CachedThreadPool
ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newCachedThreadPool();
executor.submit(() -> {}
// 2 FixedThreadPool
ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(2);
executor.submit(() -> {}
// 3 SingleThreadExecutor
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(() -> {}
// 4 SingleThreadScheduledExecutor
ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
executor.scheduleWithFixedDelay(() -> {}
// 线程池的关闭
executor.shutdown();
executor.shutdownNow();
// 信息获取
executor.isTerminated() : 是否关闭
executor.getPoolSize()
executor.getQueue().size()
十二 . 线程池的想法// 使用线程池时有一些规约和建议是需要注意的 :
- 创建线程或线程池时请指定有意义的线程名称
- 线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式
- 并且不建议创建无界线程 , 避免 OOM
- 必须回收自定义的 ThreadLocal 变量,尤其在线程池场景下
// 注意点 :
- 注意线程池的拒绝策略 , 当线程池满了时 , 可能会因为策略带来系统崩溃
- newCachedThreadPool 也有可能出现 OOM , 其最大值为 newCachedThreadPool
-
带鱼
Java 多线程 : 简简单单原子类
一. 原子类的简述1.1 原子类的应用场景原子类适用于需要原子操作而有需要减少资源消耗时 , 原子类相当于 volatile 和 CAS 的工具类 .1.2 原子类的类型基本类型 : 使用原子的方式更新基本类型AtomicInteger:整形原子类AtomicLong:长整型原子类AtomicBoolean :布尔型原子类数组类型 : 使用原子的方式更新数组里的某个元素AtomicIntegerArray:整形数组原子类AtomicLongArray:长整形数组原子类AtomicReferenceArray :引用类型数组原子类引用类型AtomicReference:引用类型原子类AtomicStampedRerence:原子更新引用类型里的字段原子类AtomicMarkableReference :原子更新带有标记位的引用类型对象的属性修改类型AtomicIntegerFieldUpdater:原子更新整形字段的更新器AtomicLongFieldUpdater:原子更新长整形字段的更新器AtomicStampedReference :原子更新带有版本号的引用类型。1.3 . AtomicInteger 类常用方法public final int get() : 获取当前的值javapublic final int getAndSet(int newValue) : 获取当前的值,并设置新的值public final int getAndIncrement(): 获取当前的值,并自增public final int getAndDecrement() : 获取当前的值,并自减public final int getAndAdd(int delta) : 获取当前的值,并加上预期的值boolean compareAndSet(int expect, int update) : 如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update)public final void lazySet(int newValue) : 最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。public final int incrementAndGet() : 以原子方式给当前值加1并获取新值public final int decrementAndGet() : 以原子方式给当前值减1并获取新值public final int addAndGet(int delta) : 以原子方式给当前值加delta并获取新值public final boolean compareAndSet(int expect, int update) : CAS 比较方法二 . 原子类的原理2.1 原理简述原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 contextswitch切换到另一个线程 , 之所以称为原子变量,是因为其包含一些以原子方式实现组合操作的方法回顾 CAS : CAS的原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的值原子类主要通过 CAS (compare and swap) + volatile 和 native 方法来保证原子操作 !// 案例 : AtomicInteger
// 它的主要内部成员是:
private volatile int value;
注意,它的声明带有volatile,这是必需的,以保证内存可见性。
// 它的大部分更新方法实现都类似,我们看一个方法incrementAndGet,其代码为:
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
// 重点 :
1 . value 是一个volatile变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值
2 . UnSafe 类的objectFieldOffset()方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址 返回值是 valueOffset
先获取当前值current,计算期望的值next然后调用CAS方法进行更新,如果当前值没有变,则更新并返回新值,否则继续循环直到更新成功为止2.2 AtomicInteger 原理深入我们从源码看看那些之前被我们忽略的东西 , 此类可以代表大多数基本类型
// Node 1 : 原子类支持序列化
implements java.io.Serializable
---------------------->
// Node 2 : CAS 对象 Unsafe , Unsafe 之前已经说过了, 其中有很多 Native 犯法
private static final Unsafe unsafe = Unsafe.getUnsafe();
---------------------->
// Node 3 : 偏移量 valueOffset
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
// objectFieldOffset() : 获取某个字段相对Java对象的“起始地址”的偏移量 , 后续通过便宜量获取方法
// getDeclaredField() : 返回一个字段对象,该对象反映由这个类对象表示的类或接口的指定声明字段
---------------------->
// Node 4 : 核心值 Value ,可以看到 value 使用 volatile 进行修饰
private volatile int value;
---------------------->
// Node 5 : 操作方法 , 可以看到 valueOffset 此时已经发挥了作用
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
---------------------->
// Node 6 : 值转换 , AtomicInteger 提供了以下四个值得固有转换方法
public int intValue() ;
public long longValue() ;
public float floatValue();
public double doubleValue();
2.3 AtomicReference 深入现在看一下 AtomicReference 有什么特别之处
// Node 1 : 不再继承 Number 接口
// Node 2 : 使用泛型方法
public class AtomicReference<V> implements java.io.Serializable
private volatile V value;
// Node 3 : 比对时使用 putOrderedObject
return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
2.4 AtomicIntegerArray 深入相对而言 AtomicIntegerArray 有更多得变化 , 其他的大同小异
// Node 1 : 对数组元素偏移进行了记录 , 此处不再是 "value" 偏移
private static final int base = unsafe.arrayBaseOffset(int[].class);
private static final int shift;
private final int[] array;
// Node 2 : 比较使用了 getAndSetInt
unsafe.getAndSetInt(array, checkedByteOffset(i), newValue)
三 . 原子类的操作3.1 原子类常见案例AtomicInteger integer = new AtomicInteger();
logger.info("------> 1 > 获取原子变量 :[{}] <-------", integer.get());
// Step 2 : 设置参数
integer.set(999);
logger.info("------> 2 > 获取原子变量 :[{}] <-------", integer.get());
logger.info("------> 失败比较获取 : 测试比较判断 :[{}] <-------", integer.compareAndSet(0, 998));
logger.info("------> 3 > 获取原子变量 :[{}] <-------", integer.get());
logger.info("------> 成功比较获取 : 测试比较判断 :[{}] <-------", integer.compareAndSet(999, 998));
logger.info("------> 4 > 获取原子变量 :[{}] <-------", integer.get());
// Step 3 : 获取当前的值,并设置新的值
logger.info("------> 测试比较判断 :[{}] <-------", integer.getAndSet(888));
logger.info("------> 5 > 获取原子变量 :[{}] <-------", integer.get());
// Step 4 : 获取当前的值,并设置新的值
logger.info("------> 测试比较判断 :[{}] <-------", integer.getAndIncrement());
logger.info("------> 6 > 获取原子变量 :[{}] <-------", integer.get());
// 以原子方式给当前值加1并获取新值
logger.info("------> 测试比较判断 :[{}] <-------", integer.incrementAndGet());
logger.info("------> 6-1 > 获取原子变量 :[{}] <-------", integer.get());
// Step 5 : 获取当前的值,并设置新的值
logger.info("------> 测试比较判断 :[{}] <-------", integer.getAndDecrement());
logger.info("------> 7 > 获取原子变量 :[{}] <-------", integer.get());
// 以原子方式给当前值减1并获取新值
logger.info("------> 测试比较判断 :[{}] <-------", integer.decrementAndGet());
logger.info("------> 7 > 获取原子变量 :[{}] <-------", integer.get());
// Step 6 : 获取当前的值,并设置新的值
logger.info("------> 测试比较判断 :[{}] <-------", integer.getAndAdd(99));
logger.info("------> 8 > 获取原子变量 :[{}] <-------", integer.get());
// 以原子方式给当前值加delta并获取新值
logger.info("------> 测试比较判断 :[{}] <-------", integer.addAndGet(99));
logger.info("------> 8 > 获取原子变量 :[{}] <-------", integer.get());
}
3.2 原子类测试多线程情况 /**
* 测多线程方式
*/
public void testThead() throws Exception {
InnerTO innerTO = new InnerTO();
MyThread[] threadDSS = new MyThread[1000];
for (int i = 0; i < 1000; i++) {
threadDSS[i] = new MyThread(innerTO);
}
for (int i = 0; i < 1000; i++) {
threadDSS[i].start();
}
logger.info("------> 原子类线程 Start 完成 :{} <-------", innerTO.getInteger().get());
for (int i = 0; i < 1000; i++) {
if (i % 100 == 0) {
Thread.sleep(1);
logger.info("------> 测试原子类 :{} <-------", innerTO.getInteger().get());
}
}
}
/**
* 包含原子类的对象
*
**/
class InnerTO {
AtomicInteger integer = new AtomicInteger();
public AtomicInteger getInteger() {
return integer;
}
public void setInteger(AtomicInteger integer) {
this.integer = integer;
}
}
/**
* 运行线程类
*
**/
class MyThread extends Thread {
public InnerTO innerTO = new InnerTO();
public MyThread(InnerTO innerTO) {
this.innerTO = innerTO;
}
@Override
public void run() {
int i = innerTO.getInteger().getAndIncrement();
if (i == 999) {
logger.info("------> 线程执行完成 <-------");
}
}
}
// 可以看到在没有锁的情况下 ,数据保证了原子性
------> 原子类线程 Start 完成 :876 <-------
------> 测试原子类 :918 <-------
------> 测试原子类 :950 <-------
------> 测试原子类 :973 <-------
------> 测试原子类 :989 <-------
------> 线程执行完成 <-------
------> 测试原子类 :1000 <-------
------> 测试原子类 :1000 <-------
------> 测试原子类 :1000 <-------
------> 测试原子类 :1000 <-------
------> 测试原子类 :1000 <-------
------> 测试原子类 :1000 <-------
欢迎大家关注我的相关文档 多线程集合参考文档[芋道源码](http://www.iocoder.cn/JUC/sike/aqs-3/)
[死磕系列](http://cmsblogs.com/?cat=151)
带鱼
盘点认证协议 : 普及篇之SAML
纯约束型协议 : OAuth , SAML , OIDC , CAS服务器类协议 : RADIUS , Kerberos , ADFS认证方式类 : OTP , 生物认证 (人脸 , 声纹 , 指纹)认证服务器(附带) : AD , LDAP这一篇主要说SAML , 这货老而弥坚 !一 . 前言SAML 其实算是一种格式规范 , 他的全称是安全断言标记语言(英语:Security Assertion Markup Language,简称SAML,发音sam-el)是一个基于XML的开源标准数据格式 . 而我们应用中的 SAML 是一种宏观实现 ,通过 SAML 格式来传输认证信息 .通常情况下 , 开发人员解除到的都是 OAuth , 第一印象往往会感觉 SAML 是一个很 '古老' 的协议 , 但是接触多了就会发现 ,SAML 在一些重量级的应用里面随处可见 , 比如 老牌的Windows Server , 其中就大量使用了 SAML 认证 , 这协议确实老而弥坚 .一句话说清楚 SAML 是什么流程 : IDP Metadata 为 SSO (Server ) 元数据 , SP Metadata 为 Client 元数据 , 元数据文件中包含其本身的认证数据 (认证地址 , 签名 , 公钥等) , Server 和 Client 互相持有对方的元数据 , 通过对方元数据中的公钥进行加密 ,通过签名校验合法性 , 再通过 认证地址进行请求或者回调 .二 . SAML 主要概念2.1 行为概念用一句话来解释其中的关联 : 服务端(客户端) 读取元数据信息, 使用指定的协议 , 通过绑定 , 将断言发送给对方 !断言 (Assertions) 即信息:断言是在 SAML 中用来描述认证的对象,其中包括一个用户在什么时间、以什么方式被认证,同时还可以包括一些扩展信息,比如用户的 Email 地址和电话等等。协议 (Protocol) 即通信:协议规定如何执行不同的行为。这些行为被细化成一些列的 Request 和 Response 对象,而在这些请求和相应的对象中包含了行为所特别需要的信息。比如,认证请求协议(AuthnRequest Protocol)就规定了一个 SP 如何请求去获得一个被认证的与用户。绑定 (Binding) 即传输:绑定定义了 SAML 信息如何使用通信协议被传输的。比如,HTTP 重定向绑定,即声明 SAML 信息将通过 HTTP 重定向消息传输;再比如 SAML SOAP 绑定,声明了通过 SOAP 来传递 SAML 消息。比如上面 AuthnRequest 就声明了 Http-POst 绑定元数据 (MetaData):SAML 的元数据是配置数据,其包含关于 SAML 通信各方的信息,比如通信另一方的 ID、Web Service 的 IP 地址、所支持的绑定类型以及通信中实用的密钥等等。2.2 说明概念认证声明:声明用户是否已经认证,通常用于单点登录。属性声明:声明某个 Subject 所具有的属性。授权决策声明:声明某个资源的权限,即一个用户在资源 R 上具有给定的 E 权限而能够执行 A 操作。2.3 元数据层次MeteData 文件中总共有四个层次 :层次 一 : Assertion : 断言 <saml:Assertion>层次 二 : Protocols :规定如何请求(samlrequest)和回复(samlresponse)saml消息,当然包含assertion消息<samlp:AuthnRequest> + <samlp:Response>层次 三 :Bindings : 绑定 ,决定用何种方式进行传输层次 四 :Profile : 配套方案 ,2.4 证书作用之前说了2点 , 一个是Server/Client 持有双方的元数据 , 一个元数据中包含了详细的证书,密钥信息,具体的元数据文件我们后面再说 , 这里先说说元数据里面的证书 .可信实体包含公钥的证书会以X.509证书格式发布在metadata中,而对应的私钥则安全保存在本地。这些密钥被用于消息层面的签名和加密,而SAML消息在传输过程中由TLS协议来进行安全交换。阶段一 : 当IDP 拿到 SP 的请求时 , 证书的作用并不明显 , 主要有如下的作用确定请求来着信任的 SP阶段二 : 当 SP 获取 IDP 的反馈时 , SP 会做以下几件事通过签名确定来自已知的 IDP获取使用IDP私钥签名的内容使用 IDP 公钥验证签名//IDP MetaData 密钥相关
- signing : 签名
- encryption : 加密
//SP MetaData 密钥相关
- sign : 签名
- encryption : 加密
- ds:Signature : IDP 密钥信息
IDP Metedata 文件参考
<EntityDescriptor
xmlns="urn:oasis:names:tc:SAML:2.0:metadata"
xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
xmlns:shibmd="urn:mace:shibboleth:metadata:1.0"
xmlns:mdui="urn:oasis:names:tc:SAML:metadata:ui" entityID="http://127.0.0.1/samlServer/idp">
<IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol urn:oasis:names:tc:SAML:1.1:protocol urn:mace:shibboleth:1.0">
<Extensions>
<shibmd:Scope regexp="false">scope</shibmd:Scope>
</Extensions>
<KeyDescriptor use="signing">
<ds:KeyInfo>
<ds:X509Data>
<ds:X509Certificate>...[签名信息]...</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</KeyDescriptor>
<KeyDescriptor use="encryption">
<ds:KeyInfo>
<ds:X509Data>
<ds:X509Certificate>...[公钥信息]...</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</KeyDescriptor>
<NameIDFormat>urn:mace:shibboleth:1.0:nameIdentifier</NameIDFormat>
<NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat>
<SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="http://127.0.0.1/samlServer/idp/profile/SAML2/POST/SLO"/>
<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="http://127.0.0.1/samlServer/idp/profile/SAML2/POST/SSO"/>
<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="http://127.0.0.1/samlServer/idp/profile/SAML2/Redirect/SSO"/>
<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="http://127.0.0.1/sso/idp/profile/SAML2/SOAP/ECP"/>
</IDPSSODescriptor>
</EntityDescriptor>
SP Metedata 文件参考
<?xml version="1.0" encoding="UTF-8"?><md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" ID="_5p2k1rpmlbr0hphrontb6zwiplqac1xxpzvzdma" entityID="http://127.0.0.1:9081/mfa-client/saml/callback" validUntil="2040-05-07T14:08:50.499Z">
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:SignedInfo>
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<ds:Reference URI="#_5p2k1rpmlbr0hphrontb6zwiplqac1xxpzvzdma">
<ds:Transforms>
<ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
</ds:Transforms>
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<ds:DigestValue>GSp3lIKMQs70Q6FQYHWFhVaGKJv31AiRTuXOcyO78mk=</ds:DigestValue>
</ds:Reference>
</ds:SignedInfo>
<ds:SignatureValue>
...[IDP签名信息]...
</ds:SignatureValue>
<ds:KeyInfo>
<ds:X509Data>
<ds:X509Certificate>
...[IDP公钥信息]...
</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</ds:Signature>
<md:Extensions xmlns:alg="urn:oasis:names:tc:SAML:metadata:algsupport">
<alg:SigningMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<alg:SigningMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha384"/>
<alg:SigningMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha512"/>
<alg:SigningMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
<alg:SigningMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256"/>
<alg:SigningMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384"/>
<alg:SigningMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha512"/>
<alg:SigningMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha1"/>
<alg:SigningMethod Algorithm="http://www.w3.org/2000/09/xmldsig#dsa-sha1"/>
<alg:SigningMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#hmac-sha256"/>
<alg:SigningMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#hmac-sha384"/>
<alg:SigningMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#hmac-sha512"/>
<alg:SigningMethod Algorithm="http://www.w3.org/2000/09/xmldsig#hmac-sha1"/>
<alg:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<alg:DigestMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#sha384"/>
<alg:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
</md:Extensions>
<md:SPSSODescriptor AuthnRequestsSigned="false" WantAssertionsSigned="false" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol urn:oasis:names:tc:SAML:1.0:protocol urn:oasis:names:tc:SAML:1.1:protocol">
<md:Extensions xmlns:init="urn:oasis:names:tc:SAML:profiles:SSO:request-init">
<init:RequestInitiator Binding="urn:oasis:names:tc:SAML:profiles:SSO:request-init" Location="http://127.0.0.1/client/saml/callback?client_name=samlClient"/>
</md:Extensions>
<md:KeyDescriptor use="signing">
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:X509Data>
<ds:X509Certificate>...[签名信息]...</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</md:KeyDescriptor>
<md:KeyDescriptor use="encryption">
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:X509Data>
<ds:X509Certificate>...[公钥信息]...</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</md:KeyDescriptor>
<md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="http://127.0.0.1/client/saml/callback?client_name=samlClient&logoutendpoint=true"/>
<md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST-SimpleSign" Location="http://127.0.0.1/client/saml/callback?client_name=samlClient&logoutendpoint=true"/>
<md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="http://127.0.0.1/client/saml/callback?client_name=samlClient&logoutendpoint=true"/>
<md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="http://127.0.0.1/client/saml/callback?client_name=samlClient&logoutendpoint=true"/>
<md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</md:NameIDFormat>
<md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:persistent</md:NameIDFormat>
<md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</md:NameIDFormat>
<md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat>
<md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="http://127.0.0.1/client/saml/callback?client_name=SAML2Client" index="0"/>
<md:AttributeConsumingService index="0">
<md:RequestedAttribute FriendlyName="eduPersonPrincipalName" Name="urn:oid:1.3.6.1.4.1.5923.1.1.1.6" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" isRequired="false"/>
</md:AttributeConsumingService>
</md:SPSSODescriptor>
</md:EntityDescriptor>
三 . SAML 流程3.1 SAML 成员及交互SAML 中有2个重要的成员 :SP(Service Provider) : 向用户提供正式商业服务的实体,通常需要认证一个用户的身份;IDP(Identity Provider) : 提供用户的身份鉴别,确保用户是其所声明的身份SAML 请求流程 :请求端作为一个资源访问者 , 期望访问服务商的资源SP 发现这是一个未认证的请求 , 此时会返回一个HTML , Form 的隐藏域中有一个 SAML 的认证请求数据包 , 实际上这里不需要手动 , HTML 会通过JS 自动执行到 IDP 中通过 JS 会自动提交到 IDP 中 , 此时 IDP 接收到请求后 , 返回验证界面( 2-3 步在浏览器端不可见 )IDP 给浏览器返回一个待登录的页面 (即常规的登录页)用户进行了认证 , 并且成功提交到 IDPIDP 组装一个HTML Form , 其中包含一个Response ,Response 中有一个对成功用户的断言 (用户信息及权限) , 有认证情况 , 及私钥签名等HTML 被自动提交到 SP , 前面说了 第六步的时候会同时返回一个私钥 , 这一步中 ,SP 会通过公钥校验断言是否合法剩下的就是判断是一个合法用户 ,然后重定向到请求的页面中SAML 中的成员定位关于 IDP 和 SP 的角色定位 , 与OAuth不同 , 认证中心和资源服务被刻意的区分为了2个概念 ,在这2个概率里面 , 我一直有一个纠结 : 身份信息到底到谁手上?关于这个其实不需要太纠结 , IDP 里面是存在身份数据的 , 同样 SP 里面也可能存在身份数据 , 通常而言 , IDP是一个认证中心 , 它认证完成后生成的credential在绝大多数下只会包含一个Username , 然后SP会拿着username 去直接使用或者获取更详细的信息就像很多业务流程一样 , 认证后返回得仅仅是一个ID , 而具体得Userinfo 放在哪 , 按照业务去规划就行3.2 SAML 的详细交互前置条件 : 持有双方的 MetedataServer 持有 Client 的 Metedata , 其中有 Client 的公钥及重定向地址Client 持有 Server 的 Metedata , 其中有 Server 的公钥及重定向地址 , 及签名等信息3.3 SAML 请求格式参考 @ www.samltool.com/generic_sso…之前提到了 , Metedata 中包含了 Server / Client 的访问信息 , 例如 :<SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="http://127.0.0.1/samlServer/idp/profile/SAML2/POST/SLO"/>
<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="http://127.0.0.1/samlServer/idp/profile/SAML2/POST/SSO"/>
<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="http://127.0.0.1/samlServer/idp/profile/SAML2/Redirect/SSO"/>
<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="http://127.0.0.1/sso/idp/profile/SAML2/SOAP/ECP"/>
其中清楚的声明了访问的地址以及访问的方式 : 例如 urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST 断言说明通过 Post 形式的请求的访问地址 ,urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect 则说明了通过 Redirect (Get) 请求时的访问地址SAML AuthnRequest 请求格式<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="ONELOGIN_809707f0030a5d00620c9d9df97f627afe9dcc24" Version="2.0" ProviderName="SP test" IssueInstant="2014-07-16T23:52:45Z" Destination="http://idp.example.com/SSOService.php" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" AssertionConsumerServiceURL="http://sp.example.com/demo1/index.php?acs">
<saml:Issuer>http://sp.example.com/demo1/metadata.php</saml:Issuer>
<samlp:NameIDPolicy Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" AllowCreate="true"/>
<samlp:RequestedAuthnContext Comparison="exact">
<saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml:AuthnContextClassRef>
</samlp:RequestedAuthnContext>
</samlp:AuthnRequest>
ID : 新生成的标识号IssueInstant : 时间戳,表示生成时间AssertionConsumerServiceURL : 服务提供者的SAML URL接口,标识提供者在此发送身份验证令牌。Issuer : 服务提供者的EntityID(唯一标识符)InResponseTo : 此响应所属的SAML请求的IDRecipient : 服务提供者的EntityID(唯一标识符)当然实际请求是加密的 :http://127.0.0.1/samlServer/idp/profile/SAML2/Redirect/SSO?
SAMLRequest=bM441nuRIzAjKeMM8RhegMFjZ4L4xPBHhAfHYqgnYDQnSxC++Qn5IocWuzuBGz7JQmT9C57nxjxgbFIatiqUCQN17aYrLn/mWE09C5mJMYlcV68ibEkbR/JKUQ+2u/N+mSD4/C/QvFvuB6BcJaXaz0h7NwGhHROUte6MoGJKMPE=
&RelayState=http%3A%2F%2F127.0.0.1%2FclientSaml%2Fcallback
3.4 SAML 返回格式案例参考 @ www.samltool.com/generic_sso…3.5 Logout 案例注意 ,实际请求过程中都是加密 ,去这里可以自己解密看看 --> www.samltool.com/attributes.…Logout Request : www.samltool.com/generic_slo…
<samlp:LogoutRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="ONELOGIN_21df91a89767879fc0f7df6a1490c6000c81644d" Version="2.0" IssueInstant="2014-07-18T01:13:06Z" Destination="http://idp.example.com/SingleLogoutService.php">
<saml:Issuer>http://sp.example.com/demo1/metadata.php</saml:Issuer>
<saml:NameID SPNameQualifier="http://sp.example.com/demo1/metadata.php" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient">ONELOGIN_f92cc1834efc0f73e9c09f482fce80037a6251e7</saml:NameID>
</samlp:LogoutRequest>
Logout Response :<samlp:LogoutResponse xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="_6c3737282f007720e736f0f4028feed8cb9b40291c" Version="2.0" IssueInstant="2014-07-18T01:13:06Z" Destination="http://sp.example.com/demo1/index.php?acs" InResponseTo="ONELOGIN_21df91a89767879fc0f7df6a1490c6000c81644d">
<saml:Issuer>http://idp.example.com/metadata.php</saml:Issuer>
<samlp:Status>
<samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
</samlp:Status>
</samlp:LogoutResponse>
四 . 请求案例
//Step 1 : Client → Server :
http://127.0.0.1/samlServer/idp/profile/SAML2/Redirect/SSO?SAMLRequest=....&RelayState=http%3A%2F%2F127.0.0.1%2FclientSaml%2Fcallback
// Step 2 : Server doLogin
// Step 3 : Server → callBack → Client
http://127.0.0.1/2FclientSaml/callback?SAMLRequest=....&RelayState=http%3A%2F%2F127.0.0.1%2FclientSaml%2Fcallback
五 . SAML FAQ5.1 SAML 与 OAuth 的区别SAML使用XML传递消息,而OAuth使用JavaScript对象表示法OAuth提供了一种更简单的移动体验 (OAuth广泛使用API调用),而SAML则面向企业安全OAuth比SAML更适合访问范围。访问范围是一种实践,一旦身份验证,只允许资源/应用中最低限度的访问。就像之前说的 ,SAML 往往被一些大厂实践 , 是偏企业级的单点方式 , 一般人接触 , 第一感觉就是复杂 , 但是重量级及意味着更多的功能 ,更企业级的安全性 , 各有各的特色附录 . 补充概念Assertion :SAML 消息(XML 文档)的一部分,它提供关于断言主题的事实(通常是关于经过身份验证的用户)。断言可以包含有关身份验证、关联属性或授权决策的信息Artifact :标识符,该标识符可用于从标识或使用后台通道绑定的服务提供者检索完整的 SAML 消息Bnding :用于传递 SAML 消息的机制。绑定分为前端通道绑定和后端通道绑定,前者使用用户的 web 浏览器进行消息传递(例如 HTTP-POST 或 HTTP-Redirect) ,后者使身份提供者和服务提供者直接通信(例如在 Artifact 绑定中使用 SOAP 调用)Dscovery :用于确定应该使用哪个身份提供程序来验证当前与服务提供程序交互的用户Metadata : 描述一个或多个身份和服务提供者的文档。元数据通常包括实体标识符、公钥、端点 url、支持的绑定和配置文件以及其他功能或需求。身份和服务提供者之间的元数据交换通常是建立联合的第一步Profile :用于实现特定用例的协议、断言、绑定和处理指令的标准化组合,如单点登录、单点注销、发现、工件解析Protocol :为 SAML 消息定义格式(模式) ,用于实现特定功能,例如从 IDP 请求身份验证、执行单个注销或从 IDP 请求属性Identity provider (IDP) 身份提供者(IDP) :知道如何认证用户并使用联邦协议向服务提供者/中继方提供有关其身份的信息的实体Service provider (SP) 服务供应商 :您的应用程序与身份提供者通信,以获取与其交互的用户的信息。身份验证状态和用户属性等用户信息是以安全断言的形式提供的Single Sign-On (SSO) 单点登录(SSO) :允许访问多个网站的进程,而不需要重复提供身份验证所需的凭证。可以使用各种联邦协议(如 SAML、 WS-Federation、 OpenID 或 OAuth)来实现 SSO 用例。身份验证方式、用户属性、授权决策或安全令牌等信息通常作为单点登录的一部分提供给服务提供者Single Logout (SLO) 单一登出(SLO) :在使用单点登录访问的所有资源上处理终止身份验证会话。通常使用的技术包括将用户重定向到每个 SSO 参与者或发送注销 SOAP 消息
带鱼
Java 多线程 : 漫谈 Volatile
一 . volatile 基础> volatile 保证内存的可见性 并且 禁止指令重排
> volatile 提供 happens-before 的保证,确保一个线程的修改能对其他线程是可见的
> 保证线程可见性且提供了一定的有序性
// Java 中可以创建 volatile 类型数组,不过只是一个指向数组的引用,而不是整个数组。
二 . volatile 深入知识点> 读写主存中的数据没有 CPU 中执行指令的速度快 , 为了提高效率 , 使用 CPU 高速缓存来提高效率
> CPU 高速缓存 : CPU高速缓存为某个CPU独有,只与在该CPU运行的线程有关
// 原理 @ https://www.cnblogs.com/xrq730/p/7048693.html
Step 1 : 先说说 CPU 缓存 , CPU 有多级缓存 , 查询数据会由一级到三级中
一级缓存:简称L1 Cache,位于CPU内核的旁边,是与CPU结合最为紧密的CPU缓存
二级缓存:简称L2 Cache,分内部和外部两种芯片,内部芯片二级缓存运行速度与主频相同,外部芯片二级缓存运行速度则只有主频的一半
三级缓存:简称L3 Cache,部分高端CPU才有
// 缓存的加载次序
1 > 程序以及数据被加载到主内存
2 > 指令和数据被加载到CPU缓存
3 > CPU执行指令,把结果写到高速缓存
4 > 高速缓存中的数据写回主内存
// Step End : 因为不同的缓存 , 就出现了数据不一致 , 所以出现了规则
当一个CPU修改缓存中的字节时,服务器中其他CPU会被通知,它们的缓存将视为无效
三 . volatile 和 synchronized 的区别volatile 本质是在告诉 JVM 当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取。synchronized 则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。volatile 仅能使用在变量级别。synchronized 则可以使用在变量、方法、和类级别的。volatile 仅能实现变量的修改可见性,不能保证原子性。而synchronized 则可以保证变量的修改可见性和原子性。volatile 不会造成线程的阻塞。synchronized 可能会造成线程的阻塞。volatile 标记的变量不会被编译器优化。synchronized标记的变量可以被编译器优化。注意 :volatile 不能取代 synchronized四 . volatile 原理观察加入 volatile 关键字和没有加入 volatile 关键字时所生成的汇编代码发现,加入volatile 关键字时,会多出一个 lock 前缀指令。lock 前缀指令,其实就相当于一个内存屏障。内存屏障是一组处理指令,用来实现对内存操作的顺序限制。volatile 的底层就是通过内存屏障来实现的Step 1 : 写volatile的时候生成汇编码是 lock addl $0x0, (%rsp)Step 2 : 在写操作之前使用了lock前缀,锁住了总线和对应的地址,这样其他的写和读都要等待锁的释放。Step 3 : 当写完成后,释放锁,把缓存刷新到主内存。读volatile就很好理解了,不需要额外的汇编指令,CPU发现对应地址的缓存被锁了,等待锁的释放,缓存一致性协议会保证它读到最新的值。只需要对写volatile的使用用lock对总线加锁就行了,这样其他的读、写操作等待总线释放才能继续读。Lock会让其他CPU的缓存invalide,从内存重新加载数据。// volatile 的内存语义
- 当写一个 volatile 变量时,JMM 会把该线程对应的本地内存中的共享变量值,立即刷新到主内存中。
- 当读一个 volatile 变量时,JMM 会把该线程对应的本地内存设置为无效,直接从主内存中读取共享变量
> 所以 volatile 的写内存语义是直接刷新到主内存中,读的内存语义是直接从主内存中读取。
// volatile 的内存语义实现原理 : 为了实现 volatile 的内存语义,JMM 会限制重排序
1. 如果第一个操作为 volatile 读,则不管第二个操作是啥,都不能重排序。
?- 这个操作确保volatile 读之后的操作,不会被编译器重排序到 volatile 读之前;
2. 如果第二个操作为 volatile 写,则不管第一个操作是啥,都不能重排序。
?- 这个操作确保volatile 写之前的操作,不会被编译器重排序到 volatile 写之后;
3. 当第一个操作 volatile 写,第二个操作为 volatile 读时,不能重排序。
// volatile 的底层实现 : 内存屏障 , 有了内存屏障, 就可以避免重排序
-> 对于编译器来说,发现一个最优布置来最小化插入内存屏障的总数几乎是不可能的,所以,JMM 采用了保守策略
• 在每一个 volatile 写操作前面,插入一个 StoreStore 屏障
- StoreStore 屏障:保证在 volatile 写之前,其前面的所有普通写操作,都已经刷新到主内存中。
• 在每一个 volatile 写操作后面,插入一个 StoreLoad 屏障
- StoreLoad 屏障:避免 volatile 写,与后面可能有的 volatile 读 / 写操作重排序。
• 在每一个 volatile 读操作后面,插入一个 LoadLoad 屏障
- LoadLoad 屏障:禁止处理器把上面的 volatile读,与下面的普通读重排序。
• 在每一个 volatile 读操作后面,插入一个 LoadStore 屏障
- LoadStore 屏障:禁止处理器把上面的 volatile读,与下面的普通写重排序。
五. volatile 原子性> 我们需要区别 volatile 变量和 atomic 变量
// volatile 并不能很好的保证原子性
volatile 变量,可以确保先行关系,即写操作会发生在后续的读操作之前,但它并不能保证原子性。例如用 volatile 修饰 count 变量,那么 count++ 操作就不是原子性的。
AtomicInteger 类提供的 atomic 方法,可以让这种操作具有原子性。例如 #getAndIncrement() 方法,会原子性的进行增量操作把当前值加一,其它数据类型和引用变量也可以进行相似操作。
六 . volatile 源码TODO : 涉及到源码 ,先留坑 , 具体可以先看 @ https://www.cnblogs.com/xrq730/p/7048693.html
// 主要节点 :
0x0000000002931351: lock add dword ptr [rsp],0h ;
*putstatic instance;
- org.xrq.test.design.singleton.LazySingleton::getInstance@13 (line 14)
> 将双字节的栈指针寄存器+0 , 保证volatile关键字的内存可见性
// 基本概念一 : LOCK# 的作用
- 锁总线
- 其它CPU对内存的读写请求都会被阻塞,直到锁释放
- 不过实际后来的处理器都采用锁缓存替代锁总线
- 因为锁总线的开销比较大,锁总线期间其他CPU没法访问内存
- lock后的写操作会回写已修改的数据,同时让其它CPU相关缓存行失效,从而重新从主存中加载最新的数据
- 不是内存屏障却能完成类似内存屏障的功能,阻止屏障两遍的指令重排序
// 基本概念二 : 缓存行
- 缓存是分段(line)的,一个段对应一块存储空间 , 即缓存行
- CPU看到一条读取内存的指令时,它会把内存地址传递给一级数据缓存
- 一级数据缓存检测是否由缓存段 , 没有加载这缓存段
// 原因 : volatile 基于 缓存一致性来实现
Step1 : 因为LOCK 效率问题 ,所以基于缓存一致性来处理
Step2 : 缓存一致性作用时 使用多组缓存,但是它们的行为看起来只有一组缓存那样
Step3 : 常见的协议是 snooping 和 MESI
Step4 : snooping 的作用是 : 仲裁所有的内存访问操作
七 . volatile 实测// 测试原子性 , 结果 ThreadC : ------> count :9823 <-------
// Thread 中操作
public static void addCount() {
for (int i = 0; i < 100; i++) {
count++;
}
logger.info("------> count :{} <-------", count);
}
ThreadC[] threadCS = new ThreadC[100];
for (int i = 0; i < 100; i++) {
threadCS[i] = new ThreadC();
}
for (int i = 0; i < 100; i++) {
threadCS[i].start();
}
// 添加 synchronized 后 -- > ThreadD count :10000 <-------
synchronized public static void addCount()
带鱼
盘点认证协议 : 普及篇之ADFS , WS-Federation
纯约束型协议 : OAuth , SAML , OIDC , CAS服务器类协议 : RADIUS , Kerberos , ADFS认证方式类 : OTP , 生物认证 (人脸 , 声纹 , 指纹)认证服务器(附带) : AD , LDAP , ADFS这一篇的深度不会太深 , 主要是对概念的普及 !一 . 前言之所以这一篇把 ADFS 放上来主要是因为它和 SAML 协议比较接近, 所以通过它来完善一下 SAML 的认证体系 .先说一下 ADFS 和 WS-Federation 是什么 .WIF : .NET Framework 类WIF (Windows Identity Foundation) : 这个.NET 库用于在NET应用程序和依赖方中驱动基于声明的身份验证。它还可以用作WS-Trust客户端和构建自定义STSWS-FederationWS-Federation(简称: WS-Fed ) 属于Web Services Security(简称: WS-Security、WSS: 针对web-service安全性方面扩展的协议标准集合) 的一部分,是由OASIS(www.oasis-open.org)发布的标准协议作用 : WS-Federation的基本目标就是为了能够简化联合(所谓联合: Federation,是指一组相互之间存在安全共享资源关系的领域集合)服务的开发。流程 : 由依赖方和STS用于协商安全令牌的协议。应用程序使用WS联合从STS请求安全令牌,STS(大多数时候)使用WS联合协议将SAML安全令牌返回给应用程序。这通常是通过HTTP(获取、post和重定向)实现的。ADFS : Windows 基于 AD 引用实现了 Federation 的服务AD FS,Active Directory Federation Services : Active Directory联合身份验证服务 , AD FS 使用基于Claims的访问控制验证模型来实现联合认证。它提供 Web 单一登录技术,这样只要在会话的有效期内,就可对一次性的对用户所访问的多个Web应用程序进行验证。由Microsoft生产并构建在Windows Identity Foundation (WIF)上的现成安全令牌服务(STS) ,依赖AD进行身份验证。可以在活动(SOAP web服务)或被动(web站点)场景中使用,并支持SAML令牌、WS-Federation、WS-Trust和SAML- protocol总结 : 这就是说 ADFS 是 Federation 协议的实现 , AD 是身份管理 , ADFS 是一个服务 , 并且实现了很多协议.ADFS 1.0 - Windows Server 2003 R2 (additional download)ADFS 1.1 - Windows Server 2008 and Windows Server 2008 R2.ADFS 2.0 - Windows Server 2008 and Windows Server 2008 R2 (download from Microsoft.com)ADFS 2.1 - Windows Server 2012.ADFS 3.0 - Windows Server 2012 R2.STS : 身份认证成员 , 安全令牌颁发者STS是位于依赖方应用程序和用户之间的代理。STS是安全令牌的颁发者 , STS 有2种 角色 ,身份提供者(IdP) : 当它们验证用户时联合提供者(FP) : 当它们位于信任链的中间,充当其他IdP的“依赖方”时我们来看一张 ADFS 的概念图 :二 . 深入 WS-Federation2.1 WS-Federation 成员解析WS-Trust WS-Trust定义了Security Token Service (STS)协议用于请求/发布安全令牌; 通过定义服务模型,安全令牌服务(STS)以及用于请求/发布这些安全令牌的协议(由WS-Security使用并由WS-SecurityPolicy描述)来提供联合的基础. STS发出了一个由资源提供者信任的令牌,因此请求者必须通过STS验证自己才能获得令牌,然后向资源提供者请求资源以及声明/令牌.2.2 Metadata Model元数据模型发起点: 请求者希望最终通信的服务。 给定目标服务的元数据端点引用(metadpoint reference,MEPR)允许请求者获取关于服务的所有需求元数据(例如联合元数据、通信策略、 WSDL 等)。作用: 联合元数据描述有关如何在联合中使用服务以及服务如何参与联合的设置和信息。身份: 联合元数据只是服务总体元数据的一个组成部分, 还有一个通信策略,描述发送到服务的 web 服务消息的需求,以及服务、端点和消息的组织结构的 WSDL 描述。范围: : 联合元数据(如通信策略)的范围可以限定到服务、端点,甚至是消息。 此外,所描述的信息类型可能根据联合中的服务角色而有所不同(例如,目标服务、安全令牌服务...)。三 . 深入 ADFS3.1 ADFS 流程为了更清楚 , 我这里画了一个图来说明3.2 核心问题身份令牌 : 有认证就会有令牌 ADFS 种会传递 security token 来表明用户 , 安全令牌包含关于用户的声明,如用户名、组成员、UPN、电子邮件地址、管理员详细信息和电话号码 . 应用程序会自行通过令牌判断该如何处理人员 , 所以 :ADFS 做身份验证决策 , 应用做权限管理以及行为控制双方之间的联合信任通过证书进行管理 , 而AD FS服务器可以自签名安全令牌签名和加密证书IDP 和 SP 这涉及到 ADFS 是一个认证方还是非认证方 , 答案是 : ADFS 中都可以配置 , ADFS 即可以让应用认证 , 也可以去 SSO 中完成认证 , 只不过作为 IDP 时 , AD DS认证成功后,AD FS的STS (security token service)组件会发出安全令牌。怎么理解联合就像一个 SSO 集群 , 可以去对方的地方认证 , 也可以让对方到你这里认证 , 只是通过一定的协议颁发令牌即可ADFS 认证的方式 :表单验证: 这种身份验证方法适用于发布在公司网络之外的资源,并且客户端可以通过internet访问这些资源。Windows集成验证: 这是默认的认证方法,适用于在企业网络中发布的资源,并且只能从内网资源访问。3.3 其他知识点Account Store/Attribute store : Active Directory联合身份验证服务使用术语“属性存储”来指组织用来存储其用户帐户及其关联的属性值的目录或数据库claims provider (CP): 声明提供方,是联合身份验证服务,负责收集和验证用户,构建声明,并将声明打包为安全令牌(Security Token),ADFS本身就是典型的CPclaims provider trust (CPT) : 受ADFS信赖的其他CP,根据Claims Rules向ADFS发送声明,受信任的CP用户可以访问ADFS配置(relying party trust)中的relying partyrelying party(RP) : 信赖方,即声明的消费方,需要依赖ADFS进行用户验证的应用程序,信赖方向Claim Provider请求并接收claims provider传过来的Claims(Claim需要根据Claim Rule进行转换/映射)relying party trust : 可以理解为ADFS的“白名单”,受信任的RP才能向接收到ADFS发送的Token/Claimsclaims rule : 声明的转换规则(通过 Claim Engine执行),规则即:如果服务器收到声明A,则颁发声明B,ADFS向外(relying party应用)发出的声明受claim rule约束,需要在claim rules事先约定(需要进行转换/映射)声明引擎(Claims Engine) : 联合身份验证服务中的唯一实体,负责在您配置的所有联合信任关系中运行每个规则集(Claims Rule),并将输出结果移交给声明管道(Claims Pipeline)信任的传递 : 为安全起见,ADFS服务器部署在内网,不直接对外网提供服务,而是通过部署在外围网络(屏蔽子网)的代理服务器进行转发信任(trust ) ,信任的方向(Trust Direction)是One-way trust 或者Two-way trust , Trust Transitivity(信任的传递)是可传递信任四 . 知识点4.1 WS联合会 和 SAML 的区别目标WS : 浏览器重定向(URL中的消息)-浏览器POST(HTML格式的消息)-SOAP(通过HTTP)SAML : 浏览器重定向(URL中的消息)-浏览器POST(HTML格式的消息)-工件(引用断言+ SOAP调用)-SOAP(通过HTTP)- 反向SOAP(通过HTTP)支持安全令牌WS : SAML断言,X509证书,kerberosSAML : SAML断言-其他任何令牌类型(通过SubjectConfirmation 元素嵌入在SAML断言中)服务依赖关系WS : WS-Trust ,WS-Policy,WS-SecurityPolicy。WS-Eventing (订阅单一注销消息)。WS-Transfer和WS-ResourceTransfeSAML : 无元数据WS :SAML :登出WS : 可以由SP或(主要)STS发起,后者将向所有RP发送注销消息SAML : 一样断言WS : 基于参考令牌的使用(即,可以进行WS-Transfer GET检索实际令牌的EPR )SAML :认证等级WS :-WS-Trust定义参数(AuthenticationType )。WS-Fed指定预定义的值(例如Ssl,SslSndKey,智能卡)。SAML : SAML 2.0提供了更广泛和可扩展的认证上下文隐私WS :SAML :联盟WS :SAML :总结 :感觉还是没讲清楚 , 这方面能查阅的资料比较少 ,而英文看到的又有误差 , 很多都没有解释清楚 ,ADFS 是一个很重量级的认证方式 ,通常在一些大型企业中可以看到 , 说句心里话 , 碰到 Windows 的东西就怂了 , 太难查问题了 , 文档是多 ,但是一个概念居然有4,5个描述他的文档 , 还每个不同 , 就是折磨了
带鱼
Java 多线程 : 细说线程状态
一. 线程等待// 等待具体时间
> sleep(time)
// 该方式不释放锁 ,低优先级有机会执行
// sleep 后转入 阻塞(blocked)
> wait(time)
> join(time)
> LockSupport.parkNanos()
> LockSupport.parkUnit()
> yield
// 该方式同样不会释放锁 ,同优先级及高优先级执行
// 执行后转入ready
// 仅 进入等待
> wait()
> join()
> LockSuppot.park()
二. 线程通知// 对于设定具体等待时间的 timeout 后自动转入就绪
// 其他等待
> notify()
> notifyAll()
> 不同线程之间采用字符串作为监视器锁,会唤醒别的线程
> 不同线程之间的信号没有共享,等待线程被唤醒后继续进入wait状态:
> 下图为不同线程的等待与唤醒
> 执行wait () 时释放锁 , 否则等待的线程如果继续持有锁 , 其他线程就没办法获取锁 , 会陷入死锁
// Wait - Notify 深入知识点
// 一 : Wait 等待知识点
- 当前线程必须拥有这个对象的监视器
// 二 : Wait 等待后
- 执行等待后 , 当前线程将自己放在该对象的等待集中,然后放弃该对象上的所有同步声明
- 如果当前线程在等待之前或等待期间被任何线程中断 , 会抛出 InterruptedException 异常
// 三 : 唤醒时注意点
- Notify 唤醒一个正在等待这个对象的线程监控 (monitor)
- 执行 wait 时会释放锁 , 同时执行 notify 后也会释放锁 (如下图)
- notify 会任意选择一个等待对象来提醒
// 四 : 唤醒后知识点
- 线程唤醒后 , 仍然要等待该对象的锁被释放
- 线程唤醒后 , 将会与任何竞争该锁的对象公平竞争
// 假醒 :
线程也可以在不被通知、中断或超时的情况下被唤醒,这就是所谓的伪唤醒。
三. 线程中断> interrupt()
// 方法,用于中断线程。调用该方法的线程的状态为将被置为”中断”状态。
> interrupted ()
// 查询当前线程的中断状态,并且清除原状态。
> isInterrupted ()
// 查询指定线程的中断状态,不会清除原状态+
// interrupt() 方法干了什么 ?
public void interrupt() {
if (this != Thread.currentThread())
checkAccess();
synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
interrupt0(); // Just to set the interrupt flag
b.interrupt(this);
return;
}
}
interrupt0();
}
// 1 checkAccess() : 其中涉及到 SecurityManager , 所以我们先看看这个类干什么的
- SecurityManager security = System.getSecurityManager();
- security.checkAccess(this);
C- SecurityManager :
?- 这是 Java.lang 底下的一个类
四. 线程死锁死锁简介 : 当多个进程竞争资源时互相等待对方的资源死锁的条件 :互斥条件 : 一个资源每次只能被一个进程使用,即在一段时间内某 资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。请求与保持条件 :进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源 已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。不可剥夺条件 : 进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能 由获得该资源的进程自己来释放(只能是主动释放)。循环等待条件 : 若干进程间形成首尾相接循环等待资源的关系// 资源的分类
- 可抢占资源 : 可抢占资源指某进程在获得这类资源后,该资源可以再被其他进程或系统抢占 , 例如 CPU 资源
- 不可抢占资源
// 死锁的常见原因 :
- 竞争不可抢占资源引起死锁 (共享文件)
- 竞争可消耗资源引起死 (程通信时)
- 进程推进顺序不当引起死锁
// 死锁的预防
- 通过系统中尽量破坏死锁的条件 , 当四大条件有一个不符合时 , 死锁就不会发生
- 通过加锁顺序处理(线程按照一定的顺序加锁)
- 加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)
- 死锁检测
// 死锁的解除
- 资源剥离 , 挂起死锁进程且强制对应资源 , 分配进程
- 撤销进程
- 回退进程
五. 线程热锁> 热锁不算一个物理概念 , 它表示线程在频繁的竞争资源并且资源在频繁的切换\
- 循环等待 :
六. 线程的状态及转换> 线程有以下状态
- NEW : 尚未启动的线程的hread状态
- RUNNABLE : 可运行 , 从虚拟机的角度 , 已经执行 ,但是可能正在等待资源
- BLOCKED : 阻塞 , 此时等待 monitor锁 , 以读取 synchronized 代码
- WAITING : 等待状态 , 处于等待状态的线程正在等待另一个线程执行特定操作
- wait()
- join()
- LockSupport#park()
- TIMED_WAITING : 指定等待时间的等待
- Thread.sleep
- wait(long)
- join(long)
- LockSupport#parkNanos
- LockSupport#parkUntil
- TERMINATED : 终止线程
// 线程间状态改变的方式
• 还没起床:sleeping 。
• 起床收拾好了,随时可以坐地铁出发:Runnable 。
• 等地铁来:Waiting 。
• 地铁来了,但要排队上地铁:I/O 阻塞 。
• 上了地铁,发现暂时没座位:synchronized 阻塞。
• 地铁上找到座位:Running 。
• 到达目的地:Dead 。
七. 状态转换的原理7.1 wait 与 notify 原理// 节点一 : 你是否发现 , wait 和 notify 是 object 的方法
点开 wait 和 notify 方法就能发现 , 这两个方法是基于 Object 对象的 , 所以我们要理解 ,通知不是通知的线程 ,而是通知的对象
这也就是为什么 , 不要用常量作为通知对象
// 节点二 : java.lang.IllegalMonitorStateException
当我们 wait/notify 时 , 如果未获取到对应对象的 Monitor , 实际上我们会抛出 IllegalMonitorStateException
所以你先要获得监视器 , 有三种方式 :
- 通过执行该对象的同步实例方法。
- 通过执行在对象上同步语句体。
- 对于类型为Class的对象,可以执行该类的同步静态方法。
// 节点三 : 如何进行转换 ?
Step 1 : 首先看 Object 对象 Native 方法 , bative src 中搜索名字即可
static JNINativeMethod methods[] = {
{"hashCode", "()I", (void *)&JVM_IHashCode},
{"wait", "(J)V", (void *)&JVM_MonitorWait},
{"notify", "()V", (void *)&JVM_MonitorNotify},
{"notifyAll", "()V", (void *)&JVM_MonitorNotifyAll},
{"clone", "()Ljava/lang/Object;", (void *)&JVM_Clone},
};
//可以看到这里分别调用了 JVM_MonitorWait , JVM_MonitorNotify , JVM_MonitorNotifyAll ,
//从名字就能看到 , 这里是和Monitor 有关的
Step 2 : 进入全路径了解 : \openjdk\hotspot\src\share\vm\prims
JVM_ENTRY(void, JVM_MonitorWait(JNIEnv* env, jobject handle, jlong ms))
JVMWrapper("JVM_MonitorWait");
Handle obj(THREAD, JNIHandles::resolve_non_null(handle));
JavaThreadInObjectWaitState jtiows(thread, ms != 0);
if (JvmtiExport::should_post_monitor_wait()) {
JvmtiExport::post_monitor_wait((JavaThread *)THREAD, (oop)obj(), ms);
// 当前线程已经拥有监视器,并且还没有添加到等待队列中,因此当前线程不能成为后续线程
}
ObjectSynchronizer::wait(obj, ms, CHECK);
JVM_END
Step 3 : ObjectSynchronizer::wait(obj, ms, CHECK);
// TODO : 看不懂了呀....先留个坑
// 总得来说就是 ObjectMonitor通过一个双向链表来保存等待该锁的线程
Step End : link by @ https://www.jianshu.com/p/a604f1a9f875
void ObjectMonitor::wait(jlong millis, bool interruptible, TRAPS) {
...................
// 创建ObjectWaiter,添加到_WaitSet队列中
ObjectWaiter node(Self);
node.TState = ObjectWaiter::TS_WAIT ;
Self->_ParkEvent->reset() ;
OrderAccess::fence(); // ST into Event; membar ; LD interrupted-flag
//WaitSetLock保护等待队列。通常只锁的拥有着才能访问等待队列
Thread::SpinAcquire (&_WaitSetLock, "WaitSet - add") ;
//加入等待队列,等待队列是循环双链表
AddWaiter (&node) ;
//使用的是一个简单的自旋锁
Thread::SpinRelease (&_WaitSetLock) ;
.....................
}
7.2 Thread run// 节点一 : 区别 run 和 start
run 是通过方法栈直接调用对象的方法 , 而 Start 才是开启线程 , 这一点我们可以从源码中发现 :
- start 方法是同步的
- start0 是一个 native 方法
- group 是线程组 (ThreadGroup) , 线程可以访问关于它自己线程组的信息
?- 线程组主要是为了管理线程 , 将一个大线程分成多个小线程 (盲猜 fork 用到了 , 后面验证一下)
?- 线程组也可以通过关闭组来关闭所有的线程
public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}
// Step 1 : Thread.c 结构 -> openjdk\src\native\java\lang
static JNINativeMethod methods[] = {
{"start0", "()V", (void *)&JVM_StartThread},
{"stop0", "(" OBJ ")V", (void *)&JVM_StopThread},
{"isAlive", "()Z", (void *)&JVM_IsThreadAlive},
{"suspend0", "()V", (void *)&JVM_SuspendThread},
{"resume0", "()V", (void *)&JVM_ResumeThread},
{"setPriority0", "(I)V", (void *)&JVM_SetThreadPriority},
{"yield", "()V", (void *)&JVM_Yield},
{"sleep", "(J)V", (void *)&JVM_Sleep},
{"currentThread", "()" THD, (void *)&JVM_CurrentThread},
{"countStackFrames", "()I", (void *)&JVM_CountStackFrames},
{"interrupt0", "()V", (void *)&JVM_Interrupt},
{"isInterrupted", "(Z)Z", (void *)&JVM_IsInterrupted},
{"holdsLock", "(" OBJ ")Z", (void *)&JVM_HoldsLock},
{"getThreads", "()[" THD, (void *)&JVM_GetAllThreads},
{"dumpThreads", "([" THD ")[[" STE, (void *)&JVM_DumpThreads},
{"setNativeName", "(" STR ")V", (void *)&JVM_SetNativeThreadName},
};
// \openjdk\hotspot\src\share\vm\prims\jvm.cpp
// Step 2 : JVM_StartThread , 翻译了一下 , 大概可以看到那一句 native_thread = new JavaThread(&thread_entry, sz);
// 以及最后的 Thread::start(native_thread);
JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread))
JVMWrapper("JVM_StartThread");
JavaThread *native_thread = NULL;
//由于排序问题,在抛出异常时不能持有Threads_lock。示例:在构造异常时,可能需要获取heap_lock。
bool throw_illegal_thread_state = false;
// 我们必须释放Threads_lock才能在Thread::start中post jvmti事件
{
// 确保c++ Thread和OSThread结构体在操作之前没有被释放
MutexLocker mu(Threads_lock);
//从JDK 5开始threadStatus用于防止重新启动一个已经启动的线程,所以我们通常会发现javthread是null。然而,对于JNI附加的线程,在创建的线程对象(带有javthread集)和更新其线程状态之间有一个小窗口,因此我们必须检查这一点
if (java_lang_Thread::thread(JNIHandles::resolve_non_null(jthread)) != NULL) {
throw_illegal_thread_state = true;
} else {
//我们也可以检查stillborn标志来查看这个线程是否已经停止,但是由于历史原因,我们让线程在开始运行时检测它自己
jlong size =
java_lang_Thread::stackSize(JNIHandles::resolve_non_null(jthread));
//分配c++线程结构并创建原生线程。
//从java检索到的堆栈大小是有符号的,但是构造函数接受size_t(无符号类型),因此避免传递负值,因为这会导致非常大的堆栈。
size_t sz = size > 0 ? (size_t) size : 0;
native_thread = new JavaThread(&thread_entry, sz);
// 此时可能由于缺少内存而没有为javthread创建osthread。检查这种情况并在必要时抛出异常。
// 最终,我们可能想要更改这一点,以便只在线程成功创建时才获取锁——然后我们也可以执行这个检查并在javthread构造函数中抛出异常。
if (native_thread->osthread() != NULL) {
// 注意:当前线程没有在“prepare”中使用。
native_thread->prepare(jthread);
}
}
}
if (throw_illegal_thread_state) {
THROW(vmSymbols::java_lang_IllegalThreadStateException());
}
assert(native_thread != NULL, "Starting null thread?");
if (native_thread->osthread() == NULL) {
// No one should hold a reference to the 'native_thread'.
delete native_thread;
if (JvmtiExport::should_post_resource_exhausted()) {
JvmtiExport::post_resource_exhausted(
JVMTI_RESOURCE_EXHAUSTED_OOM_ERROR | JVMTI_RESOURCE_EXHAUSTED_THREADS,
"unable to create new native thread");
}
THROW_MSG(vmSymbols::java_lang_OutOfMemoryError(),
"unable to create new native thread");
}
Thread::start(native_thread);
JVM_END
7.3 Thread yieldC- Thread
M- yield() : 可以看到 , yield 同样是一个 native 方法
// Step 1: \openjdk\hotspot\src\share\vm\prims\jvm.cpp
// 主要是2句 : os::sleep(thread, MinSleepInterval, false);
// os::yield();
JVM_ENTRY(void, JVM_Yield(JNIEnv *env, jclass threadClass))
JVMWrapper("JVM_Yield");
if (os::dont_yield()) return;
#ifndef USDT2
HS_DTRACE_PROBE0(hotspot, thread__yield);
#else /* USDT2 */
HOTSPOT_THREAD_YIELD();
#endif /* USDT2 */
// 当ConvertYieldToSleep关闭(默认)时,这与传统VM的yield使用相匹配。对于类似的线程行为至关重要
if (ConvertYieldToSleep) {
os::sleep(thread, MinSleepInterval, false);
} else {
os::yield();
}
JVM_END
// TODO : 主要的其实还没有看懂 , 毕竟 C基础有限
带鱼
从入门到精通:Java线程池原理 3W 字长文全面指南
前言大家好,我是 Lorin。随着计算机行业的飞速发展,摩尔定律逐渐失效,多核CPU成为主流。使用多线程并行计算逐渐成为开发人员提升服务器性能的基本武器。J.U.C提供的线程池:ThreadPoolExecutor类,帮助开发人员管理线程并方便地执行并行任务。本文从线程池概念和用途开始介绍,然后接着结合线程池的源码,领略线程池的设计思路,最后结合实践介绍线程使用的一些常见案例以及线程池参数配置难题引出动态线程池。无论您是一个经验丰富的Java开发者,还是刚刚起步的新手,我相信您都将从本文中获得有价值的信息和见解。线程池是什么线程池是一种并发编程的工具,它管理和复用线程,以便更有效地执行多个任务。线程池维护一组可用线程,任务到达时将其分配给这些线程,执行完成后线程可以被重新用于执行其他任务。线程池的主要目标是减少线程的创建和销毁开销,提高系统的性能和稳定性。线程池解决的问题和带来的好处降低资源消耗,提高响应速度通过池化技术重复利用已创建的线程,降低线程创建和销毁造成的损耗。任务到达时,无需等待线程创建即可立即执行,提高响应速度。提高线程的可管理性,保证系统的稳定性线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。提供更多更强大的功能线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如定时线程池ScheduledThreadPoolExecutor,允许任务延期执行或定期执行。核心设计与实现线程池是一种通过“池化”思想,帮助我们管理线程而获取并发性的工具,在Java中的体现是ThreadPoolExecutor类。下面我们来看一下它的核心设计与具体实现。源码JDK版本:1.8ThreadPoolExecutor UML类图ThreadPoolExecutor实现的顶层接口是Executor,顶层接口Executor提供了一种思想:将任务提交和任务执行进行解耦。用户无需关注如何创建线程,如何调度线程来执行任务,用户只需提供Runnable对象,将任务的运行逻辑提交到执行器(Executor)中,由Executor框架完成线程的调配和任务的执行部分。ExecutorService接口增加了一些能力:(1)扩充执行任务的能力,补充可以为一个或一批异步任务生成Future的方法;(2)提供了管控线程池的方法,比如停止线程池的运行。AbstractExecutorService则是上层的抽象类,将执行任务的流程串联了起来,保证下层的实现只需关注一个执行任务的方法即可。最下层的实现类ThreadPoolExecutor实现最复杂的运行部分,ThreadPoolExecutor将会一方面维护自身的生命周期,另一方面同时管理线程和任务,使两者良好的结合从而执行并行任务。ThreadPoolExecutor 状态线程池的状态并不是由用户显示维护,而是伴随线程池运行由内部进行维护,线程池内部使用一个变量维护两个值:运行状态(runState)和线程数量 (workerCount)。在具体实现中,线程池将运行状态(runState)、线程数量 (workerCount)两个关键参数的维护放在了一起(使用原子操作保证读取和修改线程安全,而不使用额外的锁),如下代码所示: private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
// runState is stored in the high-order bits
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;
运行状态从上面我们可以知道,线程池共有 5 种状态:运行状态说明RUNNING能接受新任务提交,也能处理阻塞队列中的任务SHUTDOWN关闭状态,无法接受新任务,但可以处理阻塞队列中的任务STOP无法接受新任务且不会处理阻塞队列的任务,同时中断所有线程TIDYING所有任务已取消,线程数量 (workerCount) = 0TERMINATEDterminated() 执行完成后进入 TERMINATED 状态运行状态转换ThreadPoolExecutor 运行机制我们先看一下 ThreadPoolExecutor 的整体运行机制,可以发现整体分为任务调度和 Worker 线程管理两大部分:任务调度从上面可以看出,当我们提交一个可运行任务时,我们会进行任务调度,任务调度逻辑如下:1、当线程数小于核心线程数,创建新线程执行任务
2、当线程数大于等于核心线程数,尝试将任务放入阻塞队列
3、若放入阻塞队列失败
判断线程数是否小于最大线程数,若是,创建一个线程执行该任务,否则拒绝任务
4、若放入阻塞队列成功,结束
源码分析 public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
/*
* Proceed in 3 steps:
*
* 1. If fewer than corePoolSize threads are running, try to
* start a new thread with the given command as its first
* task. The call to addWorker atomically checks runState and
* workerCount, and so prevents false alarms that would add
* threads when it shouldn't, by returning false.
*
* 2. If a task can be successfully queued, then we still need
* to double-check whether we should have added a thread
* (because existing ones died since last checking) or that
* the pool shut down since entry into this method. So we
* recheck state and if necessary roll back the enqueuing if
* stopped, or start a new thread if there are none.
*
* 3. If we cannot queue task, then we try to add a new
* thread. If it fails, we know we are shut down or saturated
* and so reject the task.
*/
int c = ctl.get();
// 当前线程小于核心线程 创建新核心线程执行该任务
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
// 线程数大于等于核心线程 尝试放入阻塞队列
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
// 双重校验 the pool shut down since entry into this method
if (! isRunning(recheck) && remove(command))
reject(command);
// 核心线程数为0 非核心线程刚好回收若不创建线程任务无法执行
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
// 线程数大于等于核心线程 放入队列失败 尝试创建非核心线程
else if (!addWorker(command, false))
reject(command);
}
任务缓冲线程池的本质是使用解耦的思想将任务和线程管理分开,从而使用生产者-消费者的模式来对任务进行调度分配。线程池通过阻塞队列实现生产者-消费者,在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。同时支持不同的阻塞队列来实现不同存取策略:任务拒绝策略任务拒绝模块是线程池的保护部分,线程池有一个最大的容量,当线程池的任务缓存队列已满,并且线程池中的线程数目达到maximumPoolSize时,就需要拒绝掉该任务,采取任务拒绝策略,保护线程池。JUC 提供了四种拒绝策略,我们也可以实现 RejectedExecutionHandler 接口实现自己想要的拒绝策略:/**
* A handler for tasks that cannot be executed by a {@link ThreadPoolExecutor}.
*
* @since 1.5
* @author Doug Lea
*/
public interface RejectedExecutionHandler {
/**
* Method that may be invoked by a {@link ThreadPoolExecutor} when
* {@link ThreadPoolExecutor#execute execute} cannot accept a
* task. This may occur when no more threads or queue slots are
* available because their bounds would be exceeded, or upon
* shutdown of the Executor.
*
* <p>In the absence of other alternatives, the method may throw
* an unchecked {@link RejectedExecutionException}, which will be
* propagated to the caller of {@code execute}.
*
* @param r the runnable task requested to be executed
* @param executor the executor attempting to execute this task
* @throws RejectedExecutionException if there is no remedy
*/
void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}
Worker 线程管理Worker 线程管理是线程池的第二个核心部分,主要负责 Worker 创建、Worker 执行(获取任务、执行任务)、Worker 回收三部分主要功能。先看一下 Worker 的结构: private final class Worker
extends AbstractQueuedSynchronizer
implements Runnable
{
/**
* This class will never be serialized, but we provide a
* serialVersionUID to suppress a javac warning.
*/
private static final long serialVersionUID = 6138294804551838833L;
/** Thread this worker is running in. Null if factory fails. */
final Thread thread;
/** Initial task to run. Possibly null. */
Runnable firstTask;
/** Per-thread task counter */
volatile long completedTasks;
/**
* Creates with given first task and thread from ThreadFactory.
* @param firstTask the first task (null if none)
*/
Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
/** Delegates main run loop to outer runWorker */
public void run() {
// Worker 执行逻辑:任务获取、执行
runWorker(this);
}
添加 Worker线程池任务调度和线程池自适应状态调整过程中会使用 addWorker() 方法尝试添加 Worker。源码解析 // java.util.concurrent.ThreadPoolExecutor#addWorker
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// Check if queue empty only if necessary.
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
for (;;) {
int wc = workerCountOf(c);
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
if (compareAndIncrementWorkerCount(c))
// WorkerCount CAS +1 成功 跳出循环进行 Worker 创建
break retry;
c = ctl.get(); // Re-read ctl
if (runStateOf(c) != rs)
// runState 发生变更 跳出循环重新判断状态进入
continue retry;
// else CAS failed due to workerCount change;
// 重新获取 workerCount 循环
retry inner loop
}
}
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
// 创建 Worker
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// Recheck while holding lock.
// Back out on ThreadFactory failure or if
// shut down before lock acquired.
int rs = runStateOf(ctl.get());
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
workers.add(w);
int s = workers.size();
if (s > largestPoolSize)
// 更新 largestPoolSize
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock();
}
if (workerAdded) {
// workerAdded success & start
t.start();
workerStarted = true;
}
}
} finally {
if (! workerStarted)
// 线程池关闭 addWorkerFailed 处理逻辑
addWorkerFailed(w);
}
return workerStarted;
}
/**
* Rolls back the worker thread creation.
* - removes worker from workers, if present
* - decrements worker count
* - rechecks for termination, in case the existence of this
* worker was holding up termination
*/
private void addWorkerFailed(Worker w) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
if (w != null)
workers.remove(w);
decrementWorkerCount();
tryTerminate();
} finally {
mainLock.unlock();
}
}
Woker 执行(获取任务、执行任务)通过自循环的方式不断获取阻塞队列的任务并执行任务。当获取任务失败会进入 Woker 回收流程。源码分析 // java.util.concurrent.ThreadPoolExecutor#runWorker
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
// 是否存在 firstTask 需要执行
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
// worker died due to user exception
boolean completedAbruptly = true;
try {
// 存在 firstTask 或 从阻塞队列获取成功则继续执行
while (task != null || (task = getTask()) != null) {
w.lock();
// If pool is stopping, ensure thread is interrupted;
// if not, ensure thread is not interrupted. This
// requires a recheck in second case to deal with
// shutdownNow race while clearing interrupt
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
beforeExecute(wt, task);
Throwable thrown = null;
try {
task.run();
} catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
afterExecute(task, thrown);
}
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
// 非用户异常退出: 阻塞队列为空退出
completedAbruptly = false;
} finally {
// worker 退出处理逻辑
processWorkerExit(w, completedAbruptly);
}
}
private void processWorkerExit(Worker w, boolean completedAbruptly) {
if (completedAbruptly) // If abrupt, then workerCount wasn't adjusted
decrementWorkerCount();
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
completedTaskCount += w.completedTasks;
workers.remove(w);
} finally {
mainLock.unlock();
}
// tryTerminate 尝试调整线程池状态
tryTerminate();
int c = ctl.get();
// 如果线程池小于 STOP 状态
// 说明还可能继续执行任务
if (runStateLessThan(c, STOP)) {
// 任务阻塞队列为空退出
if (!completedAbruptly) {
// 核心线程是否需要过期回收 默认不回收
int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
// 若核心线程会被回收 & 任务阻塞队列不为空 则至少保留一个线程
if (min == 0 && ! workQueue.isEmpty())
min = 1;
// 满足核心线程数要求则直接退出
if (workerCountOf(c) >= min)
return; // replacement not needed
}
// 用户异常退出或不满足核心线程数要求创建一个新 Worker
addWorker(null, false);
}
}
// 从阻塞队列获取任务
// getTask 一般会正常获取任务返回,当返回null会导致当前 Worker 销毁退出,有以下几种场景会返回null:
// 1、线程池处于STOP状态。这种情况下所有线程都应该被立即回收销毁;
// 2、线程池处于SHUTDOWN状态,且阻塞队列为空。不会有任务再提交到阻塞队列中。
// 3、当前线程数大于最大线程数(最大线程数调整)
// 4、线程空闲超时被回收:线程数大于核心线程数超时回收或核心线程允许超时回收
private Runnable getTask() {
// 标志 Worker 是否空闲过期 初始化为未过期
boolean timedOut = false; // Did the last poll() time out?
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// Check if queue empty only if necessary.
// 如果线程池已经关闭 则直接返回 null
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}
int wc = workerCountOf(c);
// Are workers subject to culling?
// 如果核心线程允许过期或大于核心线程数,则进行 Woker 过期处理
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
// wc > maximumPoolSize || (timed && timedOut) 返回 null
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c))
return null;
continue;
}
try {
// 根据 timed 值判断是否一直阻塞等待任务还是keepAliveTime时间内阻塞
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
// Worker 空闲过期
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
回收 Worker上文我们提到 Worker 运行过程中若获取 Task 失败则会进入 Worker 回收流程。Worker 的回收依赖 JVM 的垃圾回收,线程池根据当前线程池状态维护一定 Worker 的引用,当 Worker 获取不到任务时,则会触发 Worker 回收流程,实际上就是消除 Woker 的引用,等待 JVM 回收。源码分析// java.util.concurrent.ThreadPoolExecutor#runWorker
try {
while (task != null || (task = getTask()) != null) {
// 执行任务
}
} finally {
// 获取不到任务时,主动回收自己
// 源码参考上文 Worker 执行
processWorkerExit(w, completedAbruptly);
}
线程池关闭线程池提供了两种关闭线程池的方法:// 关闭线程池 不再接受任务但会将已接收任务处理完
void shutdown();
// 关闭线程池 不再接受任务且不处理阻塞队列中的任务
// 返回阻塞队列中未处理的任务
List<Runnable> shutdownNow();
// 具体实现
// java.util.concurrent.ThreadPoolExecutor#shutdown
public void shutdown() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// 权限检查
checkShutdownAccess();
// 将线程池状态置为 SHUTDOWN
advanceRunState(SHUTDOWN);
// Interrupts threads that might be waiting for tasks (as indicated by not being locked)
// 不中断正在执行任务的 Worker 继续执行完阻塞队列任务
interruptIdleWorkers();
onShutdown(); // hook for ScheduledThreadPoolExecutor
} finally {
mainLock.unlock();
}
// 尝试进入 TERMINATED 状态
tryTerminate();
}
// 和 shutdown() 方法逻辑基本一致
public List<Runnable> shutdownNow() {
List<Runnable> tasks;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
// 将线程池状态置为 STOP
advanceRunState(STOP);
// Interrupts all threads
// 中断所有 Worker
interruptWorkers();
tasks = drainQueue();
} finally {
mainLock.unlock();
}
tryTerminate();
return tasks;
}
/**
* Transitions to TERMINATED state if either (SHUTDOWN and pool
* and queue empty) or (STOP and pool empty). If otherwise
* eligible to terminate but workerCount is nonzero, interrupts an
* idle worker to ensure that shutdown signals propagate. This
* method must be called following any action that might make
* termination possible -- reducing worker count or removing tasks
* from the queue during shutdown. The method is non-private to
* allow access from ScheduledThreadPoolExecutor.
*/
final void tryTerminate() {
for (;;) {
int c = ctl.get();
// 1. 当前运行状态是 RUNNING 不设置TIDYING、TERMINATED
// 2. 或者 当前运行状态 处于 TIDYING 或者 TERMINATED, 那么也是暂时不设置TIDYING、TERMINATED
// 3. 或者 就是在 SHUTDOWN ,并且存在任务需要处理,那么也是暂时不设置TIDYING、TERMINATED
// 4. 否则 就是STOP,那么需要往下走
if (isRunning(c) ||
runStateAtLeast(c, TIDYING) ||
(runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty()))
return;
// 执行到这里 说明处于 STOP 状态, 但是还存在工作线程没处理完,帮忙中断线程
// 如果线程数量不为0,则中断一个空闲的工作线程(把中断状态传播出去)
if (workerCountOf(c) != 0) { // Eligible to terminate
interruptIdleWorkers(ONLY_ONE);
return;
}
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) {
try {
// 拓展点 由子类实现
terminated();
} finally {
ctl.set(ctlOf(TERMINATED, 0));
termination.signalAll();
}
return;
}
} finally {
mainLock.unlock();
}
// else retry on failed CAS
}
}
线程池实践案例快速响应用户请求用户发起查询请求,追求快速响应,比如查询商品信息,需要查询商品价格、优惠、图片等。如果使用串联查询,那么时间将会花费很长时间,我们可以使用线程池进行并发IO,这种场景对实时要求较高,因此一般设置更多的线程执行,避免任务阻塞延迟,极大的提高响应时间。离线大数据量并行计算在一些大数据量的离线计算中,比如需要计算全球商品每年的报表信息,这类任务对实时性要求不高,但计算量大,因此可以设置合适的线程数来处理任务,并设置合理的阻塞队列来缓存待执行的任务。经典问题:如何合理配置线程池参数线程池使用中最大的一个难点是:“如何合理的配置线程池参数”,一方面依赖于任务本身的特性:如 IO 密集性任务或 CPU 密集型任务,另一方面依赖于开发者的使用经验。常见配置策略在高性能 MySQL 中看到内核线程推荐配置策略为:核心线程数设置为稳定时请求量值,最大线程设置为波动峰值来解决峰值问题,这是一个可以借鉴的策略,但需要考虑本身机器的性能和任务属性,否则反而会使性能下降。动态线程池尽管我们经过谨慎的探讨和计算,但最终配置的参数也不一定满足我们的要求,因此我们考虑通过动态配置的方式来动态调整我们的线程池参数。而 ThreadPoolExecutor 正好提供运行时调整参数的能力:动态线程池框架推荐轻量级动态线程池,内置监控告警功能,集成三方中间件线程池管理,支持主流配置中心。dynamic-tp是否可以不使用线程池我们使用线程池的本质是为了并发处理任务,任务并发处理我们根据业务的实际情况也可以考虑以下几种可选方案:随着 JDK21 虚拟线程(协程)的推出,使我们有了更多的选择,可以根据实际的应用场景选择合适并发处理方案。
带鱼
Java 多线程 : 阻塞队列 没啥好说的
一 . 阻塞队列简述阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。阻塞队列常用于生产者和消费者的场景:生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。通用概念implements Queue : 基本上都是Queue的实现类 ,即实现了 Queue 的方法可以通过构造方法初始化容量和排序构造方法可以传入整个集合队列类型ArrayBlockingQueue :一个由 数组 结构组成的 有界 阻塞队列。LinkedBlockingQueue :一个由 链表 结构组成的 无界 阻塞队列。PriorityBlockingQueue :一个 支持优先级排序 的 无界 阻塞队列。DelayQueue:一个使用 优先级队列 实现的 无界 阻塞队列。SynchronousQueue:一个 不存储元素 的阻塞队列。LinkedTransferQueue:一个由 链表 结构组成的 无界 阻塞队列。LinkedBlockingDeque:一个由 链表 结构组成的双向阻塞队列。
// 按照类型分类
• 无锁非阻塞并发队列:ConcurrentLinkedQueue和ConcurrentLinkedDeque
• 普通阻塞队列:基于数组的ArrayBlockingQueue,基于链表的LinkedBlockingQueue和LinkedBlockingDeque
• 优先级阻塞队列:PriorityBlockingQueue
• 延时阻塞队列:DelayQueue
• 其他阻塞队列:SynchronousQueue和LinkedTransferQueue
处理方式 抛出异常 返回特殊值 一直阻塞 超时退出
插入方法 add(e) offer(e) put(e) offer(e, time, unit)
移除方法 remove() poll() take() poll(time, unit)
检查方法 element() peek() 不可用 不可用
性能对比1、ArrayBlockingQueue 性能优于LinkedBlockingQueue,但是LinkedBlockingQueue是无界的。2、ArrayBlockingQueue 和 LinkedBlockingQueue 的 poll方法总是比offer方法快,并发越高,差距越大3、ArrayBlockingQueue 和 LinkedBlockingQueue 的 性能远高于PriorityBlockingQueue,显然优先队列在比较优先级上的操作上耗费太多4、PriorityBlockingQueue的 offer方法与 poll方法的性能差距很小,基本维持在近似1:1线程数20501002005001000LinkedBlockingQueue15,031,1532,1663,32203,47563,110ArrayBlockingQueue15,016,1531,1547,16125,47364,68PriorityBlockingQueue78,78172,188360,422813,9693094,26416547,5453二. ArrayBlockingQueue> 一个由数组实现的有界阻塞队列。该队列采用 FIFO 的原则对元素进行排序添加的
> ArrayBlockingQueue 为有界且固定,其大小在构造时由构造函数来决定,确认之后就不能再改变了
> ArrayBlockingQueue 支持对等待的生产者线程和使用者线程进行排序的可选公平策略
- 但是在默认情况下不保证线程公平的访问,在构造时可以选择公平策略(fair = true)。
- 公平性通常会降低吞吐量,但是减少了可变性和避免了“不平衡性”。
// 构造器 :
MC- ArrayBlockingQueue(int capacity)
MC- ArrayBlockingQueue(int capacity, boolean fair)
// 抽象类和接口
I- BlockingQueue<E> : 提供了在多线程环境下的出列、入列操作
?- 内部使用可重入锁 ReentrantLock + Condition 来完成多线程环境的并发操作
// 变量
• items 变量,一个定长数组,维护 ArrayBlockingQueue 的元素。
• takeIndex 变量,int ,为 ArrayBlockingQueue 队首位置。
• putIndex 变量,int ,ArrayBlockingQueue 队尾位置。
• count 变量,元素个数。
• lock 变量,ReentrantLock ,ArrayBlockingQueue 出列入列都必须获取该锁,两个步骤共用一个锁。
• notEmpty 变量,非空,即出列条件。
• notFull 变量,未满,即入列条件。
// 入队
M- add(E e) 方法 : 将指定的元素插入到此队列的尾部(如果立即可行且不会超过该队列的容量),在成功时返回 true , 满了抛出异常
M- offer(E e) 方法 : 将指定的元素插入到此队列的尾部(如果立即可行且不会超过该队列的容量),在成功时返回 true , 满了返回false
M- offer(E e, long timeout, TimeUnit unit) 方法 : 将指定的元素插入此队列的尾部 , 已满在设定时间内等待
M- put(E e) 方法 : 将指定的元素插入此队列的尾部,如果该队列已满,则等待可用的空间
M- enqueue :
- 正常添加元素 , 到达队尾的时候重定向到队头
- 总数 + 1
- 通知阻塞线程
// 出列
M- poll() 方法:获取并移除此队列的头,如果此队列为空,则返回 null 。
M- poll(long timeout, TimeUnit unit) 方法:获取并移除此队列的头部,在指定的等待时间前等待可用的元素(如果有必要)。
M- take() 方法:获取并移除此队列的头部,在元素变得可用之前一直等待(如果有必要)。
M- remove(Object o) 方法:从此队列中移除指定元素的单个实例(如果存在)。
// 核心总结 :
M- offer : 通过 ReentrantLock 上锁
- final ReentrantLock lock = this.lock;
- lock.lock();
- finally -> lock.unlock();
// 关键点 :
1 创建后,容量将无法更改
2 尝试将元素放入满队列将导致操作阻塞
3 尝试从空队列中取出一个元素]也会类似地被阻塞
4 支持可选的公平性策略
三 . DelayQueue支持延时获取元素的无界阻塞队列。里面的元素全部都是“可延期”的元素,列头的元素是最先“到期”的元素
- 如果队列里面没有元素到期,是不能从列头获取元素的,哪怕有元素也不行。
- 也就是说只有在延迟期到时才能够从队列中取元素。
// 作用 :
• 缓存:清掉缓存中超时的缓存数据
• 任务超时处理
// 关键 :
1. 可重入锁ReentrantLock
2. 用于阻塞和通知的Condition对象
3. 根据Delay时间排序的优先级队列:PriorityQueue
4. 用于优化阻塞通知的线程元素leader
// 结构 :
E- AbstractQueue
I- BlockingQueue
M- offer() : 往PriorityQueue中添加元素
- 向 PriorityQueue中插入元素
- 判断当前元素是否为对首元素,如果是的话则设置leader=null , 唤醒所有线程
M- take()
- 获取队首 --- q.peek
IF- 队首为空 , 阻塞 ,等待off 唤醒
ELSE-
获取队首的超时时间 , 已过期则出对
- 如果存在其他线程操作 ,阻塞 , 不存在其他线程 , 独占
- 超时阻塞 --- available.awaitNanos(delay);
- 唤醒阻塞线程
// 使用方式 :
// Step 1 : new 一个
DelayQueue queue = new DelayQueue();
// Step 2 : 加东西
queue.offer(createUserDelayQueueTO());
四 . SynchronousQueueSynchronousQueue没有容量。- 与其他BlockingQueue不同,SynchronousQueue是一个不存储元素的BlockingQueue。每一个put操作必须要等待一个take操作,否则不能继续添加元素,反之亦然。因为没有容量,所以对应 peek, contains, clear, isEmpty ... 等方法其实是无效的。- 例如clear是不执行任何操作的,contains始终返回false,peek始终返回null。SynchronousQueue分为公平和非公平,默认情况下采用非公平性访问策略,当然也可以通过构造函数来设置为公平性访问策略(为true即可)。若使用 TransferQueue, 则队列中永远会存在一个 dummy node(这点后面详细阐述)。SynchronousQueue非常适合做交换工作,生产者的线程和消费者的线程同步以传递某些信息、事件或者任务。C- SynchronousQueue
E- AbstractQueue
I- BlockingQueue
C- TransferQueue
?- 实现公平性策略的核心类,其节点为QNode
五 . LinkedBlockingDeque一个有链表组成的双向阻塞队列,与前面的阻塞队列相比它支持从两端插入和移出元素。支持FIFO、FILO两种操作方式LinkedBlockingQueue是一个阻塞队列内部由两个ReentrantLock来实现出入队列的线程安全,由各自的Condition对象的await和signal来实现等待和唤醒功能。
// 简介
是先进先出队列FIFO。
采用ReentrantLock保证线程安全
// 操作结果
增加 : 队列满 >
put -> 一直阻塞
add -> 抛出异常
offer -> 返回false
删除 : 队列为空
remove -> NoSuchElementException
poll -> 返回false
take -> 阻塞
// 源码分析
LinkedBlockingQueue
C- static class Node<E> : 核心静态内部类 , 表示一个节点
|- E item : 节点原始
|- Node<E> next : 下一节点
F- int capacity : 容量界限
F- AtomicInteger count : 当前元素个数
F- Node<E> head :头节点
F- Node<E> last : 尾节点
F- ReentrantLock takeLock : take,poll等获取锁
F- Condition notEmpty : 等待任务的等待队列
F- ReentrantLock putLock : put,offer等插入锁
F- Condition notFull : 等待插入的等待队列
MC- LinkedBlockingQueue() : 最大数量
MC- LinkedBlockingQueue(int capacity) : 指定数量
MC- LinkedBlockingQueue(Collection<? extends E> c) : 指定集合
M- signalNotEmpty : 表示等待take。put/offer调用,否则通常不会锁定takeLock
|- 获取 tackLock : this.takeLock
|- 锁定takeLock -> takeLock.lock();
|- 唤醒take 线程等待队列 -> notEmpty.signal();
|- 释放锁 -> takeLock.unlock();
M- signalNotFull : 表示等待put,take/poll 调用
|- 获取putLock : this.putLock;
|- 锁定putLock -> putLock.lock();
|- 唤醒插入线程等待队列 -> notFull.signal();
|- 释放锁
M- enqueue : 在队列尾部插入
|- last = last.next = node;
M- E dequeue():移除队列头
|- 保留头指针
|- 获取当前链表的第一个元素
|- 头指针指向第一个元素
|- 获取第一个元素的值并且移除第一个
|- 返回第一个元素的值
M- fullyLock : 锁定putLock和takeLock
|- putLock.lock();
|- takeLock.lock();
M- fullyUnlock : 先解锁takeLock,再解锁putLock
|- putLock.unlock();
M- offer: 将给定的元素设置到队列中,如果设置成功返回true
|- 非空判断 , 获取计数器
|- 判断队列是否已满 -> 返回 Boolean
|- 新建节点
|- 获取插入锁 , 并且锁定
|- 队列未满 -> 插入 -> 计数
|- 如果未满 ,继续唤醒插入线程
|- 解锁
|- 如果对了为空 ,获取线程锁阻塞
M- offer(E e, long timeout, TimeUnit unit) :给定的时间内设置到队列中
M- put(E e) : 将元素设置到队列中,如果队列中没有多余的空间,该方法会一直阻塞 , 直到队列中有多余的空间
|- 核心1 : putLock.lockInterruptibly();
-> 设置前加锁
|- 核心2 : notFull.await();
-> 队列满时等待
M- take() : 从队列中获取值,如果队列中没有值
M- peek() : 非阻塞的获取队列中的第一个元素,不出队列
M- poll() : 非阻塞的获取队列中的值,未获取到返回null。
M- poll(long timeout, TimeUnit unit) :在给定的时间里,从队列中获取值
M- remove(Object o):从队列中移除指定的值。将两把锁都锁定。
M- clear():清空队列。
M- drainTo(Collection c):将队列中值,全部移除,并发设置到给定的集合中。
六 . LinkedTransferQueueLinkedTransferQueue是一个由链表组成的的无界阻塞队列它是ConcurrentLinkedQueue、SynchronousQueue (公平模式下)、无界的LinkedBlockingQueues等的超集。与其他BlockingQueue相比,他多实现了一个接口TransferQueue, 该接口是对BlockingQueue的一种补充,多了tryTranfer()和transfer()两类方法:tranfer():若当前存在一个正在等待获取的消费者线程,即立刻移交之。 否则,会插入当前元素e到队列尾部,并且等待进入阻塞状态,到有消费者线程取走该元素tryTranfer(): 若当前存在一个正在等待获取的消费者线程(使用take()或者poll()函数),使用该方法会即刻转移/传输对象元素e; 若不存在,则返回false,并且不进入队列。这是一个不阻塞的操作七 . PriorityBlockingQueue- PriorityBlockingQueue是支持优先级的无界队列。
- 默认情况下采用自然顺序排序,当然也可以通过自定义Comparator来指定元素的排序顺序。
- PriorityBlockingQueue内部采用二叉堆的实现方式,整个处理过程并不是特别复杂。
- 添加操作则是不断“上冒”,而删除操作则是不断“下掉”。
八 . ArrayBlockingQueue 与 LinkedBlockingQueue 的区别Queue阻塞与否是否有界线程安全保障适用场景注意事项ArrayBlockingQueue阻塞有界一把全局锁生产消费模型,平衡两边处理速度用于存储队列元素的存储空间是预先分配的,使用过程中内存开销较小(无须动态申请存储空间)LinkedBlockingQueue阻塞可配置存取采用 2 把锁生产消费模型,平衡两边处理速度无界的时候注意内存溢出问题,用于存储队列元素的存储空间是在其使用过程中动态分配的,因此它可能会增加 JVM 垃圾回收的负担。九 . 双端队列而 ArrayDeque、LinkedBlockingDeque 就是双端队列,类名以 Deque 结尾正如阻塞队列适用于生产者消费者模式,双端队列同样适用与另一种模式,即工作密取。在生产者-消费者设计中,所有消费者共享一个工作队列,而在工作密取中,每个消费者都有各自的双端队列。如果一个消费者完成了自己双端队列中的全部工作,那么他就可以从其他消费者的双端队列末尾秘密的获取工作。在大多数时候,他们都只是访问自己的双端队列,从而极大的减少了竞争。当工作者线程需要访问另一个队列时,它会从队列的尾部而不是头部获取工作,因此进一步降低了队列上的竞争。十 . 队列对象> 阻塞队列 : 阻塞队列有普通的先进先出队列,
> 包括基于数组的ArrayBlockingQueue
> 基于链表的LinkedBlockingQueue/LinkedBlockingDeque
> 基于堆的优先级阻塞队列PriorityBlockingQueue
> 可用于定时任务的延时阻塞队列DelayQueue
> 用于特殊场景的阻塞队列SynchronousQueue和LinkedTransferQueue
十一 . CopyOnWriteArrayListCopyOnWrite容器即写时复制的容器。 当我们往容器添加元素的时候,先将当前容器进行Copy,复制出一个新的容器, 然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。
优缺点
|- 优点:
读操作性能很高,比较适用于读多写少的并发场景。
Java的list在遍历时,若中途有别的线程对list容器进行修改,则会抛出ConcurrentModificationException异常。
而CopyOnWriteArrayList由于其"读写分离"的思想,遍历和修改操作分别作用在不同的list容器
?- 所以在使用迭代器进行遍历时候,也就不会抛出ConcurrentModificationException异常。
|- 缺点:
内存占用问题,执行写操作时会发生数组拷贝
无法保证实时性,Vector对于读写操作均加锁同步,可以保证读和写的强一致性。
而CopyOnWriteArrayList由于其实现策略的原因,写和读分别作用在新老不同容器上
?- 在写操作执行过程中,读不会阻塞但读取到的却是老容器的数据。
|- 使用场景 :
CopyOnWrite并发容器用于读多写少的并发场景。比如白名单,黑名单,商品类目的访问和更新场景。
CopyOnWriteArrayList
F- ReentrantLock lock = new ReentrantLock() --> 重入锁
F- volatile Object[] array; --> 只能通过 getArray/setArray 访问的数组
M- Object[] getArray() --> 获取数组,非私有方法以便于CopyOnWriteArraySet类的访问
M- setArray(Object[] a) --> 设置数组
M- CopyOnWriteArrayList -- 创建一个空数组
M- CopyOnWriteArrayList(Collection<? extends E> c)
?- 创建一个包含指定集合的数组
B- 如果c的类类型为CopyOnWriteArrayList
|- 直接获取其数组
E- 如果不是
|- 通过 toArray 转数组
|- 如果c.toArray返回的不是 Object[]类型,则通过数组拷贝
|- 设置数组 : setArray(elements);
M- CopyOnWriteArrayList(E[] toCopyIn) : 创建包含给定数组副本的列表
M- size() : 获取数量
M- isEmpty() : 判断列表元素是否为空
M- eq(Object o1, Object o2) : 判断o1 o2是否相等
M- indexOf(Object o, Object[] elements,int index, int fence)
B- 为null , for 循环迭代第一个 null
E- 不为 null ,for 循环 eq
M- lastIndexOf :索引倒叙
M- contains : IndexOf 判断
M- clone : 浅拷贝
|- 重置锁定
|- 返回clone 属性
M- toArray
M- get : 获取原数组中元素
M- set:用指定的元素替换列表中指定位置的元素
|- 获取当前锁并且锁定
|- 获取元素数组
|- 获取老的值
B- 如果老的值和给定值不相等
|- 原数组拷贝 , 将新数组中的索引位置修改为新值
|- 将原数组替换为新数组
E- 否则
|- setArray(elements);
|- 返回老的值
M- add(E e) : 将指定的元素追加到此列表的末尾
|- 获取重入锁 ,锁定
|- 获取原数组
|- 原数组拷贝 并增加一个空位
|- 将指定元素增加到新数组新增的空位中
|- 新数组替换原数组
M- remove :
|- 获取锁并且锁定
|- 获取原数组
|- 获取要删除的元素值 , 获取要移动的值
B- 如果为0,则删除的是最后一个元素
-> setArray(Arrays.copyOf(elements, len - 1));
E- 否则 复制拷贝
|- 新建数组
|- 将原数组中,索引index之前的所有数据,拷贝到新数组中
|- 将元素组,索引index+1 之后的numMoved个元素,复制到新数组,索引index之后
|- 替换原数组
|- 返回老的值 ,最后释放锁
C- COWSubList : 内部视图类
带鱼
Java多线程 : 细说 synchronized
1.1 synchronized 简述synchronized 被称为重量级锁 , 但是 1.6 版本后得到了优化 , 相对轻量了很多, 它可以保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块 .主要操作对象是方法或者代码块中存在的共享数据, 同时可保证一个线程的变化(主要是共享数据的变化)被其他线程所看synchronized 的核心原理为 Java 对象头 以及 Monitor1.2 Java 对象头 和 Monitor对象头结构// 原理 -->
1 Java 对象头 和 Monitor
|-> 对象头 :
|-> Mark Word(标记字段)、Klass Pointer(类型指针)
|-> Klass Pointer : 类元数据指针,决定是何数据
|-> Mark Word : 自身运行时数据 (hashcode,锁状态,偏向,标志位等)
|-> Monitor :
|-> 互斥 :一个 Monitor 锁在同一时刻只能被一个线程占用
// 关系 -->
- Monitor 是一种对象类型 , 任何Java 对象都可以是 Monitor 对象
- 当Java 对象被 synchronized 修饰时 , 就可以当成 Monitor 对象进行处理
// Mark Word 和 Class Metadata Address 组成结构
--------------------------------------------------------------------------------------------------
虚拟机位数 头对象结构 说明
|---------|-----------------------|---------------------------------------------------------------|
32/64bit Mark Word 存储对象的hashCode、锁信息或分代年龄或GC标志等信息
32/64bit Class Metadata Address 类型指针指向对象的类元数据,JVM通过这个指针确定该对象是哪个类的实例。
--------------------------------------------------------------------------------------------------
32 位虚拟机 Mark Word >>>>64 位虚拟机 Mark Word >>>>数据结构// Monitor 的实现方式 @ https://blog.csdn.net/javazejian/article/details/70768369
ObjectMonitor中有两个队列以及一个区域
_WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象)
_owner (指向持有ObjectMonitor对象的线程) 区域
1 当多个线程同时访问一段同步代码时,首先会进入 _EntryList 集合, 此时开始尝试获取monitor2 当线程获取到对象的monitor 后进入 _Owner 区域 ,并把 monitor中的owner变量 设置为当前线程同时monitor中的计数器count加13 若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSet集合中等待被唤醒。4 若当前线程执行完毕 也将 释放monitor(锁) 并 复位变量的值,以便 其他线程进入获取monitor(锁)Monitor 指令monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之匹配查看汇编情况 :// Step 1 : 准备简单的Demo
public class SynchronizedService {
public void method() {
synchronized (this) {
System.out.println("synchronized 代码块");
}
}
}
// Step 2 : 查看汇编码
javap -c -s -v -l SynchronizedService.class
// Step 3 : 注意其中 3 和 13 以及 19
public void method();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc #3 // String synchronized 代码块
9: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: aload_1
13: monitorexit
14: goto 22
17: astore_2
18: aload_1
19: monitorexit
20: aload_2
21: athrow
22: return
处理逻辑详情 :// synchronized 源码分析 @ https://blog.csdn.net/javazejian/article/details/70768369
// 具体的流程可以参考上面博客的具体分析, 此处仅总结 , 大佬们肝真好
// synchronized 代码块底层逻辑 ( monitorenter 和 monitorexit )
> synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,
其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置
Start : 当执行monitorenter指令时,当前线程将试图获取 objectref(即对象锁) 所对应的 monitor 的持有权
Thread-1 : objectref.monitor = 0 --> 获取monitor --> 设置计数器值为 1
Thread-2 : 发现objectref.monitor = 0 --> 阻塞等待 --> Thread-1 执行 monitorexit ,
计数器归 0 --> Thread-2 正常流程获取获取monitor
注意点 :
编译器将会确保无论方法通过何种方式完成,方法中调用过的每条 monitorenter 指令都有执行其对应 monitorexit 指令 ,
方法异常时通过异常处理器处理异常结束
synchronized 方法底层逻辑 (ACC_SYNCHRONIZED标识)方法级的同步是隐式,即无需通过字节码指令来控制的,它实现在方法调用和返回操作之中。JVM可以从方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。方法调用时,调用指令将会 检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置|- 如果设置了,执行线程将先持有monitor(虚拟机规范中用的是管程一词), 然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放monitor。在方法执行期间,执行线程持有了monitor,其他任何线程都无法再获得同一个monitor异常处理 : 如果一个同步方法执行期间抛 出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的monitor将在异常抛到同步方法之外时自动释放synchronized 内存级原理// 最后生成的汇编语言
lock cmpxchg %r15, 0x16(%r10) 和 lock cmpxchg %r10, (%r11)
synchronized的底层操作含义是先对对象头的锁标志位用lock cmpxchg的方式设置成"锁住"状态释放锁时,在用lock cmpxchg的方式修改对象头的锁标志位为"释放"状态,写操作都立刻写回主内存。JVM会进一步对synchronized时CAS失败的那些线程进行阻塞操作,这部分的逻辑没有体现在lock cmpxchg指令上,我猜想是通过某种信号量来实现的。lock cmpxchg 指令前者保证了可见性和防止重排序,后者保证了操作的原子性。1.3 synchronized 用法// 加锁方式 ,当前实例 ,当前class , 自定义object
> synchronized(this)
> synchronized(object)
> synchronized(class) 或者静态代码块
synchronized关键字最主要的三种使用方式:修饰实例方法,作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁 。修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。这里再提一下:synchronized关键字加到非 static 静态方法上是给对象实例上锁。另外需要注意的是 尽量不要使用 synchronized(String a), 部分字符串常量会缓冲到常量池里面, 不过可以试试 new String("a")1.4 synchronized 其他知识点解释 : synchronized 提供了一种独占式的加锁方式 ,其添加和释放锁的方式由JVM实现阻塞 : 当 synchronized 尝试获取锁的时候,获取不到锁,将会一直阻塞谈谈 synchronized和ReenTrantLock 的区别两者都是可重入锁synchronized 依赖于 JVM 而 ReenTrantLock 依赖于 APIReenTrantLock 比 synchronized 增加了一些高级功能synchronized 与等待唤醒机制 (notify/notifyAll和wait) 等待唤醒机制需要处于synchronized代码块或者synchronized方法中 , 调用这几个方法前必须拿到当前对象的监视器monitor对象synchronized 与 线程中断 线程的中断操作对于正在等待获取的锁对象的synchronized方法或者代码块并不起作用1.5 多线程中的锁概念1.5.1 锁按照等级分类锁可以按照以下等级进行升级 : 偏向锁 -> 轻量级锁 -> 重量级锁 , 锁的升级是单向的偏向锁轻量级锁自旋锁重量级锁1.5.2 锁的操作锁清除 :Java虚拟机在 JIT编译时 (可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间Java 常见的锁synchronized 关键字 , 重量锁ReentrantLock 重入锁ReadWriteLock 读写锁1.5.3 其他锁概念内部锁 :synchronized : 锁对象的引用 , 锁保护的代码块每个Java 对象都可以隐式地扮演一个用于同步的锁的角色 ,这些内置的锁被称为 内部锁 或 监视器锁 .公平锁/非公平锁公平锁是指多线程按照申请锁的顺序来获取锁,非公平锁指多个线程获取锁的顺序不是按照申请锁的顺序,有可能造成优先级反转或者饥饿现象,非公平锁的优点在于吞吐量比公平锁大,ReentrantLock默认非公平锁,可通过构造函数选择公平锁,Synchronized是非公平锁。可重入锁 可重入锁指在一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁,ReentrantLock与Synchronized都是可重入的。独享锁/共享锁 独享锁是指一个锁只能一个线程独有,共享锁指一个锁可被多个线程共享,对于ReadWriteLock,读锁是共享锁,写锁是独享所。互斥锁/读写锁 独享锁/共享锁是一种广义的说法,互斥锁/读写锁是其具体实现。乐观锁/悲观锁乐观锁与悲观锁是看待同步的角度不同,乐观锁认为对于同一个数据的修改操作,是不会有竞争的,会尝试更新,如果失败,不断重试。悲观锁与此相反,直接获取锁,之后再操作,最后释放锁。分段锁分段锁是一种设计思想,通过将一个整体分割成小块,在每个小块上加锁,提高并发。1.6 锁的转换过程对象头的变化可以看下图 , 说的很清楚了 @ https://www.cnblogs.com/jhxxb/p/10983788.html
// 之前知道 , 锁的状态改变是单向的 , 由 偏向锁 -> 轻量级锁 -> 重量级锁 ,我们分别捋一下
// 偏向锁 -> 偏向锁, 即重偏向操作
1 对象先偏向于某个线程, 执行完同步代码后 , 进入安全点时,若需要重偏向,会把类对象中 epoch 值增加
2 退出安全点后 , 当有线程需要尝试获取偏向锁时, 直接检查类实例对象中存储的 epoch 值与类对象中存储的 epoch 值是否相等, 如果不相等, 则说明该对象的偏向锁已经无效了, 可以尝试对此对象重新进行偏向操作。
// 偏向锁 -> 轻量级锁
1 当发现对象已被锁定 ,且 ThreadID 不是自己 , 转为 偏向锁 , 在该线程的栈帧中建立 Lock Record 空间
1.7 为什么锁会转换// 这要从机制说起 , 每一种锁都有各自的特点
> 偏向锁
- 优点 : 无 CAS ,消耗少 , 性能高 , 可重入
- 缺点 : 锁竞争时撤销锁消耗高
- 场景 : 同一个线程执行同步代码
> 轻量级锁
- 优点 : 竞争的线程不会阻塞
- 缺点 : 轻量级锁未获取锁时会通过自旋获取 , 消耗资源
- 场景 : 线程交替执行同步块或者同步方法,追求响应时间,锁占用时间很短
> 重量级锁
- 优点 : 线程竞争不使用自旋 , 只会唤醒和等待
- 缺点 : 造成线程阻塞 , 锁的改变也消耗资源
- 场景 : 追求吞吐量,锁占用时间较长
// 针对不同的需求 , 选择合适的锁 , 达到业务目的
1.8 Synchoized 源码
synchronized 是一个修饰符 , 我们需要从 C 的角度去看
Step 1 : 下载 OpenJDK 代码 https://blog.csdn.net/leisure_life/article/details/108367675
Step 2 : 根据代码索引 .c 文件
// TODO
1.9 Synchoized 用法public void operation(Integer check) {
/**
* 校验无锁情况
*/
public void functionShow(Integer check) {
logger.info("------> check is {} <-------", check);
if (check == 0) {
showNum = 100;
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} else if (check == 1) {
showNum = 200;
}
logger.info("------> check is Over {} :{}", check, showNum);
}
/**
* 同步方法 , 校验 synchronized 方法
*/
synchronized public void functionShowSynchronized(Integer check) {
logger.info("------> check is {} <-------", check);
if (check == 0) {
showNum = 100;
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} else if (check == 1) {
showNum = 200;
}
logger.info("------> check is Over synchronized {} :{}", check, showNum);
}
/**
* 校验 synchronized 代码块
*/
public void statementShowSynchronized(Integer check) {
logger.info("------> check is {} <-------", check);
synchronized (this) {
if (check == 0) {
showNum = 100;
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} else if (check == 1) {
showNum = 200;
}
}
logger.info("------> check is Over synchronized {} :{}", check, showNum);
}
// 校验 代码块以 Class 为对象
public void classShowSynchronized(Integer check) {
logger.info("check is {} <-------", check);
synchronized (CommonTO.class) {
if (check == 0) {
showNum = 100;
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} else if (check == 1) {
showNum = 200;
}
}
logger.info("check is Over synchronized {} :{}", check, showNum);
}
// 同步代码块 Object
public void objectShowSynchronized(Integer check) {
logger.info("check is {} <-------", check);
synchronized (lock) {
if (check == 0) {
showNum = 100;
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} else if (check == 1) {
showNum = 200;
}
}
logger.info("check is Over synchronized {} :{}", check, showNum);
}
// 同步代码块 Object
public void objectStringShowSynchronized(Integer check) {
logger.info("check is {} <-------", check);
synchronized (lock2) {
if (check == 0) {
showNum = 100;
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} else if (check == 1) {
showNum = 200;
}
}
logger.info("check is Over synchronized {} :{}", check, showNum);
}
带鱼
单例模式下双重校验锁 DCL 的灵魂三问
前言hello,大家好,我是 Lorin,今天给大家带来双重校验锁的灵魂三问?以及我们如何一步步实现一个懒汉式单例。开始阅读前,大家可以思考下面三个问题:DCL 实现中:
1、为什么需要使用两个 if 语句?
2、为什么使用了 synchronized 关键字还需要使用 volatile 关键字?
3、双重校验锁使用需要注意的问题
如何实现一个双重校验锁 DCL双重校验锁 DCL 最常用使用的场景在懒汉式单例,下面我们按照思路简单实现一个懒汉式单例:定义一个单例变量public class SingletonDemo {
private static Object object = null;
}
定义一个获取单例的方法定义一个单例的获取方法,用于单例的初始化和获取,为了支持多线程访问,我们这里使用 synchronized 进行同步,保证同一时刻只有一个线程访问。public class SingletonDemo {
private static Object object = null;
// 初始化和获取实例
public Object getObject() {
synchronized (SingletonDemo.class) {
if (object == null) {
object = new Object();
}
return object;
}
}
}
性能优化上面的懒汉式单例看起来并没有多大的问题,但是却存在很大的性能的问题,因为我们每次获取我们的实例都需要进行锁的获取和释放,即使我们的实例已经初始化完成,因此为了解决这个问题,我们需要进行一点点优化。public class SingletonDemo {
private volatile static Object object = null;
public Object getObject() {
// 如果实例已经初始化完成,直接返回实例不获取锁
if (object != null){
return object;
}
synchronized (SingletonDemo.class) {
if (object == null) {
object = new Object();
}
return object;
}
}
}
性能优化带来的一点点问题上面的代码表面上看起来已经完美了,解决了并发问题,也优化了性能问题,但是仔细看你会发现了新的问题,由于处理指令重排的优化可能导致 object != null 判断并不准确,怎么理解呢?创建一个对象分为初始化和实例化两部分,大致可以分为以下几步:
1、在堆中申请一份内存
2、创建对象
3、将 object 指向我们对象的内存引用
如果没有指令重排的情况下,我们拿到的对象一定是完整的对象,但是可能存在指令重排优化,上面的顺序可能变成下面这样:
1、申请一份内存
2、将 object 指向我们对象的内存引用
3、创建对象
那么我们将会拿到一个没有实例化完成的对象,因此我们需要禁止指令重排,Java 提供了 volatile 指令来禁止指令重排。
题外话:我们写代码的过程其实就是不断在重复优化和解决的问题,直到达到适应我们目前场景、基本情况的最优解(不一定是理论的最优解)。什么是指令重排?为了提升执行速度/性能,计算机在执行程序代码的时候,会对指令进行重排序。什么是指令重排?简单来说就是系统在执行代码的时候并不一定是按照程序的代码的顺序依次执行。指令重排可以保证单线程串行语义一致(as-if-serial),但是没有义务保证多线程间的语义也一致,所以在多线程下,指令重排可能会导致一些问题。关于指令重排更多内容可以参考 一文读懂 Java Memory Model(JMM)最后,我们得到了终极版本的代码:public class SingletonDemo {
private volatile static Object object = null;
public Object getObject() {
// 如果实例已经初始化完成,直接返回实例不获取锁
if (object != null){
return object;
}
synchronized (SingletonDemo.class) {
if (object == null) {
object = new Object();
}
return object;
}
}
}
总结如何理解文章开篇理解的三个问题1、为什么需要使用两个 if 语句?为了性能优化2、为什么使用了 synchronized 关键字还需要使用 volatile 关键字?性能优化导致带来了多线程指令重排问题,需要使用 volatile 解决指令重排的问题。3、双重校验锁使用需要注意的问题JDK版本大于1.5Volatile 屏蔽指令重排序的语义在 JDK1.5 中才被完全修复,此前的 JDK 中即使将变量声明为 volatile 也仍然不能完全避免重排序所导致的问题关于 Volatile 相关介绍可以参考 Volatile 相关章节。
带鱼
盘点认证协议 : 普及篇之 OTP 和短信认证方式
纯约束型协议 : OAuth , SAML , OIDC , CAS ,LTPA服务器类协议 : RADIUS , Kerberos , ADFS认证方式类 : OTP , 生物认证 (人脸 , 声纹 , 指纹)认证服务器(附带) : AD , LDAP , ADFS这一篇主要是闲谈一下 OTP 和短信验证码 ,他们2者的逻辑其实是一致的一 . OTP 认证1.1 什么是 OTP 认证OTP 在很多地方都能找到它的痕迹 , 例如支付宝的6位随机密码 , 其实也是一种很复杂的 OTP 方式 .OTG 是动态密码 ,每个令牌有不同的ID 与其绑定 ,令牌根据该 ID 和当前 时间 计算出 6位 随机代码 ,服务端 会根据 该 ID 生成随机码 ,如果相同则判断正确,OTP 安全的核心在于密钥 , 每个人通过对应账户生成的密钥是不同的 . 当他们用同一个算法加密时 , 会生成不同的随机密码 .Goole 身份验证器案例 :可以看到 , 其中有三个输入条件 : 账户名 + 密钥 + 密钥类型 , 这就带出了 OTP 的多种类型1.2 OTP 类型OTP类型 :时间性 : 以时间为参数事件性 : 以次数为参数挑战数 : 以异步挑战数作为参数 ,即服务端下发 挑战码根据不同的类型 , OTP 又提供了多种不同的 OTP 算法 ,主要有2种 :TOTP(Time-Based One-Time Password,基于时间的一次性密码)HOTP(HMAC-based One-Time Password,一种基于HMAC的一次性口令算法)我们来看一下他们的细节 :OTP 算法 :// 计算公式 : OTP(K,C) =Truncate ( HMAC - SHA - 1 ( K , C ) )
K : 密钥串 ,ID
C :参数
HMAC-SHA-1 : 使用SHA-1做HMAC
Truncate :截取加密后的串,并取加密后串的哪些字段组成一个数字
HMAC-SHA-1 模式 :HMAC-SHA-1加密后的长度得到一个20字节的密串取这个20字节的密串的最后一个字节,取这字节的低4位,作为截取加密串的下标偏移量按照下标偏移量开始,获取4个字节,按照大端方式组成一个整数截取这个整数的后6位或者8位转成字符串返回TOTP 算法// TOTP : TOTP只是将其中的参数C变成了由时间戳产生的数字。
C = (T - T0) / X;
T : 当前时间戳
T0 : 取值为 0
x : 步数 ,多久参数一个动态密码
HOTP 算法 详情参考 : HOTP和TOTP算法图解 @ www.jianshu.com/p/a7b900e8e…HTOP 是基于计数器的算法 , 服务端和客户端共用一个密钥 , HOTP 的问题在于怎么保证计数器的同步,一句话说明: 每次请求和验证 HOTP 时,移动因子将根据计数器递增。生成的代码是有效的,直到您主动请求另一个代码并由身份验证服务器进行验证。每次验证代码和用户获得访问权限时,OTP 生成器和服务器都会同步。对于 HOTP,通过下图我们已经看到输入算法的主要有两个元素,一个是共享密钥,另外一个是计数。在 RFC 算法中用一下字母表示:K 共享密钥,这个密钥的要求是每个 HOTP 的生成器都必须是唯一的。一般我们都是通过一些随机生成种子的库来实现。C 计数器,RFC 中把它称为移动元素(moving factor)是一个 8个 byte的数值,而且需要服务器和客户端同步。另外一个参数比较好理解,Digit 表示产生的验证码的位数最后两个参数可能暂时不好理解,我们先放在这,等用到在解释T 称为限制参数(Throttling Parameter 表示当用户尝试 T 次 OTP 授权后不成功将拒绝该用户的连接。s 称为重新同步参数(Resynchronization Parameter 表示服务器将通过累加计数器,来尝试多次验证输入的一次性密码,而这个尝试的次数及为 s。该参数主要是有效的容忍用户在客户端无意中生成了额外不用于验证的验证码,导致客户端和服务端不一致,但同时也限制了用户无限制的生成不用于验证的一次性密码。1.3 OTP 超前步数问题及方案
> 1 服务端的次数落后客户端的次数 , 服务端会匹配多次 , 以达到和客户端同步的次数 ,匹配成功
> 2 服务端超前客户端
// 以下图存在bug , 服务端超前于客户端时,讲不在能验证!!!!!!!!!!!!!
// 解决方案 : 服务器的值应该只能在成功后进行递增 , 只有用户登录成功后 , 才可以更新用户的计数
// 服务端会允许一定次数的计数器 ,但是如果超过限度 , 程序会报错
1.4 OTP 总结OTP 有其便利性 , 可以避免繁多的密码问题 , 但是这意味着需要一个 Client 端去实现一套 OTP 逻辑 , 相对于短信验证码 , 它显得更加复杂 , 问题也会更多 . 但他有存在的必要不过相对于认证 , 如果作为支付宝那样的支付方式 ,其实也是很不错的选择 .局限和优势 :虽然两者都比完全不使用 MFA 安全得多,但是 HOTP 和 TOTP 都有其局限性和优势。TOTP (两种技术中较新的一种)易于使用和实现,但是基于时间的元素确实有可能出现时间漂移(密码创建和使用之间的滞后)。如果用户没有立即输入 TOTP,有可能在他们输入之前就过期了。因此,服务器必须考虑到这一点,并使用户可以轻松地再次尝试,而不必自动锁定它们。HOTP 没有基于时间的限制,所以它比较用户友好,但是可能更容易受到穷举法的影响。这是因为 HOTP 有效的窗口可能比较长。某些形式的 HOTP 通过在其代码中添加一个基于时间的组件来解释这一漏洞,这在一定程度上模糊了这两种类型 OTP 之间的界限。二 . 短信验证码为什么有了短信验证码 , 还会有OTP ?现阶段的黑客技术种已经找到了截获这些短信代码的创造性方法,无论是通过SIM 卡欺诈还是其他类型的黑客手段,帮助他们获取你的短信。也有可能联系你的供应商,将你的电话号码转移到一个新的电话上利用用于漫游的连接系统 SS7的问题拦截网络上的 SMS 消息虽然基于 sms 的 MFAs 可能比没有 MFA 要好,但是它们的安全性比手机上的认证应用程序或者使用密钥代码生成器要差得多。这就意味着短信验证码其实并不是绝对安全 (PS : 破解也存在局限性 ,没有违规操作 ,也没有可乘之机)相对于首次登录就使用验证码 , 很多短信验证码场景是在首次认证的二次认证(MFAS)后进行使用 ,但是其实这也只是多加了一层**因为这个原因 , 所以才需要 OTP 来完成更安全的功能 . **
带鱼
盘点认证协议 : 普及篇之OAuth , OIDC , CAS
一 . 前言我这里试着把协议分为几大类 :纯约束型协议 : OAuth , SAML , OIDC , CAS服务器类协议 : RADIUS , Kerberos , ADFS认证方式类 : OTP , 生物认证 (人脸 , 声纹 , 指纹)认证服务器(附带) : AD , LDAP这一篇我们只对流程进行一个普及 , 后面陆陆续续来分析一下其中的实现方式.1.1 前置知识点 TokenToken 是认证过程中最常见的一个概念 , 它没有特定的规范 , 它仅仅是一个有着不同协议特征的钥匙 . 通常而言 , 他是有一定规律的无意义的字符串1.2 前置知识点 JWTJWT 全称 Json web token , JWT 通常由三部分组成 :JWT 头 : 头部以 JSON 格式表示有效载荷签名二 . 纯约束型纯约束型表示其长得就像个规范 ,而不同的框架去实现这个规范 (其实规范也叫框架 , 是一种狭义的框架)2.1 OAuth 协议2.1.1 OAuth 漫谈通常我们说的 OAuth 是指其 2.0 版本 , 现在OAuth 已经公布了其 2.1 的版本 , 在结构上做了简化 , OAuth 其实不是一个完整的概念 , 它实际上是由许多不同的 rfc 组成的 (RFC 6749 , RFC 6750),它们以不同的方式相互构建并添加特性 , 就如下图所示 :@ aaronparecki.com/2019/12/12/…在整个 OAuth 协议的发展中 , 他被陆陆续续添加了多个规范 , 并且实现了多种功能. 例如 RFC 6749 中就定义了我们用的最多的四种授权类型: 授权代码、隐式、密码和客户端凭据 , 而 RFC 7636 中则加入了 PKCE (一种无需客户机机密就可以使用授权代码流的方法) , RFC 8252 中将其建议为本机应用程序使用 .>>> 直到陆陆续续到了 OAuth2.1 , 还剩下以下三种类型 : Authorization code + PKCEClient CredentialsDevice Grant这一篇不会涉及到过多的 OAuth2.1 , 主要是我的实现代码还没有写完...OAuth2.0 支持的类型 :密码模式(resource owner password credentials)授权码模式(authorization code)简化模式(implicit)客户端模式(client credentials)新设备 (Device Grant) : 没有浏览器或者没有键盘的设备2.1.2 OAuth 2.0 的流程 :OAuth 的整个流程大概就长下面那样 :Step 1 : 用一个方式去认证 , 认证成功后返回一个票据AccessToken (这个不限于一步完成)Step 2 : 用返回的票据获取用户信息OAuth2.0 的流程网络上已经说的太多了 ,自认为不会被前辈们画的更好 , 这里也就直接引用了:总结性归类 :这里细说一下三者的区别 :Code 方式 :Code 方式一般是企业最常用的一种方式 , 因为它很灵活 , 安全性也高 , 它和 implicit 以及 password 模式的区别主要是多了一个获取 Code 的过程 :Step 1: Authoriza - > code : 发起请求返回codeStep 2: Code -> Token : 传递Code 换取ToeknStep 3: Token -> UserInfo : 传递Token 换取用户信息implicit 方式 :implicit 方式是相对于 Code 的简化版 , 它由 Step1 Authoriza 直接来到了Token 步骤password 方式 :password 方式就变化较大了 ,它省去了跳转登录页认证的步骤 , 直接获取TokenOAuth Code 方式OAuth implicit 方式OAuth Password 方式OAuth 协议的接口请求一览// Authoriza Code 模式
* 第一步 : http://localhost:8080/oauth/authorize?response_type=code&client_id=pair&redirect_uri=http://baidu.com
> Response_type -> 返回类型
> Client_id-> 对应的client id
> redirect_uri->重定向的地址
* 第二步 :
http://localhost:8080/oauth/token?grant_type=authorization_code&code=o4YrCS&client_id=pair&client_secret=secret&redirect_uri=http://baidu.com
> grant_type
> code
> client_id
> client_secret
> redirect_uri
* 第三步 :
通过Token 换取信息 ... 略
----------------------------
// implicit 模式 (略 , 第一步直接返回Code)
----------------------------
// Password 模式 (直接传入密码)
http://localhost:8080/oauth/accessToken?grant_type=password&client_id=b7a8cc2a-5dec-4a64&username=admin&password=123456
2.1.3 OAuth FAQ问题一 : state 的作用 问题 : 当被攻击人(平民 A )登录时 ,让 A 认为登录的是自己的账号 ,但是 ,实际上 ,登录的是 攻击者 (狼人B)事先准备的账号 ,这就导致 A 在其上做的操作 ,B均可见 。B 事先准备好攻击账号 ,进入第一步临时授权 ,获取到Code , 此时 ,基础验证已经完成 ,剩下访问 授权服务器 获取 Access Token.B 此时强行停止 自身验证流程 ,骗取 A 进行点击 ,让 A 通过Code 完成后续登录 ,此时 A 以为登录的自身账号 ,并且 以为 正确进入系统这就是通常说的中间人攻击 , 而有了 state , 开发者可以用这个参数验证请求有效性,也可以记录用户请求授权页前的位置 , 当然 ,也可以预防 CSRF问题二 : implicit 和 Password 存在的场景 ?Password 从请求上就可以看出有一定的安全漏洞 ,如果没有 SSL + 明文密码 , 这简直是把密码告诉别人了 , 而在我的使用中, 部分应用是发起存后台请求 , 不期望进行跳转 , 这个时候 , password 就能派上用场问题三 : 待完善 TODO ~~~~~2.2 OIDC 协议讲了 OAuth 当然要来说一下 OIDC 这个小兄弟啦 , OIDC 其实很简单 , 就是在 OAuth 的基础上加入了 OpenID 的概念 , 你如果为了方便 , 复用 OAuth 的代码都没问题的. 即在 OAuth 的基础上额外携带一个 JWT 传递用户信息2.2.1 OIDC 简介OpenID Connetction : OIDC= (Identity, Authentication) + OAuth 2.0 , 它是一个基于 OAuth 2 的身份认证标准协议 , 通过 OAuth 2.0 构建了一个身份层 .OIDC 提供了ID Token 来解决第三方客户端标识用户身份 的问题 ,在Oauth2 的授权流程中 ,一并提供用户的身份认证信息给第三方客户端 ,ID token 使用JWT 格式进行包装 (得益于 JWT 的包容性 紧凑性 和 防篡改机制 ,并且提供 UserInfo ,可以回看一下开头的 JWT 扩展哦 )OIDC 构成 信息- core : 定义 OIDC 的核心功能 ,在OAuth 2 之上 构建身份认证 ,以及使用 Claims 来传递用户信息
- Discovery : 发现服务 , 用于客户端动态的获取OIDC 服务相关的元数据描述信息
- Dynamic Registration : 动态注册服务 , 使客户端可以动态的注册到OIDC 的 OP
- OAuth 2.0 Multiple Response Types :可选。针对OAuth2的扩展,提供几个新的response_type。
- OAuth 2.0 Form Post Response Mode:可选。针对OAuth2的扩展,OAuth2回传信息给客户端是通过URL的querystring和fragment这两种方式,这个扩展标准提供了一基于form表单的形式把数据post给客户端的机制。
- Session Management :可选。Session管理,用于规范OIDC服务如何管理Session信息
- Front-Channel Logout:可选。基于前端的注销机制,使得RP(这个缩写后面会解释)可以不使用OP的iframe来退出
- Back-Channel Logout:可选。基于后端的注销机制,定义了RP和OP直接如何通信来完成注销
2.2.2 OIDC 请求流程RP 向 OP 申请 授权 ,OP 返回授权Access Token 以及 ID Token , 使用Access Token 向 User Info EndPoint 请求 信息AuthN 请求虽然是复用OAuth 2 的 Authorization 请求 ,但是用途不一样 ,OIDC 的 authN scope 参数 必须要有一个openid 的参数The RP (Client) sends a request to the OpenID Provider (OP).
The OP authenticates the End-User and obtains authorization.
The OP responds with an ID Token and usually an Access Token.
The RP can send a request with the Access Token to the UserInfo Endpoint.
The UserInfo Endpoint returns Claims about the End-User.
+--------+ +--------+
| | | |
| |---------(1) AuthN Request-------->| |
| | | |
| | +--------+ | |
| | | | | |
| | | End- |<--(2) AuthN & AuthZ-->| |
| | | User | | |
| RP | | | | OP |
| | +--------+ | |
| | | |
| |<--------(3) AuthN Response--------| |
| | | |
| |---------(4) UserInfo Request----->| |
| | | |
| |<--------(5) UserInfo Response-----| |
| | | |
+--------+ +--------+
// 详细步骤 :
》 OIDC 单点登录流程
> 1 . 用户点击登录 ,触发对OIDC-SERVER 的认证请求
|-> request : 包含参数URL , 指向登录成功后的跳转地址
|-> response : 返回 302 ,Location 指向 OIDC-SERVER ,Set-Cookie 设置了 nonce的cookie
> 2 . 向 OIDC-SERVER 发起 authc 请求
|-> client_id=implicit-client :发起认证请求的客户端的唯一标识
|-> reponse_mode=form_post :使用form表单的形式返回数据
|-> response_type=id_token :返回包含类型 id_token
|-> scope=openid profile :返回包含有openid这一项
|-> state :等同于OAuth2 state ,用于保证客户端一致性
|-> nonce : 写入的cookie 值
|-> redirect_uri : 认证成功后的回调地址
> 3 . OIDC-SERVER 验证 authc 请求
|-> client_id是否有效,redircet_uri是否合法 等一系列验证
> 4 . 引导用户登录 ,以及用户登录
|-> resumeURL
|-> username + password
> 5 . 返回一个自动提交form 表单的页面
|-> id_token:id_token即为认证的信息,OIDC的核心部分,采用JWT格式包装的一个字符串
|-> scope:用户允许访问的scope信息
|-> state : 类似
|-> session_state :会话状态
> 6 . 验证数据有效性,构造自身登录状态
|-> 客户端验证id_token的有效性 ,保证客户端得到的id_token是oidc-sercer.dev颁发的
OIDC 接口演示 ------------> 真不记得是哪位大佬的案例了....
// OIDC 直观流程 请求地址 :
// Step 1 : 发起 Authorize 请求
https://${yourDomain}/oauth2/default/v1/authorize?response_type=code
&client_id=12345
&redirect_uri=https://proxy.example.com:3080/v1/webapi/oidc/callback
&scope=openid,email
&state=syl
// Step 2 : 认证成功后重定向返回
https://proxy.example.com:3080/v1/webapi/oidc/callback?code=pkzdZumQi1&state=syl
// Step 3 : 申请 Token
POST https://${yourOktaDomain}/oauth2/default/v1/token
?grant_type=authorization_code&
code=pkzdZumQi1&
redirect_uri=https://proxy.example.com:3080/v1/webapi/oidc/callback
client_id=12345&
client_secret=gravitational
// Step 4 : AccessToken 返回
{
"id_token":"FW6AlBeyalZtDIRXxA0u5XBbZkLzjYzKUQBloxQXSSGPFmRS8eSfDu0A4nS4GF1aQP9PRxQk7gIh9bjaX99
aa4vDSzP1E2ajsgIomlNGhNxBqEDV5Exp0xISE9bZ4HUzM91pbzPPj7Bq5ZQUWcSuSVD0NAfkAoG6qDpbQfxPjWRyfthz3p
UEXwZe8Cz4eOXOM45UKB4Q0VnVSChVF84MWkeBFKzhrRNXd2dFv0HTlkQr6vXGlYsocMxR06wo38HvGiKjkUmL2YUyPOjZa
oUN4ovfwlwdGdjNR2GVcRsXzjxCPszJ9dTXztoL5wo2ycEpuxkkNp57BuZ9YRexoNnRHahFKH76XrFsTvdvAYk3fBVUqrO5
vvyxHAFrAIKpV0FvaMiBwKNfaE84oRC6aBXnzS3q4uVyGcHveHQMJB1temgB599rfVH3pBqurUmQCd0tVexRZj4PUkrDocf
8Z0QKkCD0eonH0Q1bRpQPY5vATiLkpF8RArU7wyB2FxhB3egtQBvwDgsVjyix7u8Cx4P9oy3IJje6SZfc6Lz61uEQttpVhy
qfzgFYUqVoQacw6rocCn3u61dM0moB"
"access_token":"IEZKr6ePPtxZBEd",
"token_type":"bearer",
"scope":"read:org",
"expires_in":3600
}
// Step 5 : 对 id_token 进行解码
{
"sub":"virag",
"iss":"https://${yourOktaDomain}/oauth2/",
"aud":"client_12345",
"iat":"1595977376",
"exp":"1595980976",
"email":"virag@goteleport.com",
"email_verified":"true"
}
// Step 6 : 对资源进行请求
2.2.3 OIDC 扩展文档https://www.ubisecure.com/education/differences-between-saml-oauth-openid-connect/
https://www.okta.com/identity-101/whats-the-difference-between-oauth-openid-connect-and-saml/
https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-v2-protocols
https://yangsa.azurewebsites.net/index.php/2019/08/08/brief-summary-of-differences-between-oauth2-and-oidc/
https://www.c-sharpcorner.com/article/oauth2-0-and-openid-connect-oidc-core-concepts-what-why-how/
2.2.4 OIDC 的额外知识点OIDC Discovery 规范定义了一个服务发现的规范,它定义了一个api( /.well-known/openid-configuration ),这个api返回一个json数据结构,其中包含了一些OIDC中提供的服务以及其支持情况的描述信息,这样可以使得oidc服务的RP可以不再硬编码OIDC服务接口信息会话管理Session Management :可选。Session管理,用于规范OIDC服务如何管理Session信息。Front-Channel Logout:可选。基于前端的注销机制。Back-Channel Logout:可选。基于后端的注销机制。OIDC 的好处OIDC使得身份认证可以作为一个服务存在。OIDC可以很方便的实现SSO(跨顶级域)。OIDC兼容OAuth2,可以使用Access Token控制受保护的API资源。OIDC可以兼容众多的IDP作为OIDC的OP来使用。OIDC的一些敏感接口均强制要求TLS,除此之外,得益于JWT,JWS,JWE家族的安全机制,使得一些敏感信息可以进行数字签名、加密和验证,进一步确保整个认证过程中的安全保障。2.3 CASCAS 我可太熟了 , 这还不随便和大家扯淡~~~~2.3.1 CAS 术语CAS分为两部分,CAS Server和CAS ClientCAS Server用来负责用户的认证工作,就像是把第一次登录用户的一个标识存在这里CAS Client就是我们自己开发的应用程序,需要接入CAS Server端CAS 的三个术语Ticket Granting ticket (TGT) :可以认为是CAS Server根据用户名密码生成的一张票,存在Server端Ticket-granting cookie (TGC) :其实就是一个Cookie,存放用户身份信息,由Server发给Client端Service ticket (ST) :由TGT生成的一次性票据,用于验证,只能用一次2.3.2 CAS 处理流程用户第一次访问网站,重定向到CAS Client , 发现没有cookie(TGC或者没有ST) ,重定向到 CAS Server端的登录页面在登陆页面输入用户名密码认证(Web 和 Server 交互),认证成功后cas-server生成TGT,再用TGT生成一个ST然后再第三次重定向并返回ST和cookie(TGC)到浏览器浏览器带着ST再访问想要访问的地址浏览器的服务器收到ST后再去cas-server验证一下是否为自己签发的再登陆另一个接入CAS的网站,重定向到CAS Server,server判断是第一次来(但是此时有TGC,也就是cookie,所以不用去登陆页面了)但此时没有ST,去cas-server申请一个于是重定向到cas-servercas-server 通过TGT + TGC 生成了ST浏览器的服务器收到ST后再去cas-server验证一下是否为自己签发的我知道一般人懒得看 ,我也是 ,所以我画了一张图!!!2.3.3 CAS FAQCAS 与 OAuth 最大的几个区别 :CAS 和 OAuth 都是一种认证结构/协议 , 而 Token/ST则是属于一种票据的方式 , 并没有特定的归属1 资源和用户信息 :2 申请流程的细微区别:3 客户端 :CAS 认证的票据 :CAS 通过将 TGC 写入 Cookie , 当下次认证是从 Cookie 中取出 TGC 认证 ,所以要想做跨浏览器登录 , 可以在这里做文章哦 !总结今天先这样了 , 又要到转钟了 , 这篇文章既是盘点 , 也是对自身知识图谱的完善 , 认证协议多种多样 , 常规的应用通常只需要选择其中最合适的一种去实现自己的业务即可.
带鱼
【多线程系列】JUC 中的另一重要大杀器 AQS 抽象队列同步器
回顾前面我们讲解 JUC 中一个重要的基础工具 CAS, 今天我们来分享 JUC 中的另一重要工具 AQS【多线程系列】高效的 CAS (Compare and Swap)【多线程系列】CAS 常见的两个升级版本 CLH、MCS导读AQS 是什么、底层原理(独占模式、共享模式实现)AQS 变种 CLH 相比于原始 CLH 的改变版本及说明AQSAQS 全称是 AbstractQueuedSynchronizer,是 Java 并发包中的一个抽象类,用于构建各种同步器和锁,如 ReentrantLock、CountDownLatch、Semaphore 等。核心思想基于 CAS 和 变种 CLH 实现对互斥资源的访问;访问互斥资源时,当互斥资源空闲时,通过 CAS 操作将互斥资源置为锁定状态,并将访问线程置为当前线程,当互斥资源被其他线程锁定时,通过变种 CLH 实现的逻辑 FIFO 队列实现对线程的阻塞以及资源释放时的唤醒机制。结构AQS 使用 int 成员变量 state 表示同步状态,通过内置的 FIFO 等待队列 来完成获取资源线程的排队工作。public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer
implements java.io.Serializable {
private transient volatile Node head;
private transient volatile Node tail;
// 使用 volatile 保证变量的可见性
private volatile int state;
protected final int getState() {
return state;
}
protected final void setState(int newState) {
state = newState;
}
// 提供 CAS 操作更新 state 的值
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
}
state 状态状态名描述SIGNAL(-1)表示该节点正常等待PROPAGATE(-3)应将 releaseShared 传播到其他节点CONDITION(-2)该节点位于条件队列,不能用于同步队列节点CANCELLED(1)由于超时、中断或其他原因,该节点被取消(0)节点初始状态Node 节点static final class Node {
/**
* Marker to indicate a node is waiting in shared mode
*/
static final Node SHARED = new Node();
/**
* Marker to indicate a node is waiting in exclusive mode
*/
static final Node EXCLUSIVE = null;
/**
* Status field, taking on only the values:
*/
volatile int waitStatus;
// 前置节点
volatile Node prev;
// 后置线程
volatile Node next;
/**
* The thread that enqueued this node. Initialized on
* construction and nulled out after use.
*/
volatile Thread thread;
/**
* Link to next node waiting on condition, or the special
* value SHARED.
*/
Node nextWaiter;
}
独占模式和共享模式AQS 支持两种资源共享方式:Exclusive(独占,只有一个线程能执行,如基于独占模式实现的 ReentrantLock)和 Share(共享,多个线程可同时执行,如基于共享模式实现的 Semaphore/CountDownLatch)。独占模式获取锁 /**
* 获取独占锁主流程:
* 1、阻塞获取锁,获取锁逻辑由具体同步器重写 tryAcquire() 实现
* 2、获取锁成功直接返回,获取锁失败进入 FIFO 线程进行线程的阻塞和唤醒
* 2.1、调用 addWaiter() 将当前线程封装为 Node 节点并入队
* 2.2、入队成功后在 acquireQueued() 方法尝试自旋获取锁或阻塞当前线程
*
* @param arg
*/
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
/**
* 将当前线程封装为 Node 节点并入队
*
* @param mode
* @return
*/
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
// 当队尾节点不为 null 时使用 CAS 快速入队
// 这种写法可以借鉴,可以提高性能(减少小概率的临界值判断)
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 快速入队失败 重新入队直到入队成功
enq(node);
return node;
}
private Node enq(final Node node) {
for (; ; ) {
Node t = tail;
// 队尾节点为空 初始化一个哨兵节点
// 作用:统一处理逻辑,首节点是哨兵节点或持有锁线程(正在持有或已释放唤起后续线程)
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
// CAS 入队
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
/**
* Node 节点入 FIFO 等待队列后 进行 CAS 操作获取锁或线程阻塞
* @param node
* @param arg
* @return
*/
final boolean acquireQueued(final Node node, int arg) {
// 获取锁结果
boolean failed = true;
try {
// 是否被中断
boolean interrupted = false;
for (; ; ) {
final Node p = node.predecessor();
// 当前节点为等待队列中的第二个节点 尝试 CAS 获取锁
// 前置可能节点为 哨兵节点 或 已经释放锁节点 尝试 CAS 获取锁
if (p == head && tryAcquire(arg)) {
// 获取成功设置当前线程为 头节点
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 非第二节点/获取失败判断是否阻塞当前线程 & 阻塞线程并判断线程是否被中断
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
// 超时、中断导致线程获取锁失败时 标记节点状态为 Cancel
if (failed)
cancelAcquire(node);
}
}
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
// 前置节点 处于等待状态 当前节点线程阻塞挂起
if (ws == Node.SIGNAL)
return true;
// 前置节点已取消 去除队列中已取消节点
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
// 前置节点状态只能是 0 或 PROPAGATE 可能需要等待
// 将前置节点 状态置为 SIGNAL 并 重新尝试是否可以获取锁
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
private void cancelAcquire(Node node) {
// Ignore if node doesn't exist
if (node == null)
return;
node.thread = null;
// Skip cancelled predecessors
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
// predNext is the apparent node to unsplice. CASes below will
// fail if not, in which case, we lost race vs another cancel
// or signal, so no further action is necessary.
Node predNext = pred.next;
// Can use unconditional write instead of CAS here.
// After this atomic step, other Nodes can skip past us.
// Before, we are free of interference from other threads.
node.waitStatus = Node.CANCELLED;
// If we are the tail, remove ourselves.
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);
} else {
// If successor needs signal, try to set pred's next-link
// so it will get one. Otherwise wake it up to propagate.
int ws;
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
Node next = node.next;
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next);
} else {
// 唤醒节点条件:pred == head或者pred.thread == null 第一个节点;
// ((ws = pred.waitStatus) != Node.SIGNAL 并且 (ws >0 || compareAndSetWaitStatus(pred, ws, Node.SIGNAL) == false)):前置节点突然释放锁
unparkSuccessor(node);
}
node.next = node; // help GC
}
}
释放锁 public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
// 等待队列不为空 同时状态不为 初始状态(节点初始化已完成)
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
// 将头节点状态置为 初始状态 0
compareAndSetWaitStatus(node, ws, 0);
// 从尾到头查找到最早的入队可以唤醒的节点(不包括头节点)
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
// 唤醒找到的节点
if (s != null)
LockSupport.unpark(s.thread);
}
共享模式获取锁 /**
* 获取共享锁主流程:
* 1、尝试获取贡献锁 获取锁逻辑由具体同步器重写 tryAcquire() 实现
* 2、获取锁成功直接返回,获取锁失败进入 FIFO 线程进行线程的阻塞和唤醒
* 2.1、调用 addWaiter() 将当前线程封装为 Node 节点并入队
*2.2、入队成功后尝试自旋获取锁(获取成功后走共享锁唤醒逻辑 setHeadAndPropagate)或阻塞当前线程
* @param arg
*/
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
// 设置头节点 & 共享锁传播唤醒逻辑
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
// 和独占锁一致
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
// 和独占锁一致
if (failed)
cancelAcquire(node);
}
}
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
// 设置为头结点
setHead(node);
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
// 如果后继节点为空或者后继节点为共享类型,则进行唤醒后继节点
if (s == null || s.isShared())
// 读锁唤醒往后传播(A 被唤醒获取锁唤醒 B ,B 被唤醒被获取锁唤醒 C...)
// 见释放锁
doReleaseShared();
}
}
释放锁 public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
// 唤醒后一个等待节点
doReleaseShared();
return true;
}
return false;
}
private void doReleaseShared() {
*/
for (;;) {
// 执行唤醒逻辑(如果从setHeadAndPropagate方法调用该方法,那么这里的head是新的头节点)
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
// CAS原子操作,因为setHeadAndPropagate和releaseShared这两个方法都会调用doReleaseShared,避免多次unpark唤醒操作
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
// 唤醒节点
unparkSuccessor(h);
}
// 如果后继节点暂时不需要唤醒,那么当前头节点状态更新为PROPAGATE,确保后续可以传递给后继节点
else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
// 防止其它线程设置了头节点,其它线程已经获取锁,交给其它线程处理
if (h == head) // loop if head changed
break;
}
}
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
// 将头节点状态置为 初始状态 0
compareAndSetWaitStatus(node, ws, 0);
// 从尾到头查找到最早的入队可以唤醒的节点(不包括头节点)
// 从尾到头的原因:避免已经入队但通过 next 节点查找不到(https://blog.csdn.net/foxException/article/details/108917338)
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
// 唤醒找到的节点
if (s != null)
LockSupport.unpark(s.thread);
}
模版方法的使用AQS 使用了模板方法模式,当实现自定义同步器时需要重写下面几个 AQS 提供的钩子方法://独占方式。尝试获取资源,成功则返回true,失败则返回false。
protected boolean tryAcquire(int)
//独占方式。尝试释放资源,成功则返回true,失败则返回false。
protected boolean tryRelease(int)
//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
protected int tryAcquireShared(int)
//共享方式。尝试释放资源,成功则返回true,失败则返回false。
protected boolean tryReleaseShared(int)
//该线程是否正在独占资源。只有用到condition才需要去实现它。
protected boolean isHeldExclusively()
AQS 条件队列 Condition在 AQS 的基础上,Java 提供了一个更高级的同步工具 Condition,它允许线程在特定条件下等待和唤醒,以实现更复杂的线程间通信。实现 synchronized 对象锁中的wait、notify、notifyAll, Condition 支持多条件,可以实现更细粒度的控制。仅支持独占锁。使用示例public class MainTest {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
new Thread(() -> {
lock.lock();
System.out.println("线程1获取锁");
// 条件等待 释放锁
System.out.println("线程1条件等待、释放锁");
condition.awaitUninterruptibly();
System.out.println("线程1重新获取锁");
lock.unlock();
System.out.println("线程1释放锁");
}).start();
new Thread(() -> {
lock.lock();
System.out.println("线程2获取锁");
// 条件等待 释放锁
System.out.println("线程唤醒条件队列的的一个锁");
condition.signal();
lock.unlock();
System.out.println("线程2释放锁");
}).start();
}
}
// 运行结果:
线程1获取锁
线程1条件等待、释放锁
线程2获取锁
线程唤醒条件队列的的一个锁
线程2释放锁
线程1重新获取锁
线程1释放锁
类图ConditionObject 类结构Condition 接口提供了常见的标准方法,ConditionObject 类是具体实现。 public class ConditionObject implements Condition, java.io.Serializable {
private static final long serialVersionUID = 1173984872572414699L;
/** First node of condition queue. */
private transient Node firstWaiter;
/** Last node of condition queue. */
private transient Node lastWaiter;
}
Condition 接口提供的方法 //响应线程中断的条件等待
void await() throws InterruptedException;
//不响应线程中断的条件等待
void awaitUninterruptibly();
//设置相对时间的条件等待(不进行自旋)
long awaitNanos(long nanosTimeout) throws InterruptedException;
//设置相对时间的条件等待(进行自旋)
boolean await(long time, TimeUnit unit) throws InterruptedException;
//设置绝对时间的条件等待
boolean awaitUntil(Date deadline) throws InterruptedException;
//唤醒条件队列中的头结点
void signal();
//唤醒条件队列的所有结点
void signalAll();
核心方法下面以 await、signal 两个核心方法介绍 Condition 的底层实现。await() public final void await() throws InterruptedException {
// 如果线程被中断 抛出中断异常
if (Thread.interrupted()) throw new InterruptedException();
// 将节点加入到条件队列
Node node = addConditionWaiter();
// 释放之前获取的锁资源
int savedState = fullyRelease(node);
int interruptMode = 0;
// 当不再同步队列时才挂起线程(因为唤醒时会重新加入同步队列竞争锁)
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// AQS acquireQueued 方法逻辑 加入同步队列后等待获取锁逻辑
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
// 中断逻辑处理
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
private Node addConditionWaiter() {
Node t = lastWaiter;
// If lastWaiter is cancelled, clean out.
if (t != null && t.waitStatus != Node.CONDITION) {
// 从后往前清理已经取消的线程
unlinkCancelledWaiters();
t = lastWaiter;
}
// 将当前线程加入到条件队列中(获取互斥锁时执行 所有不用加锁)
Node node = new Node(Thread.currentThread(), Node.CONDITION);
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
// AQS 释放锁逻辑
final int fullyRelease(Node node) {
boolean failed = true;
try {
// 获取当前节点的 state
int savedState = getState();
// 释放锁
if (release(savedState)) {
failed = false;
return savedState;
} else {
throw new IllegalMonitorStateException();
}
} finally {
if (failed)
node.waitStatus = Node.CANCELLED;
}
}
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
final boolean isOnSyncQueue(Node node) {
// 说明在条件队列中,不再同步队列
if (node.waitStatus == Node.CONDITION || node.prev == null)
return false;
if (node.next != null) // If has successor, it must be on queue
return true;
return findNodeFromTail(node);
}
// 从同步队列尾部开始遍历线程是否在同步队列中
private boolean findNodeFromTail(Node node) {
Node t = tail;
for (; ; ) {
if (t == node)
return true;
if (t == null)
return false;
t = t.prev;
}
}
signal() public final void signal() {
// 当前持有锁线程才可唤醒
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
// 存在需要唤醒的线程
if (first != null)
doSignal(first);
}
/**
* 遍历条件队列 从前往后尝试获取一个有效的线程(非取消)
*
* @param first
*/
private void doSignal(Node first) {
do {
// firstWaiter 头节点指向条件队列头的下一个节点
if ((firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
// 原来的头节点和同步队列断开
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
// 是否唤醒一个有效线程(从前往后依次尝试)
final boolean transferForSignal(Node node) {
// 判断节点是否已经在之前被取消
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
// 调用 enq 添加到 同步队列的尾部
Node p = enq(node);
int ws = p.waitStatus;
// 同步节点前置节点 修改为 SIGNAL 等待后续唤醒
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
// AQS 入同步队列逻辑
private Node enq(final Node node) {
for (; ; ) {
Node t = tail;
// 尾节点为空 需要初始化头节点,此时头尾节点是一个
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 不为空 循环赋值
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
// signalAll() 区别在于会唤醒条件队列中的所有等待线程
private void doSignalAll(AbstractQueuedSynchronizer.Node first) {
lastWaiter = firstWaiter = null;
do {
AbstractQueuedSynchronizer.Node next = first.nextWaiter;
first.nextWaiter = null;
transferForSignal(first);
first = next;
} while (first != null);
}
补充AQS 变种 CLH 改进点将 CLH 自旋操作改为线程阻塞操作扩展每个节点的状态、显式的维护前驱节点和后继节点以及出队节点显式设为 null 等辅助 GC 的优化来支持更多功能
带鱼
Java 多线程 :漫谈多线程模型
一 . happens-before 模型1.1 happens-before 定义happens-before 规则可以帮助我们有效的判断操作的顺序 , 帮助我们理解多线程的数据流动如果一个操作 happens-before 另一个操作,那么第一个操作的执行结果,将对第二个操作可见,而且第一个操作的执行顺序,排在第二个操作之前。两个操作之间存在 happens-before 关系,并不意味着一定要按照 happens-before 原则制定的顺序来执行。如果重排序之后的执行结果与按照 happens-before 关系来执行的结果一致,那么这种重排序并不非法。1.2 happens-before 规则程序次序规则:一个线程内,按照代码顺序,书写在前面的操作,happens-before 于书写在后面的操作。锁定规则:一个 unLock 操作,happens-before 于后面对同一个锁的 lock 操作。volatile 变量规则:对一个变量的写操作,happens-before 于后面对这个变量的读操作。传递规则:如果操作 A happens-before 操作 B,而操作 B happens-before 操作C,则可以得出,操作 A happens-before 操作C线程启动规则:Thread 对象的 start 方法,happens-before 此线程的每个一个动作。线程中断规则:对线程 interrupt 方法的调用,happens-before 被中断线程的代码检测到中断事件的发生。线程终结规则:线程中所有的操作,都 happens-before 线程的终止检测,我们可以通过Thread.join() 方法结束、Thread.isAlive() 的返回值手段,检测到线程已经终止执行。对象终结规则:一个对象的初始化完成,happens-before 它的 finalize() 方法的开始1.3 其他规则将一个元素放入一个线程安全的队列的操作,happens-before 从队列中取出这个元素的操作。将一个元素放入一个线程安全容器的操作,happens-before 从容器中取出这个元素的操作。在 CountDownLatch 上的 countDown 操作,happens-before CountDownLatch 上的 await 操作。释放 Semaphore 上的 release 的操作,happens-before 上的 acquire 操作。Future 表示的任务的所有操作,happens-before Future 上的 get 操作。向 Executor 提交一个 Runnable 或 Callable 的操作,happens-before 任务开始执行操作。二 . 重排序为了提高性能,处理器和编译器常常会对指令进行重排序 , 其主要目的是在不改变程序执行结果的前提下,尽可能提高程序的运行效率在单线程环境下,不能改变程序运行的结果。存在数据依赖关系的情况下,不允许重排序。总结 : 无法通过 happens-before 原则推导出来的,JMM 允许任意的排序。as-if-serial 语义所有的操作均可以为了优化而被重排序,但是你必须要保证重排序后执行的结果不能被改变,编译器、runtime、处理器都必须遵守 as-if-serial 语义as-if-serial 只保证单线程环境,多线程环境下无效JIT 优化原则 尽可能地优化程序正常运行下的逻辑,哪怕以 catch 块逻辑变得复杂为代价。三 . 线程的 CPU 时间片Java中线程会按优先级分配CPU时间片运行当前运行线程主动放弃CPU,JVM暂时放弃CPU操作(基于时间片轮转调度的JVM操作系统不会让线程永久放弃CPU,或者说放弃本次时间片的执行权),例如调用yield()方法。当前运行线程因为某些原因进入阻塞状态,例如阻塞在I/O上。当前运行线程结束,即运行完run()方法里面的任务。yield 放弃 CPU yield操作并不会永远放弃CPU,仅仅只是放弃了此次时间片,把剩下的时间让给别的线程IO 柱塞 运行程序将有两条线程工作,ioThread每次遇到I/O阻塞就放弃当前的时间片,而主线程则按JVM分配的时间片正常运行CPU 优先级 Java把线程优先级分成10个级别,线程被创建时如果没有明确声明则使用默认优先级,JVM将根据每个线程的优先级分配执行时间的概率。有三个常量 :Thread.MIN_PRIORITY : 最小优先级值(1)Thread.NORM_PRIORITY : 默认优先级值(5)Thread.MAX_PRIORITY : 最大优先级值(10)。线程的调度策略决定上层多线程运行机制,JVM的线程调度器实现了抢占式调度,每条线程执行的时间由它分配管理,它将按照线程优先级的建议对线程执行的时间进行分配,优先级越高,可能得到CPU的时间则越长。四 .内存屏障硬件层提供了一系列的内存屏障 memory barrier / memory fence(Intel的提法)来提供一致性的能力几种主要的内存屏障 :lfence,是一种Load Barrier 读屏障sfence, 是一种Store Barrier 写屏障mfence, 是一种全能型的屏障,具备ifence和sfence的能力带Lock前缀类,Lock不是一种内存屏障,但是它能完成类似内存屏障的功能。内存屏障的能力阻止屏障两边的指令重排序强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效**内存屏障最常见的地方就是 Volatile **五. CPUCPU 高速缓存原理 Java内存模型中每个线程的工作内存实际上就是寄存器以及高速缓存的抽象 , 各个核心直接通过系统总线连接 , 而总线是一种共享资源 , 这就意味着资源竞争.计算机的局部性原理 局部性原理是缓存技术的底层理论基础。局部性包括两种形式:时间局部性,一个具有良好时间局部性的程序中,被引用过一次的存储器位置很可能在不远的将来再被多次引用空间局部性,一个具有良好空间局部性的程序中,如果一个存储器位置被引用了一次,那么程序很可能在不远的将来引用附近的一个存储器位置存储器体系结构 : 存储器呈现金字塔结构 , 主要包括寄存器 , 高速缓存 , 主存等几个概念 , 存储器有以下几个特点 :一层存储器只和下层存储器打交道,不会跨级访问下层作为上层的一个缓存。CPU要访问的数据的最终一般都经过主存,主存作为下层其他设备的一个缓存,其他设备的数据最终都要进入主存才能被CPU访问到CPU 高速缓存核心原理 TODO @ blog.csdn.net/iter_zc/art…六. 内存模型之一致性一致性的满足 @ blog.csdn.net/iter_zc/art…在单机器多CPU的情况下,多CPU并发执行,共用一个内存,一般通过共享内存的方式来处理一致性问题,通过定义满足不同一致性需求的内存模型来解决内存一致性问题(Memory Consistency)在分布式环境中,多台机器多CPU通过网络来并发执行,一般通过消息通信的方式来处理一致性问题 (Paxos协议 , Zab协议)6.1 一致性分类严格一致性 Strict Consistency 线性一致性 Linearizability所有的读写操作都按照全局的时序来排列执行 , 所有的CPU需要共享一个全局的时钟顺序 , 且按照该顺序执行顺序一致性 Sequential Consistency对每个单个CPU来说,它看到自己程序的执行顺序始终是和程序定义是一致的(单个CPU角度)每个CPU看到的其他CPU的写操作都是按照相同的顺序执行的,大家看到的最终执行的视图是一致的(从全局的角度)单个CPU对共享变量的写操作马上对其他CPU可见因果一致性 Causal Consistency因果一致性是一种弱的顺序一致性,只有有因果关系的数据才需要保证顺序一致性,没有因果关系的数据不需要保证顺序一致性通俗来说就是 B 对 x 的写操作 W(x)B 会依赖于 A 对 x 的写操作 , 即对外展现为 W(x)a, W(x)b处理器一致性/ PRAM(Piplined RAM) 管道式存储器只要求从一个处理器来的写操作按照同样的顺序被其他处理器看到,不同处理器的写操作可以按照不同的顺序被看到就是说它不保证有因果关系的写操作按照执行的顺序执行弱一致性 Weak Consistency弱一致性只对被同步操作保护的共享变量而言,规定了只有对共享变量的同步操作完成之后,共享数据才可能保持一致性.在同步操作过程中,是不保证一致性的,单个处理器对共享变量的修改对其他处理器是不可见的。相比与严格的顺序一致性,它只保持了执行顺序上的顺序一致性,至于可见性必须要等待同步操作结束对同步变量的读写按照顺序一致性只有所有对同步变量的写操作完成之后才能对同步变量进行访问只有所有对同步变量的访问(读/写)完成后才能对同步变量访问释放一致性 Release Consistency释放一致性规定了对同步变量的释放操作后,就对同步变量的状态广播到其他处理器进入一致性 Entry Consistency进入同步变量时,获取同步变量的最新状态缓存一致性 Cache ConsistencyTODO九 . 多线程模型9 .1 并行模型并行 Worker : (许多 java.util.concurrent 包下的并发工具都使用了这种模型)并行 worker 的核心思想是,它主要有两个进程即代理人和工人,Delegator 负责接收来自客户端的任务并把任务下发,交给具体的 Worker 进行处理,Worker 处理完成后把结果返回给 Delegator,在 Delegator 接收到 Worker 处理的结果后对其进行汇总,然后交给客户端。9 .2 响应式 - 事件驱动系统 : Actor 模型在 Actor 模型中,每一个 Actor 其实就是一个 Worker, 每一个 Actor 都能够处理任务。简单来说,Actor 模型是一个并发模型,它定义了一系列系统组件应该如何动作和交互的通用规则,最著名的使用这套规则的编程语言是 Erlang。一个参与者Actor对接收到的消息做出响应,然后可以创建出更多的 Actor 或发送更多的消息,同时准备接收下一条消息。9 .3 响应式 - 事件驱动系统 : Channels 模型 .在 Channel 模型中,worker 通常不会直接通信,与此相对的,他们通常将事件发送到不同的 通道(Channel)上,然后其他 worker 可以在这些通道上获取消息,下面是 Channel 的模型图
带鱼
【多线程系列】高效的 CAS (Compare and Swap)
导读CAS 原理、适应场景、如何避免 ABA 问题基于 CAS 操作的原子类环境及版本运行版本 JDK 8JDK 源码版本:jdk8-b13CASCAS 全称 Compare and Swap,是 Java 中提供的一个原子操作,是一种高效且线程安全的并发编程技术。流程CAS 需要提供三个参数:原始值、当前值、期望值,执行流程如下:CAS 优缺点优点非阻塞、高效CAS 是一种非互斥的同步方式,当访问互斥变量时,不进行加锁,而是直接进行修改,修改完成后判断互斥变量有没有被其它线程修改,如果未被修改,则更新成功;若被其它线程修改,则自旋重试,避免了阻塞线程带来上下文切换开销,是一种高效保证线程安全的实现方式,适用于同步代码块执行较快,线程等待时间较短的场景。缺点自旋开销但当同步代码块执行时间比较长,线程会进行大量无用的自旋,占用CPU资源,因此CAS不适用同步代码块执行时间比较长,线程等待时间较长的场景。ABA问题CAS 有一个典型的问题就是ABA问题,即原始值最初为3,但是中间被其它线程修改多次,最后又变为了3,当进行比较时,我们程序认为没有被其它线程修改。ABA 问题对中间一定不能被其它线程操作的场景有影响,可以通过版本号的方式解决,因此,使用时需要注意是否需要规避ABA问题。CAS 原理Java Unsafe 类提供了很多偏底层的方法,其中就包括 CAS 方法,例如: /**
* Atomically update Java variable to <tt>x</tt> if it is currently
* holding <tt>expected</tt>.
* @return <tt>true</tt> if successful
*/
public final native boolean compareAndSwapInt(Object o, long offset,
int expected,
int x);
// Object o, long offset 获取内存中的当前值、int expected 原始值、int x 修改值
jdk8-b13 源码分析 compareAndSwapInt 方法// hotspot/src/share/vm/prims/unsafe.cpp
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
UnsafeWrapper("Unsafe_CompareAndSwapInt");
oop p = JNIHandles::resolve(obj);
// 获取变量在内存中的地址
jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END
// hotspot/src/os_cpu/linux_x86/vm/atomic_linux_x86.inline.hpp 以 x86 为例
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
int mp = os::is_MP();
// 使用 Lock 实现 缓存行锁
__asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
: "=a" (exchange_value)
: "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
: "cc", "memory");
return exchange_value;
}
CAS 在 Java 中的应用在 JUC 中原子类都是基于 CAS 实现。AtomicLong(存在 ABA 问题)incrementAndGet() 自增 /**
* Atomically increments by one the current value.
*
* @return the updated value
*/
public final long incrementAndGet() {
return unsafe.getAndAddLong(this, valueOffset, 1L) + 1L;
}
/**
* Atomically adds the given value to the current value of a field
* or array element within the given object <code>o</code>
* at the given <code>offset</code>.
*
* @param o object/array to update the field/element in
* @param offset field/element offset
* @param delta the value to add
* @return the previous value
* @since 1.8
*/
public final long getAndAddLong(Object o, long offset, long delta) {
long v;
do {
v = getLongVolatile(o, offset);
} while (!compareAndSwapLong(o, offset, v, v + delta));
return v;
}
AtomicStampedReference(解决 ABA 问题)public class AtomicStampedReference<V> {
private static class Pair<T> {
final T reference;
// 增加 stamp 作为版本号
final int stamp;
private Pair(T reference, int stamp) {
this.reference = reference;
this.stamp = stamp;
}
static <T> Pair<T> of(T reference, int stamp) {
return new Pair<T>(reference, stamp);
}
}
}
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair<V> current = pair;
/**
* 1、比较值的引用 & stamp 值是否被其它线程改变
* 2、当前值的引用 & stamp 已经是终态
* 3、CAS 生成新的 pair 对象
**/
return
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
private boolean casPair(Pair<V> cmp, Pair<V> val) {
return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
}
使用示例public class MainTest {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter(1000);
Thread thread = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
counter.increment();
}
});
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
counter.increment();
}
});
thread.start();
thread1.start();
thread.join();
thread1.join();
System.out.println("final value: " + counter.getValue());
System.out.println("final stamp: " + counter.getStamp());
}
}
class Counter {
private final AtomicStampedReference<Integer> value;
public Counter(int initialValue) {
value = new AtomicStampedReference<>(initialValue, 0);
}
public int getValue() {
return value.getReference();
}
public int getStamp() {
return value.getStamp();
}
public int increment() {
int[] stampHolder = new int[1];
// 注意 compareAndSet 里比较的是 value 的引用 需要使用包装类接收 否则装箱拆箱会导致原始引用丢失
Integer current;
int next;
do {
current = value.get(stampHolder);
next = current + 1;
} while (!value.compareAndSet(current, next, stampHolder[0], stampHolder[0] + 1));
return next;
}
}
AtomicMarkableReference(不解决 ABA 问题)AtomicMarkableReference 只是使用 Boolean 标记数据是否被改变,适用于一些监测数据是否改变的场景,如下是一个简单场景。class CounterAtomicMarkable {
private final AtomicMarkableReference<Integer> value;
public CounterAtomicMarkable(int initialValue) {
value = new AtomicMarkableReference<>(initialValue, false);
}
public int decrease() {
// 检测当前数据 是否被操作
// 比如 有一个账户 账户状态作为 boolean 值 当账户状态正常时可以入账出账 非正常时不可出账入账
// 一般方案: 判断和操作需要加锁进行同步
// 关停账户 value.attemptMark(value.getReference(), true);
if (value.isMarked()) {
return -1;
} else {
Integer current;
int next;
do {
if (value.isMarked()) {
return -1;
}
current = value.getReference();
next = current - 1;
} while (!value.compareAndSet(current, next, false, false));
return next;
}
}
}
带鱼
【多线程系列】CAS 常见的两个升级版本 CLH、MCS
导读普通自旋锁可能存在的一些问题:饥饿、如何实现公平、CPU 高速缓存频繁同步CLH 锁 和 MCS 锁是什么?以及使用场景环境及版本运行版本:JDK8普通自旋锁存在的问题自旋锁是 Java 并发编程中的常见解决方案,当互斥资源被其它线程占用时,通过自旋的方式尝试获取锁,避免阻塞和唤醒线程带来的上下文切换开销,但普通的自旋锁存在以下几方面问题:1、非公平锁,可能导致饥饿
2、依赖一个互斥标记,线程较多时竞技激烈,且多个CPU高速缓存同步频繁
3、实现非公平锁需要额外的字段
CLH 锁 和 MCS 锁解决上述问题,我们可以用 CLH 锁 MCS 锁通过队列实现。CLH 锁CLH 是一种逻辑队列自旋锁,由 Craig、Landin 和 Hagersten 三位作者提出,具体内容在 《Building FIFO and Priority-Queuing Spin Locks from Atomic Swap》 论文中有详细介绍。Java 中 AQS 就是基于变种 CLH 实现。流程下面是 CLH 锁 加锁和解锁的大致流程:加锁维护队列的尾节点,通过 CAS 操作将线程入队,并将前置节点置为上一个尾节点(逻辑连接),lock 状态置为 true (lock 状态为 true 表示正在获取锁或已经成功获取锁,需要结合前置节点 lock 状态判断)。入队后的节点,自旋轮询前一个尾节点(即当前节点的前置节点)lock 状态,当前置节点为空或 lock 为 false 时,当前节点成功获取锁。解锁解锁时将当前节点的 lock 状态置为 false。示例代码interface Lock {
void lock();
void unlock() throws Exception;
}
class CLHLock implements Lock {
/**
* tailNode 尾节点原子操作保证线程安全
*/
private final AtomicReference<Node> tailNode = new AtomicReference<>();
private final ThreadLocal<Node> currentNodeLocal = new ThreadLocal<>();
private static class Node {
/**
* 前驱节点
*/
private Node preNode;
/**
* 当前节点状态
* volatile 保证对后置线程的可见性
*/
private volatile Boolean lockState;
public Node(Boolean lockState) {
this.lockState = lockState;
}
}
@Override
public void lock() {
Node currentNode = currentNodeLocal.get();
if (currentNode == null) {
currentNodeLocal.set(new Node(true));
currentNode = currentNodeLocal.get();
}
// 拿到当前节点的前置节点 形成逻辑连接 无实际连接
Node preNode = tailNode.getAndSet(currentNode);
// 检查前置节点 lock state
while (currentNode.preNode != null && preNode.lockState) {
System.out.println(Thread.currentThread().getName() + " 自旋等待获取锁");
}
System.out.println(Thread.currentThread().getName() + " 获取锁成功");
}
@Override
public void unlock() throws Exception {
Node currentNode = currentNodeLocal.get();
if (!currentNode.lockState || (currentNode.preNode != null && currentNode.preNode.lockState)) {
throw new Exception("current thread is not locked");
}
currentNode.lockState = false;
// 清除线程 ThreadLocal 本次锁信息 避免拿到已经释放的锁信息
currentNodeLocal.remove();
System.out.println(Thread.currentThread().getName() + " 释放锁");
}
}
CLH 的优点性能优异,获取和释放锁开销小。CLH 的锁状态不再是单一的原子变量,而是分散在每个节点的状态中,降低了自旋锁在竞争激烈时频繁同步的开销。释放锁的开销也因为不需要使用 CAS 指令而降低。公平锁。实现简单,可用于拓展,如 AQS 就是基于变种 CLH实现。CLH 的不足对于锁长时间持有的场景会造成 CPU 自旋损耗。过于简单,实现复杂功能需要进行拓展。MCS 锁MCS 由 John M. Mellor-Crummey 和 Michael L. Scott 提出,具体内容可以在 《Algorithms for Scalable Synchronization on Shared-Memory Multiprocessors》 论文中查看。MCS 锁和 CLH 锁十分相似,都是逻辑队列自旋锁,但 CLH 锁轮询的是前置节点的 lock 域,而 MCS 锁轮询的自己当前节点的 lock 域,前置节点释放锁时会更新队列后置节点 lock 状态,即可以根据当前节点的 lock 状态来判断是否可以获取锁,主要是为了解决 NUMA(Non-Uniform Memory Access) 架构下读取远端内存速度较慢的问题。和 CLH 锁区别CLH 锁逻辑队列之间连接无物理连接,MCS 锁存在物理连接。核心:CLH 锁通过自旋轮询前置节点 lcok 域状态判断是否获取锁,MCS 锁判断当前节点 lock 状态。流程下面是 MCS 锁加锁、解锁大致流程:加锁维护队列的尾节点,通过 CAS 操作将线程入队,若前置节点为空,直接获取锁,若前置节点不为空,将前置节点的 next 节点指向当前节点,轮询当前节点 lock 状态。(初始值为 false 未获取锁,true 为获取到锁)解锁当前节点释放锁,唤醒队列中的后置节点,即将后置节点的 lock 置为 true。示例代码interface Lock {
void lock();
void unlock() throws Exception;
}
class MSCLock implements Lock {
/**
* tailNode 尾节点原子操作保证线程安全
*/
final AtomicReference<Node> tailNode = new AtomicReference<>();
private final ThreadLocal<Node> currentNodeLocal = new ThreadLocal<>();
private static class Node {
/**
* 后驱节点
* volatile 保证 nextNode 引用的可见性
*/
private volatile Node nextNode;
/**
* 当前节点状态
* volatile 保证对后置线程的可见性
*/
private volatile Boolean lockState;
public Node(Boolean lockState) {
this.lockState = lockState;
}
}
@Override
public void lock() {
Node currentNode = new Node(true);
currentNodeLocal.set(currentNode);
Node preNode = tailNode.getAndSet(currentNode);
// 首节点直接获取锁
if (preNode == null) {
currentNode.lockState = true;
} else {
preNode.nextNode = currentNode;
// 自旋检测当前节点状态
while (!currentNode.lockState) {
System.out.println(Thread.currentThread().getName() + " 自旋等待获取锁");
}
}
System.out.println(Thread.currentThread().getName() + " 获取锁成功");
}
@Override
public void unlock() {
Node currentNode = currentNodeLocal.get();
Node nextNode = currentNode.nextNode;
// 若无等待线程 尝试将tailNode置为 null
if (nextNode == null) {
if (tailNode.compareAndSet(currentNode, null)) {
System.out.println(Thread.currentThread().getName() + " 锁释放成功");
return;
} else {
nextNode = currentNode.nextNode;
}
}
// 清除线程 ThreadLocal 本次锁信息 避免拿到已经释放的锁信息
currentNodeLocal.remove();
// 唤醒下一个等待线程
nextNode.lockState = true;
}
}
带鱼
Java 多线程 : 并行处理 Fork Join
一 . Fork / Join 入门什么是 Fork / Join 该框架是一个工具 , 通过分而治之的方式尝试将所有可用的处理器内核使用起来帮助加速并行处理fork : 递归地将任务分解为较小的独立子任务 , 直到它们足够简单以便异步执行join : 将所有子任务的结果递归的连接成单个结果原理简述 :Fork / Join 的执行是先把一个大任务分解(fork)成许多个独立的小任务,然后起多线程并行去处理这些小任务。处理完得到结果后再进行合并(join)就得到我们的最终结果。Fork / Join 使用的算法为 work-stealing(工作窃取)该算法会把分解的小任务放在多个双端队列中,而线程在队列的头和尾部都可获取任务。当有线程把当前负责队列的任务处理完之后,它还可以从那些还没有处理完的队列的尾部窃取任务来处理Fork / Join 线程池 :ForkJoinPool : 用于管理 ForkJoinWorkerThread 类型的工作线程实现了 ExecutorService接口 的多线程处理器把一个大的任务划分为若干个小的任务并发执行,充分利用可用的资源,进而提高应用的执行效率参考 @ blog.csdn.net/tyrroo/arti…二 . 说一说 RecursiveTaskRecursiveTask 是一种 ForkJoinTask 的递归实现 , 例如可以用于计算斐波那契数列 : class Fibonacci extends RecursiveTask<Integer> {
final int n;
Fibonacci(int n) { this.n = n; }
Integer compute() {
if (n <= 1)
return n;
Fibonacci f1 = new Fibonacci(n - 1);
f1.fork();
Fibonacci f2 = new Fibonacci(n - 2);
return f2.compute() + f1.join();
}
}
RecursiveTask 继承了 ForkJoinTask 接口 ,其内部有几个主要的方法:
// Node 1 : 返回结果 , 存放最终结果
V result;
// Node 2 : 抽象方法 compute , 用于计算最终结果
protected abstract V compute();
// Node 3 : 获取最终结果
public final V getRawResult() {
return result;
}
// Node 4 : 最终执行方法 , 这里是需要调用具体实现类compute
protected final boolean exec() {
result = compute();
return true;
}
常见使用方式:@
public class ForkJoinPoolService extends RecursiveTask<Integer> {
private static final int THRESHOLD = 2; //阀值
private int start;
private int end;
public ForkJoinPoolService(Integer start, Integer end) {
this.start = start;
this.end = end;
}
@Override
protected Integer compute() {
int sum = 0;
boolean canCompute = (end - start) <= THRESHOLD;
if (canCompute) {
for (int i = start; i <= end; i++) {
sum += i;
}
} else {
int middle = (start + end) / 2;
ForkJoinPoolService leftTask = new ForkJoinPoolService(start, middle);
ForkJoinPoolService rightTask = new ForkJoinPoolService(middle + 1, end);
//执行子任务
leftTask.fork();
rightTask.fork();
//等待子任务执行完,并得到其结果
Integer rightResult = rightTask.join();
Integer leftResult = leftTask.join();
//合并子任务
sum = leftResult + rightResult;
}
return sum;
}
}
三 . Fork Join 用法// 前提 : 需要继承 RecursiveTask<Integer> 类 , 且实现 compute 方法
public class ForkJoinPoolReferenceService extends RecursiveTask<Integer> {
private File file;
private Integer salt;
public ForkJoinPoolReferenceService(File file, Integer salt) {
this.file = file;
this.salt = salt;
}
@Override
protected Integer compute() {
return ForkFileUtils.read(file, salt);
}
}
// 方式一 : Fork Join 方式
ForkJoinPoolReferenceService rt = new ForkJoinPoolReferenceService(files.get(0), i);
rt.fork();
result = result + rt.join();
// 方式二 : submit 方式
ForkJoinPool forkJoinPool = new ForkJoinPool();
ForkJoinTask<Integer> forkJoinTask = forkJoinPool.submit(rt);
result = result + forkJoinTask.get();
四. ForkJoinTask 用法ForkJoinTask:代表fork/join里面任务类型,我们一般用它的两个子类RecursiveTask、RecursiveAction。这两个区别在于RecursiveTask任务是有返回值,RecursiveAction没有返回值。任务的处理逻辑包括任务的切分都集中在compute()方法里面。五 . ForkJoinPool 线程池作用 : ForkJoinPool为来自非ForkJoinTask客户端的提交提供入口点,以及管理和监视操作,最原始的任务都要交给它才能处理 .主要功能包括 :负责控制整个fork/join有多少个workerThread,workerThread的创建,激活都是由它来掌控。负责workQueue队列的创建和分配,每当创建一个workerThread,它负责分配相应的workQueue。把接到的活都交给workerThread去处理,它可以说是整个frok/join的容器。备注 :ForkJoinPool不同于其他类型的ExecutorService,主要是因为它使用了窃取工作:池中的所有线程都试图找到并执行提交到池中的任务和/或其他活动任务创建的任务(如果没有工作,最终会阻塞等待工作)。ForkJoinPool 基础用法ForkJoinPool forkJoinPool = new ForkJoinPool();
// 这是一个继承 ForkJoinTask 的类
ForkJoinPoolService countTask = new ForkJoinPoolService(1, 200);
ForkJoinTask<Integer> forkJoinTask = forkJoinPool.submit(countTask);
System.out.println(forkJoinTask.get());
当大多数任务衍生出其他子任务时,以及当许多小任务从外部客户端提交到池时,这使得高效处理成为可能。特别是当在构造函数中将asyncMode设置为true时,ForkJoinPool s也可能适合用于从未连接的事件风格任务。ForkJoin 前知识点 : ForkJoinWorkerThreadForkJoinWorkerThread 为 fork/join里面真正干活的"工人",本质是一个线程 ,其里面有一个ForkJoinPool.WorkQueue的队列存放着它要干的活,接活之前它要向ForkJoinPool注册(registerWorker),拿到相应的workQueue。然后就从workQueue里面拿任务出来处理。ForkJoinWorkerThread 依附于ForkJoinPool而存活,如果ForkJoinPool的销毁了,它也会跟着结束。
// Node 1 : 内部属性 , 一个是当前工作线程池
final ForkJoinPool pool; // the pool this thread works in
final ForkJoinPool.WorkQueue workQueue; // work-stealing mechanics
// Node 2 : WorkQueue 对象 , WorkQueue 是 ForkJoinPool 内部类
// 1 支持工作窃取和外部任务提交的队列
// 2 可以避免多个工作队列实例或多个队列数组共享缓存线
// 3 双端队列
static final class WorkQueue {
// TODO : 后续有必要会深入了解该类
}
// Node 3 : run 方法
Step 1 : 判断workQueue 是否为空
Step 2 : pool.runWorker(workQueue) 将 workQueue 加入 pool 池
// Node 4 : InnocuousForkJoinWorkerThread
private static final ThreadGroup innocuousThreadGroup =createThreadGroup();
?- 正如之前猜测的 , 线程组果然用在了这里
ForkJoinPool 代码深入
// Node 1 : 内部构建线程方式
static final class DefaultForkJoinWorkerThreadFactory
M- ForkJoinWorkerThread newThread(ForkJoinPool pool)
?- 通过 new ForkJoinWorkerThread(pool) 返回新 Thread
// Node 2 : 内部属性
ForkJoinWorkerThreadFactory defaultForkJoinWorkerThreadFactory;
?- 用于创建 ForkJoinWorkerThread ,可以被重写
RuntimePermission modifyThreadPermission;
?- 该对象用于控制启用和杀掉线程的权限
static final ForkJoinPool common;
?- 公共静态池
volatile long ctl;
?- 主要用于判断状态
// Node 3 : 默认属性
private static final long IDLE_TIMEOUT = 2000L * 1000L * 1000L;
?- 线程的初始超时值(以纳秒为单位)
private static final long TIMEOUT_SLOP = 20L * 1000L * 1000L;
?- 空闲超时时间
private static final int DEFAULT_COMMON_MAX_SPARES = 256
?- 在静态初始化期间commonMaxSpares的初始值
private static final int SPINS = 0
?- 在阻塞前自旋等待的次数
// Node 4 : 主要方法
> tryAddWorker :
主要1 :add = U.compareAndSwapLong(this, CTL, c, nc); CAS 判断状态
主要2 : createWorker(); 创建工作
> WorkQueue registerWorker(ForkJoinWorkerThread wt)
主要1 : WorkQueue w = new WorkQueue(this, wt);
主要2 : 推入 workQueues
> runWorker : 运行一个 worker
重点1 : t = scan(w, r) : 扫描并试图窃取一个顶级任务
重点2 : w.runTask(t); : 执行给定的任务和任何剩余的本地任务
> scan : 窃取逻辑
// TODO
> <T> ForkJoinTask<T> submit(ForkJoinTask<T> task)
// Node 5 :构造函数
public ForkJoinPool(int parallelism,
ForkJoinWorkerThreadFactory factory,
UncaughtExceptionHandler handler,
boolean asyncMode)
- parallelism : 可并行级别 , 即可并行运行的线程数量
- factory : 线程工厂
- handler : 异常捕获处理器。当执行的任务中出现异常,并从任务中被抛出时,就会被handler捕获
- asyncMode : 工作模式类型 , 存在于队列中的待执行任务,即可以使用先进先出的工作模式,也可以使用后进先出的工作模式
总结总体来说还是不够深入 , 包括其中的性能 , invoke 实际上都还没有测试 , 实际上 ForkJoinPool 源码深入都不到一成 , 但是看源码看的有点头疼了 ,先这样了 , 后续会尽力把他完善清楚
带鱼
Java 多线程 : JUC 并发工具原理
一 . 前言趁着有空 , 赶紧把之前欠的债还上 . 这是多线程一阶段计划的最后一篇 , 后续多线程会转入修订和深入阶段 . 彻底吃透多线程.二. 工具介绍之前说 AQS 的时候曾经提到过这几个类 , 这几个类有一些各自的特点 , 很符合特定的场景 , 之前在生产上用的还挺舒服.我们一般使用的并发工具有四种 :CyclicBarrier : 放学一起走允许一组线程互相等待,直到到达某个公共屏障点 (common barrier point)让一组线程到达一个屏障时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活CountDownLatch : 等人到齐了就触发在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待用给定的计数 初始化 CountDownLatch。由于调用了 countDown() 方法,所以在当前计数到达零之前,await 方法会一直受阻塞。之后,会释放所有等待的线程,await 的所有后续调用都将立即返回。这种现象只出现一次——计数无法被重置。CountDownLatch是通过一个计数器来实现的,当我们在new 一个CountDownLatch对象的时候需要带入该计数器值,该值就表示了线程的数量。每当一个线程完成自己的任务后,计数器的值就会减1。当计数器的值变为0时,就表示所有的线程均已经完成了任务Semaphore信号量Semaphore是一个控制访问多个共享资源的计数器,和CountDownLatch一样,其本质上是一个“共享锁”。Exchanger可以在对中对元素进行配对和交换的线程的同步点每个线程将条目上的某个方法呈现给 exchange 方法,与伙伴线程进行匹配,并且在返回时接收其伙伴的对象 , Exchanger 可能被视为 SynchronousQueue 的双向形式三 .原理解析3 .1 CyclicBarrier作用 : 它允许一组线程互相等待,直到到达某个公共屏障点 (Common Barrier Point)。在涉及一组固定大小的线程的程序中,这些线程必须不时地互相等待,此时 CyclicBarrier 很有用。因为该 Barrier 在释放等待线程后可以重用,所以称它为循环( Cyclic ) 的 屏障( Barrier ) 。内部原理 : 内部使用重入锁ReentrantLock 和 Condition构造函数 :CyclicBarrier(int parties): 创建一个新的 CyclicBarrier,它将在给定数量的参与者(线程)处于等待状态时启动, 但它不会在启动 barrier 时执行预定义的操作。CyclicBarrier(int parties, Runnable barrierAction) : 创建一个新的 CyclicBarrier,它将在给定数量的参与者(线程)处于等待状态时启动, 并在启动 barrier 时执行给定的屏障操作,该操作由最后一个进入 barrier 的线程执行。使用变量 :parties 变量 : 表示拦截线程的总数量。count 变量 : 表示拦截线程的剩余需要数量。barrierAction 变量 : 为 CyclicBarrier 接收的 Runnable 命令,用于在线程到达屏障时,优先执行 barrierAction ,用于处理更加复杂的业务场景。generation 变量 : 表示 CyclicBarrier 的更新换代// 常用方法 :
M- await : 等待状态
M- await(long timeout, TimeUnit unit) : 等待超时
M- dowait
- 该方法第一步会试着获取锁
- 如果分代已经损坏,抛出异常
- 如果线程中断,终止CyclicBarrier
- 进来线程 ,--count
- count == 0 表示所有线程均已到位,触发Runnable任务
- 唤醒所有等待线程,并更新generation
> 跳出等待状态的方法
- 最后一个线程到达,即index == 0
- 超出了指定时间(超时等待)
- 其他的某个线程中断当前线程
- 其他的某个线程中断另一个等待的线程
- 其他的某个线程在等待barrier超时
- 其他的某个线程在此barrier调用reset()方法。reset()方法用于将屏障重置为初始状态。
SC- Generation : 描述了 CyclicBarrier 的更新换代。
- 在CyclicBarrier中,同一批线程属于同一代。
- 当有 parties 个线程全部到达 barrier 时,generation 就会被更新换代。
- 其中 broken 属性,标识该当前 CyclicBarrier 是否已经处于中断状态
M- breakBarrier : 终止所有的线程
M- nextGeneration : 更新换代操作
- 1. 唤醒所有线程。
- 2. 重置 count 。
- 3. 重置 generation 。
M- reset : 重置 barrier 到初始化状态
M- getNumberWaiting : 获得等待的线程数
M- 判断 CyclicBarrier 是否处于中断
使用案例 :Gitee CyclicBarrier 使用问题补充 :
// 问题一 : 拦截的核心
1. 传入总得 Count 数
2. 每次进来都会 --count , 同时判断 count ==0
3. 如果不为 0 ,当前线程就会阻塞
// 问题二 : 涉及源码
private final ReentrantLock lock = new ReentrantLock();
private final Condition trip = lock.newCondition();
3.2 CountDownLatch在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待用给定的计数 初始化 CountDownLatch。由于调用了 countDown() 方法,所以在当前计数到达零之前,await 方法会一直受阻塞。之后,会释放所有等待的线程,await 的所有后续调用都将立即返回。这种现象只出现一次, 计数无法被重置。如果需要重置计数,请考虑使用 CyclicBarrier。CountDownLatch是通过一个计数器来实现的,当我们在new 一个CountDownLatch对象的时候需要带入该计数器值,该值就表示了线程的数量。每当一个线程完成自己的任务后,计数器的值就会减1。当计数器的值变为0时,就表示所有的线程均已经完成了任务// 内部主要方法
> CountDownLatch内部依赖Sync实现,而Sync继承AQS
> sync :
: tryAcquireShared 获取同步状态
: tryReleaseShared 释放同步状态
> await() :
使当前线程在锁存器倒计数至零之前一直等待,除非线程被中断
: sync.acquireSharedInterruptibly(1);
: 内部使用AQS的acquireSharedInterruptibly(int arg)
> getState()
: 获取同步状态,其值等于计数器的值
: 从这里我们可以看到如果计数器值不等于0,则会调用doAcquireSharedInterruptibly(int arg)
> doAcquireSharedInterruptibly
: 自旋方法会尝试一直去获取同步状态
> countDown
: CountDownLatch提供countDown() 方法递减锁存器的计数,如果计数到达零,则释放所有等待的线程
: 内部调用AQS的releaseShared(int arg)方法来释放共享锁同步状态
: tryReleaseShared(int arg)方法被CountDownLatch的内部类Sync重写
参考案例Gitee CountDownLatch 使用总结 CountDownLatch 内部通过共享锁实现。在创建CountDownLatch实例时,需要传递一个int型的参数:count,该参数为计数器的初始值,也可以理解为该共享锁可以获取的总次数。当某个线程调用await()方法,程序首先判断count的值是否为0,如果不会0的话则会一直等待直到为0为止 (PS : 可以多个线程都调用 await)当其他线程调用countDown()方法时,则执行释放共享锁状态,使count值 – 1 (PS :countDown 并不会阻塞)当在创建CountDownLatch时初始化的count参数,必须要有count线程调用countDown方法才会使计数器count等于0,锁才会释放,前面等待的线程才会继续运行。注意CountDownLatch不能回滚重置3 .3 Semaphore基础点 信号量Semaphore是一个控制访问多个共享资源的计数器,和CountDownLatch一样,其本质上是一个“共享锁”。从概念上讲,信号量维护了一个许可集。如有必要,在许可可用前会阻塞每一个 acquire(),然后再获取该许可。每个 release() 添加一个许可,从而可能释放一个正在阻塞的获取者。Semaphore 通常用于限制可以访问某些资源(物理或逻辑的)的线程数目当一个线程想要访问某个共享资源时,它必须要先获取Semaphore,当Semaphore >0时,获取该资源并使Semaphore – 1。如果Semaphore值 = 0,则表示全部的共享资源已经被其他线程全部占用,线程必须要等待其他线程释放资源。当线程释放资源时,Semaphore则+1实现细节 Semaphore提供了两个构造函数:Semaphore(int permits) :创建具有给定的许可数和非公平的公平设置的 Semaphore。Semaphore(int permits, boolean fair) :创建具有给定的许可数和给定的公平设置的 Semaphore。Semaphore默认选择非公平锁。当信号量Semaphore = 1 时,它可以当作互斥锁使用。其中0、1就相当于它的状态,当=1时表示其他线程可以获取,当=0时,排他,即其他线程必须要等待。//------ 信号量获取
> acquire()方法来获取一个许可
: 内部调用AQS的acquireSharedInterruptibly(int arg),该方法以共享模式获取同步状态
> 公平
: 判断该线程是否位于CLH队列的列头
: 获取当前的信号量许可
: 设置“获得acquires个信号量许可之后,剩余的信号量许可数”
: CAS设置信号量
> 非公平
: 不需要判断当前线程是否位于CLH同步队列列头
3 .4 Exchanger可以在对中对元素进行配对和交换的线程的同步点每个线程将条目上的某个方法呈现给 exchange 方法,与伙伴线程进行匹配,并且在返回时接收其伙伴的对象 , Exchanger 可能被视为 SynchronousQueue 的双向形式Exchanger,它允许在并发任务之间交换数据: 当两个线程都到达同步点时,他们交换数据结构,因此第一个线程的数据结构进入到第二个线程中,第二个线程的数据结构进入到第一个线程中TODO : Exchanger 的源代码比较绕 ,而且这个组件使用场景并不多 , 所以先留个坑 , 以后项目上真的有场景了再实际上分析一下3.5 并发工具使用@ github.com/black-ant/c…补充 :# CountDownLatch 和 CyclicBarrier 如何理解 ?CyclicBarrier : 小学生去郊游 , 老师下车时统计人数 ,人数到齐了才能一起参观CountDownLatch : 幼儿园老师送孩子(ChildThread)放学 , 走一个记一个数 ,当所有的学生放学后 , 老师(BossThread)下班// 核心解释 :
CyclicBarrier 就是一堵墙 , 人数到了所有线程才能一起越过墙
CountDownLatch 只是一个计数器 , 数目到了主线程才能执行
// 其他要点 :
CyclicBarrier 可以重置计数 , CountDownLatch 不可以
带鱼
【多线程系列】基于 AQS 实现的同步器源码精讲(ReentrantLock、ReentrantRe
回顾前面我们讲解 JUC 中两个核心的基础工具 CAS 和 AQS,下面这篇文章我们聊聊 JUC 是如何使用这两大核心组件实现同步器【多线程系列】高效的 CAS (Compare and Swap)【多线程系列】CAS 常见的两个升级版本 CLH、MCS【多线程系列】JUC 中的另一重要大杀器 AQS 抽象队列同步器导读了解如何基于 AQS 实现自己的同步器ReentrantLock、ReentrantReadWriteLock 实现原理基于 AQS 实现的同步器JUC 并发包中一部分同步器都是基于 AQS 实现,前面介绍 AQS 时提到过模版方法,而同步器的不同特性主要通过重写这些模板方法实现://独占方式。尝试获取资源,成功则返回true,失败则返回false。
protected boolean tryAcquire(int)
//独占方式。尝试释放资源,成功则返回true,失败则返回false。
protected boolean tryRelease(int)
//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
protected int tryAcquireShared(int)
//共享方式。尝试释放资源,成功则返回true,失败则返回false。
protected boolean tryReleaseShared(int)
//该线程是否正在独占资源。只有用到condition才需要去实现它。
protected boolean isHeldExclusively()
下面是基于 AQS 同步器的主要流程,标注了实现公平锁、非公平锁、可重入、共享等特性的拓展点:ReentrantLockReentrantLock 是基于 AQS 实现的可重入式独占锁,支持公平锁和非公平锁两种模式。简单使用示例public class MainTest {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
new Thread(() -> {
lock.lock();
System.out.println("线程1获取锁");
// 条件等待 释放锁
System.out.println("线程1条件等待、释放锁");
condition.awaitUninterruptibly();
System.out.println("线程1重新获取锁");
lock.unlock();
System.out.println("线程1释放锁");
}).start();
new Thread(() -> {
lock.lock();
System.out.println("线程2获取锁");
// 条件等待 释放锁
System.out.println("线程唤醒条件队列的的一个锁");
condition.signal();
lock.unlock();
System.out.println("线程2释放锁");
}).start();
}
}
// 运行结果:
线程1获取锁
线程1条件等待、释放锁
线程2获取锁
线程唤醒条件队列的的一个锁
线程2释放锁
线程1重新获取锁
线程1释放锁
公平锁和非公平锁ReentrantLock 通过继承 AQS 实现 FairSync、NonfairSync 两种模式,默认提供非公平锁(整体性能更高): /**
* Creates an instance of {@code ReentrantLock}.
* This is equivalent to using {@code ReentrantLock(false)}.
*/
public ReentrantLock() {
sync = new ReentrantLock.NonfairSync();
}
/**
* Creates an instance of {@code ReentrantLock} with the
* given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantLock(boolean fair) {
sync = fair ? new ReentrantLock.FairSync() : new ReentrantLock.NonfairSync();
}
公平锁和非公平锁具体实现锁获取/**
* Sync object for fair locks
*/
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 公平锁:当互斥资源未被占用,需要先判断等待队列中是否有线程,若有,先唤醒等待队列中线程
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
} else if (current == getExclusiveOwnerThread()) {
// 支持重入:当互斥资源被占用时,判断持有线程是否当前线程,若为当前线程,重入次数 +1
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
/* 判断等待队列中是否存在等待中的线程 */
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
// 判断是否存在先等待的线程 具体分析如下
return h != t && ((s = h.next) == null || s.thread != Thread.currentThread());
}
hasQueuedPredecessors() 图示展示/**
* Sync object for non-fair locks
*/
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
// 非公平锁多次 CAS 直接尝试竞争锁 尽可能避免阻塞带来的上下文切换
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
/**
* Performs non-fair tryLock. tryAcquire is implemented in
* subclasses, but both need nonfair try for trylock method.
*/
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 非公平锁:直接竞争锁,不管等待是否存在等待节点
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
} else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
锁释放 protected final boolean tryRelease(int releases) {
int c = getState() - releases;
// 获取独占锁的线程才可以释放线程
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// state == 0 表示所有锁已释放
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
可重入当互斥资源被占用时,判断持有线程是否当前线程,若为当前线程,重入次数 +1 /**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
} else if (current == getExclusiveOwnerThread()) {
// 支持重入:当互斥资源被占用时,判断持有线程是否当前线程,若为当前线程,重入次数 +1
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
ReentrantReadWriteLockReentrantLock 是独占式锁,当互斥资源读多写少时,性能较差;ReentrantReadWriteLock 通过 AQS 实现 ReadLock 和 WriteLock,实现了读写分离,从而达到读写互斥、读读不互斥,提高了线程并发性能。ReentrantReadWriteLock 同样支持公平锁和非公平锁(默认非公平锁),以及可重入的特性。支持锁降级,但不支持锁升级。锁降级指当一个线程获取写锁,再获取读锁,此时再释放写锁,这个过程称为锁降级。
锁升级指一个线程获取读锁,然后获取写锁(获取失败:获取读锁后无法同时再去获取写锁)。
使用示例// 不支持锁升级验证
public class MainTest {
public static void main(String[] args) throws InterruptedException {
ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();
new Thread(()->{
readLock.lock();
System.out.println("子线程获取读锁");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
readLock.unlock();
System.out.println("子线程释放读锁");
}).start();
Thread.sleep(2000);
System.out.println("主线程开始获取写锁");
writeLock.lock();
System.out.println("主线程获取写锁成功");
writeLock.unlock();
System.out.println("主线程释放写锁成功");
}
}
// 输出
子线程获取读锁
主线程开始获取写锁
子线程释放读锁
主线程获取写锁成功
主线程释放写锁成功
如何记录读锁写锁的重入次数我们都知道 AQS 使用 state 字段记录锁的重入次数,而 ReentrantReadWriteLock 创意性的将 state 字段的高 16 位用于表示读状态,低 16 位表示写状态,把两个写操作合并到一个 CAS 操作。ReadLock获取锁 protected final int tryAcquireShared(int unused) {
/*
* Walkthrough:
* 1. If write lock held by another thread, fail.
* 2. Otherwise, this thread is eligible for
* lock wrt state, so ask if it should block
* because of queue policy. If not, try
* to grant by CASing state and updating count.
* Note that step does not check for reentrant
* acquires, which is postponed to full version
* to avoid having to check hold count in
* the more typical non-reentrant case.
* 3. If step 2 fails either because thread
* apparently not eligible or CAS fails or count
* saturated, chain to version with full retry loop.
*/
Thread current = Thread.currentThread();
int c = getState();
// 存在写锁且不是当前线程
if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current) return -1;
// 当前线程持有写锁或仅有读锁或无锁
int r = sharedCount(c);
// 根据公平锁和非公平锁重写 readerShouldBlock()
if (!readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)) {
// 如果是第一次获取 初始化 firstReader、firstReaderHoldCount 不是第一次获取 对 readHolds 对应线程计数+1
if (r == 0) {
// 第一次添加读锁
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
// firstReader 为当前线程
firstReaderHoldCount++;
} else {
// 否则更新 readHolds 对应线程读锁计数
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current)) cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0) readHolds.set(rh);
rh.count++;
}
return 1;
}
// 自旋尝试获取读锁(只要满足获取读锁条件)
return fullTryAcquireShared(current);
}
final int fullTryAcquireShared(Thread current) {
/*
* This code is in part redundant with that in
* tryAcquireShared but is simpler overall by not
* complicating tryAcquireShared with interactions between
* retries and lazily reading hold counts.
*/
HoldCounter rh = null;
// 自旋 不挂起线程
for (; ; ) {
int c = getState();
if (exclusiveCount(c) != 0) {
// 非当前线程获取到写锁 获取失败
if (getExclusiveOwnerThread() != current)
return -1;
// else we hold the exclusive lock; blocking here
// would cause deadlock.
} else if (readerShouldBlock()) {
// 走到这里说明没有写锁被占有 判断是否存在重入
// Make sure we're not acquiring read lock reentrantly
// 当前线程为 firstReader 走下面 CAS
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
} else {
if (rh == null) {
rh = cachedHoldCounter;
// cachedHoldCounter 没有缓存或缓存的不是当前线程
if (rh == null || rh.tid != getThreadId(current)) {
rh = readHolds.get();
// 说明上一行是初始化 移除上面产生的初始化
if (rh.count == 0)
readHolds.remove();
}
}
if (rh.count == 0)
return -1;
}
}
// 是否已经达到读锁获取次数上限
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// CAS 获取锁
if (compareAndSetState(c, c + SHARED_UNIT)) {
// 读锁初始化和计数
if (sharedCount(c) == 0) {
// 第一次添加读锁
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
// firstReader 为当前线程
firstReaderHoldCount++;
} else {
// 否则更新 readHolds 对应线程读锁计数
if (rh == null)
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
cachedHoldCounter = rh; // cache for release
}
return 1;
}
}
}
释放锁 protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
// 根据共享锁次数来设置 firstReader 不存在并发修改问题
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;
} else {
Sync.HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
int count = rh.count;
if (count <= 1) {
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
--rh.count;
}
// CAS 更新 state
for (; ; ) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
// Releasing the read lock has no effect on readers,
// but it may allow waiting writers to proceed if
// both read and write locks are now free.
return nextc == 0;
}
}
WriteLock获取锁 protected final boolean tryAcquire(int acquires) {
/*
* Walkthrough:
* 1. If read count nonzero or write count nonzero
* and owner is a different thread, fail.
* 2. If count would saturate, fail. (This can only
* happen if count is already nonzero.)
* 3. Otherwise, this thread is eligible for lock if
* it is either a reentrant acquire or
* queue policy allows it. If so, update state
* and set owner.
*/
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
// 存在读锁或写锁
if (c != 0) {
// (Note: if c != 0 and w == 0 then shared count != 0)
// 存在读锁或存在写锁但不是当前线程持有获取失败
if (w == 0 || current != getExclusiveOwnerThread()) return false;
// 获取锁是否超过上限
if (w + exclusiveCount(acquires) > MAX_COUNT) throw new Error("Maximum lock count exceeded");
// Reentrant acquire
// 走到这里说明当前线程持有写锁 重入
setState(c + acquires);
return true;
}
// 不存在锁 判断公平非公平阻塞策略 || 进行 CAS 尝试获取锁
if (writerShouldBlock() || !compareAndSetState(c, c + acquires)) return false;
setExclusiveOwnerThread(current);
return true;
}
释放锁 protected final boolean tryRelease(int releases) {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
int nextc = getState() - releases;
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}
带鱼
Java 多线程 : 迟来的 Future
一 . Future 是什么1.3 Future Task 简述作用 : future 可以用于异步获取多线程任务结果 , Callable 用于产生结果,Future 用于获取结果流程 : 流程类似于叫好等餐 , 等餐是花费时间的过程,但是不妨碍我们叫号当 Future 进行 submit 开始 , 业务处理已经在多线程中开始 , 而 Get 即从多线程中获取数据当 Get 获取时业务还未处理完 , 当前线程会阻塞 , 直到业务处理完成 . 所以需要注意 future 的任务安排使用 future 会有以下效果:1 启动多线程任务2 处理其他事情3 收集多线程任务结果Future 对应的方法 :cancel(boolean) : 取消操作get() : 获取结果get(long,TimeUtil) : 指定时间获取isCancelled() : 该任务是否在完成之前被取消isDone() :判断是否有结果Future 接口的作用就是先生成一个 Future 对象 ,将具体的运行放入future 对象中 ,最终通过future 对象的 get 方法来获取最终的结果1.2 Future TaskFutureTask 表示一个可以取消的异步运算 ,提供了Future 完整的流程它有启动和取消运算、查询运算是否完成和取回运算结果等方法。只有当运算完成的时候结果才能取回,如果运算尚未完成 get 方法将会阻塞我们关注一下以下源码细节点 :
// Node 1 : 实现了 RunnableFuture
// ------------------------------------
// Node 2 : 提供了7种状态
private static final int NEW = 0; // 新建
private static final int COMPLETING = 1; // 完成
private static final int NORMAL = 2; // 正常
private static final int EXCEPTIONAL = 3; // 异常
private static final int CANCELLED = 4; // 取消
private static final int INTERRUPTING = 5; // 中断(中)
private static final int INTERRUPTED = 6; // 打断
// 过程的流转 :
* NEW -> COMPLETING -> NORMAL
* NEW -> COMPLETING -> EXCEPTIONAL
* NEW -> CANCELLED
* NEW -> INTERRUPTING -> INTERRUPTED
// ------------------------------------
// Node 3 : 内部属性
// 底层Callable 对象
private Callable<V> callable;
// 输出对象
private Object outcome;
// 运行线程
private volatile Thread runner;
// 等待线程的Treiber堆栈
private volatile WaitNode waiters;
// ------------------------------------
// Node 4 : 内部方法
// 方法一 : 获取参数
V report(int s) // 为已完成的任务返回结果或抛出异常
1. Object x = outcome;
2. return (V)x;
// 注意 : 状态为 CANCELLED 时会抛出异常 CancellationException
// 方法二 : 取消
public boolean cancel(boolean mayInterruptIfRunning) // 取消
1. UNSAFE.compareAndSwapInt(this, stateOffset, NEW, mayInterruptIfRunning ? INTERRUPTING : CANCELLED)))
// 这里神奇的做了一个CAS操作, 判断当前的状态
2. Thread t = runner; + t.interrupt();
// 打断线程
3. UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED);
// CAS 方式修改状态
// 方法三 : get 类型
public V get() throws
public V get(long timeout, TimeUnit unit)
1. if (s <= COMPLETING &&(s = awaitDone(true, unit.toNanos(timeout))) <= COMPLETING)
// 核心代码就是关注是否完成
// 方法四 : run
Step 1 : CAS 操作状态
Step 2 : 准备一个 Callable<V> c = callable;
Step 3 : state == NEW 后 , result = c.call();
// 此时阻塞等待
Step 4 : 设置结果 : set(result);
Step 5 : 当然是修改状态啦
Over !!!
// 方法五 : runAndReset
// 在不设置结果的情况下执行计算,然后将这个future重置为初始状态 (其实主要是结尾修改了状态)
核心 : return ran && s == NEW;
// 方法五 : finishCompletion
// 删除并通知所有等待的线程,调用done(),并使callable为空
// 2个for 循环保证执行
for (WaitNode q; (q = waiters) != null;) {
// CAS 保证操作的准确性
if (UNSAFE.compareAndSwapObject(this, waitersOffset, q, null)) {
for (;;) {
Thread t = q.thread;
if (t != null) {
q.thread = null;
// 提供许可
LockSupport.unpark(t);
}
WaitNode next = q.next;
if (next == null)
break;
q.next = null; // unlink to help gc
q = next;
}
break;
}
}
二 . Future 使用Future 使用其实比较简单 , 发起等待即可 , 但是注意Future 是会阻塞主线程的public class FutureService extends AbstractService implements ApplicationRunner, Callable<String> {
private Logger logger = LoggerFactory.getLogger(this.getClass());
private static Long startTime;
private static Long endTime;
private String salt;
private Integer sleepNum;
public FutureService() {
}
public FutureService(String salt, Integer sleepNum) {
this.salt = salt;
this.sleepNum = sleepNum;
}
@Override
public String call() throws Exception {
logger.info("------> 业务逻辑开始执行 :{} <-------", salt);
StringBuffer sb = new StringBuffer();
for (int i = 0; i < sleepNum; i++) {
sb.append(salt);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
}
endTime = System.currentTimeMillis();
logger.info("------> {} - 业务执行完成 :{} <-------", salt, sb.toString());
getTime(startTime, endTime);
return sb.toString();
}
@Override
public void run(ApplicationArguments args) throws Exception {
logger.info("------> 创建一个初始连接池 <-------");
ExecutorService executor = Executors.newFixedThreadPool(3);
logger.info("------> 开始业务一 future - a : <-------");
FutureTask<String> future = new FutureTask<String>(new FutureService("a", 10));
startTime = System.currentTimeMillis();
executor.submit(future);
logger.info("------> 业务一请求完毕!主线程执行 <-------");
logger.info("------> 开始业务二 future - b : <-------");
FutureTask<String> future2 = new FutureTask<String>(new FutureService("b", 5));
startTime = System.currentTimeMillis();
executor.submit(future2);
logger.info("------> 业务三请求完毕!主线程执行 <-------");
logger.info("------> 开始业务三 future - c : <-------");
FutureTask<String> future3 = new FutureTask<String>(new FutureService("c", 3));
startTime = System.currentTimeMillis();
executor.submit(future3);
logger.info("------> 业务三请求完毕!主线程执行 <-------");
logger.info("------> future2 数据处理完成:{} <-------", future2.get());
logger.info("------> 2-1 测试主线程是否阻塞 <-------");
logger.info("------> future1 数据处理完成:{} <-------", future.get());
logger.info("------> 1-3 测试主线程是否阻塞 <-------");
logger.info("------> future3 数据处理完成:{} <-------", future3.get());
}
}
19.839 [ main] this is run <-------
19.839 [ main] 开始业务一 future - a : <-------
19.839 [ main] 业务一请求完毕!主线程执行 <-------
19.839 [ main] 开始业务二 future - b : <-------
19.840 [ main] 业务三请求完毕!主线程执行 <-------
19.840 [ main] 开始业务三 future - c : <-------
19.840 [ main] 业务三请求完毕!主线程执行 <-------
19.840 [pool-4-thread-2] 业务逻辑开始执行 :b <-------
19.840 [pool-4-thread-1] 业务逻辑开始执行 :a <-------
19.840 [pool-4-thread-3] 业务逻辑开始执行 :c <-------
22.843 [pool-4-thread-3] c - 业务执行完成 :ccc <-------
22.843 [pool-4-thread-3] time is :3.0 <-------
24.844 [pool-4-thread-2] b - 业务执行完成 :bbbbb <-------
24.844 [pool-4-thread-2] time is :5.0 <-------
24.844 [ main] future2 数据处理完成:bbbbb <-------
24.844 [ main] 2-1 测试主线程是否阻塞 <-------
29.847 [pool-4-thread-1] a - 业务执行完成 :aaaaaaaaaa <-------
29.847 [pool-4-thread-1] time is :10.0 <-------
29.847 [ main] future1 数据处理完成:aaaaaaaaaa <-------
29.847 [ main] 1-3 测试主线程是否阻塞 <-------
29.847 [ main] future3 数据处理完成:ccc <-------
// 流程 :
// Main 线程中 , 哭有看到 abc 时顺序执行的 , 从 submit 开始 , 开始多线程执行 (所以顺序不再固定, 变成了 bac)
// 从多线程里面看 , c > b > a 执行完成
// 当 b 业务完成后 , 因为main 一直阻塞到 futurb.get 的阶段 , 所以B future 获取值 , main 线程遇到 a future 继续阻塞
// 当 a future get 完成后 , c 才能get
// 总结 :
> future submit 多线程执行
> future get 会阻塞主线程等待 , 当 get 时 , 多线程才会把数据提供出来
三 . Future 问答按照线程池最常见的用法 , 我们通过 executor.submit 时会返回一个 Future 对象 ,然后通过 Future 对象获取First : 我们以这个方法去推理ExecutorService executor = Executors.newFixedThreadPool(1);Future future = executor.submit(A Callable Object);问题一 : Future 底层基于什么 ?Step 1 : 当通过 submit 调用的时候 , 底层会调用 : return new FutureTask<T>(runnable, value);Step 2 : 在外层会被提升为父类 RunnableFuture , 在返回的时候又会被提成 Future RunnableFuture<T> ftask = newTaskFor(task, result);总结 : 所以 , 底层的实现类主要可以看成 FutureTask , 而task 实际上可以算 Runnable 的实现类问题二 : Future 怎么运行 ?Future 运行主要基于 run()通过调用 callable.call() 完成如果call执行成功,则通过set方法保存结果 ,将 result 保存到 outcome;如果call执行有异常,则通过setException保存异常;问题三 : future 回调基于什么 -- get(long,TimeUtil) ? 通过调用 report(s) 完成调用问题四 : future 阻塞的方式 ?当判断未完成时 , 会调用 awaitDone 等待 , 具体的逻辑以后分析if (s <= COMPLETING){
s = awaitDone(false, 0L);
}
awaitDone 中主要做了以下几件事 :如果主线程被中断,则抛出中断异常;判断FutureTask当前的state,如果大于COMPLETING,说明任务已经执行完成,则直接返回;如果当前state等于COMPLETING,说明任务已经执行完,这时主线程只需通过yield方法让出cpu资源,等待state变成NORMAL;通过WaitNode类封装当前线程,并通过UNSAFE添加到waiters链表;最终通过LockSupport的park或parkNanos挂起线程;问题五 : future 怎么取消 ?cancel(boolean mayInterruptIfRunning) 中 t.interrupt()并且 UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED); 修改状态问题六 : future 怎么判断取消和结果 isCancelled -- isDone ?return state >= CANCELLED;四 . Future 与 Callable这个需要从 ExecutorService.submit() 来看 , ExecutorService 有2个主要的 submit 方法 , 不论是 Callable , 还是Runnable , 通过返回值就不难发现 ,其最终都变成了一个 Future<T> Future<T> submit(Callable<T> task);
- RunnableFuture<T> ftask = newTaskFor(task);
- return new ForkJoinTask.AdaptedCallable<T>(callable);
- execute(ftask);
<T> Future<T> submit(Runnable task, T result);
- RunnableFuture<T> ftask = newTaskFor(task, result);
- execute(ftask);
// 例如这里可以直接将 Callable 作为参数传进去 :
Future<String> future = executor.submit(createCallable());
public Callable createCallable() {
Callable<Module> call = new Callable<Module>() {
public Module call() throws Exception {
// .....
}
};
return call;
}
五 . 衍生用法 ScheduledFutureTask// ScheduledFutureTask 简介
• time:任务执行时间;
• period:任务周期执行间隔;
• sequenceNumber:自增的任务序号。
// 执行顺序 : 在等待队列里调度不再按照FIFO,而是按照执行时间,谁即将执行,谁就排在前面。
M- getDelay(TimeUnit unit)
M- int compareTo(Delayed other)
// ScheduledFutureTask 主要在 ScheduledThreadPoolExecutor中
// Node 1 : ScheduledThreadPoolExecutor内部类 ScheduledFutureTask
private class ScheduledFutureTask<V>
extends FutureTask<V> implements RunnableScheduledFuture<V>
核心代码参考线程池这篇文档
带鱼
【多线程系列】经典面试题 面试官:使用多线程实现循环顺序打印 123
问题分析多线程循环顺序打印 123?很明显,这个问题是考察我们对线程同步的掌握程度,一想到线程同步,我们可以想到 join、使用锁进行线程同步(synchronized、ReentrantLock等等)等等方式实现,下面我们按照这些思路一一进行实现。实现思路基于 join 实现join 的作用是阻塞当前线程,直到其它线程不再活动,因此我们可以按照这个思路让线程串行执行,顺序打印123。 /**
* 循环次数
*/
private volatile static int loopNum = 5000;
/**
* volatile 保证内存可见性
*/
private volatile static int currentValue = 1;
/**
* Join 可以保证线程顺序执行,可以通过 Join 的方式串行执行
* 创建 5000 个线程,每个线程需要等待前一个线程执行完成,从而实现串行执行
*
* 思考:这里是采用直接创建循环次数的线程数,可以优化为只维护两个线程的方式,即首节点线程执行完成后创建新线程执行
*/
private static void cyclePrintUseJoin() {
Thread preThread = null;
for (int i = 0; i < loopNum; i++) {
preThread = new Thread(new JoinTask(preThread));
preThread.start();
}
}
static class JoinTask implements Runnable {
private final Thread preThread;
public JoinTask(Thread thread) {
this.preThread = thread;
}
@Override
public void run() {
// 如果 preThread 不为空表示不是头节点线程需要等待 preThread 执行完成
if (preThread != null) {
try {
preThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(currentValue);
if (currentValue == 3) {
currentValue = 1;
} else {
currentValue++;
}
}
}
这里留了一个问题给大家思考:这里是采用直接创建循环次数的线程数,可以优化为只维护两个线程的方式,即首节点线程执行完成后再创建新线程基于 synchronized 实现使用 synchronized 同步代码块进行同步,让多线程串行执行 /**
* 循环次数
*/
private volatile static int loopNum = 5000;
/**
* volatile 保证内存可见性
*/
private volatile static int currentValue = 1;
private static final Object lockObj = new Object();
/**
* 使用 synchronized 串行执行代码块
*/
private static void cyclePrintUseSynchronized() {
new Thread(new SynchronizedTask()).start();
new Thread(new SynchronizedTask()).start();
new Thread(new SynchronizedTask()).start();
}
static class SynchronizedTask implements Runnable {
@Override
public void run() {
while (true) {
synchronized (Test1.class) {
if (loopNum < 0) {
return;
}
System.out.println(currentValue);
if (currentValue == 3) {
currentValue = 1;
} else {
currentValue++;
}
loopNum--;
}
}
}
}
升级版 Plus:保证指定数字由指定线程打印上面线程打印 1 2 3 由哪一个线程打印并不能保证,面试官此时会问,如何让指定线程打印对应数字,这时候我们就需要把对应的数字绑定到对应线程,当打印的数字和线程绑定的数字相同时才进行打印。 /**
* 循环次数
*/
private static int loopNum = 5000;
/**
* 当前打印数字
*/
private static int currentValue = 1;
private static final Object lockObj = new Object();
/**
* 使用 synchronized 串行执行代码块
* 且对应线程只处理对应任务
*/
private static void cyclePrintUseSynchronizedPlus() {
new Thread(new SynchronizedTaskPlus(1)).start();
new Thread(new SynchronizedTaskPlus(2)).start();
new Thread(new SynchronizedTaskPlus(3)).start();
}
static class SynchronizedTaskPlus implements Runnable {
private final int target;
public SynchronizedTaskPlus(int target) {
this.target = target;
}
@Override
public void run() {
while (true) {
synchronized (lockObj) {
if (loopNum < 0) {
return;
}
if (currentValue != target) {
try {
// 这里为了避免过多的无效抢占锁,使当前线程 进入等待状态(获取到锁但打印的数字和线程的绑定的数字不一样)
lockObj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(currentValue);
if (currentValue == 3) {
currentValue = 1;
} else {
currentValue++;
}
loopNum--;
// 注:这里需要唤醒所有的线程,因为唤醒的线程可能不是目标线程,导致无效唤醒
lockObj.notifyAll();
}
}
}
}
基于 ReentrantLock 实现ReentrantLock 和 synchronized 实现方式基本一致,但有一些细微的区别:ReentrantLock 只能保证有序性,无法保证可见性,因此需要使用 volatile 修饰变量保证多线程间的可见性。 /**
* 循环次数
*/
private volatile static int loopNum = 5000;
/**
* volatile 保证内存可见性
*/
private volatile static int currentValue = 1;
private static final ReentrantLock reentrantLock = new ReentrantLock();
/**
* 使用 ReentrantLock 串行执行代码块
*/
private static void cyclePrintUseReentrantLock() {
new Thread(new ReentrantLockTask()).start();
new Thread(new ReentrantLockTask()).start();
new Thread(new ReentrantLockTask()).start();
}
static class ReentrantLockTask implements Runnable {
@Override
public void run() {
while (true) {
reentrantLock.lock();
try {
if (loopNum < 0) {
return;
}
System.out.println(currentValue);
if (currentValue == 3) {
currentValue = 1;
} else {
currentValue++;
}
loopNum--;
} finally {
reentrantLock.unlock();
}
}
}
}
升级版 Plus:保证指定数字由指定线程打印实现思路和方式可以参考 synchronized 案例中的实现。大家可以自己动手实现一下。【多线程系列】高效的 CAS (Compare and Swap)【多线程系列】CAS 常见的两个升级版本 CLH、MCS【多线程系列】JUC 中的另一重要大杀器 AQS 抽象队列同步器
带鱼
Java 多线程 : 不一样的锁
一 . Lock 接口Lock 接口是一切的基础 , 它抽象类一种用于控制多个线程对共享资源的访问的工具 .> 提供了以下方法用于抽象整个业务 :void lock()void lockInterruptibly() throws InterruptedException : 打断锁boolean tryLock() : 非阻塞尝试获取一个锁boolean tryLock(long time, TimeUnit unit) throws InterruptedException : 带时间尝试void unlock()Condition newCondition()> Lock 接口提供了区别于隐式监视锁更多的功能 :保证排序不可重入使用死锁检测本身可以作为同步语句中的目标获取锁实例的监视器锁与调用该实例的任何lock()方法没有指定的关系> 内存同步 :成功的锁操作与成功的锁操作具有相同的内存同步效果。成功的解锁操作与成功的解锁操作具有相同的内存同步效果。不成功的锁定和解锁操作,以及可重入的锁定/解锁操作,不需要任何内存同步效果。二 . ReentranLock2.1 ReentranLock 入门ReentranLock 即重入锁 , 表示在单个线程内,这个锁可以反复进入,也就是说,一个线程可以连续两次获得同一把锁 .ReentranLock 比 synchronized 提供更具拓展行的锁操作。它允许更灵活的结构,可以具有完全不同的性质,并且可以支持多个相关类的条件对象。它的优势有:可以使锁更公平。递归无阻塞的同步机制。可以使线程在等待锁的时候响应中断。可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间。可以在不同的范围,以不同的顺序获取和释放锁。它的特点有:可重入互斥锁同时提供公平方式和非公平方式公平锁 : 公平锁的锁获取是有顺序的ReentranLock 基本使用private Lock lock = new ReentrantLock();
public void test() {
lock.lock();
for (int i = 0; i < 5; i++) {
logger.info("------> CurrentThread [{}] , i : [{}] <-------", Thread.currentThread().getName(), i);
}
lock.unlock();
}
2.2 内部重要类2.2.1 SyncSync 是 ReentranLock 的内部抽象类 , 其后续会用来实现两种不同的锁 , 这里先看看Sync 内部做了什么Node 1 : 继承于AbstractQueuedSynchronizer 又名 AQS , 这些大家就知道它了 , Sync 使用aqs state 来表示锁上的持有数abstract static class Sync extends AbstractQueuedSynchronizer
Node 2 : 有一个抽象方法 lock , 后续的公平和非公平会分别实现对应的方法abstract void lock();
// ?- 非公平锁的同步对象
static final class NonfairSync extends Sync
> 区别方法 : final void lock() : 对比公平锁有一个修改state 的操作 , 修改成功则设置当前拥有独占访问权限的线程
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
// ?- 公平锁同步对象
static final class FairSync extends Sync
> 区别方法 : tryAcquire(int acquires) , 其中最大的缺别在于会查询是否有线程等待获取的时间长于当前线程
Node 3 : nonfairTryAcquire 方法干了什么
final boolean nonfairTryAcquire(int acquires) {
// 获取当前 Thread 和状态
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// CAS 设置状态
if (compareAndSetState(0, acquires)) {
// 设置当前拥有独占访问权限的线程
// null 表示没有线程获取了访问权限
setExclusiveOwnerThread(current);
return true;
}
}
// 返回由 setExclusiveOwnerThread 设置的最后一个线程
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
Node 3 : tryRelease 释放
// 重要可以看到2个操作 : setExclusiveOwnerThread + setState
// setExclusiveOwnerThread 为 null 表示没有线程获取了访问权限
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
2.3 synchronized 和 ReentrantLock 异同相同点都实现了多线程同步和内存可见性语义 (隐式监视器锁定)。都是可重入锁不同点同步实现机制不同synchronized 通过 Java 对象头锁标记和 Monitor 对象实现同步。ReentrantLock 通过CAS、AQS(AbstractQueuedSynchronizer)和 LockSupport(用于阻塞和解除阻塞)实现同步。可见性实现机制不同synchronized 依赖 JVM 内存模型保证包含共享变量的多线程内存可见性。ReentrantLock 通过 AQS 的 volatile state 保证包含共享变量的多线程内存可见性。使用方式不同synchronized 可以修饰实例方法(锁住实例对象)、静态方法(锁住类对象)、代码块(显示指定锁对象)。ReentrantLock 显示调用 tryLock 和 lock 方法,需要在 finally 块中释放锁。功能丰富程度不同synchronized 不可设置等待时间、不可被中断(interrupted)。ReentrantLock 提供有限时间等候锁(设置过期时间)、可中断锁(lockInterruptibly)、condition(提供 await、condition(提供 await、signal 等方法)等丰富功能锁类型不同synchronized 只支持非公平锁。ReentrantLock 提供公平锁和非公平锁实现。当然,在大部分情况下,非公平锁是高效的选择。总结 : 在 synchronized 优化以前,它的性能是比 ReenTrantLock 差很多的,但是自从 synchronized 引入了偏向锁,轻量级锁(自旋锁)后,两者的性能就差不多了 .在两种方法都可用的情况下,官方甚至建议使用 synchronized 。并且,实际代码实战中,可能的优化场景是,通过读写分离,进一步性能的提升,所以使用 ReentrantReadWriteLock2.3 ReentrantLock 深入// 常用方法 :
- void lock()
- Condition newCondition()
- boolean tryLock()
- void unlock()
--------------
// Node 1 : 基础于Lock 接口 , 并且支持序列化
ReentrantLock implements Lock, java.io.Serializable
--------------
// Node 2 : 内部类 , ReentrantLock 中有几个很重要的 sync 类 , Sync 是同步控制的基础
--------------
// Node 3 : 公平非公平的切换方式
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
--------------
// Node 4 : Lock 方法的实现 , 默认调用 NonfairSync
public void lock() {
sync.lock();
}
--------------
// Node 5 : lockInterruptibly 的实现方式
sync.acquireInterruptibly(1);
// Node 6 :
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
三 . ReadWriteLock读写锁是用来提升并发程序性能的锁分离技术的 Lock 实现类。可以用于 “多读少写” 的场景,读写锁支持多个读操作并发执行,写操作只能由一个线程来操作。ReadWriteLock 对向数据结构相对不频繁地写入,但是有多个任务要经常读取这个数据结构的这类情况进行了优化。ReadWriteLock 使得你可以同时有多个读取者,只要它们都不试图写入即可。如果写锁已经被其他任务持有,那么任何读取者都不能访问,直至这个写锁被释放为止。ReadWriteLock 对程序性能的提高主要受制于如下几个因素:数据被读取的频率与被修改的频率相比较的结果。读取和写入的时间有多少线程竞争是否在多处理机器上运行特征 :公平性:支持公平性和非公平性。重入性:支持重入。读写锁最多支持 65535 个递归写入锁和 65535 个递归读取锁。锁降级:遵循获取写锁,再获取读锁,最后释放写锁的次序,如此写锁能够降级成为读锁。深入 ReadWriteLock :ReadWriteLock 是一个接口 , 它仅仅提供了2个方法 :
/**
* Returns the lock used for reading.
*
* @return the lock used for reading
*/
Lock readLock();
/**
* Returns the lock used for writing.
*
* @return the lock used for writing
*/
Lock writeLock();
四 . ReentrantReadWriteLock重入锁 ReentrantLock 是排他锁,排他锁在同一时刻仅有一个线程可以进行访问 , ReentrantReadWriteLock 则是可重入的读写锁实现类 , 只要没有线程 writer , 读取锁可以由多个 Reader 线程同时保持I- ReadWriteLockM- Lock readLock();M- Lock writeLock();C- ReentrantReadWriteLock : 可重入的读写锁实现类I- ReadWriteLock?- 内部维护了一对相关的锁,一个用于只读操作,另一个用于写入操作 , 写锁是独占的,读锁是共享的4.1 ReentrantReadWriteLock 深入使用案例 : Object data;
volatile boolean cacheValid;
final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
void processCachedData() {
rwl.readLock().lock();
if (!cacheValid) {
rwl.readLock().unlock();
rwl.writeLock().lock();
try {
if (!cacheValid) {
data = "test";
cacheValid = true;
}
rwl.readLock().lock();
} finally {
rwl.writeLock().unlock(); // Unlock write, still hold read
}
}
}
Node 1 : 内部提供了2个内部属性 , 这也就是为什么能做到独写锁分离// 读锁
private final ReentrantReadWriteLock.ReadLock readerLock;
// 写锁
private final ReentrantReadWriteLock.WriteLock writerLock;
Node 2 : 再次出现的 Sync , 老规矩 , Sync 还是通过 fair 去判断创建final Sync sync;
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
Node 3 : Sync 内部状态控制// 读取和写入计数提取常量和函数。Lock state在逻辑上分为两个 :
// 较低的(低16)表示排他(写入)锁保持计数,较高的(高16)表示共享(读取)锁保持计数。
static final int SHARED_SHIFT = 16;
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
计数的方式 :
// 获得持有读状态的锁的线程数量
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
读状态,等于 S >>> 16 (无符号补 0 右移 16 位)
// 获得持有写状态的锁的次数
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
写状态,等于 S & 0x0000FFFF(将高 16 位全部抹去)
Node 4 : HoldCounter 类的作用 : 每个读线程需要单独计数用于重入// 每个线程读取保持计数的计数器。作为ThreadLocal维护 , 缓存在cachedHoldCounter
static final class HoldCounter {
int count = 0;
// 非引用有助于垃圾回收
final long tid = getThreadId(Thread.currentThread());
}
// 成功获取readLock的最后一个线程的保持计数
private transient HoldCounter cachedHoldCounter;
Node 5 : ThreadLocalHoldCounter , 为了反序列化机制static final class ThreadLocalHoldCounter
extends ThreadLocal<HoldCounter> {
public HoldCounter initialValue() {
return new HoldCounter();
}
}
// 当前线程持有的可重入读锁的数量。仅在构造函数和readObject中初始化。当线程的读保持计数下降到0时删除
private transient ThreadLocalHoldCounter readHolds;
Node 6 : Sync 内部类NonfairSync : 不公平锁
final boolean writerShouldBlock() {
return false; // writers can always barge
}
final boolean readerShouldBlock() {
// 如果暂时出现在队列头的线程(如果存在)是正在等待的写入器,则阻塞
// 如果在其他已启用的、尚未从队列中耗尽的读取器后面有一个正在等待的写入器,那么新的读取器将不会阻塞
return apparentlyFirstQueuedIsExclusive();
}
FairSync : 公平锁
static final class FairSync extends Sync {
final boolean writerShouldBlock() {
return hasQueuedPredecessors();
}
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}
}
五 . Condition5.1 Condition 简介在 Java SE 5 后,Java 提供了 Lock 接口,相对于 synchronized 而言,Lock 提供了条件 Condition ,对线程的等待、唤醒操作更加详细和灵活
> AQS 等待队列与 Condition 队列是两个相互独立的队列
#await() 就是在当前线程持有锁的基础上释放锁资源,并新建 Condition 节点加入到 Condition 的队列尾部,阻塞当前线程 。
#signal() 就是将 Condition 的头节点移动到 AQS 等待节点尾部,让其等待再次获取锁。
5.2 Condition 流程5.3 Condition 源码Condition 其实是一个接口 , 其在 AQS 中存在一个是实现类 , ConditionObject , 我们就主要说说它 :Node 1 : 属性对象// condition queue 第一个节点
private transient Node firstWaiter;
// condition queue 最后一个节点
private transient Node lastWaiter;
Node 2 : 核心方法 doSignal + doSignalAll
// doSignal : 删除和传输节点,直到碰到非取消的1或null
private void doSignal(Node first) {
do {
// 先判断是否为头节点或者null
// 注意其中的 = 是赋值 : !transferForSignal(first) &&(first = firstWaiter) != null
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
// transferForSignal 是 AQS 方法, 将节点从条件队列转移到同步队列 , 主要是 CAS 操作修改状态
// Node p = enq(node); 这里面是一个Node 拼接操作 , 其实可以理解为已经将 Node 加入对应的队列里面了
} while (!transferForSignal(first) &&(first = firstWaiter) != null);
}
// doSignalAll : 删除和传输所有节点 , 注意区别 , 这里不像 Notify 是通知是有线程去获取
private void doSignalAll(Node first) {
lastWaiter = firstWaiter = null;
do {
Node next = first.nextWaiter;
first.nextWaiter = null;
transferForSignal(first);
first = next;
} while (first != null);
}
Node 3 : 主要方法 awaitpublic final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
// Step 1 : 添加到 Condition 队列
Node node = addConditionWaiter();
// Step 2 : 使用当前状态值调用release , 且返回保存的状态
int savedState = fullyRelease(node);
int interruptMode = 0;
// 如果一个节点(总是最初放置在条件队列上的节点)现在正在同步队列上等待重新获取,则返回true
// 即如果有节点等待
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// 以独占不可中断模式获取已经在队列中的线程。
// 用于条件等待方法和获取方法。
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
// awaitNanos(long nanosTimeout) : 定时条件等待
if (nanosTimeout <= 0L) {
transferAfterCancelledWait(node);
break;
}
if (nanosTimeout >= spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
nanosTimeout = deadline - System.nanoTime();
// awaitUntil(Date deadline) : 实现绝对定时条件等待 , 即一个定时操作
// 超时后直接传输节点
if (System.currentTimeMillis() > abstime) {
timedout = transferAfterCancelledWait(node);
break;
}
Node 4 : Release 方法 // 使用当前状态值调用release;返回保存的状态。
// 取消节点并在失败时抛出异常。
final int fullyRelease(Node node) {
boolean failed = true;
try {
// 获取状态
int savedState = getState();
if (release(savedState)) {
failed = false;
return savedState;
} else {
throw new IllegalMonitorStateException();
}
} finally {
if (failed)
// 设置当前状态为取消
node.waitStatus = Node.CANCELLED;
}
}
public final boolean release(int arg) {
// 实现具体重写
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
Node 5 : 其他方法 :方法 addConditionWaiter : 增加了一个新的服务员等待队列。awaitUninterruptibly :实现不可中断条件等待public final void awaitUninterruptibly() {
Node node = addConditionWaiter();
// 保存的状态作为参数 , 如果失败,抛出IllegalMonitorStateException
int savedState = fullyRelease(node);
boolean interrupted = false;
// 阻塞直到有信号
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if (Thread.interrupted())
interrupted = true;
}
// acquireQueued : 保存的state作为参数 , 以独占不可中断模式获取已经在队列中的线程 , 重新获取
if (acquireQueued(node, savedState) || interrupted)
selfInterrupt();
}
带鱼
盘点认证协议 : 普及篇之Kerberos
纯约束型协议 : OAuth , SAML , OIDC , CAS ,LTPA服务器类协议 : RADIUS , Kerberos , ADFS认证方式类 : OTP , 生物认证 (人脸 , 声纹 , 指纹)认证服务器(附带) : AD , LDAP , ADFS这一篇来聊一下 Kerberos 协议 , 已经基于Kerberos的 AD 域单点一 . 前言Kerberos 最初是由麻省理工学院(MIT)开发的,是雅典娜计划(projectathena)的一部分 , Kerberos 提供了一个集中的身份验证服务器,其功能是对用户到服务器的身份验证,以及对用户到服务器的身份验证。在 Kerberos 身份验证中,服务器和数据库用于客户端身份验证。Kerberos 作为第三方受信任服务器(称为密钥分发中心(KDC))运行。网络上的每个用户和服务都是一个主体。Kerberos 优点密码从不通过网络发送,因为只有密钥以加密形式发送;身份验证是相互的,因此客户端和服务器在相同的步骤进行身份验证,并且它们都确信自己正在与正确的对应方进行通信;身份验证可重用且不会过期;Kerberos 完全基于开放的互联网标准Kerberos 被许多行业采用,因此其安全协议或底层模块中的任何新缺陷都会很快得到纠正Kerberos 缺点如果未经授权的用户可以访问密钥分发中心,则整个身份验证系统将受到威胁Kerberos 只能被支持 Kerberos 的应用程序采用。为了让某些应用程序能够识别 Kerberos,重写这些应用程序的代码可能是个问题Kerberos 关键词安全认证协议tickets 验证密码保护(本地 不保存,链路不传输 )对称加密Server - client 可以相互验证有可信第三方二 . Kerberos 基础要点2.1 Kerberos 成员认证体系成员Client 成员应用程序服务器 (AP , ApplicationServer , Resource)密钥分配中心 (KDC) : AS + TGS + DB2.2 Kerberos 架构架构特点 :消息 = 可解码部分 + 不可解码部分服务端 不与 KDC 直接交流KDC 中拥有 所有用户及密码涉及概念 :principal : 认证主体 , 类似于用户名realm : 作用域 ,一个 principal 只在 指定的 realm 中起作用password : 用户密码 ,对应 于 kerberos 中的 master_key ,可存在于 keytab文件中credential : 凭证 ,用于证明用户 / 行为的有效性 (password / ticket)Long-term Key/Master Key :长期不变的 key , 他的原则是 不能在网络上传输Short-term Key/Session Key : 可在网络上进行传输的key , 这种 key 有时效性TGT 和 TGS 的区别TGT KDC 加密部分(不可解读) : name/ID + TGS的 name /ID + 时间戳 + IP 地址 + TGT 生命周期 + TGS session keyTGT 个人加密部分(可解读) :TGS 的 name / ID + 时间错 + 生命周期 + TGS session key2.3 Kerberos 请求流程Kerberos 协议过程主要有两个阶段,第一个阶段是 KDC 对 Client 身份认证,第二个阶段是Service对Client身份认证。第一次 : 客户端输入登录信息 , Kerberos 客户机创建一个加密密钥并向身份验证服务器(AS)发送一条消息第二次 : AS 使用这个密钥创建临时会话密钥,并向票据授予服务(TGS)发送消息第三次 : TGS 向客户机授予票据和服务器会话密钥 , 客户端使用这些来与服务器进行身份验证并获得访问权以下是 Kerberos 访问详情 :KRB_AS_REQ: 从身份验证服务(AS)请求TGTKRB_AS_REP : 从身份验证服务接收TGTKRB_TGS_REQ : 发送当前的 TGT 并请求TGSKRB_TGS_REP : 从 KDC 接收 TGSKRB_TGS_REP : 将 TGS 提交给应用服务器进行授权KRB_AP_REP : 授予客户端访问服务的权限2.5 KDC 流程详情基础成员 :-》 组成角色
> KDC : key distributed center 密钥配置中心 , 整个安全认证过程的票据生成管理服务 , 包含 AS 和 TGS
> AD :account database ,存储所有client的白名单
-》 主要角色
> C : Client
> AS : Authentication Server 认证服务器 ,完成用户认证
> TGS : Ticket Granting Server 凭证服务器
> ST : Http Service Ticket
> SS : Service Server
> RS : Resource server
Step 1 : KRB_AS_REQ 第一次 申请 TGT请求 C->SS : 通过 明文(Name/身份信息 , IP/client 消息 , TGT 有效时间 )访问 (亦可使用 Master key 进行加密 ,AD 中保存有 Master key)处理 IN SS : SS 判断 该 对象 是否 在 AD 中存在 , 并且 产生 Session Key 用于 TGS 之间通信返回 SS->C:返还TGT (TGT 服务端部分 + TGT 个人部分)Step 2 : KRB_TGS_REQ 第二次生成 TGS> 请求 C -> TGS :
-> TGS Session key 加密部分(Name/ID + 时间戳 + client Info),明文 (服务Name/ID+生命周期),TGT
> 处理 IN TGS (对TGT 第一部分解密 ):
-> 1. 用户名对比 (TGT <-> 认证器)
-> 2. 时间戳对比
-> 3. 是否过期
-> 4. IP是否一致
-> 5. 认证器是否已存在于缓存
-> 6. 添加权限和认证服务
-> 7. 产生 Http Service Session Key
-> 8. 准备 ST
> 返回 TGS -> C:
-> ST ( Http 服务密码 进行加密 ) = 个人name/id + Http 服务name /id + IP + 时间戳 + ST 生命周期 + Http Service Session Key
-> TGS Session Key 加密部分 = Http 服务name /id + 时间戳 + ST 生命周期 + Http Service Session Key
Step 3 : 资源服务器处理> 请求 C -> RS :
-> Http Service Session Key加密部分 : 个人 name / ID + 时间戳
> Resource 服务器 中 :
-> 1. 对比用户名
-> 2. 比较时间戳
-> 3. 检查是否过期
-> 4. 检查IP地址
-> 5. 是否已经存在于缓存
2.5 KDC 的使用前提域控制器之间的复制 : 如果部署了多个域控制器(即多个 KDC) ,则必须启用复制并及时回收。 如果复制失败或回收被延迟,当用户更改密码时,身份验证可能客户端和 kdc 必须将他们的时钟同步 在 Kerberos 中,时间的准确度量对于防止重放攻击非常重要。 Kerberos 支持可配置的时间偏移(默认5分钟) ,超过这个时间,身份验证将失败客户端和 kdc 必须能够在网络上进行通信 Kerberos 流量发生在 TCP 和 UDP 端口88上,所有客户端都必须能够访问至少一个 KDC (网域控制器)客户端、用户和服务必须具有唯一的名称 计算机、用户或服务主体名称的重复名称可能导致身份验证失败客户端和 kdc 必须使用 NETBIOS 和 DNS 名称解析 客户端和 kdc 必须使用 NETBIOS 和 DNS 名称解析 Kerberos 服务主体名称通常包括 NETBIOS 和 DNS 地址,这意味着 KDC 和 Client 必须能够以相同的方式解析这些名称 某些情况下 , IP 地址也可用于服务主体名称三 . Kerberos AD域配置3.1 配置 KDC DB 部分Step 1 : 创建Kerberos SPN 用户Step 2 : 配置用户属性 , 设置不要求验证 , 密码不过期Step 3 :生成 kerberos.keytabktpass.exe /out c:\kerberos.keytab /princ HTTP/antblack.com@ADSERVER.COM.CN /pass zzy19950810 /mapuser kerberos@ADSERVER.COM.CN /ptype KRB5_NT_PRINCIPAL /crypto RC4-HMAC-NT
ADSERVER.COM.CN
//- 当前域名
antblack.com
//- KDC Client 端域名 (即应用服务器域名)
kerberos@ADSERVER.COM.CN
//- 绑定的用户
zzy19950810
//- 绑定的密码
RC4-HMAC-NT
// -加密方式
Step 4 :生成 后用户会多委派属性 ,选择信任同时可以看到用户已经绑定了多个(PS : 这里实际上应该是ADSERVER.COM.CN , 截图问题)3.2 配置 KDCCentOS 7 可以不用安装 ,如果 klist 不存在 , 执行以下命令yum install krb5-server krb5-libs krb5-auth-dialog 修改 /etc/krb5.conf# Configuration snippets may be placed in this directory as well
includedir /etc/krb5.conf.d/
[logging]
default = FILE:/var/log/krb5libs.log
kdc = FILE:/var/log/krb5kdc.log
admin_server = FILE:/var/log/kadmind.log
[libdefaults]
dns_lookup_realm = false
ticket_lifetime = 24h
default_realm = ADSERVER.COM.CN
default_keytab_name = /opt/kerberos.keytab
default_tkt_enctypes = rc4-hmac
default_tgs_enctypes = rc4-hmac
[realms]
ADSERVER.COM.CN= {
kdc = 192.168.158.9
}
[domain_realm]
.adserver.com.cn = ADSERVER.COM.CN
adserver.com.cn = ADSERVER.COM.CN
/opt/kerberos.keytab : windows AD 之前生成的 , 拖入应用服务器192.168.158.9 : KDC DB 地址ADSERVER.COM.CN : KDC AD 域信息rc4-hmac : 加密方式Step 3 : 测试 KDCklist -k
[root@localhost ~]# klist -k
Keytab name: FILE:/opt/kerberos.keytab
KVNO Principal
---- --------------------------------------------------------------------------
3 HTTP/antblack.com@ADSERVER.COM.CN
// 测试 KeyTab 是否连接
// 这个 ANTBLACK.CN 会去查询 kerb5.conf 中的 realm , 并且去其配置的 kdc 进行认证
kinit -k HTTP/antblack.cn@ANTBLACK.CN
klist -k
// 执行后会出现票据
// PS : 此时 AD 中运行 : klist tickets
>>>>>>>>>>>>>>>>
当前登录 ID 是 0:0x12de650
缓存的票证: (2)
#0> 客户端: administrator @ WDHACPOC.COM.CN
服务器: krbtgt/WDHACPOC.COM.CN @ WDHACPOC.COM.CN
Kerberos 票证加密类型: AES-256-CTS-HMAC-SHA1-96
票证标志 0x40e10000 -> forwardable renewable initial pre_authent name_canonicalize
开始时间: 3/30/2021 16:35:39 (本地)
结束时间: 3/31/2021 2:35:39 (本地)
续订时间: 4/6/2021 16:35:39 (本地)
会话密钥类型: AES-256-CTS-HMAC-SHA1-96
缓存标志: 0x1 -> PRIMARY
调用的 KDC: WIN-U76BKIQFGGJ
#1> 客户端: administrator @ WDHACPOC.COM.CN
服务器: host/win-u76bkiqfggj.wdhacpoc.com.cn @ WDHACPOC.COM.CN
Kerberos 票证加密类型: AES-256-CTS-HMAC-SHA1-96
票证标志 0x40a50000 -> forwardable renewable pre_authent ok_as_delegate name_canonicalize
开始时间: 3/30/2021 16:35:39 (本地)
结束时间: 3/31/2021 2:35:39 (本地)
续订时间: 4/6/2021 16:35:39 (本地)
会话密钥类型: AES-256-CTS-HMAC-SHA1-96
缓存标志: 0
调用的 KDC: WIN-U76BKIQFGGJ
四 . Java 实现方式// TODO : 行业代码不便于整理 , 后续会做一个简化的 demo 填坑总结Kerberos 对外主推的是安全性 , 这个也属于常见但是用的不多的协议 , 结合 AD 域单点部分厂家还是有涉及.
带鱼
【多线程系列】终于懂了 Java 中的各种锁
源码版本JDK 8前言Java 中提供了种类丰富的锁,每种锁因有不同的特性在不同的场景能够展现出较高的性能,本文在概念的基础上结合源码 + 使用场景进行举例,让读者对 Java 中的锁有更加深刻的认识,Java 中按照是否包含某一特性来定义锁,下面是本文中介绍的锁的分类图:乐观锁 & 悲观锁乐观锁和悲观锁是一种广义上的概念,体现了线程对互斥资源进行同步的两种不同的态度,在 Java 和数据中都有实际的运用。概念对一个互斥资源的同步操作,悲观锁认为自己访问时,一定有其它线程来修改,因此在访问互斥资源时悲观锁会先加锁;而乐观锁认为自己在访问时不会有其它线程来修改,访问时不加锁,而是在更新数据时去判断有无被其他线程修改,若没被修改则写入成功,若被其他线程修改则进行重试或报错。适应场景由上面我们可以看出,乐观锁适用于读操作多的场景,而悲观锁适用于写操作多的场景。源码分析我们常见的synchronized、ReentrantLock 都属于悲观锁,而AtomicInteger.incrementAndGet 则属于乐观锁。 // ----------------- 悲观锁 -------------------------
synchronized (MUTEX) {
// 同步代码块
}
ReentrantLock lock = new ReentrantLock();
lock.lock();
// 同步代码块
lock.unlock();
// ----------------- 乐观锁 -------------------------
AtomicInteger atomicInteger = new AtomicInteger(0);
atomicInteger.incrementAndGet();
// 悲观锁的实现方式很直观,先进行加锁,然后访问互斥资源,最后释放锁;那么乐观锁时如何实现的呢?我们通过介绍乐观锁主要的实现方式 CAS 来为大家解惑。
// 这里简单给大家回顾一下 CAS ,有需要了解更多的读者请去阅读 CAS 章节。
CAS全称 Compare And Swap(比较与交换),是一种无锁算法。在不使用锁(没有线程被阻塞)的情况下实现多线程之间的同步。
CAS算法涉及到三个操作数:当前内存值 V、原始值 A、要写入的新值 B。
当且仅当 V 的值等于 A 时,CAS通过原子方式用新值B来更新V的值(“比较+更新”整体是一个原子操作),否则不会执行任何操作。
// atomicInteger.incrementAndGet() 使用上述方式实现:
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
// v 表示获取到的内存中的当前值
v = getIntVolatile(o, offset);
// compareAndSwapInt() 是一个原子操作、进行比较更新
} while (!compareAndSwapInt(o, offset, v, v + delta));
return v;
}
阻塞 & 非阻塞了解阻塞和非阻塞前,大家需要知道唤醒和阻塞一个Java线程需要操作系统进行用户态到内核态的切换,这种切换是十分耗时处理器时间的,如果同步代码块的内容过于简单,状态转换消耗的时间可能比用户代码执行时间还长,这是十分不划算的,因此我们引入了非阻塞的概念。概念从上面的介绍中我们其实已经可以了解到阻塞和非阻塞的概念。多线程访问互斥资源时,当互斥资源已被占用,阻塞线程,当互斥释放时,唤醒线程进行竞争称为阻塞式同步;而当互斥资源被占用时,不进行线程阻塞而通过自旋等待其它线程释放锁或直接返回错误的方式称为非阻塞式同步,自旋方式又可以分为普通自旋和自适应自旋。使用场景非阻塞自旋的方式本身是有缺点的,不能完全代替阻塞同步,非阻塞自旋虽然避免了线程切换的开销但是会占用处理器的时间,如果锁被占用的时间很短,那么自旋等待的效果很好,如果锁被占用时间很长那么只会白白浪费处理器时间。所以自旋一般会设置一定限制,比如Java中默认是10次(使用-XX:PreBlockSpin来修改)。自适应自旋意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。因此,阻塞式同步适用于同步代码块执行时间比较长,线程获取锁时间间隔比较长的场景,而非阻塞式同步适用于同步代码块执行比较短,线程获取锁时间间隔比较短的场景。源码分析ReentrantLock以及synchronized中的重量级锁都属于阻塞式同步,而 Java 中的原子操作类中的 CAS 失败后自旋则运用了非阻塞自旋的思想。更正一下:网上很多文章说 synchronized中轻量级锁则运用了非阻塞自旋的思想,其实上是错误的;
实际上一次 CAS 尝试获取轻量级锁失败后直接升级为重量级锁,而不会自旋。
公平锁 & 非公平锁概念公平锁和非公平锁指的是获取线程获取锁时的顺序。公平锁指按照锁申请的顺序来获取锁,线程直接进入队列中,队列中的第一个线程才能获取锁。非公平锁指多个线程获取锁时,直接尝试获取锁,只有当线程未获取到锁时才放入队列中。适应场景公平锁的优点是不会造成饥饿,但整体性能会比非公平锁低,因为除等待队列中的第一个线程,其它线程都需要进行阻塞和唤醒操作。而非公平锁有几率直接获得锁,减少了线程阻塞和唤醒的次数,但可能会造成饥饿。因此在饥饿无影响或不会产生饥饿的场景下优先考虑非公平锁。源码分析ReentrantLock 提供了公平锁和非公平锁两种实现,默认使用非公平锁。非公平锁 final void lock() {
// 多次尝试获取锁,避免将线程阻塞再唤醒
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
public final void acquire(int arg) {
// 尝试获取锁失败后再放入等待队列
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 重点:不判断队列中是否有排队线程直接获取锁
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
我们可以注意到非公平锁实现中两次尝试使用compareAndSetState()来获取锁,其实这里就是类似自旋的作用,避免线程阻塞再唤醒的过程,从而提高性能。公平锁 final void lock() {
acquire(1);
}
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 重点:当互斥资源未被占用时,先判断队列中是否存在等待线程,若无尝试竞争锁,若有或竞争失败则将当前线程放入队列中
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
// 判断当前线程是否位于同步队列中的第一个。如果是则返回true,否则返回false。
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
可重入锁 & 不可重入锁概念可重入锁又称递归锁,是指同一线程在外层获取锁后,进入内层方法再次获取同一锁时会自动获取锁。可重入锁的好处是可以一定程度避免死锁。源码分析Java 中 ReentrantLock 和 synchronized 都是可重入锁,我们以 ReentrantLock 为例进行分析:// ReentrantLock FairSync
// 获取锁
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 重点:在已经获取锁的情况下,对比当前线程ID和占用锁线程ID是否一致,若一致锁计数器 +1
// 不可重入的情况下,则无此判断
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
// 释放锁
protected final boolean tryRelease(int releases) {
// 每次释放时进行-1
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 直到计数器为 0 代表锁释放
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
排它锁 & 共享锁概念排它锁和共享锁的主要区别在于互斥资源锁是否能被多个线程同时持有。同时只能被一个线程持有称为排它锁;当能够被多个线程同时持有称为共享锁。作用进一步细化加锁粒度,提高并发性能。比如我们常见读写锁,实现读读不互斥,高效并发读,而读写、写读、写写的过程互斥。源码分析我们以 ReentrantReadWriteLock 读写锁为例,ReentrantReadWriteLock 中有两把锁 ReadLock 和 WriteLock ,一个是读锁为共享锁,一个是写锁为排它锁:当前线程已获取读锁无写锁,其它线程可以获取读锁;当前线程已获取写锁,仅当前线程可以获取读锁。 ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(true);
// 读锁
ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
// 写锁
ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();
// 读锁 公平锁
public void lock() {
sync.acquireShared(1);
}
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0) doAcquireShared(arg);
}
protected final int tryAcquireShared(int unused) {
/*
* Walkthrough:
* 1. If write lock held by another thread, fail.
* 2. Otherwise, this thread is eligible for
* lock wrt state, so ask if it should block
* because of queue policy. If not, try
* to grant by CASing state and updating count.
* Note that step does not check for reentrant
* acquires, which is postponed to full version
* to avoid having to check hold count in
* the more typical non-reentrant case.
* 3. If step 2 fails either because thread
* apparently not eligible or CAS fails or count
* saturated, chain to version with full retry loop.
*/
Thread current = Thread.currentThread();
int c = getState();
// 存在写锁且不是当前线程
if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current) return -1;
// 当前线程持有写锁或仅有读锁或无锁
int r = sharedCount(c);
if (!readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)) {
// 如果是第一次获取 初始化 firstReader、firstReaderHoldCount 不是第一次获取 对 readHolds 对应线程计数+1
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current)) cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0) readHolds.set(rh);
rh.count++;
}
return 1;
}
return fullTryAcquireShared(current);
}
final int fullTryAcquireShared(Thread current) {
/*
* This code is in part redundant with that in
* tryAcquireShared but is simpler overall by not
* complicating tryAcquireShared with interactions between
* retries and lazily reading hold counts.
*/
HoldCounter rh = null;
// 自旋 不挂起线程
for (; ; ) {
int c = getState();
if (exclusiveCount(c) != 0) {
// 非当前线程获取到写锁获取失败
if (getExclusiveOwnerThread() != current)
return -1;
// else we hold the exclusive lock; blocking here
// would cause deadlock.
} else if (readerShouldBlock()) {
// 走到这里说明没有写锁被占有 判断是否存在重入
// Make sure we're not acquiring read lock reentrantly
// 当前线程为 firstReader 走下面 CAS
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
} else {
if (rh == null) {
rh = cachedHoldCounter;
// cachedHoldCounter 没有缓存或缓存的不是当前线程
if (rh == null || rh.tid != getThreadId(current)) {
rh = readHolds.get();
// 说明上一行是初始化 移除上面产生的初始化
if (rh.count == 0)
readHolds.remove();
}
}
if (rh.count == 0)
return -1;
}
}
// 是否已经达到读锁获取次数上限
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// CAS 获取锁
if (compareAndSetState(c, c + SHARED_UNIT)) {
// 读锁初始化和计数
if (sharedCount(c) == 0) {
// 第一次添加读锁
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
// firstReader 为当前线程
firstReaderHoldCount++;
} else {
// 否则更新 readHolds 对应线程读锁计数
if (rh == null)
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
cachedHoldCounter = rh; // cache for release
}
return 1;
}
}
}
// 无法获取读锁,将获取读锁线程放入等待队列中
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (; ; ) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted) selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true;
}
} finally {
if (failed) cancelAcquire(node);
}
}
// 写锁 公平锁
public void lock() {
sync.acquire(1);
}
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt();
}
protected final boolean tryAcquire(int acquires) {
/*
* Walkthrough:
* 1. If read count nonzero or write count nonzero
* and owner is a different thread, fail.
* 2. If count would saturate, fail. (This can only
* happen if count is already nonzero.)
* 3. Otherwise, this thread is eligible for lock if
* it is either a reentrant acquire or
* queue policy allows it. If so, update state
* and set owner.
*/
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
// 存在读锁或写锁
if (c != 0) {
// (Note: if c != 0 and w == 0 then shared count != 0)
// 存在读锁或存在写锁但不是当前线程持有获取失败
if (w == 0 || current != getExclusiveOwnerThread()) return false;
// 获取锁是否超过上限
if (w + exclusiveCount(acquires) > MAX_COUNT) throw new Error("Maximum lock count exceeded");
// Reentrant acquire
// 走到这里说明当前线程持有写锁 重入
setState(c + acquires);
return true;
}
// 不存在锁 判断队列阻塞策略 并进行 CAS 尝试获取锁
if (writerShouldBlock() || !compareAndSetState(c, c + acquires)) return false;
setExclusiveOwnerThread(current);
return true;
}
ReentrantReadWriteLock 巧妙的将AQS中的state一分为二高16位为读计数,低16为为写计数,将两个原子性操作(读竞争和写竞争)合并为一个原子操作。synchronized 中的无锁、偏向锁、轻量级锁、重量级锁synchronized 中的无锁、偏向锁、轻量级锁、重量级锁是 synchronized 特有的概念,参考 volatile & synchronized 章节。
带鱼
Java 多线程 : 漫谈 CAS
一 . CAS 简介什么是 CAS ?CAS操作 —— Compare & Set ,或是 Compare & SwapCAS 的操作步骤是什么 ? -> 先比较 , 再设置jdk5增加了并发包java.util.concurrent.*,其下面的类使用CAS算法实现了区别于synchronouse同步锁的一种乐观锁。CAS是一种无锁算法,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做CAS 的效率 ?CAS(比较并交换)是CPU指令级的操作,只有一步原子操作,所以非常快CAS避免了请求操作系统来裁定锁的问题CAS 的消耗?一个8核CPU计算机系统,每个CPU有cache(CPU内部的高速缓存,寄存器),管芯内还带有一个互联模块,使管芯内的两个核可以互相通信当存在 cache 和 数据不在一个域中时 ?“最好情况”是指对某一个变量执行 CAS 操作的 CPU 正好是最后一个操作该变量的CPU,所以对应的缓存线已经在 CPU 的高速缓存中了算法假想do{
备份旧数据;
基于旧数据构造新数据;
}while(!CAS( 内存地址,备份的旧数据,新数据 ))
二 . CAS 的缺陷问题一 : ABA 问题一个线程 one 从内存位置 V 中取出 A另一个线程 two 也从内存中取出 A ,并且 two 进行了一些操作变成了 Btwo 又将 V 位置的数据变成 A ,这时候线程 one 进行 CAS 操作发现内存中仍然是 Aone 操作成功。尽管线程 one 的 CAS 操作成功,但可能存在潜藏的问题。从 Java5 开始 JDK 的 atomic包里提供了一个类 AtomicStampedReference 来解决 ABA 问题。 AtomicStampedReference 通过包装 [E,Integer] 的元组,来对对象标记版本戳 stamp问题二 : 循环时间长开销大对于资源竞争严重(线程冲突严重)的情况,CAS 自旋的概率会比较大,从而浪费更多的 CPU 资源,效率低于 synchronized。问题三 : 只能保证一个共享变量的原子操作当对一个共享变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候就可以用锁。三 . CAS 深入分析在 CAS 中有三个参数:内存值 V、旧的预期值 A、要更新的值 B ,当且仅当内存值 V 的值等于旧的预期值 A 时,才会将内存值V的值修改为 B ,否则什么都不干主要类 : UnsafeUnsafe 是 CAS 的核心类 , 他提供了硬件级别得原子操作 (其他情况下Java 需要通过本地 Native 方法访问底层操作系统)unsafe.objectFieldOffsetgetAndAddInt -> compareAndSwapInt(Object var1, long var2, int var4, int var5) CPU 的原子操作: CPU 提供了两种方法来实现多处理器的原子操作:总线加锁或者缓存加锁总线加锁: 总线加锁就是就是使用处理器提供的一个 LOCK# 信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占使用共享内存。但是这种处理方式显得有点儿霸道,不厚道,他把CPU和内存之间的通信锁住了,在锁定期间,其他处理器都不能其他内存地址的数据,其开销有点儿大。所以就有了缓存加锁。缓存加锁:其实针对于上面那种情况,我们只需要保证在同一时刻,对某个内存地址的操作是原子性的即可。缓存加锁,就是缓存在内存区域的数据如果在加锁期间,当它执行锁操作写回内存时,处理器不再输出LOCK# 信号,而是修改内部的内存地址,利用缓存一致性协议来保证原子性。缓存一致性机制可以保证同一个内存区域的数据仅能被一个处理器修改,也就是说当 CPU1 修改缓存行中的 i 时使用缓存锁定,那么 CPU2 就不能同时缓存了 i 的缓存行。CAS 主要实现得方式 :AtomicIntegeraddAndGet()四 . CAS CPU 的查询操作@ blog.csdn.net/youanyyou/a…
// CPU 结构简述 :
- 每个CPU有cache(CPU内部的高速缓存,寄存器)
- 管芯内还带有一个互联模块,使管芯内的两个核可以互相通信
- 系统互联模块可以让四个管芯相互通信,并且将管芯与主存连接起来
// 数据流动
- 数据以“缓存线”为单位在系统中传输,“缓存线”对应于内存中一个 2 的幂大小的字节块,大小通常为 32 到 256 字节之间
- 当 CPU 从内存中读取一个变量到它的寄存器中时,必须首先将包含了该变量的缓存线读取到 CPU 高速缓存
- CPU 将寄存器中的一个值存储到内存时,不仅必须将包含了该值的缓存线读到 CPU 高速缓存,
还必须确保没有其他 CPU 拥有该缓存线的拷贝
// 转变流程
• CPU0 检查本地高速缓存,没有找到缓存线。
• 请求被转发到 CPU0 和 CPU1 的互联模块,检查 CPU1 的本地高速缓存,没有找到缓存线。
• 请求被转发到系统互联模块,检查其他三个管芯,得知缓存线被 CPU6和 CPU7 所在的管芯持有。
• 请求被转发到 CPU6 和 CPU7 的互联模块,检查这两个 CPU 的高速缓存,在 CPU7 的高速缓存中找到缓存线。
• CPU7 将缓存线发送给所属的互联模块,并且刷新自己高速缓存中的缓存线。
• CPU6 和 CPU7 的互联模块将缓存线发送给系统互联模块。
• 系统互联模块将缓存线发送给 CPU0 和 CPU1 的互联模块。
• CPU0 和 CPU1 的互联模块将缓存线发送给 CPU0 的高速缓存。
• CPU0 现在可以对高速缓存中的变量执行 CAS 操作了
//效率问题
最好情况 : 某一个变量执行 CAS 操作的 CPU 正好是最后一个操作该变量的CPU
五 . CAS 初级原理// Java 里面 CAS 操作主要通过 Native 方法完成 , 主要的操作对象有 : Unsafe
C- Unsafe
- 提供了硬件级别的原子操作
- 对于Unsafe类的使用都是受限制的,只有授信的代码才能获得该类的实例
M- public native long allocateMemory(long paramLong) :
M- public native long reallocateMemory(long paramLong1, long paramLong2) : 扩充内存
M- public native void freeMemory(long paramLong) : 释放内存
// 例如 AQS 里面 :
> AbstractQueuedSynchronizer
return unsafe.compareAndSwapObject(this, headOffset, null, update);
六 . CAS 深入@ https://blog.csdn.net/qq_37113604/article/details/81582784
// .c 文件 sun.misc.Unsafe
public final native boolean compareAndSwapInt(Object o, long offset,int expected, int x);
// C 源码
#define LOCK_IF_MP(mp) __asm cmp mp, 0 \
__asm je L0 \
__asm _emit 0xF0 \
__asm L0:
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
// alternative for InterlockedCompareExchange
int mp = os::is_MP();
__asm {
mov edx, dest
mov ecx, exchange_value
mov eax, compare_value
LOCK_IF_MP(mp)
cmpxchg dword ptr [edx], ecx
}
}
总结 :程序会根据当前处理器的类型来决定是否为cmpxchg指令添加lock前缀。如果程序是在多处理器上运行,就为cmpxchg指令加上lock前缀(lock cmpxchg)。反之,如果程序是在单处理器上运行,就省略lock前缀?- (单处理器自身会维护单处理器内的顺序一致性,不需要lock前缀提供的内存屏障效果)。
带鱼
Java 并发多线程系列
深入探讨Java中的并发编程
带鱼
盘点认证协议
对开发中的认证协议 : OAuth , CAS , SAML , 和其他小众协议进行梳理 , 同时包含各大框架对其的应用和实现方式的源码级分析
带鱼
Java 多线程的源码分析
Java 多线程的源码分析 , 功能解析 . 该系列会持续迭代和完善 . 致力于快速使用和性能分析