上图是我们在并发编程中需要着重注意的地方,希望大家能够有所收获。
首先,我们先介绍下并发编程的三要素
并发编程三要素
- 原子性
原子,即一个不可再被分割的颗粒。在Java中原子性指的是一个或多个操作要么全部执行成功要么全部执行失败。 - 有序性
程序执行的顺序按照代码的先后顺序执行。(处理器可能会对指令进行重排序) - 可见性
当多个线程访问同一个变量时,如果其中一个线程对其作了修改,其他线程能立即获取到最新的值。
悲观锁与乐观锁
- 悲观锁:每次操作都会加锁,会造成线程阻塞。
- 乐观锁:每次操作不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止,不会造成线程阻塞。
线程之间的协作
wait/notify/notifyAll
这一组是 Object 类的方法
需要注意的是:这三个方法都必须在同步的范围内调用
- wait
阻塞当前线程,直到 notify 或者 notifyAll 来唤醒
wait有三种方式的调用
wait()
必要要由 notify 或者 notifyAll 来唤醒
wait(long timeout)
在指定时间内,如果没有notify或notifAll方法的唤醒,也会自动唤醒。
wait(long timeout,long nanos)
本质上还是调用一个参数的方法
public final void wait(long timeout, int nanos) throws InterruptedException {
if (timeout < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}
if (nanos > 0) {
timeout++;
}
wait(timeout);
}
-
notify
只能唤醒一个处于 wait 的线程 -
notifyAll
唤醒全部处于 wait 的线程
sleep/yield/join
这一组是 Thread 类的方法
-
sleep
让当前线程暂停指定时间,只是让出CPU的使用权,并不释放锁 -
yield
暂停当前线程的执行,也就是当前CPU的使用权,让其他线程有机会执行,不能指定时间。会让当前线程从运行状态转变为就绪状态,此方法在生产环境中很少会使用到,官方在其注释中也有相关的说明 -
join
等待调用 join 方法的线程执行结束,才执行后面的代码
其调用一定要在 start 方法之后(看源码可知)
使用场景:当父线程需要等待子线程执行结束才执行后面内容或者需要某个子线程的执行结果会用到 join 方法
valitate 关键字
java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保通过排他锁单独获得这个变量。Java语言提供了volatile,在某些情况下比锁更加方便。如果一个字段被声明成volatile,java线程内存模型确保所有线程看到这个变量的值是一致的。
valitate是轻量级的synchronized,不会引起线程上下文的切换和调度,执行开销更小。
- 使用volitate修饰的变量在汇编阶段,会多出一条lock前缀指令
- 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成
- 它会强制将对缓存的修改操作立即写入主存
- 如果是写操作,它会导致其他CPU里缓存了该内存地址的数据无效
作用
内存可见性
多线程操作的时候,一个线程修改了一个变量的值 ,其他线程能立即看到修改后的值
防止重排序
即程序的执行顺序按照代码的顺序执行(处理器为了提高代码的执行效率可能会对代码进行重排序)
synchronized 关键字
确保线程互斥的访问同步代码
synchronized 是JVM实现的一种锁,其中锁的获取和释放分别是monitorenter 和 monitorexit 指令,该锁在实现上分为了偏向锁、轻量级锁和重量级锁,其中偏向锁在 java1.6 是默认开启的,轻量级锁在多线程竞争的情况下会膨胀成重量级锁,有关锁的数据都保存在对象头中
加了 synchronized 关键字的方法,生成的字节码文件中会多一个ACC_SYNCHRONIZED 标志位,当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。 其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。
修饰普通方法
同步对象是实例对象
修饰静态方法
同步对象是类本身
修饰代码块
可以自己设置同步对象
缺点
会让没有得到锁的资源进入Block状态,争夺到资源之后又转为Running状态,这个过程涉及到操作系统用户模式和内核模式的切换,代价比较高。Java1.6为 synchronized 做了优化,增加了从偏向锁到轻量级锁再到重量级锁的过度,但是在最终转变为重量级锁之后,性能仍然较低。
CAS
CAS全称是Compare And Swap,即比较替换,是实现并发应用到的一种技术。操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。
Java不能直接的访问操作系统底层,是通过native方法(JNI)来访问。CAS底层通过Unsafe类实现原子性操作。
存在的问题
- ABA问题
什么是ABA问题?比如有一个 int 类型的值 N 是 1
此时有三个线程想要去改变它:
线程A :希望给 N 赋值为 2
线程B: 希望给 N 赋值为 2
线程C: 希望给 N 赋值为 1
此时线程A和线程B同时获取到N的值1,线程A率先得到系统资源,将 N 赋值为 2,线程 B 由于某种原因被阻塞住,线程C在线程A执行完后得到 N 的当前值2
此时的线程状态
线程A成功给 N 赋值为2
线程B获取到 N 的当前值 1 希望给他赋值为 2,处于阻塞状态
线程C获取当好 N 的当前值 2 希望给他赋值为1
然后线程C成功给N赋值为1
最后线程B得到了系统资源,又重新恢复了运行状态,在阻塞之前线程B获取到的N的值是1,执行compare操作发现当前N的值与获取到的值相同(均为1),成功将N赋值为了2。
在这个过程中线程B获取到N的值是一个旧值,虽然和当前N的值相等,但是实际上N的值已经经历了一次 1到2到1的改变
上面这个例子就是典型的ABA问题
怎样去解决ABA问题
给变量加一个版本号即可,在比较的时候不仅要比较当前变量的值 还需要比较当前变量的版本号。Java中AtomicStampedReference 就解决了这个问题 - 循环时间长开销大
在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。
CAS只能保证一个共享变量的原子操作