JUC同步锁原理
JUC同步锁原理
1.锁的本质
1.什么是锁?
通俗来讲,锁要保证的就是原子性,就是一个代码块不允许多线程同时执行,就是锁。从生活的角度上来说,就比如你要去上厕所,当你在上厕所期间,你会把门锁上,其他人只能排队。不允许多个人同时上厕所。
2.锁的底层实现
java语言是运行在jvm之上,jvm是由C++实现的。java本身没有对应的底层锁实现,它将锁的问题抛给了C++。C++将锁的实现抛给了汇编语言,汇编语言将问题抛给操作系统。所以最后还是由操作系统提供的cmpxchg指令。通过 lock cmpxchg指令实现了cpu对于单个变量的原子操作。lock cmpxchg涉及到缓存一致性协议(MESI)与总线锁。(个人能力有限,自行了解吧)
3.什么是自旋锁
线程不停执行某一个代码块,直到满足条件或者操作重试次数,也就是我们所说的CAS。
4.如何实现一把锁
1.状态:判断当前是有锁还是无锁?
boolean state = true/false;标识有锁与无锁。但是一旦有锁冲入的情况,我们就需要引入一个新的变量去存储锁冲入的次数。所以JUC中使用int state,默认值为0代表无锁状态。上增的数值代表所冲入的次数。
2.多线程如何抢锁
通过cas实现多线程抢锁
3.抢不到锁的线程如何处理
1.自旋,继续抢锁直到抢成功
2.阻塞,线程阻塞直到有线程唤醒
3.自旋+阻塞,自旋抢锁一定次数,如果失败,线程阻塞。
4.自旋锁的优缺点
1.优点:节省线程上线文切换的时间。适用于执行步骤少且快的场景,节省cpu资源
2.缺点:占用cpu资源,消耗cpu性能
注意点:当cpu个数增加且线程数增加,可能导致自旋锁的优点退化成缺点。
2.AQS源码
Node节点
static final class Node {
/** Marker to indicate a node is waiting in shared mode */
static final Node SHARED = new Node();
/** Marker to indicate a node is waiting in exclusive mode */
static final Node EXCLUSIVE = null;
/** waitStatus value to indicate thread has cancelled */
static final int CANCELLED = 1;
/** waitStatus value to indicate successor's thread needs unparking */
static final int SIGNAL = -1;
/** waitStatus value to indicate thread is waiting on condition */
static final int CONDITION = -2;
static final int PROPAGATE = -3;
volatile int waitStatus;
volatile Node prev;
volatile Node next;
volatile Thread thread;
Node nextWaiter;
}
AbstractQueuedSynchronizer类
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
private transient volatile Node head;
/**
* Tail of the wait queue, lazily initialized. Modified only via
* method enq to add new wait node.
*/
private transient volatile Node tail;
/**
* The synchronization state.
*/
private volatile int state;//最重要的一个变量
}
ConditionObject类
public class ConditionObject implements Condition, java.io.Serializable {
private static final long serialVersionUID = 1173984872572414699L;
/** First node of condition queue. */
private transient Node firstWaiter;
/** Last node of condition queue. */
private transient Node lastWaiter;
}
accquire方法
public final void acquire(int arg) {
if (!tryAcquire(arg) &&//尝试获取锁
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))//如果获取锁失败,添加到队列中,由于ReentrantLock是独占锁所以节点必须是EXCLUSIVE类型
selfInterrupt();//添加中断标识位
}
addWaiter方法
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);//新建节点
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;//获取到尾指针
if (pred != null) {//尾指针不等于空,将当前节点替换为尾指针
node.prev = pred;
if (compareAndSetTail(pred, node)) {//采用尾插法,充分利用时间局部性和空间局部性。尾插的节点一般不容易被取消。
pred.next = node;
return node;
}
}
enq(node);//cas失败后执行入队操作,继续尝试
return node;
}
private Node enq(final Node node) {
for (;;) {
Node t = tail;//获取尾指针
if (t == null) { //代表当前队列没有节点
if (compareAndSetHead(new Node()))//将当前节点置为头结点
tail = head;
} else {//当前队列有节点
node.prev = t;//
if (compareAndSetTail(t, node)) {//将当前节点置为尾结点
t.next = node;
return t;
}
}
}
}
acquireQueued方法
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)) {//前驱节点等于头节点尝试cas抢锁。
setHead(node);//抢锁成功将当前节点设置为头节点
p.next = null; // help GC 当头结点置空
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&//当队列中有节点在等待,判断是否应该阻塞
parkAndCheckInterrupt())//阻塞等待,检查中断标识位
interrupted = true;//将中断标识位置为true
}
} finally {
if (failed)//
cancelAcquire(node);//取消当前节点
}
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;//获取上一个节点的等待状态
if (ws == Node.SIGNAL)//如果状态为SIGNAL,代表后续节点有节点可以唤醒,可以安心阻塞去
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) {//如果当前状态大于0,代表节点为CANCELLED状态
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;//从尾节点开始遍历,找到下一个状态不是CANCELLED的节点。将取消节点断链移除
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
//这里需要注意ws>0时,已经找到了一个不是取消状态的前驱节点。
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);//将找到的不是CANCELLED节点的前驱节点,将其等待状态置为SIGNAL
}
return false;
}
private void cancelAcquire(Node node) {
// Ignore if node doesn't exist
if (node == null)//当前节点为空直接返回
return;
node.thread = null;//要取消了将当前节点的线程置为空
// Skip cancelled predecessors
Node pred = node.prev;//获取到当前节点的前驱节点
while (pred.waitStatus > 0)//如果当前节点的前驱节点的状态大于0,代表是取消状态,一直找到不是取消状态的节点
node.prev = pred = pred.prev;
Node predNext = pred.next;//将当前要取消的节点断链
node.waitStatus = Node.CANCELLED;//将当前节点的等待状态置为CANCELLED
// If we are the tail, remove ourselves.
if (node == tail && compareAndSetTail(node, pred)) {//如果当前节点是尾结点,将尾结点替换为浅语节点
compareAndSetNext(pred, predNext, null);//将当前节点的下一个节点置为空,因为当前节点是最后一个节点没有next指针
} else {
// If successor needs signal, try to set pred's next-link
// so it will get one. Otherwise wake it up to propagate.
int ws;
if (pred != head &&//前驱节点不等于头结点
((ws = pred.waitStatus) == Node.SIGNAL ||//前驱节点的状态不等于SIGNAL
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&//前驱节点的状态小于0,并且cas将前驱节点的等待置为SIGNAL
pred.thread != null) {//前驱节点的线程补位空
Node next = node.next;//获取当前节点的next指针
if (next != null && next.waitStatus <= 0)//如果next指针不等于空并且等待状态小于等于0,标识节点有效
compareAndSetNext(pred, predNext, next);//将前驱节点的next指针指向下一个有效节点
} else {
unparkSuccessor(node);//唤醒后续节点 条件:1.前驱节点是头结点 2.当前节点不是signal,在ReentransLock中基本不会出现,在读写锁时就会出现
}
node.next = node; // help GC 将引用指向自身
}
}
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);//cas将当前节点置为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;//将s置为空
for (Node t = tail; t != null && t != node; t = t.prev)//从尾结点遍历找到一个不是取消状态的节点
if (t.waitStatus <= 0)
s = t;
}
if (s != null)//如果s不等于空
LockSupport.unpark(s.thread);//唤醒当前节点s
}
总结:AQS提供了统一的模板,对于如何入队出队以及线程的唤醒都由AQS提供默认的实现,只需要子类实现自己上锁和解锁的逻辑。
3.ReentrantLock
基本使用
public class ReentrantLock extends Thread {
private static ReentrantLock lock=new ReentrantLock(true); //参数为true表示为公平锁,请对比输出结果
public void run() {
for(int i=0; i<100; i++) {
lock.lock();
try{
System.out.println(Thread.currentThread().getName()+"获得锁");
}finally{
lock.unlock();
}
}
}
public static void main(String[] args) {
ReentrantLock rl=new ReentrantLock();
Thread th1=new Thread(rl);
Thread th2=new Thread(rl);
th1.start();
th2.start();
}
}
lock方法:
public void lock() {
sync.lock();
}
在AQS中对于具体的lock方法,并不做具体的实现,由子类自由拓展。ReentrantLock中lock方法中的实现分为公平锁和非公平锁的实现。所以lock方法有两个实现。
公平锁FairSync的实现
final void lock() {
acquire(1);//获取锁
}
public final void acquire(int arg) {
if (!tryAcquire(arg) &&//尝试获取锁
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))//如果获取锁失败,添加到队列中,由于ReentrantLock是独占锁所以节点必须是EXCLUSIVE类型
selfInterrupt();//添加中断标识位
}
tryAcquire方法
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();//获取到当前线程
int c = getState();//获取当前的同步状态值
if (c == 0) {//代表没有线程占有锁
if (!hasQueuedPredecessors() &&//是否有前驱节点
compareAndSetState(0, acquires)) {//如果没有前驱节点,cas将当前状态值置为acquires,也就是1,成功代表获取到锁
setExclusiveOwnerThread(current);//标识当前属于互斥状态线程的拥有者是当前线程
return true;//true,代表获取锁成功
}
}
else if (current == getExclusiveOwnerThread()) {//进入这里代表state不为0,有其他线程获得锁
int nextc = c + acquires;//锁冲入,将冲入次数加1
if (nextc < 0)//冲入次数不能少于0,少于0是非法值,抛出异常
throw new Error("Maximum lock count exceeded");
setState(nextc);//设置state状态值
return true;//true,代表获取锁成功
}
return false;//返回false,代表获取锁失败
}
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // 获取队列的尾指针
Node h = head;// 获取队列的头指针
Node s;
return h != t &&//如果头结点和尾结点不是同一个节点
((s = h.next) == null || s.thread != Thread.currentThread());//头结点的下一个节点为空或者当前头结点的线程不等于当前线程。
}
addWaiter方法
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);//新建节点
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;//获取到尾指针
if (pred != null) {//尾指针不等于空,将当前节点替换为尾指针
node.prev = pred;
if (compareAndSetTail(pred, node)) {//采用尾插法,充分利用时间局部性和空间局部性。尾插的节点一般不容易被取消。
pred.next = node;
return node;
}
}
enq(node);//cas失败后执行入队操作,继续尝试
return node;
}
private Node enq(final Node node) {
for (;;) {
Node t = tail;//获取尾指针
if (t == null) { //代表当前队列没有节点
if (compareAndSetHead(new Node()))//将当前节点置为头结点
tail = head;
} else {//当前队列有节点
node.prev = t;//
if (compareAndSetTail(t, node)) {//将当前节点置为尾结点
t.next = node;
return t;
}
}
}
}
acquireQueued方法
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)) {//前驱节点等于头节点尝试cas抢锁。
setHead(node);//抢锁成功将当前节点设置为头节点
p.next = null; // help GC 当头结点置空
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&//当队列中有节点在等待,判断是否应该阻塞
parkAndCheckInterrupt())//阻塞等待,检查中断标识位
interrupted = true;//将中断标识位置为true
}
} finally {
if (failed)//
cancelAcquire(node);//取消当前节点
}
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;//获取上一个节点的等待状态
if (ws == Node.SIGNAL)//如果状态为SIGNAL,代表后续节点有节点可以唤醒,可以安心阻塞去
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) {//如果当前状态大于0,代表节点为CANCELLED状态
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;//从尾节点开始遍历,找到下一个状态不是CANCELLED的节点。将取消节点断链移除
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
//这里需要注意ws>0时,已经找到了一个不是取消状态的前驱节点。
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);//将找到的不是CANCELLED节点的前驱节点,将其等待状态置为SIGNAL
}
return false;
}
private void cancelAcquire(Node node) {
// Ignore if node doesn't exist
if (node == null)//当前节点为空直接返回
return;
node.thread = null;//要取消了将当前节点的线程置为空
// Skip cancelled predecessors
Node pred = node.prev;//获取到当前节点的前驱节点
while (pred.waitStatus > 0)//如果当前节点的前驱节点的状态大于0,代表是取消状态,一直找到不是取消状态的节点
node.prev = pred = pred.prev;
Node predNext = pred.next;//将当前要取消的节点断链
node.waitStatus = Node.CANCELLED;//将当前节点的等待状态置为CANCELLED
// If we are the tail, remove ourselves.
if (node == tail && compareAndSetTail(node, pred)) {//如果当前节点是尾结点,将尾结点替换为浅语节点
compareAndSetNext(pred, predNext, null);//将当前节点的下一个节点置为空,因为当前节点是最后一个节点没有next指针
} else {
// If successor needs signal, try to set pred's next-link
// so it will get one. Otherwise wake it up to propagate.
int ws;
if (pred != head &&//前驱节点不等于头结点
((ws = pred.waitStatus) == Node.SIGNAL ||//前驱节点的状态不等于SIGNAL
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&//前驱节点的状态小于0,并且cas将前驱节点的等待置为SIGNAL
pred.thread != null) {//前驱节点的线程补位空
Node next = node.next;//获取当前节点的next指针
if (next != null && next.waitStatus <= 0)//如果next指针不等于空并且等待状态小于等于0,标识节点有效
compareAndSetNext(pred, predNext, next);//将前驱节点的next指针指向下一个有效节点
} else {
unparkSuccessor(node);//唤醒后续节点 条件:1.前驱节点是头结点 2.当前节点不是signal,在ReentransLock中基本不会出现,在读写锁时就会出现
}
node.next = node; // help GC 将引用指向自身
}
}
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);//cas将当前节点置为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;//将s置为空
for (Node t = tail; t != null && t != node; t = t.prev)//从尾结点遍历找到一个不是取消状态的节点
if (t.waitStatus <= 0)
s = t;
}
if (s != null)//如果s不等于空
LockSupport.unpark(s.thread);//唤醒当前节点s
}
非公平锁FairSync的实现
lock方法
final void lock() {
if (compareAndSetState(0, 1))//上来直接抢锁。
setExclusiveOwnerThread(Thread.currentThread());//抢锁成功,将当前互斥锁的拥有线程设置为当前线程
else
acquire(1);//cas失败去acquire
}
public final void acquire(int arg) {
if (!tryAcquire(arg) &&//尝试获取锁
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))//如果获取锁失败,添加到队列中,由于ReentrantLock是独占锁所以节点必须是EXCLUSIVE类型
selfInterrupt();//添加中断标识位
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
nonfairTryAcquire方法
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();//获取到当前线程
int c = getState();//获取当前状态
if (c == 0) {//代表没有锁
if (compareAndSetState(0, acquires)) {//cas自旋尝试获得锁
setExclusiveOwnerThread(current);//当前独占锁的拥有线程设置为当前线程
return true;//返回
}
}
else if (current == getExclusiveOwnerThread()) {//如果当前线程之前已经获取到锁
int nextc = c + acquires;//锁重入
if (nextc < 0) // overflow 锁重入不能为0
throw new Error("Maximum lock count exceeded");
setState(nextc);//设置最新的state状态
return true;
}
return false;
}
acquireQueued方法和addWaiter方法与公平锁的实现一致
锁释放
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
if (tryRelease(arg)) {//尝试释放锁
Node h = head;
if (h != null && h.waitStatus != 0)//如果头结点不为空并且头结点的等待状态不等于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) {//状态为0
free = true;
setExclusiveOwnerThread(null);//将当前互斥锁的拥有线程设置为空
}
setState(c);//设置状态位
return free;
}
4.留言
本文章只是JUC 中AQS的一部分,后续的文章会对基于AQS锁实现的子类进行拓展讲解,以上文章内容基于个人以及结合别人文章的理解,如果有问题或者不当之处欢迎大家留言交流。由于为了保证观看流畅性,其中一部分源码有重复的地方。请见谅
JUC同步锁原理的更多相关文章
- JUC同步锁(五)
根据锁的添加到Java中的时间,Java中的锁,可以分为"同步锁"和"JUC包中的锁". 一.同步锁--synchronized关键字 通过synchroniz ...
- 结合 Redis 实现同步锁
1.技术方案 1.1.redis的基本命令 1)SETNX命令(SET if Not eXists) 语法:SETNX key value 功能:当且仅当 key 不存在,将 key 的值设为 val ...
- 001-多线程-锁-架构【同步锁、JUC锁】
一.概述 Java中的锁,可以分为"同步锁"和"JUC包中的锁". 1.1.同步锁 即通过synchronized关键字来进行同步,实现对竞争资源的互斥访问的锁 ...
- JUC——线程同步锁(Condition精准控制)
在进行锁处理的时候还有一个接口:Condition,这个接口可以由用户来自己进行锁的对象创建. Condition的作用是对锁进行更精确的控制. Condition的await()方法相当于Objec ...
- AQS学习(二) AQS互斥模式与ReenterLock可重入锁原理解析
1. MyAQS介绍 在这个系列博客中,我们会参考着jdk的AbstractQueuedLongSynchronizer,从零开始自己动手实现一个AQS(MyAQS).通过模仿,自己造轮子来学习 ...
- Java Learning:并发中的同步锁(synchronized)
引言 最近一段时间,实验室已经倾巢出动找实习了,博主也凑合了一把,结果有悲有喜,BAT理所应当的跪了,也收到了其他的offer,总的感受是有必要夯实基础啊. 言归正传,最近在看到java多线程的时候, ...
- MYSQL主从不同步延迟原理
1. MySQL数据库主从同步延迟原理. 要说延时原理,得从mysql的数据库主从复制原理说起,mysql的主从复制都是单线程的操作, 主库对所有DDL和DML产生binlog,binlog是 ...
- Zookeeper--0300--java操作Zookeeper,临时节点实现分布式锁原理
删除Zookeeper的java客户端有 : 1,Zookeeper官方提供的原生API, 2,zkClient,在原生api上进行扩展的开源java客户端 3, 一.Zookeeper原生API ...
- MYSQL主从不同步延迟原理分析及解决方案(摘自http://www.jb51.net/article/41545.htm)
1. MySQL数据库主从同步延迟原理.要说延时原理,得从mysql的数据库主从复制原理说起,mysql的主从复制都是单线程的操作,主 库对所有DDL和DML产生binlog,binlog是顺序写,所 ...
- 在有 UI 线程参与的同步锁(如 AutoResetEvent)内部使用 await 可能导致死锁
AutoResetEvent.ManualResetEvent.Monitor.lock 等等这些用来做同步的类,如果在异步上下文(await)中使用,需要非常谨慎. 本文将说一个在同步上下文中非常常 ...
随机推荐
- [ACM]STL-dfs
#include<iostream> using namespace std; int book[101],sum,n,e[101][101]; void dfs(int cur) { c ...
- 最强绘图AI:一文搞定Midjourney(附送咒语)
最强绘图AI:一文搞定Midjourney(附送咒语) Midjourney官网:https://www.midjourney.com 简介 Midjourney是目前效果最棒的AI绘图工具.访问Mi ...
- selenium验证码处理-获取验证码图片二进流数据转成原图保存
1.因为视频的作者给的代码不完整,只有核心部分的代码. 2.视频作者示例使用的第三方破解12306的脚本网页(失效了) 所以本人无法复现,此次截取部分代码作为理解核心意思(思想方法最重要) 1.面向对 ...
- 【ACM算法竞赛日常训练】DAY10题解与分析【月月给华华出题】【华华给月月出题】| 筛法 | 欧拉函数 | 数论
DAY10共2题: 月月给华华出题 华华给月月出题 难度较大. 作者:Eriktse 简介:211计算机在读,现役ACM银牌选手力争以通俗易懂的方式讲解算法!️欢迎关注我,一起交流C++/Python ...
- [Windows]BAT脚本自定义函数
1 helloworld @echo off call :helloworld helloworld goto :EOF :helloworld setlocal echo %1 endlocal&a ...
- arc076f F - Exhausted?
ARC076 F - Exhausted? [题目大意] \(有m个座位,分别位于坐标为1,2,3,...,m的地方:n个客人,第i位客人只坐位于[0,li]∪[ri,m]的座位.每个座位只能坐一个人 ...
- 10.CAS实现单点登录
1.总结: 昨天主要是了解和编写了CAS实现单点登录的代码: CAS实现单点登录的流程:用户访问资源服务器,先跳转到验证服务器验证身份通过后,认证服务器发送一个ticket给用户,用户拿着ticket ...
- Rust中的into函数和from函数
1.Rust中的into函数和from函数是做什么用的? into函数是Rust语言中的一个转换函数,它属于Into trait.它可以将一个类型转换为另一个类型.实现了From trait的类型会自 ...
- java 回行矩阵的打印
n=3 n=4 1 2 3 1 2 3 4 8 9 4 12 13 14 5 7 6 5 11 16 15 6 10 9 ...
- FreeSWITCH添加iLBC编码及转码
操作系统 :CentOS 7.6_x64 FreeSWITCH版本 :1.10.9 一.安装ilbc库 从第三方库里下载指定版本: git clone https://freeswitch.org/s ...