JAVA多线程和并发性知识点总结

转载请注明出处:http://blog.csdn.net/zhoubin1992/article/details/46861397

上次我总结了一份JAVA 面向对象和集合知识点总结: 
http://blog.csdn.net/zhoubin1992/article/details/46481759 
受到了博友们的激励,这次对JAVA多线程和并发性相关知识点进行总结,方便各位博友学习以及自己复习之用。


一、 什么是进程、线程?线程和进程的区别?

1. 进程 
当一个程序进入内存运行时,即变成一个进程。进程是处于运行过程中的程序。 
进程是操作系统进行资源分配和调度的一个独立单位。 
进程的三个特征:

  • 独立性 
    独立存在的实体,每个进程都有自己独立私有的一块内存空间。
  • 动态性 
    程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合。
  • 并发性 
    多个进程可在单处理器上并发执行。
并发性和并行性
    并发是指在同一时间点只能有一条指令执行,但多个进程指令被快速轮换执行,使得在宏观上具有多个进程同时执行的效果。
    并行指在同一时间点,有多条指令在多个处理器上同时执行。
  • 1
  • 2
  • 3

2. 线程 
线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。线程也被称作轻量级进程。线程在进程中是独立,并发的执行流。 
3.线程和进程的区别

  1. 线程是进程的组成部分,一个进程可以有很多线程,每条线程并行执行不同的任务。
  2. 不同的进程使用不同的内存空间,而线程与父进程的其他线程共享父进程的所拥有的全部资源。这样编程方便了,但是要更加小心。
  3. 别把内存空间和栈内存搞混,每个线程都拥有单独的栈内存用来存储本地数据。线程拥有自己的堆栈、自己的程序计数器和自己的局部变量,但不拥有系统资源。
  4. 线程的调度和管理由进程本身负责完成。操作系统对进程进行调度,管理和资源分配。

二、 多线程的优势

  1. 进程之间不能共享内存,但线程之间共享内存。
  2. 系统创建进程时需要为该进程重新分配系统资源,但创建线程则代价小很多,效率高。
  3. 资源利用率更好
  4. 程序设计更简单
  5. 程序响应更快 
    后三条详细见http://ifeve.com/benefits/

三、 Java中创建线程方法

1. 继承Thread类创建线程类

  1. 定义Thread类的子类,重写该类的run()方法。该方法为线程执行体。
  2. 创建Thread子类的实例。即线程对象。
  3. 调用线程对象的start()方法启动该线程 
    2. 实现Runnable接口创建线程类 
    1. 定义Runnable接口的实现类,重写该接口的run()方法。该方法为线程执行体。
    2. 创建Runnable实现类的实例。并以此实例作为Thread的target来创建Thread对象。该Thread对象才是真正的线程对象。
    3. 调用线程对象(该Thread对象)的start()方法启动该线程。 
      3. 使用Callable和Future创建线程 
      http://blog.csdn.net/ghsau/article/details/7451464

四、 用Runnable还是ThreadJava以及创建线程两种方法对比?

在java多线程中,一般推荐采用实现Runnable接口来创建多线程,因为实现Runnable接口相比继承Thread类有如下优劣势:

  • 实现Runnable接口,线程类只是实现了接口,还可以继承其他类;继承Thread类的话,不能再继承其他父类。
  • 实现Runnable接口,多个线程可以共享同一个target对象,所以适合多个相同程序代码的线程区处理同一资源的情况。分离数据和代码,体现面向对象的思想。
  • 实现Runnable接口,访问当前线程,必须使用Thread.currentThread()方法;继承Thread类的话,使用this获得当前线程。 
    与http://blog.csdn.net/ns_code/article/details/17161237互补。

五、 Thread 类中的start() 和 run() 方法有什么区别?

  1. start()方法被用来启动新创建的线程,而且start()内部调用了run()方法,这和直接调用run()方法的效果不一样。
  2. 当你调用run()方法的时候,只会是在原来的线程中调用,没有新的线程启动,start()方法才会启动新线程。 
    需要特别注意的是:不能对同一线程对象两次调用start()方法。

六、 线程的生命周期

Java线程五种状态:

  1. 新建状态(New):当线程对象创建后,即进入了新建状态。仅仅由java虚拟机分配内存,并初始化。如:Thread t = new MyThread();
  2. 就绪状态(Runnable):当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,java虚拟机创建方法调用栈和程序计数器,只是说明此线程已经做好了准备,随时等待CPU调度执行,此线程并 没有执行。
  3. 运行状态(Running):当CPU开始调度处于就绪状态的线程时,执行run()方法,此时线程才得以真正执行,即进入到运行状态。注:绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
  4. 阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种: 
    1. 等待阻塞 – 运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态,JVM会把该线程放入等待池中;
    2. 同步阻塞 – 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
    3. 其他阻塞 – 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
  5. 死亡状态(Dead):线程run()方法执行完了或者因异常退出了run()方法,该线程结束生命周期。 
    当主线程结束时,其他线程不受任何影响。

七、 java控制线程方法

1. join线程 
join方法用线程对象调用,如果在一个线程A中调用另一个线程B的join方法,线程A将会等待线程B执行完毕后再执行。 
2. 守护线程(Daemon Thread) 
Java中有两类线程:User Thread(用户线程)、Daemon Thread(守护线程) 。 
用户线程即运行在前台的线程,而守护线程是运行在后台的线程。 守护线程作用是为其他前台线程的运行提供便利服务,而且仅在普通、非守护线程仍然运行时才需要,比如垃圾回收线程就是一个守护线程。当VM检测仅剩一个守护线程,而用户线程都已经退出运行时,VM就会退出,因为没有如果没有了被守护这,也就没有继续运行程序的必要了。如果有非守护线程仍然存活,VM就不会退出。 
守护线程的特征:如果所有前台线程都死亡,后台线程会自动死亡。 
守护线程并非只有虚拟机内部提供,用户在编写程序时也可以自己设置守护线程。用户可以用Thread的setDaemon(true)方法设置当前线程为守护线程。 
虽然守护线程可能非常有用,但必须小心确保其他所有非守护线程消亡时,不会由于它的终止而产生任何危害。因为你不可能知道在所有的用户线程退出运行前,守护线程是否已经完成了预期的服务任务。一旦所有的用户线程退出了,虚拟机也就退出运行了。 因此,不要在守护线程中执行业务逻辑操作(比如对数据的读写等)。 
另外有几点需要注意:

1、setDaemon(true)必须在调用线程的start()方法之前设置,否则会跑出IllegalThreadStateException异常。
2、在守护线程中产生的新线程也是守护线程。  
3、 不要认为所有的应用都可以分配给守护线程来进行服务,比如读写操作或者计算逻辑。 

参考http://blog.csdn.net/ns_code/article/details/17099981 
3. 线程让步(yield ) 
yield可以直接用Thread类调用,可以让当前正在执行的线程暂停,不会阻塞该线程,只是将该线程转入就绪状态。yield让出CPU执行权给同等级的线程,如果没有相同级别的线程在等待CPU的执行权,则该线程继续执行。


八、 sleep()方法和yield()方法的区别

  1. sleep()方法暂停当前线程后,会给其他线程执行机会,不会理会其他线程的优先级;但yield()方法只会给优先级相同,或优先级更高的线程执行机会。
  2. sleep()方法会将线程转入阻塞状态(block状态),直到经过阻塞时间才会转入就绪状态;而yield()方法不会将线程转入阻塞状态,它只是强制当前线程进入就绪状态。 因此完全有可能某个线程调用yield()方法暂停之后,立即再次获得处理器资源被执行。
  3. sleep()方法声明抛出了InterruptedException异常,所以调用sleep()方法时要么捕捉该异常,要么显式声明抛出该异常;而yield()方法则没有声明抛出任何异常。
  4. sleep()方法比yield()方法有更好的可移植性,通常不建议使用yield()方法来控制并发线程的执行。

九、 为什么Thread类的sleep()和yield()方法是静态的?

Thread类的sleep()和yield()方法将在当前正在执行的线程上运行。所以在其他处于等待状态的线程上调用这些方法是没有意义的。它们可以在当前正在执行的线程中工作,并避免程序员错误的认为可以在其他非运行线程调用这些方法。 
现在的实现, 是只能sleep当前的线程.当前线程是自愿的.让sleep()成为实例方法, 当前线程可以直接sleep别的线程, 会引入很多 多线程问题,例如死锁。 
destroy(), suspend(), stop(),resume()这些实例方法都已经被deprecated(弃用)。活下来的是哪些? 只有static方法(只对当前线程操作和一些比较温和的实例方法, 如getXXX(), isXXX(), join(), yield()等.


十、 sleep方法与wait方法的区别?

  1. sleep方法是静态方法,wait方法是非静态方法。
  2. sleep方法在时间到后会自己“醒来”,但wait不能,必须由其它线程通过notify(All)方法让它“醒来”。
  3. sleep方法通常用在不需要等待资源情况下的阻塞,像等待线程、数据库连接的情况一般用wait。

十一、 线程安全问题

线程安全问题,其实是指多线程环境下对共享资源的访问可能会引起此共享资源的不一致性。因此,为避免线程安全问题,应该避免多线程环境下对此共享资源的并发访问。


十二、 同步代码块

同步代码块的格式为:

 synchronized (obj) {             
    //...
 }
  • 1
  • 2
  • 3
  • 4

其中,obj为锁对象,因此,选择哪一个对象作为锁是至关重要的。一般情况下,都是选择此共享资源对象作为锁对象。 
任何时刻只能有一个线程可以获得对锁对象的锁定,其他线程无法获得锁,也无法修改它。当同步代码块执行完成后,该线程会释放对锁对象的锁定。 
通过这种方式可以保证并发线程在任一时刻只有一个线程可以进入修改共享资源的代码区(临界区),从而保证线程的安全性。


十三、 同步方法

对共享资源进行访问的方法定义中加上synchronized关键字修饰,使得此方法称为同步方法。可以简单理解成对此方法进行了加锁,其锁对象为当前方法所在的对象自身。多线程环境下,当执行此方法时,首先都要获得此同步锁(且同时最多只有一个线程能够获得),只有当线程执行完此同步方法后,才会释放锁对象,其他的线程才有可能获取此同步锁,以此类推…


 public synchronized void a() {        
     // ....
 }
  • 1
  • 2
  • 3
  • 4

可变类的线程安全是以降低程序的运行效率为代价的,为了减少程序安全所带来的负面影响,程序可以采用如下策略: 
- 不要对线程安全类的所有方法都进行同步,只对那些会改变竞争资源的方法进行同步。 
- 如果可变类有两种运行环境:单线程和多线程环境,则应该为该可变类提供两种版本:线程不安全版本和线程安全版本。在单线程环境中使用线程不安全版本以保证性能,在多线程环境中使用线程安全版本。


十四、 何时会释放对同步监视器锁定?

程序无法显式的释放对同步监视器的锁定,线程可以通过以下方式释放锁定: 
A、当线程的同步方法、同步代码库执行结束,就可以释放同步监视器 
B、当线程在同步代码库、方法中遇到break、return终止代码的运行,也可释放 
C、当线程在同步代码库、同步方法中遇到未处理的Error、Exception,导致该代码结束也可释放同步监视器 
D、当线程在同步代码库、同步方法中,程序执行了同步监视器对象的wait方法,导致方法暂停,释放同步监视器

下面情况不会释放同步监视器:
A、当线程在执行同步代码库、同步方法时,程序调用了Thread.sleep()/Thread.yield()方法来暂停当前程序,当前程序不会释放同步监视器
B、当线程在执行同步代码库、同步方法时,其他线程调用了该线程的suspend方法将该线程挂起,该线程不会释放同步监视器。注意尽量避免使用suspend、resume

十五、同步锁(Lock)

通常认为:Lock提供了比synchronized方法和synchronized代码块更广泛的锁定操作,Lock更灵活的结构,有很大的差别,并且可以支持多个Condition对象 
Lock是控制多个线程对共享资源进行访问的工具。通常,锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁, 
线程开始访问共享资源之前应先获得Lock对象。不过某些锁支持共享资源的并发访问,如:ReadWriteLock(读写锁),在线程安全控制中, 
通常使用ReentrantLock(可重入锁)。使用该Lock对象可以显示加锁、释放锁。

    class C {
        //锁对象
        private final ReentrantLock lock = new ReentrantLock();
        ......
        //保证线程安全方法
        public void method() {
            //上锁
            lock.lock();
            try {
                //保证线程安全操作代码
            } catch() {

            } finally {
                lock.unlock();//释放锁
            }
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

使用Lock对象进行同步时,锁定和释放锁时注意把释放锁放在finally中保证一定能够执行。使用锁和使用同步很类似,只是使用Lock时显示的调用lock方法来同步。而使用同步方法synchronized时系统会隐式使用当前对象作为同步监视器,同样都是“加锁->访问->释放锁”的操作模式,都可以保证只能有一个线程操作资源。 
同步方法和同步代码块使用与竞争资源相关的、隐式的同步监视器,并且强制要求加锁和释放锁要出现在一个块结构中,而且获得多个锁时,它们必须以相反的顺序释放,且必须在与所有锁被获取时相同的范围内释放所有资源。 
Lock提供了同步方法和同步代码库没有的其他功能,包括用于非块结构的tryLock方法,已经试图获取可中断锁lockInterruptibly()方法, 还有获取超时失效锁的tryLock(long, timeUnit)方法。 
ReentrantLock具有重入性,也就是说线程可以对它已经加锁的ReentrantLock再次加锁,ReentrantLock对象会维持一个计数器来追踪lock方法的嵌套调用,线程在每次调用lock()加锁后,必须显示的调用unlock()来释放锁,所以一段被保护的代码可以调用另一个被相同锁保护的方法。


十六、死锁

当2个线程相互等待对方是否同步监视器时就会发生死锁,JVM没有采取处理死锁的措施,这需要我们自己处理或避免死锁。 
一旦死锁,整个程序既不会出现异常,也不会出现错误和提示,只是线程将处于阻塞状态,无法继续。 
由于Thread类的suspend也很容易导致死锁,所以Java不推荐使用此方法暂停线程。 
参考http://ifeve.com/deadlock/了解更多死锁情况。 
大部分代码并不容易产生死锁,死锁可能在代码中隐藏相当长的时间,等待不常见的条件地发生,但即使是很小的概率,一旦发生,便可能造成毁灭性的破坏。避免死锁是一件困难的事,遵循以下原则有助于规避死锁

 1、只在必要的最短时间内持有锁,考虑使用同步语句块代替整个同步方法;

 2、尽量编写不在同一时刻需要持有多个锁的代码,如果不可避免,则确保线程持有第二个锁的时间尽量短暂;

 3、创建和使用一个大锁来代替若干小锁,并把这个锁用于互斥,而不是用作单个对象的对象级别锁;

参考:http://blog.csdn.net/ns_code/article/details/17200937


这些我都是看书,以及参考网络资料总结的。接下来还会总结线程通信,线程池和线程安全集合类相关知识点。我会在本博文更新~

参考: 
http://ifeve.com/java-multi-threading-concurrency-interview-questions-with-answers/ 
http://ifeve.com/java-concurrency-thread-directory/ 
http://www.importnew.com/12773.html 
http://www.cnblogs.com/lwbqqyumidi/p/3804883.html 
java编程思想 
java疯狂讲义

你可能感兴趣的:(Java)