• 进程线程


    几乎所有的操作系统都只支持同时运行多个任务,一个任务就是一个程序,每个运行中的程序就是一个进程。当一个程序运行时,内部可能包含了多个顺序执行流,每个顺序执行流就是一个线程。

       1.1 进程与线程

    进程是运行过程中的程序,具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位。一般而言,进程包括如下特征

    • 独立性:进程是系统中独立存在的实体,拥有自己独立的资源,每个进程都拥有自己私有的地址空间。
    • 动态性:进程与程序的区别在于,程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合。
    • 并发性:多个进程可以在多个处理器上并发执行,多个进程之间不会互相影响

    并发性和并行性是两个概念。并发指在同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行,是的宏观上具有多个进程同时执行的效果。

    线程是进程的组成部分,一个进程可以拥有多个线程,一个线程必须有一个父进程。线程可以拥有自己的堆栈、自己的程序计数器和自己的局部变量。但不拥有系统资源。他和父进程的其他子线程共享进程所拥有的全部资源。线程和其他线程共同协作完成进程的任务。

      1.2 线程的创建和启动

      1.21 继承Thread类创建线程类。

     步骤如下:

    ① 定义Thread的子类,重写该类的run()方法

    ② 创建Thread子类的实例,创建子线程对象

    ③ 调用线程对象的start()方法

    复制代码
    package com.gdut.thread;
    
    public class FirstThread  extends Thread{
    
        private int i;
    
        public void run(){
            for (; i < 100; i++) {
                System.out.println(getName()+" "+i);
            }
    
        }
    
        public static void main(String[] args) {
            for (int i = 0; i < 100; i++) {
                System.out.println(Thread.currentThread().getName());
    
                if(i == 20){
                  new FirstThread().start();
                  new FirstThread().start();
                }
            }
        }
    
    }
    复制代码

      1.2.2 实现Runnable接口创建线程类

    ① 定义Runnable接口的实现类,并重写该类的run()方法

    ② 创建Runnable接口实现类的实例,并以此实例作为Thread的target来创建Thread对象

    ③ 调用线程对象的start()方法

    1.2.3 使用Callable和Future创建线程

    Java 5开始,Java提供了Callable接口,该接口提供了一个call()方法可以作为线程执行体,但call()方法更加强大

    • call()方法有返回值
    • call()方法可以声明抛出异常。

    Java 5提供了Future接口来代表Callable接口里call()方法的返回值,并未Future接口提供了FutureTask实现类,该类实现了,Future接口和Runnable接口,可以作为Thread类的target。

    在Future接口定义了如下公共方法来控制关联的Callable任务

    • boolean cancle(boolean mayInterruptRunning):试图取消Future里关联的Callable任务
    • V get():返回Callable任务里call()方法的返回值
    • V get(Long timeout,TimeUnit unit):返回Callable任务里call()方法的返回值。该方法让程序最多阻塞timeout和unit指定的时间,如果经过指定时间后Callable任务依然没有返回值,将会抛出TimeoutException。
    • boolean isCancelled():如果Callable任务正常完成前被取消,则返回true。
    • boolean isdone():如果Callable任务已完成,则返回true。

    创建启动有返回值的线程步骤如下:

    ① 创建Callable接口的实现类,并实现Call()方法,该Call()方法将作为线程执行体,且该Call方法有返回值,在创建Callable实现类的实例

    ② 使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法

    ③ 使用FutureTask对象作为Thread对象的Target创建并启动线程

    ④ 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值

    复制代码
    package com.gdut.thread;
    
    import java.util.concurrent.Callable;
    import java.util.concurrent.FutureTask;
    
    public class ThirdThread {
    
        public static void main(String[] args) {
            ThirdThread rt = new ThirdThread();
            FutureTask task = new FutureTask((Callable<Integer>)() ->{
                int i =0;
    
                for (; i <100 ; i++) {
                    System.out.println(Thread.currentThread().getName()+"循环变量的值"+i);
                }
                return i;
            });
            for (int i = 0; i < 100; i++) {
                System.out.println(Thread.currentThread().getName()+"循环变量的值"+i);
                if(i == 20){
                    new Thread(task,"有返回值的线程").start();
                }
            }
          try{
              System.out.println("子线程的返回值:"+task.get());
          }catch(Exception ex){
                ex.printStackTrace();
          }
            
            
        }
        
        
    }
    复制代码

      1.3 创建线程的三种方式对比

    采用实现Runnable、Callable接口的方式创建多线程的优缺点

    • 线程只是实现了Runnable接口或Callable接口,还可以继承其他类
    • 在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU,代码,数据分开,形成清晰的模型,较好体现面向对象的思想
    • 劣势是,编程稍微复杂,如果需要访问当前线程,则必须使用Thread.currentThread()方法。

    所以一般推荐使用实现Runnable、Callable接口的方式创建多线程。

      1.4 线程的生命周期

    线程的生命周期经过:新建,就绪,运行,阻塞和死亡5种状态。使用new关键字创建线程,该线程就处于新建状态。当线程对象调用了start()方法之后,该程序就处于就绪状态,Java虚拟机回为其创建方法调用栈和程序计数器,处于这个状态的程序还没有开始运行,只是表示该线程可以运行了,至于何时开始运行,取决于JVM里线程调度器的调度。

    处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,则该线程处于运行状态。

    当一个线程开始运行后,他不可能一直处于运行状态(除非它运行执行体非常短,一下子就执行完),线程在执行过程中需要被中断,目的使其他线程获得执行的机会,在选择下一个线程,系统会考虑线程的优先级。

    当一个线程调用了它的sleep()和yield()方法后主动放弃所占用的资源。

    当发生如下情况,线程会进入阻塞状态:

    • 线程调用sleep()方法主动放弃所占用的处理器资源
    • 线程调用了一个阻塞式的IO方法,在该方法返回前,该线程被阻塞
    • 线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有。
    • 线程在等待某个通知
    • 程序调用了线程的suspend()方法将该线程挂起。但这个方法容易导致死锁,应该尽量避免使用该方法。

    被阻塞线程在合适的时候重新进入就绪状态。重新等待线程调度器调度它。

    当发生特定的情况时可以解除上面的阻塞

    • 调用sleep()方法的线程经过了指定时间
    • 线程调用的阻塞式IO方法已经返回
    • 线程成功的获得了试图取得同步监视器
    • 线程正在等待某个通知时,其他线程发出了一个通知
    • 处于挂起状态的线程被调用了resume()回复方法

    线程会以如下三种方式结束,进入死亡状态

    • run()或call()方法执行完成,线程正常结束
    • 线程抛出一个为捕获的Exception
    • 直接调用该线程的stop()方法来结束该线程——这个方法容易导致死锁,应该尽量避免使用该方法。

    二. 控制线程

      2.1  join线程

    Thread提供了让一个线程等待另一个线程完成的方法——join()方法。

    当程序执行流中调用了其他线程的join()方法时,调用线程被阻塞,知道被join()方法加入的join线程执行完为止

    join()方法通常由使用的线程的程序调用,将大问题划分许多小问题,每个小问题分配一个线程。当所有的小问题处理后,在调用主线程进一步处理。

    复制代码
    package com.gdut.thread;
    
    public class JoinThread extends Thread{
        public JoinThread(String name) {
            super(name);
        }
    
        @Override
        public void run() {
            for (int i = 0; i <100 ; i++) {
                System.out.println(getName()+" "+i);
            }
        }
    
        public static void main(String[] args) throws Exception {
            new JoinThread("新线程").start();
            for (int i = 0; i <100 ; i++) {
                if(i == 20){
                    JoinThread jt = new JoinThread("被join的线程");
                    jt.start();
                    jt.join();
                }
                System.out.println(Thread.currentThread().getName()+" "+i);
            }
        }
    
    }
    复制代码

       2.2 后台线程

     有一种线程,后台执行的,它的任务是为其他线程提供服务。这种线程被称为后台线程,又称守护线程或精灵线程。JVM的垃圾回收线程就是典型的后台线程

    后台线程有个特征:如果所有的前台线程都死亡,后台线程会自动死亡。调用Thread对象的setDaemon(true)方法可将指定线程设置成后台线程。

      2.3 线程睡眠:sleep

    如果需要让正在执行的线程暂停一段时间,并进入阻塞状态,则可以通过Thread类的静态方法sleep()来实现。

    在调用了sleep()方法之后,线程进入阻塞状态,在其睡眠时间内,该线程将不会获得执行的机会,即使系统中没有其他可执行的线程。

      2.4 线程让步

    yield()方法是一个和sleep()方法有也是点相似的方法,它Thread类的提供的一个静态方法,它可以让当前正在执行的线程暂停,但它不会阻塞该线程,只是将线程转入就绪状态。yield()方法只是让当前的线程暂停一下,让系统的线程调度器重新调度一次,完全可能的情况是:当线程调用了yield()方法暂停之后,线程调度器又将其调度出来重新执行

    关于sleep()方法和yield()方法的区别

    • sleep()方法暂停当前线程后,会给其他线程执行机会,不会理会其他线程的优先级;但yield()方法只会给优先级相同,或优先级更高的线程执行机会
    • sleep()方法会将线程转入阻塞状态,知道经过阻塞时间才会转入就绪状态;yield()方法只是强制将线程转入就绪状态
    • sleep()方法声明抛出了InterruptedException异常,所以调用sleep()方法要么捕捉异常,要么显式声明抛出异常;而yield()方法则没有声明抛出异常。
    • sleep()方法比yield()方法有更好的可移植性,通常不建议使用yield()方法来控制并发线程的执行

      2.5 改变线程优先级

    每个线程执行时都具有一定的优先级,优先级高的线程得到更多执行的机会,而优先级低的线程则得到较少的执行机会。

    每个线程默认的优先级都和创建它的父线程的优先级相同,默认情况下,main线程具有普通优先级,由main线程创建的子线程也具有普通优先级。

    Thread类提供了setPriority(int new Priority)、getPriority()方法来设置和返回指定线程的优先级,其中setPriority(int new Priority)参数可以是一个整数,范围1~10,也可以使用Thread类的如下三个常量:

    • MAX_PRIORITY:其值是10
    • MIN_PRIORITY:其值是1
    • NORM_PRIORITY:其值是5

    三. 线程同步和线程安全问题

      3.1 线程安全

    脏数据问题,例如多个线程同时操作公共数据引发的数据出错问题,这里不再叙述

      3.2 同步代码块

    为了解决上面的问题,Java多线程支持引入了同步监视器来解决这个问题,使用同步监视器的通用方法就是同步代码块。同步代码块的语法格式如下:

    synchronized(obj){
    //此处代码就是同步代码块
    }

    上面代码的含义是:线程开始执行同步代码块之前,必须先获得同步监视器的锁定。

      3.3 同步方法

    与同步代码块对应,Java的多线程安全还支持同步方法,同步方法就是用synchronized关键字来修饰某个方法,则该方法称为同步方法。对于synchronized修饰的方法(非static方法)而言,无须显式指定同步监视器,同步方法的监视器是this,也就是调用该方法的对象

    当执行了同步监视器的wait()方法,则当前线程暂停,并释放同步监视器。

      3.4 同步锁

    从Java 5开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象实现同步,在这种机制下,同步锁由lock充当。

    某些锁可能允许对贡献资源的访问。如ReadWriteLock,Lock,ReentrantRock(可重入锁)。为ReadWriteRock提供了ReentrantReadWriteLock实现类。

    通常建议在Finally块确保在必要时释放锁。

      3.5 死锁

           当两个线程相互等待对方释放锁是就会发生死锁。一旦出现死锁,整个程序将不会发生任何异常,也不会给出任何提示。只是所有的线程都处于阻塞状态,无法继续。

  • 相关阅读:
    vs利用正则表达式统计代码行数
    C/C++/MFC的include搜索顺序
    afxwin.h afx.h afxver.h afxv_w32.h windows.h包含顺序
    C/C++中内存块置0的三种方法:memset,ZeroMemory,SecurZeroMemory
    关于socket阻塞与非阻塞情况下的recv、send、read、write返回值
    vs2008工程设置里添加宏
    使用共享网卡的NAT模式配置VMware中的CentOS的上网功能
    VMware10 安装centos6.7 设置NAT模式
    《大道至简》第7,8章读书笔记
    ie11浏览器和chrome浏览器对于bgsound和background的一些区别
  • 原文地址:https://www.cnblogs.com/xiaobing1/p/11377902.html
Copyright © 2020-2023  润新知