纠纷奇闻社交美文家庭
投稿投诉
家庭城市
爱好生活
创业男女
能力餐饮
美文职业
心理周易
母婴奇趣
两性技能
社交传统
新闻范文
工作个人
思考社会
作文职场
家居中考
兴趣安全
解密魅力
奇闻笑话
写作笔记
阅读企业
饮食时事
纠纷案例
初中历史
说说童话
乐趣治疗

ThreadLocal夺命11连问

1月13日 暗影泪投稿
  前言
  前一段时间,有同事使用ThreadLocal踩坑了,正好引起了我的兴趣。
  所以近期,我抽空把ThreadLocal的源码再研究了一下,越看越有意思,发现里面的东西还真不少。
  我把精华浓缩了一下,汇集成了下面11个问题,看看你能顶住第几个?
  1。为什么要用ThreadLocal?
  并发编程是一项非常重要的技术,它让我们的程序变得更加高效。
  但在并发的场景中,如果有多个线程同时修改公共变量,可能会出现线程安全问题,即该变量最终结果可能出现异常。
  为了解决线程安全问题,JDK出现了很多技术手段,比如:使用synchronized或Lock,给访问公共资源的代码上锁,保证了代码的原子性。
  但在高并发的场景中,如果多个线程同时竞争一把锁,这时会存在大量的锁等待,可能会浪费很多时间,让系统的响应时间一下子变慢。
  因此,JDK还提供了另外一种用空间换时间的新思路:ThreadLocal。
  它的核心思想是:共享变量在每个线程都有一个副本,每个线程操作的都是自己的副本,对另外的线程没有影响。
  例如:ServicepublicclassThreadLocalService{privatestaticfinalThreadLocalIntegerthreadLocalnewThreadLocal();publicvoidadd(){threadLocal。set(1);doSamething();IntegerintegerthreadLocal。get();}}2。ThreadLocal的原理是什么?
  为了搞清楚ThreadLocal的底层实现原理,我们不得不扒一下源码。
  ThreadLocal的内部有一个静态的内部类叫:ThreadLocalMap。publicclassThreadLocalT{。。。publicTget(){获取当前线程ThreadtThread。currentThread();获取当前线程的成员变量ThreadLocalMap对象ThreadLocalMapmapgetMap(t);if(map!null){根据threadLocal对象从map中获取Entry对象ThreadLocalMap。Entryemap。getEntry(this);if(e!null){SuppressWarnings(unchecked)获取保存的数据Tresult(T)e。}}初始化数据returnsetInitialValue();}privateTsetInitialValue(){获取要初始化的数据TvalueinitialValue();获取当前线程ThreadtThread。currentThread();获取当前线程的成员变量ThreadLocalMap对象ThreadLocalMapmapgetMap(t);如果map不为空if(map!null)将初始值设置到map中,key是this,即threadLocal对象,value是初始值map。set(this,value);else如果map为空,则需要创建新的map对象createMap(t,value);}publicvoidset(Tvalue){获取当前线程ThreadtThread。currentThread();获取当前线程的成员变量ThreadLocalMap对象ThreadLocalMapmapgetMap(t);如果map不为空if(map!null)将值设置到map中,key是this,即threadLocal对象,value是传入的value值map。set(this,value);else如果map为空,则需要创建新的map对象createMap(t,value);}staticclassThreadLocalMap{。。。}。。。}
  ThreadLocal的get方法、set方法和setInitialValue方法,其实最终操作的都是ThreadLocalMap类中的数据。
  其中ThreadLocalMap类的内部如下:staticclassThreadLocalMap{staticclassEntryextendsWeakReferenceThreadL?{OEntry(ThreadL?k,Objectv){super(k);}}。。。privateEntry〔〕。。。}
  ThreadLocalMap里面包含一个静态的内部类Entry,该类继承于WeakReference类,说明Entry是一个弱引用。
  ThreadLocalMap内部还包含了一个Entry数组,其中:EntryThreadLocalvalue。
  而ThreadLocalMap被定义成了Thread类的成员变量。publicclassThreadimplementsRunnable{。。。ThreadLocal。ThreadLocalMapthreadL}
  下面用一张图从宏观上,认识一下ThreadLocal的整体结构:
  从上图中看出,在每个Thread类中,都有一个ThreadLocalMap的成员变量,该变量包含了一个Entry数组,该数组真正保存了ThreadLocal类set的数据。
  Entry是由threadLocal和value组成,其中threadLocal对象是弱引用,在GC的时候,会被自动回收。而value就是ThreadLocal类set的数据。
  下面用一张图总结一下引用关系:
  上图中除了Entry的key对ThreadLocal对象是弱引用,其他的引用都是强引用。
  需要特别说明的是,上图中ThreadLocal对象我画到了堆上,其实在实际的业务场景中不一定在堆上。因为如果ThreadLocal被定义成了static的,ThreadLocal的对象是类共用的,可能出现在方法区。3。为什么用ThreadLocal做key?
  不知道你有没有思考过这样一个问题:ThreadLocalMap为什么要用ThreadLocal做key,而不是用Thread做key?
  如果在你的应用中,一个线程中只使用了一个ThreadLocal对象,那么使用Thread做key也未尝不可。ServicepublicclassThreadLocalService{privatestaticfinalThreadLocalIntegerthreadLocalnewThreadLocal();}
  但实际情况中,你的应用,一个线程中很有可能不只使用了一个ThreadLocal对象。这时使用Thread做key不就出有问题?ServicepublicclassThreadLocalService{privatestaticfinalThreadLocalIntegerthreadLocal1newThreadLocal();privatestaticfinalThreadLocalIntegerthreadLocal2newThreadLocal();privatestaticfinalThreadLocalIntegerthreadLocal3newThreadLocal();}
  假如使用Thread做key时,你的代码中定义了3个ThreadLocal对象,那么,通过Thread对象,它怎么知道要获取哪个ThreadLocal对象呢?
  如下图所示:
  因此,不能使用Thread做key,而应该改成用ThreadLocal对象做key,这样才能通过具体ThreadLocal对象的get方法,轻松获取到你想要的ThreadLocal对象。
  如下图所示:
  4。Entry的key为什么设计成弱引用?
  前面说过,Entry的key,传入的是ThreadLocal对象,使用了WeakReference对象,即被设计成了弱引用。
  那么,为什么要这样设计呢?
  假如key对ThreadLocal对象的弱引用,改为强引用。
  我们都知道ThreadLocal变量对ThreadLocal对象是有强引用存在的。
  即使ThreadLocal变量生命周期完了,设置成null了,但由于key对ThreadLocal还是强引用。
  此时,如果执行该代码的线程使用了线程池,一直长期存在,不会被销毁。
  就会存在这样的强引用链:Thread变量Thread对象ThreadLocalMapEntrykeyThreadLocal对象。
  那么,ThreadLocal对象和ThreadLocalMap都将不会被GC回收,于是产生了内存泄露问题。
  为了解决这个问题,JDK的开发者们把Entry的key设计成了弱引用。
  弱引用的对象,在GC做垃圾清理的时候,就会被自动回收了。
  如果key是弱引用,当ThreadLocal变量指向null之后,在GC做垃圾清理的时候,key会被自动回收,其值也被设置成null。
  如下图所示:
  接下来,最关键的地方来了。
  由于当前的ThreadLocal变量已经被指向null了,但如果直接调用它的get、set或remove方法,很显然会出现空指针异常。因为它的生命已经结束了,再调用它的方法也没啥意义。
  此时,如果系统中还定义了另外一个ThreadLocal变量b,调用了它的get、set或remove,三个方法中的任何一个方法,都会自动触发清理机制,将key为null的value值清空。
  如果key和value都是null,那么Entry对象会被GC回收。如果所有的Entry对象都被回收了,ThreadLocalMap也会被回收了。
  这样就能最大程度的解决内存泄露问题。
  需要特别注意的地方是:key为null的条件是,ThreadLocal变量指向null,并且key是弱引用。如果ThreadLocal变量没有断开对ThreadLocal的强引用,即ThreadLocal变量没有指向null,GC就贸然的把弱引用的key回收了,不就会影响正常用户的使用?如果当前ThreadLocal变量指向null了,并且key也为null了,但如果没有其他ThreadLocal变量触发get、set或remove方法,也会造成内存泄露。
  下面看看弱引用的例子:publicstaticvoidmain(String〔〕args){WeakReferenceObjectweakReference0newWeakReference(newObject());System。out。println(weakReference0。get());System。gc();System。out。println(weakReference0。get());}
  打印结果:java。lang。Object1ef7fe8enull
  传入WeakReference构造方法的是直接new处理的对象,没有其他引用,在调用gc方法后,弱引用对象会被自动回收。
  但如果出现下面这种情况:publicstaticvoidmain(String〔〕args){ObjectobjectnewObject();WeakReferenceObjectweakReference1newWeakReference(object);System。out。println(weakReference1。get());System。gc();System。out。println(weakReference1。get());}
  执行结果:java。lang。Object1ef7fe8ejava。lang。Object1ef7fe8e
  先定义了一个强引用object对象,在WeakReference构造方法中将object对象的引用作为参数传入。这时,调用gc后,弱引用对象不会被自动回收。
  我们的Entry对象中的key不就是第二种情况吗?在Entry构造方法中传入的是ThreadLocal对象的引用。
  如果将object强引用设置为null:publicstaticvoidmain(String〔〕args){ObjectobjectnewObject();WeakReferenceObjectweakReference1newWeakReference(object);System。out。println(weakReference1。get());System。gc();System。out。println(weakReference1。get());System。gc();System。out。println(weakReference1。get());}
  执行结果:java。lang。Object6f496d9fjava。lang。Object6f496d9fnull
  第二次gc之后,弱引用能够被正常回收。
  由此可见,如果强引用和弱引用同时关联一个对象,那么这个对象是不会被GC回收。也就是说这种情况下Entry的key,一直都不会为null,除非强引用主动断开关联。
  此外,你可能还会问这样一个问题:Entry的value为什么不设计成弱引用?
  答:Entry的value不只是被Entry引用,有可能被业务系统中的很多地方引用了。如果value改成了弱引用,被GC贸然回收了(数据突然没了),可能会导致业务系统出现异常。
  而相比之下,Entry的key,引用的地方就非常明确了。
  这就是Entry的key被设计成弱引用,而value没被设计成弱引用的原因。5。ThreadLocal真的会导致内存泄露?
  通过上面的Entry对象中的key设置成弱引用,并且使用get、set或remove方法清理key为null的value值,就能彻底解决内存泄露问题?
  答案是否定的。
  如下图所示:
  假如ThreadLocalMap中存在很多key为null的Entry,但后面的程序,一直都没有调用过有效的ThreadLocal的get、set或remove方法。
  那么,Entry的value值一直都没被清空。
  所以会存在这样一条强引用链:Thread变量Thread对象ThreadLocalMapEntryvalueObject。
  其结果就是:Entry和ThreadLocalMap将会长期存在下去,会导致内存泄露。6。如何解决内存泄露问题?
  前面说过的ThreadLocal还是会导致内存泄露的问题,我们有没有解决办法呢?
  答:有办法,调用ThreadLocal对象的remove方法。
  不是在一开始就调用remove方法,而是在使用完ThreadLocal对象之后。列如:
  先创建一个CurrentUser类,其中包含了ThreadLocal的逻辑。publicclassCurrentUser{privatestaticfinalThreadLocalUserInfoTHREALOCALnewThreadLocal();publicstaticvoidset(UserInfouserInfo){THREALOCAL。set(userInfo);}publicstaticUserInfoget(){THREALOCAL。get();}publicstaticvoidremove(){THREALOCAL。remove();}}
  然后在业务代码中调用相关方法:publicvoiddoSamething(UserDtouserDto){UserInfouserInfoconvert(userDto);try{CurrentUser。set(userInfo);。。。业务代码UserInfouserInfoCurrentUser。get();。。。}finally{CurrentUser。remove();}}
  需要我们特别注意的地方是:一定要在finally代码块中,调用remove方法清理没用的数据。如果业务代码出现异常,也能及时清理没用的数据。
  remove方法中会把Entry中的key和value都设置成null,这样就能被GC及时回收,无需触发额外的清理机制,所以它能解决内存泄露问题。7。ThreadLocal是如何定位数据的?
  前面说过ThreadLocalMap对象底层是用Entry数组保存数据的。
  那么问题来了,ThreadLocal是如何定位Entry数组数据的?
  在ThreadLocal的get、set、remove方法中都有这样一行代码:intikey。threadLocalHashCode(len1);
  通过key的hashCode值,与数组的长度减1。其中key就是ThreadLocal对象,与数组的长度减1,相当于除以数组的长度减1,然后取模。
  这是一种hash算法。
  接下来给大家举个例子:假设len16,key。threadLocalHashCode31,
  于是:inti31151
  相当于:inti31151
  计算的结果是一样的,但是使用与运算效率跟高一些。
  为什么与运算效率更高?
  答:因为ThreadLocal的初始大小是16,每次都是按2倍扩容,数组的大小其实一直都是2的n次方。这种数据有个规律就是高位是0,低位都是1。在做与运算时,可以不用考虑高位,因为与运算的结果必定是0。只需考虑低位的与运算,所以效率更高。
  如果使用hash算法定位具体位置的话,就可能会出现hash冲突的情况,即两个不同的hashCode取模后的值相同。
  ThreadLocal是如何解决hash冲突的呢?
  我们看看getEntry是怎么做的:privateEntrygetEntry(ThreadL?key){通过hash算法获取下标值intikey。threadLocalHashCode(table。length1);Entryetable〔i〕;如果下标位置上的key正好是我们所需要寻找的keyif(e!nulle。get()key)说明找到数据了,直接返回else说明出现hash冲突了,继续往后找returngetEntryAfterMiss(key,i,e);}
  再看看getEntryAfterMiss方法:privateEntrygetEntryAfterMiss(ThreadL?key,inti,Entrye){Entry〔〕intlentab。判断Entry对象如果不为空,则一直循环while(e!null){ThreadL?ke。get();如果当前Entry的key正好是我们所需要寻找的keyif(kkey)说明这次真的找到数据了if(knull)如果key为空,则清理脏数据expungeStaleEntry(i);else如果还是没找到数据,则继续往后找inextIndex(i,len);etab〔i〕;}}
  关键看看nextIndex方法:privatestaticintnextIndex(inti,intlen){return((i1len)?i1:0);}
  当通过hash算法计算出的下标小于数组大小,则将下标值加1。否则,即下标大于等于数组大小,下标变成0了。下标变成0之后,则循环一次,下标又变成1
  寻找的大致过程如下图所示:
  如果找到最后一个,还是没有找到,则再从头开始找。
  不知道你有没有发现,它构成了一个:环形。
  ThreadLocal从数组中找数据的过程大致是这样的:通过key的hashCode取余计算出一个下标。通过下标,在数组中定位具体Entry,如果key正好是我们所需要的key,说明找到了,则直接返回数据。如果第2步没有找到我们想要的数据,则从数组的下标位置,继续往后面找。如果第3步中找key的正好是我们所需要的key,说明找到了,则直接返回数据。如果还是没有找到数据,再继续往后面找。如果找到最后一个位置,还是没有找到数据,则再从头,即下标为0的位置,继续从前往后找数据。直到找到第一个Entry为空为止。8。ThreadLocal是如何扩容的?
  从上面得知,ThreadLocal的初始大小是16。那么问题来了,ThreadLocal是如何扩容的?
  在set方法中会调用rehash方法:privatevoidset(ThreadL?key,Objectvalue){Entry〔〕intlentab。intikey。threadLocalHashCode(len1);for(Entryetab〔i〕;e!etab〔inextIndex(i,len)〕){ThreadL?ke。get();if(kkey){e。}if(knull){replaceStaleEntry(key,value,i);}}tab〔i〕newEntry(key,value);if(!cleanSomeSlots(i,sz)szthreshold)rehash();}
  注意一下,其中有个判断条件是:sz(之前的size1)如果大于或等于threshold的话,则调用rehash方法。
  threshold默认是0,在创建ThreadLocalMap时,调用它的构造方法:ThreadLocalMap(ThreadL?firstKey,ObjectfirstValue){tablenewEntry〔INITIALCAPACITY〕;intifirstKey。threadLocalHashCode(INITIALCAPACITY1);table〔i〕newEntry(firstKey,firstValue);size1;setThreshold(INITIALCAPACITY);}
  调用setThreshold方法给threshold设置一个值,而这个值INITIALCAPACITY是默认的大小16。privatevoidsetThreshold(intlen){thresholdlen23;}
  也就是第一次设置的threshold1623,取整后的值是:10。
  换句话说当sz大于等于10时,就可以考虑扩容了。
  rehash代码如下:privatevoidrehash(){先尝试回收一次key为null的值,腾出一些空间expungeStaleEntries();if(sizethresholdthreshold4)resize();}
  在真正扩容之前,先尝试回收一次key为null的值,腾出一些空间。
  如果回收之后的size大于等于threshold的34时,才需要真正的扩容。
  计算公式如下:162434162348
  也就是说添加数据后,新的size大于等于老size的12时,才需要扩容。privatevoidresize(){Entry〔〕oldTintoldLenoldTab。按2倍的大小扩容intnewLenoldLen2;Entry〔〕newTabnewEntry〔newLen〕;intcount0;for(intj0;joldLj){EntryeoldTab〔j〕;if(e!null){ThreadL?ke。get();if(knull){e。HelptheGC}else{inthk。threadLocalHashCode(newLen1);while(newTab〔h〕!null)hnextIndex(h,newLen);newTab〔h〕e;}}}setThreshold(newLen);tablenewT}
  resize中每次都是按2倍的大小扩容。
  扩容的过程如下图所示:
  扩容的关键步骤如下:老size1新size如果新size大于等于老size的23时,需要考虑扩容。扩容前先尝试回收一次key为null的值,腾出一些空间。如果回收之后发现size还是大于等于老size的12时,才需要真正的扩容。每次都是按2倍的大小扩容。9。父子线程如何共享数据?
  前面介绍的ThreadLocal都是在一个线程中保存和获取数据的。
  但在实际工作中,有可能是在父子线程中共享数据的。即在父线程中往ThreadLocal设置了值,在子线程中能够获取到。
  例如:publicclassThreadLocalTest{publicstaticvoidmain(String〔〕args){ThreadLocalIntegerthreadLocalnewThreadLocal();threadLocal。set(6);System。out。println(父线程获取数据:threadLocal。get());newThread((){System。out。println(子线程获取数据:threadLocal。get());})。start();}}
  执行结果:父线程获取数据:6子线程获取数据:null
  你会发现,在这种情况下使用ThreadLocal是行不通的。main方法是在主线程中执行的,相当于父线程。在main方法中开启了另外一个线程,相当于子线程。
  显然通过ThreadLocal,无法在父子线程中共享数据。
  那么,该怎么办呢?
  答:使用InheritableThreadLocal,它是JDK自带的类,继承了ThreadLocal类。
  修改代码之后:publicclassThreadLocalTest{publicstaticvoidmain(String〔〕args){InheritableThreadLocalIntegerthreadLocalnewInheritableThreadLocal();threadLocal。set(6);System。out。println(父线程获取数据:threadLocal。get());newThread((){System。out。println(子线程获取数据:threadLocal。get());})。start();}}
  执行结果:父线程获取数据:6子线程获取数据:6
  果然,在换成InheritableThreadLocal之后,在子线程中能够正常获取父线程中设置的值。
  其实,在Thread类中除了成员变量threadLocals之外,还有另一个成员变量:inheritableThreadLocals。
  Thread类的部分代码如下:ThreadLocal。ThreadLocalMapthreadLThreadLocal。ThreadLocalMapinheritableThreadL
  最关键的一点是,在它的init方法中会将父线程中往ThreadLocal设置的值,拷贝一份到子线程中。
  感兴趣的小伙伴,可以找我私聊。或者看看我后面的文章,后面还会有专栏。10。线程池中如何共享数据?
  在真实的业务场景中,一般很少用单独的线程,绝大多数,都是用的线程池。
  那么,在线程池中如何共享ThreadLocal对象生成的数据呢?
  因为涉及到不同的线程,如果直接使用ThreadLocal,显然是不合适的。
  我们应该使用InheritableThreadLocal,具体代码如下:privatestaticvoidfun1(){InheritableThreadLocalIntegerthreadLocalnewInheritableThreadLocal();threadLocal。set(6);System。out。println(父线程获取数据:threadLocal。get());ExecutorServiceexecutorServiceExecutors。newSingleThreadExecutor();threadLocal。set(6);executorService。submit((){System。out。println(第一次从线程池中获取数据:threadLocal。get());});threadLocal。set(7);executorService。submit((){System。out。println(第二次从线程池中获取数据:threadLocal。get());});}
  执行结果:父线程获取数据:6第一次从线程池中获取数据:6第二次从线程池中获取数据:6
  由于这个例子中使用了单例线程池,固定线程数是1。
  第一次submit任务的时候,该线程池会自动创建一个线程。因为使用了InheritableThreadLocal,所以创建线程时,会调用它的init方法,将父线程中的inheritableThreadLocals数据复制到子线程中。所以我们看到,在主线程中将数据设置成6,第一次从线程池中获取了正确的数据6。
  之后,在主线程中又将数据改成7,但在第二次从线程池中获取数据却依然是6。
  因为第二次submit任务的时候,线程池中已经有一个线程了,就直接拿过来复用,不会再重新创建线程了。所以不会再调用线程的init方法,所以第二次其实没有获取到最新的数据7,还是获取的老数据6。
  那么,这该怎么办呢?
  答:使用TransmittableThreadLocal,它并非JDK自带的类,而是阿里巴巴开源jar包中的类。
  可以通过如下pom文件引入该jar包:dependencygroupIdcom。alibabagroupIdtransmittablethreadlocalartifactIdversion2。11。0versionscopecompilescopedependency
  代码调整如下:privatestaticvoidfun2()throwsException{TransmittableThreadLocalIntegerthreadLocalnewTransmittableThreadLocal();threadLocal。set(6);System。out。println(父线程获取数据:threadLocal。get());ExecutorServicettlExecutorServiceTtlExecutors。getTtlExecutorService(Executors。newFixedThreadPool(1));threadLocal。set(6);ttlExecutorService。submit((){System。out。println(第一次从线程池中获取数据:threadLocal。get());});threadLocal。set(7);ttlExecutorService。submit((){System。out。println(第二次从线程池中获取数据:threadLocal。get());});}
  执行结果:父线程获取数据:6第一次从线程池中获取数据:6第二次从线程池中获取数据:7
  我们看到,使用了TransmittableThreadLocal之后,第二次从线程中也能正确获取最新的数据7了。
  nice。
  如果你仔细观察这个例子,你可能会发现,代码中除了使用TransmittableThreadLocal类之外,还使用了TtlExecutors。getTtlExecutorService方法,去创建ExecutorService对象。
  这是非常重要的地方,如果没有这一步,TransmittableThreadLocal在线程池中共享数据将不会起作用。
  创建ExecutorService对象,底层的submit方法会TtlRunnable或TtlCallable对象。
  以TtlRunnable类为例,它实现了Runnable接口,同时还实现了它的run方法:publicvoidrun(){MapTransmittableThreadL?,Objectcopied(Map)this。copiedRef。get();if(copied!null(!this。releaseTtlValueReferenceAfterRunthis。copiedRef。compareAndSet(copied,(Object)null))){MapbackupTransmittableThreadLocal。backupAndSetToCopied(copied);try{this。runnable。run();}finally{TransmittableThreadLocal。restoreBackup(backup);}}else{thrownewIllegalStateException(TTLvaluereferenceisreleasedafterrun!);}}
  这段代码的主要逻辑如下:把当时的ThreadLocal做个备份,然后将父类的ThreadLocal拷贝过来。执行真正的run方法,可以获取到父类最新的ThreadLocal数据。从备份的数据中,恢复当时的ThreadLocal数据。11。ThreadLocal有哪些用途?
  最后,一起聊聊ThreadLocal有哪些用途?
  老实说,使用ThreadLocal的场景挺多的。
  下面列举几个常见的场景:在spring事务中,保证一个线程下,一个事务的多个操作拿到的是一个Connection。在hiberate中管理session。在JDK8之前,为了解决SimpleDateFormat的线程安全问题。获取当前登录用户上下文。临时保存权限数据。使用MDC保存日志信息。
  等等,还有很多业务场景,这里就不一一列举了。
  由于篇幅有限,今天的内容先分享到这里。希望你看了这篇文章,会有所收获。
  接下来留几个问题给大家思考一下:ThreadLocal变量为什么建议要定义成static的?Entry数组为什么要通过hash算法计算下标,即直线寻址法,而不直接使用下标值?强引用和弱引用有什么区别?Entry数组大小,为什么是2的N次方?使用InheritableThreadLocal时,如果父线程中重新set值,在子线程中能够正确的获取修改后的新值吗?
  来源:https:mp。weixin。qq。comsxssFckUsXI7tY74zixGQ
投诉 评论 转载

中国用户立大功!库克称iPhone很多技术灵感来自中国【手机中国新闻】提及科技公司,苹果一定是无法忽视的存在。无论是其旗下的手机、iPad等数码产品,还是其研发的各种系统,都是当今科技圈重量级的存在。尤其是iPhone,更是直接带……iPhone14基础版与高端版差别曝光,不止处理器和屏幕,差按照惯例,大约再过一个多月时间,苹果就将发布果粉们翘首以盼的iPhone14系列。届时新的iPhone想必又将引发新一轮的购机狂潮,非常值得广大消费者们期待。而就在最近,……iPhone14Pro约8999起真的假的?2022苹果秋季iPhone14Pro经证实的确是要涨价的,价格的确会比之前的iPhone13pro系列要高,苹果发布会还有一个月将至,大家都比较关注iPhone14Pro的价格,据说是iPh……反转来了,英国延迟拆除华为设备,任正非和平是打出来的工业时代,我们是属于起步比较晚的国家,正因如此,在科技发展的道路上处处受到限制。从电话通信到移动通信,欧美一直都是规则制定者,由于技术落后,我们只能支付高额的专利费用来使……今日钢铁方向美联储加息放缓,钢价不具有持续下跌的基础一、宏观解读1。富宝独家解读:前期钢材价格上涨的核心逻辑有三点,一是海外美联储加息放缓;二是疫情政策出现优化;三是房地产政策出现转向。从最新的信息来看,有两点信息出现变化……普通女生以油养肤一个月,素颜都敢出门了50岁的周海媚,68岁的赵雅芝,为什么能够被称为冻龄女神呢?因为这些女神姐姐都很注重对自己肌肤的保养。今天给各位分享一个我最近在用的以油养肤护符法,最近用它我的皮肤都明显变得光……交警小店二大队幼儿开车拍段子心大家长被传唤前段时间,抖音有这样一条视频,两岁左右的幼儿坐在驾驶人腿上,摆动着方向盘,在驾驶人看来,这样温馨的一幕,实则隐患重重,网友看了也是替当事人捏了一把冷汗。从视频可以看到,一……法新社西甲要求巴黎法院终止姆巴佩与大巴黎合同遭拒直播吧7月23日讯据《队报》援引法国新闻社消息报道,西班牙职业足球联盟(LaLiga)今天要求巴黎行政法院终止姆巴佩和大巴黎的合同。法新社从司法消息人士处得知,该请求由于……ThreadLocal夺命11连问前言前一段时间,有同事使用ThreadLocal踩坑了,正好引起了我的兴趣。所以近期,我抽空把ThreadLocal的源码再研究了一下,越看越有意思,发现里面的东西……鸟语花香,别有洞天大山深处自然美,风景秀丽鸟语花香愿你内心鸟语花香,生动而丰盈初夏时节,风光无限;鸟语花香,心情舒畅;山美水美,美不胜收;天高云淡,活力无限;送份祝福,聊表寸心;愿你……Android卡顿优化,如何做到极致?全方位分析对症下药概述无论是启动,内存,布局等等这些优化,最终的目的就是为了应用不卡顿。应用的体验性好坏,最直观的表现就是应用的流畅程度,用户不知道什么启动优化,内存不足,等等,应用卡顿,……双11人气爆棚,一加AcePro实际口碑如何?博主们这样评价今年的双十一,手机市场要比以往热闹,这次竞争更为激烈,尤其是30003999元价位段,简直就是神仙打架般精彩。其中,3K价位一加AcePro整体表现非常瞩目,京东手机竞速排行榜……
喜欢旅行的你,能否从这款乐高打造的旅行箱里找到旅行的回忆云赏园博四月限定上线,美美美极了的木绣球打包送给你2023年最新国际乒联前10世界排名,小胖莎莎真棒,伊藤美诚三季度外贸发展信心逐步恢复企业出口额和利润双双向好恭喜C罗!曝曼联松口,提1离队条件,马竞探索降薪30租借交易从数学角度理解因果关系和狭义相对论中最重要的间隔不变性定理华为公布远程PC支持设备清单快看看有没有你的电脑篮网109102胜魔术,队史胜利贡献值排行榜基德第二,第一是开学季丨走,带你去江汉路逛逛辽宁VS浙江辽宁领戒指后有输球可能,浙江看孙铭徽串联颜值逆袭!张柏芝黑长直发型不一样,让人眼前一亮宇宙加速膨胀和暗能量的研究
在市团委或县团委工作是种怎样体验?热议聚热点网 我想找找高度近视的感觉再用前韵答继周丈二首其一即事有作三国演义笑话二则三签劳动合同辞职不发工资怎么解决初二关于师恩难忘的作文优秀记忆中的青山初中作文海蛎子炒鸡蛋(海蛎煎蛋的做法)一传造句用一传造句大全梦见棺材里躺着死人会遭遇挫折的一天老人冬季做好哪几点大风天气不感冒

友情链接:中准网聚热点快百科快传网快生活快软网快好知文好找美丽时装彩妆资讯历史明星乐活安卓数码常识驾车健康苹果问答网络发型电视车载室内电影游戏科学音乐整形