当前位置: 首页 > 科技观察

了解了这些锁的用法,多线程就懂了一半

时间:2023-03-13 14:02:36 科技观察

0x01:synchronizedJava中的synchronized关键字经常用来保持数据的一致性。synchronized机制是对共享资源加锁,只有获得锁的线程才能访问共享资源,这样可以强制对共享资源的访问是顺序的。Java开发者都知道synchronized,用它来实现多线程的同步操作是非常简单的。只要在对方需要同步的方法、类或者代码块中加上这个关键字,就可以保证同时最多有一个。线程执行同一个对象的同步代码,可以保证被装饰的代码在执行过程中不会受到其他线程的干扰。用synchronized修饰的代码具有原子性和可见性,在需要进程同步的程序中使用频率很高,可以满足一般的进程同步要求。synchronized(obj){//method...}synchronized的实现机制在软件层面依赖于JVM,因此其性能会随着Java版本的不断升级而提升。在Java1.6中,synchronized进行了自适应自旋、锁淘汰、锁粗化、轻量级锁和偏向锁等多项优化,效率有了大幅度的提升。在后来推出的Java1.7和1.8中,优化了这个关键字的实现机制。需要注意的是,线程在通过synchronized等待锁的时候,是不能被Thread.interrupt()打断的,所以在程序设计的时候一定要检查一下,确保合理,否则可能会造成尴尬的情况线程死锁。最后,虽然Java中实现的锁机制很多,而且有些锁机制的性能比synchronized更高,但是强烈建议在多线程应用中使用这个关键字,因为实现简单,后续工作做好了由JVM。高的。只有确定锁机制是当前多线程程序的性能瓶颈时,才考虑使用其他机制,如ReentrantLock等。0x02:ReentrantLock是可重入锁。顾名思义,这个锁可以被线程重复进入获取操作。ReentantLock继承了Lock接口,实现了接口中定义的方法。除了完成synchronized能做的所有工作外,它还提供了响应式中断锁、可轮询锁请求、定时锁等方法来避免多线程死锁。.Lock实现的机制依赖于特殊的CPU指定,因此可以认为不受JVM的束缚,底层实现可以通过其他语言平台来完成。在并发量小的多线程应用中,ReentrantLock和synchronized的性能相差无几,但是在高并发的情况下,synchronized的性能会迅速下降几十倍,而ReentrantLock的性能仍然可以保持一个水平。因此,我们推荐在高并发情况下使用ReentrantLock。ReentrantLock引入了两个概念:公平锁和非公平锁。公平锁是指锁的分配机制是公平的。通常,最先申请锁的线程会先分配到锁。反之,JVM根据就近原则随机分配锁的机制称为非公平锁。ReentrantLock在构造函数中提供了公平锁的初始化方法,默认是非公平锁。这是因为非公平锁的实际执行效率远高于公平锁。除非程序有特殊需要,非公平锁的分配机制是最常用的。ReentrantLock使用方法lock()和unlock()来执行锁定和解锁操作。与synchronized由JVM自动解锁不同,ReentrantLock在加锁后需要手动解锁。为了避免出现程序异常无法正常解锁的情况,使用ReentrantLock时必须在finally控制块中进行解锁操作。通常的用法如下:Locklock=newReentrantLock();try{lock.lock();//...执行任务操作5}finally{lock.unlock();}0x03:Semaphore以上两种锁机制类型就是“互斥锁”,学过操作系统的都知道,互斥是进程同步关系的一种特例,相当于只有一个临界资源,所以最多只能有一个线程同时提供服务。然而,在实际复杂的多线程应用中,可能存在多个关键资源。这时候我们可以使用Semaphore信号量来完成对多个关键资源的访问。Semaphore基本可以完成ReentrantLock的所有工作,使用方法与之类似。acquire()和release()方法用于获取和释放关键资源。经过实测,Semaphone.acquire()方法默认为响应式中断锁,与ReentrantLock.lockInterruptibly()一致,也就是说在等待临界资源时可以通过Thread.interrupt()方法中断。此外,Semaphore还实现了可轮询锁请求和定时锁的功能,只是方法名tryAcquire与tryLock不同,其使用方法与ReentrantLock几乎相同。Semaphore还提供了公平锁和非公平锁的机制,也可以在构造函数中设置。Semaphore的释放锁操作也是手动进行的,所以和ReentrantLock一样,为了避免线程抛出异常导致无法正常释放锁的情况,释放锁的操作也必须在finally代码块中完成。获取权限的acquire()底层实现类似于CountDownLatch.countdown();release()释放权限的底层实现是与acquire()的互惠过程。0x04:CountDownLatchCountDownLatch是一个计数器锁,通过它可以完成类似于阻塞当前线程的功能,即:一个线程或多个线程等待,直到其他线程执行的操作完成。CountDownLatch是用给定的计数器初始化的,对计数器的操作是原子操作,即同一时刻只有一个线程可以操作计数器。调用该类的await方法的线程会一直阻塞,直到其他线程调用countDown方法使当前计数器的值为零,每次调用countDown计数器的值减1。当计数器值减为零时,所有通过调用await()方法处于等待状态的线程将继续执行。这种现象只会出现一次,因为计数器无法重置。如果业务需要可以重置计数的版本,可以考虑使用CycliBarrier。在某些业务场景下,程序执行需要等待某个条件完成后才能继续执行后续操作;典型应用如并行计算,当某个处理的计算量较大时,可以将计算任务拆分成多个,等待所有子任务完成后,父任务获取所有子任务的计算结果,并进行汇总。0x05:CyclicBarrierCyclicBarrier也是一个同步辅助类,它允许一组线程互相等待,直到到达一个共同的障碍点。通过它,多个线程可以相互等待,只有每个线程都准备好了,才能继续执行后续的操作。与CountDownLatch类似,也是通过计数器来实现的。当线程调用await方法时,线程进入等待状态,计数器加1,当计数器的值达到设定的初始值时,所有因调用await进入等待状态的线程被唤醒,继续执行执行后续操作。因为CycliBarrier释放等待线程后可以重用,所以称为循环屏障。CycliBarrier支持可选的Runnable。在计数器的值达到设定值后(但在所有线程被释放之前),Runnable运行一次。请注意,Runnable仅在每个障碍点运行一个。使用场景类似于CountDownLatch和CountDownLatch的区别。CountDownLatch主要实现1个或N个线程需要等待其他线程完成某个操作后才能继续执行操作。它描述了1个线程或N个线程在等待其他线程。关系。CyclicBarrier主要实现多个线程相互等待,直到所有线程都满足条件后才能继续执行后续操作,描述了多个线程相互等待的关系。CountDownLatch是一次性的,而CyclicBarrier可以重置和重复使用。