“深入探讨Java JUC中的ReentrantLock锁:实现多线程同步与并发控制“

news2024/10/2 10:34:52

简介

1、从Java5开始,Java提供了一种功能更强大的线程同步机制——通过显式定义同步锁对象来实现同步,在这种机制下,同步锁由Lock对象充当。

2、Lock 提供了比synchronized方法和synchronized代码块更广泛的锁定操作,Lock允许实现更灵活的结构,可以具有差别很大的属性,并且支持多个相关的Condition对象。

3、Lock是控制多个线程对共享资源进行访问的工具。通常,锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。

4、某些锁可能允许对共享资源并发访问,如ReadWriteLock(读写锁),Lock、ReadWriteLock是Java5提供的两个根接口,并为Lock 提供了ReentrantLock(可重入锁)实现类,为ReadWriteLock提供了ReentrantReadWriteLock 实现类。

5、Java8新增了新型的StampedLock类,在大多数场景中它可以替代传统的ReentrantReadWriteLock。ReentrantReadWriteLock 为读写操作提供了三种锁模式:Writing、ReadingOptimistic、Reading。
在这里插入图片描述

ReentrantLock

什么是ReentrantLock

ReentrantLock 是 Java 中的一种锁实现,它提供了与传统的 synchronized 关键字相似的功能,但具有更多的灵活性和控制能力。

ReentrantLock特性

可重入性: 与 synchronized 一样,ReentrantLock 具有可重入性,这意味着线程可以多次获取同一个锁而不会出现死锁。

锁的公平性: ReentrantLock 支持公平锁和非公平锁。在公平锁模式下,锁将按照线程请求的顺序分配。在非公平锁模式下,锁将在可用时立即分配给等待线程。

Condition 对象: ReentrantLock 提供了 Condition 对象,它允许线程在特定条件下等待和通知其他线程。这对于线程间的协作非常有用。

中断响应: ReentrantLock 支持中断响应,这意味着线程可以在等待锁的过程中响应中断信号。

超时锁定: ReentrantLock 允许您尝试获取锁,并设置一个超时时间。如果在超时时间内无法获取锁,线程可以执行其他操作。

lock 锁和synchronized 对比

可重入性:
ReentrantLock 具有可重入性,允许同一线程多次获取同一个锁而不会引发死锁。
synchronized 也是可重入的,同一线程可以多次获得同一个锁。
灵活性:
ReentrantLock 提供了更多的灵活性和控制,允许你选择公平性和非公平性、设置超时、使用读写锁等高级功能。
synchronized 相对较简单,提供的功能较少,不支持超时、读写锁等高级功能。
条件等待:
ReentrantLock 提供了 Condition 对象,允许线程在特定条件下等待,然后在条件满足时重新获取锁。
synchronized 缺少这种直接的条件等待机制,但可以使用 wait() 和 notify() 方法实现类似的功能。
公平性:
ReentrantLock 允许你选择锁的公平性,以公平或非公平方式分配锁。在公平模式下,锁将按照等待顺序分配给等待的线程。
synchronized 使用的是非公平锁,不保证按等待顺序分配。
性能:
synchronized 在某些情况下可能比 ReentrantLock 更高效,因为它是 JVM 内置的一种机制。
ReentrantLock 在高竞争情况下可以提供更好的性能,但它的创建和维护成本通常更高。
异常处理:
ReentrantLock 具有灵活的异常处理机制,可以捕获并处理锁操作中的异常。
synchronized 的异常处理相对较简单,一旦发生异常,锁将自动释放。
可中断性:
ReentrantLock 允许线程响应中断,可以在等待锁时中断线程。
synchronized 不支持线程中断。
锁的可绑定性:
ReentrantLock 允许将锁绑定到多个条件。
synchronized 不提供类似的绑定条件的机制。

使用案例

在这个示例中,我们创建了一个ReentrantLock实例,并使用它来保护SharedResource对象中的doWork方法。两个线程(“Thread 1"和"Thread 2”)共享SharedResource对象,并分别调用doWork方法。lock.lock()获取锁,lock.unlock()释放锁,确保在同一时刻只有一个线程可以进入doWork方法的同步块。这确保了线程之间的安全性和同步执行。

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LockExample {
    public static void main(String[] args) {
        // 创建一个ReentrantLock实例
        Lock lock = new ReentrantLock();

        // 创建一个共享资源
        SharedResource resource = new SharedResource(lock);

        // 创建多个线程并启动
        Thread thread1 = new Thread(new Worker(resource), "Thread 1");
        Thread thread2 = new Thread(new Worker(resource), "Thread 2");

        thread1.start();
        thread2.start();
    }
}

class SharedResource {
    private Lock lock;

    public SharedResource(Lock lock) {
        this.lock = lock;
    }

    public void doWork() {
        lock.lock(); // 获取锁
        try {
            // 同步的代码块
            for (int i = 1; i <= 5; i++) {
                System.out.println(Thread.currentThread().getName() + " is working: " + i);
            }
        } finally {
            lock.unlock(); // 释放锁
        }
    }
}

class Worker implements Runnable {
    private SharedResource resource;

    public Worker(SharedResource resource) {
        this.resource = resource;
    }

    @Override
    public void run() {
        resource.doWork();
    }
}

AQS回顾

AQS即AbstractQueuedSynchronizer的缩写,这个是个内部实现了两个队列的抽象类,分别是同步队列和条件队列。其中同步队列是一个双向链表,里面储存的是处于等待状态的线程,正在排队等待唤醒去获取锁,而条件队列是一个单向链表,里面储存的也是处于等待状态的线程,只不过这些线程唤醒的结果是加入到了同步队列的队尾,AQS所做的就是管理这两个队列里面线程之间的等待状态-唤醒的工作。
在同步队列中,还存在2中模式,分别是独占模式和共享模式,这两种模式的区别就在于AQS在唤醒线程节点的时候是不是传递唤醒,这两种模式分别对应独占锁和共享锁。
AQS是一个抽象类,所以不能直接实例化,当我们需要实现一个自定义锁的时候可以去继承AQS然后重写获取锁的方式和释放锁的方式还有管理state,而ReentrantLock就是通过重写了AQS的tryAcquire和tryRelease方法实现的lock和unlock。
详情可以参考 https://juejin.cn/post/7006895386103119908

ReentrantLock实现原理

请添加图片描述

ReentrantLock结构

在这里插入图片描述
ReentrantLock实现Lock接口 有三个内部类 分别是 Sync、NonfairSync、FairSync,其中Sync内部类继承自AQS承接了AQS的功能,NonfairSync代表非公平锁、FairSync 代表公平锁 他们都是继承了Sync类,通过Sync重写的方法tryAcquire、tryRelease可以知道,ReentrantLock实现的是AQS的独占模式,也就是独占锁,这个锁是悲观锁。

非公平锁实现原理

获取锁

请添加图片描述
ReentrantLock有两个构造方法,无参构造方法默认是创建非公平锁,fair传false 也是非公平锁
默认非公平锁 所以子类NonfairSync 实现父类的抽象方法执行 lock
1.先用case 尝试去更新state的值
如果能更新成功就表示可以抢占到锁 把state更新成1 并设置线程信息执行结束
如果更新失败即此时state不等于0代表此时锁被其他线程占据着则执行acquire方法
2.nonfairTryAcquire 首先会获取state的值 判断state是否等于0 如果此时等于0 则代表有线程释放锁了,并且把state改回了0 ,
如果此时state 等于0 就再次尝试用cas 去将state的值由0变更成1 如果变更成功就代表抢占到了锁 然后设置一下线程信息(这里就体现了非公平锁的特性 不会在意阻塞队列中是否有等待的线程)然后结束
如果此时state不等于0 或者 cas 更新 state值失败则代表有线程占据着锁 此时会去判断当前线程是否是获得锁的线程 如果是获得锁的线程则代表是重入的则将state进行+1 然后执行结束
3.如果这个当前线程不是获得锁的线程,则会构建一个Node节点 然后由尾部放到阻塞队列中 park住

    public ReentrantLock() {
        sync = new NonfairSync();
    }

    /**
     * Creates an instance of {@code ReentrantLock} with the
     * given fairness policy.
     *
     * @param fair {@code true} if this lock should use a fair ordering policy
     */
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
  final void lock() {
           //cas 原子操作修改state 的值 如果能修改成功则把0变成1 然后记录当前线程id
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                //抢占锁逻辑
                acquire(1);
        }
  public final void acquire(int arg) {
         //尝试获取独占锁 
        if (!tryAcquire(arg) &&
            //如果失败则假如aqs 队列中
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
       final boolean nonfairTryAcquire(int acquires) {
            //获取当前线程
            final Thread current = Thread.currentThread();//拿到 State 值    
            int c = getState();
            //如果是0 表示可以去获得锁
            if (c == 0) {
               //cas 原子操作修改state 的值 如果能修改成功则把0变成1 然后记录当前线程id
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            //如果当前线程就是获得锁的线程
            else if (current == getExclusiveOwnerThread()) {
                //增加重入次数
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
 private Node addWaiter(Node mode) {
        //构建一个node
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        // tail = 尾节点 默认是null
        Node pred = tail;
        if (pred != null) {
            //如果尾节点不等于空 把当前节点当成尾节点 然后把prev指针指向上一个节点 把新进来的节点改成尾节点
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                 //把上一个节点的next 指针指向刚进来的节点
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }
 private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                 //如果尾节点 = = null 用cas 构建一个节点 
                if (compareAndSetHead(new Node()))
                    //把头节点赋值给尾节点
                    tail = head;
            } else {
                //如果尾节点不等于空 把当前节点当成尾节点 然后把prev指针指向上一个节点 把新进来的节点改成尾节点
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    //把上一个节点的next 指针指向刚进来的节点
                    t.next = node;
                    return t;
                }
            }
        }
    }
 final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                //获取当前节点的前一个节点
                final Node p = node.predecessor();
                //如果当前节点的前一个结点是头节点 则说明有资格去争夺锁
                if (p == head && tryAcquire(arg)) {
                    //把当前节点设置成头节点
                    setHead(node);
                    
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
// 当获取(资源)失败后,检查并且更新结点状态
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    // 获取前驱结点的状态
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL) // 状态为SIGNAL,为-1
        // 可以进行park操作
        return true; 
    if (ws > 0) { // 表示状态为CANCELLED,为1
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0); // 找到pred结点前面最近的一个状态不为CANCELLED的结点
        // 赋值pred结点的next域
        pred.next = node; 
    } else { // 为PROPAGATE -3 或者是0 表示无状态,(为CONDITION -2时,表示此节点在condition queue中) 
        // 比较并设置前驱结点的状态为SIGNAL
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL); 
    }
    // 不能进行park操作
    return false;
}
 private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

释放锁

请添加图片描述
1.判断当前线程是不是锁的所有者,如果是则进行步骤2,如果不是则抛出异常。
2.判断此次释放锁后state的值是否为0,如果是则代表锁有没有重入,然后将锁的所有者设置成null且返回true,然后执行步骤3,如果不是则代表锁发生了重入执行步骤4。
3.现在锁已经释放完,即state=0,唤醒同步队列中的后继节点进行锁的获取。
4.锁还没有释放完,即state!=0,不唤醒同步队列。

 public void unlock() {
        sync.release(1);
    }
   public final boolean release(int arg) {
        //释放锁成功
        if (tryRelease(arg)) {
            Node h = head;
            //如果头节点不为空 并且状态不为0 
            if (h != null && h.waitStatus != 0)
                //唤醒
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
 protected final boolean tryRelease(int releases) {
            //state -1
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                 //如果c =0 表示当前是无锁状态 把线程iq清空
                free = true;
                setExclusiveOwnerThread(null);
            }
            //重新设置 state
            setState(c);
            return free;
        }
private void unparkSuccessor(Node node) {
        /*
         * If status is negative (i.e., possibly needing signal) try
         * to clear in anticipation of signalling.  It is OK if this
         * fails or if status is changed by waiting thread.
         */
        int ws = node.waitStatus;
        if (ws < 0)
            //设置head节点的状态为0 
            compareAndSetWaitStatus(node, ws, 0);

        /*
         * Thread to unpark is held in successor, which is normally
         * just the next node.  But if cancelled or apparently null,
         * traverse backwards from tail to find the actual
         * non-cancelled successor.
         */
        //拿到head节点的下一个节点
        Node s = node.next;
        //如果下一个节点为null 或者 status>0则表示是 CANCELLED 状态
        //听过尾部节点开始扫描  找到距离 head最近的一个 waitStatus<=0的节点
        if (s == null || s.waitStatus > 0) {
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        //如果next 节点不等于空直接唤醒这个线程
        if (s != null)
            LockSupport.unpark(s.thread);
    }

公平锁实现原理

请添加图片描述
1.获取状态的state的值,如果state=0即代表锁没有被其它线程占用(但是并不代表同步队列没有线程在等待),执行步骤2。如果state!=0则代表锁正在被其它线程占用,执行步骤3。
2.判断同步队列是否存在线程(节点),如果不存在则直接将锁的所有者设置成当前线程,且更新状态state,然后返回true。
3.判断锁的所有者是不是当前线程,如果是则更新状态state的值,然后返回true,如果不是,那么返回false,即线程会被加入到同步队列中

final void lock() {
    acquire(1);
}

public final void acquire(int arg) {
    //同步队列中有线程 且 锁的所有者不是当前线程那么将线程加入到同步队列的尾部,
    //保证了公平性,也就是先来的线程先获得锁,后来的不能抢先获取。
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    //判断状态state是否等于0,等于0代表锁没有被占用,不等于0则代表锁被占用着。
    if (c == 0) {
        //调用hasQueuedPredecessors方法判断同步队列中是否有线程在等待,如果同步队列中没有
        //线程在等待 则当前线程成为锁的所有者,如果同步队列中有线程在等待,则继续往下执行
        //这个机制就是公平锁的机制,也就是先让先来的线程获取锁,后来的不能抢先获取。
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    //判断当前线程是否为锁的所有者,如果是,那么直接更新状态state,然后返回true。
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    //如果同步队列中有线程存在 且 锁的所有者不是当前线程,则返回false。
    return false;
}

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

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

相关文章

YOLOv8优化:独家创新(Partial_C_Detect)检测头结构创新,实现涨点 | 检测头新颖创新系列

💡💡💡本文独家改进:独家创新(Partial_C_Detect)检测头结构创新,适合科研创新度十足,强烈推荐 Partial_C_Detect | 亲测在多个数据集能够实现大幅涨点 💡💡💡Yolov8魔术师,独家首发创新(原创),适用于Yolov5、Yolov7、Yolov8等各个Yolo系列,专栏文章提…

ES 8.x 向量检索性能测试 把向量检索性能提升100倍!

向量检索不仅在的跨模态检索场景中应用广泛&#xff0c;随着chat gpt的或者&#xff0c;利用es的向量检索&#xff0c;在Ai领域发挥着越来越大的作用。 本文&#xff0c;主要测试es的向量检索性能。我从8.x就开始关注ES的向量检索了。当前ES已经发布到 8.10 版本。以下是官方文…

微信小程序商城制作教程

在这个数字化快速发展的时代&#xff0c;微信小程序商城已经成为越来越多商家的首选。搭建微信小程序商城不仅能够拓宽销售渠道&#xff0c;还能增加品牌曝光度和用户粘性。本文将指导你如何搭建微信小程序商城&#xff0c;从登录乔拓云网后台到发布上线全流程介绍。 1. 登录乔…

【0229】libpq库实现压测PG服务器max_connections的最大连接数

1. PG服务器接收的最大连接数 在PG服务的postgresql.conf配置文件中,参数:max_connections 注明了PG服务所能够接受的最大客户端的连接数量。此值默认是100,那么PG服务此参数最大能够调到多大呢? 本文将采用libpq库编写demo来进行压测,并将最终的结论和数据于文章中给出。…

Redis过期处理策略、惰性删除、定期删除、RDB和AOF、内存淘汰机制

文章目录 &#x1f34a; Redis过期策略&#x1f389; 惰性删除流程&#x1f389; 定期删除流程 &#x1f389; 内存淘汰机制&#x1f389; RDB对过期key的处理&#x1f389; AOF对过期key的处理 &#x1f4d5;我是廖志伟&#xff0c;一名Java开发工程师、Java领域优质创作者、C…

领导:给你一个项目,如何开展性能测试工作。我:***

01 怎么开展性能测试 01 测试的一般步骤 性能测试的工作是基于系统功能已经完备或者已经趋于完备之上的&#xff0c;在功能还不够完备的情况下没有多大的意义&#xff08;后期功能完善上会对系统的性能有影响&#xff0c;过早进入性能测试会出现测试结果不准确、浪费测试资源…

react项目实现文件预览,比如PDF、txt、word、Excel、ppt等常见文件(腾讯云cos)

使用腾讯云文档预览&#xff0c;需要开通文档预览功能&#xff0c;该功能需要收费的。 使用限制 如果需要图片预览、视频或音频可以使用获取下载链接。 页面代码 <button onClick() > {handleClick(myself/文档.xlsx)}>预览</button><div style{{ height:…

百度AI代码辅助工具Baidu Comate初体验

国内的程序员们也可以使用国产的代码辅助工具了。百度Comate智能代码助手正式上线SaaS版本&#xff0c;即日起可申请试用。官网&#xff08;https://comate.baidu.com/&#xff09;。 点开使用手册&#xff0c;先瞜一眼&#xff0c;后面再细看&#xff0c;手册里面有详细的说明…

yum管理openssh服务

文章目录 yum工具使用1.yum的配置文件2.yum仓库的管理本地仓库网络仓库 3.使用yum管理软件listcleanrepolistinstallupdatedowngraderemoveinfoprovideshistorydeplistgrouplistgroupinstall opensshsshd服务Secure Shell 示例ssh主机密钥配置免密登录SSH 安全注意事项 yum工具…

【文件操作】Java -操作File对象

个人简介&#xff1a;Java领域新星创作者&#xff1b;阿里云技术博主、星级博主、专家博主&#xff1b;正在Java学习的路上摸爬滚打&#xff0c;记录学习的过程~ 个人主页&#xff1a;.29.的博客 学习社区&#xff1a;进去逛一逛~ 文件操作 Java - File对象 Java - File对象 Fi…

Matlab绘制散点的95%置信区间图

Matlab常绘制95%置信区间图&#xff0c;主要使用到patch函数。 如果直接使用散点进行拟合&#xff0c;在patch函数绘制95%置信区间时&#xff0c;会绘制的很乱&#xff0c;这个是由于patch函数所导致的&#xff0c;其实这个问题在 Matlab绘制95%置信区间图 中已经讲到过&#…

JS 严格模式和正常模式详解

JavaScript 严格模式&#xff08;Strict Mode&#xff09;和正常模式&#xff08;Non-Strict Mode&#xff09;是用于控制 JavaScript 代码执行的两种不同模式。严格模式引入了一些限制和变化&#xff0c;旨在帮助开发人员编写更可靠、安全和高效的代码。在本文中&#xff0c;我…

js实现将文本生成二维码(腾讯云cos)

示例 页面代码 import { getQCodeUrl } from /utils/cosInstance; import { PageContainer } from ant-design/pro-components; import { Access, useAccess } from umijs/max; import { Button, Image } from antd; import { useState } from react;const AccessPage: Reac…

SpringCloud复习:(2)@LoadBalanced注解的工作原理

LoadBalanced注解标记了一个RestTemplate或WebClient bean使用LoadBalancerClient来进行负载均衡。 LoadBalancerAutoConfiguration类给带注解的RestTemplate添加了拦截器&#xff1a;LoadBalancerInterceptor. 具体流程如下&#xff1a; 首先定义一个LoadBalancerInterceptor…

超强满血不收费的AI绘图教程来了(在线Stable Diffusion一键即用)

超强满血不收费的AI绘图教程来了&#xff08;在线Stable Diffusion一键即用&#xff09; 一、简介1.1 AI绘图1.2 Stable Diffusion1.2.1 原理简述1.2.2 应用流程 二、AI绘图工具2.1 吐司TusiArt2.2 哩布哩布LibLibAI2.3 原生部署 三、一键即用3.1 开箱尝鲜3.2 模型关联3.3 Cont…

小白必看,手把手教你重装系统

一&#xff0c;安装步骤 二&#xff0c;重装之前需要做的准备 1、重装之前请大家务必注意备份重要资料。电脑有价&#xff0c;数据无价——重要数据备份之后&#xff0c;电脑随便折腾都没问题。大不了就是重装不成功。系统软件问题多试几次总能解决的&#xff0c;但重要数据一…

《红蓝攻防对抗实战》七.常规反弹之利用NC在Linux系统执行反弹shell

目录 一.利用NC工具在linux执行反弹shell 1.Linux正向连接shell 2.利用Linux自带bash反弹Shell 前文推荐&#xff1a; 《红蓝攻防对抗实战》一. 隧道穿透技术详解《红蓝攻防对抗实战》二.内网探测协议出网之TCP/UDP协议探测出网《红蓝攻防对抗实战》三.内网探测协议出网之H…

最新壁纸自动采集系统网站PHP源码/360壁纸官方数据接口采集/ZHEYI采集源码

源码介绍&#xff1a; 最新壁纸自动采集系统网站PHP源码&#xff0c;它是ZHEYI自动采集源码&#xff0c;能够在360壁纸官方数据接口采集。很好用的壁纸网站源码分享&#xff0c;仅供学习&#xff0c;请勿商用。 ZHEYI自动采集壁纸PHP源码&#xff0c;能全自动采集高清壁纸网源…

设计模式—设计模式总览

设计模式—设计模式总览 在 1994 年&#xff0c;由 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 四人合著出版了一本名为 《Design Patterns - Elements of Reusable Object-Oriented Software》&#xff08;中文译名&#xff1a;《设计模式 - 可复用的面向对…

Wish如何入驻?最全防封攻略

近日来&#xff0c;跨境电商平台Wish有了新改革&#xff0c;为提高产品质量把控效率&#xff0c;Wish最近将入驻机制又完全开放转变为“邀请制”&#xff0c;加强了品控措施&#xff0c;也意味着商家入驻门槛变高&#xff0c;流程与之前截然不同。但对于已有跨境电商经验/没有跨…