# Java 并发编程的艺术(三)

news2024/11/22 15:52:06

Java 并发编程的艺术(三)

文章目录

  • Java 并发编程的艺术(三)
    • Java 内存模型
      • Java 内存模型的基础
        • Java 内存模型的抽象结构
        • 从源代码到指令序列的重排序
      • 重排序
      • happens-before
        • JMM 的设计
        • happens-before 的定义
    • Java 中的锁
      • Lock 接口
        • 代码清单
        • 相关API
      • 重入锁
        • 公平锁和非公平锁
      • 读写锁
        • 读写锁的接口与实例
      • Condition 接口
    • Java 并发容器和框架
      • ConcurrentHashMap 的实现原理与使用
        • ConcurrentHashMap 的结构
        • ConcurrentHashMap 的初始化
          • segments 数组
        • 定位 Segment
        • ConcurrentHashMap 的操作
          • get
          • put
      • 阻塞队列
        • 什么是阻塞队列
        • Java 里的阻塞队列
          • ArrayBlockingQueue
          • LinkedBlockingQueue
          • SynchronousQueue
          • PriorityBlockingQueue
          • DelayQueue
        • 阻塞队列的实现原理

Java 内存模型

Java 内存模型的基础

Java 内存模型的抽象结构

  • 线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是 JMM 的一个抽象概念,并不真实存在。
    在这里插入图片描述

从源代码到指令序列的重排序

  • 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排
    语句的执行顺序。

  • 指令级并行的重排序。现代处理器采用了指令级并行技术将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。

  • 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

    • Java 源代码到最终实际执行的指令序列,会分别经历下面 3 种重排序
      在这里插入图片描述

重排序

  • 重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段

happens-before

JMM 的设计

/**
 * 存在 3 个 happens-before 关系
 * a happens-before c
 * b happens-before c
 * a happens-before b
 * <p> 1 和 2 是硬性条件 计算 c;3 这个条件不会影响计算结果 </p>
 */
@Test
public void test1() {
    int a = 1;
    int b = 2;
    int c = a + b;
}
  • JMMhappens-before 要求禁止的重排序分为了下面两类 :
    • 会改变程序执行结果的重排序
    • 不会改变程序执行结果的重排序
  • JMM 对这两种不同性质的重排序,采取了不同的策略:
    • 对于会改变程序执行结果的重排序, JMM 要求编译器和处理器必须禁止这种重排序
    • 对于不会改变程序执行结果的重排序, JMM 对编译器和处理器不做要求(JMM允许这种重排序)
      在这里插入图片描述

happens-before 的定义

  • 如果一个操作 happens-before 另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
  • 两个操作之间存在 happens-before 关系,并不意味着 Java 平台的具体实现必须要按照 happens-before 关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before 关系来执行的结果一致,那么这种重排序并不非法(也就是说,JMM 允许这种重排序)。

Java 中的锁

Lock 接口

  • 锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁能够防止多个线程同时访问共享资源(但是有些锁可以允许多个线程并发的访问共享资源,比如读写锁)。

代码清单

Lock lock = new ReentrantLock();
lock.lock();
try {
    // 具体业务
} finally {
 lock.unlock();
}

相关API

在这里插入图片描述

  • 实例代码:三个窗口共售出 100 张票
/**
 * 三个窗口总共买 100 张票
 *
 * @throws InterruptedException interrupted exception
 */
@Test
public void test2() throws InterruptedException {
    Tickets tickets = new Tickets();
    new Thread(tickets, "1号窗口").start();
    new Thread(tickets, "2号窗口").start();
    new Thread(tickets, "3号窗口").start();
    Thread.sleep(10000);
}

static class Tickets implements Runnable {

    private final Logger logger = LoggerFactory.getLogger(Tickets.class);
    private int tickets = 100;
    private final Lock lock = new ReentrantLock(false);

    @Override
    public void run() {
        while (true) {
            lock.lock();
            try {
                /*上Lock锁*/
                if (tickets > 0) {
                    logger.info("{} ======完成售票,余票为{}", Thread.currentThread().getName(), --tickets);
                } else {
                    logger.info("{} ======余票为{}", Thread.currentThread().getName(), tickets);
                    break;
                }
            } catch (Exception e) {
                logger.error(e.getMessage(), e);
            } finally {
                // 释放 Lock 锁避免发生死锁
                lock.unlock();
            }
        }

    }
}

重入锁

  • 支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁。除此之外,该锁的还支持获取锁时的公平和非公平性选择。

公平锁和非公平锁

  • 如果在绝对时间上,先对锁进行获取的请求一定先被满足,那么这个锁是公平的,反之,是不公平的。公平的获取锁,也就是等待时间最长的线程最优先获取锁,也可以说锁获取是顺序的。 ReentrantLock 提供了一个构造函数,能够控制锁是否是公平的。
// 非公平锁
Lock lock = new ReentrantLock(false);
// 公平锁
Lock lock = new ReentrantLock(true);

读写锁

  • 读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大提升。
  • ReadWriteLock 中的读锁是共享锁,写锁是排他锁,共享锁允许不同线程同时读,排他锁只允许一个线程写,其他线程等待。

读写锁的接口与实例

public interface ReadWriteLock {
    /**
     * Returns the lock used for reading.
     *
     * @return the lock used for reading
     */
    Lock readLock();

    /**
     * Returns the lock used for writing.
     *
     * @return the lock used for writing
     */
    Lock writeLock();
}
  • ReadWriteLockTest.java
public class ReadWriteLockTest {

    @Test
    public void test() throws InterruptedException {
        // 创建读写锁
        ReadWriteLock lock = new ReentrantReadWriteLock();
        // 读锁
        Lock readLock = lock.readLock();
        // 写锁
        Lock writeLock = lock.writeLock();
        Map<String, Object> map = new HashMap<>();
        for (int i = 10; i > 0; i--) {
            String key = String.valueOf(System.currentTimeMillis());
            WriteTask writeTask = new WriteTask(writeLock, map, key);
            ReadTask readTask = new ReadTask(readLock, map, key);
            ThreadPoolUtils.executor(writeTask);
            ThreadPoolUtils.executor(readTask);
        }
        Thread.sleep(10000);

    }

    /**
     * 读任务
     */
    private class ReadTask implements Runnable {
        private final Logger logger = LoggerFactory.getLogger(ReadTask.class);
        private Lock readLock;
        private Map<String, Object> map;
        private String key;

        private ReadTask(Lock readLock, Map<String, Object> map, String key) {
            this.readLock = readLock;
            this.map = map;
            this.key = key;
        }

        @Override
        public void run() {
            readLock.lock();
            try {
                if (Objects.nonNull(map)) {
                    Object object = map.get(key);
                    logger.info("ReadTask read value:{}", object);
                }
            } catch (Exception e) {
                logger.error(e.getMessage(), e);
            } finally {
                // 释放锁
                readLock.unlock();
            }
        }
    }

    /**
     * 写任务
     */
    private class WriteTask implements Runnable {

        private final Logger logger = LoggerFactory.getLogger(WriteTask.class);
        private Lock writeLock;
        private Map<String, Object> map;
        private String key;

        private WriteTask(Lock writeLock, Map<String, Object> map, String key) {
            this.writeLock = writeLock;
            this.map = map;
            this.key = key;
        }

        @Override
        public void run() {
            writeLock.lock();
            try {
                if (Objects.isNull(map)) {
                    map = new HashMap<>();
                }
                long l = System.currentTimeMillis();
                map.put(key, l);
                logger.info("WriteTask write value:{}", l);
            } catch (Exception e) {
                logger.error(e.getMessage(), e);
            } finally {
                // 释放锁
                writeLock.unlock();
            }
        }
    }
}

Condition 接口

  • 任意一个 Java 对象,都拥有一组监视器方法(定义在 java.lang.Object 上),主要包括 wait()wait(long timeout)notify()以及 notifyAll()方法,这些方法与 synchronized 同步关键字配合,可以实现等待/通知模式。
public class ConditionTest {

    private static final Logger logger = LoggerFactory.getLogger(ConditionTest.class);

    /**
     * Condition 是一个多线程间协调通信的工具类,使得某个,或者某些线程一起等待某个条件(Condition),
     * 只有当该条件具备( signal 或者 signalAll方法被带调用)时 ,这些等待线程才会被唤醒,从而重新争夺锁。
     *
     * @throws InterruptedException interrupt exception
     */
    @Test
    public void test1() throws InterruptedException {
        Lock lock = new ReentrantLock();
        Condition condition = lock.newCondition();
        new Thread(() -> {
            try {
                lock.tryLock();
                logger.info("线程:{} 等待信号", Thread.currentThread().getId());
                condition.await();
            } catch (InterruptedException e) {
                logger.error(e.getMessage(), e);
            } finally {
                logger.info("线程:{} 得到信号", Thread.currentThread().getId());
                lock.unlock();
            }
        }).start();
        TimeUnit.MILLISECONDS.sleep(10);
        new Thread(() -> {
            lock.tryLock();
            logger.info("线程:{} 拿到锁", Thread.currentThread().getId());
            condition.signal();
            logger.info("线程:{} 发出信号", Thread.currentThread().getId());
            lock.unlock();
        }).start();
        TimeUnit.SECONDS.sleep(2);
    }

}

Java 并发容器和框架

ConcurrentHashMap 的实现原理与使用

  • 在并发编程中使用 HashMap 可能导致程序死循环。而使用线程安全的 HashTable 效率又非常低下,基于以上两个原因,便有了 ConcurrentHashMap 的登场机会。

ConcurrentHashMap 的结构

  • 类图
    在这里插入图片描述
  • 结构图
    在这里插入图片描述

ConcurrentHashMap 的初始化

  • ConcurrentHashMap 初始化方法是通过 initialCapacityloadFactorconcurrencyLevel 等几个参数来初始化 segment 数组、段偏移量 segmentShift、段掩码segmentMask 和每个 segment 里的 HashEntry 数组来实现的 。
segments 数组
  • 初始化 segments 数组 :segments 数组的长度是 2 的 N 次方
  • 初始化 segmentShiftsegmentMask :这两个全局变量需要在定位 segment 时的散列算法里使用, sshift 等于 ssize 从 1 向左移位的次数
  • 初始化每个 segmentsegment 的容量 threshold=(int) cap*loadFactor,默认情况下 initialCapacity 等于16loadfactor 等于 0.75,通过运算 cap 等于 1threshold 等于零。

定位 Segment

  • ConcurrentHashMap 使用分段锁 Segment 来保护不同段的数据,那么在插入和获取元素的时候,必须先通过散列算法定位到 Segment

ConcurrentHashMap 的操作

get
  • get 操作的高效之处在于整个 get 过程不需要加锁,除非读到的值是空才会加锁重读
public V get(Object key) {
    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
    // 根据key.hashCode()计算hash: 运算后得到得到更散列的hash值
    int h = spread(key.hashCode());
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (e = tabAt(tab, (n - 1) & h)) != null) {

        // 如果所要找的元素就在数组上,直接返回结果
        if ((eh = e.hash) == h) {
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val;
        }
        
        // 条件成立:即,hash小于0 分2种情况,是树或者正在扩容,需要借助find方法寻找元素,find的寻找方式依据Node的不同子类有不同的实现方式:
        	// 情况一:eh=-1 是fwd结点 -> 说明当前table正在扩容,且当前查询的这个桶位的数据已经被迁移走了,需要借助fwd结点的内部方法find去查询
        	// 情况二:eh=-2 是TreeBin节点 -> 需要使用TreeBin 提供的find方法查询。
        else if (eh < 0)
            return (p = e.find(h, key)) != null ? p.val : null;

        // 说明是当前桶位已经形成链表的这种情况: 遍历整个链表寻找元素
        while ((e = e.next) != null) {
            if (e.hash == h &&
                ((ek = e.key) == key || (ek != null && key.equals(ek))))
                return e.val;
        }
    }
    return null;
}
put
  • 由于 put 方法里需要对共享变量进行写入操作,所以为了线程安全,在操作共享变量时必须加锁。
  • put 方法首先定位到 Segment,然后在 Segment 里进行插入操作。
  • 插入操作需要经历两个步骤,第一步判断是否需要对 Segment 里的 HashEntry 数组进行扩容,第二步定位添加元素的位置,然后将其放在 HashEntry 数组里。

阻塞队列

什么是阻塞队列

  • 在队列为空时等待从队列中获取元素,或者在队列已满时等待向队列中添加元素

  • 阻塞队列常用于生产者和消费者的场景,生产者是向队列里添加元素的线程,消费者是从队列里取元素的线程。阻塞队列就是生产者用来存放元素、消费者用来获取元素的容器。

Java 里的阻塞队列

ArrayBlockingQueue
  • 有界的队列,基于数组创建,在创建的时候容量确定
// 公平的阻塞队列
ArrayBlockingQueue fairQueue = new ArrayBlockingQueue(1000, true);
LinkedBlockingQueue
  • 可选的有界的队列,它的容量可以在创建时指定,也可以不指定,默认为Integer.MAX_VALUE,即无界队列。
  • ArrayBlockingQueue不同,LinkedBlockingQueue在队列已满时仍然可以继续添加元素,只有在指定了容量并且队列已满时才会阻塞添加操作
SynchronousQueue
  • 一个没有存储空间的阻塞队列。每个插入操作必须等待另一个线程的移除操作。
PriorityBlockingQueue
  • PriorityBlockingQueue 是一个支持优先级的无界阻塞队列。
  • 默认情况下元素采取自然顺序升序排列。也可以自定义类实现 compareTo()方法来指定元素排序规则,或者初始化PriorityBlockingQueue 时,指定构造参数 Comparator 来对元素进行排序。
  • 需要注意的是不能保证同优先级元素的顺序。
DelayQueue
  • DelayQueue 是一个支持延时获取元素的无界阻塞队列。队列使用 PriorityQueue 来实
    现。队列中的元素必须实现 Delayed 接口,在创建元素时可以指定多久才能从队列中获
    取当前元素。只有在延迟期满时才能从队列中提取元素。

  • DelayQueue 非常有用,可以将 DelayQueue 运用在以下应用场景。

    • 缓存系统的设计:可以用 DelayQueue 保存缓存元素的有效期,使用一个线程循环查询 DelayQueue,一旦能从 DelayQueue 中获取元素时,表示缓存有效期到了。
    • 定时任务调度:使用 DelayQueue 保存当天将会执行的任务和执行时间,一旦从DelayQueue 中获取到任务就开始执行,比如 TimerQueue 就是使用 DelayQueue实现的

阻塞队列的实现原理

  • 当队列满时,插入线程会被阻塞,直到队列有空闲位置;当队列为空时,获取线程会被阻塞,直到队列有元素可用。这种阻塞和唤醒的机制是通过条件变量来实现的。
public ArrayBlockingQueue(int capacity, boolean fair) {
    if (capacity <= 0)
        throw new IllegalArgumentException();
    this.items = new Object[capacity];
    // 使用ReentrantLock提供线程安全。此锁确保一次只有一个线程可以访问队列。
    lock = new ReentrantLock(fair);
    // 两个条件变量notEmpty和notFull用于管理队列的阻塞行为。notEmpty条件用于在队列为空时阻塞线程,而notFull条件用于在队列已满时阻塞线程。
    notEmpty = lock.newCondition();
    notFull =  lock.newCondition();
}
  • take方法
public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (count == 0)
            // 当前获取lock的线程进入到等待队列
            notEmpty.await();
        return dequeue();
    } finally {
        lock.unlock();
    }
}

private E dequeue() {
    // assert lock.getHoldCount() == 1;
    // assert items[takeIndex] != null;
    final Object[] items = this.items;
    @SuppressWarnings("unchecked")
    E x = (E) items[takeIndex];
    items[takeIndex] = null;
    if (++takeIndex == items.length)
        takeIndex = 0;
    count--;
    if (itrs != null)
        itrs.elementDequeued();
    // signal()唤醒一个等待线程
    notFull.signal();
    return x;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/747704.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Java零基础入学,你应该了解什么

最近很多人问我想学Java但是一点基础没有&#xff0c;网上看了一堆学习线路图还是无从下手。今天耗时3小时整理了一套保姆级的Java入门教程&#xff0c;建议收藏按照线路图一点点学习。 一、Java的概况 Java是1995年6月美国Sun公司推出的计算机语言。Java支付是James Gosling…

java动态导出excel头

java动态导出excel头 java根据动态头导出excel文件一、需求背景1、调用接口将表头传给给后端2、请求结果展示3、核心代码1、工具类&#xff0c;注意异常抛出类如报错&#xff0c;需自定义异常类2、标题设置类3、单元各简单设置类4、controller接收参数 java根据动态头导出excel…

大势Inside |《倾斜摄影测量实景三维建模技术规程》发布

2023年6月21日&#xff0c;由广西壮族自治区自然资源产品质量检验中心、广西科学院、武汉大学提出&#xff0c;武汉大势智慧科技有限公司、武汉大学、广西科学院等27家单位起草的广西人工智能学会团体标准《倾斜摄影测量实景三维建模技术规程》正式发布。 此前&#xff0c;广西…

今天给大家分享几款非常实用的小工具

在当今数字化的时代&#xff0c;我们离不开各种智能设备和应用程序。然而&#xff0c;有时候我们可能会感到需要一些简单而实用的小工具来帮助我们更高效地完成日常任务或提升生活品质。今天我将与大家分享几款非常实用的小工具&#xff0c;这些工具可以在我们的生活中发挥积极…

Vue3+ElementPlus el-tree递归获取当前选中节点的所有子节点ID (适用所有场景)

点击节点触发两遍此方法问题 将@check-change改成@check即可~ 然后说下选中当前节点去获取其节点下的所有子节点的ID问题,网上搜了许多没有合适自己的方法,项目需求是单选,用了父子不关联的方法实现的单选,需要做下控制就是父节点不允许点击的情况下的子节点也不允许点击…

程序员如何高质量重构代码?

有道无术&#xff0c;术尚可求也&#xff0c;有术无道止于术。你好&#xff0c;我是程序员雪球&#xff0c;今天和你聊聊程序员重构代码那些事。 程序员重构代码的重要性不言而喻&#xff0c;但如何进行有效的重构呢&#xff1f;下面是一些建议和指导。 为什么要重构&#xff…

宝塔安装勾股OA系列服务

勾股oa地址 勾股OA办公系统—开源的OA办公系统 一、服务器配置 二、安装宝塔 1.下载宝塔面板、设置宝塔面板、增加网站。 yum install -y wget && wget -O install.sh https://download.bt.cn/install/install_6.0.sh && sh install.sh ed8484bec 根据提示…

Elasticsearch 倒排索引原理

看下面这个表格里的文档内容&#xff1a; 如果我这时候想要在这么多文档中查找带有 比亚迪 的我要怎么查&#xff0c;传统这个查询里面我想查这个比亚迪的话。就是先在文档1里面搜索一下有没有 比亚迪&#xff0c;没有&#xff0c;我在到文档2中查找比亚迪&#xff0c;还是没有…

go-zero系列-限流(并发控制)

参考文章&#xff1a;https://go-zero.dev/docs/tutorials/service/governance/limiter 步骤&#xff1a; 1、压测工具hey下载安装&#xff1a; go install github.com/rakyll/hey (会安装到GOPATH/bin目录下)2、在yaml配置文件中加入&#xff1a; #最大连接数 MaxConns: 10…

数字化时代:虚拟数字人的智能进化与生活变革

我们需要实现对人工智能的有效监管。政府应该与科技公司合作&#xff0c;建立监管框架&#xff0c;确保人工智能的发展能够在保护人类利益的基础上进行。人工智能的快速发展带来了巨大的机遇&#xff0c;但同时也伴随着一些潜在的风险。如果没有适当的监管措施&#xff0c;人工…

【华为OD机试】完全二叉树非叶子部分后序遍历【2023 B卷|200分】

【华为OD机试】-真题 !!点这里!! 【华为OD机试】真题考点分类 !!点这里 !! 题目描述: 给定一个以顺序储存结构存储整数值的完全二叉树序列(最多1000个整数), 请找出此完全二叉树的所有非叶子节点部分,然后采用后序遍历方式将此部分树(不包含叶子)输出。 1、只有一个…

[RocketMQ] Broker 消息刷盘服务源码解析 (十二)

同步刷盘: 在消息真正持久化至磁盘后RocketMQ的Broker端才会真正返回给Producer端一个成功的ACK响应。异常刷盘: 能够充分利用OS的PageCache的优势, 只要消息写入PageCache即可将成功的ACK返回给Producer端。消息刷盘采用后台异步线程提交的方式进行, 降低了读写延迟和提高了MQ…

LT15 取消TO 显示 转储请求项目 WH XXX 0001 不存在

LT15取消TO的时候&#xff0c;收到的错误收件箱信息 这个实际就是一个BUG&#xff0c;目前是无解的。 最后SAP官方使用debug处理掉的&#xff08;如果自己艺高人胆大的话也可以处理&#xff09; 以下为官方处理后的回复&#xff1a; the document has been cancelled. In L…

cobaltStrike克隆网站

试验目的 使用CS克隆网站获取键盘试验实验准备 kali cs实验步骤 启动cs服务端 sudo su切换root权限 进入cs目录 使用chmod 获取目录内最高权限 ./teamserver 192.168.24.131 12456 启动服务端[./teamserverip设置密码] 启动客户端登录并建立监听 打开新终端 切换进入cs目录 .…

百万网友AI导师李沐离职投身大模型,B站“组会”还会有吗?(文末赠书)

目录 1 求学之路&#xff1a;全能学霸2 AI之路&#xff1a;与深度学习结缘3 一战封神&#xff1a;亚马逊首席科学家4 动手学习深度学习 前阵子“沐神”李沐离开亚马逊、加入创业公司BosonAI的消息&#xff0c;引起了业内比较广泛的讨论。 而BosonAI的创始人正好是他的博士生导师…

基于Springboot的漫画网站(包论文)

原理和技术有: B/S、java技术和MySQL数据库原理和技术有: B/S、java技术和MySQL数据库原理和技术有: B/S、java技术和MySQL数据库 困扰管理层的许多问题当中,漫画信息管理一定是不敢忽视的一块。但是管理好漫画网站又面临很多麻烦需要解决,如何在工作琐碎,记录繁多的情况下将漫…

Layui之动态选项卡iframe使用(附源码)

目录 一、前言 1.什么是Tab选项卡 2.什么是iframe标签 3.使用iframe标签 二、案例实现 1.需求分析 ①在线Layui示例寻找合适的选项卡 ②点击左侧右侧没有url属性 ③点击左侧列表右侧内容多开问题 ④优化公共文件 2.Dao层的优化 3.JSP页面搭建 4.案例演示 5.总结 …

DevOps平台-图形化流水线调研总结

DevOps平台是企业级持续集成和持续交付工具&#xff0c;通过构建自动化、集成自动化、验证自动化、部署自动化&#xff0c;完成从开发到上线CICD过程。通过持续向团队提供及时反馈&#xff0c;让交付过程高效顺畅。 对以下产品做了一些调研:

win+shift+s截屏失效问题解决

windows自带截图键(shift win s)不知道为何一开始能用,过段时间便失灵了,每次都得重启电脑才能恢复?以下是无需重启电脑的恢复方式: 方式一 、 Fnwin 如果上面的没用 方式二、打开任务管理器 选择Windows资源管理器重启 ctrl alt . 或者 任务栏右键选择任务管理器

Moonbeam赞助波卡黑客松亚洲区,促进互连合约应用发展

波卡黑客松亚洲区于7月3日开跑&#xff01;Moonbeam将在本次黑客松提供两个赛题&#xff0c;促进Connected Contract应用发展&#xff0c;优选者将于9月6日在首尔进行发表。 波卡黑客松亚洲区于7月3日正式开启&#xff0c;由波卡生态系统中的众协议共同举办&#xff0c;一同召…