java线程基础知识整理
目录
线程基本概念
1、什么是进程?什么是线程?
进程是一个应用程序,线程是一个进程中的执行场景/执行单元。一个进程可以启动多个线程。在java语言中对于两个线程A和B,堆内存和方法区内存共享。但是栈内存独立,一个线程一个栈。 在使用了多线程机制之后,main()方法结束了,只是主线程结束了,主栈空了,但其他线程不一定结束,其他栈(线程)可能还在压栈弹栈。
1、java实现线程
java语言支持多线程机制。并且java已经实现了多线程(java.lang.Thread类和java.lang.Runnable接口)
第一种实现方式(继承java.lang.Thread类并重写run方法)
public class Thread_01 extends Thread {
@Override
public void run() {
super.run();
System.out.println("第一个线程");
}
public static void main(String[] args) {
Thread thread=new Thread(new Thread_01(),"first_thread"); //创建线程对象
thread.start(); //启动线程 start方法使线程处于就绪队列,等待CPU调用
}
}
第二种实现方式(实现java.lang.Runnable接口并实现run方法)
public class Thread_02 implements Runnable {
@Override
public void run() {
System.out.println("第一个线程");
}
public static void main(String[] args) {
Thread thread=new Thread(new Thread_01(),"first_thread"); //创建线程对象
thread.start(); //启动线程 start方法使线程处于就绪队列,等待CPU调用
}
}
通常使用第二种方法,因为一个类实现了接口还可以继承其他类
注意:start方法和run方法的区别!!!
run方法不会启动线程。
start方法的作用是:启动一个线程,在JVM中为线程开辟一个新的栈空间。之后start方法就结束了。线程启动成功并进入排队等待序列。等到被CPU调用到,就会自动调用run方法。
2、线程的生命周期
3、线程常用的方法
3.1、sleep()
public static native void sleep(long millis) throws InterruptedException;
作用:让线程进入休眠,进入“阻塞状态”。放弃占有CPU时间片,让其他线程使用。
public class Thread_03 {
public static void main(String[] args) throws InterruptedException {
Thread thread=new Thread(new Thread_03_1(),"Thread_03_1");
thread.start();
int count=1; //计数器
while(true){
System.out.println("Thread_03_1线程沉睡了"+count+++"秒");
Thread.sleep(1000);
if(count>5){
break;
}
}
}
}
class Thread_03_1 implements Runnable{
@Override
public void run() {
try {
Thread.sleep(1000*5); //让线程沉睡(等待) 5秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
}
}
3.2、interrupt方法
interrupt方法可以中断线程的睡眠,依靠了java异常处理机制
public class Thread_03 {
public static void main(String[] args) throws InterruptedException {
Thread thread=new Thread(new Thread_03_1(),"Thread_03_1");
thread.start();
int count=1; //计数器
while(true){
System.out.println("Thread_03_1线程沉睡了"+count+++"秒");
Thread.sleep(1000);
if(count==3){
System.out.println("打断Thread_03_1睡眠");
thread.interrupt();
break;
}
}
}
}
class Thread_03_1 implements Runnable{
@Override
public void run() {
try {
Thread.sleep(1000*5); //让线程沉睡(等待) 5秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"醒了");
}
}
线程Thread_03_1原计划沉睡5秒,在它睡到3秒时,使用interrupt方法打断其睡眠。
3.3、stop方法
stop方法可以强制终止一个线程的执行。不过这种方式容易丢失数据。因为这种方式会直接杀死线程,线程没有保存的数据会丢失。所以不建议使用
public class Thread_04 {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new Thread_04_1(), "Thread_03_1");
thread.start();
//线程Thread_03_1执行5秒后,强制结束线程Thread_03_1
Thread.sleep(1000*6);
System.out.println("强制终止线程Thread_03_1");
thread.stop(); //强制终止线程
}
}
class Thread_04_1 implements Runnable{
@Override
public void run() {
for(int i=1;i<1000;i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread()+"-->"+i+"秒");
}
}
}
建议使用如下方法结束一个线程:在线程类中增加一个布尔类型的变量run,通过改变run的值,来控制线程运行/停止状态。
public class Thread_05 {
public static void main(String[] args) throws InterruptedException {
Thread_05_1 t=new Thread_05_1();
Thread thread = new Thread(t, "Thread_05_1");
thread.start();
//等候5秒之后,终止该线程
Thread.sleep(1000*5);
t.run=false;
System.out.println(thread.getName()+"线程已暂停");
}
}
class Thread_05_1 implements Runnable{
boolean run=true; //通过引入一个布尔类型的变量,来标记该线程的状态(运行/停止)
@Override
public void run() {
for (int i = 1; i < 1000; i++) {
if (run) {
System.out.println(Thread.currentThread().getName() + "-->" + i + "秒");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
/**
* return就表示该线程结束了
* 如果有什么需要保存的,可以写在return之前
*/
return;
}
}
}
}
4、线程调度
4.1、常见的线程调度模型
抢占式调度模型、均分式调度模型
4.2、java中提供的线程调度方法
void setPriority(int newPriority) //设置线程优先级
int getPriority() //获取线程优先级
最低优先级:1 默认优先级:5 最高优先级:10
优先级高的线程抢占的CPU时间片就多一些,处于运行状态的时间片就多一些
public class Thread_06 {
public static void main(String[] args) {
Thread_06_1 t61=new Thread_06_1();
Thread_06_2 t62=new Thread_06_2();
Thread_06_3 t63=new Thread_06_3();
Thread thread1=new Thread(t61,"Thread_06_1");
Thread thread2=new Thread(t62,"Thread_06_2");
Thread thread3=new Thread(t63,"Thread_06_3");
//设置线程优先级
thread1.setPriority(1); thread2.setPriority(2); thread3.setPriority(10);
thread1.start(); thread2.start(); thread3.start();
System.out.println("主线程优先级为:"+Thread.currentThread().getPriority());
for(int i=0;i<1000;i++){
System.out.println(Thread.currentThread().getName()+"-->"+i);
}
}
}
class Thread_06_1 implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"优先级为:"+Thread.currentThread().getPriority());
for(int i=0;i<1000;i++){
System.out.println(Thread.currentThread().getName()+"-->"+i);
}
}
}
class Thread_06_2 implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"优先级为:"+Thread.currentThread().getPriority());
for(int i=0;i<1000;i++){
System.out.println(Thread.currentThread().getName()+"-->"+i);
}
}
}
class Thread_06_3 implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"优先级为:"+Thread.currentThread().getPriority());
for(int i=0;i<1000;i++){
System.out.println(Thread.currentThread().getName()+"-->"+i);
}
}
}
4.3、线程让步
yield方法:使当前线程暂停,回到就绪状态,让给其他线程
public static native void yield();
public class Thread_07 {
public static void main(String[] args) {
Thread_07_1 tt=new Thread_07_1();
Thread thread=new Thread(tt,"Thread_07_1");
thread.start();
for(int i=0;i<100;i++){
System.out.println(Thread.currentThread().getName()+"-->"+i);
}
}
}
class Thread_07_1 implements Runnable{
@Override
public void run() {
for(int i=0;i<100;i++){
if(i%10==0){
System.out.println(Thread.currentThread().getName()+"暂停了一下");
Thread.yield(); //让当前线程暂停一下
}
System.out.println(Thread.currentThread().getName()+"-->"+i);
}
}
}
4.4、线程合并
join方法可以使得在t.join()中让CPU优先执行完t。将t合并到当前线程中,使当前线程受阻,t线程执行直到结束。
public class Thread_08 {
public static void main(String[] args) throws InterruptedException {
Thread_08_1 thread_08_1=new Thread_08_1();
Thread_08_2 thread_08_2=new Thread_08_2();
Thread thread=new Thread(thread_08_1,"Thread_08_1");
Thread thread1=new Thread(thread_08_2,"Thread_08_2");
thread.start();
//合并线程
thread.join(); //t合并到当前线程中,当前线程受阻塞,t线程执行直到结束
thread1.start();
thread1.join();
//Thread.currentThread().join();
System.out.println("main over");
}
}
class Thread_08_1 implements Runnable{
@Override
public void run() {
for(int i=0;i<3;i++){
System.out.println(Thread.currentThread().getName()+"-->"+i);
}
}
}
class Thread_08_2 implements Runnable{
@Override
public void run() {
for(int i=0;i<3;i++){
System.out.println(Thread.currentThread().getName()+"-->"+i);
}
}
}
join()的底层实现代码。
public final void join() throws InterruptedException {
join(0);
}
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
join()是在底层调用了wait方法,当主线程调用了thread.join()之后,主线程进入此方法,调用join()方法中的wait(0)方法,wait(0)表示无限等待直到被notify。即主线程会无限等待thread线程执行完成。
public final void wait(long timeout, int nanos) throws InterruptedException {
if (timeout < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}
if (nanos > 0) {
timeout++;
}
wait(timeout);
}
4.5、线程安全
数据在多线程并发的环境下会存在安全问题。例如如果多个用户想要修改某个共享的数据,就会引发线程安全问题。因此需要引入线程同步机制(即线程排队执行,不能并发)
4.5.1、线程同步的实现
java里面通过关键字synchronized给线程加锁。线程会获取锁,并独占cpu,只有当线程释放了锁之后,其余线程拿到锁之后才能运行。
当一个线程在运行状态时遇到synchronized关键字,该线程就会放弃占有的cpu时间片,在锁池里面找共享对象的对象锁。
一个线程同步synchronized的例子——模拟ATM机取款
Account类
package ATM;
/*
银行账户,使用线程同步机制,解决线程安全问题
*/
public class Account {
private String accountName; //账户名
private double remain; //余额
Object obj=new Object();
public Account(String accountName, int remain) {
this.accountName = accountName;
this.remain = remain;
}
public String getAccountName() {
return accountName;
}
public void setAccountName(String accountName) {
this.accountName = accountName;
}
public double getRemain() {
return remain;
}
public void setRemain(double remain) {
this.remain = remain;
}
//取款方法
public void withdraw(double money) {
//synchronized (obj){ //obj是一个全局变量,实例一次Account创建一个obj对象,是可以被共享的
/*Object obj1=new Object();
synchronized (obj1){*/ //obj1是一个局部变量,每调用一次run方法则会创建一个obj1对象,所以obj1不是共享对象
//synchronized (null){ //报错:空指针
//synchronized ("abc"){ //"abc"在字符串常量池当中,会让所有的线程都同步
synchronized (this){
double before = this.getRemain(); //取款之前的余额
double after = before - money; //取款之后的余额
try{
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.setRemain(after); //更新余额
}
}
}
AccountThread类
package ATM;
public class AccountThread extends Thread {
//两个线程必须共享同一个账户对象
private Account act;
private double money; //取款金额
//通过构造方法传递过来构造对象
public AccountThread(Account act,double money) {
this.act = act;
this.money=money;
}
@Override
public void run() { //run方法执行表示取款操作
act.withdraw(money);
System.out.println(Thread.currentThread().getName()+"在账户"+
act.getAccountName()+"取款"+money+"之后余额为:"+act.getRemain());
}
}
启动类:Test类
package ATM;
public class Test {
public static void main(String[] args) {
Account act=new Account("act001",10000); //创建账户对象
//创建两个线程,对同一账户取款
Thread threadA=new AccountThread(act,5000);
Thread threadB=new AccountThread(act,3000);
threadA.setName("小明");
threadB.setName("小华");
threadA.start();
threadB.start();
}
}
结果:
//synchronized可以用在实例方法上,此时锁是this
public synchronized void withdraw(double money) {
double before = this.getRemain(); //取款之前的余额
double after = before - money; //取款之后的余额
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.setRemain(after); //更新余额
}
}
可以在实例方法上使用synchronized,synchronized出现在实例方法上,锁一定是this,不能是其他的对象了。所以这种方式不灵活。
另外还有一个缺点:synchronized出现在实例方法上,表示整个方法体都需要同步,可能会无故扩大同步的范围,导致程序的执行效率降低。所以这种方式不常用。
4.5.2、java中的线程安全性
java中有三大变量 :1、实例变量(在堆中) 2、静态变量(在方法中) 3、局部变量(在栈中)
局部变量不会有线程安全问题,因为局部变量不共享,局部变量在栈中,一个线程一个栈。
常量不会有线程安全问题,因为常量不会被改变。
实例变量在堆中,堆只有1个。静态变量在方法区中,方法区只有1个。由于堆和方法区都是多线程可共享的,所以实例变量和静态变量可能存在线程安全问题。
ArrayList、HashMap 、HashSet 是非线程安全的
Vector、Hashtable 是线程安全的
4.5.3、synchronized总结
synchronized有三种写法:
第一种:同步代码块(灵活)
synchronized(线程共享对象){
同步代码块;
}
第二种:在实例方法上使用 synchronized
表示共享对象一定是this,并且同步代码块是整个方法体。
第三种:在静态方法上使用 synchronized
表示找类锁。
类锁永远只有1把(就算创建了100个对象,那类锁也只有1把)
对象锁:1个对象1把锁,100个对象100把锁。 类锁:100个对象,也可能只是1把类锁。
package Synchronized;
//Q:doOther方法执行的时候需要等待doSome方法的结束吗?
//A:需要,因为静态方法是类锁,不管创建了几个对象,类锁只有1把
public class Exam01 {
public static void main(String[] args) throws InterruptedException {
MyClass mc1 = new MyClass();
MyClass mc2 = new MyClass();
Thread t1 = new Mythread(mc1);
Thread t2 = new Mythread(mc2);
t1.setName("t1");
t2.setName("t2");
t1.start();
Thread.sleep(1000);
t2.start();
}
}
class Mythread extends Thread {
private MyClass mc;
public Mythread(MyClass mc) {
this.mc = mc;
}
@Override
public void run() {
super.run();
if (Thread.currentThread().getName().equals("t1")) {
try {
mc.doSome();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (Thread.currentThread().getName().equals("t2")) {
mc.doOther();
}
}
}
class MyClass {
//synchronized出现在静态方法上是走 类锁
public synchronized static void doSome() throws InterruptedException {
System.out.println("doSome begin");
Thread.sleep(1000 * 5);
System.out.println("doSome over");
}
public synchronized static void doOther() {
System.out.println("doOther begin");
System.out.println("doOther over");
}
}
4.5.4、死锁
一个简单的死锁实现的例子
package Thread;
/*
实现一个死锁,
t1中obj1锁上后就睡了,t2中obj2锁上后就睡了。
t1想要释放obj1锁,就必须请求到obj2锁
t2想要释放obj2锁,就必须请求到obj1锁
t1与t2互相请求锁,但彼此都无法释放锁,所以形成了死锁
*/
public class Dead_lock {
public static void main(String[] args) {
Object obj1 = new Object();
Object obj2 = new Object();
//t1,t2两个线程共享o1,o2
Thread t1 = new MyThread_1(obj1, obj2);
Thread t2 = new MyThread_2(obj1, obj2);
t1.start();
t2.start();
}
}
class MyThread_1 extends Thread {
Object obj1 = new Object();
Object obj2 = new Object();
public MyThread_1(Object obj1, Object obj2) {
this.obj1 = obj1;
this.obj2 = obj2;
}
@Override
public void run() {
synchronized (obj1) {
try {
Thread.sleep(1000 * 3);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj2) {
}
}
}
}
class MyThread_2 extends Thread {
Object obj1 = new Object();
Object obj2 = new Object();
public MyThread_2(Object obj1, Object obj2) {
this.obj1 = obj1;
this.obj2 = obj2;
}
@Override
public void run() {
synchronized (obj2) {
try {
Thread.sleep(1000 * 3);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj1) {
}
}
}
}
注:synchronized在开发中最好不要嵌套使用,一不小心就可能导致死锁现象发生
4.6、守护线程
java语言中线程可分为两大类:
一类是:用户线程 例如:main方法主线程
一类是:守护线程(后台线程) 例如java垃圾回收线程
4.6.1、守护线程的特点
一般守护线程是一个死循环,所有用户线程只要结束,守护线程自动结束。
守护线程一般会用在一些定时任务,例如每天0点系统自动备份需要用到定时器,我们可以将定时器设置为守护线程一直在那里看着。每到0点就备份一次。所有的用户线程如果结束了,守护线程就自动退出。
4.6.2、一个简单的守护线程的例子
package Thread;
/**
* 用户线程备份数据,守护线程守护。
*/
public class GuardThread {
public static void main(String[] args) {
Thread thread = new BakDataThread();
thread.setName("备份数据的线程");
//启动线程之前,将线程设置为守护线程
thread.setDaemon(true);
thread.start();
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + "--->" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class BakDataThread extends Thread {
@Override
public void run() {
int i = 0;
//即使是死循环,但由于该线程是守护者。当用户线程结束,守护线程自动终止
while (true) {
System.out.println(Thread.currentThread().getName() + "--->" + i++);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
5、定时任务
5.1、实现一个定时器
package Thread;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
public class TimerTest {
public static void main(String[] args) throws ParseException {
//创建定时器对象
Timer timer = new Timer();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date firstTime = sdf.parse("2020-07-25 19:10:30");
//指定定时任务
//timer.schedule(定时任务,第一次执行时间时间,间隔多久执行一次)
timer.schedule(new LogTimerTask(), firstTime, 1000 * 5);
}
}
//编写一个定时任务
class LogTimerTask extends TimerTask {
@Override
public void run() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String strTime = sdf.format(new Date());
System.out.println(strTime + ":成功完成了一次数据备份!");
}
}
6、通过Callable接口实现一个线程
使用Callable接口实现线程,可以获得该线程的返回值(JDK8新特性)
一个简单的实例:
package Thread;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class ThirdWay {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask futureTask = new FutureTask(new Task());
Thread thread = new Thread(futureTask);
thread.start();
Object object = futureTask.get(); //通过get方法可以获取当前线程的返回值
////主线程中这里的程序必须等待get()方法结束才执行
// get()方法为了拿另一个线程的执行结果需要等待其执行完成,因此要等待较长时间
System.out.print("线程执行结果:");
System.out.println(object);
}
}
class Task implements Callable {
@Override
public Object call() throws Exception {
System.out.println("call method begin");
Thread.sleep(1000 * 10);
System.out.println("call method end");
int a = 100;
int b = 200;
return a + b;
}
}
FutureTask类相关源码
public FutureTask(Callable<V> callable) {
if (callable == null)
throw new NullPointerException();
this.callable = callable;
this.state = NEW; // ensure visibility of callable
}
public V get() throws InterruptedException, ExecutionException {
int s = state;
if (s <= COMPLETING)
s = awaitDone(false, 0L);
return report(s);
}
Future类中实现了get方法获取传入线程的返回结果
7、Object类中的wait和notify方法
7.1、wait和notify方法介绍
wait和notify方法不是线程对象的方法,不能通过线程对象调用。
Object object=new Object();
object.wait();//object.wait()让正在object对象上活动的线程进入等待状态,无限等待,直到被唤醒为止
object.notify();//object.notify()唤醒正在object对象上等待的线程
object.notifyAll();//object.notifyAll唤醒正在object对象上等待的所有线程
7.2、生产者和消费者模式
一个简单的生产者和消费者实例
模拟生产者生产一个,消费者就消费一个。让仓库始终零库存。
package Thread;
import java.awt.*;
import java.util.*;
import java.util.List;
public class Producer_Consumer {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
Thread thread1 = new Thread(new Consumer(list), "消费者线程");
Thread thread2 = new Thread(new Producer(list), "生产者线程");
thread1.start();
thread2.start();
}
}
//消费者线程
class Consumer implements Runnable {
//仓库
private List<String> list;
public Consumer(List<String> list) {
this.list = list;
}
@Override
public void run() {
//消费
while (true) {
synchronized (list) {
//如果仓库已经空了,消费者线程等待并释放list集合的锁
if (list.size() == 0) {
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//仓库中有商品,消费者进行消费
String str = list.remove(0);
System.out.println(Thread.currentThread().getName() + " 消费 " + str);
list.notify(); //唤醒生产者
}
}
}
}
//生产者线程
class Producer implements Runnable {
//仓库
private List<String> list;
public Producer(List<String> list) {
this.list = list;
}
@Override
public void run() {
//生产
while (true) {
synchronized (list) {
//如果仓库里有东西,则停止生产。生产者线程等待并释放list集合的锁
if (list.size() > 0) {
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
list.add("商品");
System.out.println(Thread.currentThread().getName() + " 生产 " + list.get(0));
list.notify(); //唤醒消费者
}
}
}
}
:
7.3、实现奇偶数的交替输出
package Thread;
/**
* 使用生产者和消费者模式实现两个线程交替输出:一个线程负责输出奇数,另一个线程负责输出偶数
*/
public class Number {
public static void main(String[] args) throws InterruptedException {
Num num = new Num(0);
Thread thread1 = new Thread(new Odd(num), "Odd");
Thread thread2 = new Thread(new Event(num), "Event");
thread1.start();
thread2.start();
}
}
class Odd implements Runnable {
Num num;
public Odd(Num num) {
this.num = num;
}
@Override
public void run() {
while (true) {
synchronized (num) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (num.getI() % 2 == 0) {
try {
num.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + "--->" + num.printNum());
num.notifyAll();
}
}
}
}
class Event implements Runnable {
Num num;
public Event(Num num) {
this.num = num;
}
@Override
public void run() {
while (true) {
synchronized (num) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (num.getI() % 2 != 0) {
try {
num.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + "--->" + num.printNum());
num.notifyAll();
}
}
}
}
class Num {
private int i = 0;
public Num(int i) {
this.i = i;
}
public int getI() {
return i;
}
public void setI(int i) {
this.i = i;
}
int printNum() {
return i++;
}
}
本文地址:https://blog.csdn.net/weixin_42032770/article/details/107536554
上一篇: 软文推广平台应该这么选