《Java并发编程实战》-11

第11章 性能与可伸缩性


11.1 对性能的思考

11.1.1 性能与可伸缩性

可伸缩性指的是:当增加计算资源时(例如CPU、内存、存储容量或I/O带宽),程序的吞吐量或处理能力能相应地增加。

11.1.2 评估各种性能权衡因素

避免不成熟的优化。首先使程序正确,然后提高运行速度–如果它还运行得不够快。

以测试为基准,不要猜测。

11.2 Amdahl定律

Amdahl定律描述的是:在增加计算资源的情况下,程序在理论上能够实现最高加速比,这个值取决于程序中可并行组件和串行组件所占的比重。

在所有并发程序中都包含一些串行部分。如果你认为在你的程序中不存在串行部分,那么可任意在仔细检查一遍。

11.2.1 实列:在各种框架中隐藏的串行部分

11.2.2 Amdahl定律的应用

11.3 线程引入的开销

11.3.1 上下文切换

切换线程上下文需要一定的开销,而在线程调度过程中需要访问由操作系统和JVM共享的数据结构。应用程序、操作系统以及JVM都使用一组相同的CPU。

11.3.2 内存同步

同步操作的性能开销包括多个方面。在synchronizedvolatile提供的可见性保证中可能会使用一些特殊指令,即内存栅栏(Memory Barrier)。内存栅栏可以刷新缓存,使缓存无效,刷新硬件的写缓冲,以及停止执行管道。

不要过度担心非竞争同步带来的开销。这个基本的机制已经非常快了,并且JVM还能进行额外的优化以及进一步降低或消除开销。因此,我们应该将优化重点放在那些发生锁竞争的地方。

阻塞

当线程无法获取某个锁或者由于某个条件等待或在I/O操作上阻塞时,需要被挂起,在这个过程中将包含两次额外的上下文切换,以及所有必要的操作系统操作和缓存操作:被阻塞的线程在其执行时间片还未用完之前就被就被交换换出去,而在随后当要获取的锁或者其他资源可用时,又再次被切换回来。(由于锁竞争而导致阻塞时,线程在持有锁时将存在一定的开销:当它释放锁时,必须告诉操作系统恢复运行阻塞的线程)。

11.4 减少锁的竞争

在并发程序中,对可伸缩性的最主要威胁就是独占方式的资源锁。

由3种方式可以降低锁的竞争程度:

  • 减少锁的持有时间。
  • 降低锁的请求频率。
  • 使用带有协调机制的独占锁,这些机制允许更高的并发性。

11.4.1 缩小锁的范围(”快进快出”)

降低发生竞争可能性的一种有效方式就是尽可能缩短锁的持有时间。例如,而可以将一些与锁无关的代码移出同步代码块,尤其是那些开销较大的操作,以及可能被阻塞的操作,例如I/O操作。

11.4.2 减小锁的粒度

通过锁分段和锁分解等降低线程请求锁的频率(从而减小竞争的可能性)的技术可以减小锁的持有时间,在这些技术中将采用多个互相独立的锁来保护独立的状态变量,从而改变这些变量之前由单个锁保护的情况。

11.4.3 锁分段

锁分段的一个劣势在于:与采用单个锁来实现独占访问相比,要获取多个锁来实现独占访问将更加困难并且开销更高。

11.4.4 避免热点域

11.4.5 一些替代独占锁的方法

放弃使用独占锁来管理共享状态:

  • 使用并发容器
  • 读-写锁
  • 不可变对象
  • 原子对象

11.4.6 监测CPU的利用率

如果CPU没有得到充分利用,那么需要找到其中的原因。通常有一下几种原因:

  • 负载不充足。
  • I/O密集。
  • 外部限制。
  • 锁竞争。

11.4.7 向对象池说“不”

通常,对象分配操作的开销比同步的开销更低。

11.5 实例:比较Map的性能

11.6 减少上下文切换的开销

小结

由于使用线程常常是为了充分利用多个处理器的计算能力,因此并发程序性能的讨论中,童话参观更多地将侧重点放在吞吐量和可伸缩性上,而不是服务时间。Amdahl定律告诉我们,程序的可伸缩性取决于在所有代码中必须被串行执行的代码比例。因为Java程序中串行执行的主要来源是独占凡是的资源锁,因此通常可以通过以下方式来提升可伸缩性:减少锁的持有时间,降低锁的粒度,意见采用非独占锁或非阻塞锁来代替独占锁。

《Java并发编程实战》-1

第2章 线程安全性

2.1 什么是线程安全性

当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程架构如何交替执行,并且在主调用代码中不需要任何额外的同步或协同,这个类都表示出正确的行为,那么就称这个类是线程安全。

在线程安全类中封装了必要的同步机制,因此客户端无需进一步采用同步措施。

无状态对象一定是线程安全的。

2.2 原子性

2.2.1 竞态条件

2.2.2 延迟初始化中的竞态条件

延迟初始化中的竞态条件(不要这样做)

@NotThreadSafe
public class LazyInitRace {
    private ExpensiveObject instance = null;
    public ExpensiveObject getInstance() {
    if( instance == null)
        instance = new ExpensiveObject();
    return instance;
}

2.2.3 复合操作

假定有两个操作A和B,如果从执行A的线程来看,当另一个线程执行B时,要么将B全部执行完,要么完成不执行B,那么A和B对彼此来说时原子的。原子操作是指,对于访问同一个状态的所有操作(包括该操作本身)来说,这个操作是一个以原子方式执行的操作。

在实际情况中,应尽可能使用现有的线程安全对象(例如AcomicLong)来管理类的状态。与非线程安全的对象相比,判断线程安全对象的可能状态及其状态转换情况要更为容易,从而也更容易维护和验证线程安全性。

2.3 加锁机制

要保持状态的一致性,就需要在单个原子操作中更新所有相关的状态变量。

2.3.1 内置锁

2.3.2 重入

2.4 用锁来保护状态

对于可能被多个线程同时访问的可变状态变量,在访问它的时都需要持有同一个锁,在这种情况下,我们称状态变量是由这个锁保护的。

每个共享的和可变的变量都应该只由一个锁来保护,从而使维护人员知道是哪一个锁。

对于每个包含多个变量的不变性条件,其中涉及的所有变量都需要由同一个锁来保护。

 if (!vector.contains(element) )
     vector.add(element)

即使Vector时同步的,因为上米娜的复合操作不是原子的,所以上面的代码也是非线程安全的。

2.5 活跃性与性能

通常,在简单性与性能之间存在着互相约制因素。当实现某个同步策略时,一定不要盲目地为了性能而牺牲简单性(这可能破坏安全性)。

当执行时间较长的计算或者可能无法完成的操作时(例如,网络I/O或控制台I/O),一定不要持有锁。

springboot (转载自zooooooooy)

springMVC

springboot提供了开箱即用的web mvc,项目中用到的是gradle,只需要引用两个包就可以了

compile('org.springframework.boot:spring-boot-starter')
compile('org.springframework.boot:spring-boot-starter-web')

并不需要做什么其他配置,就可以启动web项目,编写restful接口。还是有一些需要个性化定制的功能需要我们手动去配置。有一些并不是springboot独有,配置的时候都是采用代码代替xml的方式。问题记录如下:

  • Json序列化,
    springmvc都是需要配置的,之前都是通过xml来配置。在springboot里面需要自定义MessageConverter。注册bean的方式加载到spring消息转换器里面,默认是放置到转换器队列的最前面优先解析。
    springmvc默认的json解析器是Jackson,我这边算是先继承MappingJackson2HttpMessageConverter,修改覆盖初始方法来定制化项目需要的json格式化规则。

    public class JsonMessageConverter extends MappingJackson2HttpMessageConverter {
    
           @Override
           protected void init(ObjectMapper objectMapper) {
               objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
               objectMapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
               objectMapper.enable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING);
               objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
               objectMapper.enable(DeserializationFeature.READ_ENUMS_USING_TO_STRING);
               objectMapper.enable(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT);
    
               objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
    
               super.init(objectMapper);
           }
       }
    
    

    后面需要将这个注册到spring bean管理里面。在configuration类里面注册

    @Bean
    public HttpMessageConverters customConverters() {
       return new HttpMessageConverters(new JsonMessageConverter());
    }
    
    
  • UTF8编码
    同样是在configuration类里面注册,其实configuration就相当于spring的一个xml文件,每个方法相当于定义的一个bean,方法之间是可以互相依赖的。spring已经考虑到这点,不用担心依赖的时候破坏单例的特性。

    @Bean
    public CharacterEncodingFilter encodingFilter() {
    
       CharacterEncodingFilter encodingFilter = new CharacterEncodingFilter();
       encodingFilter.setEncoding("UTF-8");
       encodingFilter.setForceEncoding(true);
       return encodingFilter;
    }
    
    
  • 拦截器
    用到拦截器的时候,需要继承WebMvcConfigurerAdapter,通过适配器的模式,覆盖相应的方法。当然同时这个类需要标注为configuration类。

    | 方法 |
    |——–|
    |configurePathMatch|
    |configureContentNegotiation|
    |configureAsyncSupport|
    |configureDefaultServletHandling|
    |addFormatters|
    |addInterceptors|
    |addResourceHandlers|
    |addCorsMappings|
    |addViewControllers|
    |configureViewResolvers|
    |addArgumentResolvers|
    |addReturnValueHandlers|
    |configureMessageConverters|
    |extendMessageConverters|
    |configureHandlerExceptionResolvers|
    |extendHandlerExceptionResolvers|
    |getValidator|
    |getMessageCodesResolver|
    拦截器添加选择覆盖addInterceptors

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
       registry.addInterceptor(new MappedInterceptor(new String[]{"/**"}, new String[]{""},
               new MvcInterceptor()));
    }
    

    MvcInterceptor是自定义的拦截器,如果需要用到url拦截的功能,需要使用spring带的MappedInterceptor定义拦截的url和排除的url
    项目中还用到了addArgumentResolvers,addReturnValueHandlers就不一一列举了。

项目

  • 属性文件
    使用@PropertySources({@PropertySource(value = “classpath:setting.properties”)})来引入,不过这个不是springboot里面的东西。

  • 项目分布

    • css,js前端等用到的组件都分布在public下面或者static下面
      需要渲染的模板定义在templates里面,安利一下我正在使用的模板pebble
    • 数据库使用的是mybatis,sql还是写在xml里面在,始终觉得在代码里面写sql比较别扭。
    • 打包工具使用的是gradle,第一次接触,感觉还是不错,在编译速度上超过了maven。

      compile('org.springframework.boot:spring-boot-starter')
      compile('org.springframework.boot:spring-boot-starter-log4j2')
      compile('org.springframework.boot:spring-boot-starter-web') {
        //exclude group:'org.springframework.boot',module:"spring-boot-starter-tomcat"
      }
      compile('org.springframework.boot:spring-boot-devtools')
      compile('org.springframework.boot:spring-boot-starter-test')
      compile('org.mybatis.spring.boot:mybatis-spring-boot-starter:1.3.0')
      
      compile('mysql:mysql-connector-java:6.0.5')
      compile('com.zaxxer:HikariCP:2.6.1')
      
      compile('org.projectlombok:lombok:1.16.16')
      
      //token
      compile("io.jsonwebtoken:jjwt:0.7.0")
      
      //bean copy
      compile('commons-beanutils:commons-beanutils:1.9.3')
      
      //apache commons lang3
      compile('org.apache.commons:commons-lang3:3.6')
      
      compile('javax.validation:validation-api:2.0.0.CR2')
      
      testCompile('junit:junit:4.12')
      
      

      项目中用到的jar包如上,springboot的版本号定义在buildscript里面在。使用的是1.5.2

  • 项目发布
    项目还是采用外置的tomcat发布,需要在启动类Application继承SpringBootServletInitializer,覆盖

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
       return builder.sources(Application.class);
    }
    
    

    发布的脚本上gradle clean build -x test

  • profile配置
    使用了gradle,在dev环境和prod环境的配置上,相对来说是没有maven好使的,
    需要启动的时候在环境变量里面配置-Dspring.profiles.active=dev
    其他的跟平常的spring开发还都是保持一致的。随着后续功能的开发慢慢会涉及到更多springboot的功能,个人感觉在开发效率上还是提升了不少。

后续研究了下gradle的分环境打包方式,参数网上的例子,操作步骤如下

  • 添加配置文件config.groovy
    environments {
    dev {
    }
    prod {
    }
    }
    在dev和prod写不同环境下的变量值,按照groovy的方式
  • 在build.gradle获取打包时指定的环境参数
    ext {
       profile = System.properties['spring.profiles.active']
    }
    
  • 读取config.groovy对应环境的的变量值

    def loadGroovyConfig(){
       def configFile = file('config.groovy')
       new ConfigSlurper(profile).parse(configFile.toURL()).toProperties()
    }
    
    processResources {
       from(sourceSets.main.resources.srcDirs) {
           filter(org.apache.tools.ant.filters.ReplaceTokens, tokens: loadGroovyConfig())
       }
    }
    
  • 变量覆盖如图
    log4j2
    后续打包还是按照正常的方式,指定-Dspring.profiles.active=dev或者prod就可以