java lock可重入_Java源码解析之可重入锁ReentrantLock

本文基于jdk1.8进行分析。

ReentrantLock是一个可重入锁,在ConcurrentHashMap中使用了ReentrantLock。

首先看一下源码中对ReentrantLock的介绍。如下图。ReentrantLock是一个可重入的排他锁,它和synchronized的方法和代码有着相同的行为和语义,但有更多的功能。ReentrantLock是被最后一个成功lock锁并且还没有unlock的线程拥有着。如果锁没有被别的线程拥有,那么一个线程调用lock方法,就会成功获取锁并返回。如果当前线程已经拥有该锁,那么lock方法会立刻返回。这个可以通过isHeldByCurrentThread方法和getHoldCount方法进行验证。除了这部分介绍外,类前面的javadoc文档很长,就不在这里全部展开。随着后面介绍源码,会一一涉及到。

/**

* A reentrant mutual exclusion {@link Lock} with the same basic

* behavior and semantics as the implicit monitor lock accessed using

* {@code synchronized} methods and statements, but with extended

* capabilities.

*

A {@code ReentrantLock} is owned by the thread last

* successfully locking, but not yet unlocking it. A thread invoking

* {@code lock} will return, successfully acquiring the lock, when

* the lock is not owned by another thread. The method will return

* immediately if the current thread already owns the lock. This can

* be checked using methods {@link #isHeldByCurrentThread}, and {@link

* #getHoldCount}.

首先看一下成员变量,如下图。ReentrantLock只有一个成员变量sync,即同步器,这个同步器提供所有的机制。Sync是AbstractQueuedSynchronizer的子类,同时,Sync有2个子类,NonfairSync和FairSync,分别是非公平锁和公平锁。Sync,NonfaireSync和FairSync的具体实现后面再讲。

/** Synchronizer providing all implementation mechanics **/

private final Sync sync;

下面看一下构造函数。如下图。可以看到,ReentrantLock默认是非公平锁,它可以通过参数,指定初始化为公平锁或非公平锁。

/**

* Creates an instance of {@code ReentrantLock}.

* This is equivalent to using {@code ReentrantLock(false)}.

**/

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();

}

下面看一下ReentrantLock的主要方法。首先是lock方法。如下图。lock方法的实现很简单,就是调用Sync的lock方法。而Sync的lock方法是个抽象的,具体实现在NonfairSync和FairSync中。这里我们先不展开讲,而是先读一下lock方法的注释,看看它的作用。lock方法的作用是获取该锁。分为3种情况。

1,如果锁没有被别的线程占有,那么当前线程就可以获取到锁并立刻返回,并把锁计数设置为1。

2,如果当前线程已经占有该锁了,那么就会把锁计数加1,立刻返回。

3,如果锁被另一个线程占有了,那么当前线程就无法再被线程调度,并且开始睡眠,直到获取到锁,在获取到到锁时,会把锁计数设置为1。

lockInterruptibly方法与lock功能类似,但lockInterruptibly方法在等待的过程中,可以响应中断。

/**

* Acquires the lock.

*

Acquires the lock if it is not held by another thread and returns

* immediately, setting the lock hold count to one.

*

If the current thread already holds the lock then the hold

* count is incremented by one and the method returns immediately.

*

If the lock is held by another thread then the

* current thread becomes disabled for thread scheduling

* purposes and lies dormant until the lock has been acquired,

* at which time the lock hold count is set to one.

**/

public void lock() {

sync.lock();

}

public void lockInterruptibly() throws InterruptedException {

sync.acquireInterruptibly(1);

}

下面,详细看一下非公平锁和公平锁中对lock函数的实现。如下图。下图同时列出了公平锁和非公平锁中lock的实现逻辑。从注释和代码逻辑中,都可以看出,非公平锁进行lock时,先尝试立刻闯入(抢占),如果成功,则获取到锁,如果失败,再执行通常的获取锁的行为,即acquire(1)。

/**

* 非公平锁中的lock

* Performs lock. Try immediate barge, backing up to normal

* acquire on failure.

**/

final void lock() {

if (compareAndSetState(0, 1))

setExclusiveOwnerThread(Thread.currentThread());

else

acquire(1);

}

//公平锁中的lock

final void lock() {

acquire(1);

}

那么,我们首先了解下,非公平锁“尝试立刻闯入”,究竟做了什么。稍后再继续讲解通常的获取锁的行为。下图是立即闯入行为compareAndSetState(0, 1)的实现。从compareAndSetState函数的注释中,可以知道,如果同步状态值与期望值相等,那么就把它的值设置为updated值。否则同步状态值与期望值不相等,则返回false。这个操作和volatile有着相同的内存语义,也就是说,这个操作对其他线程是可见的。compareAndSetState函数注释里描述的功能,是通过unsafe.compareAndSwapInt方法实现的,而unsafe.compareAndSwapInt是一个native方法,是用c++实现的。那么继续追问,c++底层是怎么实现的?C++底层是通过CAS指令来实现的。什么是CAS指令呢?来自维基百科的解释是,CAS,比较和交换,Compare and Swap,是用用于实现多线程原子同步的指令。它将内存位置的内容和给定值比较,只有在相同的情况下,将该内存的值设置为新的给定值。这个操作是原子操作。那么继续追问,CAS指令的原子性,是如何实现的呢?我们都知道指令时CPU来执行的,在多CPU系统中,内存是共享的,内存和多个cpu都挂在总线上,当一个CPU执行CAS指令时,它会先将总线LOCK位点设置为高电平。如果别的CPU也要执行CAS执行,它会发现总线LOCK位点已经是高电平了,则无法执行CAS执行。CPU通过LOCK保证了指令的原子执行。

现在来看一下非公平锁的lock行为,compareAndSetState(0, 1),它期望锁状态为0,即没有别的线程占用,并把新状态设置为1,即标记为占用状态。如果成功,则非公平锁成功抢到锁,之后setExclusiveOwnerThread,把自己设置为排他线程。非公平锁这小子太坏了。如果抢占失败,则执行与公平锁相同的操作。

/**

* Atomically sets synchronization state to the given updated

* value if the current state value equals the expected value.

* This operation has memory semantics of a {@code volatile} read

* and write.

* @param expect the expected value

* @param update the new value

* @return {@code true} if successful. False return indicates that the actual

* value was not equal to the expected value.

**/

protected final boolean compareAndSetState(int expect, int update) {

// See below for intrinsics setup to support this

return unsafe.compareAndSwapInt(this, stateOffset, expect, update);

}

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

下面看一下公平锁获取锁时的行为。如下图。这部分的逻辑有些多,请阅读代码中的注释进行理解。

/**

* 公平锁的lock

**/

final void lock() {

acquire(1);

}

/**

* Acquires in exclusive mode, ignoring interrupts. Implemented

* by invoking at least once {@link #tryAcquire},

* returning on success. Otherwise the thread is queued, possibly

* repeatedly blocking and unblocking, invoking {@link

* #tryAcquire} until success. This method can be used

* to implement method {@link Lock#lock}.

* @param arg the acquire argument. This value is conveyed to

* {@link #tryAcquire} but is otherwise uninterpreted and

* can represent anything you like.

**/

public final void acquire(int arg) {

/**

* acquire首先进行tryAcquire()操作。如果tryAcquire()成功时则获取到锁,即刻返回。

* 如果tryAcquire()false时,会执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg)

* 操作。如果acquireQueued(addWaiter(Node.EXCLUSIVE), arg)true时,则当前线程中断自己。

* 如果acquireQueued(addWaiter(Node.EXCLUSIVE), arg)false,则返回。

* 其中tryAcquire()操作在NonfairSync中和FairSync中实现又有所区别。

**/

if (!tryAcquire(arg) &&

acquireQueued(addWaiter(Node.EXCLUSIVE), arg))

selfInterrupt();

}

/**

* NonfairSync中的tryAcquire。

* @param acquires

* @return

**/

protected final boolean tryAcquire(int acquires) {

return nonfairTryAcquire(acquires);

}

/**

* Performs non-fair tryLock. tryAcquire is implemented in

* subclasses, but both need nonfair try for trylock method.

**/

final boolean nonfairTryAcquire(int acquires) {

final Thread current = Thread.currentThread();

//首先获取当前同步状态值

int c = getState();

if (c == 0) {

//c为0,表示目前没有线程占用锁。没有线程占用锁时,当前线程尝试抢锁,如果抢锁成功,则返回true。

if (compareAndSetState(0, acquires)) {

setExclusiveOwnerThread(current);

return true;

}

}

else if (current == getExclusiveOwnerThread()) {

//c不等于0时表示锁被线程占用。如果是当前线程占用了,则将锁计数加上acquires,并返回true。

int nextc = c + acquires;

if (nextc < 0) // overflow

throw new Error("Maximum lock count exceeded");

setState(nextc);

return true;

}

//以上情况都不是时,返回false,表示非公平抢锁失败。

return false;

}

/**

* Fair version of tryAcquire. Don't grant access unless

* recursive call or no waiters or is first.

* 这个是公平版本的tryAcquire

**/

protected final boolean tryAcquire(int acquires) {

final Thread current = Thread.currentThread();

int c = getState();

if (c == 0) {

//c=0时表示锁未被占用。这里是先判断队列中前面是否有别的线程。没有别的线程时,才进行CAS操作。

//公平锁之所以公平,正是因为这里。它发现锁未被占用时,首先判断等待队列中是否有别的线程已经在等待了。

//而非公平锁,发现锁未被占用时,根本不管队列中的排队情况,上来就抢。

if (!hasQueuedPredecessors() &&

compareAndSetState(0, acquires)) {

setExclusiveOwnerThread(current);

return true;

}

}

else if (current == getExclusiveOwnerThread()) {

int nextc = c + acquires;

if (nextc < 0)

throw new Error("Maximum lock count exceeded");

setState(nextc);

return true;

}

return false;

}

/**

* Acquires in exclusive uninterruptible mode for thread already in

* queue. Used by condition wait methods as well as acquire.

* 当抢锁失败时,先执行addWaiter(Node.EXCLUSIVE),将当前线程加入等待队列,再执行该方法。

* 该方法的作用是中断当前线程,并进行检查,知道当前线程是队列中的第一个线程,并且抢锁成功时,

* 该方法返回。

* @param node the node

* @param arg the acquire argument

* @return {@code true} if interrupted while waiting

**/

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);

}

}

接下来是tryLock方法。代码如下。从注释中我们可以理解到,只有当调用tryLock时锁没有被别的线程占用,tryLock才会获取锁。如果锁没有被另一个线程占用,那么就获取锁,并立刻返回true,并把锁计数设置为1. 甚至在锁被设置为公平排序的情况下,若果锁可用,调用tryLock会立刻获取锁,而不管有没有别的线程在等待锁了。从这里我们总结出,不管可重入锁是公平锁还是非公平锁,tryLock方法只会是非公平的。

/**

* Acquires the lock only if it is not held by another thread at the time

* of invocation.

*

Acquires the lock if it is not held by another thread and

* returns immediately with the value {@code true}, setting the

* lock hold count to one. Even when this lock has been set to use a

* fair ordering policy, a call to {@code tryLock()} will

* immediately acquire the lock if it is available, whether or not

* other threads are currently waiting for the lock.

* This "barging" behavior can be useful in certain

* circumstances, even though it breaks fairness. If you want to honor

* the fairness setting for this lock, then use

* {@link #tryLock(long, TimeUnit) tryLock(0, TimeUnit.SECONDS) }

* which is almost equivalent (it also detects interruption).

*

If the current thread already holds this lock then the hold

* count is incremented by one and the method returns {@code true}.

*

If the lock is held by another thread then this method will return

* immediately with the value {@code false}.

* @return {@code true} if the lock was free and was acquired by the

* current thread, or the lock was already held by the current

* thread; and {@code false} otherwise

**/

public boolean tryLock() {

return sync.nonfairTryAcquire(1);

}

public boolean tryLock(long timeout, TimeUnit unit)

throws InterruptedException {

return sync.tryAcquireNanos(1, unit.toNanos(timeout));

}

接下来是释放锁的方法unlock。代码如下。unlock方式的实现,是以参数1来调用sync.release方法。而release方法是如何实现的呢?release方法首先会调用tryRelease方法,如果tryRelease成功,则唤醒后继者线程。而tryRelease的实现过程十分清晰,首先获取锁状态,锁状态减去参数(放锁次数),得到新状态。然后判断持有锁的线程是否为当前线程,如果不是当前线程,则抛出IllegalMonitorStateException。然后判断,如果新状态为0,说明放锁成功,则把持有锁的线程设置为null,并返回true。如果新状态不为0,则返回false。从tryRelease的返回值来看,它返回的true或false,指的是否成功的释放了该锁。成功的释放该锁的意思是彻底释放锁,别的线程就可以获取锁了。这里要认识到,即便tryRelease返回false,它也只是说明了锁没有完全释放,本次调用的这个释放次数值,依然是释放成功的。

/**

* Attempts to release this lock.

*

If the current thread is the holder of this lock then the hold

* count is decremented. If the hold count is now zero then the lock

* is released. If the current thread is not the holder of this

* lock then {@link IllegalMonitorStateException} is thrown.

* @throws IllegalMonitorStateException if the current thread does not

* hold this lock

**/

public void unlock() {

sync.release(1);

}

/**

* Releases in exclusive mode. Implemented by unblocking one or

* more threads if {@link #tryRelease} returns true.

* This method can be used to implement method {@link Lock#unlock}.

* @param arg the release argument. This value is conveyed to

* {@link #tryRelease} but is otherwise uninterpreted and

* can represent anything you like.

* @return the value returned from {@link #tryRelease}

**/

public final boolean release(int arg) {

if (tryRelease(arg)) {

Node h = head;

if (h != null && h.waitStatus != 0)

unparkSuccessor(h);

return true;

}

return false;

}

protected final boolean tryRelease(int releases) {

int c = getState() - releases;

if (Thread.currentThread() != getExclusiveOwnerThread())

throw new IllegalMonitorStateException();

boolean free = false;

if (c == 0) {

free = true;

setExclusiveOwnerThread(null);

}

setState(c);

return free;

}

/**

* Wakes up node's successor, if one exists.

* @param node the node

**/

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)

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.

**/

Node s = node.next;

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;

}

if (s != null)

LockSupport.unpark(s.thread);

}

接下来是newCondition方法。关于Condition这里不展开介绍,只是了解下该方法的作用。如下图。该方法返回一个和这个锁实例一起使用的Condition实例。返回的Condition实例支持和Object的监控方法例如wait-notify和notifyAll相同的用法。

1,如果没有获取锁,调用Condition的await,signal,signalAll方法的任何一个时,会抛出IllegalMonitorStateException异常。

2,调用Condition的await方法时,锁也会释放,在await返回之前,锁会被重新获取,并且锁计数会恢复到调用await方法时的值。

3,如果一个线程在等待的过程中被中断了,那么等待就会结束,并抛出InterruptedException异常,线程的中断标志位会被清理。

4,等待的线程以FIFO的顺序被唤醒。

5,从await方法返回的线程们的获取到锁的顺序,和线程最开始获取锁的顺序相同,这是未指定情况下的默认实现。但是,公平锁更钟爱那些已经等待了最长时间的线程。

/**

* Returns a {@link Condition} instance for use with this

* {@link Lock} instance.

*

The returned {@link Condition} instance supports the same

* usages as do the {@link Object} monitor methods ({@link

* Object#wait() wait}, {@link Object#notify notify}, and {@link

* Object#notifyAll notifyAll}) when used with the built-in

* monitor lock.

*

*

If this lock is not held when any of the {@link Condition}

* {@linkplain Condition#await() waiting} or {@linkplain

* Condition#signal signalling} methods are called, then an {@link

* IllegalMonitorStateException} is thrown.

*

When the condition {@linkplain Condition#await() waiting}

* methods are called the lock is released and, before they

* return, the lock is reacquired and the lock hold count restored

* to what it was when the method was called.

*

If a thread is {@linkplain Thread#interrupt interrupted}

* while waiting then the wait will terminate, an {@link

* InterruptedException} will be thrown, and the thread's

* interrupted status will be cleared.

*

Waiting threads are signalled in FIFO order.

*

The ordering of lock reacquisition for threads returning

* from waiting methods is the same as for threads initially

* acquiring the lock, which is in the default case not specified,

* but for fair locks favors those threads that have been

* waiting the longest.

*

* @return the Condition object

**/

public Condition newCondition() {

return sync.newCondition();

}

可重入锁还有一些其他的方法,这里就不一一介绍了。This is the end.

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对脚本之家的支持。如果你想了解更多相关内容请查看下面相关链接

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

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

相关文章

matlab的qammod函数_基于-MATLAB下的16QAM仿真.doc

1.课程设计目的随着现代通信技术的发展&#xff0c;特别是移动通信技术高速发展&#xff0c;频带利用率问题越来越被人们关注。在频谱资源非常有限的今天&#xff0c;传统通信系统的容量已经不能满足当前用户的要求。正交幅度调制QAM(Quadrature Amplitude Modulation)以其高频…

POJ3264 【RMQ基础题—ST-线段树】

ST算法Code&#xff1a; //#include<bits/stdc.h> #include<cstdio> #include<math.h> #include<iostream> #include<queue> #include<algorithm> #include<string.h> using namespace std; typedef long long LL;const int N5e410;…

leetcode199. 二叉树的右视图(bfs)

给定一棵二叉树&#xff0c;想象自己站在它的右侧&#xff0c;按照从顶部到底部的顺序&#xff0c;返回从右侧所能看到的节点值。示例:输入: [1,2,3,null,5,null,4] 输出: [1, 3, 4] 解释:1 <---/ \ 2 3 <---\ \5 4 <---解题思…

开发人员工作周报_如何增加找到开发人员工作的机会

开发人员工作周报In a recent job as a senior developer, I helped interview and hire many of my employer’s development team members. This is a brain dump of my advice based on those interviews.在最近担任高级开发人员的工作中&#xff0c;我帮助面试和雇用了许多…

安全专家教你如何利用Uber系统漏洞无限制的免费乘坐?

本文讲的是安全专家教你如何利用Uber系统漏洞无限制的免费乘坐&#xff1f;&#xff0c;近日&#xff0c;根据外媒报道&#xff0c;美国一名安全研究人员发现Uber上存在一处安全漏洞&#xff0c;允许发现这一漏洞的任何用户在全球范围内免费享受Uber乘车服务。据悉&#xff0c;…

flume介绍

flume 1.flume是什么 Flume:** Flume是Cloudera提供的一个高可用的&#xff0c;高可靠的&#xff0c;分布式的海量日志采集、传输、聚合的系统。** Flume仅仅运行在linux环境下** flume.apache.org(Documentation--Flume User Guide) Flume体系结构(Architecture)&#xff1a; …

threadx 信号量 应用_操作系统及ThreadX简介.ppt

操作系统及ThreadX简介操作系统及ThreadX简介 软件二部 2006.09 主要内容 多任务操作系统概述 ThreadX简介 关于驱动的交流 操作系统概述 什么是操作系统 管理计算机的所有资源&#xff0c;并为应用程序提供服务的最重要的系统软件 操作系统的目的 为用户编程提供简单的接口&am…

java中同步组件_Java并发编程(自定义同步组件)

并发包结构图&#xff1a;编写一个自定义同步组件来加深对同步器的理解业务要求&#xff1a;* 编写一个自定义同步组件来加深对同步器的理解。* 设计一个同步工具&#xff1a;该工具在同一时刻&#xff0c;只允许至多两个线程同时访问&#xff0c;超过两个线程的* 访问将被阻塞…

maven学习资料

maven学习资料maven学习教程&#xff1a;What、How、Whyhttp://www.flyne.org/article/167Maven 那点事儿 https://my.oschina.net/huangyong/blog/194583项目管理工具&#xff1a;Maven教程http://www.flyne.org/article/884转载于:https://www.cnblogs.com/zhao1949/p/634641…

leetcode127. 单词接龙(bfs)

给定两个单词&#xff08;beginWord 和 endWord&#xff09;和一个字典&#xff0c;找到从 beginWord 到 endWord 的最短转换序列的长度。转换需遵循如下规则&#xff1a; 每次转换只能改变一个字母。 转换过程中的中间单词必须是字典中的单词。 说明: 如果不存在这样的转换序…

算法之旅 | 快速排序法

HTML5学堂-码匠&#xff1a;前几期“算法之旅”跟大家分享了冒泡排序法和选择排序法&#xff0c;它们都属于时间复杂度为O(n^2)的“慢”排序。今天跟大家分享多种排序算法里使用较广泛&#xff0c;速度快的排序算法—— 快速排序法 [ 平均时间复杂度为O (n logn) ]。Tips 1&…

springmvd接收参数问题

问题描述&#xff1a; 好久不写博客了&#xff0c;今天遇到一个问题&#xff0c;那就是post请求时&#xff0c;参数接收不到&#xff0c;当时我很纳闷&#xff0c;看代码&#xff1a; 就是这样几个参数&#xff0c;我使用postman请求时无法获取参数&#xff1a; 报错信息&#…

figma下载_如何在Figma中创建逼真的3D对象

figma下载by Gbolahan Taoheed Fawale通过Gbolahan Taoheed Fawale 如何在Figma中创建逼真的3D对象 (How to create realistic 3D objects in Figma) Prior to using Figma, I used Adobe Illustrator for most of my designs (like logos, mockups, illustrations, and so on…

OpenGL中的二维编程——从简单的矩形开始

一、OpenGL的组成 图元函数&#xff08;primitive function&#xff09;指定要生成屏幕图像的图元。包括两种类型&#xff1a;可以在二维、三维或者四维空间进行定义的几何图元&#xff0c;如多边形&#xff1b;离散实体&#xff1b;位图。属性函数&#xff08;attribute funct…

圆与平面的接触面积_如果一个绝对的圆放在绝对的平面上,接触面是不是无限小?...

这种问题其实并不难解答&#xff1a;如果你真的能找到一个绝对的圆还有一个绝对平的平面上&#xff0c;并且保证放上去之后圆和平面不会有任何变化&#xff0c;那么接触面就可以是无限小&#xff01;如果不能&#xff0c;很抱歉&#xff0c;接触面很显然就不会是无限小&#xf…

leetocde1129. 颜色交替的最短路径(bfs)

在一个有向图中&#xff0c;节点分别标记为 0, 1, …, n-1。这个图中的每条边不是红色就是蓝色&#xff0c;且存在自环或平行边。 red_edges 中的每一个 [i, j] 对表示从节点 i 到节点 j 的红色有向边。类似地&#xff0c;blue_edges 中的每一个 [i, j] 对表示从节点 i 到节点…

第38天:运算符、字符串对象常用方法

一、运算符 一元操作符 &#xff0c; --&#xff0c; &#xff0c; - 5 -6 逻辑操作符 !&#xff0c; &&&#xff0c; || 基本运算符 , -, *, /, % 关系操作符 >, <, >, <, , , !, ! 赋值 判断 全等 条件操作符 &#xff08;三…

Redux Todos Example

此项目模板是使用Create React App构建的&#xff0c;它提供了一种简单的方法来启动React项目而无需构建配置。 使用Create-React-App构建的项目包括对ES6语法的支持&#xff0c;以及几种非官方/尚未最终形式的Javascript语法 先看效果 这个例子可以帮助你深入理解在 Redux 中 …

有效电子邮件地址大全_如何优雅有效地处理介绍电子邮件

有效电子邮件地址大全by DJ Chung由DJ Chung 如何优雅有效地处理介绍电子邮件 (How to handle intro emails gracefully and effectively) 您想帮个忙时不想忘恩负义... (You don’t want to sound ungrateful when asking for a favor…) Let me tell you the story that ins…

notability录音定位_Notability的一些使用技巧?

作为使用了一年Notability的考研狗 今天也来回答回答这个问题&#xff0c;希望可以给考研的同学一点点帮助。这个软件的优点估计大家都知道&#xff0c;我在这里就不多说了。好吧&#xff0c;还有一个原因是我比较懒&#xff01;好了不多说废话了&#xff0c;等会你们要打我了本…