0

0

简单总结Java多线程知识点

WBOY

WBOY

发布时间:2022-10-12 14:58:11

|

1349人浏览过

|

来源于掘金

转载

本篇文章给大家带来了关于java的相关知识,其中主要介绍了关于多线程的相关问题,一个进程可以并发多个线程,每条线程并行执行不同的任务,线程是进程的基本单位,是一个单一顺序的控制流,下面一起来看一下,希望对大家有帮助。

简单总结Java多线程知识点

推荐学习:《java视频教程

Java 提供了多线程编程的内置支持,让我们可以轻松开发多线程应用。

Java 中我们最为熟悉的线程就是 main 线程——主线程。

立即学习Java免费学习笔记(深入)”;

一个进程可以并发多个线程,每条线程并行执行不同的任务。线程是进程的基本单位,是一个单一顺序的控制流,一个进程一直运行,直到所有的“非守护线程”都结束运行后才能结束。Java 中常见的守护线程有:垃圾回收线程、

这里简要述说以下并发和并行的区别。并发:同一时间段内有多个任务在运行并行:同一时间点上有多个任务同时在运行

多线程可以帮助我们高效地执行任务,合理利用 CPU 资源,充分地发挥多核 CPU 的性能。但是多线程也并不总是能够让程序高效运行的,多线程切换带来的开销、线程死锁、线程异常等等问题,都会使得多线程开发较单线程开发更麻烦。因此,有必要学习 Java 多线程的相关知识,从而提高开发效率。

1 创建多线程

根据官方文档 Thread (Java Platform SE 8 ) (oracle.com) 中 java.lang.Thread 的说明,可以看到线程的创建方式主要有两种:

There are two ways to create a new thread of execution. One is to declare a class to be a subclass of Thread. This subclass should override the run method of class Thread. An instance of the subclass can then be allocated and started.

The other way to create a thread is to declare a class that implements the Runnable interface. That class then implements the run method. An instance of the class can then be allocated, passed as an argument when creating Thread, and started.

可以看到,有两种创建线程的方式:

  • 声明一个类继承 Thread 类,这个子类需要重写 run 方法,随后创建这个子类的实例,这个实例就可以创建并启动一个线程执行任务;

  • 声明一个类实现接口 Runnable 并实现 run 方法。这个类的实例作为参数分配给一个 Thread 实例,随后使用 Thread 实例创建并启动线程即可

除此之外的创建线程的方法,诸如使用 Callable 和 FutureTask、线程池等等,无非是在此基础上的扩展,查看源码可以看到 FutureTask 也实现了 Runnable 接口。

使用继承 Thread 类的方法创建线程的代码:

/**
 * 使用继承 Thread 类的方法创建线程
 */
public class CreateOne {
    public static void main(String[] args) {
        Thread t = new MySubThread();
        t.start();
    }
}
class MySubThread extends Thread {
    @Override
    public void run() {
        // currentThread() 是 Thread 的静态方法,可以获取正在执行当前代码的线程实例
        System.out.println(Thread.currentThread().getName() + "执行任务");
    }
}
// ================================== 运行结果
Thread-0执行任务

使用实现 Runnable 接口的方法创建线程的代码:

/**
 * 使用实现 Runnable 接口的方法创建线程
 */
public class CreateTwo {
    public static void main(String[] args) {
        RunnableImpl r = new RunnableImpl();
        Thread t = new Thread(r);
        t.start();
    }
}
class RunnableImpl implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "执行任务");
    }
}
// ================================== 运行结果
Thread-0执行任务

1.1 孰优孰劣

创建线程虽然有两种方法,但是在实际开发中,使用实现接口 Runnable 的方法更好,原因如下:

查看 Thread 的 run 方法,可以看到:

// Thread 实例的成员变量 target 是一个 Runnable 实例,可以通过 Thread 的构造方法传入
private Runnable target;
// 如果有传入 Runnable 实例,那么就执行它的 run 方法
// 如果重写,就完全执行我们自己的逻辑
public void run() {
    if (target != null) {
        target.run();
    }
}

查看上面的源码,我们可以知道,Thread 类并不是定义执行任务的主体,而是 Runnable 定义执行任务内容,Thread 调用执行,从而实现线程与任务的解耦。

由于线程与任务解耦,我们可以复用线程,而不是当需要执行任务就去创建线程、执行完毕就销毁线程,这样带来的系统开销太大。这也是线程池的基本思想。

此外,Java 只只支持单继承,如果继承 Thread 使用多线程,那么后续需要通过继承的方式扩展功能,那会相当麻烦。

2 start 和 run 方法

从上面可以得知,有两种创建线程的方式,我们通过 Thread 类或 Runnable 接口的 run 方法定义任务,通过 Thread 的 start 方法创建并启动线程。

我们不能通过 run 方法启动并创建一个线程,它只是一个普通方法,如果直接调用这个方法,其实只是调用这个方法的线程在执行任务罢了。

// 将上面的代码修改一下,查看执行结果
public class CreateOne {
    public static void main(String[] args) {
        Thread t = new MySubThread();
        t.run();
        //t.start();
    }
}
// ===================== 执行结果
main执行任务

查看 start 方法的源码:

// 线程状态,为 0 表示还未启动
private volatile int threadStatus = 0;
// 同步方法,确保创建、启动线程是线程安全的
public synchronized void start() {
    // 如果线程状态不为 0,那么抛出异常——即线程已经创建了
    if (threadStatus != 0)
        throw new IllegalThreadStateException();
// 将当前线程添加到线程组
    group.add(this);
    boolean started = false;
    try {
        // 这是一个本地方法
        start0();
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
        }
    }
}
// 由本地方法实现,只需要知道,该方法调用后会创建一个线程,并且会执行 run 方法
private native void start0();

由上面的源码可以得知:

  • 创建并启动一个线程是线程安全的

  • start() 方法不能反复调用,否则会抛出异常

3 怎么停止线程

线程并不是无休止地执行下去的,通常情况下,线程停止的条件有:

  • run 方法执行结束

  • 线程发生异常,但是没有捕获处理

除此之外,我们还需要自定义某些情况下需要通知线程停止,例如:

用户主动取消任务

任务执行时间超时、出错

出现故障,服务需要快速停止

...

为什么不能直接简单粗暴的停止线程呢?通过通知线程停止任务,我们可以更优雅地停止线程,让线程保存问题现场、记录日志、发送警报、友好提示等等,令线程在合适的代码位置停止线程,从而避免一些数据丢失等情况。

令线程停止的方法是让线程捕获中断异常或检测中断标志位,从而优雅地停止线程,这是推荐的做法。而不推荐的做法有,使用被标记为过时的方法:stop,resume,suspend,这些方法可能会造成死锁、线程不安全等情况,由于已经过时了,所以不做过多介绍。

3.1 通知线程中断

我们要使用通知的方式停止目标线程,通过以下方法,希望能够帮助你掌握中断线程的方法:

/**
 * 中断线程
 */
public class InterruptThread {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            long i = 0;
            // isInterrupted() 检测当前线程是否处于中断状态
            while (i < Long.MAX_VALUE && !Thread.currentThread().isInterrupted()) {
                i++;
            }
            System.out.println(i);
        });
        
        t.start();
        // 主线程睡眠 1 秒,通知线程中断
        Thread.sleep(1000);
        t.interrupt();
    }
}
// 运行结果
1436125519

这是中断线程的方法之一,还有其他方法,当线程处于阻塞状态时,线程并不能运行到检测线程状态的代码位置,然后正确响应中断,这个时候,我们需要通过捕获异常的方式停止线程:

/**
 * 通过捕获中断异常停止线程
 */
public class InterruptThreadByException {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()->{
            long i = 0;
            while (i < Long.MAX_VALUE) {
                i++;
                try {
                    // 线程大部分时间处于阻塞状态,sleep 方法会抛出中断异常 InterruptedException
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    // 捕获到中断异常,代表线程被通知中断,做出相应处理再停止线程
                    System.out.println("线程收到中断通知   " + i);
                    // 如果 try-catch 在 while 代码块之外,可以不用 return 也可以结束代码
                    // 在 while 代码块之内,如果没有 return / break,那么还是会进入下一次循环,并不能正确停止
                    return;
                }
            }
        });
        t.start();
        Thread.sleep(1000);
        t.interrupt();
    }
}
// 运行结果
线程收到中断通知   10

以上,就是停止线程的正确做法,此外,捕获中断异常后,会清除线程的中断状态,在实际开发中需要特别注意。例如,修改上面的代码:

public class InterruptThreadByException {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()->{
            long i = 0;
            while (i < Long.MAX_VALUE) {
                i++;
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    System.out.println("线程收到中断通知   " + i);
                    //  添加这行代码,捕获到中断异常后,检测中断状态,中断状态为 false
                    System.out.println(Thread.currentThread().isInterrupted());
                    return;
                }
            }
        });
        t.start();
        Thread.sleep(1000);
        t.interrupt();
    }
}

所以,在线程中,如果调用了其他方法,如果该方法有异常发生,那么:

将异常抛出,而不是在子方法内部捕获处理,由 run 方法统一处理异常

捕获异常,并重新通知当前线程中断,Thread.currentThread().interrupt()

例如:

public class SubMethodException {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new ExceptionRunnableA());
        Thread t2 = new Thread(new ExceptionRunnableB());
        t1.start();
        t2.start();
        Thread.sleep(1000);
        t1.interrupt();
        t2.interrupt();
    }
}
class ExceptionRunnableA implements Runnable {
    @Override
    public void run() {
        try {
            while (true) {
                method();
            }
        } catch (InterruptedException e) {
            System.out.println("run 方法内部捕获中断异常");
        }
    }
    public void method() throws InterruptedException {
        Thread.sleep(100000L);
    }
}
class ExceptionRunnableB implements Runnable {
    @Override
    public void run() {
        while (!Thread.currentThread().isInterrupted()) {
            method();
        }
    }
    public void method()  {
        try {
            Thread.sleep(100000L);
        } catch (InterruptedException e) {
            System.out.println("子方法内部捕获中断异常");
            // 如果不重新设置中断,线程将不能正确响应中断
            Thread.currentThread().interrupt();
        }
    }
}

综上,总结出令线程正确停止的方法为:

使用 interrupt() 方法通知目标线程停止,标记目标线程的中断状态为 true

目标线程通过 isInterrupted() 不时地检测线程的中断状态,根据情况决定是否停止线程

如果线程使用了阻塞方法例如 sleep(),那么需要捕获中断异常并处理中断通知,捕获了中断异常会重置中断标记位

如果 run() 方法调用了其他子方法,那么子方法:

将异常抛出,传递到顶层 run 方法,由 run 方法统一处理

将异常捕获,同时重新通知当前线程中断

下面再说说关于中断的几个相关方法和一些会抛出中断异常的方法,使用的时候需要特别注意。

3.2 线程中断的相关方法

  • interrupt() 实例方法,通知目标线程中断。

  • static interrupted() 静态方法,获取当前线程是否处于中断状态,会重置中断状态,即如果中断状态为 true,那么调用后中断状态为 false。方法内部通过    Thread.currentThread() 获取执行线程实例。

  • isInterrupted() 实例方法,获取线程的中断状态,不会清除中断状态。

3.3 阻塞并能响应中断的方法

  • Object.wait()

  • Thread.sleep()

  • Thread.join()

  • BlockingQueue.take() / put()

  • Lock.lockInterruptibly()

  • CountDownLatch.await()

  • CyclicBarrier.await()

  • Exchanger.exchange()

4 线程的生命周期

线程的生命周期状态由六部分组成:

19.png

可以用一张图总结线程的生命周期,以及各个过程之间是如何转换的:

20.png

5 Thread 和 Object 中的线程方法

现在,我们已经知道了线程的创建、启动、停止以及线程的生命周期了,那么,再来看看线程相关的方法有哪些。

首先,看看 Thread 中的一些方法:

网梦购物系统
网梦购物系统

一套功能完善、性能稳定的经典网上购物系统,掌握了一整套从算法,数据结构到产品安全性方面的领先技术,使程序无论在安全性、负载能力方面均获得了成功,新版购物系统集成多种在线支付方式,全后台操作管理,并集成了Ewebedit编辑器,即使只有电脑基础知识的人也能够轻松操作和管理部分新增功能:集成多种网上支付形式,后台灵活切换增加Ewebedit编辑器,添加信息更容易!简洁、明快、新颖的界面,给人以美的感觉

下载

21.png

再看看 Object 中的相关方法:

22.png

运行以下代码,查看 wait() 和 sleep() 是否会释放同步锁

/**
* 证明 sleep 不会释放锁,wait 会释放锁
*/
public class SleepAndWait {
    private static Object lock = new Object();
    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            synchronized (lock) {
                System.out.println(Thread.currentThread().getName() + "获得同步锁,调用 wait() 方法");
                try {
                    lock.wait(2000);
                    System.out.println(Thread.currentThread().getName() + "重新获得同步锁");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        Thread t2 = new Thread(()->{
            synchronized (lock) {
                System.out.println(Thread.currentThread().getName() + "获得同步锁,唤醒另一个线程,调用 sleep()");
                lock.notify();
                try {
                    // 如果 sleep() 会释放锁,那么在此期间,上面的线程将会继续运行,即 sleep 不会释放同步锁
                    Thread.sleep(2000);
                    // 如果执行 wait 方法,那么上面的线程将会继续执行,证明 wait 方法会释放锁
                    //lock.wait(2000);
                    System.out.println(Thread.currentThread().getName() + "sleep 结束");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t1.start();
        t2.start();
    }
}

上面的代码已经证明了 sleep() 不会释放同步锁,此外,sleep() 也不会释放 Lock 的锁,运行以下代码查看结果:

/**
 * sleep 不会释放 Lock 锁
 */
public class SleepDontReleaseLock implements Runnable {
    private static Lock lock = new ReentrantLock();
    @Override
    public void run() {
        // 调用 lock 方法,线程会尝试持有该锁对象,如果已经被其他线程锁住,那么当前线程会进入阻塞状态
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "获得 lock 锁");
            // 如果 sleep 会释放 Lock 锁,那么另一个线程会马上打印上面的语句
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName() + "释放 lock 锁");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 当前线程释放锁,让其他线程可以占有锁
            lock.unlock();
        }
    }
    public static void main(String[] args) {
        SleepDontReleaseLock task = new SleepDontReleaseLock();
        new Thread(task).start();
        new Thread(task).start();
    }
}

5.1 wait 和 sleep 的异同

接下来总结 Object.wait() 和 Thread.sleep() 方法的异同点。

相同点:

  • 都会使线程进入阻塞状态

  • 都可以响应中断

不同点:

  • wait() 是 Object 的实例方法,sleep() 是 Thread 的静态方法

  • sleep() 需要指定时间

  • wait() 会释放锁,sleep() 不会释放锁,包括同步锁和 Lock 锁

  • wait() 必须配合 synchronized 使用

6 线程的相关属性

现在我们已经对 Java 中的多线程有一定的了解了,我们再看看 Java 中线程 Thread 的一些相关属性,即它的成员变量。

23.png

运行以下代码,了解线程的相关属性

public class ThreadFields {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            // 自定义线程的 ID 并不是从 2 开始
            System.out.println("线程 " + Thread.currentThread().getName()
                               + " 的线程 ID " + Thread.currentThread().getId());
            while (true) {
                // 守护线程一直运行,但是 用户线程即这里的主线程结束后,也会随着虚拟机一起停止
            }
        });
        // 自定义线程名字
        t.setName("自定义线程");
        // 将其设置为守护线程
        t.setDaemon(true);
        // 设置优先级 Thread.MIN_PRIORITY = 1     Thread.MAX_PRIORITY = 10
        t.setPriority(Thread.MIN_PRIORITY);
        t.start();
        // 主线程的 ID 为 1
        System.out.println("线程 " + Thread.currentThread().getName() + " 的线程 ID " + Thread.currentThread().getId());
        Thread.sleep(3000);
    }
}

7 全局异常处理

在子线程中,如果发生了异常我们能够及时捕获并处理,那么对程序运行并不会有什么恶劣影响。

但是,如果发生了一些未捕获的异常,在多线程情况下,这些异常打印出来的堆栈信息,很容易淹没在庞大的日志中,我们可能很难察觉到,并且不好排查问题。

如果对这些异常都做捕获处理,那么就会造成代码的冗余,编写起来也不方便。

因此,我们可以编写一个全局异常处理器来处理子线程中抛出的异常,统一地处理,解耦代码。

7.1 源码查看

在讲解如何处理子线程的异常问题前,我们先看看 JVM 默认情况下,是如何处理未捕获的异常的。

查看 Thread 的源码:

public class Thread implements Runnable {
    【1】当发生未捕获的异常时,JVM 会调用该方法,并传递异常信息给异常处理器
        可以在这里打下断点,在线程中抛出异常不捕获,IDEA 会跳转到这里
    // 向处理程序发送未捕获的异常。此方法仅由JVM调用。
private void dispatchUncaughtException(Throwable e) {
        【2】查看第 9 行代码,可以看到如果没有指定异常处理器,默认是线程组作为异常处理器
        【3】调用这个异常处理器的处理方法,处理异常,查看第 15 行
        getUncaughtExceptionHandler().uncaughtException(this, e);
    }
    
    public UncaughtExceptionHandler getUncaughtExceptionHandler() {
        return uncaughtExceptionHandler != null ?
            uncaughtExceptionHandler : group;
    }
    
    【4】UncaughtExceptionHandler 是 Thread 的内部接口,线程组也是该接口的实现,
        只有一个方法处理异常,接下来查看第 25 行,看看 Group 是如何实现的
    @FunctionalInterface
    public interface UncaughtExceptionHandler {
        void uncaughtException(Thread t, Throwable e);
    }
}
public class ThreadGroup implements Thread.UncaughtExceptionHandler {
    【5】默认异常处理器的实现
    public void uncaughtException(Thread t, Throwable e) {
        // 如果有父线程组,交给它处理
        if (parent != null) {
            parent.uncaughtException(t, e);
        } else {
            // 获取默认的异常处理器,如果没有指定,那么为 null
            Thread.UncaughtExceptionHandler ueh =
                Thread.getDefaultUncaughtExceptionHandler();
            if (ueh != null) {
                ueh.uncaughtException(t, e);
            } 
            // 没有指定异常处理器,打印堆栈信息
            else if (!(e instanceof ThreadDeath)) {
                System.err.print("Exception in thread \""
                                 + t.getName() + "\" ");
                e.printStackTrace(System.err);
            }
        }
    }
}

7.2 自定义全局异常处理器

通过上面的源码讲解,已经可以知道 JVM 是如何处理未捕获的异常的了,即只打印堆栈信息。那么,要如何自定义异常处理器呢?

具体方法为:

实现接口 Thread.UncaughtExceptionHandler 并实现方法 uncaughtException()

为创建的线程指定异常处理器

示例代码:

public class MyExceptionHandler implements Thread.UncaughtExceptionHandler{
    @Override
    public void uncaughtException(Thread t, Throwable e) {
        System.out.println("发生了未捕获的异常,进行日志处理、报警处理、友好提示、数据备份等等......");
        e.printStackTrace();
    }
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            throw new RuntimeException();
        });
        t.setUncaughtExceptionHandler(new MyExceptionHandler());
        t.start();
    }
}

8 多线程带来的问题

合理地利用多线程能够带来性能上的提升,但是如果因为一些疏漏,多线程反而会成为程序员的噩梦。

例如,多线程开发,我们需要考虑线程安全问题、性能问题。

首先,讲讲线程安全问题。

什么是线程安全?所谓线程安全,即

在多线程情况下,如果访问某个对象,不需要额外处理,例如加锁、令线程阻塞、额外的线程调度等,调用这个对象都能获得正确的结果,那么这个对象就是线程安全的

因此,在编写多线程程序时,就需要考虑某个数据是否是线程安全的,如果这个对象满足:

  • 被多个线程共享

  • 操作具有时序要求,先读后写

  • 这个对象的类有他人编写,并且没有声明是线程安全的

那么我们就需要考虑使用同步锁、Lock、并发工具类(java.util.concurrent)来保证这个对象是在多线程下是安全的。

再看看多线程带来的性能问题。

多个线程的调度需要上下文切换,这需要耗费 CPU 资源。

所谓上下文,即处理器中寄存器、程序计数器内的信息。

上下文切换,即 CPU 挂起一个线程,将其上下文保存到内存中,从内存中获取另一个运行线程的上下文,恢复到寄存器中,根据程序计数器中的指令恢复线程运行。

一个线程被挂起,另一个线程恢复运行,这个时候,被挂起的线程的数据缓存对于运行线程来说是无效的,减缓了线程的运行速度,新的线程需要重新缓存数据提升运行速度。

通常情况下,密集的 IO 操作、抢锁操作都会带来密集的上下文切换。

以上,是上下文切换带来的性能问题,Java 的内存模型也会带来性能问题,为了保证数据的可见性,JVM 会强制令数据缓存失效,保证数据是实时最新的,这也牺牲了缓存带来的性能提升。

9 总结

这里总结下上面的内容。

  • 创建线程有两种方式,继承 Thread 和实现 Runnable

  • start 方法才能正确创建和启动线程,run 方法只是一个普通方法

  • start 方法不能反复调用,反复调用会抛出异常

  • 正确停止线程的方法是通过 interrupt() 通知线程

线程不时地检查中断状态并判断是否停止线程,使用方法 isInterrupt()

如果线程阻塞,捕获中断异常,判断是否停止线程

线程调用的子方法最好将异常抛出,由 run 方法统一捕获处理

线程调用的子方法如果捕获异常,需要重新通知线程中断

  • 线程的生命周期为

NEW

RUNNABLE

BLOCKED

WAITING

TIMED WAITING

TERMINATED

  • wait()/notify()/notifyAll() 必须配合同步锁使用

  • wait() 会释放锁,sleep() 不会释放锁,包括同步锁和 Lock 锁

  • 线程的一些属性

线程ID,无法修改

线程名 name,可以自定义

守护线程 daemon,线程类型会继承自父线程,通常不指定线程为守护线程

优先级 priority,通常使用默认优先级,不改变优先级

  • 可以自定义全局异常处理器,处理非主线程中的未捕获的异常,如备份数据、日志处理、报警等等

  • 多线程开发会带来线程安全问题、性能问题,开发过程需要特别注意

推荐学习:《java视频教程

相关文章

java速学教程(入门到精通)
java速学教程(入门到精通)

java怎么学习?java怎么入门?java在哪学?java怎么学才快?不用担心,这里为大家提供了java速学教程(入门到精通),有需要的小伙伴保存下载就能学习啦!

下载

相关标签:

本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
拼多多赚钱的5种方法 拼多多赚钱的5种方法
拼多多赚钱的5种方法 拼多多赚钱的5种方法

在拼多多上赚钱主要可以通过无货源模式一件代发、精细化运营特色店铺、参与官方高流量活动、利用拼团机制社交裂变,以及成为多多进宝推广员这5种方法实现。核心策略在于通过低成本、高效率的供应链管理与营销,利用平台社交电商红利实现盈利。

28

2026.01.26

edge浏览器怎样设置主页 edge浏览器自定义设置教程
edge浏览器怎样设置主页 edge浏览器自定义设置教程

在Edge浏览器中设置主页,请依次点击右上角“...”图标 > 设置 > 开始、主页和新建标签页。在“Microsoft Edge 启动时”选择“打开以下页面”,点击“添加新页面”并输入网址。若要使用主页按钮,需在“外观”设置中开启“显示主页按钮”并设定网址。

8

2026.01.26

苹果官方查询网站 苹果手机正品激活查询入口
苹果官方查询网站 苹果手机正品激活查询入口

苹果官方查询网站主要通过 checkcoverage.apple.com/cn/zh/ 进行,可用于查询序列号(SN)对应的保修状态、激活日期及技术支持服务。此外,查找丢失设备请使用 iCloud.com/find,购买信息与物流可访问 Apple (中国大陆) 订单状态页面。

31

2026.01.26

npd人格什么意思 npd人格有什么特征
npd人格什么意思 npd人格有什么特征

NPD(Narcissistic Personality Disorder)即自恋型人格障碍,是一种心理健康问题,特点是极度夸大自我重要性、需要过度赞美与关注,同时极度缺乏共情能力,背后常掩藏着低自尊和不安全感,影响人际关系、工作和生活,通常在青少年时期开始显现,需由专业人士诊断。

3

2026.01.26

windows安全中心怎么关闭 windows安全中心怎么执行操作
windows安全中心怎么关闭 windows安全中心怎么执行操作

关闭Windows安全中心(Windows Defender)可通过系统设置暂时关闭,或使用组策略/注册表永久关闭。最简单的方法是:进入设置 > 隐私和安全性 > Windows安全中心 > 病毒和威胁防护 > 管理设置,将实时保护等选项关闭。

5

2026.01.26

2026年春运抢票攻略大全 春运抢票攻略教你三招手【技巧】
2026年春运抢票攻略大全 春运抢票攻略教你三招手【技巧】

铁路12306提供起售时间查询、起售提醒、购票预填、候补购票及误购限时免费退票五项服务,并强调官方渠道唯一性与信息安全。

35

2026.01.26

个人所得税税率表2026 个人所得税率最新税率表
个人所得税税率表2026 个人所得税率最新税率表

以工资薪金所得为例,应纳税额 = 应纳税所得额 × 税率 - 速算扣除数。应纳税所得额 = 月度收入 - 5000 元 - 专项扣除 - 专项附加扣除 - 依法确定的其他扣除。假设某员工月工资 10000 元,专项扣除 1000 元,专项附加扣除 2000 元,当月应纳税所得额为 10000 - 5000 - 1000 - 2000 = 2000 元,对应税率为 3%,速算扣除数为 0,则当月应纳税额为 2000×3% = 60 元。

12

2026.01.26

oppo云服务官网登录入口 oppo云服务登录手机版
oppo云服务官网登录入口 oppo云服务登录手机版

oppo云服务https://cloud.oppo.com/可以在云端安全存储您的照片、视频、联系人、便签等重要数据。当您的手机数据意外丢失或者需要更换手机时,可以随时将这些存储在云端的数据快速恢复到手机中。

40

2026.01.26

抖币充值官方网站 抖币性价比充值链接地址
抖币充值官方网站 抖币性价比充值链接地址

网页端充值步骤:打开浏览器,输入https://www.douyin.com,登录账号;点击右上角头像,选择“钱包”;进入“充值中心”,操作和APP端一致。注意:切勿通过第三方链接、二维码充值,谨防受骗

7

2026.01.26

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

相关下载

更多

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Kotlin 教程
Kotlin 教程

共23课时 | 2.9万人学习

C# 教程
C# 教程

共94课时 | 7.6万人学习

Java 教程
Java 教程

共578课时 | 51.2万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号