天天看点

JAVA学习笔记——多线程

1.进程

  进程是正在运行的程序,是系统进行资源分配和调用的独立单位,每一个进程都有他自己的内存空间和系统资源。

  多进程:单进程的计算机只能一次做一件事,而多进程就可以在同一个时间段内执行多个任务,提高CPU的使用率。

  问:一边玩游戏一边听音乐是同时进行的吗?

  并不是,因为单CPU在某个时间点上只能进行一个任务,只是CPU在做着程序间的高效切换而让我们感觉是同时进行的。

2.线程

  在一个进程内又可以执行多个任务。而这每一个任务就可以看成是一个线程。

  线程是程序的执行单元、执行路径,是程序使用CPU的最基本单位。

  

  单线程:程序只有一条执行路径

  多线程:程序有多条执行路径

 

  多线程的意义?

  答:多线程的存在,不是为了提高程序的执行速度,其实是为了提高应用程序的使用率。

   程序的执行其实都是在抢CPU的资源,CPU的执行权。

   多个线程是在抢这个资源,而其中的某一个进程如果执行路径比较多,就会有更高的几率抢到CPU的执行权。

   我们不能保证哪一个线程能够在哪个时刻抢到,所以线程的执行有随机性。

  并行:前者是逻辑上同时发生,指在某一个时间段内同时运行多个程序

  并发:后者是物理上同时发生,指在某一个时间点同时运行多个程序(高并发问题)

3.Java程序运行原理:

  由Java命令启动JVM,JVM启动就相当于启动了一个进程。

  接着该进程创建了一个主线程去调用main方法。

  问:JVM虚拟机的启动是单线程还是多线程的?

    多线程的

    原因是例如垃圾回收线程也要先启动,否则很容易出现内存溢出。所以是多线程的。

4.如何实现多线程程序

  由于线程依赖于进程而存在,所以要实现多线程必须先创建一个进程。

  而进程是由系统创建的,所以必须去调用系统功能创建一个进程。

  而Java是无法直接调用系统功能的,所以需要去调用底层C/C++写好的程序来实现多线程。因此Java提供了一些类供我们实现多线程。

5.Thread类:通过查看API,可以发现有两种实现多线程方式(实际上三种)

 (1)方式1:继承Thread类

  步骤:

    A:自定义MyThread类继承Thread类

    B:MyThread类重写run()方法

      不是类中的所有代码都需要被线程执行,为了区分哪些代码能够被线程执行,Java提供了Thread类中的run()用来包含那些被线程执行的代码

    C:创建对象

    D:启动线程

  注意:

    A:run()方法直接调用的话和普通方法一样,都是单线程的,无论调用几次。

    B:为了有多线程效果,应该是调用start()方法开启线程。start()方法的作用有两个,1是启动线程,2是调用run()方法。

    C:一个线程对象的start()方法只能同时调用一次,要想让run()方法里的代码多线程运行,需要创建多个该线程对象并调用她们的start()方法。

    面试题:run()方法和start()方法有什么区别?

      run():仅仅是封装被线程执行的代码,直接调用是普通方法

      start():首先启动线程,再由Jvm去调用run()方法

  <1>如何获取线程对象的名称?

   public final String getName():获取线程的名称

  <2>如何设置线程对象的名称?

   A(set方法):public final void setName(String name):设置线程的名称

   B(构造方法):public MyThread(String name) {

           super(name); //Thread类有该方法

           } //重写有参构造方法

  <3>针对不是Thread类的子类中如何获取线程对象呢?

   public static Thread currentThread():返回当前正在执行的线程对象

     用法:Thread.currentThread().getName()

  <4>线程调度

    线程有两种调度模型:

    分时调度模型 所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片

    抢占式调度模型 优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些。

    Java使用的是抢占式调度模型。

   设置优先级方法:

    public final int getPriority() :获取线程的优先级数

    public final void setPriority(int newPriority):设置线程的优先级数

     线程默认优先级是5(NORMAL_PRIORITY)

     线程优先级的范围是1-10(MIN_PRIORITY ~ MAX_PRIORITY)

     注意:线程优先级仅仅代表线程获取CPU时间片的几率高,要在次数比较多,多运行几次的情况下才能看出效果。

     IllegalArgumentException:非法参数异常,表明向方法传递了一个不合法或者不正确的参数

  <5>线程控制

   线程休眠:public static void sleep(long millis):在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。  

           Thread.sleep(1000); 休眠1秒

   线程加入:public final void join():等待该线程终止,这时候该线程其他线程对象暂停运行,等该线程结束再运行

   线程礼让:public static void yield():暂停当前正在执行的线程对象,并执行其他线程。 作用是让多个线程执行更加和谐,但是不能保证每个线程一人一次。

   守护线程:public final void setDaemon(boolean on):将线程标记为守护线程或者用户线程。当正在运行的线程都是守护线程时,Java虚拟机自动退出。该方法必须在线程启动前调用。

   中断线程:public final void stop():让线程停止,过时了,但是还可以用

        public void interrupt():中断线程,把线程状态终止,并抛出一个InterruptedException异常

  <6>线程的生命周期(4个状态):

    新建:创建线程对象

    就绪:有执行资格,没有执行权

    运行:有执行资格,有执行权

       阻塞:由于一些操作让线程处于该状态,没有执行资格,没有执行权

          而另一些操作可以激活线程,此时它处于就绪状态

    死亡:线程对象变成垃圾,等待被回收。

     

JAVA学习笔记——多线程

 (2)方式二:实现Runnable接口

    步骤:

     A:自定义MyRunnable类实现接口Runnable接口

     B:重写run()方法

     C:创建MyRunnable类的对象

     D:创建Thread类的对象,并把C步骤的对象作为构造参数传递

面试题:为什么有了第一种方式还要来第二种方式?

  A:可以避免java单继承带来的局限性

  B:第二种方式适合多个相同程序的代码

5.线程安全性

  只要线程里的操作不是原子性的(不可拆分的),那么在对线程进行延迟操作(sleep())时,就有可能存在安全性问题。

  <1>解决线程安全问题的基本思想

    首先想为什么出现问题?(也是我们判断是否有问题的标准)

      A:是否是多线程环境

      B:是否有共享数据

      C:是否有多条语句操作共享数据

    如何解决多线程安全问题呢?

      基本思想:让程序没有安全问题的环境。

    怎么实现呢?

      把多个语句操作共享数据的代码给锁起来,让任意时刻只能有一个线程执行即可。(同步)

  

  <2>同步代码块

    A:格式:

      synchronized(对象){

       需要同步的代码;

      }

    B:同步可以解决安全问题的根本原因就在那个对象上。该对象如同锁的功能。每次只能有一个进程同时访问该同步代码块。

    C:同步代码块的锁对象可以是 任意对象 

    D:同步方法的格式以及锁对象问题

       同步方法是指把同步关键字加在方法上 private synchronized void method(){}

       同步方法的锁是什么呢? —— this ,因为每个方法都自带一个隐藏对象this

    E:静态方法及锁对象问题

       静态方法的锁对象是什么呢?———类的字节码文件 类.class

  <3>同步的特点

    同步的前提:存在多个线程

      解决问题的时候要注意:多个线程使用的是同一个锁对象

    同步的好处:同步的出现解决了多线程的安全问题。

    同步的弊端:当线程相当多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率。

  <4>线程安全类(同步)

    StringBuffer sb = new StringBuffer() ;

    Vector<String> v = new Vector<String>() ; //我们并不用它

    Hashtable<String, String> h = new Hashtable<String, String>() ;

    //如何创建安全的List?

    List<String> list1 = new ArrayList<String>() ; //线程不安全

    List<String> list2 = Collection.synchronizedList(new ArrayList<String>()) ; //线程安全

6.Lock锁

  虽然已经可以理解同步代码块和同步方法的锁对象的问题,但是我们并没有直接看到在哪边加上了锁,在哪里释放了锁。

  为了更清晰的表达如何加锁和释放锁,JDK5提供了一个新的锁对象Lock。

  加锁:void lock():获取锁

  解锁:void lock():释放锁

  

7.死锁

  (1)同步弊端

    A:效率低

    B:如果出现了同步嵌套,就容易产生死锁问题

  (2)死锁问题及其代码

    是指两个或者两个以上的线程在执行的过程中,因争夺资源产生的一种互相等待现象

8.线程间的通信:不同种类的线程针对同一个资源的操作

  当不同种类的线程针对同一个资源进行操作的时候容易出现线程安全问题

  解决方案:

    A:不同种类的线程都要加锁

    B:不同种类的线程加的锁要是同一把(对象是同一个)

    

JAVA学习笔记——多线程

    等待唤醒:

     Object类中提供了三种方法:(设置一个flag来判断资源状态)

       wait():等待,线程立即释放锁

       notify():唤醒单个线程,并不代表可以立马执行,还得抢CPU资源

       notify():唤醒所有线程

     为什么这些方法不定义在Thread中呢?

       因为这些方法的调用必须通过锁对象调用,而我们刚才试用的锁对象是任意锁对象

       所以这些方法必须定义在Object类里。

9.线程组:把多个线程组合到一起,它可以对一批线程进行分组管理,Java允许程序直接对线程组进行控制。

  线程默认的线程组是main

  构造方法:

    ThreadGroup(String name) :构造一个新线程组。

  方法:

     String getName() :返回此线程组的名称。

     void setDaemon(boolean daemon) :设置此线程组的所有线程为守护线程(后台线程)

     void destroy() :销毁此线程组及其所有子组。

  

10.线程池

  程序启动一个新线程成本是比较高的,因为它涉及到要与操作系统进行交互。而使用线程池可以很好的提高性能,尤其是当程序中要创建大量生存期很短的线程时,更应该考虑使用线程池。

  线程池里的每一个线程代码结束后,并不会死亡,而是再次回到线程池中成为空闲状态,等待下一个对象来使用。

  在JDK5之前,我们必须手动实现自己的线程池,从JDK5开始,Java内置支持线程池

  JDK5新增了一个Executors工厂类来产生线程池,有如下几个方法

    public static ExecutorService newCachedThreadPool()

    public static ExecutorService newFixedThreadPool(int nThreads)

    public static ExecutorService newSingleThreadExecutor()

  这些方法的返回值是ExecutorService对象,该对象表示一个线程池,可以执行Runnable对象或者Callable对象代表的线程。它提供了如下方法

  Future< T> submit(Runnable task)

  < T> Future< T> submit(Callable< T> task)

  

  (1)如何实现线程池?

   A:创建一个线程池对象,控制要创建几个线程对象

   public static ExecutorService newFixedThreadPool(int nThreads):nThreads表示几个线程

   B:这种线程池的线程可以执行Runnable对象或者Callable对象代表的线程

     即 做一个类实现Runnable接口

   C:调用如下方法,添加线程到线程池

   Future< T> submit(Runnable task)

   D:结束线程池(如果不结束线程池,线程池里的线程只是闲置,但会一直存在)

    void shutdown() : 启动一次顺序关闭,执行以前提交的任务,但不接受新任务。

//创建一个线程池对象
        ExecutorService pool = Executors.newFixedThreadPool() ;

        //调用submit()方法
        pool.submit(new MyRunnable()) ;
        pool.submit(new MyRunnable()) ;

        //结束线程
        pool.shutdown();
           

11.方式3:Callable方式

  带泛型返回值的多线程方式,但是它仅限于线程池存在。

  步骤和刚才演示线程池执行Runnable对象的差不多。

  好处:

    A:可以有返回值

    B:可以抛出异常

  弊端:

    A:代码比较复杂,所以一般不用

   submit()方法返回的Future< T> 这个中括号里面的T是返回值的类型,该返回值可通过Future对象调用submit()方法来得到。

   Future< Integer> f1 = pool.submit(new MyCallable(100)) ;

   Integer i = f1.get() ;

12.匿名内部类方式使用多线程,如下:

/*
         * 继承Thread类方式
         */
        new Thread() {
            public void run() {
                for (int i = ; i < ; i++) {
                    System.out.println(Thread.currentThread().getName() + ":"
                            + i);
                }
            };
        }.start();

        /*
         * 实现Runnable接口
         */
        new Thread(new Runnable() {

            public void run() {
                for (int i = ; i < ; i++) {
                    System.out.println(Thread.currentThread().getName() + ":"
                            + i);
                }
            }
        }) {
        }.start();

        /*
         * 面试题:这种情况下会报错吗?不会的话是运行谁的start()方法?
         */
        new Thread(new Runnable() {

            public void run() {
                for (int i = ; i < ; i++) {
                    System.out.println(Thread.currentThread().getName() + ":"
                            + i);
                }
            }
        }) {
            public void run() {
                for (int i = ; i < ; i++) {
                    System.out.println(Thread.currentThread().getName() + ":"
                            + i);
                }
            };
        }.start();
           

13.定时器

  依赖Timer和TimerTask这两个类:

  Timer:定时

    public Timer():创建一个新计时器。相关的线程不 作为守护程序运行。

    public void schedule(TimerTask task , long delay):安排在指定延迟后执行指定的任务。(delay作为毫秒值,1000是1秒,即1秒后执行task任务)

    public void schedule(TimerTask task, long delay, long period):安排指定的任务从指定的延迟后开始进行重复的固定延迟执行。(第一次执行在delay时候执行任务task,然后在每隔period后执行一次task)

    public void cancel():终止此计时器,丢弃所有当前已安排的任务。

  TimerTask:任务

14.多线程常见面试题

  (1)多线程有几种实现方案?分别是哪几种?

    两种。

    继承Thread类

    实现Runnable接口

    扩展一种:实现Callable接口。这个得和线程池结合。

   (2):同步有几种方式,分别是什么?

    两种。

    同步代码块

    同步方法

    

   (3):启动一个线程是run()还是start()?它们的区别?

     run():封装了被线程执行的代码,直接调用仅仅是普通方法的调用

     start():启动线程,并由JVM自动调用run()方法

   (4):sleep()和wait()方法的区别

     sleep():必须指定时间;不释放锁。

     wait():可以不指定时间,也可以指定时间;释放锁。

   (5):为什么wait(),notify(),notifyAll()等方法都定义在Object类中

     因为这些方法的调用是依赖于锁对象的,而同步代码块的锁对象是任意锁。

    而Object代码任意的对象,所以,定义在这里面。

   (6):线程的生命周期图

     新建 – 就绪 – 运行 – 死亡

     新建 – 就绪 – 运行 – 阻塞 – 就绪 – 运行 – 死亡

     建议:画图解释。

继续阅读