本文字数:7444字
预计阅读时间:19分钟
一、什么是死锁
说到死锁,大家可能都不陌生,每次遇到死锁,总会让计算机产生比较严重的后果,比如资源耗尽,界面无响应等。死锁的精确定义:
集合中的每一个进程(或线程)都在等待只能由本集合中的其他进程(或线程)才能引发的事件,那么该组进程是死锁的。
对于这个定义大家可能有点迷惑,换一种通俗的说法就是:
死锁是指两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
经典的“哲学家进餐问题”可以帮助我们形象的理解死锁问题。
有五个哲学家,他们的生活方式是交替地进行思考和进餐,哲学家们共用一张圆桌,分别坐在周围的五张椅子上,在圆桌上有五个碗和五支筷子,平时哲学家进行思考,饥饿时便试图取其左、右最靠近他的筷子,只有在他拿到两支筷子时才能进餐,该哲学家进餐完毕后,放下左右两只筷子又继续思考。
当五个哲学家同时去取他左边的筷子,每人拿到一只筷子且不释放,即五个哲学家只得无限等待下去,这样就产生了死锁的问题
在计算机中也可以用有向图来描述死锁问题,首先假定每个线程为有向图中的一个节点,申请锁的线程A为起点, 拥有锁的线程B为终点,这样就形成线程A到线程B的一条有向边,而众多的锁(边)和线程(点), 就构成了一个有向图。
如果在有向图中形成一条环路,就会产生一个死锁,如上图所示。在很多计算机系统中,检测是否有死锁存在就是将问题抽象为寻找有向图中的环路。
二、常见的死锁的场景
下面分析几种常见的死锁形式:
public class TestDeadLock {
private final Object lockA = new Object();
private final Object lockB = new Object();
public void lockAtoB(){
synchronized (lockA){
synchronized (lockB){
doSomething();
}
}
}
public void lockBtoA(){
synchronized (lockB){
synchronized (lockA){
doSomething();
}
}
}
private void doSomething(){
System.out.println("doSomething");
}
}
上述代码中,如果一个线程调用lockAtoB(),另一个线程调用lockBtoA(),并且两个线程是交替执行,那么在程序运行期间是有一定几率产生死锁。
而产生死锁的原因是:两个线程用不同的顺序去获取两个相同的锁,如果可以始终用相同的顺序,即每个线程都先获取lockA,然后再获取lockB,就不会出现循环的加锁依赖,也就不会产生死锁。
当然上面的代码只是一个示例,实际的代码中不会这么简单,而有些函数中,虽然看似都是以相同的顺序加锁,但是由于外部调用的不确定性,仍然会导致实际以不同的顺序加锁而产生死锁。
再看一个例子:
//仓库
public static interface IStore{
public void inCome(int count);
public void outCome(int count);
}
/**
* 从 in 仓库 调用货物去 out仓库
* @param from
* @param to
* @param count 调用货物量
*/
public void transportGoods(IStore from,IStore to,int count){
synchronized (from){
synchronized (to){
from.outCome(count);//出仓库
to.inCome(count);//入仓库
}
}
}
货运公司将货物从一个仓库转运到另一个仓库,转运前,需要同时获得两个仓库的锁,以确保两个仓库中的货物数量是以原子方式更新。看起来这个函数都是以相同的顺序获取锁,但这只是函数内部的顺序,而真正的执行顺序,取决于外部传入的对象。
transportGoods(storeA,storeB,100);
transportGoods(storeB,storeA,40);
如果用上述代码调用,在频繁的调用过程中,也很容易产生死锁。从上面的代码中可以看出,需要一个方法来确保在整个程序运行期间,锁都按照事先定义好的顺序来获取。这里提供一种方式:通过比较对象的hashcode值,来定义锁的获取顺序。
下面来改造一下上述代码
private static final Object extraLock = new Object();
/**
* 从 in 仓库 调用货物去 out仓库
* @param from
* @param to
* @param count 调用货物量
*/
public void transportGoods(IStore from,IStore to,int count){
int fromHashCode = System.identityHashCode(from);
int toHashCode = System.identityHashCode(to);
if(fromHashCode > toHashCode){
synchronized (from){
synchronized (to){
transportGoodsInternal(from,to,count);
}
}
}else if(fromHashCode < toHashCode){
synchronized (to){
synchronized (from){
transportGoodsInternal(from,to,count);
}
}
}else {//hash散列冲突,需要用新的一个锁来保证这种低概率情况下不出现问题
synchronized (extraLock){
synchronized (from){
synchronized (to){
transportGoodsInternal(from,to,count);
}
}
}
}
}
public void transportGoodsInternal(IStore from,IStore to,int count){
from.outCome(count);//出仓库
to.inCome(count);//入仓库
}
上述代码不难理解,使用hashcode的大小来唯一确定锁的顺序,需要值得注意的是,使用identityHashCode
而不是对象自身的hashCode方法,这样可以降低用户重写hashcode后带来的冲突风险。具体可参考 System.identityHashCode(obj) 与 obj.hashcode()的区别。
当然使用identityHashCode也不能完全避免冲突,当identityHashCode也冲突的时候,引入了额外的一个锁extraLock,这个锁是static的,也就是说,整个应用程序只有一个,虽然理论上整个程序都使用一个 extraLock可能会导致并发性能的下降,但是考虑实际情况下,identityHashCode冲突的可能性非常小,所以并发性能问题也将不是问题。
那么如果能从业务的层面找到IStore中唯一的,不可变的编码,例如,仓库在数据库中的唯一编码,就可以不使用hashcode了,也可以避免使用extraLock。当然这需要大家通过实际的业务逻辑来进行分析提取这个唯一编码。
需要注意的是,使用hashcode这种方式是兼容性最好,成本最低也最不容易出错的方式,如果使用自有编码,你需要确保编码的唯一性,不可变性,这要保证这一点很不容易。
之前讨论的死锁发生在一个对象内部,这样的死锁问题,比较明显,也容易发现。当互相调用的类变为两个或者更多,而两个类中又分别有各自加锁同步的逻辑,这样的死锁隐藏在代码逻辑中,不容易发现,也不容易寻找。
首先来看一个例子。
/**
* 玩游戏者
*/
class Player {
private SystemMonitor monitor;
private int cardCount;//收集的卡片的数量
public Player(SystemMonitor monitor) {
this.monitor = monitor;
}
public synchronized int getCardCount() {
return cardCount;
}
public synchronized void collectCard(int count){
cardCount += count;
if(cardCount >= 50){
monitor.notifyComplete(this);
}
}
}
/**
* 监控系统
*/
class SystemMonitor {
private ArrayList<Player> playerArrayList;//所有玩家
private ArrayList<Player> completePlayerArrayList = new ArrayList<>();//完成的玩家
//通知监控系统完成
public synchronized void notifyComplete(Player player){
System.out.println("玩家完成收集");
completePlayerArrayList.add(player);
}
//实时监控大家手中牌的数量
public synchronized void monitorAllPlayer(){
for (Player player : playerArrayList){
System.out.println("玩家有"+ player.getCardCount() + "张牌");
}
}
}
Player代表玩家,玩家收集完成,50张牌后通知监控系统自己完成游戏,而监控系统通过monitorAllPlayer来实时监控玩家目前手中的牌的数量。
不难理解,在Player和SystemMonitor的方法中加锁,是为了避免数据的不一致性。粗略看这一段代码时,没有任何方法会显式的获取两个锁。但是collectCard方法与monitorAllPlayer方法由于调用了外部类的方法,所以他们其实是会拥有两个锁的。
假设这样一种情形,当一个玩家收集满50张牌,他通知监控系统他已完成收集,玩家先后获取了Player对象的锁与SystemMonitor对象的锁,而这个时候,监控系统正在扫描所有玩家,而监控系统会先获取自身的锁,然后再获取玩家的锁。
这样就有可能出现在两个线程中获取锁顺序不一致的情况,因此就有可能产生死锁。
当一个对象的方法在持有锁期间调用外部方法,这时应该格外注意,因为无法显式判断外部方法是否有其他锁,而这样就有可能产生死锁。
针对上述描述,该如何避免死锁呢?
首先引入一个术语开放调用,即调用某个方法的时候,不需要持有锁,这种调用称为开放调用。通过尽可能地使用开放调用,更容易找出其他锁的路径,也更容易保证加锁的顺序,以此来避免死锁问题。
上述的代码很容易修改为开放调用,此时需要做的就是缩小锁的粒度,使得同步方法只用来保护真正需要保护的变量或者代码段。
public void collectCard(int count){
boolean isComplete = false;
synchronized (this){
cardCount += count;
if(cardCount >= 50){
isComplete = true;
}
}
if(isComplete){
monitor.notifyComplete(this);
}
}
//实时监控大家手中牌的数量
public void monitorAllPlayer(){
ArrayList<Player> copy;
synchronized (this){
copy = new ArrayList<>(playerArrayList);
}
for (Player player : copy){
System.out.println("玩家有"+ player.getCardCount() + "张牌");
}
}
在线程池中,如果任务依赖于其他任务,就可能产生死锁。举一个简单的单线程Executor的例子,如果任务A已经在Executor中运行,而任务A又向相同的Executor中提交了一个任务B,通常情况下,这样会产生死锁。
任务B在队列中一直等待任务A完成,而任务A由于是在单线程Executor中,所以又在等待任务B执行完成,这样就造成了死锁。在更大的线程池中,考虑极限情况,如果所有正在执行任务的线程,都在等待之前提交到线程池中排队的任务,这样线程会永远等待下去,这种问题称为线程饥饿死锁。
下面的代码展示了线程饥饿死锁。
private ThreadPoolExecutor executor = new ThreadPoolExecutor(5,5,0,TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());;
@Test
public void test() throws ExecutionException, InterruptedException {
int count = 0;
while (true) {
System.out.println("开始 = " + (count));
start();
System.out.println("结束 = " + (count++));
Thread.sleep(10);
}
}
public void start() throws ExecutionException, InterruptedException {
Callable<String> second = new Callable<String>() {
@Override
public String call() throws Exception {
Thread.sleep(100);
return "second callable";
}
};
Callable<String> first = new Callable<String>() {
@Override
public String call() throws Exception {
Thread.sleep(10);
Future<String> secondFuture = executor.submit(second);
String secondResult = secondFuture.get();
Thread.sleep(10);
return "first callable. second result = " + secondResult;
}
};
List<Future<String>> futures = new ArrayList<>();
for(int i = 0; i< 5; i++){
System.out.println("submit : " + i);
Future<String> firstFuture = executor.submit(first);
futures.add(firstFuture);
}
for(int i = 0; i < 5; i++){
String firstrResult = futures.get(i).get();
System.out.println(firstrResult + ":" + i);
}
}
三、Android系统处理死锁方案
Android系统的Framework层有一个WatchDog用于定期检测关键系统服务是否发生死锁。WatchDog功能主要是分析系统核心服务和重要线程是否处于Blocked状态。
下面我们以Android 9.0为例分析WatchDog的实现原理。通过分析源码,也可以给自己实现一套死锁监控提供一些思路。源码见:WatchDog
看源码之前,可以先自己思考下,如果让我们去实现一个WatchDog,我们会如何设计。其实原理倒是不难,无外乎需要做两件事情。
其实WatchDog也是这么设计的。WatchDog是继承自Thread,那么我们分析它的工作流程也就从run方法开始吧。
为了方便代码展示,下面源码只保留一些关键代码。run方法是整个检测的核心,我在代码片段里面标注了“代码关键点*”字样,方便在文中引用定位。
public void run() {
boolean waitedHalf = false;
while (true) {//我们要在Android系统运行的整个过程中监控,当然我们需要一个死循环
final List<HandlerChecker> blockedCheckers;
final String subject;
final boolean allowRestart;
int debuggerWasConnected = 0;
synchronized (this) {
long timeout = CHECK_INTERVAL;
//代码关键点1
for (int i=0; i<mHandlerCheckers.size(); i++) {
HandlerChecker hc = mHandlerCheckers.get(i);
hc.scheduleCheckLocked();
}
....
//代码关键点2
long start = SystemClock.uptimeMillis();
while (timeout > 0) {
...
try {
wait(timeout);
} catch (InterruptedException e) {
Log.wtf(TAG, e);
}
...
timeout = CHECK_INTERVAL - (SystemClock.uptimeMillis() - start);
}
...
//代码关键点3
boolean fdLimitTriggered = false;
if (mOpenFdMonitor != null) {
fdLimitTriggered = mOpenFdMonitor.monitor();
}
//代码关键点4
if (!fdLimitTriggered) {
final int waitState = evaluateCheckerCompletionLocked();
if (waitState == COMPLETED) {
waitedHalf = false;
continue;
} else if (waitState == WAITING) {
continue;
} else if (waitState == WAITED_HALF) {
if (!waitedHalf) {
ArrayList<Integer> pids = new ArrayList<Integer>();
pids.add(Process.myPid());
ActivityManagerService.dumpStackTraces(true, pids, null, null,
getInterestingNativePids());
waitedHalf = true;
}
continue;
}
blockedCheckers = getBlockedCheckersLocked();
subject = describeCheckersLocked(blockedCheckers);
} else {
blockedCheckers = Collections.emptyList();
subject = "Open FD high water mark reached";
}
allowRestart = mAllowRestart;
}
//代码关键点5
//代码运行到这里,说明系统已经卡死
final File stack = ActivityManagerService.dumpStackTraces(
!waitedHalf, pids, null, null, getInterestingNativePids());
doSysRq('w');
doSysRq('l');
...
IActivityController controller;
if (controller != null) {
...
int res =controller.systemNotResponding(subject);
if (res >= 0) {
...
continue;
}
}
// 代码关键点6
if (Debug.isDebuggerConnected()) {
debuggerWasConnected = 2;
}
if (debuggerWasConnected >= 2) {
} else if (debuggerWasConnected > 0) {
} else if (!allowRestart) {
} else {//只有这种情况下,杀死system_server
...
//代码关键点6
Process.killProcess(Process.myPid());
System.exit(10);
}
waitedHalf = false;
}
}
整个run方法是一个死循环,这也是可以理解的,毕竟WatchDog需要在Android系统的整个运行期间进行监测。
在“代码关键点1”这里,通过遍历所有需要检测的线程,需要检测的线程集合是在WatchDog的构造函数中初始化的。
private Watchdog() {
super("watchdog");
...
mMonitorChecker = new HandlerChecker(FgThread.getHandler(),
"foreground thread", DEFAULT_TIMEOUT);
mHandlerCheckers.add(mMonitorChecker);
mHandlerCheckers.add(new HandlerChecker(new Handler(Looper.getMainLooper()),
"main thread", DEFAULT_TIMEOUT));
mHandlerCheckers.add(new HandlerChecker(UiThread.getHandler(),
"ui thread", DEFAULT_TIMEOUT));
mHandlerCheckers.add(new HandlerChecker(IoThread.getHandler(),
"i/o thread", DEFAULT_TIMEOUT));
mHandlerCheckers.add(new HandlerChecker(DisplayThread.getHandler(),
"display thread", DEFAULT_TIMEOUT));
addMonitor(new BinderThreadMonitor());
mOpenFdMonitor = OpenFdMonitor.create();//这个monitor有额外作用,后面我们会有提到
...
}
WatchDog构造函数中,初始化了我们要监控的系统线程。包含FgThread,主线程,UiThread,IoThread,DisplayThread,Binder通信线程。 (需要着重说明的是监控FgThread的mMonitorChecker通过向外部暴露接口,通过调用WatchDog的addMonitor方法,来监控所有实现了Monitor接口的服务。)
public void addMonitor(Monitor monitor) {
....
mMonitorChecker.addMonitor(monitor);
}
}
代码中的HandlerChecker便是今天的主角之一,它的主要作用就是用来检测线程是否卡死。在“代码关键点1”的循环中,调用了scheduleCheckLocked,而这个方法是HandlerChecker的核心。
下面HandlerChecker代码片段,这个方法通过postAtFrontOfQueue向被监控线程的Handler消息队列的头部插入当前HandlerChecker,如果被监控线程消息执行正常,则会回调HandlerChecker的run方法,在run方法里面遍历所有Monitor对象(实现Monitor接口的服务很多,包含AMS,WMS,IMS等),执行monitor方法,如果服务正常,最后我们便会将mCompleted置为true。
这个mCompleted变量就是后续WatchDog用来判断对应线程是否卡死依据。
public final class HandlerChecker implements Runnable {
...
private final Handler mHandler;
private final ArrayList<Monitor> mMonitors = new ArrayList<Monitor>();
private boolean mCompleted;
...
public void scheduleCheckLocked() {
if (mMonitors.size() == 0 && mHandler.getLooper().getQueue().isPolling()) {//特殊的条件,需要注意,下面有解释
mCompleted = true;
return;
}
if (!mCompleted) {
return;
}
mCompleted = false;
mCurrentMonitor = null;
mStartTime = SystemClock.uptimeMillis();
mHandler.postAtFrontOfQueue(this);
}
...
@Override
public void run() {
final int size = mMonitors.size();
for (int i = 0 ; i < size ; i++) {
synchronized (Watchdog.this) {
mCurrentMonitor = mMonitors.get(i);
}
mCurrentMonitor.monitor();
}
synchronized (Watchdog.this) {
mCompleted = true;
mCurrentMonitor = null;
}
}
}
scheduleCheckLocked方法中有一个代码引起了我们的注意,如果mHandler.getLooper().getQueue().isPolling()为true,那么直接将mCompleted置为true,这又是什么原理。
通过查阅MessageQueue源码,里面的一段注释解决了我们的迷惑。
Returns whether this looper's thread is currently polling for more work .This is a good signal that the loop is still alive rather than being stuck handling a callback
这段话含义就是isPolling表示正在从队列中取消息,为true则代表Looper依然运行良好,通过这个标记就不需要等待回调来得知状态,这样效率更高。
了解了检测卡死的原理,那我们继续回到WatchDog的run方法,来看“代码关键点2”。通过wait方法实现了每30s检测一次的效果,这里看到了Google工程师的一个小技巧,由于wait的timeout时间可能没那么准确,为了保证至少等待30s,使用了一个while循环,并且循环完毕通过timeout = CHECK_INTERVAL - (SystemClock.uptimeMillis() - start);来保证时间够30s。
“代码关键点3”中使用了OpenFdMonitor,这个类的主要作用是为了判断剩余可用文件句柄的数量,大家知道Linux中打开文件都需要分配文件句柄,系统的文件句柄数量是有限制的。当然这个OpenFdMonitor只在编译模式为userdebug 和 eng的Android编译版本起作用,这也是为了方便开发人员调试信息。
“代码关键点4”中evaluateCheckerCompletionLocked便是用来评估当前所有线程的卡死情况
private int evaluateCheckerCompletionLocked() {
int state = COMPLETED;
for (int i=0; i<mHandlerCheckers.size(); i++) {
HandlerChecker hc = mHandlerCheckers.get(i);
state = Math.max(state, hc.getCompletionStateLocked());
}
return state;
}
代码获取了当前线程中状态值最大的state;state的定义如下:
COMPLETED = 0;已完成,不存在卡死情况
WAITING = 1; 等待时间小于DEFAULT_TIMEOUT的一半,即<30s
WAITED_HALF = 2; 等待时间超过DEFAULT_TIMEOUT的一半,即>=30s
OVERDUE = 3;等待时间大于等于DEFAULT_TIMEOUT ,即 >=60s
如果有线程状态已经是OVERDUE,那么说明被监控的线程有卡死情况。我们的流程也来到了“代码关键点5”。这里就比较好理解了,通过dumpStackTraces输出kernel栈信息,通过doSysRq触发系统dump所有阻塞线程堆栈。这样所有相关的信息就保存好了。
“ 代码关键点6”中,以下几种情况,即使触发了WatchDog,也不杀死系统进程。
debuggerWasConnected>=0 debuggerWasConnected>=2 代表debugger正在连接调试中
allowRestart设置为true,是通过adb logcat am hang命令设置的
最后通过下面两行代码将SystemServer进程杀死,当system_server被杀后,就会导致Zygote进程自杀,进而做到Zygote进程的重启。而这个现象也就是我们平常看到了手机死机了,然后又自动重启的现象。
Process.killProcess(Process.myPid());
System.exit(10);
四、Android开发过程中死锁分析方法
分析完系统如何处理死锁情况后,我们再来看看在Android开发中最容易碰到的死锁表现形式ANR。
当然产生ANR的原因很多,死锁只是其中一种。如果ANR发生,对应的应用会收到SIGQUIT异常终止信号,dalvik虚拟机就会自动在/data/anr/目录下生成trace.txt(Android8.1以后文件名不是这个了)文件,这个文件记录了在发生ANR时刻系统各个线程的执行状态,trace文件中记录的线程执行状态详细描述了各个线程加锁等待的情况。
通过分析,就可以相对容易的找到发生死锁所在的线程及代码。
主线程死锁导致的问题,可以通过ANR的trace文件分析,如果是非主线程呢,这种死锁一般很难察觉,但是这种死锁有时候也会造成很严重的后果,因为线程可能一直在占用某些资源,比如端口,数据库连接,文件句柄等。对于普通的java程序,JVM提供了jstack工具,可以将线程信息dump出来进行分析。
由于Android系统中没有提供类似jstack的工具, 这里笔者给大家提供两种方法来检查是否有线程发生死锁。 1.借助Android Studio的调试工具
首先通过工具栏Run->Attach to Process
或者快捷入口 ,将App的进程attach进去。
然后在Android Debugger窗口中,找到Get Thread Dump按钮,点击后,稍等片刻,Androd Studio就会将对应调试进程的线程堆栈信息dump出来。
下图就是得到的线程信息,这样就可以分析线程中的死锁了。
Android应用发生ANR时,系统会发出SIGQUIT信号给发生ANR的进程。利用系统这个机制,当监控线程发现被监控线程卡死时,主动向系统发送SIGQUIT信号,等待/data/anr/traces.txt文件生成。这样可以得到一个和ANR日志相同的线程堆栈信息,这样分析死锁的问题就和之前分析ANR那个trace文件就一样了。
下面我举一个具体例子来看看如何借助发送SIGQUIT信号来生成trace文件。
首先我们通过ps命令拿到我们进程的进程id-
adb shell ps | grep com.sohu.sohuvideo
这取主进程 id : 22841,执行如下命令
adb shell run-as com.sohu.sohuvideo kill -3 22841
紧接着会在logcat中输出日志 com.sohu.sohuvideo I/.sohu.sohuvide: Wrote stack traces to '[tombstoned]',这时我们的trace文件便已经生成好了(这里需要注意Android8.1之前输出的日志为 Wrote stack traces to traces.txt)。
在这里我们需要注意,run-as 命令需要在debug包下面才管用,如果是Release包则不行。
如果是Android8.1之前的系统那么我们就可以愉快的通过 adb pull /data/anr/traces.txt 命令直接将文件拿到了,但是8.1之后trace文件便没有权限直接可以拿到了。这里我们可能想到了用adb shell bugreport命令来导出trace文件,但是当我们兴奋的打开bugreport文件,找到anr文件夹,却发现里面只有app真正发生anr时候的trace文件,却没有我们刚刚用命令执行完毕后生成的文件。
通过adb shell直接进入手机目录查看发现,该目录下有我们刚刚生成的文件dumptrace_YbVvLP,只不过bugreport没有将其导出。
最后经过一番探索终于找到一个途径,这样绕过了系统的权限,终于将我们自己生产的trace文件导出了。
adb shell cat /data/anr/dumptrace_YbVvLP > ~/Desktop/dump
结束语
死锁问题是一个老大难问题,而且只要有死锁,一般都会引起严重的后果,我们需要不断强化自己的编程能力,写代码的过程中遇到多线程加锁同步的问题,多思考是否会产生死锁,只有多思考,多实践,才能将死锁问题发生的频率降到最低。
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为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 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。