多个线程在执行同一段代码的时候,每次的执行结果和单线程执行的结果都是一样的,不存在执行结果的二义性,就可以称作是线程安全的。
讲到线程安全问题,其实是指多线程环境下对共享资源的访问可能会引起此共享资源的不一致性。因此,为避免线程安全问题,应该避免多线程环境下对此共享资源的并发访问。
线程安全问题多是由全局变量和静态变量引起的,当多个线程对共享数据只执行读操作,不执行写操作时,一般是线程安全的;当多个线程都执行写操作时,需要考虑线程同步来解决线程安全问题。
线程同步:将操作共享数据的代码行作为一个整体,同一时间只允许一个线程执行,执行过程中其他线程不能参与执行。目的是为了防止多个线程访问一个数据对象时,对数据造成的破坏。
(1)同步方法(synchronized)
对共享资源进行访问的方法定义中加上 synchronized 关键字修饰,使得此方法称为同步方法。可以简单理解成对此方法进行了加锁,其锁对象为当前方法所在的对象自身。多线程环境下,当执行此方法时,首先都要获得此同步锁(且同时最多只有一个线程能够获得),只有当线程执行完此同步方法后,才会释放锁对象,其他的线程才有可能获取此同步锁,以此类推... 格式如下:
- public synchronized void run() {
- // ....}
(2)同步代码块(synchronized)
使用同步方法时,使得整个方法体都成为了同步执行状态,会使得可能出现同步范围过大的情况,于是,针对需要同步的代码可以直接另一种同步方式——同步代码块来解决。格式如下:
- synchronized (obj) {
- // ....}
其中,obj 为锁对象,因此,选择哪一个对象作为锁是至关重要的。一般情况下,都是选择此共享资源对象作为锁对象。
(3)同步锁(Lock)
使用 Lock 对象同步锁可以方便地解决选择锁对象的问题,唯一需要注意的一点是 Lock 对象需要与资源对象同样具有一对一的关系。Lock 对象同步锁一般格式为:
- class X {
- // 显示定义Lock同步锁对象,此对象与共享资源具有一对一关系
- private finalLock lock =new ReentrantLock();
- public void m(){
- // 加锁
- lock.lock();
- //... 需要进行线程安全同步的代码
- // 释放Lock锁
- lock.unlock();
- }
- }
什么时候需要同步:
(1)可见性同步:在以下情况中必须同步: 1)读取上一次可能是由另一个线程写入的变量 ;2)写入下一次可能由另一个线程读取的变量
(2)一致性同步:当修改多个相关值时,您想要其它线程原子地看到这组更改—— 要么看到全部更改,要么什么也看不到。
这适用于相关数据项(如粒子的位置和速率)和元数据项(如链表中包含的数据值和列表自身中的数据项的链)。
在某些情况中,您不必用同步来将数据从一个线程传递到另一个,因为 JVM 已经隐含地为您执行同步。这些情况包括:
锁的原理:
wait():导致当前线程等待并使其进入到等待阻塞状态。直到其他线程调用该同步锁对象的 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() 方法代码后面继续往下执行的。
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. wai()t 和 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. 线程类的一些常用方法:
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。
参考链接:
http://www.importnew.com/21136.html
http://lavasoft.blog.51cto.com/62575/99155/
http://www.cnblogs.com/lwbqqyumidi/p/3821389.html
来源: http://www.cnblogs.com/IUbanana/p/7112296.html