什么是多线程并发编程
并发与并行
并发是一共要处理(deal with)很多事情,并行是一次可以做(do)多少事情
场景:
做一道红烧肉
并行的做法是:
请很多人,买肉、择菜、熬糖色分别让不同的人去做。
并发的做法是:
一个人,先去买菜,烧水炖肉,然后去择菜,肉炖好之后,开始熬糖色。
并发是指同一时间段内多个任务同时都在执行,并且都没有执行结束,而并行是说在单位时间内多个任务同时在执行。
并发任务强调在一个时间段内同时执行,而一个时间段由多个单位时间累积而成
并发编程的本质:充分利用CPU资源
Java中线程安全问题
线程安全问题是指当多个线程同时读写一个共享资源并且没有任何同步措施时,导致出现脏数据或者其他不可预见的结果。
共享资源:该资源被多个线程所持有或者说多个线程去访问该资源
如图所示:多个线程同时操作共享资源,如果操作的资源是各不相同,不会出现线程安全问题。当多个线程同时操作同一个共享资源的时候,这个时候如果只是读取也不会出现问题,但是,如果多个线程写入同一个共享资源时,不增加限制的前提的有可能会出现脏数据或者是其他不可预见情况。所以,对于内存中的共享资源在写入操作的到时候需要增加相关的限制。同一时间,同一个共享资源只对一个线程可见。
线程安全体现
- 原子性: 提供互斥访问,统一时刻只能有一个线程对数据进行操作。(atomic,synchronized)
- 可见性: 一个线程对主内存的修改可以及时的被其他线程看到(synchronized,volatie)
- 有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序。(happens-before原则)
happens-before原则理解
为什么要有一个happens-before原则
happens-before 可以明白什么时候变量对你可见。因为我们编写的程序都要经过优化(编译器和处理器会对我们写的程序进行优化以提高运行效率)后才会被运行,优化分为很多种,其中一种优化叫做重排序,重排序需要遵循happens-before规则,不能说你想怎么排序就怎么排序的。
规则如下:
- 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
- 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
- volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。 - start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。
- join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。
- 程序中断规则:对线程interrupted()方法的调用先行于被中断线程的代码检测到中断时间的发生。
对象finalize规则:一个对象的初始化完成(构造函数执行结束)先行于发生它的finalize()方法的开始。
java 中共享变量的内存可见性问题
内存可见性,首先看下多线程下处理共享变量时java的内存模型
Java内存模型规定,将所有的变量存放在内存中,当线程使用变量时,会把主内存的变量复制到自己的工作空间或者工作内存,线程读写变量时操作的是工作内存中的变量。
也就是说,当一个线程共享变量时,它首先从主内存复制共享变量到自己的工作内存,然后对工作内存里的变量进行处理,处理后将变量值更新到主内存。
Java中synchronized关键字
synchronized块是Java提供的一种原子性内置锁,内置锁是排它锁,也就是一个线程获取这个锁后,其他线程必须等待该线程释放锁之后才能获取该锁。
Synchronized应用场景
Java中Volatile关键字
- volatile关键字可以确保一个变量的更新对其他线程马上可见
- 一个变量被声明为volatile时,线程在写入变量时不会把值缓存在寄存器或者其他地方,而是会把值刷新回主内存。其他线程读取该共享变量时,会从主内存重新获取最新值。
知识点
volatile关键字虽然提供了可见性保证,但并不保证操作的原子性。
什么时候使用volatile关键字?
- 写入变量值不依赖变量的当前值。因为如果依赖当前值,将是获取–计算–写入三步操作,这三步操作不是原子性的,volatile不保证原子性
- 读写变量值时没有加锁。因为加锁本身已经保证了内存可见性,这个时候不需要把变量声明为volatile的。
Java中原子性操作
什么是原子性操作?
程序在执行中 ,执行一系列操作时,这些操作要么全部执行,要么全部不执行,不存在执行一部分的情况。
如何保证原子性?
在操作的方法上,增加synchronized关键字。但是synchronized是独占锁,没有获取内部锁的线程会被阻塞掉,会大大降低并发性。解决办法是:在内部使用非阻塞CAS算法实现的原子性操作类AtomicLong就是一个不错的选择。
Java中CAS操作
- 锁在并发处理中占据了一席之地,但是使用锁有个非常不好的地方,就是当一个线程没有获取到锁时会被阻塞挂起,会导致线程上下文的切换和重新调度开销
- Java提供非阻塞的volatile关键字,但是volatile只能保证共享变量的可见性,不能解决读–改--写等的原子性问题。
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 替换
旧的值expect 。这是处理器提供的一个原子性指令。
CAS中ABA问题如何解决
ABA问题的产生时因为变量的状态值产生了环形转换,就是变量的值可以从A到B,然后再从B到A。如果变量的值只能朝着一个方法转换,比如 A 到B ,B到C,不构成环形,就不会存在问题。
JDK中AtomicStampedReference类给每个变量的状态值都配备了一个时间戳,从而避免了ABA问题
Unsafe类
JDK 的此jar 包中的Unsafe 类提供了硬件级别的原子性操作, Un s afe 类中的方法都是native 方法,它们使用刑I 的方式访问本地C++实现库
UnSafe类中几个主要方法以及编程时如何使用Unsafe类做一些事情。
- long objectFieldOffset(Field field)
方法: 返回指定的变量在所属类中的内存偏移地址,
该偏移地址仅仅在该Unsafe 函数中访问指定宇段时使用。如下代码使用Unsafe 类
获取变量value 在AtomicLong 对象中的内存偏移。 - int anayBaseOffset(Class anayClass) 方法: 获取数组中第一个元素的地址。
- int arraylndexScale(Class arrayClass) 方法: 获取数组中一个元素占用的字节。
- boolean compareAndSwapLong(Object obj, long offset, long expect, long update)
方法:比较对象obj 中偏移量为offset 的变量的值是否与expect 相等, 相等则使用update
值更新, 然后返回tru巳,否则返回false 。 - public native long getLongvolatile(Object obj, long offset)
方法: 获取对象obj 中偏移量为offset 的变量对应volatile i吾义的值。
- void putLongvolatile(Object obj, long offset, long value)
方法: 设置obj 对象中offset偏移的类型为long 的field 的值为value ,支持volatile 语义。 - void putOrderedLong(Object obj, long offset, long value)
方法: 设置obj 对象中offset偏移地址对应的long 型field 的值为value 。这是一个有延迟的putLongvolatile 方法,
并且不保证值修改对其他线程立刻可见。只有在变量使用volatile 修饰并且预计会被意外修改时才使用该方法。 - void park(boolean isAbsolute, long time)
方法: 阻塞当前线程, 其中参数isAbsolute等于false 且time 等于0 表示一直阻塞。time 大于0 表示等待指定的time 后阻塞线程会被唤醒, 这个time 是个相对值, 是个增量值, 也就是相对当前时间累加time
后当前线程就会被唤醒。如果isAb s olute 等于true , 并且time 大于0 ,则表示阻塞
的线程到指定的时间点后会被唤醒,这里time 是个绝对时间, 是将某个时间点换
算为ms 后的值。另外,当其他线程调用了当前阻塞线程的intem1pt 方法而中断了
当前线程时-, 当前线程也会返回, 而当其他线程调用了unPark 方法并且把当前线
程作为参数时当前线程也会返回。 - void unpark(Object thread)
方法: 唤醒调用park 后阻塞的线程。下面是JDK8 新增的函数, 这里只列出Long 类型操作。 - long getAndSetLong(Object obj , long offset, long update)
方法: 获取对象obj 中偏移量为offset 的变量volatile i吾义的当前值, 并设置变量volatile i吾义的值为update 。 - long getAndAddLong(Object obj, long offset, long addValue)
方法: 获取对象。同中偏移量为offset 的变量volatile i吾义的当前值, 并设置变量值为原始值+addValue