- 并发概念:
并发:当有多个线程在操作时,如果系统只有一个CPU,则它根本不可能真正同时进行一个以上的线程,它只能把CPU运行时间划
分成若干个时间段,再将时间 段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状。.这种方式我们称
之为并发(Concurrent)。
并行:当系统有一个以上CPU时,则线程的操作有可能非并发。当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,
两个线程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)。
高并发:服务能同时处理很多请求,提高程序性能。
案例Demo:
package com.mmall.concurrency;
import com.mmall.concurrency.annoations.NotThreadSafe;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.*;
@Slf4j
@NotThreadSafe
public class ConcurrencyTest {
//请求总数
public static int clientTotal=5000;
//同时并发执行的线程数
public static int threadTotal=50;
public static int count=0;
public static void main(String[] args) throws Exception{
ExecutorService executorService= Executors.newCachedThreadPool();
final Semaphore semaphore=new Semaphore(threadTotal);
final CountDownLatch countDownLatch=new CountDownLatch(clientTotal);
for(int i=0;i<clientTotal;i++){
executorService.execute(()->{
try{
semaphore.acquire();
add();
semaphore.release();
}catch (Exception e){
log.error("exception",e);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
log.info("count:{}",count);
}
private static void add(){
count++;
}
}
基础知识:
CPU多级缓存:CPU速度远大于内存速度。
CPU多级缓存的问题:
缓存一致性(MESI协议):用于保证多核CPU cache缓存共享数据的一致性问题。
乱序执行优化:处理器为提高运算速度而作出违背代码原有顺序的优化。(单核稳定,多核易错)
如何解决?=====》首先弄清部分概念:Java内存模型(Java Memory Model)
Java内存模型:
为了屏蔽各种硬件和操作系统内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果,Java虚拟机规范中定义
了Java内存模型;Java内存模型本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,规定了一个线程如何和何
时可以看到其他线程修改后的共享变量的值,以及在必须时如何同步访问共享变量。
同步的八种操作和规则:
操作:
1.锁定(lock):作用于主内存的变量,把一个变量标识为一条线程独占状态。
2.解锁(unlock):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
3.读取(read):作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用。
4.load(载入):作用于工作内存的变量,他把read操作从主内存中得到的变量值放入工作内存的变量副本中。
5.使用(use):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎。
6.赋值(assign):作用于工作内存的变量,把一个从执行引擎接收到的值赋值给工作内存的变量。
7.存储(store):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
8.写入(write):作用于主内存的变量,把store操作从工作内存中一个变量的值传送到主内存的变量中。
规则:
1.如果把一个变量从主内存中复制到工作内存中,就需要按顺序地执行read和load操作;如果把变量从工作内存中同步回主内存中,就要按顺序的执行store和write操作。但是Java内存模型只要求上述操作必须按照顺序执行,而没有保证必须是连续执行的。
2.不允许read和load、store和write操作之一单独出现。
3.不允许一个线程丢弃它最近assign操作,即变量在工作内存中改变了之后必须同步到主内存中。
4.不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存中同步回主内存中。
5.一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即就是一个变量实施use和store操作之前,必循先执行assign和load操作。
6.一个变量在同一时刻只允许一条线程对其进行lock操作,但是lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操纵,变量才能被解锁。lock和unlock必须成对出现。
7.如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值。
8.如果一个变量事先没有被lock操作锁定,则不允许对它进行unlock操作,也不允许unlock一个被其他线程锁定的变量。
9.对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)
并发的优势和风险:
优势1:速度:同时处理多个请求,响应更快;复杂的操作可以分成多个进程同时进行。
优势2:设计:程序设计在某些情况下可以有更多的选择。
优势3:资源利用:CPU能够在等待IO的时候做一些其他的事情。
风险1:安全性:多个线程共享数据可能会产生期望不相符的结果。
风险2:活跃性:某个操作无法继续执行下去会产生活跃性问题,比如死锁、饥饿等问题。
风险3:性能:线程过多使的CPU频繁切换,调度时间增多;同步机制;消耗过多内存
解决方案体系:
一、线程安全性:
定义:当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些进程将如何交替执行,并且在主调代码中不需要额外的同步或者协同,这些类都能表现出正确行为,那么这个类是线程安全的。
体现在三个方面:
1.原子性:提供了互斥访问,同一时刻只能有一个线程来对它进行操作。
原子性---Atomic包:
(1) AtomicXXX:CAS/Unsafe.compareAndSwapInt
public static AtomicInteger count=new AtomicInteger(0);
AtomicInteger类中的方法:
public final int getAndSet(int newValue) {
return unsafe.getAndSetInt(this, valueOffset, newValue);
}
unsafe中该方法实现:
//var1表示当前对象,如本例中count,var2当前对象值,与var4执行相加
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
//var5底层变量
var5 = this.getIntVolatile(var1, var2);
//工作内存中的var2值和主内存中var5值不断比较,相同执行var5 + var4操作
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
(2) AutomicLong、LongAdder(区别)
LongAdder在AtomicLong的基础上将单点的更新压力分散到各个节点,在低并发的时候通过对base的直接更新可以很好
的保障和AtomicLong的性能基本保持一致,而在高并发的时候通过分散提高了性能。
缺点是LongAdder在统计的时候如果 有并发更新,可能导致统计的数据有误差。
在使用时候优先考虑使用LongAdder,低并发情况下可以考虑使用AutomicLong。
两者区别详见:https://blog.csdn.net/yao123long/article/details/63683991
(3)AtomicReference、AtomicReferenceFieldUpdater类====》更新数据
package com.mmall.concurrency.example.atomic;
import com.mmall.concurrency.annoations.ThreadSafe;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
@Slf4j
@ThreadSafe
public class AtomicExample4 {
private static AtomicReference<Integer> count=new AtomicReference<Integer>(0);
public static void main(String[] args) {
count.compareAndSet(0,2);
count.compareAndSet(0,1);
count.compareAndSet(1,3);
count.compareAndSet(2,4);
count.compareAndSet(3,5);
log.info("count:{}",count.get());
}
}
输出:4
package com.mmall.concurrency.example.atomic;
import com.mmall.concurrency.annoations.ThreadSafe;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
import java.util.concurrent.atomic.AtomicReference;
@Slf4j
@ThreadSafe
public class AtomicExample5 {
//原子性更新类的某个实例指定的某个字段count,count需要特殊关键词volatile修饰
private static AtomicIntegerFieldUpdater<AtomicExample5> updater=
AtomicIntegerFieldUpdater.newUpdater(AtomicExample5.class,"count");
@Getter
public volatile int count=100;
public static void main(String[] args) {
AtomicExample5 example5=new AtomicExample5();
if(updater.compareAndSet(example5,100,120)){
log.info("update success1,{}",example5.getCount());
}
if(updater.compareAndSet(example5,100,120)){
log.info("update success2,{}",example5.getCount());
}else {
log.info("update fail,{}",example5.getCount());
}
}
}
输出:
14:39:54.287 [main] INFO com.mmall.concurrency.example.atomic.AtomicExample5 - update success1,120
14:39:54.294 [main] INFO com.mmall.concurrency.example.atomic.AtomicExample5 - update fail,120
(4)AtmoicStampReference:CAS的ABA问题:变量值虽然相同,但是版本不同了。
(5)AtomicBoolean
//如何让某段代码只执行一次
package com.mmall.concurrency.example.atomic;
import com.mmall.concurrency.annoations.ThreadSafe;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
@Slf4j
@ThreadSafe
public class AtomicExample6 {
private static AtomicBoolean ishappend=new AtomicBoolean(false);
//请求总数
public static int clientTotal=5000;
//同时并发执行的线程数
public static int threadTotal=50;
public static void main(String[] args) throws Exception {
ExecutorService executorService = Executors.newCachedThreadPool();
final Semaphore semaphore = new Semaphore(threadTotal);
final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
for (int i = 0; i < clientTotal; i++) {
executorService.execute(() -> {
try {
semaphore.acquire();
test();
semaphore.release();
} catch (Exception e) {
log.error("exception", e);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
log.info("count:{}",ishappend.get());
}
private static void test(){
if(ishappend.compareAndSet(false,true)){
log.info("execute");
}
}
}
输出:
14:45:51.127 [pool-1-thread-1] INFO com.mmall.concurrency.example.atomic.AtomicExample6 - execute
14:45:51.412 [main] INFO com.mmall.concurrency.example.atomic.AtomicExample6 - count:true
原子性---锁:
synchronized:依赖JVM---不可中断锁,适合竞争不激烈,可读性好。
1.修饰代码块:大括号起来的代码,作用于调用的对象
2.修饰方法:整个方法,作用于调用的对象 -----子类无法继承父类的synchronized
3.修饰静态方法:整个静态方法,作用于所有的对象
4.修饰类:括号起来的部分,作用于所有对象
Lock:依赖特殊的CPU指令,代码实现,ReentrantLock---可中断锁,多样化同步,竞争激烈时能维持常态
Atomic:竞争激烈时能维持常态,比Lock性能好,只能同步一个值。
2.可见性:一个线程对内存的修改可以及时的被其他线程观察到。
导致共享变量在线程中不可见的原因:线程交叉执行;重排序结合线程交叉执行;共享变量更新后的值没有在工作内存与主存间及时更新。
可见性:
synchronized--JMM关于synchronized的两条规定:
线程解锁前,必须把共享变量的最新值刷新到主内存中;
线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值。
volatile--通过加入内存屏障和禁止重排序优化来实现
对volatile变量写操作时,会在写操作后加入一条store平渣指令,将本地内存中的共享变量值刷新到主内存中。
对volatile变量读操作时,会在读操作前加入一条load屏障指令,从主内存中读取共享变量。
---CPU指令级实现,并不是线程安全的,不具有原子性。
适合场景:状态标识量;doubleCheck(安全发布对象双重检测机制);
状态标识量例子伪代码:
volatile boolean inited=false;
//线程1:
context=loadContext();
inited=true;
//线程2:
while(!inited){
sleep();
}
doSomethingWithConfig(context);
3.有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序的存在,该观察结果一般杂乱无序。
Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响单线程程序的执行,却会影响到多线程并发执行的正确性。
Java先天有序性:happen...before原则
1.程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作。
-----虽然java虚拟机会重排序,但是只是对数据不依赖的指令重排序,最后执行的顺序看起来就是代码书写的顺序;无法保证多线程执行的正确性。
2.锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作。
3.volatile变量原则:对一个变量的写操作先行发生于后面对这个变量的读操作。
4.传递原则:A在B前,B在C前,则A在C前。
5-8:线程启动原则、线程中断原则、线程终结原则、对象终结原则。
-----如果两个操作的执行次序无法从该八个原则中推导出来,就不能保证有序性,虚拟机可以对他们进行重排序。
二、安全发布对象:
发布对象:使一个对象能够被当前范围之外的代码所使用。
对象逸出:一个错误的发布。当一个对象还没有构造完成时,就使它被其他线程所见。
安全发布对象的四种方法(对应视频5-2-5.3):
1.在静态初始化函数中初始化一个对象引用。
2.将对象的引用保存到volatile类型域或者AtomicReference对象中。
3.将对象的引用保存到某个正确构造对象的final类型域中。
4.将对象的引用保存到一个由锁保护的域中。
example1: 懒汉模式,单例实例在第一次使用时创建===>线程不安全
example2: 饿汉模式,单例实例在类装载创建=>线程安全;不足:构造方法中有过多处理会使得类加载慢;如果不调用会造成资源浪费;
example3: synchronized使懒汉模式线程安全,但是不推荐
example4:懒汉模式+双重检测机制+同步锁===》线程不安全
example5:懒汉模式+双重检测机制+同步锁+volatile===》线程安全
example6: 饿汉模式,使用静态块
example7: 枚举模式:最安全的===》对象的创建放在枚举内
package com.mmall.concurrency.example.singleton;
import com.mmall.concurrency.annoations.Recommend;
import javax.annotation.concurrent.ThreadSafe;
@ThreadSafe
@Recommend
/*
枚举模式:最安全的
*/
public class SingletonExample7 {
//私有构造函数
private SingletonExample7(){
}
//静态的工厂方法
public static SingletonExample7 getInstance(){
return Singleton.INSTANCE.getInstance();
}
private enum Singleton{
INSTANCE;
private SingletonExample7 singleton;
//jvm保证这个方法只会被调用一次
Singleton(){
singleton=new SingletonExample7();
}
public SingletonExample7 getInstance(){
return singleton;
}
}
}
三、不可变对象:
1.对象创建后其状态不能修改
2.对象所有域都是final类型
3.对象是正确创建的(创建期间,this引用没有逸出)
补充知识点:
final关键字:类、方法、变量
1. 修饰类不能被继承;
2.修饰方法:final修饰方法的目的:锁定方法不被继承类修改;提高代码执行效率。注意:一个类的privte方法回你被隐式 指定为final方法。
3.修饰变量:基本数据类型;引用型数据类型(初始化后不能指向另外一个对象,但可以修改内部的值)
创建不可变对象的三种方法:
1.final(如果是引用型变量初始化以后不能指向另外一个对象,但是可以修改)
package com.mmall.concurrency.example.immutable;
import com.google.common.collect.Maps;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
@Slf4j
public class ImmutableExample1 {
private final static Integer a=1;
private final static String b="2";
private final static Map<Integer,Integer> map=Maps.newHashMap();
static {
map.put(1,2);
map.put(3,4);
map.put(5,6);
}
public static void main(String[] args) {
// a=2;
// b="3";
map.put(1,3);
log.info("{}",map.get(1));
}
private void test(final int a){
// a=1;
}
}
2.Collections.unmodifiableXXX:Collection、List、Set、Map...(Collections.unmodifiableMap处理后不可修改,如果修改执行会抛出异常)
package com.mmall.concurrency.example.immutable;
import com.google.common.collect.Maps;
import com.mmall.concurrency.annoations.ThreadSafe;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.CollectionUtils;
import java.util.Collections;
import java.util.Map;
@Slf4j
@ThreadSafe
public class ImmutableExample2 {
private static Map<Integer,Integer> map=Maps.newHashMap();
static {
map.put(1,2);
map.put(3,4);
map.put(5,6);
//Collections.unmodifiableMap处理后不可修改
map = Collections.unmodifiableMap(map);
}
public static void main(String[] args) {
map.put(1,3);
log.info("{}",map.get(1));
}
}
3.Guava:ImmutableXXX:Collection、List、Set、Map...(生成对象初始化以后不可以修改,安全的,如果修改执行会抛出异常)
package com.mmall.concurrency.example.immutable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.mmall.concurrency.annoations.ThreadSafe;
@ThreadSafe
public class ImmutableExample3 {
private final static ImmutableList list=ImmutableList.of(1,2,3);
private final static ImmutableSet set=ImmutableSet.copyOf(list);
// private final static ImmutableMap<Integer,Integer> map=ImmutableMap.of(1,2,3,4);
private final static ImmutableMap<Integer,Integer> map=ImmutableMap.<Integer,Integer>
builder().put(1,2).put(3,4).build();
public static void main(String[] args) {
// list.add(4);
}
}
四、线程封闭:将对象封装到一个线程中来保证线程安全。
1.堆栈封闭:定义局部变量不会被多个线程共享,无并发问题,经常在用却未察觉。
2.ThreadLocal线程封闭:特别好的封闭方法;Map-->key对应线程,value对应对象。
ThreadLocal用于保存某个线程共享变量:对于同一个static ThreadLocal,不同线程只能从中get,set,remove自己的变量,而不会影响其他线程的变量。
1、ThreadLocal.get: 获取ThreadLocal中当前线程共享变量的值。
2、ThreadLocal.set: 设置ThreadLocal中当前线程共享变量的值。
3、ThreadLocal.remove: 移除ThreadLocal中当前线程共享变量的值。
4、ThreadLocal.initialValue: ThreadLocal没有被当前线程赋值时或当前线程刚调用remove方法后调用get方法,返回此方法值。
ThreadLocal用法详解和原理:https://www.cnblogs.com/coshaho/p/5127135.html
应用场景:将前端请求request的相关信息放入ThreadLocal中。
package com.mmall.concurrency.example.threadLocal;
/**
* TreadLocal所在的类,请求数据处理的类
*/
public class RequestHolder {
private final static ThreadLocal<Long> requestHolder=new ThreadLocal<>();
public static void add(Long id){
requestHolder.set(id);
}
public static Long getId(){
return requestHolder.get();
}
//防止内存泄露
public static void remove(){
requestHolder.remove();
}
}
package com.mmall.concurrency;
import com.mmall.concurrency.example.threadLocal.RequestHolder;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
/**
* 过滤器
*/
@Slf4j
public class HttpFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request=(HttpServletRequest)servletRequest;
log.info("do filter,{},{}",Thread.currentThread().getId(),request.getServletPath());
RequestHolder.add(Thread.currentThread().getId());
filterChain.doFilter(servletRequest,servletResponse);
}
@Override
public void destroy() {
}
}
package com.mmall.concurrency;
import com.mmall.concurrency.example.threadLocal.RequestHolder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 拦截器
*/
@Slf4j
public class HttpInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("preHandle");
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
RequestHolder.remove();
log.info("afterCompletion");
return;
}
}
package com.mmall.concurrency.example.threadLocal;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
@RequestMapping("/threadLocal")
@Slf4j
public class ThreadLocalController {
@RequestMapping("/test")
@ResponseBody
public Long test(){
log.info("test");
return RequestHolder.getId();
}
}
package com.mmall.concurrency;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
@SpringBootApplication
public class ConcurrencyApplication extends WebMvcConfigurerAdapter{
public static void main(String[] args) {
SpringApplication.run(ConcurrencyApplication.class, args);
}
/**
*启动类配置过滤器和拦截器
*/
@Bean
public FilterRegistrationBean httpFilter(){
FilterRegistrationBean registrationBean=new FilterRegistrationBean();
registrationBean.setFilter(new HttpFilter());
registrationBean.addUrlPatterns("/threadLocal/*");
return registrationBean;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new HttpInterceptor()).addPathPatterns("/**");
}
}
执行过程:前端请求->过滤器(do filter,44,/threadLocal/test)将相关信息添加到TreadLocal对象中
->拦截器preHandle处理->Controller层打印test->拦截器afterCompletion执行TreadLocal对象remove方法,防止内存泄漏
输出:
2018-08-14 21:23:57.610 INFO 35187 --- [nio-8080-exec-1] com.mmall.concurrency.HttpFilter : do filter,44,/threadLocal/test
2018-08-14 21:23:57.623 INFO 35187 --- [nio-8080-exec-1] com.mmall.concurrency.HttpInterceptor : preHandle
2018-08-14 21:23:57.724 INFO 35187 --- [nio-8080-exec-1] c.m.c.e.t.ThreadLocalController : test
2018-08-14 21:23:57.902 INFO 35187 --- [nio-8080-exec-1] com.mmall.concurrency.HttpInterceptor : afterCompletion
总结和补充:
常见线程不安全类与写法:
1.StringBuilder---->StringBuffer(方法都使用了synchronized)
2.SimpleDateFormat---->JodaTime
3.ArrayList、HashSet、HashMap等Collections
4.先检查后执行:if(condition(a)){handle(a)}
五、线程安全---同步容器类(并不一定线程安全)
1.ArrayList--->Vector,Stack
2.HashMap--->HashTable(key和value不能为null)
3.Collections.synchronizedXXX(List、Set、Map)
缺陷:多数容器类都是非线程安全的,即使部分容器是线程安全的,由于使用sychronized进行锁控制,导致读/写均需进行锁操作,性能很低。
同步容器类详情:https://www.cnblogs.com/dolphin0520/p/3933404.html
六、线程安全---并发容器J.U.C
ArrayList-->CopyOnwriteArrayList(适合读多写少)
HashSet、TreeSet--->CopyOnWriteArraySet、ConcurrentSkipListSet
HashMap、TreeMap--->ConcurrentHashMap(性能非常好)、ConcurrentSkipListMap(key排序性能好)
并发容器J.U.C相对于同步器:https://www.cnblogs.com/daoqidelv/p/6753162.html
总结和补充:
安全共享对象策略:
1.线程限制:一个被线程限制的对象,由线程独占,并且只能被占有它的线程修改。
2.共享只读:一个共享只读的对象,在没有额外同步的情况下,可以被多个线程并发访问,但是任何线程都不能修改它。
3.线程安全对象:一个线程安全的对象或者容器,在内部通过同步机制来保证线程安全,所以其他线程无需额外的同步就可以通过公共接口随意访问它。
4.被守护对象:被守护对象只能通过获取特定的锁来访问。
J.U.C(java.util.concurrent)包:
核心:AQS(AbstractQueuedSynchronized)
AQS设计思想:详见相关博客:https://www.cnblogs.com/chengxiao/archive/2017/07/24/7141160.html
1.使用Node实现FIFO队列,可以用于构建或者其他同步装置的基础框架。
2.利用一个int类型表示状态。
3.使用方法是继承,子类通过继承并通过实现它的方法管理其状态,(acquire和release)的方法操纵状态。
4.可以同时实现排他锁和共享锁模式(独占和共享)。
AQS是JUC中很多同步组件的构建基础,简单来讲,它内部实现主要是状态变量state和一个FIFO队列来完成,同步队列的头结点是当前获取到同步状态的结点,获取同步状态state失败的线程,会被构造成一个结点(或共享式或独占式)加入到同步队列尾部(采用自旋CAS来保证此操作的线程安全),随后线程会阻塞;释放时唤醒头结点的后继结点,使其加入对同步状态的争夺中。
AQS为我们定义好了顶层的处理实现逻辑,我们在使用AQS构建符合我们需求的同步组件时,只需重写tryAcquire,tryAcquireShared,tryRelease,tryReleaseShared几个方法,来决定同步状态的释放和获取即可,至于背后复杂的线程排队,线程阻塞/唤醒,如何保证线程安全,都由AQS为我们完成了,这也是非常典型的模板方法的应用。AQS定义好顶级逻辑的骨架,并提取出公用的线程入队列/出队列,阻塞/唤醒等一系列复杂逻辑的实现,将部分简单的可由使用者决定的操作逻辑延迟到子类中去实现。
AQS同步组件:CountDownLatch、Semaphore、CyclicBarrier、ReentrantLock、Condition、FutureTask
CountDownLatch:
CountDownLatch是一个同步工具,它主要用线程执行之间的协作。CountDownLatch 的作用和 Thread.join()方法类似,让一些线程阻塞直到另一些线程完成一系列操作后才被唤醒。在直接创建线程的年代(Java 5.0 之前),我们可以使用 Thread.join()。在线程池出现后,因为线程池中的线程不能直接被引用,所以就必须使用 CountDownLatch了。
实现原理:计数器的值由构造函数传入,并用它初始化AQS的state值。当线程调用await方法时会检查state的值是否为0,如果是就直接返回(即不会阻塞);如果不是,将表示该节点的线程入列,然后将自身阻塞。当其它线程调用countDown方法会将计数器减1,然后判断计数器的值是否为0,当它为0时,会唤醒队列中的第一个节点,由于CountDownLatch使用了AQS的共享模式,所以第一个节点被唤醒后又会唤醒第二个节点,以此类推,使得所有因await方法阻塞的线程都能被唤醒而继续执行。
应用场景1:某一线程在开始运行前等待n个线程执行完毕。将CountDownLatch的计数器初始化为n new CountDownLatch(n)
,每当一个任务线程执行完毕,就将计数器减1 countdownlatch.countDown()
,当计数器的值变为0时,在CountDownLatch上 await()
的线程就会被唤醒。一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行;另外 await()
可以限制时间,如果其他不能在规定时间内完成,该等待主线程也不再等待了。
应用场景2:实现多个线程开始执行任务的最大并行性。注意是并行性,不是并发,强调的是多个线程在某一时刻同时开始执行。类似于赛跑,将多个线程放到起点,等待发令枪响,然后同时开跑。做法是初始化一个共享的CountDownLatch(1),将其计数器初始化为1,多个线程在开始执行任务前首先 coundownlatch.await()
,当主线程调用 countDown()
时,计数器变为0,多个线程同时被唤醒。
缺点:CountDownLatch是一次性的,计数器的值只能在构造方法中初始化一次,之后没有任何机制再次对其设置值,当CountDownLatch使用完毕后,它不能再次被使用。
应用场景1例子:
package com.mmall.concurrency.example.aos;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
@Slf4j
public class CountDownLatchExample2 {
private final static int threadTotal=200;
public static void main(String[] args) throws Exception {
ExecutorService executorService=Executors.newCachedThreadPool();
CountDownLatch count=new CountDownLatch(threadTotal);
for(int i=0;i<threadTotal;i++){
final int threadNum=i;
executorService.execute(()->{
try {
test(threadNum);
}catch (Exception e){
log.error("Exception",e);
}finally {
count.countDown();
}
});
}
count.await(10,TimeUnit.MILLISECONDS);
log.info("finished");
System.out.println(count);
executorService.shutdown();
}
public static void test(int threadNum) throws Exception{
Thread.sleep(100);
log.info("threadNum={}",threadNum);
}
}
输出:
15:22:20.581 [pool-1-thread-2] INFO com.mmall.concurrency.example.aos.CountDownLatchExample2 - threadNum=1
15:22:20.581 [pool-1-thread-1] INFO com.mmall.concurrency.example.aos.CountDownLatchExample2 - threadNum=0
15:22:20.581 [pool-1-thread-3] INFO com.mmall.concurrency.example.aos.CountDownLatchExample2 - threadNum=2
15:22:20.586 [pool-1-thread-4] INFO com.mmall.concurrency.example.aos.CountDownLatchExample2 - threadNum=3
15:22:20.678 [pool-1-thread-5] INFO com.mmall.concurrency.example.aos.CountDownLatchExample2 - threadNum=4
15:22:20.678 [pool-1-thread-6] INFO com.mmall.concurrency.example.aos.CountDownLatchExample2 - threadNum=5
15:22:20.679 [pool-1-thread-7] INFO com.mmall.concurrency.example.aos.CountDownLatchExample2 - threadNum=6
15:22:20.683 [pool-1-thread-8] INFO com.mmall.concurrency.example.aos.CountDownLatchExample2 - threadNum=7
15:22:20.683 [pool-1-thread-11] INFO com.mmall.concurrency.example.aos.CountDownLatchExample2 - threadNum=10
15:22:20.683 [pool-1-thread-12] INFO com.mmall.concurrency.example.aos.CountDownLatchExample2 - threadNum=11
15:22:20.683 [pool-1-thread-14] INFO com.mmall.concurrency.example.aos.CountDownLatchExample2 - threadNum=13
15:22:20.683 [pool-1-thread-9] INFO com.mmall.concurrency.example.aos.CountDownLatchExample2 - threadNum=8
15:22:20.683 [pool-1-thread-10] INFO com.mmall.concurrency.example.aos.CountDownLatchExample2 - threadNum=9
15:22:20.683 [pool-1-thread-13] INFO com.mmall.concurrency.example.aos.CountDownLatchExample2 - threadNum=12
15:22:20.684 [pool-1-thread-17] INFO com.mmall.concurrency.example.aos.CountDownLatchExample2 - threadNum=16
15:22:20.684 [pool-1-thread-19] INFO com.mmall.concurrency.example.aos.CountDownLatchExample2 - threadNum=18
15:22:20.684 [pool-1-thread-21] INFO com.mmall.concurrency.example.aos.CountDownLatchExample2 - threadNum=20
15:22:20.683 [pool-1-thread-15] INFO com.mmall.concurrency.example.aos.CountDownLatchExample2 - threadNum=14
15:22:20.683 [pool-1-thread-16] INFO com.mmall.concurrency.example.aos.CountDownLatchExample2 - threadNum=15
15:22:20.684 [pool-1-thread-18] INFO com.mmall.concurrency.example.aos.CountDownLatchExample2 - threadNum=17
15:22:20.684 [pool-1-thread-22] INFO com.mmall.concurrency.example.aos.CountDownLatchExample2 - threadNum=21
15:22:20.684 [pool-1-thread-23] INFO com.mmall.concurrency.example.aos.CountDownLatchExample2 - threadNum=22
15:22:20.688 [pool-1-thread-20] INFO com.mmall.concurrency.example.aos.CountDownLatchExample2 - threadNum=19
15:22:20.688 [pool-1-thread-26] INFO com.mmall.concurrency.example.aos.CountDownLatchExample2 - threadNum=25
15:22:20.688 [pool-1-thread-27] INFO com.mmall.concurrency.example.aos.CountDownLatchExample2 - threadNum=26
15:22:20.688 [pool-1-thread-28] INFO com.mmall.concurrency.example.aos.CountDownLatchExample2 - threadNum=27
15:22:20.688 [pool-1-thread-29] INFO com.mmall.concurrency.example.aos.CountDownLatchExample2 - threadNum=28
15:22:20.688 [pool-1-thread-30] INFO com.mmall.concurrency.example.aos.CountDownLatchExample2 - threadNum=29
15:22:20.688 [pool-1-thread-33] INFO com.mmall.concurrency.example.aos.CountDownLatchExample2 - threadNum=32
15:22:20.688 [main] INFO com.mmall.concurrency.example.aos.CountDownLatchExample2 - finished
java.util.concurrent.CountDownLatch@47d384ee[Count = 171]
15:22:20.689 [pool-1-thread-24] INFO com.mmall.concurrency.example.aos.CountDownLatchExample2 - threadNum=23
15:22:20.689 [pool-1-thread-34] INFO com.mmall.concurrency.example.aos.CountDownLatchExample2 - threadNum=33
15:22:20.689 [pool-1-thread-35] INFO com.mmall.concurrency.example.aos.CountDownLatchExample2 - threadNum=34
15:22:20.689 [pool-1-thread-36] INFO com.mmall.concurrency.example.aos.CountDownLatchExample2 - threadNum=35
15:22:20.689 [pool-1-thread-4] INFO com.mmall.concurrency.example.aos.CountDownLatchExample2 - threadNum=40
15:22:20.690 [pool-1-thread-3] INFO com.mmall.concurrency.example.aos.CountDownLatchExample2 - threadNum=41
......
Semaphore:
Semaphore是一个计数信号量,它的本质是一个”共享锁”。
信号量维护了一个信号量许可集。线程可以通过调用acquire(),来获取信号量的许可;当信号量中有可用的许可时,线程能获
取该许可;否则线程必须等待,直到有可用的许可为止。线程可以通过release()来释放它所持有的信号量许可。注意acquire()可
以传入多个参数,例如一次获取多个许可,超时时间等。
适用场景:例如数据库连接的数量有限,远低于并发访个数。
package com.mmall.concurrency.example.aos;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
@Slf4j
public class SemaphoreExample1 {
private final static int threadTotal=20;
private final static int threadConcurrent=3;
public static void main(String[] args) throws Exception {
ExecutorService executorService=Executors.newCachedThreadPool();
final Semaphore semaphore=new Semaphore(threadConcurrent);
for(int i=0;i<threadTotal;i++){
final int threadNum=i;
executorService.execute(()->{
try {
semaphore.acquire();//获取一个许可
test(threadNum);
semaphore.release();//释放一个许可
}catch (Exception e){
log.error("Exception",e);
}
});
}
executorService.shutdown();
}
public static void test(int threadNum) throws Exception{
log.info("threadNum={}",threadNum);
Thread.sleep(1000);
}
}
CyclicBarrier:
CyclicBarrier字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier默认的构造方法是CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞。
CountDownLatch和CyclicBarrier的区别:
CountDownLatch: 一个线程(或者多个), 等待另外N个线程完成某个事情之后才能执行。
CyclicBarrier: N个线程相互等待,任何一个线程完成之前,所有的线程都必须等待。
这样应该就清楚一点了,对于CountDownLatch来说,重点是那个“一个线程”, 是它在等待,而另外那N的线程在把“某个事情”做完之后可以继续等待,可以终止。
而对于CyclicBarrier来说,重点是那N个线程,他们之间任何一个没有完成,所有的线程都必须等待。
CountDownLatch 是计数器, 线程完成一个就记一个, 就像报数一样, 只不过是递减的。
而CyclicBarrier更像一个水闸, 线程执行就想水流, 在水闸处都会堵住, 等到水满(线程到齐)了, 才开始泄流。
CyclicBarrier计数器可以重置,循环使用。
package com.mmall.concurrency.example.aos;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@Slf4j
public class CyclicBarrierExample2 {
private final static int threadTotal=20;
private static CyclicBarrier cyclicBarrier=new CyclicBarrier(5,()->{
log.info("start~~~~~");
});
public static void main(String[] args) throws Exception {
ExecutorService executorService=Executors.newCachedThreadPool();
for(int i=0;i<threadTotal;i++){
final int threadNum=i;
Thread.sleep(1000);
executorService.execute(()->{
try {
test(threadNum);
}catch (Exception e){
log.error("Exception",e);
}
});
}
executorService.shutdown();
}
public static void test(int threadNum) throws Exception{
Thread.sleep(1000);
log.info("threadNum,wait={}",threadNum);
cyclicBarrier.await();
log.info("threadNum,continue={}",threadNum);
}
}
ReentrantLock:
synchronized是jvm层面的锁,是不会引发死锁的。ReentrantLock需要加锁和解锁,有死锁可能。
在JDK5.0版本之前,重入锁的性能远远好于synchronized关键字,JDK6.0版本之后synchronized 得到了大量的优化,二者
性能也不分伯仲,但是重入锁是可以完全替代synchronized关键字的。此外,重入锁又自带一系列高逼格:可中断响应、锁申请
等待限时、可以指定非公平锁和公平锁(先等待的先执行),另外可以结合Condition来使用,可以分组唤醒需要唤醒的线程。
适用场景:
1.只有少量线程的时候适合Synchronized
2.线程不少,但是增长可以预估的适合ReentrantLock
private static void add(){
lock.lock();
try {
count++;
}catch (Exception e){
log.info("exception={}",e);
}finally {
lock.unlock();
}
}
Condition:
Condition的作用是对锁进行更精确的控制。Condition中的await()方法相当于Object的wait()方法,Condition中的signal()方法
相当于Object的notify()方法,Condition中的signalAll()相当于Object的notifyAll()方法.不同的是Object中的wait(),notify(),notifyAll()
方法是和"同步锁" (synchronized关键字)捆绑使用的;而Condition是需要与"互斥锁"/"共享锁"捆绑使用的。
CountDownLatch、CyclicBarrier、Condition都是一个同步工具,它主要用线程执行之间的协作。
ReentrantLock配合Condition使用:
package com.mmall.concurrency.example.lock;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@Slf4j
public class LockExample2 {
public static void main(String[] args) {
ReentrantLock reentrantLock=new ReentrantLock();
Condition condition=reentrantLock.newCondition();
new Thread(()->{
try{
reentrantLock.lock();
log.info("wait signal");//执行顺序1
condition.await();//等待状态,其他线程可以获取锁
}catch (Exception e){
e.printStackTrace();
}
log.info("get signal");//执行顺序4
reentrantLock.unlock();
}).start();
new Thread(()->{
reentrantLock.lock();
log.info("get lock");//执行顺序2
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
condition.signalAll();
log.info("send signal");//执行顺序3
reentrantLock.unlock();
}).start();
}
}
FutureTask(并不是AQS子类):
在Java中一般通过继承Thread类或者实现Runnable接口这两种方式来创建多线程,但是这两种方式都有个缺陷,就是不能
在执行完成后获取执行的结果,因此Java1.5之后提供了Callable和Future接口,通过它们就可以在任务执行完毕之后得到任务的
执行结果。
详见: http://www.importnew.com/25286.html
package com.mmall.concurrency.example.future;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.*;
@Slf4j
public class FutureExample1 {
static class myCallable implements Callable<String> {
@Override
public String call() throws Exception {
log.info("do something in callable");
Thread.sleep(5000);
return "done";
}
}
public static void main(String[] args) throws Exception{
ExecutorService executors=Executors.newCachedThreadPool();
Future<String> future=executors.submit(new myCallable());
log.info("do something in main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
String result=future.get();
log.info("result={}",result);
}
}
package com.mmall.concurrency.example.future;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.*;
@Slf4j
public class FutureTaskExample2 {
public static void main(String[] args) throws Exception{
FutureTask<String> futureTask=new FutureTask<String>(new Callable<String>() {
@Override
public String call() throws Exception {
log.info("do something in callable");
Thread.sleep(5000);
return "done";
}
});
new Thread(futureTask).start();
log.info("do something in main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
String result=futureTask.get();
log.info("result={}",result);
}
}
Fork/Join框架详解(与Future相关):
Fork/Join框架是Java 7提供的一个用于并行执行任务的框架,是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。===>MapReduce
Fork/Join框架要完成两件事情:
1. 任务分割:首先Fork/Join框架需要把大的任务分割成足够小的子任务,如果子任务比较大的话还要对子任务进行继续分割。
2. 执行任务并合并结果:分割的子任务分别放到双端队列里,然后几个启动线程分别从双端队列里获取任务执行。子任务执行
完的结果都放在另外一个队列里,启动一个线程从队列里取数据,然后合并这些数据。
详见: https://www.cnblogs.com/senlinyang/p/7885964.html
package com.mmall.concurrency.example.forkjoin;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.Future;
import java.util.concurrent.RecursiveTask;
@Slf4j
public class ForkJoinExample1 extends RecursiveTask<Integer> {
private static final int threshold=2;
private int start;
private int end;
public ForkJoinExample1(int start,int end){
this.start=start;
this.end=end;
}
@Override
protected Integer compute() {
int n=start-end;
int sum=0;
if(n<threshold){
for(int i=start;i<=end;i++){
sum+=i;
}
}else{
int mid=(start+end)/2;
ForkJoinExample1 leftTask=new ForkJoinExample1(start,mid);
ForkJoinExample1 rightTask=new ForkJoinExample1(mid+1,end);
//执行子任务
leftTask.fork();
rightTask.fork();
//等待任务执行结束 合并结果
int leftResult=leftTask.join();
int rightResult=rightTask.join();
sum=leftResult+rightResult;
}
return sum;
}
public static void main(String[] args) {
ForkJoinExample1 task=new ForkJoinExample1(1,100);
ForkJoinPool forkJoinPool=new ForkJoinPool();
Future<Integer> future=forkJoinPool.submit(task);
try {
int result=future.get();
log.info("result={}",result);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
BlockingQueue:
在新增的Concurrent包中,BlockingQueue很好的解决了多线程中,如何高效安全“传输”数据的问题。通过这些高效并且线
程安全的队列类,为我们快速搭建高质量的多线程程序带来极大的便利。多线程环境中,通过队列可以很容易实现数据共享,比
如经典的“生产者”和“消费者”模型中,通过队列可以很便利地实现两者之间的数据共享。假设我们有若干生产者线程,另外又有若
干个消费者线程。如果生产者线程需要把准备好的数据共享给消费者线程,利用队列的方式来传递数据,就可以很方便地解决他
们之间的数据共享问题。但如果生产者和消费者在某个时间段内,万一发生数据处理速度不匹配的情况呢?理想情况下,如果生
产者产出数据的速度大于消费者消费的速度,并且当生产出来的数据累积到一定程度的时候,那么生产者必须暂停等待一下(阻
塞生产者线程),以便等待消费者线程把累积的数据处理完毕,反之亦然。然而,在concurrent包发布以前,在多线程环境下,
我们每个程序员都必须去自己控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。好在此
时,强大的concurrent包横空出世了,而他也给我们带来了强大的BlockingQueue。
详见:https://www.cnblogs.com/KingIceMou/p/8075343.html
七、线程池:
在开发中,频繁的创建和销毁一个线程,是很耗资源的,为此找出了一个可以循环利用已经存在的线程来达到自己的目
的,线程池顾名思义,也就是线程池的集合,通过线程池执行的线程任务,可以很有效的去规划线程的使用。
new Thread()弊端:
1.每次new Thread 新建对象,性能差。
2.线程缺乏统一管理,可能无限制的新建线程,相互竞争,有可能占用过多系统资源导致死机或OOM(Out Of Memory)
3.缺少更多功能,如更多执行、定期执行、线程中断。
线程池的好处:
1.重用存在的线程,减少对象创建、消亡的开销,性能佳。
2.可有效控制最大并发线程数,提高系统资源利用率,同时可以避免过多资源竞争,避免阻塞。
3.提供定时执行、定期执行、单线程、并发数控制等功能。
线程池类:ThreadPoolExecutor-->构造方法相关参数
1.corePoolSize:核心线程数量
2.maxmumPoolSize:线程最大线程数
3.workQueue:阻塞队列,存储等待执行的任务,很重要,会对线程池运行过程产生重大影响。
4.keepAliveTime:线程没有任务执行时最多保持多久时间终止
5.unit:keepAliveTime时间单位
6.threadFactory:线程工厂,用来创建线程
7.rejectHandler:当拒绝处理任务时的策略
ThreadPoolExecutor提供的方法:
基础方法:
1.execute():提交任务,交给线程池执行
2.submit():提交任务,能够返回执行结果=execute+Future
3.shutdown():关闭线程池,等待任务都完成
4.shutdownNow():关闭线程池,不等待任务执行完
监控方法:
1.getTaskCount():线程池已执行和未执行的任务总数。
2.getCompletedTaskCount():已完成的任务数量
3.getPoolSize():线程池当前的线程数量。
4.getPoolSize():线程池当前的线程数量
5.getActiveCount():当前线程池中正在执行任务的线程数量。
线程池---Executor框架接口:(以下四种线程池都是由一个线程工具类Executors来创造的)
1.Executors.newCachedThreadPool:创建一个可缓存线程池,如果线程池长度超过需要的线程数量,可灵活回收空闲线程,若无可回收,则新建线程。
2.Executors.newFixedThreadPool:创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待,当创建的线程池数量为1的时候。也类似于单线程化的线程池,当为1的时候,也可控制线程的执行顺序
3.Executrors.newSingleThreadPool:创建一个单线程化的线程池, 它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行,可以控制线程的执行顺序
4.Executors.newScheduledTreadPool:创建一个定长线程池,支持定时及周期性任务执行。,可以作一个定时器使用。
详见:https://www.cnblogs.com/qm-article/p/7821602.html
八、多线程扩展知识:
多线程产生死锁的条件:互斥条件、请求和保持条件、不剥夺条件、环路等待条件
九、多线程并发最佳实践:
1.使用本地变量
2.使用不可变类
3.最小化锁的作用于范围:s=1/(1-a+a/n)---->阿姆达尔公式
阿姆达尔曾致力于并行处理系统的研究。对于固定负载情况下描述并行处理效果的加速比s,阿姆达尔经过深入研究给出了
S=1/(1-a+a/n)其中,a为并行计算部分所占比例,n为并行处理结点个数。这样,当1-a=0时,(即没有串行,只有并行)最大加
速比s=n;当a=0时(即只有串行,没有并行),最小加速比s=1;当n→∞时,极限加速比s→ 1/(1-a),这也就是加速比的上限
例如,若串行代码占整个代码的25%,则并行处理的总体性能不可能超过4。
4.使用线程池的Executor,而不是直接new Thread执行
5.宁可使用同步也不要使用线程的wait和notify
6.使用BlockingQueue实现生产-消费模式
7.使用并发集合而不是加了锁的同步集合
8.使用Semaphore创建有界的访问
9.宁可使用同步代码块也不要使用同步方法
10.避免使用静态变量
重要专题讲解:
一、Spring与线程安全:
SpringMVC是基于方法的拦截,而Struts2是基于类的拦截。
对于Struts2来说,因为每次处理一个请求,struts就会实例化一个对象,这样就不会有线程安全的问题了;
而Spring的controller默认是Singleton的,这意味着每一个request过来,系统都会用原有的instance去处理。
好处:一是我们不用每次创建Controller,二是减少了对象创建和垃圾回收的时间;
缺点:只有一个Controller的instance,当多个线程调用它的时,里面的instance变量就不是线程安全的了,会发生窜数据问题。
当然大多数情况下,我们根本不需要考虑线程安全的问题,比如dao,service等,除非在bean中声明了实例变量。因此,我们在使用spring mvc 的contrller时,应避免在controller中定义实例变量。
保证Spring线程安全的策略:
1、在控制器中不使用实例变量(可以使用方法参数的形式解决)
2、在spring配置文件Controller声明 scope="prototype",每次都创建新controller。默认Controller、Dao、Service都是单例的。3、在Controller中使用ThreadLocal变量
详见:https://blog.csdn.net/a236209186/article/details/61460211
二、HashMap和ConcurrentHashMap
HashMap不支持并发操作,线程不安全。HashMap 里面是一个数组,然后数组中每个元素是一个单向链表。在插入新值的时候,如果当前的 size 已经达到了阈值,并且要插入的数组位置上已经有元素,那么就会触发扩容,扩容后,数组大小为原来的 2 倍。扩容就是用一个新的大数组替换原来的小数组,并将原来数组中的值重新哈希迁移到新的数组中,多线程进行该步骤操作会造成线程不安全。
Java7中ConcurrentHashMap 是一个 Segment 数组,Segment 通过继承 ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全。concurrencyLevel:并发数或者说Segment 数,默认是 16,也就是说 ConcurrentHashMap 有 16 个 Segments,所以理论上,这个时候,最多可以同时支持 16 个线程并发写,分布在不同的 Segment 上。这个值可以在初始化的时候设置为其他值,初始化以后不允许扩容的。而具体到每个 Segment 内部,其实每个 Segment 很像之前介绍的 HashMap,不过它要保证线程安全,所以处理起来要麻烦些。
Java8 中对HashMap 进行了一些修改,最大的不同就是利用了红黑树,所以其由数组+链表+红黑树组成。根据 Java7 HashMap 的介绍,根据 hash 值我们能够快速定位到数组的具体下标,但是之后的话,需要顺着链表一个个比较下去才能找到我们需要的,时间复杂度取决于链表的长度,为O(n)。为了降低这部分的开销,在 Java8 中,当链表中的元素超过了 8 个以后,会将链表转换为红黑树,在这些位置进行查找的时候可以降低时间复杂度为 O(logN)。
Java8中ConcurrentHashMap 就不讨论了,太难了。
详见:http://www.importnew.com/28263.html
高并发之扩容思路:
1、垂直扩容(纵向扩展):提高系统部件能力以提高效率。
2、水平扩容(横向扩展):增加更多系统成员来实现。
数据库--扩容:
1、读操作扩展:memcache、redis、CDN等缓存
2、写操作扩展。
主要看自身业务需求,如果是博客系统,读操作远多于写操作,那就想办法进行读操作扩展。