一步一步掌握java的线程机制(一)----创建线程
现在将1年前写的有关线程的文章再重新看了一遍,发现过去的自己还是照本宣科,毕竟是刚学java的人,就想将java的精髓之一---线程进制掌握到手,还是有点难度。等到自己已经是编程一年级生了,还是无法将线程这个高级的概念完全贯通,所以,现在趁着自己还在校,尽量的掌握多点有关线程机制的知识。
我们以一个简单的例子开始下手:
public class SwingTypeTester extends JFrame implements CharacterSource{
protected RandomCharacterGenerator producer;
private CharacterDisplayCanvas displayCanvas;
private CharacterDisplayCanvas feedbackCanvas;
private JButton quitButton;
private JButton startButton;
private CharacterEventHandler handler; public SwingTypeTester() {
initComponents();
} private void initComponents() {
handler = new CharacterEventHandler();
displayCanvas = new CharacterDisplayCanvas();
feedbackCanvas = new CharacterDisplayCanvas();
quitButton = new JButton();
startButton = new JButton();
add(displayCanvas, BorderLayout.NORTH);
add(feedbackCanvas, BorderLayout.CENTER);
JPanel p = new JPanel();
startButton.setLabel("Start");
quitButton.setLabel("Quit");
p.add(startButton);
p.add(quitButton); add(p, BorderLayout.SOUTH);
addWindowListener(new WindowAdapter(){
public void windowClosing(WindowEvent evt){
quit();
}
}); feedbackCanvas.addKeyListener(new KeyAdapter(){
public void keyPressed(KeyEvent ke){
char c = ke.getKeyChar();
if(c != KeyEvent.CHAR_UNDEFINED){
newCharacter((int)c);
}
}
}); startButton.addActionListener(new ActionListener() { @Override
public void actionPerformed(ActionEvent arg0) {
producer = new RandomCharacterGenerator();
displayCanvas.setCharacterSource(producer);
producer.start();
startButton.setEnabled(false);
feedbackCanvas.setEnabled(true);
feedbackCanvas.requestFocus();
}
}); quitButton.addActionListener(new ActionListener() { @Override
public void actionPerformed(ActionEvent e) {
quit();
}
});
pack();
} private void quit(){
System.exit(0);
} public void addCharacterListener(CharacterListener cl){
handler.addCharacterListener(cl);
} public void removeCharacterListener(CharacterListener cl){
handler.removeCharacterListener(cl);
} public void newCharacter(int c){
handler.fireNewCharacter(this, c);
} public void nextCharacter(){
throw new IllegalStateException("We don't produce on demand");
} public static void main(String[] args){
new SwingTypeTester().show();
}
}
这是一个java的Swing小例子,就是每隔一段时间就会显示一个随机的字母或者数字。具体的源码我会放在后面,现在只是对其中涉及到线程的部分进行重点讲解。
使用到线程的地方就只有那个显示下一个字母或者数字的功能,它需要在前一个字母或者数字在显示一段时间后显示出来,并且它的产生是不断进行的,除非我们按下停止按钮。这是需要一个线程不断在运行的:
public class RandomCharacterGenerator extends Thread implements CharacterSource {
static char[] chars;
static String charArray = "abcdefghijklmnopqrstuvwxyz0123456789";
static {
chars = charArray.toCharArray();
} Random random;
CharacterEventHandler handler; public RandomCharacterGenerator() {
random = new Random();
handler = new CharacterEventHandler();
} public int getPauseTime() {
return (int) (Math.max(1000, 5000 * random.nextDouble()));
} @Override
public void addCharacterListener(CharacterListener cl) {
handler.addCharacterListener(cl);
} @Override
public void removeCharacterListener(CharacterListener cl) {
handler.removeCharacterListener(cl);
} @Override
public void nextCharacter() {
handler.fireNewCharacter(this,
(int) chars[random.nextInt(chars.length)]);
} public void run() {
for (;;) {
nextCharacter();
try {
Thread.sleep(getPauseTime());
} catch (InterruptedException ie) {
return;
}
}
}
}
虽然方法多,但是这个类只有一个方法run()方法是值得我们注意的。
开启线程的方式是非常简单的,只要声明一个Thread,然后在适当的时候start就行。创建Thread的方式可以像是这样,创建一个Thread的子类,然后实现它的run()方法,在run()方法中进行该线程的主要工作。当然,我们也可以在需要线程的地方才创建一个Thread,但是这里的情况就是我们的Thread类还需要实现其他接口(当然,这个设计并不好,但我们会以这个例子的逐步完善工作,将一些线程的基本知识融会进去)。
要想明白线程机制,我们还是得从一些基本内容的概念下手。感谢一年前的我,虽然文章写得不咋样,但是作为一个勤奋的记录员,还是将一些基本知识都记录下来,省得我去找。
线程和进程是两个完全不同的概念,进程是运行在自己的地址空间内的自包容的程序,而线程是在进程中的一个单一的顺序控制流,因此,单个进程可以拥有多个线程。
还有一个抽象的概念,就是任务和线程的区别。线程似乎是进程内的一个任务,但实际上在概念上两者并不一样。准确点讲,任务是由执行线程来驱动的,而任务是附着在线程上的。
现在正式讲讲线程的创建。
正如我们前面讲的,任务是由执行线程驱动的,没有附着任务的线程根本就不能说是线程,所以我们在创建线程的时候,将任务附着到线程上。所谓的任务,对应的就是Runnable,我们要在这个类中编写相应的run()方法来描述这个任务所要执行的命令,接着就是将任务附着到线程上。像是这样:
Thread thread = new Thread(new Runnable(){
@Override
public void run(){
...
}
});
接着我们只要通过start()启动该Thread就行。
如果我们在main()方法中启动线程,我们就会发现,就算线程还没有执行完毕,剩下的代码还是会被运行。这是因为我们的main()方法也是一个线程,我们可以在main线程中启动一个线程。所以,任何线程都可以开启另一个线程。
这样的话,问题也就来了:如果一个程序中开启了多个线程,那么,它们的执行顺序是怎样的,毕竟程序的内存空间是有限的,不可能允许无限多个线程同时进行。事实就是,它们是交替进行的,而且还是我们无法控制的,是由线程调度器控制的,而且每次执行的顺序都是不一样的!
这就是多线程最大的问题,如果我们的程序设计不好,在这样的情况下,就很容易出现问题,而且是我们所无法把握的问题。
另一种方式就是上面使用的:创建一个Thread的子类,然后实现run()方法,接着同样是通过start()来开启它。
这两种方式到底应该采取哪种好呢?如果不想类的管理太麻烦,建议还是采取第一种方式,而且这也是我们在大部分的情况下所采用的,它充分使用了java的匿名内部类,但如果还想我们的Thread能够体现出其他行为而不单单只是个执行任务的线程,那么可以采取第二种方式,这样我们可以通过实现接口的方式让Thread具有更多的功能,但是必须注意,Thread的子类只能承载一个任务,但是第一种方式却可以非常自由的根据需要创建相应的任务。
除了上面两种方法,java还提供了第三种方法:Executor(执行器)。
Executor会在客户端和任务之间提供一个间接层,由这个间接层来执行任务,并且允许管理异步任务的执行,而无需通过显式的管理线程的生命周期。
ExecutorService exec = Executors.newCachedThreadPool();
exec.executor(new RunnableClass);
其中,CachedThreadPool是一种线程池。线程池在多线程处理技术中是一个非常重要的概念,它会将任务添加到队列中,然后在创建线程后自动启动这些任务。线程池的线程都是后台线程,每个线程都是用默认的堆栈大小,以默认的优先级运行,并处于多线程单元中。线程池中的线程数目是有一个最大值,但这并不意味着只能运行这样多的线程,它的真正意思是同时能够运行的最大线程数目,所以可以等待其他线程运行完毕后再启动。
线程池都有一个线程池管理器,用于创建和管理线程池,还有一个工作线程,也就是线程池中的线程。我们必须提供给线程池中的工作线程一个任务,这些任务都是实现了一个任务接口,也就是Runnable。线程池还有一个重要的组成:任务队列,用于存放没有处理的任务,这就是一种缓冲机制。
通过线程池的介绍,我们可以知道,使用到线程池的情况就是这样:需要大量的线程来完成任务,并且完成任务的时间比较短,就像是我们现在的服务器,同时间接受多个请求并且处理这些请求。
java除了上面的CachedThreadPool,还有另一种线程池:FixedThreadPool。CachedThreadPool会在执行过程中创建与所需数量相同的线程,然后在它回收旧线程的时候停止创建新的线程,也就是说,它每次都要保证同时运行的线程的数量不能超过所规定的最大数目。而FixedThreadPool是一次性的预先分配所要执行的线程,像是这样:
ExecutorService exec = Executors.newFixedThreadPool(5);
就是无论要分配的线程的数目是多少,都是运行5个线程。这样的好处是非常明显的,就是用于限制线程的数目。CachedThreadPool是按需分配线程,直到有的线程被回收,也就是出现空闲的时候才会停止创建新的线程,这个过程对于内存来说,代价是非常高昂的,因为我们不知道实际上需要创建的线程数量是多少,只会一直不断创建新线程。
看上去似乎FixedThreadPool比起CachedThreadPool更加好用,但实际上使用更多的是CachedThreadPool,因为一般情况下,无论是什么线程池,现有线程都有可能会被自动复用,而CachedThreadPool在线程结束的时候就会停止创建新的线程,也就是说,它能确保结束掉的线程的确是结束掉了,不会被重新启动,而FixedThreadPool无法保证这点。
接下来我们可以看看使用上面两种线程池的简单例子:
public void main(String[] args){
ExecutorService cachedExec = Executors.newCachedThreadPool();
for(int i = 0; i < 5; i++){
cachedExec.execute(new RunnableClass);
}
cachedExec.shutdown(); ExecutorService fixedExec = Executors.newFixedThreadPool(3);
for(int i = 0; i < 5; i++){
fixedExec.execute(new RunnableClass);
}
fixedExec.shutdown();
}
CachedThreadPool会不断创建线程直到有线程空闲下来为止,而FixedThreadPool会用3个线程来执行5个任务。
在java中,还有一种执行线程的模式:SingleThreadExecutor。顾名思义,该执行器只有一个线程。它就相当于数量为1的FixedThreadPool,如果我们向它提交多个任务,它们就会按照提交的顺序排队,直到上一个任务执行完毕,因为它们就只有一个线程可以运行。这种方式是为了防止竞争,因为任何时刻都只有一个任务在运行,从而不需要同步共享资源。
竞争是线程机制中一个非常重要的现象,有关于它的解决贯穿了整个线程机制的发展,而且可怕的是,就算是合理的解决方案,也无法保证我们已经完全避免了这个问题,因为无法预知的错误仍然存在于不远的将来。
一步一步掌握java的线程机制(一)----创建线程的更多相关文章
- Java并发编程:如何创建线程?
Java并发编程:如何创建线程? 在前面一篇文章中已经讲述了在进程和线程的由来,今天就来讲一下在Java中如何创建线程,让线程去执行一个子任务.下面先讲述一下Java中的应用程序和进程相关的概念知识, ...
- 2、Java并发编程:如何创建线程
Java并发编程:如何创建线程? 在前面一篇文章中已经讲述了在进程和线程的由来,今天就来讲一下在Java中如何创建线程,让线程去执行一个子任务.下面先讲述一下Java中的应用程序和进程相关的概念知识, ...
- 【转】Java并发编程:如何创建线程?
一.Java中关于应用程序和进程相关的概念 在Java中,一个应用程序对应着一个JVM实例(也有地方称为JVM进程),一般来说名字默认是java.exe或者javaw.exe(windows下可以通过 ...
- 【Java 线程的深入研究1】Java 提供了三种创建线程的方法
Java 提供了三种创建线程的方法: 通过实现 Runnable 接口: 通过继承 Thread 类本身: 通过 Callable 和 Future 创建线程. 1.通过实现 Runnable 接口来 ...
- 【java并发】传统线程技术中创建线程的两种方式
传统的线程技术中有两种创建线程的方式:一是继承Thread类,并重写run()方法:二是实现Runnable接口,覆盖接口中的run()方法,并把Runnable接口的实现扔给Thread.这两种方式 ...
- java多线程(一)创建线程的四种方式
1. 什么是并发与并行 要想学习多线程,必须先理解什么是并发与并行 并行:指两个或多个事件在同一时刻发生(同时发生). 并发:指两个或多个事件在同一个时间段内发生. 2. 什么是进程.线程 进 ...
- Java并发基础01. 传统线程技术中创建线程的两种方式
传统的线程技术中有两种创建线程的方式:一是继承Thread类,并重写run()方法:二是实现Runnable接口,覆盖接口中的run()方法,并把Runnable接口的实现扔给Thread.这两种方式 ...
- java并发编程:如何创建线程
原文:http://www.cnblogs.com/dolphin0520/p/3913517.html 一.Java中关于应用程序和进程相关的概念 在Java中,一个应用程序对应着一个JVM实例(也 ...
- Java多线程学习总结--线程概述及创建线程的方式(1)
在Java开发中,多线程是很常用的,用得好的话,可以提高程序的性能. 首先先来看一下线程和进程的区别: 1,一个应用程序就是一个进程,一个进程中有一个或多个线程.一个进程至少要有一个主线程.线程可以看 ...
随机推荐
- myeclipse集成jdk、tomcat8、maven、svn
今天一个同学要回家了.回家之前叫我帮他配置一下开发环境.然后在家里面自己研究一下.敲下代码. 帮他配置好之后自己回来把这个过程写下来.别让自己把这个东西给忘了. myeclipse安装 myeclip ...
- 20160205.CCPP体系具体解释(0015天)
程序片段(01):01.杨辉三角.c 内容概要:杨辉三角 #include <stdio.h> #include <stdlib.h> #define N 10 //01.杨辉 ...
- 【BLE】CC2541之加入自己定义任务
本篇博文最后改动时间:2017年01月06日,11:06. 一.简单介绍 本文介绍怎样在SimpleBLEPeripheralproject中.加入一个香瓜任务. (香瓜任务与project原有任务相 ...
- 【树莓派】【网摘】树莓派与XBMC及Kodi、LibreELEC插件(三)
之前的相关文章参考: [树莓派]树莓派与XBMC及Kodi.LibreELEC插件(一) [树莓派]树莓派与XBMC及Kodi.LibreELEC插件(二) [树莓派]树莓派与XBMC及Kodi.Li ...
- 【树莓派】树莓派与XBMC及Kodi、LibreELEC插件(二)
之前的相关文章参考: [树莓派]树莓派与XBMC及Kodi.LibreELEC插件(一) [树莓派]树莓派与XBMC及Kodi.LibreELEC插件(二) [树莓派]树莓派与XBMC及Kodi.Li ...
- Python-统计svn代码总行数
1 #!/bin/bash/python 2 # -*-coding:utf-8-*- 3 #svn统计url代码行数脚本,过滤空行,不过滤注释. 4 5 import subprocess,os,s ...
- 2019微信公开课Pro微信之夜内容笔记总结
2019微信公开课Pro 微信之夜内容笔记总结 小程序入口 我的小程序 任务栏入口 线下扫码 搜索小程序 附近小程序升级 用户留存问题 小程序成长 关注用户需求 性能监控 广告主&& ...
- Git如何获得两个版本间所有变更的文件列表
https://segmentfault.com/q/1010000000133613 git diff --name-status HEAD~2 HEAD~3
- Markdown中实时显示数学公式的方法
Markdown中实时显示数学公式的方法 Markdown非常好用,但是对于数学公式的实时显示有一些缺陷,如何解决这一问题呢? 一.在线LaTex编辑 点击在线LaTeX编辑方式 在对话框中输入数学公 ...
- Linux内存分配机制之伙伴系统和SLAB
转载请注明原文地址:http://www.cnblogs.com/ygj0930/p/6539590.html 内核内存管理的一项重要工作就是如何在频繁申请释放内存的情况下,避免碎片的产生.这就要求 ...