java 互斥同步
Java 并发 API 包括多种同步机制,可以支持你:
定义用于访问某一共享资源的临界段;
在某一共同点上同步不同
synchronized
synchronized
关键字解决的是多个线程之间访问资源的同步性,synchronized
关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
同步语句块原理:利用2个指令对monitor加锁/释放锁实现同步
1、monitor enter 0可以
每个对象多有与monitor与之关联,一个monitor的lock锁只能被一个线程在同一时间获取,在一个线程尝试获取与锁对象相关联的monitor时会发生以下几件事情。
如果monitor的计数器为0,意味着该monitor的lock锁还没有被获取,当一个线程获的后会立刻对该计数器+1,这样就代表这该monitor被占有
如果一个已经拥有该monitor所有权的线程重入,则会导致monitor的计数器再次被累加
如果monitor已经被其他线程占有,其他线程尝试获取该monitor的所有权时,被陷入到阻塞状态,知道monitor计数器变为0,才再次尝试获取monitor所有权
2、monitor exit
释放对monitor的所有权,前提是曾经获得过所有权。释放的过程较为简单,就是将monitor的计数器-1,如果计数器的结果为0。则代表这线程失去了对该monitor的所有权,与此同时被该monitor block的线程将再次尝试获取该monitor的所有权。
同步方法原理
1.同步⼀个代码块
它只作⽤于同⼀个对象,如果调⽤两个对象上的同步代码块,就不会进⾏同步。
对于以下代码,使⽤ ExecutorService 执⾏了两个线程,由于调⽤的是同⼀个对象的同步代码块,因此 这两个线程会进⾏同步,当⼀个线程进⼊同步语句块时,另⼀个线程就必须等待。
public class SynchronizedExample { public void func1() { synchronized (this) { for (int i = 0; i < 10; i++) { System.out.print(i + " "); } } } } public static void main(String[] args) { SynchronizedExample e1 = new SynchronizedExample(); ExecutorService executorService = Executors.newCachedThreadPool(); executorService.execute(() -> e1.func1()); executorService.execute(() -> e1.func1()); }
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
使⽤ ExecutorService 执⾏了两个线程,由于调⽤的是同⼀个对象的同步代码块(不同方法),因此 这两个线程会进⾏同步,当⼀个线程进⼊同步语句块时,另⼀个线程就必须等待
public class Main { public void func1() { synchronized (this) { for (int i = 0; i < 10; i++) { System.out.print(i + " "); } } } public void func2() { synchronized (this) { for (int i = 10; i < 20; i++) { System.out.print(i + " "); } } } public static void main(String[] args) { Main e1 = new Main(); ExecutorService executorService = Executors.newCachedThreadPool(); executorService.execute(() -> e1.func1()); executorService.execute(() -> e1.func2()); } }
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
2个线程调用同一对象的同步代码块(不管是不是一个方法),都要同步
对于以下代码,两个线程调⽤了不同对象的同步代码块,因此这两个线程就不需要同步。从输出结果可 以看出,两个线程交叉执⾏。
public static void main(String[] args) { SynchronizedExample e1 = new SynchronizedExample(); SynchronizedExample e2 = new SynchronizedExample(); ExecutorService executorService = Executors.newCachedThreadPool(); executorService.execute(() -> e1.func1()); executorService.execute(() -> e2.func1()); }
0 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9
2.同步⼀个⽅法
它和同步代码块⼀样,作⽤于同⼀个对象。
3. 同步⼀个类
作⽤于整个类,也就是说两个线程调⽤同⼀个类的不同对象上的这种同步语句,也会进⾏同步
public class SynchronizedExample { public void func2() { synchronized (SynchronizedExample.class) { for (int i = 0; i < 10; i++) { System.out.print(i + " "); } } } }
public static void main(String[] args) { SynchronizedExample e1 = new SynchronizedExample(); SynchronizedExample e2 = new SynchronizedExample(); ExecutorService executorService = Executors.newCachedThreadPool(); executorService.execute(() -> e1.func2()); executorService.execute(() -> e2.func2()); }
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
synchronized
关键字加到static
静态方法和synchronized(class)
代码块上都是是给 Class 类上锁。synchronized
关键字加到实例方法上是给对象实例上锁。- 尽量不要使用
synchronized(String a)
因为 JVM 中,字符串常量池具有缓存功能!
构造方法不能使用 synchronized 关键字修饰。
构造方法本身就属于线程安全的,不存在同步的构造方法一说
DK1.6 之后的 synchronized 关键字底层做了哪些优化
DK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。
锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率
Lock 接口:
Lock 提供了比 synchronized 关键字更为灵活的同步操作。Lock 接口有多种 不同类型:ReentrantLock 用于实现一个可与某种条件相关联的锁;ReentrantReadWriteLock 将读写操作分离开来;StampedLock 是 Java 8 中增加的一种新特性,它包括三种 控制读/写访问的模式
ReentrantLock 是 java.util.concurrent(J.U.C)包中的锁。
public class LockExample { private Lock lock = new ReentrantLock(); public void func() { lock.lock(); try { for (int i = 0; i < 10; i++) { System.out.print(i + " "); } } finally { lock.unlock(); // 确保释放锁,从⽽避免发⽣死锁。 } } } public static void main(String[] args) { LockExample lockExample = new LockExample(); ExecutorService executorService = Executors.newCachedThreadPool(); executorService.execute(() -> lockExample.func()); executorService.execute(() -> lockExample.func()); }
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
⽐较
相同
两者都是可重入锁
可重入锁” 指的是自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,
如果是不可重入锁的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增 1,所以要等到锁的计数器下降为 0 时才能释放锁
不同
1. 锁的实现
synchronized 是 JVM 实现的,⽽ ReentrantLock 是 JDK 实现的。
2 ReentrantLock 比 synchronized 增加了一些高级功能
2.1等待可中断
当持有锁的线程⻓期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。
2.2 公平锁
公平锁是指多个线程在等待同⼀个锁时,必须按照申请锁的时间顺序来依次获得锁。 synchronized 中的锁是⾮公平的,ReentrantLock 默认情况下也是⾮公平的,但是也可以是公平的。
2.3 可实现选择性通知(锁可以绑定多个条件)
: synchronized
关键字与wait()
和notify()
/notifyAll()
方法相结合可以实现等待/通知机制。ReentrantLock
类当然也可以实现,但是需要借助于Condition
接口与newCondition()
方法。
使⽤选择
除⾮需要使⽤ ReentrantLock 的⾼级功能,否则优先使⽤ synchronized。
这是因为 synchronized 是 JVM 实现的⼀种锁机制,JVM 原⽣地⽀持它,⽽ ReentrantLock 不是所有的 JDK 版本都⽀持。
并且使 ⽤ synchronized 不⽤担⼼没有释放锁⽽导致死锁问题,因为 JVM 会确保锁的释放
synchronized不会死锁,不准确。假设A,B两个方法都是synchronized修饰,A方法里面调用B方法,B方法里面调用A方法,则某一时刻,M,N两个线程同时访问A,B两方法,这不就死锁了
Semaphore 类:
该类通过实现经典的信号量机制来实现同步。可以控制对互斥资源的访问线程数。Java 支持二进制信号量和一般 信号量。
信号量机制是 Edsger Dijkstra 于 1962 年提出的,用于控制对一个或多个共享资源的访问。
该机制 基于一个内部计数器以及两个名为 wait()和 signal()的方法。
当一个线程调用了 wait()方法时, 如果内部计数器的值大于 0,那么信号量对内部计数器(可允许的)做递减操作,并且该线程获得对该共享资源的访问。
如果内部计数器的值为 0,那么线程将被阻塞,直到某个线程调用 singal()方法为止
当一 个线程调用了 signal()方法时,信号量将会检查是否有某些线程处于等待状态(它们已经调用了 wait()方法)。
如果没有线程等待,它将对内部计数器做递增操作。如果有线程在等待信号量,就获 取这其中的一个线程,该线程的 wait()方法结束返回并且访问共享资源。
其他线程将继续等待,直 到轮到自己为止。
CountDownLatch 类
CountDownLatch的作用就是等待其他的线程都执行完任务,必要时可以对各个任务的执行结果进行汇总,然后主线程才继续往下执行。
可用于使用多线程读取多个文件处理的场景
public class CountdownLatchExample { public static void main(String[] args) throws InterruptedException { final int totalThread = 10; CountDownLatch countDownLatch = new CountDownLatch(totalThread); ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < totalThread; i++) { executorService.execute(() -> { System.out.print("run.."); countDownLatch.countDown(); }); } countDownLatch.await(); System.out.println("end"); executorService.shutdown(); } }
CyclicBarrier
⽤来控制多个线程互相等待,只有当多个线程都到达时,这些线程才会继续执⾏。
和 CountdownLatch 相似,都是通过维护计数器来实现的。线程执⾏ await() ⽅法之后计数器会减 1, 并进⾏等待,直到计数器为 0,所有调⽤ await() ⽅法⽽在等待的线程才能继续执⾏。
CyclicBarrier 和 CountdownLatch 的⼀个区别是,CyclicBarrier 的计数器通过调⽤ reset() ⽅法可以循 环使⽤,所以它才叫做循环屏障。
CyclicBarrier 有两个构造函数,其中 parties 指示计数器的初始值,barrierAction 在所有线程都到达屏 障的时候会执⾏⼀次。
简述java中volatile关键字作用
https://blog.csdn.net/m0_37506254/article/details/83239797?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522167151244516800180681677%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fblog.%2522%257D&request_id=167151244516800180681677&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~blog~first_rank_ecpm_v1~rank_v31_ecpm-1-83239797-null-null.nonecase&utm_term=volatile&spm=1018.2226.3001.4450
- 保证变量对所有线程的可见性。 当一条线程修改了变量值,新值对于其他线程来说是立即可以得知的。 ]
-
//线程1
boolean stop = false;
while(!stop){
doSomething();
}
//线程2
stop = true; -
但是用volatile修饰之后就变得不一样了:
第一:使用volatile关键字会强制将修改的值立即写入主存;
第二:使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);
第三:由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。
那么在线程2修改stop值时(当然这里包括2个操作,修改线程2工作内存中的值,然后将修改后的值写入内存),会使得线程1的工作内存中缓存变量stop的缓存行无效,然后线程1读取时,发现自己的缓存行无效,它会等待缓存行对应的主存地址被更新之后,然后去对应的主存读取最新的值。
那么线程1读取到的就是最新的正确的值
- 禁止指令重排序优化。对于处理器重排序,JMM的处理器重排序规则会要求Java编译器在生成指令序列时,插入特定类型的内存屏障(Memory Barriers,Intel称之为Memory Fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序
下面是基于保守策略的JMM内存屏障插入策略:
- 在每个volatile写操作的前面插入一个StoreStore屏障。
- 在每个volatile写操作的后面插入一个StoreLoad屏障。
- 在每个volatile读操作的后面插入一个LoadLoad屏障。
- 在每个volatile读操作的后面插入一个LoadStore屏障。
volatile 无法保证原子性
在前面已经提到过,自增操作是不具备原子性的,它包括读取变量的原始值、进行加1操作、写入工作内存。那么就是说自增操作的三个子操作可能会分割开执行,就有可能导致下面这种情况出现:
假如某个时刻变量inc的值为10,
线程1对变量进行自增操作,线程1先读取了变量inc的原始值,然后线程1被阻塞了;
然后线程2对变量进行自增操作,线程2也去读取变量inc的原始值,由于线程1只是对变量inc进行读取操作,而没有对变量进行修改操作,所以不会导致线程2的工作内存中缓存变量inc的缓存行无效,所以线程2会直接去主存读取inc的值,发现inc的值时10,然后进行加1操作,并把11写入工作内存,最后写入主存。
然后线程1接着进行加1操作,由于已经读取了inc的值,注意此时在线程1的工作内存中inc的值仍然为10,所以线程1对inc进行加1操作后inc的值为11,然后将11写入工作内存,最后写入主存。
那么两个线程分别进行了一次自增操作后,inc只增加了1。
解释到这里,可能有朋友会有疑问,不对啊,前面不是保证一个变量在修改volatile变量时,会让缓存行无效吗?然后其他线程去读就会读到新的值,对,这个没错。这个就是上面的happens-before规则中的volatile变量规则,但是要注意,线程1对变量进行读取操作之后,被阻塞了的话,并没有对inc值进行修改。然后虽然volatile能保证线程2对变量inc的值读取是从内存中读取的,但是线程1没有进行修改,所以线程2根本就不会看到修改的值。
要始终牢记使用 volatile 的限制 —— 只有在状态真正独立于程序内其他内容时才能使用 volatile —— 这条规则能够避免将这些模式扩展到不安全的用例。
volatile 应用场景
模式 #1:状态标志
也许实现 volatile 变量的规范使用仅仅是使用一个布尔状态标志,用于指示发生了一个重要的一次性事件,例如完成初始化或请求停机。
很多应用程序包含了一种控制结构,形式为 “在还没有准备好停止程序时再执行一些工作”,举个例子:
volatile boolean shutdownRequested; ... public void shutdown() { shutdownRequested = true; } public void doWork() { while (!shutdownRequested) { // do stuff } }
2 单例模式
双检锁 --volatile 用于禁止指令重排优化
synchronized 关键字和 volatile 关键字的区别
synchronized: 具有原子性,有序性和可见性;
volatile:具有有序性和可见性
synchronized
关键字和 volatile
关键字是两个互补的存在,而不是对立的存在!
volatile
关键字是线程同步的轻量级实现,所以volatile
性能肯定比synchronized
关键字要好 。但是volatile
关键字只能用于变量而synchronized
关键字可以修饰方法以及代码块 。volatile
关键字能保证数据的可见性,但不能保证数据的原子性。synchronized
关键字两者都能保证。volatile
关键字主要用于解决变量在多个线程之间的可见性,而synchronized
关键字解决的是多个线程之间访问资源的同步性
要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。
ThreadLocal
通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。如果想实现每一个线程都有自己的专属本地变量该如何解决呢?
JDK 中提供的ThreadLocal
类正是为了解决这样的问题。 ThreadLocal
类主要解决的就是让每个线程绑定自己的值,可以将ThreadLocal
类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。
原理
如果你创建了一个ThreadLocal
变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是ThreadLocal
变量名的由来。他们可以使用 get()
和 set()
方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题
每个Thread
中都具备一个ThreadLocalMap
,(Hashmap性质的),最终的变量是放在了当前线程的 ThreadLocalMap
中。而ThreadLocalMap
可以存储以ThreadLocal
为 key ,Object 对象为 value 的键值对。
ThrealLocal
类中可以通过Thread.currentThread()
获取到当前线程对象后,直接通过getMap(Thread t)
可以访问到该线程的ThreadLocalMap
对象。在私有ThreadLocalMap的操作。
ThreadLocal 内存泄露
ThreadLocalMap
中使用的 key 为 ThreadLocal
的弱引用,而 value 是强引用。所以,如果 ThreadLocal
没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。
这样一来,ThreadLocalMap
中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。
Java 乐观锁和悲观锁 实现和应用场景?
定义:
1、乐观锁:顾名思义,对每次的数据操作都保持乐观的态度,不担心数据会被修改,所以不会对数据进行上锁。由于数据没有上锁,这就存在数据会被多人读写的情况。所以每次修改数据的时候需要对数据进行判断是否被修改过(写操作时判断)。
2、悲观锁:与乐观锁相反,对每次的数据操作都保存悲观的态度,总是担心数据会被修改,所以在自己操作的时候会对数据上锁,防止在自己操作的时候被他人同时操作导致更新丢失
使用场景
1、乐观锁:由于乐观锁的不上锁特性,所以在性能方面要比悲观锁好,比较适合用在DB的读大于写的业务场景。
2、悲观锁:对于每一次数据修改都要上锁,如果在DB读取需要比较大的情况下有线程在执行数据修改操作会导致读操作全部被挂载起来,等修改线程释放了锁才能读到数据,体验极差。所以比较适合用在DB写大于读的情况。
实现
乐观锁两种常用的实现方式
第一种是使用版本号或者时间戳。在表中加个version或updatetime字段,在每次更新操作时对此一下该字段,如果一致则更新数据,数据不等则放弃本次修改,根据实际业务需求做相应的处理。
第二种是CAS方式。
乐观锁一般都采用 Compare And Swap(CAS)算法进行实现。顾名思义,该算法涉及到了两个操作,比较(Compare)和交换(Swap)。
CAS 算法的思路如下:
一个线程失败或挂起并不会导致其他线程也失败或挂起,那么这种算法就被称为非阻塞算法。而CAS就是一种非阻塞算法实现,也是一种乐观锁技术,
它能在不使用锁的情况下实现多线程安全,所以CAS也是一种无锁算法。
CAS比较并交换,是一种实现并发算法时常用到的技术,Java并发包中的很多类都使用了CAS技术。
CAS具体包括三个参数:当前内存值V、旧的预期值A、即将更新的值B,当且仅当预期值A和内存值V相同时,将内存值修改为B并返回true,否则什么都不做,并返回false。
CAS缺点
【1】循环时间长、开销很大。
当某一方法比如:getAndAddInt执行时,如果CAS失败,会一直进行尝试。如果CAS长时间尝试但是一直不成功,可能会给CPU带来很大的开销。
【2】只能保证一个共享变量的原子操作。
当操作1个共享变量时,我们可以使用循环CAS的方式来保证原子操作,但是操作多个共享变量时,循环CAS就无法保证操作的原子性,这个时候就需要用锁来保证原子性。
【3】存在ABA问题
ABA问题:
ABA问题是CAS中的一个漏洞。CAS的定义,当且仅当内存值V等于就得预期值A时,CAS才会通过原子方式用新值B来更新V的值,否则不会执行任何操作。
那么如果先将预期值A给成B,再改回A,那CAS操作就会误认为A的值从来没有被改变过,这时其他线程的CAS操作仍然能够成功,但是很明显是个漏洞,因为预期值A的值变化过了。
如何解决这个异常现象?java并发包为了解决这个漏洞,提供了一个带有标记的原子引用类“AtomicStampedReference”,它可以通过控制变量值的版本来保证CAS的正确性,即在变量前面添加版本号,每次变量更新的时候都把版本号+1,这样变化过程就从“A-B-A”变成了“1A-2B-3A”
悲观锁三种常用的实现方式
第一种是数据库实现方式。 使用数据库的读锁、写锁、行锁等实现进程的悬挂阻塞等当前操作完成后才能进行下一个操作。
第二种是synchronize的实现方式。 在Java里面可以使用synchronize实现悲观锁。
第三种是使用封装JUC包的实现方式。 在Java中使用LinkedBlockingQueue、ArrayBlockingQueue等JUC的封装包来实现悲观锁,其根本原理是AQS,而AQS是synchronize的升级版。
简述阻塞队列
阻塞队列是生产者消费者的实现具体组件之一。当阻塞队列为空时,从队列中获取元素的操作将会被阻塞,当阻塞队列满了,往队列添加元素的操作将会被阻塞。具体实现有:
- ArrayBlockingQueue:底层是由数组组成的有界阻塞队列。
- LinkedBlockingQueue:底层是由链表组成的有界阻塞队列。
- PriorityBlockingQueue:阻塞优先队列。
- DelayQueue:创建元素时可以指定多久才能从队列中获取当前元素
- SynchronousQueue:不存储元素的阻塞队列,每一个存储必须等待一个取出操作
- LinkedTransferQueue:与LinkedBlockingQueue相比多一个transfer方法,即如果当前有消费者正等待接收元素,可以把生产者传入的元素立刻传输给消费者。
- LinkedBlockingDeque:双向阻塞队列
锁的四种状态与锁升级过程
锁的状态总共有四种,级别由低到高依次为:无锁、偏向锁、轻量级锁、重量级锁
并且四种状态会随着竞争的情况逐渐升级,而且是不可逆的过程,即不可降级,也就是说只能进行锁升级(从低级别到高级别),不能锁降级(高级别到低级别),意味着偏向锁升级成轻量级锁后不能降级成偏向锁。
这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。
synchronized
最初的实现方式是 “阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态切换需要耗费处理器时间,如果同步代码块中内容过于简单,这种切换的时间可能比用户代码执行的时间还长”,synchronized
实现同步最初的方式,这也是当初开发者诟病的地方,这也是在JDK6以前 synchronized
效率低下的原因,JDK6中为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”。
互斥锁与自旋锁
当已经有一个线程加锁后,其他线程加锁则就会失败,互斥锁和自旋锁对于加锁失败后的处理方式是不一样的:
- 互斥锁加锁失败后,线程会释放 CPU ,给其他线程; 开销成本是什么呢?会有两次线程上下文切换的成本
- 自旋锁加锁失败后,线程会忙等待,直到它拿到锁;
自旋锁是通过 CPU 提供的 CAS
函数(Compare And Swap),在「用户态」完成加锁和解锁操作,不会主动产生线程上下文切换,所以相比互斥锁来说,会快一些,开销也小一些
AQS
AQS(AbstractQuenedSynchronizer)抽象的队列式同步器。 AQS是将每一条请求共享资源的线程封装成一个锁队列的一个结点(Node),来实现锁的分配。
AQS是用来构建锁或其他同步组件的基础框架,它使用一个 volatile int state 变量作为共享资源,如果线程获取资源失败,则进入同步队列等待;如果获取成功就执行临界区代码,释放资源时会通知同步队列中的等待线程。
子类通过继承同步器并实现它的抽象方法getState、setState 和 compareAndSetState对同步状态进行更改
AQS 定义两种资源共享方式
- Exclusive(独占):只有一个线程能执行,如
ReentrantLock
。又可分为公平锁和非公平锁:- 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
- 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
- Share(共享):多个线程可同时执行,如
CountDownLatch
、Semaphore
、CyclicBarrier
、ReadWriteLock
我们都会在后面讲到。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
2020-02-23 Keras