文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

Java并发之同步器设计

2024-12-11 21:25

关注

计算机中通过设计同步器来协调进程(线程)之间执行顺序。同步器作用就像登机安检人员一样可以协调旅客按顺序通过。

在Java中,同步器可以理解为一个对象,它根据自身状态协调线程的执行顺序。比如锁(Lock),信号量(Semaphore),屏障(CyclicBarrier),阻塞队列(Blocking Queue)。

这些同步器在功能设计上有所不同,但是内部实现上有共通的地方。

同步器

同步器的设计一般包含几个方面:状态变量设计(同步器内部状态),访问条件设定,状态更新,等待方式,通知策略。

访问条件是控制线程是否能执行(访问共享对象)的条件,它往往与状态变量紧密相关。而通知策略是线程释放锁定状态后通知其它等待线程的方式,一般有以下几种情况

看下面例子,通过锁方式的同步器 

  1. public class Lock{  
  2.   // 状态变量 isLocked  
  3.   private boolean isLocked = false 
  4.   public synchronized void lock() throws InterruptedException{  
  5.     // 访问条件 当isLocked=false 时获得访问权限否则等待  
  6.     while(isLocked){  
  7.       // 阻塞等待  
  8.       wait();  
  9.     }  
  10.     //状态更新 线程获得访问权限  
  11.     isLocked = true 
  12.   }  
  13.   public synchronized void unlock(){  
  14.     //状态更新 线程释放访问权限  
  15.     isLocked = false 
  16.     // 通知策略 object.notify | object.notifyAll  
  17.     notify();   
  18.   }  

我们用计数信号量控制同时执行操作活动数。这里模拟一个连接池。 

  1. public class PoolSemaphore {  
  2.       // 状态变量 actives 计数器  
  3.     private int actives = 0 
  4.     private int max;  
  5.     public PoolSemaphore(int max) {  
  6.         this.max = max;  
  7.     }  
  8.     public synchronized void acquire() throws InterruptedException {  
  9.         //访问条件 激活数小于最大限制时,获得访问权限否则等待  
  10.         while (this.actives == max) wait();  
  11.         //状态更新 线程获得访问权限  
  12.         this.actives++;  
  13.         // 通知策略 object.notify | object.notifyAll  
  14.         this.notify();  
  15.     }  
  16.     public synchronized void release() throws InterruptedException {  
  17.         //访问条件 激活数不为0时,获得访问权限否则等待  
  18.         while (this.actives == 0) wait();  
  19.          //状态更新 线程获得访问权限  
  20.         this.actives--;  
  21.         // 通知策略 object.notify | object.notifyAll  
  22.         this.notify();  
  23.     }  

原子指令

同步器设计里面,最重要的操作逻辑是“如果满足条件,以更新状态变量来标志线程获得或释放访问权限”,该操作应具备原子性。

比如test-and-set 计算机原子指令,意思是进行条件判断满足则设置新值。 

  1. function Lock(boolean *lock) {   
  2.     while (test_and_set(lock) == 1);   

另外还有很多原子指令 fetch-and-add compare-and-swap,注意这些指令需硬件支持才有效。

同步操作中,利用计算机原子指令,可以避开锁,提升效率。java中没有 test-and-set 的支持,不过 java.util.concurrent.atomic 给我们提供了很多原子类API,里面支持了 getAndSet 和compareAndSet 操作。

看下面例子,主要在区别是等待方式不一样,上面是通过wait()阻塞等待,下面是无阻塞循环。 

  1. public class Lock{  
  2.   // 状态变量 isLocked  
  3.   private AtomicBoolean isLocked = new AtomicBoolean(false);  
  4.   public void lock() throws InterruptedException{  
  5.     // 等待方式 变为自旋等待  
  6.     while(!isLocked.compareAndSet(false, true));  
  7.     //状态更新 线程获得访问权限  
  8.     isLocked.set(true);  
  9.   }  
  10.   public synchronized void unlock(){  
  11.     //状态更新 线程释放访问权限  
  12.     isLocked.set(false);  
  13.   }  

关于阻塞扩展说明

阻塞意味着需要将进程或线程状态进行转存,以便还原后恢复执行。这种操作是昂贵繁重,而线程基于进程之上相对比较轻量。线程的阻塞在不同编程平台实现方式也有所不同,像Java是基于JVM运行,所以它由JVM完成实现。

在《Java Concurrency in Practice》中,作者提到

竞争性同步可能需要OS活动,这增加了成本。当争用锁时,未获取锁的线程必须阻塞。 JVM可以通过旋转等待(反复尝试获取锁直到成功)来实现阻塞,也可以通过操作系统挂起阻塞的线程来实现阻塞。哪种效率更高取决于上下文切换开销与锁定可用之前的时间之间的关系。对于短暂的等待,最好使用自旋等待;对于长时间的等待,最好使用暂停。一些JVM基于对过去等待时间的分析数据来自适应地在这两者之间进行选择,但是大多数JVM只是挂起线程等待锁定。

从上面可以看出JVM实现阻塞两种方式

JVM中通过 -XX: +UseSpinning 开启旋转等待, -XX: PreBlockSpi =10指定最大旋转次数。

AQS

AQS是AbstractQueuedSynchronizer简称。本节对AQS只做简单阐述,并不全面。

java.util.concurrent包中的 ReentrantLock,CountDownLatch,Semaphore,CyclicBarrier等都是基于是AQS同步器实现。

状态变量 是用 int state 来表示,状态的获取与更新通过以下API操作。     

  1. int getState()  
  2.     void setState(int newState)  
  3.     boolean compareAndSetState(int expect, int update) 

该状态值在不同API中有不同表示意义。比如ReentrantLock中表示持有锁的线程获取锁的次数,Semaphore表示剩余许可数。

关于等待方式和通知策略的设计

AQS通过维护一个FIFO同步队列(Sync queue)来进行同步管理。当多线程争用共享资源时被阻塞入队。而线程阻塞与唤醒是通过 LockSupport.park/unpark API实现。

它定义了两种资源共享方式。

每个节点包含waitStatus(节点状态),prev(前继),next(后继),thread(入队时线程),nextWaiter(condition队列的后继节点)

waitStatus 有以下取值。

AQS 几个关键 API

acquire(int arg)   

  1. public final void acquire(int arg) {  
  2.         if (  
  3.           // 尝试直接去获取资源,如果成功则直接返回  
  4.           !tryAcquire(arg)  
  5.             &&  
  6.             //线程阻塞在同步队列等待获取资源。等待过程中被中断,则返回true,否则false  
  7.             acquireQueued(  
  8.               // 标记该线程为独占方式,并加入同步队列尾部。  
  9.               addWaiter(Node.EXCLUSIVE), arg)   
  10.            )  
  11.             selfInterrupt();  
  12.     } 

release(int arg)   

  1. public final boolean release(int arg) {  
  2.          // 尝试释放资源  
  3.        if (tryRelease(arg)) {  
  4.            Node h = head 
  5.            if (h != null && h.waitStatus != 0)  
  6.              // 唤醒下一个线程(后继节点)  
  7.              unparkSuccessor(h);  
  8.            return true;  
  9.        }  
  10.        return false;  
  11.    }  
  1. private void unparkSuccessor(Node node) {  
  2.         ....  
  3.               Node s = node.next; // 找到后继节点  
  4.         if (s == null || s.waitStatus > 0) {//无后继或节点已取消  
  5.             s = null 
  6.            // 找到有效的等待节点   
  7.             for (Node t = tail; t != null && t != node; tt = t.prev)  
  8.                 if (t.waitStatus <= 0)  
  9.                     s = t 
  10.         }  
  11.         if (s != null)  
  12.             LockSupport.unpark(s.thread); // 唤醒 
  13.     } 

总结

本文记录并发编程中同步器设计的一些共性特征。并简单介绍了Java中的AQS。 

 

来源:segmentfault内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯