Java编程之多线程&并发编程(上)

1. 介绍-多任务和多线程

Java 支持单线程和多线程,

  • 单线程程序有一个单一入口(main()函数)及单一出口;
  • 多线程程序有一个初始入口(main()函数),随之有很多与main()函数并行的入口和出口。“并行”指的是同时进行多项任务。

Java支持在一个程序中并发运行多个线程以实现并发编程。线程,也称作轻量进程,是一个具有确定起点和终点的单一顺序控制流。线程的生命周期中只有一个执行点。一个线程不能独立存在,它必须是进程的一部分。

下图所示为在一个单独CPU下运行的三线程程序:


单CPU下运行的三线程程序
单CPU下运行的三线程程序

1.1 多任务(或多进程)

现在的操作系统(如Windows和 UNIX)都是多任务系统,多任务系统能通过共享计算资源如CPUs,主内存,I/O通道等并发执行多任务。在单CPU机器中同一时间(CPU时间片)只有一个任务被执行;而在多CPU机器中多任务能在CPU之间或者是在时间片上同时被执行。

多任务在当今的操作系统中被大量运用,通过充分利用并优化计算资源的使用能实现更高执行效率。通常有以下两种多任务操作系统:

  1. 协同式多任务系统(cooperative multitasking system):每个任务必须自发将资源控制权交与其他任务。这种系统的缺点是失控或没能实现协同操作的任务可能会挂起整个系统;
  2. 抢占式多任务系统(Pre-emptive multitasking systems):任务被分配CPU(s)时间片,一旦分配耗尽将强制将控制权交与其他任务。

1.2 多线程 (进程中)

在UNIX中用fork命令创建新进程,而在Windows中通过启动一个程序创建新进程。一个进程或程序有自己的地址空间和控制区块。之所以被称为重量级(heavyweight)是因为它消耗更多的系统资源。在一个进程或程序中,我们能并发运行多个线程以提高处理效率。

线程,不同于重量级进程,轻量并在进程中运行-它们共享相同的地址空间、资源和环境,之所以谓之轻量是因为它在重量级进程中运行且利用系统为该程序及其环境分配的资源。线程必须在运行的进程中划分出自己的资源,如:线程有自己的堆栈,寄存器和程序计数器。运行在线程中的代码仅在那个环境(context)中运行,因此线程(含有线性执行流)也称为执行上下文(Execution context)。

程序中的多线程通过优化系统资源的使用提升了程序的运行效率,如:当一个线程被阻塞(等待I/O操作的完成),另一个线程能利用CPU时间执行计算,这样能获得更高的效率和更大的吞吐量。

多线程在提供更好的用户交互方面也很重要。例如,在文本处理器中,当一个线程正在打印或保存文件的时候,另一个线程能继续打字。在图形用户界面(GUI)应用中,多线程对于提供响应式用户界面而言必不可少。

在这篇文章里我们用到Swing应用来进行详细说明,因为Swing应用依赖于多线程(执行特有的函数,重回并加工事件),比较适合用来说明多线程的应用。

典型的Java程序在单一进程中运行,一般不会用到多个进程,不过在该进程中经常会用到多线程来并行处理多个任务。一个独立的Java应用以关联main()方法的单一线程(主线程)开始,之后该主线程可以启动更多新线程。

2. "界面无响应"

计数程序
计数程序

通过下面计数Swing程序能最好地说明这个常见的“界面无响应”(Unresponsive User Interface(UI))问题。

该GUI程序有两个按钮。点击“Start Counting”开始计数,点击“Stop Counting”停止(暂停)计数。这两个处理函数通过一个名为“stop”的布尔值通信。Stop按钮设置“stop”标记; Start按钮检查在继续下个计数的时候是否已经设置了“stop”标记。

在Eclipse或NetBeans下面编写程序来追踪这些线程的执行情况。

2.1 例1: 无响应 UI

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
 
/** Illustrate Unresponsive UI problem caused by "busy" Event-Dispatching Thread */
public class UnresponsiveUI extends JFrame {
    private boolean stop = false;  // start or stop the counter
    private JTextField tfCount;
    private int count = 1;
 
    /** Constructor to setup the GUI components */
    public UnresponsiveUI() {
        Container cp = this.getContentPane();
        cp.setLayout(new FlowLayout(FlowLayout.CENTER, 10, 10));
        cp.add(new JLabel("Counter"));
        tfCount = new JTextField(count + "", 10);
        tfCount.setEditable(false);
        cp.add(tfCount);

        JButton btnStart = new JButton("Start Counting");
        cp.add(btnStart);
        btnStart.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent evt) {
                stop = false;
                for (int i = 0; i < 100000; ++i) {
                    if (stop) break;  // check if STOP button has been pushed,
                                      //  which changes the stop flag to true
                    tfCount.setText(count + "");
                    ++count;
                }
            }
        });
        JButton btnStop = new JButton("Stop Counting");
        cp.add(btnStop);
        btnStop.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent evt) {
                stop = true;  // set the stop flag
            }
        });

        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setTitle("Counter");
        setSize(300, 120);
        setVisible(true);
    }
 
    /** The entry main method */
    public static void main(String[] args) {
        // Run GUI codes in Event-Dispatching thread for thread safety
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                new UnresponsiveUI();  // Let the constructor do the job
            }
        });
    }
}

运行程序可知:一旦点击START按钮,UI界面就像挂起了一样–计数值并未更新(即显示无刷新),且用户界面没有响应STOP按钮的点击以及任何其他的用户交互。

追踪线程(高级)

从程序追踪(通过Eclipse/NetBeans),我们观察到:

  1. 在主线程中启动Main()方法;
  2. JRE的窗口子系统通过SwingUtilities.invokeLater()启动三个线程: "AWT-Windows"(守护线程), "AWT-Shutdown"和"AWT-EventQueue-0"。"AWT-EventQueue-0"作为事件分发线程(Event-Dispatching Thread (EDT)),负责处理所有事件(如按钮的点击)及界面刷新显示以保证操作GUI和操控GUI组件线程安全。指定构造函数 UnresponsiveUI()在事件分发线程上通过invokeLater()运行,“主”线程在main()方法完成后退出。新线程 "DestroyJavaVM" 随之创建;
  3. 当点击START按钮的时候,actionPerformed()在EDT上运行,EDT现在完全被(计算密集型的)计数循环所占用。换言之,当计数正在运行的时候,EDT处于busy状态并且不能够处理任何事件(如:点击STOP按钮或关闭窗口按钮)及刷新显示-直到计数完成且EDT解除占用。其表现为显示会在计数循环完成之前一直处于冻结状态。

推荐在EDT上通过invokeLater()运行GUI创建代码,因为GUI组件中很多都不能保证线程安全。在单一线程中引导对GUI组件的访问能保证线程安全。假定直接在main()方法(在主线程下)中运行构造函数,如下:

public static void main(String[] args) {
    new UnresponsiveUI();
}

跟踪显示:

  1. 在主线程中启动Main()方法;
  2. 新线程“AWT-Windows”(守护线程)在进入构造函数“new UnresponsiveUI()”(因为“extends JFrame”)时启动;
  3. 在执行“setVisible(true)”之后,另外两个线程"AWT-Shutdown"和"AWT-EventQueue-0"(即EDT)被创建;
  4. 主线程在main()方法完成之后退出,新线程 "DestroyJavaVM" 随之创建;
  5. 此时有四个线程正在运行 - "AWT-Windows", "AWT-Shutdown", "AWT-EventQueue-0 (EDT)" 和 "DestroyJavaVM";
  6. 点击START按钮会在EDT上运行actionPerformed()。

在之前的例子里,EDT通过invokeLater()启动;而在后面的例子中EDT在setVisible()之后启动。

2.2 例2: 带线程而依然无响应UI

不使用EDT,而是新建一个线程来计数,如下:

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

/** Illustrate the Unresponsive UI problem caused by "starved" event-dispatching thread */
public class UnresponsiveUIwThread extends JFrame {
    private boolean stop = false;
    private JTextField tfCount;
    private int count = 1;
 
    /** Constructor to setup the GUI components */
    public UnresponsiveUIwThread() {
        Container cp = getContentPane();
        cp.setLayout(new FlowLayout(FlowLayout.CENTER, 10, 10));
        cp.add(new JLabel("Counter"));
        tfCount = new JTextField(count + "", 10);
        tfCount.setEditable(false);
        cp.add(tfCount);
    
        JButton btnStart = new JButton("Start Counting");
        cp.add(btnStart);
        btnStart.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent evt) {
                stop = false;
                // Create our own Thread to do the counting
                Thread t = new Thread() {
                    @Override
                    public void run() {  // override the run() to specify the running behavior
                        for (int i = 0; i < 100000; ++i) {
                            if (stop) break;
                            tfCount.setText(count + "");
                            ++count;
                        }
                    }
                };
                t.start();  // call back run()
            }
        });
 
        JButton btnStop = new JButton("Stop Counting");
        cp.add(btnStop);
        btnStop.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent evt) {
                stop = true;
            }
        });
 
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setTitle("Counter");
        setSize(300, 120);
        setVisible(true);
    }
 
    /** The entry main method */
    public static void main(String[] args) {
        // Run GUI codes in Event-Dispatching thread for thread safety
        javax.swing.SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                new UnresponsiveUIwThread();  // Let the constructor do the job
            }
        });
    }
}

通过Thread类创建一个新线程,重写 run ()方法来进行计数。创建一个实例,调用实例的start ()会在自身线程上执行run ()方法(创建新线程细节会在后面介绍)。

响应较之前有轻微的改善。但是计数值还是没有显示,而且STOP按钮响应有延迟。(在双核处理器下可能看不到这种差异)

这是因为计数线程本身不会自动将资源控制权交与EDT,EDT便不能更新显示并响应STOP按钮。再者,JVM会根据调度算法强制计数线程放弃对资源的控制, 这会导致在更新显示上的延迟 (尚未确证).

跟踪和线程 (高级)

当点击START按钮时,一个名为Thread-n(n是一个流水号)的线程被创建运行计数循环,此线程不会将资源控制权交与其他线程,尤其是EDT。

而这个程序比之前好一点:显示会更新,且STOP按钮在一些延时之后起作用。

2.3 例3: 带线程响应式UI

对程序作以下修改:调用计数线程的sleep ()方法以使计数线程将资源控制权交与EDT来更新显示并响应STOP按钮。计数线程现在可以按照预期运行。Sleep ()方法也提供必需的延迟。

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
 
/** Resolve the unresponsive UI problem by running the compute-intensive task
    in this own thread, which yields control to the EDT regularly */
public class UnresponsiveUIwThreadSleep extends JFrame {
   private boolean stop = false;
   private JTextField tfCount;
   private int count = 1;
 
   /** Constructor to setup the GUI components */
   public UnresponsiveUIwThreadSleep() {
      Container cp = getContentPane();
      cp.setLayout(new FlowLayout(FlowLayout.CENTER, 10, 10));
      cp.add(new JLabel("Counter"));
      tfCount = new JTextField(count + "", 10);
      tfCount.setEditable(false);
      cp.add(tfCount);
 
      JButton btnStart = new JButton("Start Counting");
      cp.add(btnStart);
      btnStart.addActionListener(new ActionListener() {
         @Override
         public void actionPerformed(ActionEvent evt) {
            stop = false;
            // Create a new Thread to do the counting
            Thread t = new Thread() {
               @Override
               public void run() {  // override the run() for the running behaviors
                  for (int i = 0; i < 100000; ++i) {
                     if (stop) break;
                     tfCount.setText(count + "");
                     ++count;
                     // Suspend this thread via sleep() and yield control to other threads.
                     // Also provide the necessary delay.
                     try {
                        sleep(10);  // milliseconds
                     } catch (InterruptedException ex) {}
                  }
               }
            };
            t.start();  // call back run()
         }
      });
 
      JButton btnStop = new JButton("Stop Counting");
      cp.add(btnStop);
      btnStop.addActionListener(new ActionListener() {
         @Override
         public void actionPerformed(ActionEvent evt) {
            stop = true;
         }
      });
 
      setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
      setTitle("Counter");
      setSize(300, 120);
      setVisible(true);
   }
 
   /** The entry main method */
   public static void main(String[] args) {
      // Run GUI codes in Event-Dispatching thread for thread safety
      javax.swing.SwingUtilities.invokeLater(new Runnable() {
         @Override
         public void run() {
            new UnresponsiveUIwThreadSleep();  // Let the constructor do the job
         }
      });
   }
}

sleep()方法暂停当前线程并将其置于等待特定时间的状态,另一个线程开始执行(在单CPU环境中)。调用线程interrupt()方法能中断sleep(),但是会引发InterruptedException 。

在这个例子中,创建的计数线程("Thread-n")在每次计数之后通过sleep(10)将资源控制权交与其他线程,EDT就可以更新界面显示并在每次计数之后响应“STOP”按钮。

2.4 例4: SwingWorker

JDK 1.6 提供了一个新的javax.swing.SwingWorker类,它能用来在后台线程中执行计算密集型任务 并将最后结果或中间结果传递给在EDT上运行的方法。关于SwingWorker 的讨论会在后面的章节中进行。

翻译自:
https://www3.ntu.edu.sg/home/ehchua/programming/java/j5e_multithreading.html

Java编程之多线程&并发编程(中)
Java编程之多线程&并发编程(下)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,864评论 6 494
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,175评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,401评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,170评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,276评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,364评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,401评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,179评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,604评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,902评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,070评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,751评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,380评论 3 319
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,077评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,312评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,924评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,957评论 2 351

推荐阅读更多精彩内容

  • 前言:虽然自己平时都在用多线程,也能完成基本的工作需求,但总觉得,还是对线程没有一个系统的概念,所以,查阅了一些资...
    justCode_阅读 705评论 0 9
  • 面向对象主要针对面向过程。 面向过程的基本单元是函数。 什么是对象:EVERYTHING IS OBJECT(万物...
    sinpi阅读 1,048评论 0 4
  • 下面是Java线程相关的热门面试题,你可以用它来好好准备面试。 1) 什么是线程? 线程是操作系统能够进行运算调度...
    冰箱哥哥阅读 519评论 0 2
  • 在人们的日常生活中,有这样一个的规律。好比我们买了50块钱的票去看电影,如果我们在路上丢了50块钱,我们或许会骂骂...
    发疯的然然阅读 164评论 0 0
  • 有些事情,需要亲身经历才能明白;可现实是,并非所有的事情都需要亲身去经历,人生苦短,没有那么多时间去尝试! 有个事...
    宁小南阅读 276评论 0 0