Java基础(下)之多线程&JUC

多线程

进程和线程

进程:程序的基本执行实体
线程:操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位

并发和并行

并发:在同一时刻,多个指令在单个CPU上交替执行
并行:在同一时刻,多个指令在多个CPU上同时执行

多线程的实现方式

image

继承Thread类

  1. 定义一个继承Thread的类
  2. 重写run方法
  3. 创建子类的对象,并调用start方法启动线程
package com.example.helloworld;
import java.io.*;

class MyThread extends Thread{

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(getName() + ":" + i);
        }
    }
}

class Main {
    public static void main(String[] args) throws IOException {
        //1.创建线程
        MyThread th1 = new MyThread();
        MyThread th2 = new MyThread();

        //为线程起名字
        th1.setName("Thread1");
        th2.setName("Thread2");

        //2.启动线程,执行run()方法
        th1.start();
        th2.start();
    }

}

实现Runnable接口

  1. 自定义一个实现Runnable接口的类
  2. 重写run方法
  3. 创建含有自定义类的Thread对象,并调用start方法启动线程
package com.example.helloworld;
import java.io.*;

class Counter implements Runnable{

    String name;
    Counter(String name){
        this.name = name;
    }

    @Override
    public void run(){
        for (int i = 0; i < 100; i++) {
            System.out.println(this.name + ":" + i);
        }
    }
}

class Main {
    public static void main(String[] args) throws IOException {
        //1.创建自定义类的对象并起名字
        Counter c1 = new Counter("Thread1");
        Counter c2 = new Counter("Thread2");

        //2.创建含有自定义类的线程对象
        Thread th1 = new Thread(c1);
        Thread th2 = new Thread(c2);

        //3.启动线程,执行Counter的run()方法
        th1.start();
        th2.start();
    }

}

实现Callable接口 能够获取多线程的返回值结果

特点:可以获取到多线程运行的返回值结果
基本步骤:

  1. 创建实现Callable接口的自定义类,重写call方法
  2. 创建包含自定义类的FutureTask对象
  3. 创建Thread线程对象
  4. 调用start方法启动线程
  5. 调用FutureTask的get方法获取线程执行的返回值
package com.example.helloworld;
import java.io.*;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

class Counter implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
            sum += i;
        }
        return sum;
    }
}

class Main {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 1.创建实现Callable接口的自定义类的对象(表示多线程要执行的任务)
        Counter c1 = new Counter();

        // 2.创建FutureTask的对象(作用管理多线程运行的结果)
        FutureTask<Integer> ft = new FutureTask<>(c1);
        
        // 3.创建线程的对象
        Thread th1 = new Thread(ft);
        
        // 4.启动线程
        th1.start();
        
        // 5.获取多线程运行的结果
        Integer res = ft.get();
        System.out.println(res);
    }
}

多线程中常用的成员方法

image
注意:

  1. 若不给线程设置名字,默认为“Thread-0,1,2,3,...”
  2. 当JVM启动后,会自动创建多个线程,其中执行main方法中代码的线程叫main
  3. Java中线程优先级范围是1-10,默认为5,数值越大,越有可能抢占到CPU
  4. yield方法能够让当前线程主动让出CPU的执行权
  5. join方法是等待该线程执行完毕,在程序中能够让该线程先于其它线程执行完毕

守护线程

在 Java 并发编程与 JVM 架构中,线程被严格划分为两类:用户线程(User Thread)和守护线程(Daemon Thread)。守护线程(有时也译作后台线程)是一种为其他线程提供服务和支持的附属线程。其最核心的架构语义在于它不决定 JVM 的生命周期:
JVM 退出触发点:当 JVM 中所有的非守护线程(用户线程)全部执行完毕(或退出)时,JVM 就会主动发起关闭流程。
守护线程的命运:一旦 JVM 决定退出,所有的守护线程会被强制且立即终止,无论它们当前正在执行什么运算。JVM 不会等待守护线程执行完毕。
image
应用场景:

  1. JVM 内部机制(最典型的例子):
    垃圾回收器 (GC) 线程:GC 线程就是守护线程。只要还有用户线程在运行(产生垃圾),GC 线程就需要运行。当所有用户线程结束,程序即将退出,GC 线程继续存在也没有意义了,因此随 JVM 一同销毁。
    JIT 编译线程:在后台将热点字节码编译为本地机器码的线程。

  2. 分布式系统与中间件:
    心跳检测 (Heartbeat) 线程:在后台定期向注册中心或集群其他节点发送存活状态的线程。
    连接池维护线程:定期清理数据库连接池中空闲或失效连接的背景线程。

误区:守护线程随着主线程的退出而退出。❌️
在以下程序中,主线程执行完成后,作为用户线程的线程1尚未执行完成,JVM需要等待所有用户线程完成后才退出,此时守护线程随着JVM的退出而停止。

package com.example.helloworld;
import java.io.*;
import java.util.concurrent.ExecutionException;

class Counter extends Thread {

    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            System.out.println(getName() + ":" + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
class Main {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Counter c1 = new Counter(),c2 = new Counter();
        c1.setName("线程1");
        c1.setPriority(1);
        c2.setName("线程2");
        c2.setPriority(10);
        c1.setDaemon(false);
        c2.setDaemon(true);
        c1.start();
        c2.start();
        for (int i = 1; i <= 100; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }
}

线程的生命周期

image
Q:一个线程调用了sleep方法,时间到后会立刻继续执行接下来的代码吗?
A:不是,sleep方法会使当前线程进入阻塞态,时间到后会唤醒该线程进入就绪态,此时该线程才有抢占CPU的资格,当OS为其分配CPU时间片,进程进入运行态后才能接着执行。

线程安全的问题

例子

现有100张票,三个窗口可同时出售,请设计一个程序模拟卖票过程。
分析以下代码存在什么问题?

package com.example.helloworld;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

class Casher implements Callable<Integer>{

    static int ticket = 100;
    private String name;
    
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    
    @Override
    public Integer call() throws Exception {
        int cnt = 0;
        while(ticket > 0){
            ticket--;
            System.out.println(name + "卖出1张票,剩余"+ticket+"张票.");
            ++cnt;
        }
        return cnt;
    }

    public Casher(String name){
        this.name = name;
    }
}

class Main {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Casher[] cashers = new Casher[3];
        for(int i = 0;i < 3;i++){
            cashers[i] = new Casher("窗口" + i);
        }

        FutureTask<Integer>[] fts = new FutureTask[3];
        for(int i = 0;i < 3;i++){
            fts[i] = new FutureTask<>(cashers[i]);
        }

        Thread[] threads = new Thread[3];
        for(int i = 0;i < 3;i++){
            threads[i] = new Thread(fts[i]);
            threads[i].start();
        }

        for (int i = 0; i < 3; i++) {
            System.out.println(cashers[i].getName()+"共卖出" + fts[i].get()+"张票");
        }

    }
}

上述代码存在多个窗口卖出同一张票,以及卖出票数超出的问题。
image
image
解决方案:引入同步代码块,将操作共享数据的代码锁起来

同步代码块

image
注意:

  1. 同步代码块中的锁对象一定是唯一的,否则锁就会失效,但是锁对象属性的变化不会导致失效,一般使用当前对象.class作为锁对象。
  2. 重点:同步代码块中只应包含真正需要保证原子性和可见性的共享变量操作。任何耗时的 I/O 操作(如数据库访问、网络请求)或阻塞调用(如 sleep、wait)都应尽最大可能移出同步代码块。如果在同步块内部发生阻塞,将导致所有等待获取该锁的线程发生操作系统的用户态与内核态切换(Context Switch),带来极高的性能损耗。
    用同步代码块解决上述场景中的同步问题:
@Override
public Integer call() throws Exception {
	int cnt = 0;
	while(true){
		synchronized (lock) {
			if(ticket > 0){
				ticket--;
				System.out.println(name + "卖出1张票,剩余" + ticket + "张票.");
				++cnt;
			}else{
				break;
			}
		}
		Thread.sleep(10);
	}
	return cnt;
}

同步方法

如果需要将一个方法的整个方法体加入同步锁,则把synchronized关键字加到方法上,该方法为同步方法。
image
使用同步方法改写卖票的互斥逻辑

@Override
    public Integer call() throws Exception {
        int cnt = 0;
        while(true){
            if(!sell_ticket(name)) break;
            ++cnt;
            Thread.sleep(100);
        }
        return cnt;
    }
	//必须是静态方法,锁对象为class本身,否则为对象本身,此时多个对象间不存在互斥关系,同步锁失效
    static public synchronized boolean sell_ticket(String name) {
        if (ticket > 0) {
            ticket--;
            System.out.println(name + "卖出1张票,剩余" + ticket + "张票.");
            return true;
        } else return false;
    }

Lock锁 ReentrantLock

同步代码块和同步方法不能直接看到在哪里加锁和解锁,为了更清晰地表达如何加锁和解锁,JDK5以后提供了一个新的锁对象Lock,Lock实现提供比synchronized方法和语句更广泛的锁定操作。
Lock是一种接口,无法直接实例化,这里采用它的实现类ReentrantLock来实例化,使用lock和unlock方法进行加锁和解锁。
使用Lock锁改写卖票场景的互斥逻辑

@Override
public Integer call() throws Exception {
	int cnt = 0;
	while(true){
		try {
			lock.lock();
			if(ticket > 0){
				ticket--;
				cnt++;
				System.out.println(name + "卖出1张票,剩余" + ticket + "张票.");
			}else break;
		} catch (Exception e) {
			throw new RuntimeException(e);
		} finally { //注意:此处通过try-catch-finally结构保证unlock一定被执行,否则跳出循环时互斥锁未解锁,导致死锁:仍有进程被阻塞
			lock.unlock();
		}
	}
	return cnt;
}

等待-唤醒机制(以生产者消费者为例)

image

wait-notify实现

生产者-消费者模拟程序示例,由桌子、厨师、顾客和盘子组成,厨师只有拿到空盘子和食材时才能做饭,顾客只有拿到食物时才能吃。
image

package com.example.helloworld;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class Desk {
    // 共享状态变量
    int material = 100;    // 剩余食材
    int food = 0;          // 做好的食物
    int emptyPlates = 5;   // 可用的空盘子
    int cooking = 0;       // 正在制作中的食物数量(关键:用于修复终止竞态)

    // 显式锁与条件变量
    final Lock lock = new ReentrantLock();
    // 消费者等待食物的条件队列
    final Condition foodCondition = lock.newCondition();
    // 厨师等待空盘子的条件队列
    final Condition plateCondition = lock.newCondition();
}

class Producer extends Thread {
    private final int id;
    private final Desk desk;

    public Producer(int id, Desk desk) {
        this.id = id;
        this.desk = desk;
    }

    private boolean produce() throws InterruptedException {
        // --- 临界区 1:获取资源 ---
        int cur_material;
        desk.lock.lock();
        try {
            if (desk.material == 0) {
                return false; // 食材耗尽,生产结束
            }
            // 标准范式:在 while 循环中进行条件等待,防止虚假唤醒
            while (desk.emptyPlates == 0) {
                desk.plateCondition.await();
            }
            cur_material = desk.material--;
            desk.emptyPlates--;
            desk.cooking++; // 标记正在做饭,防止消费者提前判定结束
        } finally {
            desk.lock.unlock(); // 耗时操作前必须释放锁
        }

        // --- 模拟耗时操作:做饭 ---
        Thread.sleep(200);
        System.out.printf("Producer %d: Ding!!! material left: %d\n", id, cur_material);

        // --- 临界区 2:产出结果 ---
        desk.lock.lock();
        try {
            desk.cooking--;
            desk.food++;
            // 精确唤醒:只唤醒在 foodCondition 上等待的消费者,不打扰其他厨师
            desk.foodCondition.signalAll();
        } finally {
            desk.lock.unlock();
        }
        return true;
    }

    @Override
    public void run() {
        try {
            while (produce()) {}
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

class Consumer extends Thread {
    private final int id;
    private final Desk desk;

    public Consumer(int id, Desk desk) {
        this.id = id;
        this.desk = desk;
    }

    private boolean eat() throws InterruptedException {
        // --- 临界区 1:获取食物 ---
        desk.lock.lock();
        try {
            while (desk.food == 0) {
                // 严谨的终止判定:没有现成食物,没有剩余食材,且没有厨师正在做饭
                if (desk.material == 0 && desk.cooking == 0) {
                    // 级联唤醒:当前消费者确认结束后,必须唤醒其他可能还在阻塞的消费者,防止线程死锁挂起
                    desk.foodCondition.signalAll();
                    return false;
                }
                desk.foodCondition.await();
            }
            desk.food--;
        } finally {
            desk.lock.unlock(); // 耗时操作前释放锁
        }

        // --- 模拟耗时操作:吃饭 ---
        Thread.sleep(50);
        System.out.printf("Consumer %d: Yummy!!!\n", id);

        // --- 临界区 2:归还盘子 ---
        desk.lock.lock();
        try {
            desk.emptyPlates++;
            // 精确唤醒:只唤醒等待空盘子的厨师
            desk.plateCondition.signalAll();
        } finally {
            desk.lock.unlock();
        }
        return true;
    }

    @Override
    public void run() {
        try {
            while (eat()) {}
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

class Main {
    public static void main(String[] args) {
        Desk desk = new Desk(); // 将状态对象注入线程,避免使用 static 全局变量
        int cntProducer = 2;
        int cntConsumer = 10;

        for (int i = 0; i < cntProducer; i++) {
            new Producer(i, desk).start();
        }
        for (int i = 0; i < cntConsumer; i++) {
            new Consumer(i, desk).start();
        }
    }
}

阻塞队列实现 BlockingQueue

image
通过阻塞队列的put()和take()方法对场景进行改写:

package com.example.helloworld;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class Desk {
    // 共享状态变量
    int material = 100;    // 剩余食材

    Object lock = new Object();

    BlockingQueue<Integer> FoodQueue = new ArrayBlockingQueue<>(5);//5个空盘子

}

class Producer extends Thread {
    private final int id;
    private final Desk desk;

    public Producer(int id, Desk desk) {
        this.id = id;
        this.desk = desk;
    }

    private boolean produce() throws InterruptedException {
        // --- 临界区 1:获取资源 ---
        int cur_material;

        synchronized (this.desk.lock){
            if (desk.material <= 0) {
                return false; // 食材耗尽,生产结束
            }
            cur_material = --desk.material;
        }

        // --- 模拟耗时操作:做饭 ---
        Thread.sleep(200);
        desk.FoodQueue.put(cur_material);
        System.out.printf("Producer %d: Ding!!! material left: %d\n", id, cur_material);

        return true;
    }

    @Override
    public void run() {
        try {
            while (produce()) {}
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

class Consumer extends Thread {
    private final int id;
    private final Desk desk;

    public Consumer(int id, Desk desk) {
        this.id = id;
        this.desk = desk;
    }

    private boolean eat() throws InterruptedException {
        // --- 临界区 1:获取食物 ---
        int food = this.desk.FoodQueue.take();

        // 识别毒药丸,如果是 -1,说明生产线已彻底关闭,消费者应下线
        if (food == -1) {
            System.out.printf("Consumer %d: Received Poison Pill, terminating.\n", id);
            return false; // 返回 false,终止 run() 中的 while 循环
        }

        // --- 模拟耗时操作:吃饭 ---
        Thread.sleep(50);
        System.out.printf("Consumer %d: Yummy!!! ate food ID: %d\n", id, food);

        return true;
    }

    @Override
    public void run() {
        try {
            while (eat()) {}
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

class Main {
    // Main.java 修改
    public static void main(String[] args) throws InterruptedException {
        Desk desk = new Desk();
        int cntProducer = 2;
        int cntConsumer = 10;

        Producer[] producers = new Producer[cntProducer];
        for (int i = 0; i < cntProducer; i++) {
            producers[i] = new Producer(i, desk);
            producers[i].start();
        }

        for (int i = 0; i < cntConsumer; i++) {
            new Consumer(i, desk).start();
        }

        // 主线程等待所有生产者完成工作
        for (int i = 0; i < cntProducer; i++) {
            producers[i].join();
        }

        // 所有的真实食物都已经生产完毕,向队列投放 10 颗“毒药丸”
        for (int i = 0; i < cntConsumer; i++) {
            // 使用 put,即使队列满了也会阻塞等待消费者腾出空间
            desk.FoodQueue.put(-1);
        }
    }
}

线程的六大状态

理论模型

image

JVM模型

image

注意:Java 没有 RUNNING 状态,是因为 JVM 作为运行在用户态的虚拟机,无法也不需要去实时捕获纳秒/微秒级的 CPU 时间片分配情况。RUNNABLE 状态传达的真实语义是:该线程在 JVM 层面并未受阻,它正在执行,或者随时准备好接受操作系统的调度执行。

线程池

image
image

package com.example.helloworld;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

class Counter implements Runnable{

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }
}


class Main {
    // Main.java 修改
    public static void main(String[] args) throws InterruptedException {
        //创建线程池
        ExecutorService pool1 = Executors.newFixedThreadPool(3);
//        ExecutorService pool1 = Executors.newCachedThreadPool();
        
        pool1.submit(new Counter());
        Thread.sleep(1000);
        pool1.submit(new Counter());//若上一个任务已执行完成,则当前任务会复用线程池中的线程
        Thread.sleep(1000);
        pool1.submit(new Counter());
        Thread.sleep(1000);
        pool1.submit(new Counter());
        Thread.sleep(1000);
        pool1.submit(new Counter());
        
        //服务器程序中线程池一般不关闭
        pool1.shutdown();

    }
}

自定义线程池解析

image
image

package com.example.helloworld;
import java.util.concurrent.*;

class Counter implements Runnable{
    int id;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public Counter(int id) {
        this.id = id;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + "Counter" + this.id + " " + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}


class Main {
    // Main.java 修改
    public static void main(String[] args) throws InterruptedException {

        int core_cnt = Runtime.getRuntime().availableProcessors();
        System.out.println(core_cnt);
        //创建线程池
        ThreadPoolExecutor pool = new ThreadPoolExecutor(
                3,//核心线程数量,不能小于0
                6,//最大线程数量,不能小于corePoolSize,临时线程数量 = 最大 - 核心
                60, //空闲线程最大存活时间值
                TimeUnit.SECONDS,//空闲线程最大存活时间单位
                new ArrayBlockingQueue<>(3),//任务阻塞队列
                Executors.defaultThreadFactory(),//创建线程工厂
                new ThreadPoolExecutor.AbortPolicy()//任务的拒绝策略
                );

        for (int i = 1; i <= 10; i++) {
            try {
                pool.submit(new Counter(i));
            } catch (Exception e) {
                System.out.println(e.getMessage());
                System.out.printf("Counter %d was rejected.\n",i);
            }
        }
    }
}

注意:

  1. 临时线程创建时机:只有核心线程均被占用,且任务队列也满,若有新的任务请求时,才会创建临时线程并分配给新的任务请求,而不是任务队列中的请求。
  2. 先提交的任务请求不一定先被执行:正如1所述,新的任务请求提交晚于任务队列中的线程,但在请求时直接被分配给临时线程执行。
拒绝策略

image

线程池多大合适?

最大并行数 = 逻辑处理器个数,如8核16线程,最大并行数为16,在Java中可以通过Runtime.getRuntime().availableProcessors()获取
对于CPU密集型程序:线程池个数 = 最大并行数 + 1,其中+1是为了当正在执行的线程出现错误停止时,有备用线程可以上CPU执行
对于IO密集型程序(大多数):最大并行数 × 期望CPU利用率 * 总时间(CPU计算 + 等待) / CPU计算时间,例如,从文件读取两个数据(1s)并相加(1s),期望利用率=100%,最大并行数=8,则线程池设置为16

额外扩展

案例

多人抢红包

package com.example.helloworld;


import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Random;

import java.util.concurrent.CountDownLatch;

class Main {
    public static void main(String[] args)  {
        final int cnt_person = 300;
        Person[] people = new Person[cnt_person];
        Gift g1 = new Gift(50,BigDecimal.valueOf(100));
        // 创建一个初始值为 1 的发令枪
        CountDownLatch startSignal = new CountDownLatch(1);
        for (int i = 0; i < cnt_person; i++) {
            people[i] = new Person(i);
        }
        for (int i = 0; i < cnt_person; i++) {
            int finalI = i;
            Thread th = new Thread(new Runnable() {
                @Override
                public void run() {
                    // 所有线程在此处阻塞,等待主线程的倒计时指令
                    try {
                        startSignal.await();
                        people[finalI].get_gift(g1);
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }

                }
            });

            th.start();
        }
        System.out.println("开抢红包!");
        startSignal.countDown();

    }
}

class Gift{
    private int cnt;
    private BigDecimal balance;
    private final double MIN = 0.01;
    public Gift(int cnt, BigDecimal balance) {
        this.cnt = cnt;
        this.balance = balance;
    }

    public int getCnt() {
        return cnt;
    }

    public void setCnt(int cnt) {
        this.cnt = cnt;
    }

    public BigDecimal getBalance() {
        return balance;
    }

    public void setBalance(BigDecimal balance) {
        this.balance = balance;
    }

    public BigDecimal[] get_one(){
        BigDecimal res;
        synchronized (this) {
            if (cnt <= 0) return new BigDecimal[]{BigDecimal.valueOf(0.0), BigDecimal.valueOf(0.0)};

            if (cnt == 1) {
                res = balance;
            } else {
                double value = new Random().nextDouble((balance.doubleValue() / cnt) * 2);//二倍均值法的工业级抢红包核心切分算法:上限为剩余金额 / 红包个数 * 2
                value = Math.max(value,MIN);//最少一分钱
                res = BigDecimal.valueOf(value);
                res = res.setScale(2, RoundingMode.HALF_UP);//保留两位小数,四舍五入
            }
            --cnt;

            balance = balance.subtract(res);
        }
            return new BigDecimal[]{res,balance};
    }

}

class Person {
    private int id;
    public Person(int id) {
        this.id = id;
    }

    public void get_gift(Gift gift){
        BigDecimal[] res = gift.get_one();
        BigDecimal amount = res[0],balance = res[1];
        if(amount.equals(BigDecimal.valueOf(0.0))) System.out.println(id + "号手慢了,没抢到");
        else System.out.printf("%d号抢到%s元,剩余%s元\n",id,amount.toString(),balance.toString());
    }

}
posted @ 2026-03-04 13:02  安河桥北i  阅读(1)  评论(0)    收藏  举报