Java多线程(二) —— 线程安全、线程同步、线程间通讯(含面试题集)

上一篇博文:Java多线程(一) —— 线程的状态详解中详细介绍了线程的五种状态及状态间的转换。本文着重介绍了线程安全的相关知识点,包括线程同步和锁机制、线程间通讯以及相关面试题的总结html

1、线程安全

多个线程在执行同一段代码的时候,每次的执行结果和单线程执行的结果都是同样的,不存在执行结果的二义性,就能够称做是线程安全的。面试

讲到线程安全问题,实际上是指多线程环境下对共享资源的访问可能会引发此共享资源的不一致性。所以,为避免线程安全问题,应该避免多线程环境下对此共享资源的并发访问。编程

线程安全问题可能是由全局变量和静态变量引发的,当多个线程对共享数据只执行读操做,不执行写操做时,通常是线程安全的;当多个线程都执行写操做时,须要考虑线程同步来解决线程安全问题。安全

 

2、线程同步(synchronized/Lock)

线程同步:将操做共享数据的代码行做为一个总体,同一时间只容许一个线程执行,执行过程当中其余线程不能参与执行。目的是为了防止多个线程访问一个数据对象时,对数据形成的破坏。多线程

(1)同步方法(synchronized)并发

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

public synchronized void run() {        
     // ....
}

 

(2)同步代码块(synchronized)this

使用同步方法时,使得整个方法体都成为了同步执行状态,会使得可能出现同步范围过大的状况,因而,针对须要同步的代码能够直接另外一种同步方式——同步代码块来解决。格式以下:spa

synchronized (obj) {        
     // ....
}

其中,obj为锁对象,所以,选择哪个对象做为锁是相当重要的。通常状况下,都是选择此共享资源对象做为锁对象。操作系统

 

(3)同步锁(Lock)

 使用Lock对象同步锁能够方便地解决选择锁对象的问题,惟一须要注意的一点是Lock对象须要与资源对象一样具备一对一的关系。Lock对象同步锁通常格式为:

class X {
    // 显示定义Lock同步锁对象,此对象与共享资源具备一对一关系
    private final Lock lock = new ReentrantLock();
    
    public void m(){
        // 加锁
        lock.lock();
        
        //...  须要进行线程安全同步的代码
        
        // 释放Lock锁
        lock.unlock();
    }
}

 

何时须要同步:

(1)可见性同步:在如下状况中必须同步: 1)读取上一次多是由另外一个线程写入的变量 ;2)写入下一次可能由另外一个线程读取的变量

(2)一致性同步:当修改多个相关值时,您想要其它线程原子地看到这组更改—— 要么看到所有更改,要么什么也看不到。

这适用于相关数据项(如粒子的位置和速率)和元数据项(如链表中包含的数据值和列表自身中的数据项的链)。

在某些状况中,您没必要用同步来将数据从一个线程传递到另外一个,由于 JVM 已经隐含地为您执行同步。这些状况包括:

  1. 由静态初始化器(在静态字段上或 static{} 块中的初始化器)
  2. 初始化数据时 
  3. 访问 final 字段时
  4. 在建立线程以前建立对象时 
  5. 线程能够看见它将要处理的对象时

 

锁的原理:

  • Java中每一个对象都有一个内置锁
  • 当程序运行到非静态的synchronized同步方法上时,自动得到与正在执行代码类的当前实例(this实例)有关的锁。得到一个对象的锁也称为获取锁、锁定对象、在对象上锁定或在对象上同步。
  • 当程序运行到synchronized同步方法或代码块时才该对象锁才起做用。
  • 一个对象只有一个锁。因此,若是一个线程得到该锁,就没有其余线程能够得到锁,直到第一个线程释放(或返回)锁。这也意味着任何其余线程都不能进入该对象上的synchronized方法或代码块,直到该锁被释放。
  • 释放锁是指持锁线程退出了synchronized同步方法或代码块。
锁与同步要点:
1)、只能同步方法,而不能同步变量和类;
2)、每一个对象只有一个锁;当提到同步时,应该清楚在什么上同步?也就是说,在哪一个对象上同步?
3)、没必要同步类中全部的方法,类能够同时拥有同步和非同步方法。
4)、若是两个线程要执行一个类中的synchronized方法,而且两个线程使用相同的实例来调用方法,那么一次只能有一个线程可以执行方法,另外一个须要等待,直到锁被释放。也就是说:若是一个线程在对象上得到一个锁,就没有任何其余线程能够进入(该对象的)类中的任何一个同步方法。
5)、若是线程拥有同步和非同步方法,则非同步方法能够被多个线程自由访问而不受锁的限制。
6)、线程睡眠时,它所持的任何锁都不会释放。
7)、线程能够得到多个锁。好比,在一个对象的同步方法里面调用另一个对象的同步方法,则获取了两个对象的同步锁。
8)、同步损害并发性,应该尽量缩小同步范围。同步不但能够同步整个方法,还能够同步方法中一部分代码块。
9)、在使用同步代码块时候,应该指定在哪一个对象上同步,也就是说要获取哪一个对象的锁。
10)、同步静态方法,须要一个用于整个类对象的锁,这个对象是就是这个类(XXX.class)。
 
线程不能得到锁会怎么样:若是线程试图进入同步方法,而其锁已经被占用,则线程在该对象上被阻塞。实质上,线程进入该对象的的一种池中,必须在哪里等待,直到其锁被释放,该线程再次变为可运行或运行为止。
 
线程死锁:当两个线程被阻塞,每一个线程都在等待另外一个线程时就发生死锁。有一些设计方法能帮助避免死锁,如始终按照预约义的顺序获取锁这一策略。
 
线程同步小结
 
一、线程同步的目的是为了保护多个线程反问一个资源时对资源的破坏。
二、线程同步方法是经过 来实现,每一个对象都有且仅有一个锁,这个锁与一个特定的对象关联,线程一旦获取了对象锁,其余访问该对象的线程就没法再访问该对象的其余同步方法。
三、对于静态同步方法,锁是针对这个类的,锁对象是该类的Class对象。静态和非静态方法的锁互不干预。一个线程得到锁,当在一个同步方法中访问另外对象上的同步方法时,会获取这两个对象锁。
四、对于同步,要时刻清醒在哪一个对象上同步,这是关键。
五、编写线程安全的类,须要时刻注意对多个线程竞争访问资源的逻辑和安全作出正确的判断,对“原子”操做作出分析,并保证原子操做期间别的线程没法访问竞争资源。
六、当多个线程等待一个对象锁时,没有获取到锁的线程将发生阻塞。
七、死锁是线程间相互等待锁锁形成的,在实际中发生的几率很是的小。真让你写个死锁程序,不必定好使,呵呵。可是,一旦程序发生死锁,程序将死掉。
 

3、线程通讯:wait()/notify()/notifyAll()

wait():致使当前线程等待并使其进入到等待阻塞状态。直到其余线程调用该同步锁对象的notify()或notifyAll()方法来唤醒此线程。

  • void wait(long timeout) -- 致使当前线程等待,直到其余线程调用此对象的 notify() 方法或 notifyAll() 方法,或者超过指定的时间量。 
  • void wait(long timeout, int nanos) -- 致使当前线程等待,直到其余线程调用此对象的 notify() 方法或 notifyAll() 方法,或者其余某个线程中断当前线程,或者已超过某个实际时间量。

notify():唤醒在此同步锁对象上等待的单个线程,若是有多个线程都在此同步锁对象上等待,则会任意选择其中某个线程进行唤醒操做,只有当前线程放弃对同步锁对象的锁定,才可能执行被唤醒的线程。

notifyAll():唤醒在此同步锁对象上等待的全部线程,只有当前线程放弃对同步锁对象的锁定,才可能执行被唤醒的线程。

  这三个方法主要都是用于多线程中,但实际上都是Object类中的本地方法。所以,理论上,任何Object对象均可以做为这三个方法的主调,在实际的多线程编程中,只有同步锁对象调这三个方法,才能完成对多线程间的线程通讯。

 

注意点:

1.wait()方法执行后,当前线程当即进入到等待阻塞状态,其后面的代码不会执行;

2.notify()/notifyAll()方法执行后,将唤醒此同步锁对象上的(任意一个-notify()/全部-notifyAll())线程对象,可是,此时还并无释放同步锁对象,也就是说,若是notify()/notifyAll()后面还有代码,还会继续进行,知道当前线程执行完毕才会释放同步锁对象;

3.notify()/notifyAll()执行后,若是右面有sleep()方法,则会使当前线程进入到阻塞状态,可是同步对象锁没有释放,依然本身保留,那么必定时候后仍是会继续执行此线程,接下来同2;

4.wait()/notify()/nitifyAll()完成线程间的通讯或协做都是基于不一样对象锁的,所以,若是是不一样的同步对象锁将失去意义,同时,同步对象锁最好是与共享资源对象保持一一对应关系;

5.当wait线程唤醒后并执行时,是接着上次执行到的wait()方法代码后面继续往下执行的。

 

4、相关面试题

1. 线程和进程有什么区别?
答:一个进程是一个独立(self contained)的运行环境,它能够被看做一个程序或者一个应用。而线程是在进程中执行的一个任务。线程是进程的子集,一个进程能够有不少线程,每条线程并行执行不一样的任务。不一样的进程使用不一样的内存空间,而全部的线程共享一片相同的内存空间。别把它和栈内存搞混,每一个线程都拥有单独的栈内存用来存储本地数据。

2. 如何在Java中实现线程?比较这种种方式
答:建立线程有两种方式:
(1)继承 Thread 类,扩展线程。
(2)实现 Runnable 接口。

继承Thread类的方式有它固有的弊端,由于Java中继承的单一性,继承了Thread类就不能继承其余类了;同时也不符合继承的语义,Dog跟Thread没有直接的父子关系,继承Thread只是为了能拥有一些功能特性。

而实现Runnable接口,①避免了单一继承的局限性,②同时更符合面向对象的编程方式,即将线程对象进行单独的封装,③并且实现接口的方式下降了线程对象(Dog)和线程任务(run方法中的代码)的耦合性,④如上面所述,可使用同一个Dog类的实例来建立并开启多个线程,很是方便的实现资源的共享。实际上Thread类也是实现了Runnable接口。实际开发中可能是使用实现Runnable接口的方式。

3. 启动一个线程是调用run()仍是start()方法?
答:启动一个线程是调用start()方法,使线程所表明的虚拟处理机处于可运行状态,这意味着它能够由JVM 调度并执行,这并不意味着线程就会当即运行。run()方法是线程启动后要进行回调(callback)的方法。

4. wait()和sleep()比较

共同点: 
1). 他们都是在多线程的环境下,sleep()方法和对象的wait()方法均可以让线程暂停执行,均可以在程序的调用处阻塞指定的毫秒数,并返回。 
2). wait()和sleep()均可以经过interrupt()方法打断线程的暂停状态 ,从而使线程马上抛出InterruptedException。 
若是线程A但愿当即结束线程B,则能够对线程B对应的Thread实例调用interrupt方法。若是此刻线程B正在wait/sleep /join,则线程B会马上抛出InterruptedException,在catch() {} 中直接return便可安全地结束线程。 须要注意的是,InterruptedException是线程本身从内部抛出的,并非interrupt()方法抛出的。对某一线程调用 interrupt()时,若是该线程正在执行普通的代码,那么该线程根本就不会抛出InterruptedException。可是,一旦该线程进入到 wait()/sleep()/join()后,就会马上抛出InterruptedException 。 

不一样点: 
1). Thread类的方法:sleep(),yield()等 
     Object类的方法:wait()和notify()等 
2). 每一个对象都有一个锁来控制同步访问。Synchronized关键字能够和对象的锁交互,来实现线程的同步。 
     sleep()方法让当前线程暂停执行指定的时间,将执行机会(CPU)让给其余线程,可是对象的锁依然保持,休眠结束后线程会自动回到就绪状态;

     wait()方法致使当前线程放弃对象的锁(线程暂停执行),进入对象的等待池(wait pool),只有调用对象的notify()方法(或notifyAll()方法)时才能唤醒等待池中的线程进入等锁池(lock pool),若是线程从新得到对象的锁就能够进入就绪状态。
3). wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep能够在任何地方使用 
4). sleep必须捕获异常,而wait,notify和notifyAll不须要捕获异常
因此sleep()和wait()方法的最大区别是:
  sleep()睡眠时,保持对象锁,仍然占有该锁;
  而wait()睡眠时,释放对象锁。
可是wait()和sleep()均可以经过interrupt()方法打断线程的暂停状态,从而使线程马上抛出InterruptedException(但不建议使用该方法)。

5. sleep()方法和yield()方法有什么区别?
① sleep()方法给其余线程运行机会时不考虑线程的优先级,所以会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会;
② 线程执行sleep()方法后转入阻塞(blocked)状态,而执行yield()方法后转入就绪(ready)状态;
③ sleep()方法须要声明抛出InterruptedException,而yield()方法没有声明任何异常;
④ sleep()方法比yield()方法(跟操做系统CPU调度相关)具备更好的可移植性。

6. 线程类的一些经常使用方法: 

  • sleep(): 强迫一个线程睡眠N毫秒,是一个静态方法,调用此方法要处理InterruptedException异常;
  • join():  让一个线程等待另外一个线程完成才继续执行;
  • yeild(): 线程让步,暂停当前正在执行的线程对象让出CPU资源,将当前线程从运行状态转换到就绪状态并执行其余优先级相同或更高的线程;
  • isAlive(): 判断一个线程是否存活。 
  • activeCount(): 程序中活跃的线程数。 
  • enumerate(): 枚举程序中的线程。 
  • currentThread(): 获得当前线程。 
  • isDaemon(): 一个线程是否为守护线程。 
  • setDaemon(): 设置一个线程为守护线程。(用户线程和守护线程的区别在于,是否等待主线程依赖于主线程结束而结束) 
  • setName(): 为线程设置一个名称。 
  • setPriority(): 设置一个线程的优先级。
  • wait():使一个线程处于等待(阻塞)状态,而且释放所持有的对象的锁;
  • notify():唤醒一个处于等待状态的线程,固然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM肯定唤醒哪一个线程,并且与优先级无关;
  • notityAll():唤醒全部处于等待状态的线程,该方法并非将对象的锁给全部线程,而是让它们竞争,只有得到锁的线程才能进入就绪状态;

7. 同步代码块和同步方法的区别

  二者的区别主要体如今同步锁上面。对于实例的同步方法,由于只能使用this来做为同步锁,若是一个类中须要使用到多个锁,为了不锁的冲突,必然须要使用不一样的对象,这时候同步方法不能知足需求,只能使用同步代码块(同步代码块能够传入任意对象);或者多个类中须要使用到同一个锁,这时候多个类的实例this显然是不一样的,也只能使用同步代码块,传入同一个对象。

8. 对比synchronized和Lock

1)、synchronized是关键字,就和if...else...同样,是语法层面的实现,所以synchronized获取锁以及释放锁都是Java虚拟机帮助用户完成的;ReentrantLock是类层面的实现,所以锁的获取以及锁的释放都须要用户本身去操做。特别再次提醒,ReentrantLock在lock()完了,必定要手动unlock(),通常放在finally语句块中。

2)、synchronized简单,简单意味着不灵活,而ReentrantLock的锁机制给用户的使用提供了极大的灵活性。这点在Hashtable和ConcurrentHashMap中体现得淋漓尽致。synchronized一锁就锁整个Hash表,而ConcurrentHashMap则利用ReentrantLock实现了锁分离,锁的只是segment而不是整个Hash表

3)、synchronized是不公平锁,而ReentrantLock能够指定锁是公平的仍是非公平的

4)、synchronized实现等待/通知机制通知的线程是随机的,ReentrantLock实现等待/通知机制能够有选择性地通知

5)、和synchronized相比,ReentrantLock提供给用户多种方法用于锁信息的获取,好比能够知道lock是否被当前线程获取、lock被同一个线程调用了几回、lock是否被任意线程获取等等

总结起来,我认为若是只须要锁定简单的方法、简单的代码块,那么考虑使用synchronized,复杂的多线程处理场景下能够考虑使用ReentrantLock

 

Voiatile关键字:

volatile关键字是Java并发的最轻量级实现,本质上有两个功能,在生成的汇编语句中加入LOCK关键字和内存屏障

做用就是保证每一次线程load和write两个操做,都会直接从主内存中进行读取和覆盖,而非普通变量从线程内的工做空间(默认各位已经熟悉Java多线程内存模型)

但它有一个很致命的缺点,致使它的使用范围很少,就是他只保证在读取和写入这两个过程是线程安全的。若是咱们对一个volatile修饰的变量进行多线程 下的自增操做,仍是会出现线程安全问题。根本缘由在于volatile关键字没法对自增进行安全性修饰,由于自增分为三步,读取-》+1-》写入。中间多 个线程同时执行+1操做,仍是会出现线程安全性问题。

 

参考连接:

http://www.importnew.com/21136.html

http://lavasoft.blog.51cto.com/62575/99155/

http://www.cnblogs.com/lwbqqyumidi/p/3821389.html

相关文章
相关标签/搜索