JAVA并发编程(一)多线程介绍

Scroll Down

JAVA并发编程

线程以及多线程编程

什么是进程,什么是线程,进程与线程的区别是什么?

进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位
进程和线程都是一个时间段的描述,是CPU工作时间段的描述.
进程就是包含上下文切换的程序执行时间总和 = CPU加载上下文+CPU执行+CPU保存上下文
线程是什么呢?进程的颗粒度太大,每次都要有上下的调入,保存,调出。如果我们把进程比喻为一个运行在电脑上的软件,那么一个软件的执行不可能是一条逻辑执行的,必定有多个分支和多个程序段,就好比要实现程序A,实际分成 a,b,c等多个块组合而成。那么这里具体的执行就可能变成:程序A得到CPU =》CPU加载上下文,开始执行程序A的a小段,然后执行A的b小段,然后再执行A的c小段,最后CPU保存A的上下文。这里a,b,c的执行是共享了A的上下文,CPU在执行的时候没有进行上下文切换的。这里的a,b,c就是线程,也就是说线程是共享了进程的上下文环境,的更为细小的CPU时间段。到此全文结束,再一个总结:进程和线程都是一个时间段的描述,是CPU工作时间段的描述,不过是颗粒大小不同。

作者:zhonyong
链接:https://www.zhihu.com/question/25532384/answer/81152571
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

什么是多线程编程?

多线程编程是以线程为基本单位的一种编程范式

为什么要使用多线程?

比如有个水缸,要挑水把水填满,如果一个人的话,就需要很长的时间才能提满水,如果是多个人的话,可以大大节约时间.多线程最重要的功能就是使系统资源利用率提升,程序处理效率提高

线程的创建,启动与执行

java中创建一个线程就是创建一个Thread类的实例,在java中运行一个线程实际上就是让java虚拟机执行该线程的run方法,从而使响应线程的任务处理逻辑代码得以执行,其中Thread类的start方法作用就是启动响应的线程,启动一个线程的实质是请求虚拟机运行相应的线程,而这个线程具体何时能够运行是由线程调度器(操作系统的一部分)来决定的,因此start方法调用结束并不意味着相应线程已经开始运行,这个线程可能稍后才会被运行甚至永远不会运行.

以定义Thread子类的方式创建线程

public class TestThread extends Thread {

    @Override
    public void run() {
        System.out.printf("2-线程名为:%s.%n",Thread.currentThread().getName());
    }
}
public class App {
    public static void main(String[] args) {
        Thread thread = new TestThread();
        thread.start();
        System.out.printf("1.主线程名:%s.%n", Thread.currentThread().getName());
    }
}

image-20200308215305311

以创建Runnable接口实例的方式创建线程

public class RunnableTest implements Runnable {

    @Override
    public void run() {
        System.out.printf("2-线程名为:%s.%n",Thread.currentThread().getName());
    }
}
        Thread thread = new Thread(new RunnableTest());
        thread.start();
        System.out.printf("1.主线程名:%s.%n", Thread.currentThread().getName());

image-20200308220115334

当然结果也可能会是反转,先输出子线程,后输出main线程

这两种方式都可以创建线程,线程的run方法执行结束后,相应的线程运行也就结束了,包括因为异常而导致的中止.线程是"一次性的",不能通过重复调用一个线程的start方法,让他重新开始运行,多次调用一个Thread的实例的start方法也会导致其抛出java.lang.IllegalThreadStateException异常.JAVA中并没有禁止直接调用Thread实例的run方法,虽然这样做没有意义,但是线程类也只是一个普通的对象而已,如果直接调用run方法,那么将会运行在当前线程中,而不是新启动一个线程.

        Thread thread = new Thread(new RunnableTest());
        thread.run();
        System.out.printf("1.主线程名:%s.%n", Thread.currentThread().getName());

image-20200308221208349

最终会发现结果运行的线程是一致的.

线程两种创建方式的区别

定义Thread子类的方式创建线程是基于继承的技术,以runnable实例为构造器参数直接通过new创建实例是基于组合的技术,组合中类与类的耦合性更低,并且由于JAVA中线程是一个特殊的Runnable实例,因此在创建它的时候虚拟机会为其分配调用栈空间,内核线程等资源,因此创建一个线程实例比创建一个普通的Runnable实例来说消耗更高,所以我们通常建议使用第二种方式

线程的属性

属性属性类型及用途是否只读注意事项
编号(ID)类型Long,标识不同的线程,不同的线程有不同的ID号JAVA虚拟机的一次运行中,ID是唯一的,但是如果重启之后,ID是会重新生成的,无法拿ID来作为持久化的信息,因为重启服务之后ID可能会重复
名称(NAME)类型String,面向人工的一个属性,用于区分不同的线程,可重新赋值我们可以将不同的线程设置相同的名称,但是设置不同的名称有利于代码调试和问题的定位
线程类别(Daemon)类型boolean,值为true时表示为守护线程,否则表示为普通的用户线程线程类别属性必须在相应线程启动之前设置,在start之前调用setDaemon方法.
优先级(Priority)类型int,该属性本质上是给线程调度器提示,用于应用程序希望优先执行哪个线程,java定义了1-10的优先级,默认值一般是5.一般使用默认优先级即可,不恰当的设置可能会导致严重的问题(线程饥饿)

按照线程是否会阻止虚拟机正常停止来分为守护线程和用户线程,线程的daemon属性用于表示该线程是否为守护线程,用户线程会阻止JAVA虚拟机的正常停止,守护线程通常不会,守护线程通常用于执行一些重要性不是很高的任务,例如监视其他线程的运行情况

Thread类中的常用方法

方法功能备注
static native Thread currentThread()返回当前线程,即当前代码的执行线程在多线程中调用该方法,返回值可能对应不同的线程对象
void run()用于实现线程的任务处理逻辑该方法是由JAVA虚拟机直接调用的,一般情况下应用程序不应该调用该方法
void start()启动相应线程该方法返回时并不意味着方法已经启动或结束,一个Thread实例只能被调用一次,多次调用会抛出异常
void join()等待相应线程运行结束若线程A调用线程B的join方法,则线程A的线程会暂停,等待B线程运行结束
static native void yield()使当前线程主动放弃对处理器的占用,这可能导致当前线程被暂停这个方法是不可靠,该方法被调用时当前线程可能仍然继续运行.
static native void sleep()使当前线程休眠指定的世界

线程的层次关系

JAVA中线程与线程之间是存在联系的,假设线程A执行的代码创建了线程B,那么我们一般称线程B是线程A的子线程,相应的线程A是线程B的父线程.

父线程是守护线程,则子线程默认是守护线程,父线程是用户线程,则子线程也默认是用户线程,但是父线程可以在创建子线程户启动子线程前将子线程设置为守护线程或用户线程.

子线程的优先级默认和父线程相同

java中没有API可以通过子线程获取父线程或者父线程获取所有的子线程,父线程与子线程的声明周期也没有必然的联系,父线程运行结束后,子线程依然可以运行,子线程结束后,父线程依然可以继续运行

线程的生命周期状态

  • NEW:一个已创建而未启动的线程处于该状态,因为线程只能启动一次,所以一个线程只可能一次处于该状态
  • RUNNABLE:该状态可以看成一个复合状态,它包括了两个子状态,READYRUNNING,前者标识线程可以被线程调度器进行调度而使之处于RUNNING状态,后者表示线程正在运行,而线程执行Thread.yield()之后,状态可能会有RUNNING变成Ready,处于READY子状态线程也称为活跃线程.
  • BLOCKED:一个线程发起一个阻塞式I/O操作或者申请一个其他线程持有的独占资源时(锁),线程会处于该状态,处于BLOCKED状态的线程并不会占用处理器资源,当线程完成I/O操作或者获得了其申请的资源后,该线程的状态又可以转换为RUNNABLE
  • WAITING:一个线程执行了某些特定方法之后就会处于这种等待其他线程执行另外一些特定操作的状态,能够使其执行线程变更为WAITING状态的方法包括:Obejct.wait(),Thread.join(),LockSupport.park(Object),使相应线程从WAITING变更为RUNNABLE的相应方法包括:Object.notify()/notifyAll()和LockSupport.unpark(Object).
  • TIMED_WAITING:该状态和WAITING类似,差别在于处于该状态的线程并非无限制等待其他线程执行特性操作,而是处于带有时间限制的等待状态.
  • TERMINATED:已经结束的线程状态,一个线程只能处于一次该状态

线程在生命周期中,只可能有一次处于NEW状态和TERMINATED状态.

多线程的优势和风险

优势:

  • 提高系统的吞吐率,多线程编程使得一个进程中可以有多个并发的操作
  • 提高响应性
  • 充分利用多核处理器资源
  • 最小化对系统资源的使用,多个线程可以共享其进程中所申请的资源与上下文,可节约对系统资源的使用
  • 简化程序的结构,线程可以简化复杂应用程序的结构

风险:

  • 线程安全问题,多个线程共享数据的时候,如果没有采取相应的并发访问控制措施,那么就可能产生数据一致性的问题,例如读取脏数据,丢失更新
  • 线程活性问题,代码编写不当可能会使线程永远处于BLOCKED状态,产生死锁
  • 上下文切换,处理器由执行一个线程到切换另一个线程的时候操作系统所需要做的一个动作称为上下文切换,它增加了系统的消耗,降低了系统的吞吐率
  • 可靠性,一方面多线程中一个线程的终止不会影响其他线程,另一方面线程是进程的一个组件,如果一个进程产生内存泄露导致虚拟机崩溃,会导致所有的线程都无法继续运行

总结

image-20200308235857904