zoukankan      html  css  js  c++  java
  • java进阶知识--多线程入门

    1.1 线程与进程

    • 进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。

    • 线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。

      简而言之:一个程序运行后至少有一个进程,一个进程中可以包含多个线程。

      线程调度:

    • 分时调度

       所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。

    • 抢占式调度

       优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。

      1. 设置线程的优先级
      2. 抢占式调度详解
          CPU(中央处理器)使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核而言,某个时刻,只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是在同一时刻运行。
          其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。

    1.2 并发与并行

    • 并发:指两个或多个事件在同一个时间段内发生。

    • 并行:指两个或多个事件在同一时刻发生(同时发生)。

      在操作系统中,安装了多个程序,并发指的是在一段时间内宏观上有多个程序同时运行,这在单 CPU 系统中,每一时刻只能有一道程序执行,即微观上这些程序是分时的交替运行,只不过是给人的感觉是同时运行,那是因为分时交替运行的时间是非常短的。

      而在多个 CPU 系统中,则这些可以并发执行的程序便可以分配到多个处理器上(CPU),实现多任务并行执行,即利用每个处理器来处理一个可以并发执行的程序,这样多个程序便可以同时执行。目前电脑市场上说的多核 CPU,便是多核处理器,核越多,并行处理的程序越多,能大大的提高电脑运行的效率。

    注意:单核处理器的计算机肯定是不能并行的处理多个任务的,只能是多个任务在单个CPU上并发运行。同理,线程也是一样的,从宏观角度上理解线程是并行运行的,但是从微观角度上分析却是串行运行的,即一个线程一个线程的去运行,当系统只有一个CPU时,线程会以某种顺序执行多个线程,我们把这种情况称之为线程调度

    1.3 创建线程类

     1.3.1 方式一:继承Thread类

      1.3.1.1 实现与原理

      Java使用java.lang.Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。每个线程的作用是完成一定的任务,实际上就是执行一段程序流(即一段顺序执行的代码)。Java使用线程执行体来代表这段程序流。Java中通过继承Thread类来创建启动多线程的步骤如下:

    1. 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体。run()方法相当于其他线程的main方法。

    2. 创建Thread子类的实例,即创建了线程对象。

    3. 调用线程对象的start()方法来启动该线程。

       多线程执行时,到底在内存中是如何运行的呢?以下图解为简单案例说明:

       多线程执行时,在栈内存中,其实每一个执行线程都有一片自己所属的栈内存空间,进行方法的压栈和弹栈。

      当执行线程的任务结束了,线程自动在栈内存中释放了。当所有的执行线程都结束了,那么进程就结束了。

      1.3.1.2 相关方法

       构造方法

    • public Thread():分配一个新的线程对象;
    • public Thread(String name):分配一个指定名字的新的线程对象;
    • public Thread(Runnable target):分配一个带有指定目标的新的线程对象;
    • public Thread(Runnable target, String name):分配一个带有指定目标的新的线程对象并指定名字;

       常用方法

    • public String getName():获取当前线程名称。
    • public void start():导致此线程开始执行;Java虚拟机调用此线程的run方法。//开启线程,使线程处于就绪状态
    • public void run():此线程要执行的任务在此处定义代码。//线程执行体
    • public final void setPriority(int newPriority):更改线程的优先级。 //用1~10表示,数字越大,优先级越高。默认值为5。
    • public static void sleep(long millis):使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。
    • public static Thread currentThread():返回对当前正在执行的线程对象的引用。

     1.3.2 方式二:实现Runnable接口

      1.3.2.1 实现与原理

      实现步骤:

        1. 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
        2. 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
        3. 调用线程对象的start()方法来启动线程。

      实现原理:

        通过实现Runnable接口,使得该类有了多线程类的特征。run()方法是多线程程序的一个执行目标。所有的多线程代码都在run方法里面。Thread类实际上也是实现了Runnable接口的类。
        在启动的多线程的时候,需要先通过Thread类的构造方法Thread(Runnable target)构造出对象,然后调用Thread对象的start()方法来运行多线程代码。
        实际上所有的多线程代码都是通过运行Thread的start()方法来运行的。因此,不管是继承Thread类还是实现Runnable接口来实现多线程,最终还是通过Thread的对象的API来控制线程的,熟悉Thread类的API是进行多线程编程的基础。

    注意:Runnable对象仅仅作为Thread对象的target,Runnable实现类里包含的run()方法仅作为线程执行体。而实际的线程对象依然是Thread实例,只是该Thread线程负责执行其target的run()方法。

     1.3.3 方式三:匿名内部类的方式实现线程的创建

      使用线程的内匿名内部类方式,可以方便的实现每个线程执行不同的线程任务操作。
      使用匿名内部类的方式实现Runnable接口,重写Runnable接口中的run方法:

    public class NoNameInnerClassThread {
    	public static void main(String[] args) {
    //		new Runnable(){
    //			public void run(){
    //				for (int i = 0; i < 20; i++) {
    //					System.out.println("开启新线程:"+i);
    //				}
    //			}
    //		};			//---这个整体﹐相当于new MyRunnable()
    		Runnable r = new Runnable(){
    			public void run(){
    				for (int i = 0; i < 20; i++) {
    					System.out.println("开启新线程:"+i);
    				}
    			}
    		};
    
    		new Thread(r).start();
    		
    		for (int i; i < 20; i++) {
    			System.out.println("主线程:"+i);
    		}
    	}
    }
    

     1.3.4 方式四:ExecutorService、Callable<Class>、Future 有返回值线程

      有返回值的任务必须实现 Callable 接口,类似的,无返回值的任务必须实现 Runnable 接口。

      执行Callable 任务后,可以获取一个 Future 的对象,在该对象上调用 get 就可以获取到 Callable 任务返回的 Object 了,再结合线程池接口 ExecutorService 就可以实现传说中有返回结果的多线程了。

    //创建一个线程池
    ExecutorService pool = Executors.newFixedThreadPool(taskSize);
    // 创建多个有返回值的任务
    List<Future> list = new ArrayList<Future>();
    for (int i = 0; i < taskSize; i++) {
    	Callable c = new MyCallable(i + " ");
    	// 执行任务并获取 Future 对象
    	Future f = pool.submit(c);
    	list.add(f);
    }
    
    // 关闭线程池
    pool.shutdown();
    
    // 获取所有并发任务的运行结果
    for (Future f : list) {
    	// 从 Future 对象上调用get获取任务的返回值,并输出到控制台
    	System.out.println("res:" + f.get().toString());
    }
    

      1.3.5 方式五:基于线程池的方式

      线程和数据库连接这些资源都是非常宝贵的资源。那么每次需要的时候创建,不需要的时候销毁,是非常浪费资源的。那么我们就可以使用缓存的策略,也就是使用线程池。

    // 创建线程池(newFixedThreadPool是创建线程池的方式之一)
    ExecutorService threadPool = Executors.newFixedThreadPool(10);
    while(true) {
    	threadPool.execute(new Runnable() { // 提交多个线程任务,并执行
    		@Override
    		public void run() {
    			System.out.println(Thread.currentThread().getName() + " is running ..");
    			try {
    				Thread.sleep(3000);
    			} catch (InterruptedException e) {
    				e.printStackTrace();
    			}
    		}
    	});
    }
    

    1.4 Thread和Runnable的区别

      实现Runnable接口比继承Thread类所具有的优势:
        1. 适合多个相同的程序代码的线程去共享同一个资源。
        2. 可以避免java中的单根继承的局限性。
        3. 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
        4. 线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类。

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

    1.5 start 与 run 区别

      1. start()方法来启动线程,真正实现了多线程运行。这时无需等待 run 方法体代码执行完毕,可以直接继续执行下面的代码。

      2. 通过调用 Thread 类的 start()方法来启动一个线程, 这时此线程是处于就绪状态, 并没有运行。

      3. 方法 run()称为线程体,它包含了要执行的这个线程的内容,线程就进入了运行状态,开始运行 run 函数当中的代码。 Run 方法运行结束, 此线程终止。然后 CPU 再调度其它线程。

    1.6 终止线程的4种方式

     1.6.1 正常运行结束

      程序正常运行结束,线程自动结束终止。

     1.6.2 使用退出标志退出线程

      一般 run()方法执行完,线程就会正常结束,然而,常常有些线程是伺服线程,它们需要长时间的运行,只有在外部某些条件满足的情况下,才能关闭这些线程。使用一个变量来控制循环,例如:最直接的方法就是设一个 boolean 类型的标志,并通过设置这个标志为 true 或 false 来控制 while循环是否退出,代码示例:

    public class ThreadSafe extends Thread {
    	public volatile boolean exit = false;
    	public void run() {
    		while (!exit){
    			//do something
    		}
    	}
    }
    

      定义了一个退出标志 exit,当 exit 为 true 时,while 循环退出,exit 的默认值为 false。在定义 exit 时,使用了一个 Java 关键字 volatile,这个关键字的目的是使 exit 同步,也就是说在同一时刻只能由一个线程来修改 exit 的值。

     1.6.3 Interrupt方法结束线程

      使用 interrupt()方法来中断线程有两种情况:

      1. 线程处于阻塞状态。

      如使用了 sleep,同步锁的 wait,socket 中的 receiver,accept 等方法时,会使线程处于阻塞状态。当调用线程的 interrupt()方法时,会抛出 InterruptException 异常。阻塞中的那个方法抛出这个异常,通过代码捕获该异常,然后 break 跳出循环状态,从而让我们有机会结束这个线程的执行。

      通常很多人认为只要调用 interrupt 方法线程就会结束,实际上是错的, 一定要先捕获 InterruptedException 异常之后通过 break 来跳出循环,才能正常结束 run 方法。

      2.线程未处于阻塞状态

      使用 isInterrupted()判断线程的中断标志来退出循环。当使用interrupt()方法时,中断标志就会置 true,和使用自定义的标志来控制循环是一样的道理。

      Thread类中interrupt()、interrupted()和isInterrupted()方法详解

     1.6.4 stop方法终止线程(线程不安全)

      thread.stop()这种方法属于强行终止线程,但是 stop 方法是很危险的,就象突然关闭计算机电源,而不是按正常程序关机一样,可能会产生不可预料的结果,不安全主要是:thread.stop()调用之后,创建子线程的线程就会抛出 ThreadDeatherror 的错误,并且会释放子线程所持有的所有锁。一般任何进行加锁的代码块,都是为了保护数据的一致性,如果在调用thread.stop()后导致了该线程所持有的所有锁的突然释放(不可控制),那么被保护数据就有可能呈现不一致性,其他线程在使用这些被破坏的数据时,有可能导致一些很奇怪的应用程序错误。因此,并不推荐使用 stop 方法来终止线程。

    1.7 线程基本方法

      线程相关的基本方法有 wait,notify,notifyAll,sleep,join,yield 等。

      1. 线程等待(wait)

      调用该方法的线程进入 WAITING 状态,只有等待另外线程的通知或被中断才会返回,需要注意的是调用 wait()方法后,会释放对象的锁。因此,wait 方法一般用在同步方法或同步代码块中。

      2. 线程睡眠(sleep)

      sleep 导致当前线程休眠,与 wait 方法不同的是 sleep 不会释放当前占有的锁,sleep(long)会导致线程进入 TIMED-WATING 状态,而 wait() 方法会导致当前线程进入 WATING 状态。

     sleep 与 wait 区别

       1. 对于 sleep()方法,我们首先要知道该方法是属于 Thread 类中的。而 wait()方法,则是属于Object 类中的。
       2. sleep()方法导致了程序暂停执行指定的时间,让出 cpu 该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。
       3. 在调用 sleep()方法的过程中,线程不会释放对象锁。
       4. 而当调用 wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用 notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。

      3. 线程让步(yield)

      yield 会使当前线程让出 CPU 执行时间片,与其他线程一起重新竞争 CPU 时间片。一般情况下,优先级高的线程有更大的可能性成功竞争得到 CPU 时间片,但这又不是绝对的,有的操作系统对线程优先级并不敏感。

      4. 线程中断(interrupt)

      中断一个线程,其本意是给这个线程一个通知信号,会影响这个线程内部的一个中断标识位。这个线程本身并不会因此而改变状态(如阻塞,终止等)。

    • 调用 interrupt()方法并不会中断一个正在运行的线程。也就是说处于 Running 状态的线程并不会因为被中断而被终止,仅仅改变了内部维护的中断标识位而已。
    • 若调用 sleep()而使线程处于 TIMED-WATING 状态,这时调用 interrupt()方法,会抛出InterruptedException,从而使线程提前结束 TIMED-WATING 状态。
    • 许多声明抛出 InterruptedException 的方法(如 Thread.sleep(long mills 方法)),抛出异常前,都会清除中断标识位,所以抛出异常后,调用 isInterrupted()方法将会返回 false。
    • 中断状态是线程固有的一个标识位,可以通过此标识位安全的终止线程。比如,你想终止一个线程 thread 的时候,可以调用 thread.interrupt()方法,在线程的 run 方法内部可以根据 thread.isInterrupted()的值来优雅的终止线程。

      5. 等待其他线程终止(join)

      join() 方法,等待其他线程终止,在当前线程中调用一个线程的 join() 方法,则当前线程转为阻塞状态,等到另一个线程结束,当前线程再由阻塞状态变为就绪状态,等待 cpu 的宠幸。

     为什么要用 join()方法?

       很多情况下,主线程生成并启动了子线程,需要用到子线程返回的结果,也就是需要主线程需要在子线程结束后再结束,这时候就要用到 join() 方法。

      6. 线程唤醒(notify/notifyAll)

      Object 类中的 notify() 方法,唤醒在此对象监视器上等待的单个线程,如果所有线程都在此对象上等待,则会选择唤醒其中一个线程,选择是任意的,并在对实现做出决定时发生,线程通过调用其中一个 wait() 方法,在对象的监视器上等待,直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程,被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争。类似的方法还有 notifyAll()唤醒再次监视器上等待的所有线程

      7. 其他方法

      isAlive(): 判断一个线程是否存活。
      activeCount(): 程序中活跃的线程数。
      enumerate(): 枚举程序中的线程。
      currentThread(): 得到当前线程。
      isDaemon(): 一个线程是否为守护线程。
      setDaemon(): 设置一个线程为守护线程。(用户线程和守护线程的区别在于,是否等待主线程依赖于主线程结束而结束)
      setName(): 为线程设置一个名称。
      setPriority(): 设置一个线程的优先级。
      getPriority()::获得一个线程的优先级。

  • 相关阅读:
    CoreOS Hyper-V 安装
    RancherOS Hyper-V 安装
    Android sdk content loader 0%的解决方案
    在launcher隐藏应用图标[大杂烩]
    RTSP、HTTP、HTTPS、SDP四种协议详解
    Webview上下滑动渐变色问题解决
    调节listview的item高度
    软键盘消失有残影 不影响activity原有布局 不会重绘之前界面
    android横竖屏禁止重新绘制的方法
    去除actionbar的左侧图标
  • 原文地址:https://www.cnblogs.com/sun9/p/13499261.html
Copyright © 2011-2022 走看看