一、并发编程三要素
1.原子性
原子,即一个不可再被分割的颗粒。java中指一个或多个操作要么全部执行成功,要么全不成功。
2.有序性
程序执行的顺序按代码的先后顺序(编译器等会对指令重排序)。
3.可见性
当多个线程同时对一个对象访问时,一个线程对对象做了修改,其他线程能立即获得最新的值。
二、线程的五大状态
1.创建状态
当使用new操作符创建一个线程的时候。
2.就绪状态
调用start方法,此时线程还未执行run方法,还在等待CPU的调度。
3.运行状态
CPU开始调度线程,线程开始执行run方法。
4.阻塞状态
线程由于一些原因进入阻塞状态。
例如:调用sleep方法,尝试得到一个锁。
5.死亡状态
run方法执行完或者执行中出现异常。
三、乐观锁和悲观锁
1.乐观锁
每次操作不加锁假设没有冲突去执行某个操作,如果因为冲突失败就重试,直到成功为止,不会造成线程阻塞。
2.悲观锁
每次操作都加锁,会造成线程阻塞。
四、线程之间的协作
1.wait/notify/notifyAll
这是Object类的一组方法。
需要注意的是:这三个方法都必须在同步的范围内调用。
-
wait
阻塞当前线程,等待notify或者notifyAll来唤醒。 -
notify
只能唤醒一个处于wait的线程。 -
notifyAll
唤醒所有处于wait的线程。
2.sleep/yield/join
这是一组Thread类的方法。
- sleep
让当前线程暂停指定时间,让出CPU使用权,并不会释放锁。 - yield
暂停线程,不能指定时间,让出CPU使用权,使线程从运行状态转为就绪状态。 - join
等待调用join的线程执行完毕,才执行后面的代码。
注意必须在start方法后才能调用join方法。
五、Volatile
1.定义
java编程语言允许多个线程共享变量,为了确保变量能准确和一致地更新,线程应通过排它锁单独得到这个变量。java提供了Volatile关键字,在某些情况下比锁更加方便。如果一个字段被声明成Volatile,java内存线程模型保证所有线程看到的变量的值是一致的。
2.原理
- 使用volatile修饰的变量在汇编阶段,会多出一个lock前缀指令。
- 它确保指令重排序的时候不会把前面的指令放到内存屏障后;也不会把后面的指令放到内存屏障钱。即:在执行到内存屏障时,确保前面的指令都执行完了。
- 它会强制的将对缓存的修改立即写入主存中。
- 如果是写操作,它会导致其他CPU中缓存了该内存地址的数据失效。
3.作用
- 内存可见性
多线程操作共享变量时,一个线程修改了值,其他线程能立即看到修改后的值。 - 防止重排序
即程序的执行顺序按代码的顺序执行。
六、Synchronized
1.定义
synchronized是JVM的一种锁,其中获取和释放锁的指令是分别是monitorenter和monitorexit,该锁在实现上分为偏向锁、轻量级锁和重量级锁,其中偏向锁在java1.6中是默认开启的,轻量级锁在多线程竞争的时候回膨胀成重量级锁,有关锁的机制都保存在对象头中。
2.原理
加了synchronized关键字的代码段,生成的字节码文件会多出 monitorenter 和monitorexit 两条指令。
加了synchronized关键字的方法,字节码文件会多出一个ACC_SYNCHRONIZED 标志位,当方法被调用时,指令会检查ACC_SYNCHRONIZED 标志位有没有被设置,如果设置了,将会先获取monitor,获取成功后才能执行方法体,访问结束后释放monitor,在方法执行期间其他线程将不能获取同一个monitor对象。其实本质上没什么区别,只是方法的同步是一种隐式的方式来实现。
3.使用说明
-
修饰普通方法
同步对象是实例对象 -
修饰静态方法
同步的是类本身 -
修饰代码块
可以自己设置同步对象
4.缺点
会让没有得到锁的资源进入Block状态,争夺到资源之后又转为Running状态,这个过程涉及到操作系统用户模式和内核模式的切换,代价比较高。Java1.6为 synchronized 做了优化,增加了从偏向锁到轻量级锁再到重量级锁的过度,但是在最终转变为重量级锁之后,性能仍然较低。
七、CAS
1.什么是CAS?
CAS全称是Compare And Swap,即比较替换,是实现并发应用到的一种技术。操作包含三个操作数——内存位置(V),
预期原值(A)、新值(B)。如果预期原值和新值相等,处理器就会将地址自动更新为新值。否则,处理器不做处理。
2.为什么会有CAS?
如果只是用 synchronized 来保证同步会存在以下问题:
synchronized是一种悲观锁,会引起很大的性能问题。
多个线程竞争时,加锁、释放锁会导致较多的上下文切换和调度延时,引起性能问题。
一个线程得到资源会导致其他线程挂起。
3.实现原理
java不能直接访问操作系统底层,是通过native方法来访问。CAS底层是通过Unsafe类来实现原子性操作。
4.存在的问题
- 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带来很大的压力。
八、AbstractQueuedSynchronizer(AQS)
AQS抽象的队列式同步器,是一种基于状态(state)的链表管理方式。state 是用CAS去修改的。它是 java.util.concurrent 包中最重要的基石,要学习想学习 java.util.concurrent 包里的内容这个类是关键。 ReentrantLock、CountDownLatcher、Semaphore 实现的原理就是基于AQS。
想知道他怎么实现以及实现原理 可以参看这篇文章https://www.cnblogs.com/waterystone/p/4920797.html
九、Future
在处理并发时我们一般使用Runnable区执行异步任务,然而这样我们是拿不到返回值的,但是Future可以。
使用Future很简单,只需要把Runnable改成FutureTask。
十、线程池
java中的线程池实现类是ThreadPoolExecutor。说说它的几个参数:
- corePoolSize:核心线程数即一直存在线程池的线程数,即使处于空闲状态也不会被销毁。
只有设置allowCoreThreadTimeOut 为true时,才能销毁。 - maximumPoolSize:线程池中允许的最大线程数。
- keepAliveTime 非核心线程允许的最大空闲时间,超过就会销毁。
- workQueue:用来存放任务的队列。