JUC 并发编程学习笔记(中)

PageObject框架设计

文章目录

spring cloud

1. 集合类不安全

1.1 List不安全

代码如下:

stm32

//java.util.ConcurrentModificationException 并发修改异常
public class ListTest {
    public static void main(String[] args) {
        // 并发下ArrayList不安全
        /**
         * 解决方案:
         * 1. List<String> list = new Vector<>();
         * 2. List<String> list = Collections.synchronizedList(new ArrayList<>());
         * 3. List<System> list = new CopyOnWriteArrayList<>();
         */
        
        // CopyOnWrite写入时复制  COW 计算机程序设计领域的一种优化策略
        // 多个线程调用的时候,list读取的时候,固定的,写入(覆盖)
        // 在写入的时候避免覆盖造成数据问题
        // 读写分离

        List<String> list = new CopyOnWriteArrayList<>();
        for (int i = 1; i <= 10; i ++ ) {
            new Thread(()-> {
                list.add(UUID.randomUUID().toString().substring(0, 5));
                System.out.println(list);
            }, String.valueOf(i)).start();
        }
    }
}

 
运行结果如下:
在这里插入图片描述
 

软著

1.2 Set不安全

public class SetTest {

    public static void main(String[] args) {

        // Set<String> set = new HashSet<>();
        // Set<String> set = Collections.synchronizedSet(new HashSet<>());
        Set<String> set = new CopyOnWriteArraySet<>();
        for (int i = 1; i <= 30; i++) {
            new Thread(() -> {
                set.add(UUID.randomUUID().toString().substring(0, 5));
                System.out.println(set);
            }, String.valueOf(i)).start();
        }
    }
}

 
HashSet的底层实现:

EIP

public HashSet(){
	map = new HashMap<>();
}

//add Set本质就是 map Key是无法重复的
public boolean add(E e){
	return map.put(e,PRESENT) == null; 
}

private static final Object PRESENT = new Object(); //不变得值!

 

oauth2

1.3 Map不安全

在这里插入图片描述
 

oracle

public class MapTest {
    public static void main(String[] args) {
        // map是这样用的吗    不是,工作中不用HashMap
        // 默认等价于什么  new HashMap<>(16,0.75);
        // Map<String, String> map = new HashMap<>();

        Map<String, String> map = new ConcurrentHashMap<>();
        // 加载因子 、初始化容量

        for (int i = 1; i <= 30; i++) {
            new Thread(() -> {
                map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0, 5));
                System.out.println(map);
            }, String.valueOf(i)).start();
        }
    }
}

 

定位打卡

2. Callable(简单)

在这里插入图片描述

mingw

  1. 可以有返回值
  2. 可以抛出异常
  3. 方法不同,run() / call()
    在这里插入图片描述
    在这里插入图片描述
     
    测试代码如下:
public class CallableTest {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // new Thread(new Thread()).start();
        // new Thread(new FutureTask<V>).start();
        // new Thread().start(); // 怎么启动Callable
        MyThread thread = new MyThread();
        FutureTask futureTask = new FutureTask(thread); // 适配类
        new Thread(futureTask, "A").start();
        String o = (String) futureTask.get(); // 获取Callable 的返回结果。 这个get方法可能会产生阻塞,把它放到最后或者用异步通信来处理
        System.out.println(o);
    }

}

class MyThread implements Callable<String> {
    @Override
    public String call() {
        System.out.println("call()");
        return "123";
    }
}

 
运行结果如下:

图形学

call()
123

 
细节:

JWT

  1. 有缓存
  2. 结果可能需要等待会阻塞

 

图像分割

3. 常用的辅助类(必会)

3.1 CountDownLatch

在这里插入图片描述
 
测试代码如下:

ParticleSystem

// 计数器
public class CountDownLatchDemo {
    public static void main(String[] args) throws InterruptedException {
        // 总数是6,必须要执行任务的时候,再使用
        CountDownLatch countDownLatch = new CountDownLatch(6);
        for (int i = 1; i <= 6; i ++ ) {
            new Thread(()->{
                System.out.println(Thread.currentThread().getName() + " Go out");
                countDownLatch.countDown(); // 数量-1
            }, String.valueOf(i)).start();
        }
        countDownLatch.await(); // 等待计数器归0,然后再向下执行
        System.out.println("close door");

    }
}

 
运行结果如下:
在这里插入图片描述

开发语言

 
原理:
countDownLatch.countDown(); // 数量 -1
countDownLatch.await() // 等待计数器归0,然后再向下执行
每次有线程调用 countDown() 数量-1,假设计数器变为0, countDownLatch.await() 就会被唤醒,继续执行。
 

马里奥

3.2 CyclicBarrier

在这里插入图片描述
 
CyclicBarrier加法计数器,测试代码如下:

集成学习

public class CyclicBarrierDemo {

    public static void main(String[] args) {
        /**
         * 集齐7颗龙珠召唤神龙
         */
        CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> {
            System.out.println("召唤神龙成功!");
        });

        for (int i = 1; i <= 7; i ++ ) {
            final int temp = i;
            // lambda 不能直接取到i,必须借助于temp
            new Thread(()-> {
                System.out.println(Thread.currentThread().getName() + "收集" + temp + "个龙珠");
                try {
                    cyclicBarrier.await(); // 等待计数器变成7
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

 
运行结果如下:
在这里插入图片描述
 

python绘制玫瑰花

3.3 Semaphore

Semaphore即信号量。
在这里插入图片描述
 
测试代码如下:

砸壳

public class SemaphoreDemo {

    // 6辆车、3个车位
    public static void main(String[] args) {
        // 线程数量;停车位!限流!
        Semaphore semaphore = new Semaphore(3);

        for (int i = 1; i <= 6; i ++ ) {
            new Thread(()-> {
                // acquire() 得到
                try {
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName() + "抢到车位");
                    TimeUnit.SECONDS.sleep(2);
                    System.out.println(Thread.currentThread().getName() + "离开车位");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    semaphore.release();
                }
                // release() 释放
            }, String.valueOf(i)).start();
        }
    }
}

 
运行结果如下:
在这里插入图片描述
 
原理:
semaphore.acquire(); 获得,假设如果已经满了,等待被释放为止。
semaphore.release(); 释放,会将当前的信号量释放+1,然后唤醒等待的线程。
作用:多个共享资源互斥的使用!并发限流,控制最大的线程数。
 

需求

4. 读写锁

在这里插入图片描述
 
旧版:即自定义缓存,测试代码如下:

磁盘分区

public class ReadWriteLock {

    public static void main(String[] args) {
        MyCache myCache = new MyCache();

        // 只写入
        for (int i = 1; i <= 5; i ++ ) {
            final int temp = i;
            new Thread(()-> {
                myCache.put(temp + "", temp + "");
            }, String.valueOf(i)).start();
        }

        // 读取
        for (int i = 1; i <= 5; i ++ ) {
            final int temp = i;
            new Thread(()-> {
                myCache.get(temp + "");
            }, String.valueOf(i)).start();
        }
    }
}

/**
 * 自定义缓存
 */
class MyCache {
    private volatile Map<String, Object> map = new HashMap<>();

    // 存,写
    public void put(String key, Object value) {
        System.out.println(Thread.currentThread().getName() + "写入" + key);
        map.put(key, value);
        System.out.println(Thread.currentThread().getName() + "写入完毕");
    }

    // 取,读
    public void get(String key) {
        System.out.println(Thread.currentThread().getName() + "读取" + key);
        Object o = map.get(key);
        System.out.println(Thread.currentThread().getName() + "读取完毕");
    }
}

 
运行结果如下:
在这里插入图片描述
 
出现的问题:在一个线程写入的过程中会有其他线程的插入,可以用加锁的缓存来解决该问题,相关代码如下:

新手教程

/**
 * 独占锁(写锁) 一次只能被一个线程占有
 * 共享锁(读锁) 多个线程可以同时占有
 * 读——读 可以共存
 * 读——写 不能共存
 * 写——写 不能共存
 */
public class ReadWriteLock {

    public static void main(String[] args) {
        //MyCache myCache = new MyCache();
        MyCacheLock myCache = new MyCacheLock();

        // 只写入
        for (int i = 1; i <= 5; i ++ ) {
            final int temp = i;
            new Thread(()-> {
                myCache.put(temp + "", temp + "");
            }, String.valueOf(i)).start();
        }

        // 读取
        for (int i = 1; i <= 5; i ++ ) {
            final int temp = i;
            new Thread(()-> {
                myCache.get(temp + "");
            }, String.valueOf(i)).start();
        }
    }
}

/**
 * 加锁的
 */
class MyCacheLock {
    private volatile Map<String, Object> map = new HashMap<>();

    // 读写锁:更加细粒度的控制
    private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    //存,写的时候,只希望同时只有一个线程写
    public void put(String key, Object value) {
        readWriteLock.writeLock().lock();

        try {
            System.out.println(Thread.currentThread().getName() + "写入" + key);
            map.put(key, value);
            System.out.println(Thread.currentThread().getName() + "写入完毕");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readWriteLock.writeLock().unlock();
        }
    }

    // 取,读的时候,所有人都可以读取
    public void get(String key) {
        readWriteLock.readLock().lock();

        try {
            System.out.println(Thread.currentThread().getName() + "读取" + key);
            Object o = map.get(key);
            System.out.println(Thread.currentThread().getName() + "读取完毕");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readWriteLock.readLock().unlock();
        }
    }

}


/**
 * 自定义缓存
 */
class MyCache {
    private volatile Map<String, Object> map = new HashMap<>();

    // 存,写
    public void put(String key, Object value) {
        System.out.println(Thread.currentThread().getName() + "写入" + key);
        map.put(key, value);
        System.out.println(Thread.currentThread().getName() + "写入完毕");
    }

    // 取,读
    public void get(String key) {
        System.out.println(Thread.currentThread().getName() + "读取" + key);
        Object o = map.get(key);
        System.out.println(Thread.currentThread().getName() + "读取完毕");
    }
}

 
运行结果如下:
在这里插入图片描述
 

scss

5. 阻塞队列

5.1 阻塞队列BlockingQueue

在这里插入图片描述
 
在这里插入图片描述
 
在这里插入图片描述
在这里插入图片描述
 
什么情况下会使用阻塞队列:多线程并发处理,线程池!
使用队列: 添加、移除

android-studio

5.2 BlockingQueue四组API

方式 抛出异常 有返回值,不抛出异常 阻塞等待 超时等待
添加 add offer() put() offer(, ,)
删除 remove poll() take() poll(,)
检测队首元素 element peek()

 

栈和队列

  1. 抛出异常的测试代码如下:
public class Test {

    public static void main(String[] args) {
        test1();
    }

    /**
     * 抛出异常
     */
    public static void test1() {
        // 队列的大小
        ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);

        System.out.println(blockingQueue.add("a"));
        System.out.println(blockingQueue.add("b"));
        System.out.println(blockingQueue.add("c"));
        System.out.println("---------------");
        //IllegalStateException: Queue full 抛出异常!
        //System.out.println(blockingQueue.add("d"));

        System.out.println(blockingQueue.remove());
        System.out.println(blockingQueue.remove());
        System.out.println(blockingQueue.remove());
        //java.util.NoSuchElementException 抛出异常!
        //System.out.println(blockingQueue.remove());
    }
}

 
运行结果如下:
在这里插入图片描述
 
2. 有返回值,没有异常的测试代码如下:

java 处理树形结构数据

public class Test {

    public static void main(String[] args) {
        test2();
    }

    /**
     * 有返回值,没有异常
     */
    public static void test2() {
        // 队列的大小
        ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);

        System.out.println(blockingQueue.offer("a"));
        System.out.println(blockingQueue.offer("b"));
        System.out.println(blockingQueue.offer("c"));
        System.out.println(blockingQueue.offer("d")); // 不抛出异常,false
        System.out.println(blockingQueue.peek()); // 弹出队首元素
        System.out.println("--------");

        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll()); // 不抛出异常,null
    }
}

 
运行结果如下:
在这里插入图片描述
 
3. 等待,阻塞(一直阻塞) 的测试代码如下:

Vue+Java案例

public class Test {

    public static void main(String[] args) throws InterruptedException {
        test3();
    }

    /**
     * 等待,阻塞(一直阻塞)
     */
    public static void test3() throws InterruptedException {
        // 队列的大小
        ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);

        // 一直阻塞
        blockingQueue.put("a");
        blockingQueue.put("b");
        blockingQueue.put("c");
        //blockingQueue.put("d"); // 队列没有位置了,一直阻塞

        System.out.println(blockingQueue.take());
        System.out.println(blockingQueue.take());
        System.out.println(blockingQueue.take());
        //System.out.println(blockingQueue.take()); // 没有这个元素,一直阻塞
    }
}

 

神眼

运行结果如下:
在这里插入图片描述
 
4. 等待,阻塞(等待超时) 测试代码如下:

内存优化

public class Test {

    public static void main(String[] args) throws InterruptedException {
        test4();
    }
   

    /**
     * 等待,阻塞(等待超时)
     */
    public static void test4() throws InterruptedException {
        // 队列的大小
        ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);

        blockingQueue.offer("a");
        blockingQueue.offer("b");
        blockingQueue.offer("c");
        //blockingQueue.offer("d", 2, TimeUnit.SECONDS); // 等待超过2秒就退出
        System.out.println("-------");

        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll());
        //System.out.println(blockingQueue.poll(2, TimeUnit.SECONDS)); // 等待超过2秒就退出
    }
}

 
运行结果如下:
在这里插入图片描述
 

swoole

5.3 SynchronousQueue同步队列

SynchronousQueue 是没有容量的,进去一个元素,必须等待取出来之后,才能再往里面放入一个元素。
添加操作 put、删除操作take

canal

测试代码如下:

/**
 * 同步队列
 * 和其他的BlockQueue不一样,SynchronousQueue不存储元素
 * put 了一个元素,必须从里面take取出来,否则不能put进去值
 */
public class SynchronousQueueDemo {

    public static void main(String[] args) {
        BlockingQueue<String> blockingQueue = new SynchronousQueue<>(); // 同步队列

        new Thread(()->{
            try {
                System.out.println(Thread.currentThread().getName() + " put 1");
                blockingQueue.put("1");
                System.out.println(Thread.currentThread().getName() + " put 2");
                blockingQueue.put("2");
                System.out.println(Thread.currentThread().getName() + " put 3");
                blockingQueue.put("3");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "T1").start();

        new Thread(()->{
            try {
                TimeUnit.SECONDS.sleep(3);
                System.out.println(Thread.currentThread().getName() + " take " + blockingQueue.take());
                TimeUnit.SECONDS.sleep(3);
                System.out.println(Thread.currentThread().getName() + " take " + blockingQueue.take());
                TimeUnit.SECONDS.sleep(3);
                System.out.println(Thread.currentThread().getName() + " take " + blockingQueue.take());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "T2").start();
    }
}

 
运行结果如下:
在这里插入图片描述
 

6. 线程池(重点)

线程池:3大方法、7大参数、4种拒绝策略

线程池的好处:

  1. 降低资源的消耗。
  2. 提高响应的速度。
  3. 方便管理。

线程复用、可以控制最大并发数、管理线程。

线程池:三大方法

在这里插入图片描述
 

3大方法的测试如下:

  1. Executors.newSingleThreadExecutor() 方法的测试如下:
// Executors 工具类、3大方法
public class Demo01 {
    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newSingleThreadExecutor(); // 单个线程
        //ExecutorService threadPool = Executors.newFixedThreadPool(5);   // 创建一个固定的线程池的大小
        //ExecutorService threadPool = Executors.newCachedThreadPool();   // 可伸缩的,遇强则强,遇弱则弱

        try {
            for (int i = 0; i < 10; i++) {
                // 使用线程池之后,使用线程池来创建线程
                threadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName() + " 创建成功");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 线程池用完,程序结束,关闭线程池
            threadPool.shutdown();
        }
    }
}

 
在这里插入图片描述
 

  1. Executors.newFixedThreadPool(5) 方法的测试如下:
// Executors 工具类、3大方法
public class Demo01 {
    public static void main(String[] args) {
        //ExecutorService threadPool = Executors.newSingleThreadExecutor(); // 单个线程
        ExecutorService threadPool = Executors.newFixedThreadPool(5);   // 创建一个固定的线程池的大小
        //ExecutorService threadPool = Executors.newCachedThreadPool();   // 可伸缩的,遇强则强,遇弱则弱

        try {
            for (int i = 0; i < 10; i++) {
                // 使用线程池之后,使用线程池来创建线程
                threadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName() + " 创建成功");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 线程池用完,程序结束,关闭线程池
            threadPool.shutdown();
        }
    }
}

 
在这里插入图片描述

  1. Executors.newCachedThreadPool() 方法的测试如下:
// Executors 工具类、3大方法
public class Demo01 {
    public static void main(String[] args) {
        //ExecutorService threadPool = Executors.newSingleThreadExecutor(); // 单个线程
        //ExecutorService threadPool = Executors.newFixedThreadPool(5);   // 创建一个固定的线程池的大小
        ExecutorService threadPool = Executors.newCachedThreadPool();   // 可伸缩的,遇强则强,遇弱则弱

        try {
            for (int i = 0; i < 10; i++) {
                // 使用线程池之后,使用线程池来创建线程
                threadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName() + " 创建成功");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 线程池用完,程序结束,关闭线程池
            threadPool.shutdown();
        }
    }
}

 
在这里插入图片描述
 

线程池:七大参数

源码分析:

public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
}


public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
}

public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,	//21亿 OOM
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
}


//本质: ThreadPoolExecutor()



public ThreadPoolExecutor(int corePoolSize, //核心线程池大小
                              int maximumPoolSize, //最大核心线程池大小
                              long keepAliveTime, //超时了没有人调用就会释放
                              TimeUnit unit, //超时单位
                              BlockingQueue<Runnable> workQueue, //阻塞队列
                              ThreadFactory threadFactory,//线程工厂:创建线程的,一般不用动
                              RejectedExecutionHandler handler//拒绝策略) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
}

 
在这里插入图片描述
 
在这里插入图片描述
 
手动创建一个线程池,相关代码如下:

//Executors 工具类 3大方法
/**
 * 1. new ThreadPoolExecutor.AbortPolicy()); //银行满了 还有人进来,不处理这个人的,并且抛出异常
 * 2. new ThreadPoolExecutor.CallerRunsPolicy()); //哪来的去哪里!
 * 3. new ThreadPoolExecutor.DiscardPolicy()); //队列满了,丢掉任务,不会抛出异常
 * 4. new ThreadPoolExecutor.DiscardOldestPolicy()); //队列满了,尝试和最早的竞争,也不会抛出异常
 */
public class Demo02 {
    public static void main(String[] args) {
        //自定义线程池!工作ThreadPoolExecutor
        ExecutorService threadPool = new ThreadPoolExecutor(
                2,
                5,
                3,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.DiscardOldestPolicy()); //队列满了,尝试和最早的竞争,也不会抛出异常
        try {
            //最大承载:Deque + max
            // 超过 RejectedExecutionException
            for (int i = 1; i <= 9; i++) {
                //使用了线程池之后,使用线程池来创建线程
                threadPool.execute(() -> {
                    System.out.println(Thread.currentThread().getName() + " ok");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //线程池用完,程序结束,关闭线程池
            threadPool.shutdown();
        }

    }
}

运行结果如下:
在这里插入图片描述

 

四种拒绝策略

在这里插入图片描述
 

4种拒绝策略:
/**
 * 1. new ThreadPoolExecutor.AbortPolicy()); //银行满了 还有人进来,不处理这个人的,并且抛出异常
 * 2. new ThreadPoolExecutor.CallerRunsPolicy()); //哪来的去哪里!
 * 3. new ThreadPoolExecutor.DiscardPolicy()); //队列满了,丢掉任务,不会抛出异常
 * 4. new ThreadPoolExecutor.DiscardOldestPolicy()); //队列满了,尝试和最早的竞争,也不会抛出异常
 */

 

小结和扩展

了解:IO密集型、CPU密集型:(调优)

public class Demo03 {
    public static void main(String[] args) {
        //自定义线程池!工作ThreadPoolExecutor
        //最大线程池该如何定义
        //1、CPU密集型,几核CPU就定义为几 可以保证cpu的效率最高
        //2、IO密集型 判断你程序中十分耗IO线程,
        //      程序  15个大型任务 io十分占用资源!
        //获取CPU核数
        System.out.println(Runtime.getRuntime().availableProcessors());

        ExecutorService threadPool = new ThreadPoolExecutor(
                2,
                Runtime.getRuntime().availableProcessors(),
                3,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.DiscardOldestPolicy()); //队列满了,尝试和最早的竞争,也不会抛出异常
        try {
            //最大承载:Deque + max
            // 超过 RejectedExecutionException
            for (int i = 1; i <= 9; i++) {
                //使用了线程池之后,使用线程池来创建线程
                threadPool.execute(() -> {
                    System.out.println(Thread.currentThread().getName() + " ok");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //线程池用完,程序结束,关闭线程池
            threadPool.shutdown();
        }

    }
}

 
 
创作不易,如果有帮助到你,请给题解点个赞和收藏,让更多的人看到!!!
关注博主不迷路,内容持续更新中。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注