多线程(一)

来源:互联网 发布:csgo国服mac 编辑:程序博客网 时间:2024/06/05 06:20

进程和线程的概述

  • 进程就是应用程序在内存中分配的空间,也可理解为一个正在执行中的程序。每一个进程执行都有一个执行顺序,该顺序就是一个执行路径或者叫一个控制单元。
  • 线程就是进程中负责程序执行的执行单元,也可理解为进程中的一个独立的控制单元。线程在控制着进程的执行。

多线程

多线程的概述

一个进程中至少有一个线程在负责该进程的运行。如果一个进程中出现了多个线程,就称该程序为多线程程序。

多线程解决的问题

多线程这门技术的出现解决了多部分代码同时执行的需求,这样做的好处就是可以提高用户的体验效果。
这里有一个疑问——多线程真的能提高效率吗?显然不是,反倒容易死机,但可合理的使用CPU资源。

JVM中的多线程与垃圾回收

多线程的运行是根据CPU的切换完成的,怎么切换CPU说了算,所以多线程运行有一个随机性(CPU的快速切换造成的)。
本节我首先给出结论——JVM中的多线程至少有两个线程:

  1. 一个是负责自定义代码运行的,这个从main方法开始执行的线程称之为主线程。
  2. 一个是负责垃圾回收的。

然后我通过一个简单的案例来演示JVM中的多线程。例如,有如下实验代码:

class Demo{    // 定义垃圾回收方法    public void finalize()    {        System.out.println("demo ok");    }}class FinalizeDemo {    public static void main(String[] args)     {        new Demo();        new Demo();        new Demo();        System.gc(); // 启动垃圾回收器。        System.out.println("Hello World!");    }}

运行FinalizeDemo类,可能在屏幕上打印(截图如下):
这里写图片描述
通过实验会发现每次的结果不一定相同,那是因为随机性造成的。而且每一个线程都有自己的代码内容,这个称之为线程的任务,之所以创建一个线程就是为了去运行指定的任务代码。而线程的任务都封装在特定的区域中,比如:

  1. 主线程运行的任务都定义在main方法中。
  2. 垃圾回收线程在收垃圾时都会运行finalize方法。

创建线程方式一

如何在自定义的代码中,自定义一个线程呢?也即如何建立一个执行路径呢?
答:通过对API的查找,java已经提供了对线程这类事物的描述,即Thread类。该类的描述中创建线程有两种方式,下面我就来讲解其第一种方式。
创建线程的第一种方式:继承Thread类。
步骤:

  1. 继承Thread
  2. 重写Thread类中的run()。目的:将自定义的代码存储在run(),让线程运行
  3. 创建子类对象也即创建线程对象
  4. 调用线程的start()。该方法有2个作用:启动线程,调用run()

例,

class Demo extends Thread {    public void run() {        for (int x = 0; x < 60; x++) {            System.out.println("demo run---"+x);        }    }}class ThreadDemo {    public static void main(String[] args) {        Demo d = new Demo(); // 创建好一个线程        // d.start(); // 开启线程,并执行该线程的run()        d.run(); // 仅仅是对象的调用方法,而线程创建了,并没有被运行        for (int x = 0; x < 60; x++) {            System.out.println("Hello World!---"+x);        }    }}

发现运行结果每一次都不同。
因为多个线程都在获取CPU的执行权,CPU执行到谁,谁就运行。明确一点,在某一个时刻,只能有一个程序在运行(多核除外)。CPU在做着快速的切换,以达到看上去是同时运行的效果。我们可以形象地把多线程的运行形容为互相抢夺CPU的执行权,这就是多线程的一个特点:随机性。谁抢到谁执行,至于执行多长,CPU说了算。
问题一、为什么要覆盖run()呢?
答:Thread类用于描述线程,该类就定义了一个功能,用于存储线程要运行的代码,该存储功能就是run()。也就是说Thread类中的run()用于存储线程要运行的代码。
问题二、调用start方法和调用run方法的区别?
答:调用start方法会开启线程,让开启的线程去执行run方法中的线程任务;而直接调用run方法,线程并未开启,去执行run方法的只有主线程。
练习:创建两个线程,和主线程交替执行。
解:

class Test extends Thread {    Test(String name) {        super(name);    }    public void run() {        for (int x = 0; x < 60; x++) {            System.out.println((Thread.currentThread()==this)+"..."+this.getName()+" run..."+x);        }    }}class ThreadTest {    public static void main(String[] args) {        Test t1 = new Test("one---");        Test t2 = new Test("two+++");        t1.start();        t2.start();        for (int x = 0; x < 60; x++) {            System.out.println("main..."+x);        }    }}

通过上例,可发现原来线程都有自己默认的名称:Thread-编号,该编号从0开始。

  • static Thread currentThread():获取当前线程对象
  • getName():获取线程名称
  • setName()或者构造函数:设置线程名称

多线程的运行状态

多线程的运行状态用图来表示即为:
这里写图片描述

创建线程方式二

以此例引申出创建线程的第二种方式:
例,需求:简单的卖票程序。多个窗口同时卖票。

class Ticket implements Runnable {    private int tick = 100;    public void run() {        while(true) {            if(tick > 0) {                try { Thread.sleep(10); } catch(Exception e) {}                System.out.println(Thread.currentThread().getName()+"...sale:"+tick--);            }        }    }}class TicketDemo {    public static void main(String[] args) {        Ticket t = new Ticket();        Thread t1 = new Thread(t); // 创建一个线程        Thread t2 = new Thread(t); // 创建一个线程        Thread t3 = new Thread(t); // 创建一个线程        Thread t4 = new Thread(t); // 创建一个线程        t1.start();        t2.start();        t3.start();        t4.start();    }}

创建线程的第二种方式:实现Runnable接口。
步骤:

  1. 定义类实现Runnable接口
  2. 覆盖Runnable接口中的run()。目的:将线程要运行的代码存放在该run()
  3. 通过Thread类建立线程对象
  4. Runnable接口的子类对象作为实际参数传递给Thread类的构造函数。
    为什么要将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数?
    答:因为自定义的run()所属的对象是Runnable接口的子类对象,所以要让线程去运行指定对象的run(),就必须明确该run()所属的对象。
  5. 调用Thread类的start()开启线程并调用Runnable接口子类的run方法。

实现Runnable接口的好处

现将实现Runnable接口的好处总结如下:

  1. 避免了继承Thread类的单继承的局限性。
  2. Runnable接口的出现更符合面向对象,将线程任务单独进行了对象的封装。
  3. Runnable接口的出现降低了线程任务和线程对象的耦合性。

所以,以后创建线程都使用第二种方式。