java并发编程和高并发基础一

一 基本概念

1.1 并发

多线程处理,要保证线程的安全执行。

1.2 高并发

同时处理多个请求,可以通过这种手段优化

1.3 JMM

是一种规范,规范了jvm和计算机主存之间如何协同工作,规范了一个线程得到其他线程修改共享变量之后的值以及读取共享变量,来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。
Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存中取出变量这样的底层细节。此处的变量包括了实例字段、静态字段和构成数组对象的元素,但不包括局部变量与方法参数,因为后者是线程私有的,不会被共享。
注意:如果局部变量是一个reference类型,它引用的对象在Java堆中可被各个线程共享,但是reference本身在Java栈的局部变量表中,它是线程私有的。

1.3.1 追溯:

计算机的运算速度与它的存储和通信子系统速度的差距太大(几个数量级的差距,一般会有高速缓存),大量的时间都花费在磁盘I/O、网络通信或者数据库访问上。如果不希望处理器在大部分时间都处于等待其他资源的状态,就必须使用一些手段去把处理器的运算能力"压榨"出来。同时衡量一个服务器性能的高低好坏,TPS是重要指标之一。对于高效并发虚拟机如何实现多线程、多线程之间由于共享和竞争数据而导致的一系列问题及解决方案。
在这里插入图片描述

1.3.1.1如何实现一致性
1.3.1.1.1 锁实现

锁住总线期间,其他CPU无法访问内存,导致效率低下。

1.3.1.1.2 缓存一致性协议

当CPU写数据时,如果发现操作的变量时共享变量,即在其他CPU也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存设置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中该缓存变量时无效的,那么它就会从内存中重新读取。

1.3.2 主内存与工作内存

Java内存模型规定了所有的变量都存储在主内存中(此处的主内存与介绍物理硬件时的主内存名字一样,两者也可以相互类比,但此处仅是虚拟机内存的一部分)。每条线程还有自己的工作内存(可与高速缓存类比),线程的工作内存中保存了该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值)都必须都在工作内存中进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成,线程、主内存、工作内存三者的关系如图:
在这里插入图片描述
在这里插入图片描述

1.3.3 Java内存模型同步的八种操作和同步规则

关于主内存与工作内存之间具体的交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存之类的实现细节,Java内存模型中定义了以下8种操作来完成,虚拟机实现时必须保证下面提及的每一种操作都是原子的、不可再分的(对于double和long类型的变量来说,load,store,read和write操作在某些平台上允许有例外)
lock(锁定):作用于主内存变量,它把一个变量标识为一条线程独占的状态。
unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放,释放后的变量才可以被其他线程锁定。
read(读取):作用于主内存的变量,它把一个变量的值从主存传输到线程的工作内存中,以便随后的load动作使用。
load(载入):作用于工作内存的变量,它把read操作从主内存得到的变量值放入工作内存的变量副本中。
use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量值的字节码指令时将会执行这个操作。
assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。
write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存变量中。
在这里插入图片描述
如果要把一个变量从主内存复制到工作内存,那就要顺序地执行read和load操作,如果要把变量从工作内存同步回主内存,就要顺序地执行store和write操作。注意:Java内存模型只要求上述两个操作是顺序执行,而没有保证是连续执行。也就是说,read和load之间、store与write之间是可插入其他指令的,如对主内存中的变量a、b进行访问时,一种可能出现顺序是read a、read b、load a、load b、load a。出此之外,Java内存模型还规定了在执行上述8种基本操作时必须满足如下规则:
1)不允许read和load、store和write操作之一单独出现,即不允许一个变量从主内存读取了但工作内存不接受,或者从工作内存发起回写但主内存不接受的情况出现。
2)不允许一个线程丢弃它的最近assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。
3)不允许一个线程无原因地(没有发生任何assign)操作把数据从线程的工作内存同步回主内存中。
4)一个新的变量只能在主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,换句话说,就是对一个变量实施use、store操作之前,必须先执行过了assign和load操作。
5)一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。
6)如果对一个变量执行lock操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行或assign操作初始化变量的值。
7)如果一个变量事先没有被lock操作锁定,那就不允许它执行unlock操作,也不允许去unlock一个被其他线程锁定住的变量。
8)对一个变量执行unlock操作之前,必须先把此变量同步执行回主内存中(执行store、write操作)。
这8种内存访问操作以及上述规则限定,再加上稍后介绍的对volatile的一些特殊规定,就已经完全确定了Java程序中哪些内存访问操作在并发下是安全的。由于这种定义相当严谨但又什么繁琐,实践起来很麻烦,等效判断原则----先行发生原则,用来确定一个访问在并发环境下是否安全。

二 一些需要知道的原理

2.1 线程安全性

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

2.1.1 原理

  1. 线程自己的数据会存放在一个自身的工作内存中线程栈中,两个线程之间交互通过共享内存才可以。

2.1.2 为什么共享内存是需要注意的点:

  • 重排序:Java内存模型中,为了性能,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发的执行的正确性。
  • 可见性:当共享数据变化时,另外一个线程中的变量能够感知,叫做可见;当一个线程对共享数据发生改变,另一个线程无法感知数据,这是不可见。

导致共享变量在线程间不可见的原因:
线程交叉执行
重排序结合线程交叉执行
共享变量更新后的值没有在工作内存与主存间及时更新

2.1.3 特性

2.1.3.1 原子性 (互斥访问)

由Java内存模型来直接保证原子性变量操作包括 read、load、use、assign、store和write,基本数据类型的访问读写是具备原子性的(例外是double和long)。如果应用场景需要一个更大范围的原子性保证,Java内存模型还提供了lock和unlock操作来满足这种需求,尽管虚拟机未把lock和unlock操作直接开放给用户使用户,但是却提供了更高层次字节码指令monitorenter和monitorexit来隐式地使用这两个操作,这两个字节码指令反映到Java代码中就是同步块-synchronized关键字。

2.1.3.1.1 atomic包-详见https://blog.csdn.net/xi15232131135/article/details/104516680
2.1.3.1.2 synchronized-实现和原理详见https://blog.csdn.net/xi15232131135/article/details/104517044:依赖JVM,串行执行。不可中断锁,适合竞争不激烈

作用对象的作用代码,同步锁
修饰代码块:大括号括起来的代码,作用于调用的对象
修饰方法:整个方法,作用于作用的对象
修饰静态方法:整个静态方法,作用于所有对象
修饰类:括号括起来部分,作用于所有对象

public class SynchronizedExample1 {

    // 修饰一个代码块
    public void test1(int j) {
        synchronized (this) {
            for (int i = 0; i < 10; i++) {
                log.info("test1 {} - {}", j, i);
            }
        }
    }

    // 修饰一个方法
    public synchronized void test2(int j) {
        for (int i = 0; i < 10; i++) {
            log.info("test2 {} - {}", j, i);
        }
    }

    public static void main(String[] args) {
        SynchronizedExample1 example1 = new SynchronizedExample1();
        SynchronizedExample1 example2 = new SynchronizedExample1();
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.execute(() -> {
            example1.test1(1);
        });
        executorService.execute(() -> {
            example2.test1(2);
        });
    }
}

总结:其实对于一个方法来说,都是一个单独的线程栈,虽然并行执行,对最后的结果不会影响;但是如果加了synchronized,对用上边的结果,同一个对象调用方法是串行执行的。
所以可以有如下count例子:

public class CountExample3 {

    // 请求总数
    public static int clientTotal = 5000;

    // 同时并发执行的线程数
    public static int threadTotal = 200;

    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 synchronized static void add() { //重点
        count++;
    }
}
2.1.3.1.3 lock 见csdn https://blog.csdn.net/xi15232131135/article/details/104519436

对比:
synchronized:不可中断锁
Lock: 可中断锁,多样化同步,竞争激烈时能维持常态
Atomic:竞争激烈时能维持常态,比Lock性能好;只能同步一个值

2.1.3.2 可见性

volatile不适合计数的场景,不是原子性,适合状态标识,double check
可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性,无论是普通变量还是volatile变量都是如此,普通变量与volatile变量的区别是,volatile的特殊规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。
volatile:通过加入内存屏障和禁止重排序优化来实现。

对于volatile变量写操作时,会在写操作后加入一条store屏障指令,将本地内存中的共享变量值刷新到主内存。
对volatile变量读操作时,会在读操作之前加入一条load屏障指令,从主内存中读取共享变量。

synchronized:同步块的可见性是由“对一个变量执行unlock操作之前,必须把此变量同步回主内存中(执行store,write操作)这条规则获得的”。线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值。
final:

2.1.3.3 有序性

在java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
Java程序中天然的有序性可以总结为一句话:如果在本线程内观察,所有的操作都是有序的;如果在另一个线程中观察另一个线程,所有操作都是无需的。前半句是指“线程内表现为串行的语义”,后半句是指“指令重排序”现象和“工作内存与主内存同步延迟”现象。
Java提供了volatile和synchronized两个关键字来保证线程之间操作的有序性,volatile关键字本身就包含了禁止指令重排序的语义,而synchronized则是由“一个变量在同一个时刻只允许一条线程对其进行lock操作”这条规则获得的,这条规则决定了持有同一个锁的两个同步块只能串行地进入。

先行发生原则

如果Java内存模型中所有的有序性仅仅靠volatile和synchronized来完成,那么有一些操作将会变得很繁琐,但是我们再编写Java并发代码的时候并没有感觉到这一点,这个是因为Java语言中有一个“先行发生”的原则,这个原则非常重要,它是判断数据是否存在竞争、线程是否安全的主要依据,依靠这个原则,我们可以通过几条规则一揽子地解决并发环境下两个操作之间是否可能存在冲突的所有问题。
先行发生是Java内存模型中定义的两项操作之间的偏序关系。

  1. 程序次序原则:一个线程内,按照代码顺序,书写在前边的操作先行发生于书写在后面的操作。(是保证单线程操作的执行顺序,对结果不影响的指令进行重排序)
  2. 锁定规则:一个unlock操作先行发生于后面对同一个锁的unlock操作。后面是指时间上的先后顺序。
  3. volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作,“后面”是指时间上的先后顺序
  4. 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作。
  5. 线程终止规则:线程的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。
  6. 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrputed()方法检测到是否有中断发生。
  7. 对象终结规则:一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。
  8. 传递性:如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论。
指令重排序需要满足的条件

a. 在单线程环境下不能改变程序运行的结果
b. 存在数据依赖关系的不允许重排序
c. 如果两个操作不满足上述任意一个happends-before规则,那么这两个操作就没有顺序的保障,JVM可以对这两个操作进行重排序

2.2 安全发布对象

发布对象:使一个对象能够被当前范围之外的代码所使用
对象溢出:一 种错误发布,当一个对象还没有构造完成时,就使它被其他线程所见

2.3 无状态,有状态对象

2.3.1 spring关于多线程

  1. spring框架里的bean默认都是单例模式,全局只有一个实例。
    多用户或者多线程访问就会出现问题。如果在程序中出现私有变量,尽量替换参数。对于每个访问私有变量的方法增加变量传入或者通过ThreadLocal来获取也是不错的方法。 而如果是prototype的话,就不会出现资源共享问题了。
  2. 线程安全问题都是由全局变量及静态变量引起的。若每个线程中全局变量、静态变量只有读操作,而无写操作,线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则就可能影响线程安全。
  3. 常量始终是线程安全的,因为只存在读操作;局部变量时线程安全的(包括方法的参数变量和方法内变量)

2.3.2 无状态和有状态解释

无状态:无属性,或者有属性但是没有操作该属性的方法,这种对象只能被用作读取功能,是线程安全的
有状态:有属性,并且可以操作属性值,这种对象,如果是单例模式(全局有且只有一个)则存在多线程安全问题。因为多个线程共享堆内存是线程不安全的,要用prototype作用域。

2.3.3 ThreadLocal

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  1. ThreadLocal提供了线程内部存储变量的能力
  2. 对于同一个线程的不同ThreadLocal来讲,这些ThreadLocal实例共享一个table数组,每个ThreadLocal实例再table中的索引i是不同的。

例子:

public class DdcHelper {
	private static ThreadLocal<DdcinfoEntity> local = new ThreadLocal<DdcinfoEntity>();//多线程调用,每个当前线程保存ddcinfo
	
	public static void setCurrentDdcInfo(DdcinfoEntity entity) {
		local.set(entity);
	}
	
	public static DdcinfoEntity getCurrentDdcInfo(){
		DdcinfoEntity ddcinfoEntity = local.get();
		if(ddcinfoEntity!=null){
			if(ddcinfoEntity.getLatitude()==null){
				ddcinfoEntity.setLatitude("0");
			}
			if(ddcinfoEntity.getLongitude()==null){
				ddcinfoEntity.setLatitude("0");
			}
		}
		return ddcinfoEntity;
	}
}

public class DdcFilter implements Filter {
	private static final Logger logger = LoggerFactory.getLogger(DdcFilter.class);

	//DES_KEY
	private static final String DES_KEY="bShORr6y6EQ=";

	@Override
	public void init(FilterConfig filterConfig) throws ServletException {
		// nothing to do.
	}

	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
		HttpServletRequest httpRequest = (HttpServletRequest)request;
		String ddcinfo = httpRequest.getHeader("ddcinfo");
		String requestPath = httpRequest.getRequestURI();
		if(StringUtils.isNotBlank(ddcinfo)){
			String decryptDdcInfo = null;
			try {
				decryptDdcInfo = EncryptUtil.decryptDESBase64(ddcinfo, DES_KEY);
				DdcinfoEntity entity = new Gson().fromJson(decryptDdcInfo, DdcinfoEntity.class);
				if(null != entity){
					DdcHelper.setCurrentDdcInfo(entity);//调用DdcHelper方法
				} else {
					DdcHelper.setCurrentDdcInfo(null);
				}
			} catch (JsonParseException e) {
				logger.error(String.format("request[%s] parse ddcinfo[%s] failed.", requestPath, decryptDdcInfo),e);
			} catch (Exception e) {
				logger.error(String.format("request[%s] decrypt ddcinfo[%s] failed.", requestPath, ddcinfo),e);
			}
		}else{
			DdcHelper.setCurrentDdcInfo(null);
		}
		
		chain.doFilter(request, response);
	}

	@Override
	public void destroy() {
		// nothing to do.
	}

}

ThreadLocal的坑:使用ThreadLocal,线程重用会导致信息错乱的坑
例子:

private static final ThreadLocal<Integer> currentUser = ThreadLocal.withInitial(() -> null);

@GetMapping("wrong")
public Map wrong(@RequestParam("userId") Integer userId) {
    //设置用户信息之前先查询一次ThreadLocal中的用户信息
    String before  = Thread.currentThread().getName() + ":" + currentUser.get();
    //设置用户信息到ThreadLocal
    currentUser.set(userId);
    //设置用户信息之后再查询一次ThreadLocal中的用户信息
    String after  = Thread.currentThread().getName() + ":" + currentUser.get();
    //汇总输出两次查询结果
    Map result = new HashMap();
    result.put("before", before);
    result.put("after", after);
    return result;
}

按理说,每次获取的before应该都是null,但是呢,程序运行在Tomcat中,执行程序的线程是Tomcat的工作线程,而Tomcat的工作线程是基于线程池的。
把tomcat的工作线程设置为1,server.tomcat.max-threads=1
用户1,请求过来,会有以下结果,符合预期:
在这里插入图片描述
用户2请求过来,会有以下结果,「不符合预期」:
在这里插入图片描述

因此,使用类似 ThreadLocal 工具来存放一些数据时,需要特别注意在代码运行完后,显式地去清空设置的数据,正例如下:


```java
@GetMapping("right")
public Map right(@RequestParam("userId") Integer userId) {
    String before  = Thread.currentThread().getName() + ":" + currentUser.get();
    currentUser.set(userId);
    try {
        String after = Thread.currentThread().getName() + ":" + currentUser.get();
        Map result = new HashMap();
        result.put("before", before);
        result.put("after", after);
        return result;
    } finally {
        //在finally代码块中删除ThreadLocal中的数据,确保数据不串
        currentUser.remove();
    }
}```

关于变量是否线程安全的问题:

首先,不可变的值是绝对线程安全的

1.局部变量:线程安全
因为每个线程各自保存自己的局部变量,不会共享同一个变量,所以安全

2.成员变量分为静态变量和实例变量:
静态变量:线程不安全
静态变量被所有线程共享,所以线程不安全

实例变量:可能安全,可能不安全
实例变量,如果该实例变量为单例,则同样被所有线程共享,则线程不安全,如果该实例变量是多例,则每个线程各自保存一份实例变量,这种情况是线程安全的

注:spring的IOC容器中的bean,默认都是单例的,但一般而言,该bean都是无状态的(即只读,不可写),所以不存在线程安全问题,但如果bean是有状态的(可读,可写),同样要注意线程安全问题(可以通过ThreadLocal,加锁,多例等方式解决)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值