Q1:有几种方法可以创建线程?各有什么优缺点?
答案:继承Thread类,重写run()方法即可。优点是编码简单,但缺点是不能继承其他类,功能单一。2实现Runnable接口,重写run()方法,然后将实现类作为参数传递给Thread构造函数。优点是可以通过继承其他类来避免单个继承的限制。适合多个相同程序代码的线程可以共享一个资源(可以将同一个线程工作对象包装成多个线程对象),从而实现解耦操作、代码和线程独立。缺点是实现比较复杂。实现Callable接口,重写call()方法,将其包装到FutureTask对象中,然后作为参数传递给Thread构造函数。优点是,与方法2相比,可以得到返回值。缺点是实施复杂。可以通过线程池制作。
Q2:线程的状态是什么?
使用A: 1 new: NEW操作创建新线程。此时,程序没有开始执行线程的代码。调用Runnable: start()方法后进入可执行状态。Blocked:处于阻塞状态,在获得内部锁(不是juc的锁)失败时进入阻塞状态。Waiting:在等待其他线程醒来的同时进入待机状态。定时排队:定时等待,具有睡眠(长时间)等超时参数。Terminated:退出状态,线程正常工作或未捕获的异常退出。
Q3:什么是线程安全问题,如何解决?
答:当多个线程对同一个共享变量工作时,可能会出现问题。解决方法:使用内部锁定synchronized可以使用同步代码块,如果是实例方法,可以使用this作为锁定对象,如果是静态方法,则可以使用类。您可以使用class作为锁定,也可以使用同步方法基本和同步代码块。对于实例方法,默认情况下使用this作为锁定;对于静态方法,默认情况下使用类。使用Java.u包中的锁(如Reentrantlock)。
Q4:多线程隐形问题的原因和解决方案?
A: 1不可见的原因是每个线程都有自己的工作内存,线程从主内存中复制共享变量的副本值。每个线程在其自己的工作内存中操作共享变量。解决方法:锁定:锁定后线程释放工作内存,从主内存中复制共享变量的最新值,使其成为副本,修改后重新刷新到主内存,解锁。使用Volatile关键字:使用volatile修改的变量通知其他线程以前读取的值已过期,线程将最新值加载到工作内存中。
Q5:谈谈volatile关键词的作用。
A: 1确保修饰后的变量在所有线程中都可见。当一个线程修改变量值时,新值可以立即用于另一个线程。禁止命令顺序更改,修饰的变量不会缓存到寄存器或其他处理器看不见的地方,因此读取volatile修饰的变量时,总是返回最近记录的值。不执行锁定操作,线程不阻塞。主要适用于一个变量由多个线程共享,多个线程可以为该变量分配或读取值的任务。volatile可以严格保证变量单一读写操作的原子性,但不能保证I等操作的原子性。因为I本质上是读、写两次作业。
问6:谈谈同步关键词的作用。
答:1为Java对象、方法和代码块提供线程安全的操作属于悲观锁定,可以再次锁定。2 synchronized修改的方法和代码块一次只能访问一个线程,其他线程必须等到当前线程释放锁定资源后才能访问。Java中的每个对象都有一个monitor监视器对象,锁定在竞争monitor中,代码块锁定通过前后分别添加monitorenter和monitorexit命令来实现,对方的锁定与否以标记位来判断。
Q7: synchronized内部包括哪些区域?
答:synchronized内部包含6个不同的区域,每个区域的数据表示锁定的不同状态。ContentionList:锁定竞争队列并请求锁定的所有线程都放在竞争队列中。EntryList:竞争候选人列表,在锁定竞争队列中,有资格成为竞争锁定资源的候选人的线程将被移到候选人列表中。WaitSet:等待集合,调用wait方法后,被阻止的线程放置在WaitSet中。OnDeck:竞争候选人,一次最多只有一个线程在竞争锁定资源中。此线程的状态称为OnDeck。运营商:对锁资源线程状态的竞争。!Owner:解除锁定后的状态。
Q8:简述同步化的实施原则。
A: 收到新的锁请求时,先旋转,如果通过自旋得不到锁资源,则放入ContentionList。(这对于已经入队的线程是不公平的,反映了同步的不公平。) (阿尔伯特爱因斯坦,Northern Exposure(美国电视剧),成功)为了避免CAS访问受到CAS访问影响,Owner线程受到ContentionList尾部元素较多的线程的影响。
是释放锁时将ContentionList的部分线程移动到EntryList并指定某个线程(一般是最先进入的)为OnDeck线程。Owner并没有将锁直接传递给OnDeck线程而是把锁竞争的权利交给他,该行为叫做竞争切换,牺牲了公平性但提高了性能。③获取到锁的OnDeck线程会变为Owner线程,未获取到的仍停留在EntryList中。④Owner线程在被wait阻塞后会进入WaitSet,直到某个时刻被唤醒再次进入EntryList。⑤ContentionList、EntryList、WaitSet中的线程均为阻塞状态。⑥当Owner线程执行完毕后会释放锁资源并变为!Owner状态。
Q9:JDK对synchronized做了哪些优化?
答:JDK1.6中引入了适应自旋、锁消除、锁粗化、轻量级锁以及偏向锁等以提高锁的效率。锁可以从偏向锁升级到轻量级锁,再升级到重量级锁,这种过程叫做锁膨胀。JDK1.6中默认开启了偏向锁和轻量级锁,可以通过-XX:UseBiasedLocking禁用偏向锁。
Q10:volatile和synchronized的区别?
答:①volatile只能修饰实例变量和类变量,而synchronized可以修饰方法以及代码块。②
volatile只能保证数据的可见性,但是不保证原子性,synchronized是一种排它机制,可以保证原子性。只有在特殊情况下才适合取代synchronized:对变量的写操作不依赖于当前值(例如i++),或者是单纯的变量赋值;该变量没有被包含在具有其他变量的不等式中,不同的volatile变量不能互相依赖,只有在状态真正独立于程序内的其它内容时才能使用volatile。③volatile是一种轻量级的同步机制,在访问volatile修饰的变量时并不会执行加锁操作,线程不会阻塞,使用synchronized加锁会阻塞线程。
Q11:讲一讲ReentrantLock
答:①ReentrantLock是Lock接口的实现类,是一个可重入式的独占锁,通过AQS实现。②支持公平锁与非公平锁,还提供了可响应中断锁(线程在等待锁的过程中可以根据需要取消对锁的请求,通过interrupt方法中断)、可轮询锁(通过tryLock获取锁,如果有可用锁返回true否则立即返回false)、定时锁(通过带long时间参数的tryLock方法获取锁,如果在给定时间内获取到可用锁且当前线程未被中断返回true,如果超过指定时间则返回false,如果获取锁时被终断则抛出异常并清除已终止状态)等避免死锁的方法。③通过lock和unlock方法显式地加锁和释放锁。
Q12:synchronized和ReentrantLock有哪些区别?
答:①synchronized是隐式锁,ReentrantLock是显式锁,使用时必须在finally代码块中进行释放锁的操作。②synchronized是非公平锁,ReentrantLock可以实现公平锁。③ReentrantLock可响应中断,可轮回,为处理锁提高了更多灵活性。④synchronized是一个关键字,是JVM级别,ReentrantLock是一个接口,是API级别。⑤synchronized采用悲观并发策略,ReentrantLock采用的是乐观并发策略,会先尝试以CAS方式获取锁。
Q13:Lock接口有哪些方法?
答:①lock():给对象加锁。②tryLock()/tryLock(long time,TimeUnit unit):尝试给对象加锁,成功返回true,可以无参也可以指定等待时间。③unlock():释放锁,锁只能由持有者释放否则抛出异常。④newCondition():创建条件对象,使用条件对象管理那些已经获得锁但不满足有效条件的线程,调用await()方法把线程进入等待集,调用sign()/signAll()解除阻塞。⑤lockInterruptibly():如果当前线程未被中断则获取该锁。
Q14:Java中的锁有什么作用?有哪些分类?
答:①Java中的锁主要用于保障多并发情况下数据的一致性,线程必须先获取锁才能进行操作,可以保证数据的安全。②从乐观和悲观的角度可以分为乐观锁和悲观锁。③从获取资源的公平性可以分为公平锁和非公平锁。④从是否共享资源的角度可以分为共享锁和排它锁。⑤从锁的状态角度可分为偏向锁、轻量级锁和重量级锁。同时在JVM中还设计了自旋锁以更快地使用CPU资源。
Q15:讲一讲乐观锁和悲观锁
答:①乐观锁采用乐观的思想处理数据,在每次读取数据时都认为别人不会修改该数据,所以不会上锁。但在更新时会判断在此期间别人有没有更新该数据,通常采用在写时先读出当前版本号然后加锁的方法,具体过程为:比较当前版本号与上一次的版本号,如果一致则更新,否则重复进行读、比较、写操作。Java中的乐观锁是基于CAS操作实现的,CAS是一种原子性操作,在对数据更新之前先比较当前值和传入的值是否一样,一样则更新否则直接返回失败状态。②悲观锁采用悲观的思想处理数据,每次读取数据时都认为别人会修改数据,所以每次都会上锁,其他线程将被阻塞。Java中的悲观锁基于AQS实现,该框架下的锁会先尝试以CAS乐观锁去获取锁,如果获取不到则会转为悲观锁。
Q16:讲一讲自旋锁
答:①自旋锁认为如果持有锁的线程能在很短的时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞、挂起状态,只需等待小段时间,在等待持有锁的线程释放锁后即可立即获取锁,这样就避免了用户线程在内核态的切换上导致锁时间消耗。②优点:减少CPU的上下文切换,对于占用锁时间非常短或锁竞争不激烈的代码块来说性能很高。③缺点:在持有锁的线程长时间占用锁或竞争过于激烈时,线程会长时间自旋浪费CPU资源,有复杂锁依赖的情况不适合使用自旋锁。
Q17:讲一讲公平锁与非公平锁
答:①公平锁指在分配锁前检查是否有线程在排队等待获取该锁,优先将锁分配给排队时间最长的线程。②非公平锁指在分配锁时不考虑线程排队等待的情况,直接尝试获取锁,获取不到锁就在排到队尾等待。③因为公平锁需要在多核情况下维护一个锁线程等待队列,基于该队列进行锁的分配,因此效率比非公平锁低很多。synchronized是非公平锁,ReentrantLock默认的lock方法也是非公平锁。
Q18:讲一讲读写锁
答:①Lock接口提供的锁是普通锁,为了提高性能Java提供了读写锁,读写锁分为读锁和写锁,读锁之间不互斥,读锁与写锁,写锁之间都互斥。②如果系统要求共享数据可以同时支持很多线程并发读,但不能支持很多线程并发写,那么读锁能大大提高效率。如果系统要求共享数据在同一时刻只能有一个线程在写,且写的过程中不能读,则需要使用写锁。③提高juc的locks包中ReadWriteLock的实现类ReentrantReadWriteLock的readLock()和writeLock()来分别获取读锁和写锁。
Q19:讲一讲共享锁与排它锁
答:①共享锁:允许多个线程同时获取该锁,并发访问共享资源,ReentrantReadWriteLock的读锁为共享锁的实现。②排它锁:也叫互斥锁 ,每次只允许有一个线程独占该锁,ReentrantLock为排它锁的实现。③排它锁是一种悲观的加锁策略,同一时刻只允许一个线程读取锁资源,限制了读操作的并发性,因为并发读线程并不会影响数据的一致性,因此共享锁采用了乐观的加锁策略,允许多个执行读操作的线程同时访问共享资源。
Q20:锁有哪些状态?
答:①无锁,偏向锁,轻量级锁和重量级锁。②重量级锁是基于操作系统互斥量实现的,会导致进程在用户态和内核态之间来回切换,开销较大,synchronized内部基于监视器实现,监视器基于底层操作系统实现,因此属于重量级锁,运行效率不高。JDK1.6后为了减少获取锁和释放锁带来的性能消耗提高性能,引入了轻量级锁和偏向锁。③轻量级锁是相对于重量级锁而言的,核心设计实在没有多线程竞争的前提下,减少重量级锁的使用来提高性能。适用于线程交替执行同步代码块的情况,如果同一时刻有多线程访问同一个锁,会导致轻量级锁膨胀成重量级锁。④偏向锁用于在某个线程获取某个锁后,消除这个线程锁重入的开销,看起来似乎是这个线程得到了锁的偏袒。偏向锁的主要目的是在同一个线程多次获取某个所的情况下尽量减少轻量级锁的执行路径,因为轻量级锁需要多次CAS操作,而偏向锁只需要切换ThreadID时执行一次CAS操作,提高效率。出现多线程竞争锁时,JVM会自动撤销偏向锁。偏向锁是进一步提高轻量级锁性能的。⑤随着锁竞争越来越严重,锁可能从偏向锁升级到轻量级锁再到重量级锁,但在Java中只会单向升级不会降级。
Q21:如何进行锁优化?
答:①减少锁持有的时间:只在有线程安全要求的程序上加锁来尽量减少同步代码块对锁的持有时间。②减小锁粒度:将单个耗时较多的锁操作拆分为多个耗时较少的锁操作来增加锁的并行度,减少同一个锁上的竞争。在减少锁的竞争后,偏向锁、轻量级锁的使用率才会提高,例如ConcurrentHashMap中的分段锁。③读分离:指根据不同的应用场景将锁的功能进行分离以应对不同的变化,最常见的锁分离思想就是读写锁,这样既保证了线程安全又提高了性能。④锁粗化:指为了保障性能,会要求尽可能将锁的操作细化以减少线程持有锁的时间,但如果锁分的太细反而会影响性能提升,这种情况下建议将关联性强的锁操作集中处理。⑤锁消除:注意代码规范,消除不必要的锁来提高性能。
Q22:线程池是什么?为什么需要线程池?
答:①在生产中为每一个任务创建一个线程存在一些缺陷,如果无限制地大量创建线程会消耗很多资源,影响系统稳定性和性能,产生内存溢出等问题。②线程池是管理一组同构工作线程的资源池,线程池与工作队列密切相关,工作队列中保存了所有需要等待执行的任务。工作线程的任务很简单,从工作队列获取任务,执行任务,返回线程池并等待下一次任务。③线程池通过重用现有的线程,可以在处理多个请求时分摊线程在创建和撤销过程中的开销,另一个好处是当请求到达时工作线程通常已经存在,不会出现等待线程而延迟的任务的执行,提高了响应性。通过调整线程池的大小,可以创建足够多的线程保持处理器处于忙碌状态,同时还可以防止线程过多导致内存资源耗尽。
Q23:创建线程池时,ThreadPoolExecutor构造器中都有哪些参数,有什么含义?
答:①corePoolSize: 线程池核心大小,即在没有任务执行时线程池的大小,并且只有在工作队列满了的情况下才会创建超出这个数量的线程。②maximumPoolSize: 线程池最大大小,表示可同时活动的线程数量的上限。③keepAliveTime:存活时间,如果某个线程的空闲时间超过了存活时间,那么将被标记为可回收的,并且当线程池的当前大小超过基本大小时,这个线程将被终止。④unit: 存活时间的单位,可选的参数为TimeUnit枚举中的几个静态变量: NANOSECONDS、MICROSECONDS、MILLISECONDS、SECONDS。⑤workQueue: 线程池所使用的阻塞队列。⑥thread factory:线程池使用的创建线程工厂方法,可省略,将使用默认工厂。⑦handler:所用的拒绝执行处理策略,可省略,将使用默认拒绝执行策略。
Q24:线程池的阻塞队列有哪些选择?
答:①ArrayBlockingQueue:基于数组的有界阻塞队列。②LinkedBlockingQueue:基于链表的有界阻塞队列。③PriorityBlockingQueue:支持优先级排序的无界阻塞队列。④DelayedWorkQueue:基于优先级队列的无界阻塞队列。⑤SynchronousQueue:队列内部仅允许容纳一个元素,用于控制互斥的阻塞队列。
Q25:线程池的拒绝执行策略有哪些选择?
答:①AbortPolicy(): 线程池默认的拒绝策略,抛出RejectedExecutionException异常。②CallerRunsPolicy(): 重试添加当前的任务,他会自动重复调用execute()方法。③DiscardOldestPolicy(): 抛弃旧的任务,加入新的任务。④DiscardPolicy(): 直接抛弃当前的任务。
Q26:创建线程池的方法有哪些?
答:可以通过Executors的静态工厂方法创建线程池,内部通过重载ThreadExecutorPool不同的构造器创建线程池。①newFixedThreadPool,创建一个固定长度的线程池,每当提交一个任务就创建一个线程,直到达到线程池的最大数量,这时线程池的规模将不再变化(如果某个线程由于发生了未预期的异常而结束,那么线程池会补充一个新的线程)。将线程池的核心大小和最大大小都设置为参数中指定的值,创建的线程不会超时,使用LinkedBlockingQueue。②newCachedThreadPool,创建一个可缓存的线程池,如果线程池的当前规模超过了处理器需求,那么将回收空闲的线程,而当需求增加时,可以添加新的线程,线程池的规模不存在任何限制。将线程池的最大大小设置为In,而将核心大小设置为0,并将超时设为1分钟,使用SynchronousQueue,这种方法创建出的线程池可被无限扩展,并当需求降低时自动收缩。③newSingleThreadExecutor,一个单线程的Executor,创建单个工作者线程来执行任务,如果这个线程异常结束,会创建另一个线程来代替。确保依照任务在队列中的顺序来串行执行。将核心线程和最大线程数都设置为1,使用LinkedBlockingQueue。④newScheduledThreadPool,创建一个固定长度的线程池,而且以延迟或定时的方式来执行任务,类似于Timer,使用DelayedWorkQueue。
Q27:线程池的工作原理?
答:①线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。即使队列里面有任务,线程池也不会马上执行它们。②通过 execute(Runnable command)方法被添加到线程池,任务就是一个 Runnable类型的对象,任务的执行方法就是Runnable类型对象的run()方法。③如果workerCount<corePoolSize,那么创建并启动一个线程执行新提交的任务。如果workerCount>=corePoolSize,且线程池内的阻塞队列未满,那么将这个任务放入队列。如果workerCount>=corePoolSize,且阻塞队列已满,若满足workerCount<maximumPoolSize,那么还是要创建并启动一个线程执行新提交的任务。若阻塞队列已满,并且workerCount>=maximumPoolSize,则根据 handler所指定的策略来处理此任务,默认的处理方式直接抛出异常。也就是处理任务的优先级为: 核心线程corePoolSize、任务队列workQueue、最大线程maximumPoolSize,如果三者都满了,使用handler处理被拒绝的任务。④当一个线程完成任务时,它会从队列中取下一个任务来执行。⑤当一个线程没有任务可执行,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于corePoolSize时,那么这个线程会被停用掉,所以线程池的所有任务完成后,它最终会收缩到corePoolSize的大小。
Q28:简述ExecutorService的生命周期
答:①ExecutorService的生命周期有3种状态:运行、关闭和已终止。②ExecutorService在初始创建时处于运行状态。③shutdown方法将执行平缓的关闭过程:不再接受新的任务,同时等待已经提交的任务执行完成——包括那些还未开始执行的任务。shutdownNow方法将执行粗暴的关闭过程:它将尝试取消所有运行中的任务,并且不再启动队列中尚未开始执行的任务。在ExecutorService关闭后提交的任务将有“拒绝执行处理器REH”来处理,它会抛弃任务,或者使得execute方法抛出一个未检查的RejectedExecutionException。④等所有任务都完成后,ExecutorService将转入终止状态。可以调用awaitTermination来等待ExecutorService到达终止状态,或者通过调用isTerminated来轮询ExecutorService是否已终止。通常在调用awaitTermination后会理解调用shutdown,从而产生同步地关闭ExecutorService的效果。
Q29:什么是CAS?
答:①CAS指Compare And Swap,比较并交换。CAS(V,E,N)算法包括三个参数,V表示要更新的变量的值,E表示预期的值,N表示新值。在且仅在V的值和E相等时才会将V的值设置为N,如果不同则说明已经有其他线程做了更改,当前线程就什么也不做。最后CAS返回当前V的真实值。②CAS操作采用了乐观锁的思想,有多个线程同时使用CAS操作一个共享变量时只有一个线程会成功,失败的线程不会被挂起仅会被告知失败,并且允许再次尝试,或者放弃操作。基于这样的原理虽然CAS没有使用锁,也可以及时发现其他线程的操作进行适当地并发处理。
Q30:CAS有什么问题?(什么是ABA问题?)如何解决?
答:①CAS算法地实现有一个重要前提:需要取出内存中某时刻的数据,然后在下一刻进行比较、替换,但在这个时间差内数据可能已经发生了变化,导致ABA问题。②ABA问题指线程1从内存V位置取出A,这时线程2也从内存中取出A,并将其首先修改为B,接着又修改为A,这时线程1在进行CAS操作时会发现内存中数据仍是A,然后线程1操作成功。尽管从操作角度来说线程1成功了,但是在该过程中实际上数据已发生了变化但并未被感知到,某些应用场景下可能会出现数据不一致的问题。③乐观锁通过版本号来解决ABA问题,具体的操作是每次执行数据修改操作时都会带上一个版本号,如果预期版本号和数据版本号一致就进行操作,并将版本号加1,否则执行失败。
Q31:讲一讲wait、sleep、yield、join方法的区别
答:①wait是Object类的方法,调用wait方法的线程会进入WAITING状态,只有等待其他线程的通知或被中断后才会解除阻塞,调用wait方法会释放锁资源。②sleep是Thread类的方法,调用sleep方法会导致当前线程进入休眠状态,与wait不同的是该方法不会释放锁资源,进入的是TIMED-WAITING状态。③yiled方法会使当前线程让出CPU时间片给优先级相同或更高的线程,回到RUNNABLE状态,与其他线程一起重新竞争CPU时间片。④join方法用于等待其他线程运行终止,如果当前线程调用了另一个线程的join方法,则当前线程进入阻塞状态,当另一个线程结束时当前线程才能从阻塞状态转为就绪态,等待获取CPU时间片。底层使用的是wait,也会释放锁。
Q32:讲一讲线程中断
答:①interrupt方法用于向线程发送一个终止信号,会影响该线程内部的中断标识位,这个线程本身不会因为调用了interrupt方法而改变状态,状态的具体变化需要等待接收到中断标识的程序的处理结果判定。②调用interrupt方法不会中断一个正在运行的线程,只会改变内部的中断标识位的值为true。③当调用sleep方法使线程处于TIMED-WAITING状态使,调用interrupt方法会抛出InterruptedException,使线程提前结束TIMED-WAITING状态。在抛出该异常前将清除中断标识位,所以在抛出异常后调用isInterrupted方法返回的值是false。④中断状态是线程固有的一个标识位,可以通过此标识位安全终止线程。比如想终止某个线程时,先调用interrupt方法然后在run方法中根据该线程isInterrupted方法的返回值安全终止线程。
Q33:什么是守护线程?
答:①守护线程是运行在后台的一种特殊线程,独立于控制终端并且周期性地执行某种任务或等待处理某些已发生的事件。守护线程不依赖于终端,但是依赖于JVM,当JVM中仅剩下守护线程时,JVM就会退出。②通过setDaemon方法定义一个守护线程,守护线程的优先级较低,将一个用户线程设置为守护线程必须要在启动守护线程之前。
Q34:start和run方法的区别?
答:①start方法用于启动线程,真正实现了多线程,调用了start方法后,会在后台创建一个新的线程来执行,不需要等待run方法执行完毕就可以继续执行其他代码。调用start方法时,该线程处于就绪状态,并没有开始运行。②run方法也叫做线程体,包含了要执行的线程的逻辑代码,在调用run方法并没有创建新的线程,而是直接运行run方法中的代码。
更多大厂等BAT高级java必考题和答案
题目包含内容:必考的Java基础、多线程、JVM、Spring、分布式缓存等题目和答案
领取方法:
关注+转发后,私信关键词 【BAT】即可领到题目和答案链接!
关注微信公众号:慕容千语(视频、资料持续更新)
1.文章《【奥迪q3显示死锁怎么解】直通大厂——点击率非常高的多线程面试问题,建议收藏》援引自互联网,为网友投稿收集整理,仅供学习和研究使用,内容仅代表作者本人观点,与本网站无关,侵删请点击页脚联系方式。
2.文章《【奥迪q3显示死锁怎么解】直通大厂——点击率非常高的多线程面试问题,建议收藏》仅供读者参考,本网站未对该内容进行证实,对其原创性、真实性、完整性、及时性不作任何保证。
相关推荐
- . 现代买票为什么带上携程保险
- . 潮阳怎么去广州南站
- . 湖南马拉河怎么样
- . 烧纸为什么到三岔路口
- . 百色为什么这么热
- . 神州租车怎么样
- . 芜湖方特哪个适合儿童
- . 护肤品保养液是什么类目
- . 早晚的护肤保养有哪些项目
- . 女孩护肤品怎么保养的最好