知识点

相关文章

更多

最近更新

更多

一步一步掌握java的线程机制(一)----创建线程

2019-03-12 17:18|来源: 网络

      现在将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,如果我们向它提交多个任务,它们就会按照提交的顺序排队,直到上一个任务执行完毕,因为它们就只有一个线程可以运行。这种方式是为了防止竞争,因为任何时刻都只有一个任务在运行,从而不需要同步共享资源。

       竞争是线程机制中一个非常重要的现象,有关于它的解决贯穿了整个线程机制的发展,而且可怕的是,就算是合理的解决方案,也无法保证我们已经完全避免了这个问题,因为无法预知的错误仍然存在于不远的将来。

    

  

      

     

     

相关问答

更多
  • 兄弟连JavaEE战狼班: 第一阶段:Java语言基础 ★ Java语言基础 1、面向对象思维JAVASE 2、(类加载机制与反射,annotation,泛型,网络编程,多线程,IO,异常处理,常用API,面向对象,JAVA编程基础) 3、Java8新特性 第二阶段:数据库 ★ 数据库 1、Oracle(SQL语句、SQL语句原理、SQL语句优化、表、视图 2、序列、索引、Oracle数据字典、Oracle 数据库PL/SQL开发 3、数据库设计原则、 MySQL 、 JDBC 第三阶段:Web基础 ★ W ...
  • 其实Java学的好坏,一方面是理论和语法,一方面就是熟悉API,Java就是因为有一个强大的类库,所以你光有编程思想,光会了语法,还不行,还得熟悉这些Sun提供的类,但是挨个看API一个个方法读也不是办法,而且也不用什么都记,那么多类挨个记,记得住么?不能这么学,分清主次,一些重要的类,常用的类,常用的方法有些记住,比如说System.out.print要是还去查文档那就太过份了,有些熟悉就行,剩下的用到就查文档,我们做Java的,这辈子只要是还干Java,这个API文档就离不开了,什么方法不明白马上去查, ...
  • 进入这个网址 http://www.verycd.com/topics/2767912/,把韩顺平老师的视频教程下下来学,很不错
  • 文件没保存吧,难道你都是手动敲的?eclipse有自动填充的
  • 先画身体,壮壮的! 还有‘恐龙'腿!要画粗一点哦。 紧跟着是尾巴!长一点。 添上眼睛和鼻子! 然后就是身上很有特色的恐龙背刺啦! 最后画上一棵树!不知它吃不吃树叶!
  • MySQL也是很好安装的,就和APACHE的安装一样。但是大部分人包括我在内,都在配置的最后一步的第三小项目Start Service出现了错误。这十分令人懊恼。由于注册表的原因,我因此重装了系统,XP变身成为2003. MySQL之所以会出现这个错误,原因再简单不过——由于注册表里之前安装过的MySQL信息无法彻底删除导致新的MySQL不能启动。目前的解决方法似乎是将注册表内的旧信息删除,但是由于注册表的目录庞大,若要找到了再删除十分麻烦而且不一定能根除。我试过这一方法,却依然不能启动。最终只有重启了。不 ...
  • facebook是国外的一个社交网络及微博客服务的网站,目前在中国很多人都可能打不开这个网站,平时要上话要进行设置之后才可以上。 1、首先打开facebook官网进行注册,可能是英文,看不懂选择右上角设置语言一下。(注意:如果你打不开facebook的网站你需要进行V/P/N设置或者用fan墙软件才能够打开) 2、然后选择简体中文,就会翻译成中文了。 3、点注册 4、填上你名字、邮箱、密码和用户名。 5、然后点击选择创建我的账号。 6、登陆你的注册facebook的邮箱确认验证你的账号。 7、确认过后就注册 ...
  • 有一定的数据库基础吗?没有的话,建议从SQL语句学起。比较好的教材是Oracle OCP认证的《SQL and PL/SQL》。学习SQL的时候,尽可能坚持使用Oracle自带的工具:SQLPLUS。  有了一定的SQL基础后,就要尽可能的了解Oracle的体系结构,这就涉及到了Oracle管理的内容了。《Oracle10g OCP认证手册》这本书不错。 不过,如果是初学者的话,不建议自己去摸索,因为这样往往会如盲人摸象,不仅会事倍功半,而且会有一些错误的概念。你可以去CUUG报个DBA就业培训班进行学习, ...
  • 将这些内容放入Tomcat服务器中可能更容易 - 并且跨平台。 查看Quartz调度程序 ,它完成了cron所做的事情,更容易集成到Web应用程序中,并允许您调用用Java编写的作业,而无需任何复杂的管道。 作为一个额外的优势,这些作业可以像Tomcat中的任何其他Web应用程序一样进行部署。 It's probably easier - and cross platform - to just put that stuff inside your Tomcat server. Check out the ...
  • 好吧,如果你打印: System.out.println ('S'-'c'); 你得到-16 String的compareTo比较两个String的一个字符。 第一对不相等的字符(在您的情况下,“String”的“S”和“compareTo”的“c”)确定结果。 由于词典上的'S'在'c'之前,比较返回一个负值,这意味着“String”应该在“compareTo”之前。 在第二次比较中,所有字符对都相等,因此compareTo返回0。 Well, if you print : System.out.pri ...