线程Thread

目录

概念

线程:系统调度的最小单位,CPU资源分配的基本单位。
进程:一段执行的程序,可以包含多个进程。

在Android中只有一个主线程main,也称之为UI线程,用于运行四大组件以及与用户的交互。
而子线程也称之为工作线程,用于执行耗时操作,避免在UI线程中执行造成卡顿从而出现ANR。

线程的创建

1,继承Thread类创建线程

//创建线程(缺点:由于java中是单继承,无法再通过继承进行功能扩展)
class MyThread extends Thread {
        @Override
        public void run() {
            //执行线程操作
        }
    }
//开启线程
new MyThread().start();

2,实现Runnable接口创建线程

class MyRunnable implements Runnable {
        @Override
        public void run() {
            //执行线程操作
        }
    }
开启线程(一般采用这种方案,既能保证Thread 的扩展,同时Runnable对象也可以作为任务单元在其它地方使用)
new Thread(new MyRunnable()).start();

3,使用Callable和Future创建线程

FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
    @Override
    public Integer call() throws Exception {
        Log.d(TAG, "call: AAA = " + Thread.currentThread().getName());
        return 10;
    }
});
//这种方式针对需要获取返回值的线程开启
new Thread(task,"有返回值的线程").start();
try {
    Integer integer = task.get();
    Log.d(TAG, "onCreate: AAA = " + integer);
} catch (InterruptedException e) {
    e.printStackTrace();
} catch (ExecutionException e) {
    e.printStackTrace();
}
//打印结果
10-20 21:26:53.838 28300-28329/com.learn.study D/MainActivity: call: AAA = 有返回值的线程
10-20 21:26:53.838 28300-28300/com.learn.study D/MainActivity: onCreate: AAA = 10

线程的生命周期

线程状态转换关系(来源于书籍:java虚拟机)

1,新建(New):创建后尚未启动的线程处于这种状态。
2,运行(Runable):Runable包括了操作系统线程状态中的Running和Ready,也就是处于此状态的线程有可能正在执行,也有可能正在等待着CPU为它分配执行时间。
3,无限期等待(Waiting):处于这种状态的线程不会被分配CPU执行时间,它们要等待被其他线程显式地唤醒。

以下方法会让线程陷入无限期的等待状态:

  • 没有设置Timeout参数的Object.wait()
  • 没有设置Timeout参数的Thread.join()
  • LockSupport.park()

4,限期等待(Timed Waiting):处于这种状态的线程也不会被分配CPU执行时间,不过无须等待被其他线程显式地唤醒,在一定时间之后它们会由系统自动唤醒。

以下方法会让线程进入限期等待状态:

  • Thread.sleep()方法。
  • 设置了Timeout参数的Object.wait()方法。
  • 设置了Timeout参数的Thread.join()方法。
  • LockSupport.parkNanos()方法。
  • LockSupport.parkUntil()方法

5,阻塞(Blocked):线程被阻塞了,“阻塞状态”与“等待状态”的区别是:“阻塞状态”在等待着获取到一个排他锁,这个事件将在另外一个线程放弃这个锁的时候发生;而“等待状态”则是在等待一段时间,或者唤醒动作的发生。 在程序等待进入同步区域的时候,线程将进入这种状态。
6,结束(Terminated):已终止线程的线程状态,线程已经结束执行。

sleep()和wait()的区别

  • sleep()来自Thread类;wait()来自Object类
  • sleep()用于线程控制自身流程;而wait()用于线程间通信,配合notify()/notifyAll()在同步代码块或同步方法里使用
  • sleep()的线程不会释放对象锁;wait()会释放对象锁进入等待状态,使得其他线程能使用同步代码块或同步方法

多线程

1,优点:为了充分利用计算机处理器的能力,避免处理器在磁盘I/O、网络通信或数据库访问时总是处于等待其他资源的状态。将单线程的串行处理改为多线程的并行处理,可以大大提高处理效率。

2,线程安全:当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的。基本特点如下:

1,原子性:简单说就是相关操作不会中途被其他线程干扰,一般通过同步机制保障。如使用:synchronized或ReentrantLock(对接口Lock的实现)
2,可见性:是一个线程修改了某个共享变量,其状态能够立即被其它线程知晓。使用volatile关键字修饰变量
3,有序性:保证线程内串行语义,避免指令重排等。

volatile: 本身就包含了禁止指令重排序的语义
synchronized:保证一个变量在同一个时刻只允许一条线程对其进行lock操作,使得持有同一个锁的两个同步块只能串行地进入

ReentrantLock和synchronized的区别
1,ReentrantLock作为Lock的实现类,使用时更加灵活,可以进行中断操作,执行完需要调用unLock()进行释放(在finally中释放锁)。
2,ReentrantLock默认也是不公平锁,但可以在创建时的构造方法传入true实现公平锁。但是会增加额外的开销
3,锁绑定多个条件:一个ReentrantLock对象可以通过多次调用newCondition()同时绑定多个Condition对象。而在synchronized中,锁对象wait()和notify()或notifyAl()只能实现一个隐含的条件,若要和多于一个的条件关联不得不额外地添加一个锁。
4,synchronized内部锁使用非公平策略,是非公平锁,不会增加上下文切换开销,使用监视器Monitor来实现。

ThreadLocal:线程内部的数据存储类,可以在指定的线程内存储数据,通过泛型来声明数据类型。变量的作用域为当前的线程。最典型的使用是在线程中获取Loop对象时,采用的就是ThreadLocal来进行存取。提供三个方法:
set()用于存储数据,
get()用于获取数据,
remove()用于手动释放存储的变量,
initialValue():用于重写该方法设定默认值

3,线程调度:指系统为线程分配处理器使用权的过程。主要调度方式有两种,分别是协同式线程调度(Cooperative Threads-Scheduling)和抢占式线程调度(Preemptive ThreadsScheduling)。

1,协同式调度:线程的执行时间由线程本身来控制,线程把自己的工作执行完了之后,要主动通知系统切换到另外一个线程上。 好处:实现简单,而且由于线程要把自己的事情干完后才会进行线程切换,切换操作对线程自己是可知的,所以没有什么线程同步的问题。坏处:线程执行时间不可控制,甚至如果一个线程编写有问题,一直不告知系统进行线程切换,那么程序就会一直阻塞在那里。
2,抢占式调度:每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定(在Java中,Thread.yield()可以让出执行时间,但是要获取执行时间的话,线程本身是没有什么办法的)。 在这种实现线程调度的方式下,线程的执行时间是系统可控的,也不会有一个线程导致整个进程阻塞的问题。但线程优先级并不是太靠谱。
Java使用的线程调度方式就是抢占式调度

4,锁优化:为了在线程之间更高效地共享数据,以及解决竞争问题,从而提高程序的执行效率。

1,自旋锁:竞争锁失败的线程,并不会真实的在操作系统层面挂起等待,而是JVM会让线程做几个空循环(基于预测在不久的将来就能获得),在经过若干次循环后,如果可以获得锁,那么进入临界区,如果还不能获得锁,才会真实的将线程在操作系统层面进行挂起。
适用场景:自旋锁可以减少线程的阻塞,这对于锁竞争不激烈,且占用锁时间非常短的代码块来说,有较大的性能提升,因为自旋的消耗会小于线程阻塞挂起操作的消耗。如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,就不适合使用自旋锁了,因为自旋锁在获取锁前一直都是占用cpu做无用功,线程自旋的消耗大于线程阻塞挂起操作的消耗,造成cpu的浪费。
2,锁消除:虚拟机中编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。锁消除的主要判定依据来源于逃逸分析的数据支持,如果判断在一段代码中,堆上的所有数据都不会逃逸出去从而被其他线程访问到,那就可以把它们当做栈上数据对待,认为它们是线程私有的,同步加锁自然就无须进行。
3,锁粗化:一般情况下,推荐将同步块的作用范围限制到只在共享数据的实际作用域中才进行同步,使得需要同步的操作数量尽可能变小,保证就算存在锁竞争,等待锁的线程也能尽快拿到锁。但如果反复操作对同一个对象进行加锁和解锁,即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗,此时,虚拟机将会把加锁同步的范围粗化到整个操作序列的外部,这样只需加一次锁。
4,轻量级锁:并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。
5,偏向锁:目的是消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能。 如果说轻量级锁是在无竞争的情况下使用CAS操作去消除同步使用的互斥量,那偏向锁就是在无竞争的情况下把整个同步都消除掉,连CAS操作都不做了。偏向锁可以提高带有同步但无竞争的程序性能。 它同样是一个带有效益权衡(TradeOff)性质的优化,也就是说,它并不一定总是对程序运行有利,如果程序中大多数的锁总是被多个不同的线程访问,那偏向模式就是多余的。

5,java中信号量Semaphore:用于控制临界区域访问的个数,即可以设置多个线程或单个线程对同一个资源的使用。内部使用计数的方式对线程进行阻塞控制。

  • 初始化时定义同时控制访问的个数
  • 调用acquire()方法获取当前线程是否可以执行。当计数大于0,临界区域可以被访问,统计计数减少1。否则当前线程对临界区域的访问被阻塞。
  • 调用release()方法,计数加1,然后唤醒等待队列中的线程。内部使用链表进行存储。

6,Monitor:同一个时刻,只有一个线程能够进行monitor定义的临界区,可以达到互斥的效果。

  • java中通过synchronized的修饰限定了临界区域的范围。
  • java中的关联对象:monitor object,也就是java中的object对象。object在存储时分为:对象头,实例数据,对齐填充,在对象头中保留了锁标识。当前线程执行时不满足monitor条件时会加入到waitSet中,当添加满足时被唤醒后转移到EntrySet中(对应c语言实现的objectMonitor中)。
  • monitor提供wait(),signal(),signalAll()能够实现等待与唤醒功能。

补充:锁的是对象而不是方法,同时synchronized()代码块不能保证一次执行完。

new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized ("aa") {
                    Log.d(TAG, "run: 11");
                    try {
                        Thread.sleep(100);
                        Log.d(TAG, "run: 22");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized ("aa") {
                    Log.d(TAG, "run: 33");
                    try {
                        Thread.sleep(100);
                        Log.d(TAG, "run: 44");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                Log.d(TAG, "run: 55");
                try {
                    Thread.sleep(100);
                    Log.d(TAG, "run: 66");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
//打印结果:(第一条线程执行时插入了第三条线程的内容,但包含相同锁对象进行互斥的代码块必须相互独立的一次执行完)
10-20 23:47:43.445 7141-7171/com.learn.study D/MainActivity: run: 11
10-20 23:47:43.446 7141-7173/com.learn.study D/MainActivity: run: 55
10-20 23:47:43.545 7141-7171/com.learn.study D/MainActivity: run: 22
10-20 23:47:43.546 7141-7173/com.learn.study D/MainActivity: run: 66
10-20 23:47:43.552 7141-7172/com.learn.study D/MainActivity: run: 33
10-20 23:47:43.652 7141-7172/com.learn.study D/MainActivity: run: 44

5,常见案例:

//模拟四个窗口卖票
public class SellTicket {

    private static final String TAG = "SellTicket";

    //用于统计四个窗口卖的票
    private static List<Integer> sWindow1 = new ArrayList<>();
    private static List<Integer> sWindow2 = new ArrayList<>();
    private static List<Integer> sWindow3 = new ArrayList<>();
    private static List<Integer> sWindow4 = new ArrayList<>();

    private static int sTicketNumber;

    /**
     * 模拟卖票
     * @param tickets:票的总数
     */
    public static void sell(int tickets) {
        sTicketNumber = tickets;
        sWindow1.clear();
        sWindow2.clear();
        sWindow3.clear();
        sWindow4.clear();
        new MyThread("窗口1").start();
        new MyThread("窗口2").start();
        new MyThread("窗口3").start();
        new MyThread("窗口4").start();
    }

    static class MyThread extends Thread {

        public MyThread(String name) {
            super(name);
        }

        @Override
        public void run() {
            while (true) {
                synchronized ("aaa") {
                    if (sTicketNumber > 0) {
                        String name = Thread.currentThread().getName();
                        if ("窗口1".equals(name)) {
                            sWindow1.add(sTicketNumber);
                        } else if ("窗口2".equals(name)) {
                            sWindow2.add(sTicketNumber);
                        } else if ("窗口3".equals(name)) {
                            sWindow3.add(sTicketNumber);
                        } else if ("窗口4".equals(name)) {
                            sWindow4.add(sTicketNumber);
                        }
                        sTicketNumber--;
                        if (sTicketNumber <= 0) {
                            Log.d(TAG, "run: 窗口1卖票数量 = " + sWindow1.size());
                            Log.d(TAG, "run: 窗口2卖票数量 = " + sWindow2.size());
                            Log.d(TAG, "run: 窗口3卖票数量 = " + sWindow3.size());
                            Log.d(TAG, "run: 窗口4卖票数量 = " + sWindow4.size());
                        }
                    } else {
                        break;
                    }
                }
            }
        }
    }
}
//调用情况:验证了synchronized 锁的不平衡性。
SellTicket.sell(10);
10-21 00:34:00.677 13740-13766/com.learn.study D/SellTicket: run: 窗口1卖票数量 = 10
    run: 窗口2卖票数量 = 0
    run: 窗口3卖票数量 = 0
    run: 窗口4卖票数量 = 0
SellTicket.sell(100);
10-21 00:41:39.549 16601-16638/com.learn.study D/SellTicket: run: 窗口1卖票数量 = 11
    run: 窗口2卖票数量 = 80
    run: 窗口3卖票数量 = 9
10-21 00:41:39.550 16601-16638/com.learn.study D/SellTicket: run: 窗口4卖票数量 = 0
SellTicket.sell(1000);
10-21 00:42:23.104 16851-16878/? D/SellTicket: run: 窗口1卖票数量 = 312
    run: 窗口2卖票数量 = 255
    run: 窗口3卖票数量 = 0
    run: 窗口4卖票数量 = 433
SellTicket.sell(10000);
10-21 00:43:06.227 17176-17219/com.learn.study D/SellTicket: run: 窗口1卖票数量 = 1777
    run: 窗口2卖票数量 = 2774
    run: 窗口3卖票数量 = 1851
    run: 窗口4卖票数量 = 3598
//模拟死锁
public class DeadLock {

    private static final String TAG = "DeadLock";

    public static void deadLock() {
        new MyThread("线程1","aa","bb").start();
        new MyThread("线程2","bb","aa").start();
    }

    static class MyThread extends Thread {

        private String mLock1;
        private String mLock2;
        private int total = 100;

        public MyThread(String name, String lock1, String lock2) {
            super(name);
            mLock1 = lock1;
            mLock2 = lock2;
        }

        @Override
        public void run() {
            while (total > 0) {
                synchronized (mLock1) {
                    Log.d(TAG, "run: " + Thread.currentThread().getName() + " ;; 等待获取 : " + mLock2 + " ;; total = " + total);
                    synchronized (mLock2) {
                        Log.d(TAG, "run: " + Thread.currentThread().getName() + " ;; 等待获取 : " + mLock1 + " ;; total = " + total);
                        total--;
                    }
                }
            }
        }
    }
}
DeadLock.deadLock();
//打印结果(发现线程1与线程2都在等待获取对方持有的锁对象,造成互锁)
10-21 00:58:04.536 19486-19589/com.learn.study D/DeadLock: run: 线程1 ;; 等待获取 : bb ;; total = 100
    run: 线程1 ;; 等待获取 : aa ;; total = 100
    run: 线程1 ;; 等待获取 : bb ;; total = 99
10-21 00:58:04.536 19486-19593/com.learn.study D/DeadLock: run: 线程2 ;; 等待获取 : aa ;; total = 100

线程池

1,优点:

1,复用线程池中的线程,避免因为线程的创建于销毁带来性能的消耗。
2,能有效的控制线程池的最大并发数,避免大量的线程之间因相互抢占系统资源而导致阻塞现象
3,能够对线程进行简单的管理,并提供定时执行以及指定间隔循环执行的任务。

2,创建:利用ThreadPoolExecutor的构造方法创建线程池

new ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler)
参数说明如下:
1,corePoolSize:核心线程数,默认情况下核心线程会一直存活,即使处于闲置状态也不会受存keepAliveTime限制。除非将allowCoreThreadTimeOut设置为true。
2,maximumPoolSize:线程池所能容纳的最大线程数。超过这个数的线程将被阻塞。当任务队列为没有设置大小的LinkedBlockingDeque时,这个值无效。
3,keepAliveTime:非核心线程的闲置超时时间,超过这个时间就会被回收。
4,unit:指定keepAliveTime的单位,TimeUnit是一个时间单位的枚举类。当allowCoreThreadTimeOut设置为true时对corePoolSize生效。
5,workQueue:线程池中的任务队列,通过execute()提交的Runnable任务会存储在该队列中,然后根据对应的规则进行执行。
6,threadFactory:线程工厂,提供创建新线程的功能。ThreadFactory是一个接口,只有一个方法Thread newThread(Runnable r)(一般使用系统默认)
7,handler:当线程池中的资源已经全部使用,添加新线程被拒绝时,会调用RejectedExecutionHandler的rejectedExecution方法,抛出异常提示。(一般使用系统默认)

3,任务执行的规则:

1,线程数量<核心线程数量,那么直接启动一个核心线程来执行任务,不会放入队列中。
2,线程数量>=核心线程数,但<最大线程数。当任务队列是LinkedBlockingDeque,超过核心线程数量的任务会放在任务队列中排队(只会启动核心数量的线程);当任务队列为SynchronousQueue,线程池会创建新线程执行任务,这些任务也不会被放在任务队列中。这些线程属于非核心线程,在任务完成后,闲置时间达到了超时时间就会被清除。
3,如果线程数量>核心线程数,并且>最大线程数。当任务队列为LinkedBlockingDeque,会将超过核心线程的任务放在任务队列中排队。此时线程池的最大线程数设置是无效的,他的线程数最多不会超过核心线程数。当任务队列为SynchronousQueue的时候,会因为线程池拒绝添加任务而抛出异常。

4,四种常见的线程池:利用Executors的工厂模式创建

1,FixedThreadPool :通过方法newFixedThreadPool()创建,是线程数量固定的线程池。只有核心线程数并且不会被回收。
2,CachedThreadPool:通过方法newCachedThreadPool()创建,没有固定线程池,线程大小无限制,适合执行大量的耗时少的任务。
3,ScheduledThreadPool:通过方法newScheduledThreadPool()创建,核心线程数固定,非核心线程数没有限制。适合执行定时任务和固定周期的重复任务。
4,SingleThreadExecutor:通过方法newSingleThreadExecutor()创建,内部只有一个核心线程数,适合执行串行的任务,可以不用考虑同步问题。

实际开发中建议使用ThreadPoolExecutor创建线程池,而不是采用Executors默认的四种。其中FixedThreadPool与SingleThreadExecutor,在任务过度时会出现OOM,CachedThreadPool,ScheduledThreadPool在任务过多时会大量创建线程,造成OOM

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,142评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,298评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,068评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,081评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,099评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,071评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,990评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,832评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,274评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,488评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,649评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,378评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,979评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,625评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,643评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,545评论 2 352

推荐阅读更多精彩内容

  • 本文是我自己在秋招复习时的读书笔记,整理的知识点,也是为了防止忘记,尊重劳动成果,转载注明出处哦!如果你也喜欢,那...
    波波波先森阅读 11,253评论 4 56
  • 1.ios高性能编程 (1).内层 最小的内层平均值和峰值(2).耗电量 高效的算法和数据结构(3).初始化时...
    欧辰_OSR阅读 29,369评论 8 265
  • 从哪说起呢? 单纯讲多线程编程真的不知道从哪下嘴。。 不如我直接引用一个最简单的问题,以这个作为切入点好了 在ma...
    Mr_Baymax阅读 2,752评论 1 17
  • 今天早上体重又回来了,昨天晚饭吃半条鱼,有点撑,快走40分钟,又跳操40分钟,还好,下来了,有点沾沾自喜。 早上去...
    影子3623253阅读 100评论 0 1
  • 午后的空气很干净。晨起的雷雨粉墨登场之后,湿气方兴未艾。阳光散进一片阔叶林,挂在叶尖上的雨滴折射出小小的一道彩虹。...
    陟卓阅读 300评论 0 0