JAVA进阶学习-多线程基础详解(一)

.1 线程与进程的区别

1-1 定义

       进程:进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位.
       线程:线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。

1-2 区别

  •        简而言之, 一个程序至少有一个进程,一个进程至少有一个线程。

  •        线程的划分尺度小于进程,使得多线程程序的并发性高。

  •        另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。

  •        线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。

  •        从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。

    注:在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。因为每当使用java命令执行一个类的时候,实际上都会启动一个JVM,每一个JVM实际就是在操作系统中启动了一个进程。

.2 线程的5种状态

线程可以有如下5种状态:

  1. New(新创建)
  2. Runnable(就绪)
  3. Running(运行)
  4. Blocked(阻塞)
  5. Dead(死亡)

下面这张图对于理解这五种状态之间的关系非常有帮助。

  1. 新创建线程
           当用new操作符创建一个新线程时,如new Thread(r),该线程还没有开始运行。这意味着它的状态是new。

  2. 就绪和运行线程
           一旦调用start方法,线程处于runnable(就绪)状态。是否进入运行状态,这取决于操作系统给线程提供运行的时间,当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行。
           一旦一个线程开始运行,它不必始终保持运行。事实上,运行中的线程被中断,目的是为了让其他线程获得运行的机会。线程调度的细节依赖于操作系统提供的服务。抢占式调度系统给每一个可运行的线程一个时间片来执行任务。当时间片用完,操作系统剥夺该线程的运行权,并给另一个线程运行的机会。当选择下一个线程时,操作系统考虑线程的优先级。(本人将之后博文中解释线程的优先级和线程的竞争关系)

  3. 阻塞线程
           当线程处于阻塞状态时,它暂时不活动。它不运行任何代码且消耗最少的资源。直到线程进入就绪状态。根阻塞的不同原因,阻塞大致分为以下三种

           1)等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)
           2)同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
           3)其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(注:sleep是不会释放持有的锁)。

    注:关于阻塞将会在之后的博文中进行分析

  4. 死亡状态(Dead)
           线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

.3 线程启动的两种方式

       创建新执行线程有两种方法。一种方法是将类声明为 Thread 的子类。该子类应重写 Thread 类的 run 方法。接下来可以分配并启动该子类的实例

       创建线程的另一种方法是声明实现 Runnable 接口的类。该类然后实现 run 方法。然后可以分配该类的实例,在创建 Thread 时作为一个参数来传递并启动。(其实准确来讲,应该有三种,还有一种是实现Callable接口,并与Future、线程池结合使用,这部分的内容将在后续博文详细总结。)

3-1 继承Thread类

该方法是创建一个线程的常用方法,在方法中应该重写Thread类的run方法。
编写如下代码:

//继承Thread类
class MyThread extends Thread {

    private int i = 0;
    //重写run方法
    public void run() {

        for (; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + "----" + i);
        }

    }

}


public class ThreadTest {

    public static void main(String[] args) {

        System.out.println(Thread.currentThread().getName()); //输出当前线程的名字

        Thread thread1 = new MyThread(); //新建线程对象 线程进入新建状态
        Thread thread2 = new MyThread(); 

        thread1.start();//调用start方法线程进入就绪状态
        thread2.start();

        /*
        上面四行代码可简写为:
        new MyThread().start();
        new MyThread().start();
        */
    }

}

运行结果如下:
main
Thread-0—-0
Thread-0—-1
Thread-1—-0
Thread-1—-1
Thread-1—-2
Thread-1—-3
Thread-1—-4
Thread-0—-2
Thread-0—-3
Thread-0—-4

       观察运行结果我们发现两个线程都执行了十次输出。并且程序是乱序执行的。
       程序启动运行main时候,java虚拟机启动一个进程,主线程main在main()调用时候被创建。随着thread1和thread2调用start方法,另外两个线程进入就绪状态等待CPU的调度。
       不要让同一个线程重复调用start方法,会出现java.lang.IllegalThreadStateException异常。

3-2 实现Runnable接口

        实现Runnable接口,并重写该接口的run()方法,该run()方法同样是线程执行体,创建Runnable实现类的实例,并以此实例作为Thread类的target来创建Thread对象,该Thread对象才是真正的线程对象。
编写如下代码:

//实现Runnable接口
class MyRunnable implements Runnable {

    private int i = 0;
    //实现run方法
    public void run() {

        for (; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + "----" + i);
        }

    }

}


public class ThreadTest {

    public static void main(String[] args) {

        System.out.println(Thread.currentThread().getName());

        Runnable myRunnable1 = new MyRunnable(); //新建一个Runnable对象
        Runnable myRunnable2 = new MyRunnable();

        //新建一个线程对象,将target设为刚创建的Runnable对象
        Thread thread1 = new Thread(myRunnable1); 
        Thread thread2 = new Thread(myRunnable2);

        thread1.start();//调用start方法
        thread2.start();

        /*
        上面四行代码可简写为:
        new Thread(myRunnable1).start();
        new Thread(myRunnable2).start();
        */

    }

}

运行结果如下:
main
Thread-1—-0
Thread-1—-1
Thread-1—-2
Thread-1—-3
Thread-1—-4
Thread-0—-0
Thread-0—-1
Thread-0—-2
Thread-0—-3
Thread-0—-4

       观察运行结果我们发现两个线程都执行了十次输出。并且程序是乱序执行的。这与上一段代码的运行结果是一致的。
       Thread类提供了一个构造方法,可以将Runnable对象作为参数即:Thread(Runnable target),并且Thread类也实现了Runnable接口。

注:不要调用Thread类或Runnable对象的run方法。直接调用run方法,只会执行同一个线程中的任务,而不会启动新线程。应该调用Thread.start方法。 这个方法将创建一个执行run方法的新线程。

.4 线程启动的两种方式的对比

       之前我们用继承Thread类和实现Runnable接口两种方法创建和启动了线程,大家从运行的结果可以发现两种形式的运行结果是一致的,这是就有了一个问题,它们的区别是什么?我到底应该使用哪种方式?
       针对这个问题我们不妨设计这样一个案例:在之前的两种案例中我们发现每次的运行结果均是两个线程都执行十次输出,即每个线程处理不同的资源对象,但是如果我们需要的是不同的线程共同处理同一个资源呢?

       结合上面讨论的结果,我们分别用继承Thread类和实现Runnable接口来分析这个程序。

(一)使用继承Thread的方法

编写如下代码:

class MyThread1 extends Thread {

    private int i = 10;

    public void run() {
        while (true) {
            if (i > 0) {
                System.out.println(Thread.currentThread().getName() + "---i="
                        + i--);
            } else {
                break;
            }
        }
    }

}


public class ThreadTest2 {

    public static void main(String[] args) {

        new MyThread1().start();
        new MyThread1().start();
        new MyThread1().start();
        new MyThread1().start();


    }

}

       运行之后发现四个线程在处理不同的资源对象,并没有处理同一个资源对象,因为运行结果较长这里就不粘贴过来了,大家可以自己运行看看。
       我们发现要实现这个程序,我们只能创建一个资源对象,但要创建多个线程去处理同一个资源对象,并且每个线程上所运行的是相同的程序代码。之前我们前调过不要让同一个线程重复调用start方法,会出现java.lang.IllegalThreadStateException异常。所以这里不作尝试。

(二)使用实现Runnable接口的方法

编写如下代码:

class Mythread2 implements Runnable {

    private int i = 10;

    public void run() {

        while (true) {
            if (i > 0) {
                System.out.println(Thread.currentThread().getName() + "---i="
                        + i--);
            } else {
                break;
            }
        }

    }

}



public class ThreadTest2 {

    public static void main(String[] args) {

        Mythread2 t = new Mythread2();
        new Thread(t).start();
        new Thread(t).start();
        new Thread(t).start();
        new Thread(t).start();

    }

}

运行结果如下:
Thread-1—i=10
Thread-0—i=8
Thread-2—i=9
Thread-0—i=5
Thread-3—i=6
Thread-1—i=7
Thread-3—i=2
Thread-0—i=3
Thread-2—i=4
Thread-1—i=1

       观察运行结果我们发现,每个线程调用的是同一个MyThread2对象中的run()方法,访问的是同一个对象中的变量(i)的实例,这个程序满足了我们的需求。

可见, 实现Runnable接口相对于继承Thread类来说,有如下显著的好处:

(1)适合多个相同程序代码的线程去处理同一资源的情况,把虚拟CPU(线程)同程序的代码,数据有效的分离,较好地体现了面向对象的设计思想。

(2)可以避免由于Java的单继承特性带来的局限。我们经常碰到这样一种情况,即当我们要将已经继承了某一个类的子类放入多线程中,由于一个类不能同时有两个父类,所以不能用继承Thread类的方式,那么,这个类就只能采用实现Runnable接口的方式了。

(3)有利于程序的健壮性,代码能够被多个线程共享,代码与数据是独立的。当多个线程的执行代码来自同一个类的实例时,即称它们共享相同的代码。多个线程操作相同的数据,与它们的代码无关。当共享访问相同的对象是,即它们共享相同的数据。当线程被构造时,需要的代码和数据通过一个对象作为构造函数实参传递进去,这个对象就是一个实现了Runnable接口的类的实例。


笔者水平有限,若有错漏,欢迎指正,如果转载以及CV操作,请务必注明出处,谢谢!

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值