并发编程相关基础知识

1. 多线程并发编程

并发是指同一个时间段内多个任务同时都在执行,并且都没有执行结束,而并行是说在单位时间内多个任务同时在执行。

单CPU时代多线程编程是没有太大意义的,并且线程间频繁的上下文切换还会带来额外开销。

多核CPU时代的到来打破了单核CPU对多线程效能的限制。多个CPU意味着每个线程可以使用自己的CPU运行,这减少了线程上下文切换的开销,但随着对应用系统性能和吞吐量要求的提高,出现了处理海量数据和请求的要求,这些都对高并发编程有着迫切的需求。

多线程编程实践中,线程的个数往往多于CPU的个数,所以一般都称多线程并发编程而不是多线程并行编程。

2. Java中的线程安全问题

线程安全问题是指当多个线程同时读写一个共享资源并且没有任何同步措施时,导致出现脏数据或者其他不可预见的结果的问题。

这就需要在线程访问共享变量时进行适当的同步,在Java中最常见的是使用关键字synchronized进行同步。

3. Java中共享变量的内存可见性问题

Java内存模型规定,将所有的变量都存放在主内存中,当线程使用变量时,会把主内存里面的变量复制到自己的工作空间或者叫作工作内存,线程读写变量时操作的是自己工作内存中的变量。

线程操作一个共享变量时,首先会去自己所在CPU的缓存里读取,如果未命中,再去主存读取。这样存在一个问题,某个线程更新共享变量时,并不能更新其他线程的缓存,所以其他线程可能命中自己缓存中的错误数据,这就是内存可见性问题。

解决共享变量内存不可见问题,使用Java中的volatile关键字。

4. synchronized关键字

synchronized块是Java提供的一种原子性内置锁,Java中的每个对象都可以把它当作一个同步锁来使用,这些Java内置的使用者看不到的锁被称为内部锁,也叫作监视器锁。

内置锁是排它锁,线程的执行代码在进入synchronized代码块前会自动获取内部锁,这时候其他线程访问该同步代码块时会被阻塞挂起。synchronized的使用会导致上下文切换。

进入synchronized块的内存语义是把在synchronized块内使用到的变量从线程的工作内存中清除,这样在synchronized块内使用到该变量时就不会从线程的工作内存中获取,而是直接从主内存中获取。

退出synchronized块的内存语义是把在synchronized块内对共享变量的修改刷新到主内存。

除可以解决共享变量内存可见性问题外,synchronized经常被用来实现原子性操作。

5. volatile关键字

使用锁的方式可以解决共享变量内存可见性问题,但是使用锁太笨重,因为它会带来线程上下文的切换开销。volatile关键字可以确保对一个变量的更新对其他线程马上可见。

当一个变量被声明为volatile时,线程在写入变量时不会把值缓存在寄存器或者其他地方,而是会把值刷新回主内存。当其他线程读取该共享变量时,会从主内存重新获取最新值,而不是使用当前线程的工作内存中的值。

volatile虽然提供了可见性保证,但并不保证操作的原子性。

6. Java中的原子性操作

所谓原子性操作,是指执行一系列操作时,这些操作要么全部执行,要么全部不执行,不存在只执行其中一部分的情况。

简单的一句代码i++;并不是原子操作,其底层操作包括取值,计算,更新多步,中间可能被打断。

7. Java中的CAS操作

锁虽然能实现原子性,但一个线程没有获取到锁时会被阻塞挂起,这会导致线程上下文的切换和重新调度开销。

CAS即Compare and Swap,其是JDK提供的非阻塞原子性操作,它通过硬件保证了比较—更新操作的原子性。JDK里面的Unsafe类提供了一系列的compareAndSwap*方法。

8. Unsafe类

JDK的rt.jar包中的Unsafe类提供了硬件级别的原子性操作,Unsafe类中的方法都是native方法,它们使用JNI的方式访问本地C++ 实现库。

9. Java指令重排序

Java内存模型允许编译器和处理器对指令重排序以提高运行性能,并且只会对不存在数据依赖性的指令重排序。在单线程下重排序可以保证最终执行的结果与程序顺序执行的结果一致,但是在多线程下就会存在问题。

重排序在多线程下会导致非预期的程序执行结果,而使用volatile修饰ready就可以避免重排序和内存可见性问题。写volatile变量时,可以确保volatile写之前的操作不会被编译器重排序到volatile写之后。读volatile变量时,可以确保volatile读之后的操作不会被编译器重排序到volatile读之前。

10. 伪共享

在Cache内部是按行存储的,其中每一行称为一个Cache行,其是Cache与主内存进行数据交换的单位。

当多个线程同时修改一个缓存行里面的多个变量时,由于同时只能有一个线程操作缓存行,所以相比将每个变量放到一个缓存行,性能会有所下降,这就是伪共享。

多线程下并发修改一个缓存行中的多个变量时就会竞争缓存行,从而降低程序运行性能。

11. 锁的概述
乐观锁与悲观锁

悲观锁指对数据被外界修改持保守态度,认为数据很容易就会被其他线程修改,所以在数据被处理前先对数据进行加锁,并在整个数据处理过程中,使数据处于锁定状态。

乐观锁是相对悲观锁来说的,它认为数据在一般情况下不会造成冲突,所以在访问记录前不会加排它锁,而是在进行数据提交更新时,才会正式对数据冲突与否进行检测。

乐观锁并不会使用数据库提供的锁机制,一般在表中添加version字段或者使用业务状态来实现。乐观锁直到提交时才锁定,所以不会产生任何死锁。

公平锁与非公平锁

公平锁表示线程获取锁的顺序是按照线程请求锁的时间早晚来决定的,也就是最早请求锁的线程将最早获取到锁。而非公平锁则在运行时闯入,也就是先来不一定先得。

//公平锁
ReentrantLock pairLock = new ReentrantLocktrue//非公平锁
ReentrantLock pairLock = new ReentrantLockfalse

在没有公平性需求的前提下尽量使用非公平锁,因为公平锁会带来性能开销。

独占锁与共享锁

根据锁只能被单个线程持有还是能被多个线程共同持有,锁可以分为独占锁和共享锁。

独占锁是一种悲观锁,由于每次访问资源都先加上互斥锁,限制了并发性。

共享锁则是一种乐观锁,它放宽了加锁的条件,允许多个线程同时进行读操作。

可重入锁

当一个线程要获取一个被其他线程持有的独占锁时,该线程会被阻塞,当一个线程再次获取它自己已经获取的锁时,如果不被阻塞,那么我们说该锁是可重入的,也就是只要该线程获取了该锁,那么可以无限次数地进入被该锁锁住的代码。

synchronized内部锁是可重入锁。

自旋锁

自旋锁,当前线程在获取锁时,如果发现锁已经被其他线程占有,它不马上阻塞自己,在不放弃CPU使用权的情况下,多次尝试获取(默认次数是10,可以使用-XX:PreBlockSpinsh参数设置该值)。

参考书籍:《Java并发编程之美》-- 翟陆续,薛宾田

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值