首先要澄清并发和并行的概念,并发和指同一个时间段内多个任务同时都在执行,并且都没有执行结束。而并行是在单位时间内多个任务同时在执行。 并发任务强调在一个时间段内同时执行,而一个时间段由多个单位时间累积而成,所以说并发的多个任务在单位时间内不一定同时执行。 在单cpu的时代多个任务都是并发执行,因为单个cpu同时只能执行一个任务。 在单个cpu时代多任务是共享一个cpu的,当一个任务占用cpu运行时,其他任务就会被挂起,当占用cpu的任务时间片用完之后,会把cpu让给其他任务来使用。
多核cpu时代的到来打破了单核cpu对于多线程效能的限制,多个cpu意味着每个线程可以使用自己的cpu运行,这减少了线程上下文切换的开销,但随着对应用系统性能和吞吐量要求的提高,出现了处理海量数据和请求的要求,这些都对高并发编程有着迫切的需求。
什么是共享资源? 该资源被多个线程所持有或者多个线程都可以去访问该资源 线程安全问题是指当多个线程同时读写一个共享资源并且没有任何同步措施时,导致出现脏数据或者其他不可预见的结果的问题。 线程安全问题和共享资源之间的关系?
并不是多个线程都去访问了共享资源就会出现线程安全问题,比如说所有线程都是去读。
至少有一个线程去对共享资源进行了修改才会存在线程安全问题。比如说高并发下的计数器,如果不同步,就会统计不准确。
count++; 分为三步: 1.将count从内存读取到本线程的工作内存 2.在工作内存进行递增 3.写回到主内存
以上三步,如果同时有多个线程在进行,势必会让最终的count++的数据不准确。比如说线程A 执行了12然后切换到线程B 执行了123 然后切换回线程A执行了3, 此时的值其实是等于线程A递增的值为1次,实际上应该是递增了2次。
java内存模型
将所有的变量都存放到主内存中,当线程使用的变量时,会将主内存里面的变量复制到自己的工作空间或者叫工作内存。 线程读写变量操作的是自己工作内存中的变量。
当一个线程操作共享变量时,首先从主内存复制共享变量到自己的工作内存,然后对工作内存里面的变量进行处理,处理完后将变量值更新到主内存。
对应的物理内存
假设有一个双核cpu系统。 每个核都有自己的控制器和运算器,其中控制器包含一组寄存器和操作控制器,运算器执行算术逻辑运算。
每个核都有自己的一级缓存,在有些架构里面还有一个所有cpu都共享的二级缓存,java内存模型里面的工作内存就对应这里的L1或者L2缓存或者cpu的寄存器。
因为cpu的核都各自的缓存,假设两个线程分别执行在不同的核上,将内存上要用的数据同步到使用各自的缓存,然后对各自缓存内数据更改,对其他核的缓存数据是没有影响的,也无法感知到其他核的缓存数据变更。
解决方式:使用一个值的时候,从主存读取最新的,对该值进行更改之后,立即写入到主存中。
java中的volatile可以达到这种效果
synchronized也可以达到这种效果,因为线程进入同步块的时候,会清空本地缓存并从主内读取最新的。然后在退出同步块的时候又会将工作内存里面的内容都写入到主存中。
5.1 synchronized关键字介绍 synchronized块是java提供的一种原子性内置锁,java中每个对象都可以把它当作一个同步锁使用,这些java内置的使用者看不到的锁叫做内部锁,也叫监视器锁。 线程的执行代码在进入synchronized代码块前会自动获取内部锁,这时候其他线程访问该同步代码块时就会阻塞挂起。拿到内部锁的线程会在正常退出同步代码块或者抛出异常后或者在同步块内调用了该内置锁资源的wait系列方法时释放该内置锁。内置锁是排他锁,也就是当一个线程获取这个锁后,其他线程必须等待该线程释放锁后才能获取该锁。 内部锁的标志在对象头上
java中的线程是与操作系统的原生线程一一对应的,所以当阻塞一个线程时,需要从用户态切换到内核态执行阻塞操作,这是很耗时的操作,而synchronized的使用会导致上下文切换,因为获取不到就要进行阻塞状态。
5.2 synchronized内存语义 进入synchronized块的内存语义是把synchronized块内使用到的变量从线程的工作内存中清除,这样在synchronized块内存中使用到该变量时候,就会去主内存中获取,而不是使用本地工作内的值。 退出synchronized块的内存语义是把synchronized块内对共享变量的修改刷新到主内存。 获取锁后会清空锁块本地内存中将会被用到的共享变量,在使用这些共享变时从主内存加载,在释放锁的时候从本地内存修改的共享变量刷新到主内存。 synchronized除了可以解决内存可见性问题之外,还经常被用来实现原子操作,但是要注意,synchronized关键字会引起上下文切换并带来线程调度的开销
除了使用锁的方式可以解决共享变量内存可见性问题,但是使用太笨重,因为它给予带来线程上下文切换开销。对于解决内存可见性问题,java还提供了一种弱形式的同步,也就是使用vlolatitle关键字。 该关键字可以确保对一个变量的更新对其他线程马上可见。 当一个变量被声明为volatitle时,线程在写入变量时不会把值缓存在寄存器或者其他地方,而是会把值刷新回主存。当其他线程读取该共享变量时,会从主内存重新获取最新值,而不是使用当前线程的工作内存中的值。
volatitle的内存语义和synchronized有相似之处,具体来说,就是当线程写入了volatitle变量时就等价于线程退出synchronized同步块(将写入工作内存的变量值同步到主内存),读取volatile变量值时就相当于进入同步块(先清空本地内存变量值,再从主存获取最新值)
volatile提供了内存可见性的保证,但并不保证操作的原子性,因此不能替代锁
什么时候使用volatile? 写入变量值不依赖变量当前值时。 因为如果依赖当前值,将是1.获取 2.计算,3.写入,三步操作,这三步操作不是原子性的,而volatile不保证原子性
也就是,即使有volatile 修饰int类型的count,在并发修改的时候,也可能会出现问题。 比如说线程1执行了12 切换到线程2执行了123,然后切换线程1执行了3.
读写变量值时没有加锁,因为加锁本身已经保证了内存可见性,这时候不需要把变量声明为volatitle
什么是原子操作? 所谓原子操作,是指执行一系列操作时,这些操作要么全部执行,要么全部不执行。不存在只只执行其中一部分的清空。 比如说在设计计数器时一般都先读取当前值,然后+1,再更新。这个过程是读-改-写的过程,如果不能保证这个过程是原子性的,那么就会出现线程安全问题。 比如说在设计计数器时一般都先读取当前值,然后+1,再更新。这个过程是读-改-写的过程,如果不能保证这个过程是原子性的,那么就会出现线程安全问题。 1.获取当前值,2,将常量1放到栈顶,3.将当前栈顶的两个值相加并放回到栈顶 4.将栈顶的结果赋予value变量
如何保证原子性 使用synchronzied关键字可以实现线程安全性,即内存可见性和原子性,但是synchronized是独占锁,没有获取内部锁的线程会被阻塞调。 public synchronized void inc(){ ++value; }
为什么要用CAS? 在java中,锁在并发处理中占据了一席之地,但是使用锁有一个不好的地方,就是当一个线程没有获取到锁时会被阻塞挂起,这回导致线程的上下文切换和重新调度开销。java提供了非阻塞的volatile关键字来解决共享内存的可见性问题,这在一定程度上弥补了锁带来的开销问题,但是volatitle只能保证共享变量的可见性,不能解决读-改-写的原子性问题。
CAS即compare and swap,是jdk提供的非阻塞原子性操作,它通过硬件保证了比较-更新的原子性。jdk里面的unsafe类提供了一系列的compareAndSwap方法。
以compareAndSwapLong方法为例
boolean compareAndSwapLong(Object obj,long valueOffset,long expect,long update)其中compareAndSwap的意思是比较并交换。CAS有四个操作数,分别为:对象内存位置,对象中变量的偏移量,变量预期值和新的值。其操作含义是,如果对象obj中内存偏移量valueoffset的变量值为expect,则使用新的值update替换旧的值。 这是处理器提供的一个原子性指令
ABA问题
cas操作有个经典的ABA问题,具体来说:假如线程1使用CAS修改初始值为A的变量X,那么线程1会首先去获取当前变量X的值(为A),然后用CAS操作尝试修改X的值为B,如果CAS操作成功了,那么程序运行一定是正确的吗?其实未必,这是因为有可能在线程1获取到变量X的值A后,在执行CAS前,线程2使用CAS修改了变量X的值为B,然后又使用CAS修改了变量X的值为A。 所以虽然线程1执行CAS时候X的值为A但是这个A已经不是线程1获取时的A了,这就是ABA问题
ABA问题的产生是因为变量的状态值发生了环形转换,就是变量的值可以从A到B,然后从B到A,如果变量的值只能朝一个方向转换,比如A到B,B到C,不构成环,就不会存在问题,JDK中的AtomicStampedReference类给每个变量的状态值都配备了一个时间戳,从而避免了ABA问题的产生
public class AtomicStampedReference<V> { private static class Pair<T> { final T reference; 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); } } private volatile Pair<V> pair;假设变量X的初始值为A,时间戳为1,然后线程1通过cas获取到了该值,打算更新为B。 此时线程2也通过cas操作获取到了该值,然后更新为D,然后又更新回了A,此时变量X的值为A,但是时间戳已经变化了,为2。 然后此时线程1打算更新了,就会发现自己的A和预期的A不相同,因此更新失败,必须重新获取过A的值,进行更新。
