一、线程与进程的区别

在上两篇博文中,我们主要讲述了什么是进程,什么是线程,总结一下进程和线程的区别:

  1. 进程包含线程,在一个进程中,包含至少一个线程。
  2. 进程是系统分配资源的基本单位,线程是系统进行调度的基本单位。创建进程的时候已经分配好资源了,后续创建线程的时候直接使用公共资源即可。
  3. 进程是独立执行的,它具有独立的地址空间,如果一个进程挂掉了不会影响另外一个进程。进程具有独立性,导致系统十分的稳定。但是线程不一样,多个线程共用一份地址空间,一旦某个线程出 bug,很可能会导致整个进程受到牵连,异常结束,所以多个线程之间容易相互影响。

以上就是线程和进程最主要的区别,只能说各有各的好处,根据实际的业务需求来考虑使用哪种方式。


二、创建线程的代码实现

2.1、继承 Thread

说了这么多,那作为一名 Java 程序猿的我们又该如何来创建一个线程呢?欸,这就不得不说我们的 API 了,那什么是 API 呢?大家可以把他理解成一个接口,一个操控操作系统的接口,其中有个 Thread 类,这个类中有个 run 方法的,重写这个方法之后,再调用 stert 方法,就是创建出一个新的线程了,具体代码如下:

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Hello Thread");
    }
}

public class Demo1 {
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start();
    }
}

如下是运行结果:

线程与进程的区别及线程实现(JavaEE初阶)_System

那有的人就会说了,如果我直接运行 MyThread 方法里面的 run 方法,不是也能得出相同的结果吗?大家可以试试,结果确实是一样的,但是和调用 run 方法有着本质的区别,那就是调用 run 只是调用了这个方法,并没有创造出新的线程,而 start 方法就不一样了,这个是让系统帮你创造了一个新的线程出来,我们更改一下代码更方便观察,如下:

class MyThread extends Thread {
    @Override
    public void run() {
        while (true) {
            System.out.println("Hello Runnable");
        }
    }
}

public class Demo1 {
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start();
        while (true) {
            System.out.println("Hello Main");
        }
    }
}

可以先来推测一下,因为我们 cpu 是并发执行线程的,那我们所执行的结果中,肯定是两种交叉打印,但是并不是有规律的交叉,这个对于系统来说,可以看成是 "随机" 的,执行结果如下:

线程与进程的区别及线程实现(JavaEE初阶)_Java_02

因为我们的 cpu 是并发执行线程的所以并不会再某一个循环卡死。

2.2、实现 Runnable 接口

Runnable 接口也和上面 Thread 类的写法差不多,只不过意思不一样,大家看代码就知道了,如下:

class MyRunnable implements Runnable {
    @Override
    public void run() {
        while (true) {
            System.out.println("Hello Runnable");
        }
    }
}

public class Demo2 {
    public static void main(String[] args) {
        MyRunnable r = new MyRunnable();
        Thread t = new Thread(r);
        t.start();
        while (true) {
            System.out.println("Hello Main");
        }
    }
}

这个代码相信大家都看明白了把?相当于就是把另外一个线程要执行的步骤和执行这一步给拆开了,降低了代码的耦合性,运行结果如下:

线程与进程的区别及线程实现(JavaEE初阶)_多线程_03

2.3、使用匿名内部类

  1. Thread 匿名内部类:

代码实现如下:

public class Demo3 {
    public static void main(String[] args) {
        Thread t = new Thread() {
            @Override
            public void run() {
                while (true) {
                    System.out.println("Hello Thread");
                }
            }
        };
        t.start();

        while (true) {
            System.out.println("Hello Main");
        }
    }
}
  1. Runnable 匿名内部类:

代码如下:

public class Demo4 {
    public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    System.out.println("Hello Runnable");
                }
            }
        });

        t.start();
        while (true) {
            System.out.println("Hello Main");
        }
    }
}

2.4、使用lambda表达式

lambda表达式也是之后经常用到的方法,代码如下:

public class Demo5 {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while (true) {
                System.out.println("Hello Thread");
            }
        });
        t.start();
        while (true) {
            System.out.println("Hello Main");
        }
    }
}

三、线程的常见方法

3.1、手动结束线程

  1. 手动创建标志位
public class Demo6 {
    public static boolean flag = false;

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while (!flag) {
                System.out.println("Hello Thread");
            }
            System.out.println("t 线程结束");
        });

        t.start();
        Thread.sleep(3000);
        flag = true;
    }
}

这里不可以吧 flag 设置成局部变量,因为变量捕捉的缘故,设置成局部变量的话,如果说 flag 之后要改变就捕捉不到,除非把 flag 设置成 final 修饰的常量。

  1. 使用库标志位
public class Demo7 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while (!Thread.currentThread().isInterrupted()) {
                System.out.println("Hello Thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    break;
                }
            }
        });
        t.start();
        Thread.sleep(3000);
        // interrupt 方法会将上一个标志位变成 true,并且强制唤醒正在 sleep 的线程
        // 这时候想要退出 t 线程,只需要在 catch 的地方加入 break 就好了,
        // 还可以不立即退出,做一些其他的工作再退出也行,可供程序猿选择
        t.interrupt();
        System.out.println("线程结束!!!");
    }
}

3.2、join() 方法

这个方法是在另外一个线程中使用,执行到 joinjoin 的线程结束,然后继续执行该线程,代码如下:

public class Demo8 {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            int i = 0;
            while (i++ < 5) {
                System.out.println("Hello t1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("t1 结束了!!!");
        });

        Thread t2 = new Thread(() -> {
            int i = 0;
            while (i++ < 3) {
                System.out.println("Hello t2");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            try {
                t1.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("t2 结束了!!!");
        });
        t1.start();
        t2.start();
    }
}

运行结果如下:

线程与进程的区别及线程实现(JavaEE初阶)_System_04

3.3、观察线程状态

观察线程状态的方法是 getState 方法,直接线程对象名.getState(); 就行,一共有种状态,如下:

//NEW: 安排了工作, 还未开始行动
//RUNNABLE: 可工作的. 又可以分成正在工作中和即将开始工作.
//BLOCKED: 这几个都表示排队等着其他事情
//WAITING: 这几个都表示排队等着其他事情
//TIMED_WAITING: 这几个都表示排队等着其他事情
//TERMINATED: 工作完成了.

四、总结

线程是提高应用程序并发性和响应性的关键工具,但它们也带来了复杂的设计和实现挑战。通过深入理解线程与进程的区别以及线程的生命周期、同步机制和错误处理,Java EE开发者可以更有效地利用线程来构建高效、可伸缩和健壮的应用程序。

通过这篇博客,我们希望能够帮助你在Java EE初阶阶段建立起对线程和进程的坚实理解,并为你在并发编程领域的进一步学习和实践打下良好的基础。


五、结语

总结来说,线程是轻量级的执行单元,它们共享进程的资源,这使得线程之间的上下文切换比进程之间的上下文切换要快得多。然而,线程的共享特性也带来了同步和数据共享的复杂性。在Java中,我们可以通过继承Thread类或实现Runnable接口来创建线程,并通过join 等方法来更高效地管理、控制线程。

学习线程和进程的区别以及线程的实现是一个持续的过程,需要不断实践和探索。在这个过程中,我们可能会遇到挑战,但这些挑战也是成长的机会。通过解决实际问题,我们能够更深入地理解并发编程的复杂性,并提高我们的设计和开发技能。

作为一名Java EE开发者,掌握线程和进程的概念将帮助你构建更高效、更健壮的应用程序。在这个快速发展的技术领域中,持续学习和实践是至关重要的。记住,每一个挑战都是成长的机会,每一次成功都是对你努力的肯定。

最后,愿你在并发编程的旅程中不断前进,不断探索,不断学习。愿你的代码像你的意志一样强大,愿你的应用程序像你的梦想一样高效。编码愉快,未来可期!

线程与进程的区别及线程实现(JavaEE初阶)_Java_05