最近是上班忙项目,下班带娃,忙的不可开交,连摸鱼的时间都没有了。今天趁假期用图解的方式从源码角度给大家说一下ReentrantLock
加锁解锁的全过程。系好安全带,发车了。
在聊它的源码之前,我们先来做个简单的使用说明。当我在IDEA
中创建了一个简单的Demo
之后,它会给出以下提示
在使用阻塞等待获取锁的方式中,必须在try
代码块之外,并且在加锁方法与try
代码块之间没有任何可能抛出异常的方法调用,避免加锁成功后,在finally
中无法解锁。
lock
方法与try
代码块之间的方法调用抛出异常,那么无法解锁,造成其它线程无法成功获取锁。lock
方法在try
代码块之内,可能由于其它方法抛出异常,导致在finally
代码块中,unlock
对未加锁的对象解锁,它会调用AQS
的tryRelease
方法(取决于具体实现类),抛出IllegalMonitorStateException
异常。Lock
对象的lock
方法实现中可能抛出unchecked
异常,产生的后果与说明二相同。java.concurrent.LockShouldWithTryFinallyRule.rule.desc
还举了两个例子,正确案例如下:
Lock lock = new XxxLock();
// ...
lock.lock();
try {
doSomething();
doOthers();
} finally {
lock.unlock();
}
错误案例如下:
Lock lock = new XxxLock();
// ...
try {
// 如果在此抛出异常,会直接执行 finally 块的代码
doSomething();
// 不管锁是否成功,finally 块都会执行
lock.lock();
doOthers();
} finally {
lock.unlock();
}
上边的案例中加锁调用的是lock()
方法,解锁用的是unlock()
方法,而通过查看源码发现它们都是调用的内部静态抽象类Sync
的相关方法。
abstract static class Sync extends AbstractQueuedSynchronizer
Sync
是通过继承AbstractQueuedSynchronizer
来实现的,没错,AbstractQueuedSynchronizer
就是AQS
的全称。AQS
内部维护着一个FIFO
的双向队列(CLH
),ReentrantLock
也是基于它来实现的,先来张图感受下。
//此处是 Node 的部分属性
static final class Node {
//排他锁标识
static final Node EXCLUSIVE = null;
//如果带有这个标识,证明是失效了
static final int CANCELLED = 1;
//具有这个标识,说明后继节点需要被唤醒
static final int SIGNAL = -1;
//Node对象存储标识的地方
volatile int waitStatus;
//指向上一个节点
volatile Node prev;
//指向下一个节点
volatile Node next;
//当前Node绑定的线程
volatile Thread thread;
//返回前驱节点即上一个节点,如果前驱节点为空,抛出异常
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
}
对于里边的waitStatus
属性,我们需要做个解释:(非常重要)
Condition
中使用,当前线程阻塞在Condition
,如果其他线程调用了Condition
的signal
方法,这个结点将从等待队列转移到同步队列队尾,等待获取同步锁;// 头结点
private transient volatile Node head;
// 尾结点
private transient volatile Node tail;
//0->1 拿到锁,大于0 说明当前已经有线程占用了锁资源
private volatile int state;
今天我们先简单了解下AQS
的构造以帮助大家更好的理解ReentrantLock
,至于深层次的东西先不做展开!
对AQS
的结构有了基本了解之后,我们正式进入主题——加锁。从源码中可以看出锁被分为公平锁和非公平锁。
/**
* 公平锁代码
*/
final void lock() {
acquire(1);
}
/**
* 非公平锁代码
*/
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
初步查看代码发现非公平锁似乎包含公平锁的逻辑,所以我们就从“非公平锁”开始。
final void lock() {
//通过 CAS 的方式尝试将 state 从0改为1,
//如果返回 true,代表修改成功,获得锁资源;
//如果返回false,代表修改失败,未获取锁资源
if (compareAndSetState(0, 1))
// 将属性exclusiveOwnerThread设置为当前线程,该属性是AQS的父类提供的
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
compareAndSetState()
:底层调用的是unsafe
的compareAndSwapInt
,该方法是原子操作;
假设有两个线程(t1
、t2
)在竞争锁资源,线程1获取锁资源之后,执行setExclusiveOwnerThread
操作,设置属性值为当前线程t1
此时,当t2
想要获取锁资源,调用lock()
方法之后,执行compareAndSetState(0, 1)
返回false
,会走else
执行acquire()
方法。
public final void accquire(int arg) {
// tryAcquire 再次尝试获取锁资源,如果尝试成功,返回true,尝试失败返回false
if (!tryAcquire(arg) &&
// 走到这,代表获取锁资源失败,需要将当前线程封装成一个Node,追加到AQS的队列中
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// 线程中断
selfInterrupt();
}
accquire()
中涉及的方法比较多,我们将进行拆解,一个一个来分析,顺序:tryAcquire() -> addWaiter() -> acquireQueued()
//AQS中
protected boolean tryAcquire(int arg) {
//AQS 是基类,具体实现在自己的类中实现,我们去查看“非公平锁”中的实现
throw new UnsupportedOperationException();
}
//ReentrantLock 中
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
//获取AQS 的 state
int c = getState();
// 如果 state 为0,代表尝试再次获取锁资源
if (c == 0) {
// 步骤同上:通过 CAS 的方式尝试将 state 从0改为1,
//如果返回 true,代表修改成功,获得锁资源;
//如果返回false,代表修改失败,未获取锁资源
if (compareAndSetState(0, acquires)) {
//设置属性为当前线程
setExclusiveOwnerThread(current);
return true;
}
}
//当前占有锁资源的线程是否是当前线程,如果是则证明是可重入操作
else if (current == getExclusiveOwnerThread()) {
//将 state + 1
int nextc = c + acquires;
//为什么会小于 0 呢?因为最大值 + 1 后会将符号位的0改为1 会变成负数(可参考Integer.MAX_VALUE + 1)
if (nextc < 0) // overflow
//加1后小于0,超出锁可重入的最大值,抛异常
throw new Error("Maximum lock count exceeded");
//设置 state 状态
setState(nextc);
return true;
}
return false;
}
因为线程1已经获取到了锁,此时state
为1,所以不走nonfairTryAcquire()
的if
。又因为当前是线程2,不是占有当前锁的线程1,所以也不会走else if
,即tryAcquire()
方法返回false
。
走到本方法中,代表获取锁资源失败。addWaiter()
将没有获取到锁资源的线程甩到队列的尾部。
private Node addWaiter(Node mode) {
//创建 Node 类,并且设置 thread 为当前线程,设置为排它锁
Node node = new Node(Thread.currentThread(), mode);
// 获取 AQS 中队列的尾部节点
Node pred = tail;
// 如果 tail == null,说明是空队列,
// 不为 null,说明现在队列中有数据,
if (pred != null) {
// 将当前节点的 prev 指向刚才的尾部节点,那么当前节点应该设置为尾部节点
node.prev = pred;
// CAS 将 tail 节点设置为当前节点
if (compareAndSetTail(pred, node)) {
// 将之前尾节点的 next 设置为当前节点
pred.next = node;
// 返回当前节点
return node;
}
}
enq(node);
return node;
}
当tail
不为空,即队列中有数据时,我们来图解一下pred!=null
代码块中的代码。初始化状态如下,pred
指向尾节点,node
指向新的节点。
node.prev = pred;
将node
的前驱节点设置为pred
指向的节点
compareAndSetTail(pred, node)
通过CAS
的方式尝试将当前节点node
设置为尾结点,此处我们假设设置成功,则FIFO
队列的tail
指向node
节点。
pred.next = node;
将pred
节点的后继节点设置为node
节点,此时node
节点成功进入FIFO
队列尾部。
而当pred
为空,即队列中没有节点或将node
节点设置为尾结点失败时,会走enq()
方法。我们列举的例子就符合pred
为空的情况,就让我们以例子为基础继续分析吧。
//现在没人排队,我是第一个 || 前边CAS失败也会进入这个位置重新往队列尾巴去塞
private Node enq(final Node node) {
//死循环
for (;;) {
//重新获取tail节点
Node t = tail;
// 没人排队,队列为空
if (t == null) {
// 初始化一个 Node 为 head,而这个head 没有意义
if (compareAndSetHead(new Node()))
// 将头尾都指向了这个初始化的Node,第一次循环结束
tail = head;
} else {
// 有人排队,往队列尾巴塞
node.prev = t;
// CAS 将 tail 节点设置为当前节点
if (compareAndSetTail(t, node)) {
//将之前尾节点的 next 设置为当前节点
t.next = node;
return t;
}
}
}
}
进入死循环,首先会走if
方法的逻辑,通过CAS
的方式尝试将一个新节点设置为head
节点,然后将tail
也指向新节点。可以看出队列中的头节点只是个初始化的节点,没有任何意义。
继续走死循环中的代码,此时t
不为null
,所以会走else
方法。将node
的前驱节点指向t
,通过CAS
方式将当前节点node
设置为尾结点,然后将t
的后继节点指向node
。此时线程2的节点就被成功塞入FIFO
队列尾部。
将已经在队列中的node
尝试去获取锁否则挂起。
final boolean acquireQueued(final Node node, int arg) {
// 获取锁资源的标识,失败为 true,成功为 false
boolean failed = true;
try {
// 线程中断的标识,中断为 true,不中断为 false
boolean interrupted = false;
for (;;) {
// 获取当前节点的上一个节点
final Node p = node.predecessor();
//p为头节点,尝试获取锁操作
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null;
// 将获取锁失败标识置为false
failed = false;
// 获取到锁资源,不会被中断
return interrupted;
}
// p 不是 head 或者 没拿到锁资源,
if (shouldParkAfterFailedAcquire(p, node) &&
// 基于 Unsafe 的 park方法,挂起线程
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
这里又出现了一次死循环,首先获取当前节点的前驱节点p,如果p是头节点(头节点没有意义),说明node
是head
后的第一个节点,此时当前获取锁资源的线程1可能会释放锁,所以线程2可以再次尝试获取锁。
假设获取成功,证明拿到锁资源了,将node
节点设置为head
节点,并将node
节点的pre
和thread
设置为null
。因为拿到锁资源了,node
节点就不需要排队了。
将头节点p的next
置为null
,此时p节点就不在队列中存在了,可以帮助GC
回收(可达性分析)。failed
设置为false
,表明获取锁成功;interrupted
为false
,则线程不会中断。
如果p不是head
节点或者没有拿到锁资源,会执行下边的代码,因为我们的线程1没有释放锁资源,所以线程2获取锁失败,会继续往下执行。
//该方法的作用是保证上一个节点的waitStatus状态为-1(为了唤醒后继节点)
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//获取上一个节点的状态,该状态为-1,才会唤醒下一个节点。
int ws = pred.waitStatus;
// 如果上一个节点的状态是SIGNAL即-1,可以唤醒下一个节点,直接返回true
if (ws == Node.SIGNAL)
return true;
// 如果上一个节点的状态大于0,说明已经失效了
if (ws > 0) {
do {
// 将node 的节点与 pred 的前一个节点相关联,并将前一个节点赋值给 pred
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0); // 一直找到小于等于0的
// 将重新标识好的最近的有效节点的 next 指向当前节点
pred.next = node;
} else {
// 小于等于0,但是不等于-1,将上一个有效节点状态修改为-1
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
只有节点的状态为-1,才会唤醒后一个节点,如果节点状态未设置,默认为0。
图解一下ws>0
的过程,因为ws>0
的节点为失效节点,所以do...while
中会重复向前查找前驱节点,直到找到第一个ws<=0
的节点为止,将node
节点挂到该节点上。
我们的pred
是头结点且未设置状态,所以状态为0,会走else
。通过CAS
尝试将pred
节点的waitStatus
设置为-1,表明node
节点需要被pred
唤醒。
shouldParkAfterFailedAcquire()
返回false
,继续执行acquireQueued()
中的死循环。
步骤和上边一样,node
的前驱节点还是head
,继续尝试获取锁。如果线程1释放了锁,线程2就可以拿到,返回true
;否则继续调用shouldParkAfterFailedAcquire()
,因为上一步已经将前驱结点的ws
设置为-1了,所以直接返回true
。
执行parkAndCheckInterrupt()
方法,通过UNSAFE.park();
方法阻塞当前线程2。等以后执行unpark
方法的时候,如果node
是头节点后的第一个节点,会进入acquireQueued()
方法中走if (p == head && tryAcquire(arg))
的逻辑获取锁资源并结束死循环。
该方法执行的机率约等于0,为什么这么说呢?因为针对failed
属性,只有JVM
内部出现问题时,才可能出现异常,执行该方法。
// node 为当前节点
private void cancelAcquire(Node node) {
if (node == null)
return;
node.thread = null;
// 上一个节点
Node pred = node.prev;
// 节点状态大于0,说明节点失效
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
// 将第一个不是失效节点的后继节点声明出来
Node predNext = pred.next;
// 节点状态变为失效
node.waitStatus = Node.CANCELLED;
// node为尾节点,cas设置pred为尾节点
if (node == tail && compareAndSetTail(node, pred)) {
//cas将pred的next设置为null
compareAndSetNext(pred, predNext, null);
} else {
int ws;
// 中间节点
// 如果上一个节点不是head 节点
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
// 前边已经判断了大于0的操作,
// pred 是需要唤醒后继节点的,所以当 waitStatus 不为 -1 时,需要将 pred 节点的 waitStatus 设置为 -1
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
Node next = node.next;
if (next != null && next.waitStatus <= 0)
// CAS 尝试将 pred 的 next 指向当前节点的 next
compareAndSetNext(pred, predNext, next);
} else {
// head 节点,唤醒后继节点
unparkSuccessor(node);
}
node.next = node; // help GC
}
}
执行到while
时找到前驱节点中最近的有效节点,把当前节点node
挂到有效节点后边,可以过滤掉当前节点前的失效节点。声明出有效节点的第一个后继无效节点predNext
,并把当前的node
节点状态设置为失效状态。
if
中的操作:如果当前节点是尾节点,CAS
尝试将最近的有效节点设置为尾节点,并将尾节点的next
设置为null
。
else
中的操作:
如果pred
节点不是头结点即中间节点,并且pred
的waitStatus
为-1或者waitStatus<=0
,为了让pred
节点能唤醒后继节点,需要设置为-1,并且pred
节点的线程不为空。获取node
节点的后继节点,如果后继节点有效,CAS
尝试将pred
的next
指向node
节点的next
。
当其他节点来找有效节点的时候走当前node
的prev
这条线,而不是再一个一个往前找,可以提高效率。
如果是头结点则唤醒后继节点。
最后将node
节点的next
指向自己。
释放锁是不区分公平锁和非公平锁的,释放锁的核心是将state
由大于 0 的数置为 0。废话不多说,直接上代码
//释放锁方法
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
//尝试释放锁资源,如果释放成功,返回true
if (tryRelease(arg)) {
Node h = head;
// head 不为空且 head 的 ws 不为0(如果为0,代表后边没有其他线程挂起)
if (h != null && h.waitStatus != 0)
// AQS的队列中有 node 在排队,并且线程已经挂起
// 需要唤醒被挂起的 Node
unparkSuccessor(h);
return true;
}
// 代表释放一次没有完全释放
return false;
}
如果释放锁成功,需要获取head
节点。如果头结点不为空且waitStatus
不为0,则证明有node
在排队,执行唤醒挂起其他node
的操作。
protected final boolean tryRelease(int releases) {
//获取当前锁的状态,先进行减1操作,代表释放一次锁资源
int c = getState() - releases;
//如果释放锁的线程不是占用锁的线程,直接抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 如果 c 为0 ,代表锁完全释放了,如果不为0,代表锁之前重入了,一次没释放掉,等待下次再次执行时,再次判断
if (c == 0) {
// 释放锁标志为 true,代表完全释放了
free = true;
// 将占用互斥锁的标识置为 null
setExclusiveOwnerThread(null);
}
// 设置 state 状态
setState(c);
return free;
}
我们的例子中线程1占用锁资源,线程1释放锁之后,state
为0。进入if
操作,将释放标志更新为true
,将FIFO
队列的exclusiveOwnerThread
标志置为null
。
用于唤醒AQS
中被挂起的线程。
// 注意当前的 node 节点是 head 节点
private void unparkSuccessor(Node node) {
//获取 head 的状态
int ws = node.waitStatus;
if (ws < 0)
// CAS 将 node 的 ws 设置为0,代表当前 node 接下来会舍弃
compareAndSetWaitStatus(node, ws, 0);
// 获取头节点的下一个节点
Node s = node.next;
// 如果下一个节点为null 或者 下一个节点为失效节点,需要找到离 head 最近的有效node
if (s == null || s.waitStatus > 0) {
s = null;
// 从尾节点开始往前找不等于null且不是node的节点
for (Node t = tail; t != null && t != node; t = t.prev)
// 如果该节点有效,则将s节点指向t节点
if (t.waitStatus <= 0)
s = t;
}
// 找到最近的node后,直接唤醒
if (s != null)
LockSupport.unpark(s.thread);
}
因为在addWaiter
方法中是先给prev
指针赋值,最后才将上一个节点的next
指针赋值,为了避免防止丢失节点或者跳过节点,必须从后往前找。
我们举例中head
节点的状态为-1
,通过CAS
的方式将head
节点的waitStatus
设置为0。
我们的头结点的后继节点是线程2所在的节点,不为null
,所以这边会执行unpark
操作,从上边的acquireQueued()
内的parkAndCheckInterrupt()
方法继续执行。
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
//返回目标线程是否中断的布尔值:中断返回true,不中断返回false,且返回后会重置中断状态为未中断
return Thread.interrupted();
}
因为线程2未中断,所以返回false
。继续执行acquireQueued()
中的死循环
for (;;) {
// 获取当前节点的上一个节点
final Node p = node.predecessor();
//p为头节点,尝试获取锁操作
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null;
// 将获取锁失败标识置为false
failed = false;
// 获取到锁资源,不会被中断
return interrupted;
}
// p 不是 head 或者 没拿到锁资源,
if (shouldParkAfterFailedAcquire(p, node) &&
// 基于 Unsafe 的 park方法,挂起线程
parkAndCheckInterrupt())
interrupted = true;
}
此时p是头节点,且能获取锁成功,将exclusiveOwnerThread
设置为线程2,即线程2 获取锁资源。
将node
节点设置为head
节点,并将node
节点的pre
和thread
设置为null
。因为拿到锁资源了,node
节点就不需要排队了。
将头节点p的next
置为null
,此时p节点就不在队列中存在了,可以帮助GC
回收(可达性分析)。failed
设置为false
,表明获取锁成功;interrupted
为false
,则线程不会中断。
从上边的方法可以看出,当parkAndCheckInterrupt()
方法返回true
时,即Thread.interrupted()
方法返回了true
,也就是该线程被中断了。为了让被唤醒的线程继续执行后续获取锁的操作,就需要让中断的线程像没有被中断过一样继续往下执行,所以在返回中断标记的同时要清除中断标记,将其设置为false
。
清除中断标记之后不代表该线程不需要中断了,所以在parkAndCheckInterrupt()
方法返回true
时,要自己设置一个中断标志interrupted = true
,为的就是当获取到锁资源执行完相关的操作之后进行中断补偿,故而需要执行selfInterrupt()
方法中断线程。
以上就是我们加锁解锁的图解过程了。最后我们再来说一下公平锁和非公平锁的区别。
前边已经说过了,似乎非公平锁包含了公平锁的全部操作。打开公平锁的代码,我们发现accquire()
方法中只有该方法的实现有点区别。
hasQueuedPredecessors()
返回false
时才会尝试获取锁资源。该方法代码实现如下
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
h==t
时,队列为空,表示没人排队,可以获取锁资源;以上就是我们的全部内容了,我们在最后再做个总结:
finally
中无法解锁;AQS
的FIFO
队列以及Node
的相关属性,尤其注意waitStatus
的状态;本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/hrFUK7If8JmBbaA-bR6M3A
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。