zoukankan      html  css  js  c++  java
  • 线程(Thread)和异常

    线程Thread

    实现多线程有两种方式:

      1、继承Thread类(本质也是实现Runnable接口的一个实例)

      Thread类源码

    public class Thread implements Runnable {}

     

    定义一个线程

    public class MyThreadextends Thread{
        public void run(){     //重写run方法
        }
    }
    MyThread thread1 = new MyThread();
    thread1.start();

     

      启动线程唯一的方法就是通过Thread类的start()实例方法。start()方法是一个native方法,它将启动一个新线程,并执行run()方法。

      2、实现Runnable接口

      如果一个类已经extends另一个类,就无法直接extends Thread,此时,必须实现一个Runnable接口

    public class MyThread extends otherclass implements Runnable{
        public void run(){     //重写run方法
        }
    }

     

    启动线程,需要首先实例化一个Thread,并传入自己的Thread实例

    MyThread mythread = new MyThread();

    Thread thread  = new Thread(mythread);

    thread.start();

    区别

      继承Thread类,来实现多线程,相当于拿出三件事,分别交给三个线程来做。因为MyThread继承Thread,所以在new MyThread的时候在创建三个对象的同时创建了三个线程。

      实现Runnable的,相当于拿出一件事让三个线程去做, new MyThread 相当于创建一个任务,然后实例化三个Thread。多线程的实例变量也是共享的,故而实现Runnable接口适合于资源共享。

     

    解析

    (1)start()方法

      用start方法来启动线程,是真正实现了多线程,通过调用Thread类的start()方法来启动一个线程,这时此线程处于就绪(可运行)状态,并没有运行,一旦得到cpu时间片,就开始执行run()方法(run()方法是开始执行的点)。但要注意的是,此时无需等待run()方法执行完毕,即可继续执行下面的代码。所以run()方法并没有实现多线程。

    (2)run()方法

      run()方法只是类的一个普通方法而已,如果直接调用Run方法,程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序执行,还是要等待run方法体执行完毕后才可继续执行下面的代码。

    (3)线程可以设置优先级大小:thread1.setPriority();

    (4)Thread类是在java.lang包中定义的。一个类只要继承了Thread类同时重写了本类中的run方法就可以实现多线程操作,但是一个类只能继承一个父类。

      Runnable接口避免继承的局限,一个类可以继承多个接口。适用于资源的共享。

    (5)除了Thread、Runnable还有Callable等方式来实现线程。

    (6)wait()方法必须进行try catch异常捕捉,线程抛出的错误一般与InterruptedException有关。

    (7)将一个线程设置成daemon(后台线程),意味着主线程结束,后台线程自动结束。

    (8)线程停止有三种方法:①调用stop()方法②线程执行完成③异常抛出。

     

    守护线程(Daemon Thread

      守护线程也称“服务进程”、“后台线程”,是指在程序运行时在后台提供一种通用服务的线程。如果用户线程已经全部退出运行,只剩下守护线程存在了,JVM也就退出了。

      守护线程一般具有较低的优先级,用户也可以自己设置守护线程,在调用start()方法启动线程之前调用对象的setDaemon(true)方法,设置为false,则表示的是用户进程模式。当一个守护线程中产生了其他线程,这些新的线程还是守护线程。

      典型的例子就是垃圾回收器

     

    线程同步

    (1)同步方法

             用synchronized关键字修饰方法。由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。

    (2)同步代码块

             用synchronized关键字修饰的语句块,语句块会自动被加上内置锁,从而实现同步。

             同步是一种高开销的操作,因此应该尽量减少同步的内容。

    (3)wait和notify

             wait():使线程处理等待状态,并且释放所持有的对象的lock。

    (4)使用特殊变量(volatile)实现线程同步

    (5)使用重入锁实现线程同步

    (6)使用局部变量实现线程同步

    (7)使用阻塞队列实现线程同步

     

    volatile 与 synchronized

      volatile主要用在多个线程感知实例变量被更改了场合,从而使得各个线程获得最新的值。它强制线程每次从主内存中讲到变量,而不是从线程的私有内存中读取变量,从而保证了数据的可见性。

    比较

      ①volatile轻量级,只能修饰变量。synchronized重量级,还可修饰方法。

      ②volatile只能保证数据的可见性,不能用来同步,因为多个线程并发访问volatile修饰的变量不会阻塞。

      synchronized不仅保证可见性,而且还保证原子性,因为,只有获得了锁的线程才能进入临界区,从而保证临界区中的所有语句都全部执行。多个线程争抢synchronized锁对象时,会出现阻塞。

    线程安全性

      线程安全性包括两个方面:①可见性;②原子性。

      从上面自增的例子中可以看出:仅仅使用volatile并不能保证线程安全性。而synchronized则可实现线程的安全性。

     

    线程之间状态转换

      线程从新建到结束一般有5个状态,为新建、可运行、运行、阻塞、结束,其中阻塞不是必经状态!

     

    1. 新建(new):新创建了一个线程对象。

    2. 可运行(runnable):线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取cpu 的使用权。

    3. 运行(running):可运行状态(runnable)的线程获得了cpu 时间片(timeslice),执行程序代码。

    4. 阻塞(block):阻塞状态是指线程因为某种原因放弃了cpu 使用权,也即让出了cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得cpu timeslice 转到运行(running)状态。阻塞的情况分三种:

      (一). 等待阻塞:运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中。

      (二). 同步阻塞:运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。

      (三). 其他阻塞:运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。

    5. 死亡(dead):线程run()、main() 方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。

     

    sleep()和wait()

      sleep()是线程类(Thread)的方法,导致此线程暂停执行指定时间,给执行机会给其他线程,但是监控状态依然保持,到时后会自动恢复。调用sleep不会释放对象锁。

      wait()是Object类的方法,对此对象调用wait方法导致本线程放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象发出notify方法(或notifyAll)后本线程才进入对象锁定池准备获得对象锁进入就绪状态

    区别:

      sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常。

      sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。

     

      Condition是在java 1.5中才出现的,它用来替代传统的Object的wait()、notify()实现线程间的协作,相比使用Object的wait()、notify(),使用Condition1的await()、signal()这种方式实现线程间协作更加安全和高效。因此通常来说比较推荐使用Condition,阻塞队列实际上是使用了Condition来模拟线程间协作。

      Condition是个接口,基本的方法就是await()和signal()方法;

     

    死锁

    死锁产生的原因及必要条件

        产生死锁的原因主要是:

      (1)因为系统资源不足。

      (2)进程运行推进的顺序不合适。

      (3)资源分配不当等。

      如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。其次,进程运行推进顺序与速度不同,也可能产生死锁。

        产生死锁的四个必要条件:

      (1)互斥条件:一个资源每次只能被一个进程使用。

      (2)请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。

      (3)不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。

      (4)循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

      这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。

     

    进程、线程、纤程

             进程通常被定义为一个正在运行的程序的实例,是具有一定独立功能的程序关于某个数据集合上的依次运动活动,是系统进行资源分配和调度的一个独立单位。

             线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属于一个进程的其他线程共享进程所拥有的全部资源。

             纤程是以用户方式代码来实现的,内核并不知道纤程,并且他们是根据用户定义的算法来调度的。由于定义了纤程的调度算法,因此,就内核而言,纤程采用非抢占式调度的方式。

    (1)关系

             一个线程可以创建和撤销另一个线程;同一个进程中的多个线程之间可以并发执行。相对进程而言,线程是一个更加接近于执行体的概念,它可以与同进程中的其他线程共享数据,但拥有自己的栈空间,拥有独立的执行序列。

    (2)区别

      进程和线程的主要差别在于它们是不同的操作系统资源管理方式。

      进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。

      线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。

        a. 一个程序至少有一个进程,一个进程至少有一个线程。

        b. 线程的划分尺度小于进程,使得多线程程序的并发性高。

        c. 进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。

        d. 线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。

        e. 从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。

    线程安全和线程不安全

      线程安全就是多线程访问的时候,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可以使用,不会出现数据不一致或者数据污染。Vector、HashTable、StringBuffer

      线程不安全就是不提供数据的访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。

       线程安全的问题就是由全局变量以及静态变量引起的,若每个线程中对全局变量,静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时进行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。

      举例:开启1000个线程分别向ArrayList中添加元素,每个对象添加100个元素,最终size应该为100000,结果会发现size会小于100000

     

     

    异常

     

     

      Java是通过面向对象的方法进行异常处理,把各种不同的异常进行分类,并提供了良好的接口。在Java中,每一个异常都是一个对象,它是Throwable类或者其他子类的实例。

    当一个方法出现异常后便抛出一个异常对象,该对象中包含异常信息,调用这个对象的方法可以捕获到这个异常并进行处理。

     

     

      异常的继承结构:基类为Throwable,Error和Exception继承Throwable,RuntimeException和IOException等继承Exception。

        非RuntimeException一般是外部错误(非Error),其必须被 try{}catch语句块所捕获

        手动创建一个异常需要写new exception

      运行异常,可以通过java虚拟机来自行处理。非运行异常,我们应该捕获或者抛出。

      throw关键字用于抛出异常

      throws关键字可以在方法上声明该方法要抛出的异常,然后在方法内部通过throw抛出异常对象。

      运行时异常:都是RuntimeException类及其子类异常,如NullPointerException、IndexOutOfBoundsException等,这些异常是不检查异常,程序可以选择捕获处理,也可以不处理,一般都是由逻辑错误引起的。

      非运行时异常:是RuntimeException以外的异常,类型上都属于Exception类及其子类。必须进行异常处理,不处理,程序不能编译通过。

     

    finally

        两种情况下finally语句是不会被执行的:

    (1)try语句没有被执行到,如在try语句之前就返回了,这样finally语句就不会执行,这也说明了finally语句被执行的必要而非充分条件是:相应的try语句一定被执行到。

    (2)在try块中有System.exit(0);这样的语句,System.exit(0);是终止Java虚拟机JVM的,连JVM都停止了,所有都结束了,当然finally语句也不会被执行到。

      1、finally语句是在try的return语句执行之后,return返回之前执行。

      2、finally块中的return语句会覆盖try块中的return返回。

      3、如果finally语句中没有return语句覆盖返回值,那么原来的返回值可能因为finally里的修改而改变也可能不变。

      4、finally语句先于 return 和 throw语句执行。

    总结

      finally块的语句在try或catch中的return语句执行之后返回之前执行且finally里的修改语句可能影响也可能不影响try或catch中 return已经确定的返回值,若finally里也有return语句则覆盖try或catch中的return语句直接返回。

     

     

  • 相关阅读:
    Linux ,Ubuntu 分区建议大小
    [No000019A]【波浪理论精典教学课程】
    CS(计算机科学)知识体
    如何量化考核技术人的KPI?
    Linux系统的命令应该如何记?
    数学和物理太难?这些 GIF 让你秒懂抽象概念
    加密数字货币的 7 个致命悖论
    量子力学中,全体自然数之和是负十二分之一,物理意义是什么?
    30岁左右的人,来谈谈你犯了哪些错误?
    一篇文章学懂Shell脚本,最简明的教程在这里
  • 原文地址:https://www.cnblogs.com/ghq120/p/8280693.html
Copyright © 2011-2022 走看看