文档章节

认识线程

如比如比
 如比如比
发布于 2015/10/02 19:04
字数 4433
阅读 29
收藏 0

线程

 

单CPU:时间切片 

多核多线程CPU:真正的并行 

在并发编程中,有两个基本的执行单元:进程和线程。在java语言中,并发编程最关心的是线程,然而,进程也是非常重要的。 

 

即使在只有单一的执行核心的计算机系统中,也有许多活动的进程和线程。因此,在任何给定的时刻,只有一个线程在实际执行。处理器的处理时间是通过操作系统的时间片在进程和线程中共享的。 

现在具有多处理器或有多个执行内核的多处理器的计算机系统越来越普遍,这大大增强了系统并发执行的进程和线程的吞吐量–但在不没有多个处理器或执行内核的简单的系统中,并发任然是可能的。 

 

进程 

进程具有一个独立的执行环境。通常情况下,进程拥有一个完整的、私有的基本运行资源集合。特别地,每个进程都有自己的内存空间。 

进程往往被看作是程序或应用的代名词,然而,用户看到的一个单独的应用程序实际上可能是一组相互协作的进程集合。为了便于进程之间的通信,大多数操作系统都支持进程间通信(IPC),如pipes 和sockets。IPC不仅支持同一系统上的通信,也支持不同的系统。 

Java虚拟机的大多数实现是单进程的。Java应用可以使用的ProcessBuilder对象创建额外的进程,多进程应用超出了本课的范围。 

 

线程 

线程有时也被称为轻量级的进程。进程和线程都提供了一个执行环境,但创建一个新的线程比创建一个新的进程需要的资源要少。 

线程是在进程中存在的 — 每个进程最少有一个线程。线程共享进程的资源,包括内存和打开的文件。这样提高了效率,但潜在的问题就是线程间的通信。 

多线程的执行是Java平台的一个基本特征。每个应用都至少有一个线程 – 或几个,如果算上“系统”线程的话,比如内存管理和信号处理等。但是从程序员的角度来看,启动的只有一个线程,叫主线程。这个线程有能力创建额外的线程

尽管面临很多挑战,多线程有一些优点使得它一直被使用。这些优点是: 

资源利用率更好

程序设计在某些情况下更简单

程序响应更快

 

资源利用率更好 

想象一下,一个应用程序需要从本地文件系统中读取和处理文件的情景。比方说,从磁盘读取一个文件需要5秒,处理一个文件需要2秒。处理两个文件则需要: 

 

5秒读取文件A 

2秒处理文件A 

5秒读取文件B 

2秒处理文件B 

--------------------- 

总共需要14秒 

 

从磁盘中读取文件的时候,大部分的CPU时间用于等待磁盘去读取数据。在这段时间里,CPU非常的空闲。它可以做一些别的事情。通过改变操作的顺序,就能够更好的使用CPU资源。看下面的顺序: 

 

5秒读取文件A 

5秒读取文件B + 2秒处理文件A 

2秒处理文件B 

--------------------- 

总共需要12秒 

CPU等待第一个文件被读取完。然后开始读取第二个文件。当第二文件在被读取的时候,CPU会去处理第一个文件。记住,在等待磁盘读取文件的时候,CPU大部分时间是空闲的。 

 

总的说来,CPU能够在等待IO的时候做一些其他的事情。这个不一定就是磁盘IO。它也可以是网络的IO,或者用户输入。通常情况下,网络和磁盘的IO比CPU和内存的IO慢的多。 

 

程序设计更优雅 

在单线程应用程序中,如果你想编写程序手动处理上面所提到的读取和处理的顺序,你必须记录每个文件读取和处理的状态。相反,你可以启动两个线程,每个线程处理一个文件的读取和操作。线程会在等待磁盘读取文件的过程中被阻塞。在等待的时候,其他的线程能够使用CPU去处理已经读取完的文件。其结果就是,磁盘总是在繁忙地读取不同的文件到内存中。这会带来磁盘和CPU利用率的提升。而且每个线程只需要记录一个文件,因此这种方式也很容易编程实现。 

 

程序响应更快 

将一个单线程应用程序变成多线程应用程序的另一个常见的目的是实现一个响应更快的应用程序。设想一个服务器应用,它在某一个端口监听进来的请求。当一个请求到来时,它去处理这个请求,然后再返回去监听。 

服务器的流程如下所述: 

while(server is active){ 

    listen for request 

    process request 

如果一个请求需要占用大量的时间来处理,在这段时间内新的客户端就无法发送请求给服务端。只有服务器在监听的时候,请求才能被接收。另一种设计是,监听线程把请求传递给工作者线程(worker thread),然后立刻返回去监听。而工作者线程则能够处理这个请求并发送一个回复给客户端。这种设计如下所述: 

while(server is active){ 

    listen for request 

    hand request to worker thread 

这种方式,服务端线程迅速地返回去监听。因此,更多的客户端能够发送请求给服务端。这个服务也变得响应更快。 

 

桌面应用也是同样如此。如果你点击一个按钮开始运行一个耗时的任务,这个线程既要执行任务又要更新窗口和按钮,那么在任务执行的过程中,这个应用程序看起来好像没有反应一样。相反,任务可以传递给工作者线程(word thread)。当工作者线程在繁忙地处理任务的时候,窗口线程可以自由地响应其他用户的请求。当工作者线程完成任务的时候,它发送信号给窗口线程。窗口线程便可以更新应用程序窗口,并显示任务的结果。对用户而言,这种具有工作者线程设计的程序显得响应速度更快。

从一个单线程的应用到一个多线程的应用并不仅仅带来好处,它也会有一些代价。不要仅仅为了使用多线程而使用多线程。而应该明确在使用多线程时能多来的好处比所付出的代价大的时候,才使用多线程。如果存在疑问,应该尝试测量一下应用程序的性能和响应能力,而不只是猜测。 

设计更复杂 

虽然有一些多线程应用程序比单线程的应用程序要简单,但其他的一般都更复杂。在多线程访问共享数据的时候,这部分代码需要特别的注意。线程之间的交互往往非常复杂。不正确的线程同步产生的错误非常难以被发现,并且重现以修复。 

 

上下文切换的开销 

当CPU从执行一个线程切换到执行另外一个线程的时候,它需要先存储当前线程的本地的数据,程序指针等,然后载入另一个线程的本地数据,程序指针等,最后才开始执行。这种切换称为“上下文切换”(“context switch”)。CPU会在一个上下文中执行一个线程,然后切换到另外一个上下文中执行另外一个线程。 

上下文切换并不廉价。如果没有必要,应该减少上下文切换的发生。 

你可以通过维基百科阅读更多的关于上下文切换相关的内容: 

http://en.wikipedia.org/wiki/Context_switch 

 

增加资源消耗 

线程在运行的时候需要从计算机里面得到一些资源。除了CPU,线程还需要一些内存来维持它本地的堆栈。它也需要占用操作系统中一些资源来管理线程。我们可以尝试编写一个程序,让它创建100个线程,这些线程什么事情都不做,只是在等待,然后看看这个程序在运行的时候占用了多少内存。

创建Thread的子类 

创建Thread子类的一个实例并重写run方法,run方法会在调用start()方法之后被执行。

一旦线程启动后start方法就会立即返回,而不会等待到run方法执行完毕才返回。就好像run方法是在另外一个cpu上执行一样。

实现Runnable接口 

第二种编写线程执行代码的方式是新建一个实现了java.lang.Runnable接口的类的实例,实例中的方法可以被线程调用。

创建子类还是实现Runnable接口? 

对于这两种方式哪种好并没有一个确定的答.案,它们都能满足要求。就我个人意见,我更倾向于实现Runnable接口这种方法。因为线程池可以有效的管理实现了Runnable接口的线程,如果线程池满了,新的线程就会排队等候执行,直到线程池空闲出来为止。而如果线程是通过实现Thread子类实现的,这将会复杂一些。 

 

有时我们要同时融合实现Runnable接口和Thread子类两种方式。例如,实现了Thread子类的实例可以执行多个实现了Runnable接口的线程。一个典型的应用就是线程池。 

 

常见错误:调用run()方法而非start()方法 

创建并运行一个线程所犯的常见错误是调用线程的run()方法而非start()方法,如下所示: 

Java代码

Thread newThread = new Thread(MyRunnable());  

newThread.run();  //should be start();  

 

起初你并不会感觉到有什么不妥,因为run()方法的确如你所愿的被调用了。但是,事实上,run()方法并非是由刚创建的新线程所执行的,而是被创建新线程的当前线程所执行了。也就是被执行上面两行代码的线程所执行的。想要让创建的新线程执行run()方法,必须调用新线程的start方法。 

 

线程名 

当创建一个线程的时候,可以给线程起一个名字。它有助于我们区分不同的线程。例如:如果有多个线程写入System.out,我们就能够通过线程名容易的找出是哪个线程正在输出。

Java代码

MyRunnable runnable = new MyRunnable();  

Thread thread = new Thread(runnable, "New Thread");  

thread.start();  

System.out.println(thread.getName());  

需要注意的是,尽管启动线程的顺序是有序的,但是执行的顺序并非是有序的。也就是说,1号线程并不一定是第一个将自己名字输出到控制台的线程。这是因为线程是并行执行而非顺序的。Jvm和操作系统一起决定了线程的执行顺序,他和线程的启动顺序并非一定是一致的。 

 

使用Sleep方法暂停一个线程 

使用Thread.sleep()方法可以暂停当前线程一段时间。这是一种使处理器时间可以被其他线程或者运用程序使用的有效方式。sleep()方法还可以用于调整线程执行节奏和等待其他有执行时间需求的线程。 

 

在Thread中有两个不同的sleep()方法,一个使用毫秒表示休眠的时间,而另一个是用纳秒。由于操作系统的限制休眠时间并不能保证十分精确。休眠周期可以被interrups所终止,我们将在后面看到这样的例子。不管在任何情况下,我们都不应该假定调用了sleep()方法就可以将一个线程暂停一个十分精确的时间周期。 

 

SleepMessages程序为我们展示了使用sleep()方法每四秒打印一个信息的例子 

Java代码

public class SleepMessages {  

    public static void main(String args[])  

        throws InterruptedException {  

        String importantInfo[] = {  

            "Mares eat oats",  

            "Does eat oats",  

            "Little lambs eat ivy",  

            "A kid will eat ivy too"  

        };  

        for (int i = 0; i < importantInfo.length;i++) {  

            //Pause for 4 seconds  

            Thread.sleep(4000);  

            //Print a message  

            System.out.println(importantInfo[i]);  

        }  

    }  

}  

 

main()方法声明了它有可能抛出InterruptedException。当其他线程中断当前线程时,sleep()方法就会抛出该异常。由于这个应用程序并没有定义其他的线程,所以并不用关心如何处理该异常。 

 

中断(Interrupts) 

中断是给线程的一个指示,告诉它应该停止正在做的事并去做其他事情。一个线程究竟要怎么响应中断请求取决于程序员,不过让其终止是很普遍的做法。这是本文重点强调的用法。 

 

一个线程通过调用对被中断线程的Thread对象的interrupt()方法,发送中断信号。为了让中断机制正常工作,被中断的线程必须支持它自己的中断(即要自己处理中断) 

 

中断支持 

线程如何支持自身的中断?这取决于它当前正在做什么。如果线程正在频繁调用会抛InterruptedException异常的方法,在捕获异常之后,它只是从run()方法中返回。例如,假设在SleepMessages的例子中,关键的消息循环在线程的Runnable对象的run方法中,代码可能会被修改成下面这样以支持中断: 

Java代码

for (int i = 0; i < importantInfo.length; i++) {  

    // Pause for 4 seconds  

    try {  

       Thread.sleep(4000);  

    } catch (InterruptedException e) {  

       // We've been interrupted: no more messages.  

      return;  

 }  

 // Print a message  

 System.out.println(importantInfo[i]);  

}  

 

许多会抛InterruptedException异常的方法(如sleep()),被设计成接收到中断后取消它们当前的操作,并在立即返回。 

 

如果一个线程长时间运行而不调用会抛InterruptedException异常的方法会怎样? 那它必须周期性地调用Thread.interrupted()方法,该方法在接收到中断请求后返回true。例如: 

Java代码

for (int i = 0; i < inputs.length; i++) {  

    heavyCrunch(inputs[i]);  

    if (Thread.interrupted()) {  

        // We've been interrupted: no more crunching.  

        return;  

    }  

}  

 

在这个简单的例子中,代码只是检测中断,并在收到中断后退出线程。在更复杂的应用中,抛出一个InterruptedException异常可能更有意义。 

Java代码

if (Thread.interrupted()){  

   throw new InterruptedException();  

}  

 

这使得中断处理代码能集中在catch语句中。 

 

下面的例子中,有线程池模拟了一个终端的场景。1个是基于sleep的终端,另外一个是循环检测终端的状态,抛出终端的异常。线程池executor.shutdownNow();方法,会立刻终端所有线程。 

Java代码

import java.util.concurrent.ExecutorService;  
import java.util.concurrent.Executors;  
  
public class InterruptedTest {  
  private static ExecutorService executor = Executors.newFixedThreadPool(10);  
  
  public static void main(String[] args) throws Exception {  
  
    executor.execute(new Runnable() {  
  
      @Override  
      public void run() {  
        try {  
          for (int i = 0; i < Integer.MAX_VALUE; i++) {  
            if (Thread.currentThread().isInterrupted()) {  
              throw new InterruptedException("循环被中断了");  
            }  
          }  
        } catch (InterruptedException e) {  
          e.printStackTrace();  
        }  
      }  
    });  
  
    executor.execute(new Runnable() {  
      @Override  
      public void run() {  
        try {  
          Thread.sleep(Integer.MAX_VALUE);  
        } catch (InterruptedException e) {  
          e.printStackTrace();  
        }  
      }  
    });  
    System.out.println("begin");  
    Thread.sleep(1000);  
    System.out.println("sleep 1000");  
    executor.shutdownNow();  
    System.out.println("end");  
  }  
  
}  

 

输出 

begin 

sleep 1000 

end 

java.lang.InterruptedException: 循环被中断了 

at com.chinaso.phl.InterruptedTest$1.run(InterruptedTest.java:18) 

at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145) 

at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615) 

at java.lang.Thread.run(Thread.java:744) 

java.lang.InterruptedException: sleep interrupted 

at java.lang.Thread.sleep(Native Method) 

at com.chinaso.phl.InterruptedTest$2.run(InterruptedTest.java:31) 

at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145) 

at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615) 

at java.lang.Thread.run(Thread.java:744) 

 

 

中断状态标记 

中断机制通过使用称为中断状态的内部标记来实现。调用Thread.interrupt()设置这个标记。当线程通过调用静态方法Thread.interrupted()检测中断时,中断状态会被清除。非静态的isInterrupted()方法被线程用来检测其他线程的中断状态,不改变中断状态标记。 

 

按照惯例,任何通过抛出一个InterruptedException异常退出的方法,当抛该异常时会清除中断状态。不过,通过其他的线程调用interrupt()方法,中断状态总是有可能会立即被重新设置。 

 

Joins 

Join()方法可以让一个线程等待另一个线程执行完成。若t是一个正在执行的Thread对象, 

Java代码

t.join();  

 

将会使当前线程暂停执行并等待t执行完成。重载的join()方法可以让开发者自定义等待周期。然而,和sleep()方法一样join()方法依赖于操作系统的时间处理机制,你不能假定join()方法将会精确的等待你所定义的时长。 

 

如同sleep()方法,join()方法响应中断并在中断时抛出InterruptedException。

 

package javay.test.java;

 

public class ThreadTest {

    public static void main(String[] args) {

        ThreadTestThread tt = new ThreadTestThread();

        tt.start();

        for (int i = 0; i < 1000; i++) {

            if ((i + 1) % 40 == 0) {

                System.out.println();

            }

            System.out.print('#');

        }

    }

}

class ThreadTestThread extends Thread {

    public void run() {

        for (int i = 0; i < 1000; i++) {

            if ((i + 1) % 80 == 0) {

                System.out.println();

            }

            System.out.print('*');

        }

    }

}

 

package javay.test.java;

 

public class RunnableTest {

    public static void main(String[] args) {

        RunnableTestThread tt = new RunnableTestThread();

        Thread t = new Thread(tt);

        t.start();

        for (int i = 0; i < 1000; i++) {

            if ((i + 1) % 40 == 0) {

                System.out.println();

            }

            System.out.print('#');

        }

    }

}

class RunnableTestThread implements Runnable {

    public void run() {

        for (int i = 0; i < 1000; i++) {

            if ((i + 1) % 80 == 0) {

                System.out.println();

            }

            System.out.print('*');

        }

    }

}

 

 

© 著作权归作者所有

如比如比
粉丝 127
博文 178
码字总数 286951
作品 0
日本
程序员
私信 提问
解决 - java.lang.OutOfMemoryError: unable to crea...

工作中碰到过这个问题好几次了,觉得有必要总结一下,所以有了这篇文章,这篇文章分为三个部分:认识问题、分析问题、解决问题。 一、认识问题: 首先我们通过下面这个测试程序来认识这个问题...

Mr&Cheng
2013/02/28
0
0
解决java.lang.OutOfMemoryError: unable to create new

工作中碰到过这个问题好几次了,觉得有必要总结一下,所以有了这篇文章,这篇文章分为三个部分:认识问题、分析问题、解决问题。 首先我们通过下面这个 测试程序 来认识这个问题。运行的环境...

陶邦仁
2015/03/23
0
2
解决 - java.lang.OutOfMemoryError: unable to create new native thread

学习中碰到过这个问题好几次了,觉得有必要总结一下,所以有了这篇文章,这篇文章分为三个部分:认识问题、分析问题、解决问题。 一、认识问题: 首先我们通过下面这个测试程序来认识这个问题...

zchuanzhao
2015/09/29
15
0
线程的基本概念

一、线程的初步认识 1、区分进程、程序和线程 程序:没有执行的指令和相关数据的集合,没有任何运行的含义。 进程:正在运行的程序就是一个进程,当你运行一个程序,也就启动了一个进程。进程...

石飞飞
2016/09/25
1
0
C#~异步编程再续~async异步方法与同步方法的并行

今天晚上没事写了个测试的代码,又看了看.net的并行编程,两个方法,一个是异步async修饰的,另一个是普通的方法,在控制台程序的Main方法里去调用这两个方法,会有什么结果呢? 首先我们看一...

mcy247
2017/12/05
0
0

没有更多内容

加载失败,请刷新页面

加载更多

重定向又是什么“垃圾”——教你再分类

  前言:之前写了几篇JSP的博客,现在又回过头来看Servlet,温故而知新,再回顾回顾,总会有收获的。以前学习Servlet感觉内容很多,现在看的时候,其实也没多少东西,只需知道Servlet的生命...

SEOwhywhy
17分钟前
0
0
一图胜千言!这10种可视化技术你必须知道

全文共4549字,预计学习时长9分钟 图片来源:Willian Justen deVasconcellos on Unsplash 相比于浩如烟海的数据表格,大部分人还是更喜欢视觉资料,这一点已不足为奇。也是出于这个原因,人们...

读芯术
21分钟前
1
0
Spring Boot 自动配置(auto-configurtion) 揭秘

本章,我们为你揭秘Spring Boot自动配置(Auto Configuration)运行机制,谈到auto-configuration,肯定离不开@EnableAutoConfiguration注解。 package org.springframework.boot.autoconfi...

爱编程的浪子
27分钟前
0
0
RabbitMQ延迟消息的延迟极限是多少?

之前在写Spring Cloud Stream专题内容的时候,特地介绍了一下如何使用RabbitMQ的延迟消息来实现定时任务。最近正好因为开发碰到了使用过程中发现,延迟消息没有效果,消息直接就被消费了的情...

程序猿DD
27分钟前
2
0
MySQL知识库语雀

类型:所有 MySQL知识库 MySQL知识库 06-05 20:57 近期阅读文章 近期已读或者未读文章列表 03-08 10:49 日常脚本 这里是平时工作会用到的一些常用的脚本,作为统一管理 03-08 05:09 培训 03...

rootliu
30分钟前
0
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部