Java多线程技能-线程的启动

java 多线程技能

技术点:

  • 线程的启动
  • 如何使线程暂停
  • 如何使线程停止
  • 线程的优先级
  • 线程安全相关的问题

进程和线程的定义及多线程的优点

进程:进程是受操作系统管理的基本运行单元。

程序:程序是指令序列,这些指令可以让 CPU 做指定的任务。

线程:线程可以理解为在进程中独立运行的子任务。

进程负责向操作系统申请资源。在一个进程中,多个线程可以共享进程中相同的内存或文件资源。先有进程,后有线程。在一个进程中可以创建多个线程。


进程和线程的总结:

  1. 进程虽然是相互独立的,但它们可以互相通信,较为通用的方式是使用 Socket 或 HTTP 协议。
  2. 进程拥有共享的系统资源,比如内存、网络端口,供其内部线程使用。
  3. 进程较重,因为创建进程需要操作系统分配资源,会占用内存。
  4. 线程存在于进程中,是进程的一个子集,先有进程,后有线程。
  5. 虽然线程更轻,但线程上下文切换的时间成本非常高。

在什么场景下使用多线程技术?

  1. 阻塞:一旦系统中出现了阻塞现象,则可以根据实际情况来使用多线程提高运行效率。
  2. 依赖:业务分为两个执行过程,分别是 A 和 B,当 A 业务有阻塞的情况发生时,B 业务的执行不依赖 A 业务的执行结果,这时可以使用多线程来提高运行效率;如果 B 业务依赖 A 业务的执行结果,则不需要使用多线程技术,按顺序串行执行即可。

在实际的开发应用中,不要为了使用多线程而使用多线程,要根据实际场景决定

注意:多线程是异步的,所以千万不要把 IDE 里代码的顺序当作线程执行的顺序,线程被调用的时机是随机的。

使用多线程

一个进程正在运行时至少会有一个线程在运行,这些线程在后台默默地执行。

/**
 * @author 软柠柠吖(Runny)
 * @date 2023-02-20
 * @description 比如:调用 public static void main() 方法的 main 线程就是这样,而且它由 JVM 创建。
 */
public class MainThread {
    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName());
        // 控制台输出的 main 和 main 方法没有任何关系,仅仅是同名而已!
        new OtherClass().threadInfo();
    }
}
/**
 * @author 软柠柠吖(Runny)
 * @date 2023-02-20
 * @description
 */
public class OtherClass {
    public void threadInfo() {
        System.out.println("OtherClass threadInfo " + Thread.currentThread().getName());
    }
}

继承 Thread 类

实现多线程编程的方式主要有两种:① 继承 Thread 类,② 实现 Runnable 接口

/**
 * @author 软柠柠吖(Runny)
 * @date 2023-02-20
 * @description 实现多线程编程的方式主要有两种:① 继承 Thread 类,② 实现 Runnable 接口
 */
public class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("MyThread");
    }
}
/**
 * @author 软柠柠吖(Runny)
 * @date 2023-02-20
 * @description
 */
public class Run {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        // 启动线程
        myThread.start(); // 耗时大
        System.out.println("运行结束!"); // 耗时小
    }
}

方法 start() 耗时多的原因是内部执行了多个步骤,步骤如下:

  1. 通过 JVM 告诉操作系统创建 Thread
  2. 操作系统开辟内存并使用 Windows SDK 中的 createThread() 函数创建 Thread 线程对象
  3. 操作系统对 Thread 对象进行调度,以确定执行时机
  4. Thread 在操作系统中被成功执行

main 线程执行 start() 方法时不必等待上述步骤都执行完成,而是立即继续执行 start() 方法后面的代码,这 4 个步骤会与后面的代码一同执行。


注意:如果多次调用 start() 方法,则出现异常 Exception in thread "main" java.lang.IllegalThreadStateException

使用常见的 3 个命令分析线程的信息

jps + jstack.exe

jmc.exe

jvisualvm.exe

/**
 * @author 软柠柠吖(Runny)
 * @date 2023-02-20
 * @description
 */
public class Run {
    public static void main(String[] args) throws InterruptedException {

        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                try {
                    TimeUnit.SECONDS.sleep(200);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }).start();
        }
    }
}

线程随机性的展现

public class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            System.out.println("run=" + Thread.currentThread().getName());
        }
    }
}
public class Run {
    public static void main(String[] args) throws InterruptedException {
        MyThread thread = new MyThread();
        thread.setName("myThread");
        thread.start();

        for (int i = 0; i < 10000; i++) {
            System.out.println("main=" + Thread.currentThread().getName());
        }
    }
}

Thread.java 类中的 start() 方法通知 “线程规划器” —— 此线程已经准备就绪,准备调用线程对象的 run() 方法。这个过程其实就是让操作系统安排一个时间来调用 Thread 中的 run() 方法执行具体的任务,具有异步随机顺序执行的效果。


多线程随机输出的原因是:CPU 将时间片分给不同的线程,线程获得时间片后就执行任务,所以这些线程在交替执行并输出,导致输出结果呈乱序。

时间片即 CPU 分配给各个程序的时间。每个线程被分配一个时间片,在当前的时间片内执行线程中的任务。需要注意的是,当 CPU 在不同的线程上进行切换时是需要耗时的,所以并不是创建的线程越多,软件运行效率就越快,相反,线程数过多反而会降低软件的执行效率。


如果调用代码 thread.run(); 而不是 thread.start(); ,其实就不是异步执行了,而是同步执行,那么此线程对象并不交给线程规划器来进行处理,而是由 main 线程来调用 run() 方法,也就是必须等 run() 方法中的代码执行完毕后才可以执行后面的代码。

执行 start() 的顺序不代表执行 run() 的顺序

注意:执行 start() 方法的顺序不代表线程启动的顺序,即不代表 run() 方法执行的顺序,执行 run() 方法的顺序是随机的。(也从另外一个角度说明线程是随机执行的)

public class MyThread extends Thread {
    private int i;

    public MyThread(int i) {
        super();
        this.i = i;
    }

    @Override
    public void run() {
        System.out.println(i);
    }
}
public class Run {
    public static void main(String[] args) {
        MyThread t1 = new MyThread(1);
        MyThread t2 = new MyThread(2);
        MyThread t3 = new MyThread(3);
        MyThread t4 = new MyThread(4);
        MyThread t5 = new MyThread(5);
        MyThread t6 = new MyThread(6);
        MyThread t7 = new MyThread(7);

        t1.start();
        t2.start();
        t3.start();
        t4.start();
        t5.start();
        t6.start();
        t7.start();
    }
}

图示:image-20230221104048461.

实现 Runnable 接口

如果想创建的线程类已经有了一个父类,就不能再继承自 Thread 类,因为 java 不支持多继承,所以需要实现 Runnable 接口来解决这样的问题。

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " 运行中!");
    }
}
public class Run {
    public static void main(String[] args) {
        MyRunnable runnable = new MyRunnable();
        Thread thread = new Thread(runnable);
        thread.start();
        System.out.println(Thread.currentThread().getName() + " 运行结束!");
    }
}

图示:image-20230221105108096.(异步执行)

使用 Runnable 接口实现多线程的优点

优点:通过实现 Runnable 接口,可间接实现 “多继承” 的效果

public class AServer {
    public void saveMethod() {
        System.out.println("a 中的保存数据方法被执行!");
    }
}
public class BServer extends AServer implements Runnable {
    @Override
    public void saveMethod() {
        System.out.println("b 中的保存数据方法被执行!");
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
        saveMethod();
    }
}
public class Run {
    public static void main(String[] args) {
        BServer bServer = new BServer();

        new Thread(bServer).start();
    }
}

图示:image-20230221111650970.


彩蛋Thread.java 类也实现了 Runnable 接口,这就意味着构造函数 Thread(Runnable target) 不仅可以传入 Runnable 接口的对象,还可以传入一个 Thread 类的对象,这样做完全可以将一个 Thread 对象中的 run() 方法交由其他线程进行调用。

public class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " 正在执行~");
    }
}
public class Run {
    public static void main(String[] args) throws InterruptedException {
        MyThread thread = new MyThread();
        // MyThread 是 Thread 的子类
        // 而 Thread 是 Runnable 的实现类
        // 所以 MyThread 也相当于是 Runnable 的实现类
        Thread t = new Thread(thread);
        t.start();
        TimeUnit.SECONDS.sleep(3);
        thread.start();
    }
}

图示:image-20230221112708825.


优点:使用 Runnable 接口方式实现多线程可以把 “线程” 和 “任务” 分离Thread 代表线程,而 Runnable 代表可运行的任务,Runnable 里面包含 Thread 线程要执行的代码,这样处理可以实现多个 Thread 共用一个 Runnable

public Thread(Runnable target) 中的 target 参数

当执行 start() 方法时,由 JVM 直接调用的是 Thread.java 类中的 run() 方法。该方法的源码是:

@Override
public void run() {
    if (target != null) {
        target.run();
    }
}

然后结合 if 判断执行 Runnable target 对象的 run() 方法。

实例变量共享导致的 “非线程安全” 问题与相应的解决方案

不共享数据的情况

public class MyThread extends Thread {
    private int count = 5;

    @Override
    public void run() {
        while (count > 0) {
            sell();
        }
    }

    private void sell() {
        count--;
        System.out.println(" 由 " + Thread.currentThread().getName() + " 计算,count=" + count);
    }
}
public class Run {
    public static void main(String[] args) throws InterruptedException {
        MyThread a = new MyThread();
        MyThread b = new MyThread();
        MyThread c = new MyThread();

        a.start();
        b.start();
        c.start();
    }
}

说明:一共创建了 3 个线程,每个线程都有各自的 count 变量,自己减少自己的 count 变量的值,这样的情况就是变量不共享,此示例并不存在多个线程访问同一个实例变量的情况。

共享数据的情况

共享数据的情况就是多个线程可以访问同一个变量,例如:在实现投票功能的软件时,多个线程同时处理同一个人的票数。

public class MyThread extends Thread {
    private int count = 5;

    @Override
    public void run() {
        sell();
    }

    synchronized private void sell() {
        if (count > 0) {
            count--;
            System.out.println(" 由 " + Thread.currentThread().getName() + " 计算,count=" + count);
        }
    }
}
public class Run {
    public static void main(String[] args) throws InterruptedException {
        // 任务
        MyThread thread = new MyThread();
        // 执行线程
        Thread a = new Thread(thread);
        Thread b = new Thread(thread);
        Thread c = new Thread(thread);
        Thread d = new Thread(thread);
        Thread e = new Thread(thread);

        a.start();
        b.start();
        c.start();
        d.start();
        e.start();
    }
}

图示:image-20230221170337622.

① 使用 synchronized 关键字修饰的方法称为 “同步方法”,可用来对方法内部的全部代码进行加锁,而加锁的这段代码称为 “互斥区” 或 “临界区”。

② 当一个线程想要执行同步方法里面的代码时,它会首先尝试去拿这把锁,如果能够拿到,那么该线程就会执行 synchronized 里面的代码。如果不能拿到,那么这个线程就会不断尝试去拿这把锁,直到拿到为止。

③ 加入 synchronized 关键字的方法可以保证同一时间只有一个线程在执行方法,多个线程执行方法具有排队的特性。

Servlet 技术也会引起 “非线程安全” 问题

非线程安全主要是指多个线程对同一个对象中的同一个实例变量进行操作时会出现值被更改、值不同步的情况,进而影响程序执行流程。

public class LoginServlet {
    private static String usernameRef;
    private static String passwordRef;

    synchronized public static void doPost(String username, String password) {
        try {
            usernameRef = username;
            if ("a".equals(username)) {
                Thread.sleep(5000);
            }
            passwordRef = password;
            System.out.println("usernameRef=" + usernameRef + " passwordRef=" + passwordRef);
            System.out.println("username=" + username + " password=" + password);
            System.out.println();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}
public class ALogin implements Runnable {

    @Override
    public void run() {
        LoginServlet.doPost("a", "aa");
    }
}
public class BLogin implements Runnable {

    @Override
    public void run() {
        LoginServlet.doPost("b", "bb");
    }
}
public class Run {
    public static void main(String[] args) throws InterruptedException {
        // 任务
        Runnable a = new ALogin();
        Runnable b = new BLogin();

        // 执行进程
        Thread t1 = new Thread(a);
        Thread t2 = new Thread(b);

        t1.start();
        t2.start();
    }
}

提示:两个线程向同一个对象的 public static void doPost(String username, String password) 方法传递参数时,方法的参数值不会被覆盖,而是绑定到当前执行线程上。


注意:在 Web 开发中,Servlet 对象本身就是单例的,所以为了不出现的非线程安全,建议不要在 Servlet 中出现实例变量

留意 i-- 与 System.out.println() 出现的 “非线程安全” 问题

public class MyThread extends Thread {
    private int i = 5;

    @Override
    public void run() {
        sub();
    }

    private void sub() {
        System.out.println("i=" + (i--) + " threadName=" + Thread.currentThread().getName());
    }
}
public class Run {
    public static void main(String[] args) throws InterruptedException {
        // 任务
        MyThread run = new MyThread();

        // 执行进程
        Thread t1 = new Thread(run);
        Thread t2 = new Thread(run);
        Thread t3 = new Thread(run);
        Thread t4 = new Thread(run);
        Thread t5 = new Thread(run);

        t1.start();
        t2.start();
        t3.start();
        t4.start();
        t5.start();
    }
}

图示:image-20230221193436327.


警示:不要看到 synchronized 就以为代码是安全的,在 synchronized 之前执行的代码也有可能是不安全的。

方法 run() 被 JVM 所调用

/**
 * Causes this thread to begin execution; the Java Virtual Machine
 * calls the <code>run</code> method of this thread.
 * <p>
 * The result is that two threads are running concurrently: the
 * current thread (which returns from the call to the
 * <code>start</code> method) and the other thread (which executes its
 * <code>run</code> method).
 * <p>
 * It is never legal to start a thread more than once.
 * In particular, a thread may not be restarted once it has completed
 * execution.
 *
 * @exception  IllegalThreadStateException  if the thread was already
 *               started.
 * @see        #run()
 * @see        #stop()
 */
public synchronized void start() {
posted @ 2023-02-21 19:51  软柠柠吖  阅读(102)  评论(0编辑  收藏  举报