synchronized
作用
- 修饰普通方法
- 修饰静态方法
- 修饰代码块
原理
加锁:monitorenter
每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
- 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
- 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
- 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
释放锁:monitorexit
执行monitorexit的线程必须是objectref所对应的monitor的所有者。
指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。
详见《锁》
Synchronized & ReentrantLock
相同点:
- 协调多线程对共享对象、变量的访问
- 可重入,同一线程可以多次获得同一个锁
- 都保证了可见性和互斥性
不同点:
- ReentrantLock显示获得、释放锁,synchronized隐式获得释放锁。synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
- ReentrantLock可响应中断、可轮回,为处理锁的不可用性提供了更高的灵活性。使用synchronized时,等待的线程会一直等待下去,不能够响应中断
- ReentrantLock是API级别的,synchronized是JVM级别的
- ReentrantLock可以实现公平锁,synchronized是非公平的
- ReentrantLock通过Condition可以绑定多个条件。一个ReentrantLock对象可以同时绑定多个Condition对象,而在synchronized中,锁对象的wait()和notify()或notifyAll()方法可以实现一个隐含的条件,如果要和多余一个条件关联的时候,就不得不额外地添加一个锁,而ReentrantLock则无须这么做,只需要多次调用new Condition()方法即可。
- 底层实现不一样, synchronized是同步阻塞,使用的是悲观并发策略,lock是同步非阻塞,采用的是乐观并发策略
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-7. before于线程A从ThreadB.join()操作成功返回。
- 程序中断规则:对线程interrupted()方法的调用先行于被中断线程的代码检测到中断时间的发生。
对象finalize规则:一个对象的初始化完成(构造函数执行结束)先行于发生它的finalize()方法的开始。
wait/notify/sleep/yield/join
wait
wait的三种方法:
- wait()方法的作用是将当前运行的线程挂起(即让其进入阻塞状态),直到notify或notifyAll方法来唤醒线程.
- wait(long timeout),该方法与wait()方法类似,唯一的区别就是在指定时间内,如果没有notify或notifAll方法的唤醒,也会自动唤醒
- 至于wait(long timeout,long nanos),本意在于更精确的控制调度时间,不过从JDK1.8来看,该方法貌似没有完整的实现该功能
wait方法的使用必须在同步的范围内,否则就会抛出IllegalMonitorStateException异常,wait方法的作用就是阻塞当前线程等待notify/notifyAll方法的唤醒,或等待超时后自动唤醒。
notify/notifyAll
如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。
当有线程调用了对象的 notifyAll()方法(唤醒所有 wait 线程)或 notify()方法(只随机唤醒一个 wait 线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。也就是说,调用了notify后只要一个线程会由等待池进入锁池,而notifyAll会将该对象等待池内的所有线程移动到锁池中,等待锁竞争
优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用 wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了 synchronized 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。
sleep/yield/join
sleep:sleep方法的作用是让当前线程暂停指定的时间(毫秒),sleep方法是最简单的方法,在上述的例子中也用到过,比较容易理解。唯一需要注意的是其与wait方法的区别。最简单的区别是,wait方法依赖于同步,而sleep方法可以直接调用。而更深层次的区别在于sleep方法只是暂时让出CPU的执行权,并不释放锁。而wait方法则需要释放锁。
yield:yield方法的作用是暂停当前线程,以便其他线程有机会执行,不过不能指定暂停的时间,并且也不能保证当前线程马上停止。yield方法只是将Running状态转变为Runnable状态。
- 调度器可能会忽略该方法。
- 使用的时候要仔细分析和测试,确保能达到预期的效果。
- 很少有场景要用到该方法,主要使用的地方是调试和测试。
join:join方法的作用是父线程等待子线程执行完成后再执行,换句话说就是将异步执行的线程合并为同步的线程。JDK中提供三个版本的join方法,其实现与wait方法类似,join()方法实际上执行的join(0),而join(long millis, int nanos)也与wait(long millis, int nanos)的实现方式一致,暂时对纳秒的支持也是不完整的。join方法就是通过wait方法来将线程的阻塞,如果join的线程还在执行,则将当前线程阻塞起来,直到join的线程执行完成,当前线程才能执行。不过有一点需要注意,这里的join只调用了wait方法,却没有对应的notify方法,原因是Thread的start方法中做了相应的处理,所以当join的线程执行完成以后,会自动唤醒主线程继续往下执行。
闭锁、信号量、栅栏
Semaphore信号量
跟锁机制存在一定的相似性,semaphore也是一种锁机制,所不同的是,reentrantLock是只允许一个线程获得锁,而信号量持有多个许可(permits),允许多个线程获得许可并执行。可以用来控制同时访问某个特定资源的操作数量,或者同时执行某个指定操作的数量。
CountDownLatch闭锁
允许一个或多个线程一直等待,直到其他线程的操作执行完后再执行。CountDownLatch是通过一个计数器来实现的,计数器的初始值为线程的数量。每当一个线程完成了自己的任务后,计数器的值就会减1。当计数器值到达0时,它表示所有的线程已经完成了任务,然后在闭锁上等待的线程就可以恢复执行任务。
主要方法:
- CountDownLatch.await():将某个线程阻塞住,直到计数器count=0才恢复执行。
- CountDownLatch.countDown():将计数器count减1。
使用场景:
- 实现最大的并行性:有时我们想同时启动多个线程,实现最大程度的并行性。例如,我们想测试一个单例类。如果我们创建一个初始计数为1的CountDownLatch,并让所有线程都在这个锁上等待,那么我们可以很轻松地完成测试。我们只需调用 一次countDown()方法就可以让所有的等待线程同时恢复执行。
- 开始执行前等待n个线程完成各自任务:例如应用程序启动类要确保在处理用户请求前,所有N个外部系统已经启动和运行了。
- 死锁检测:一个非常方便的使用场景是,你可以使用n个线程访问共享资源,在每次测试阶段的线程数目是不同的,并尝试产生死锁。
- 计算并发执行某个任务的耗时。
CyclicBarrier栅栏
用于阻塞一组线程直到某个事件发生。所有线程必须同时到达栅栏位置才能继续执行下一步操作,且能够被重置以达到重复利用。而闭锁是一次性对象,一旦进入终止状态,就不能被重置。
区别
- 闭锁用来等待事件,就是说闭锁用来等待的事件就是countDown事件,只有该countDown事件执行后所有之前在等待的线程才有可能继续执行;而栅栏没有类似countDown事件控制线程的执行,只有线程的await方法能控制等待的线程执行。
- 栅栏用来等待线程,CyclicBarrier强调的是n个线程,大家相互等待,只要有一个没完成,所有线程都得等着。
闭锁是一次性对象,一旦进入终止状态,就不能重置。而栅栏可以使一定数量的参入方反复的在栅栏位置汇集。
并发编程实战笔记
- synchronized内置锁可重入:即线程获取锁后在获取已获锁会成功。_操作粒度为线程而非调用,即线程可重复获取已持有的锁而非单次调用_。获取一次计数值+1,退出一次代码块计数值-1.
- 当获取与对象关联的锁时,并不能阻止其他线程获操作对象访问该对象。某个线程在获得对象的锁之后,只能阻止其他线程获得同一个锁。(OneClass.class和OneClass.oneAttribute是两个不同的锁,前者并不包括后者。对于一个静态属性SC.SA,SC.SA锁与其实例SC.SA锁不是同一个锁,既是他们代表的是同一个东西。同样,SC.class锁与SC锁也不能保证SA线程安全)
- 多线程中使用共享且可变的long/double类型变量是不安全的,因为JVM读取64位数据时会折分为2个32位(除非用volatile关键字或者加锁)。因此long,double无最低安全性(也就是volatile long/double 读写是原子的)
- volatile变量不会被重排序,不会被缓存在寄存器或者其他处理器不可见的位置。因此读取volatile变量总会返回最新的写入值。volatile开销很低,只比无volatile高一点点。
- 当某个对象封闭在一个线程中时,这种用法将自动实现线程安全性,既是被封闭的对象本身不是线程安全的。(线程封闭:不与其他线程共享数据)
- 任何线程都可以在不需要额外同步的情况下安全地访问不可变对象,即使在发布这些对象的时候没有使用同步。
- 对于集合,在使用迭代器对其遍历过程中,增加或者删除而不是通过Iterator.remove删除会引发ConcurrentModificationException(快速失败),快速失败是善意的,虽然报错但语句会执行成功。HashMap和Vector快速失败机制则不同。synchronized不会阻止快速失败。(隐式迭代:Collection.toString()。当容器作为另一个容器的元素或者键值时,hashCode()和equals()方法也有迭代)。安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。
- ConcurrentHashMap:分段锁,JDK1.8中采用HashEntry<K,V>保存数据,对数组每一行加锁。结构:数组+单向链表+红黑树
- CopyonWrite…:在迭代时共享资源,在修改是先创建一个副本,更新后代替原有的使用。
- 工作密取:每个消费者有自己的双端队列,完成了自己队列的全部工作时,可以从其他队列队尾秘密地获取工作。适用于执行某个工作可能导致产生更多工作的情况(例:网页爬虫)
- BlockingQueue:put(),take()在队空或者队满时会进入阻塞,当被interrupt()则会抛出InterruptedException。而offer()和poll()不会。
处理InterruptedException:- 传递,即直接抛出或简单处理后抛出
- 不能传递时调用interrupt(),产生一个中断
中断是协助性质的,它不会强制终止或阻塞线程,二是由被interrupt的线程决定作出什么响应。
interrupt()会使被Object.wait(),Thread.join(),Thread.sleep()阻塞的线程退出阻塞状态。
- 闭锁:延迟线程的进度直到其到达终止状态,之后会永远保持开启状态。
CountDownLatch:计数器到0时开放闭锁。方法:await()加入闭锁,countDown()减一,计数器初始值在构造函数中。
FutureTask:先run()后get(),get()会阻塞知道结果产生,相当于闭锁。可以通过Thread t=new Thread(FutureTask f);t.start();提前开始任务,f.get()可减少等待时间,因为FutureTask implements Future,Runable.不能使用f.run(),这个方法也会阻塞到运行结束,和get()一样 - 信号量:Semaphore:用于控制访问或则操作的数量。acquire()会阻塞直到有许可(或被中断或超时)。relase()会释放一个信号量(有可能使可用信号量超过初始信号量数)
- 栅栏cyclicBarrier:
- 多个线程在栅栏中等待时,其中一个线程调用interrupt()方法,此线程会突破栅栏并报出InterruptException,其他线程也会突破栅栏并报BrockingBarrierException异常。
- 构造方法CyclicBarrier(int parices,Runable barrierAction)中,后一个是一个线程,当栅栏中等待的线程到达阈值时会先执行barrierAction,可用于执行合并处理
- ConcurrentHashMap.putIfAbsent(K,V)如果没有则放入
- FutureTask cancel(boolean mayInterruptIfRunning)用于取消任务,参数表示是否向正在工作的任务发送中断 cancel()后调用get()会报错CancellationException,是默认cache的错误不包括这个错误,程序会终止运行。isCancelled()可判断是否取消
- ExecutorService.invokeAll(collection,time,timeUnie)将callable集合,最长等待时间,时间单位传入,返回一个Future的集合(与callable集合相同),超时未运行结束的任务会被cancel()
- ComplesionService内部维护一个BlockingQueue来保存状态为”结束“的Future,submit(callable)提交任务,take()获取Future,解决了几个任务会等待一个Future.get()阻塞至产生结果的问题
- ExecutorService内部维护的线程任务在程序结束后不会自动销毁,需要调用shutdown()(Runable和Callable都不会,submit(Runable)会产生一个Future(Runable,null)),而单纯的FutureTask不需要(单纯的FutureTask需要点调用run()之后get()才会有结果)
- sleep()与wait()区别:sleep()让线程暂停工作一段时间,但不是放对象锁。wait()释放对象锁,并使本线程进入等待状态,等待后续再次获取对象锁。
ReentrantLock
ReentrantLock意思为可重入锁,指的是一个线程能够对一个临界资源重复加锁。
与synchronized区别:
AQS
Java中的大部分同步类(Lock、Semaphore、ReentrantLock等)都是基于AbstractQueuedSynchronizer(简称为AQS)实现的。AQS是一种提供了原子式管理同步状态、阻塞和唤醒线程功能以及队列模型的简单框架。
- 上图中有颜色的为Method,无颜色的为Attribution。
- 总的来说,AQS框架共分为五层,自上而下由浅入深,从AQS对外暴露的API到底层基础数据。
- 当有自定义同步器接入时,只需重写第一层所需要的部分方法即可,不需要关注底层具体的实现流程。当自定义同步器进行加锁或者解锁操作时,先经过第一层的API进入AQS内部方法,然后经过第二层进行锁的获取,接着对于获取锁失败的流程,进入第三层和第四层的等待队列处理,而这些处理方式均依赖于第五层的基础数据提供层。
原理概述
AQS核心思想是,如果被请求的共享资源空闲,那么就将当前请求资源的线程设置为有效的工作线程,将共享资源设置为锁定状态;如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中。
CLH:Craig、Landin and Hagersten队列,是单向链表,AQS中的队列是CLH变体的虚拟双向队列(FIFO),AQS是通过将每条请求共享资源的线程封装成一个节点来实现锁的分配。
主要原理图如下:
AQS使用一个Volatile的int类型的成员变量来表示同步状态,通过内置的FIFO队列来完成资源获取的排队工作,通过CAS完成对State值的修改。