【Java线程】锁机制:synchronized、Lock、Condition

  • http://www.infoq.com/cn/articles/java-memory-model-5  深入理解Java内存模型(五)——锁
  • http://www.ibm.com/developerworks/cn/java/j-jtp10264/  Java 理论与实践: JDK 5.0 中更灵活、更具可伸缩性的锁定机制
  • http://blog.csdn.net/ghsau/article/details/7481142

1、synchronized

把代码块声明为 synchronized,有两个重要后果,通常是指该代码具有 原子性(atomicity)和 可见性(visibility)

1.1 原子性

原子性意味着个时刻,只有一个线程能够执行一段代码,这段代码通过一个monitor object保护。从而防止多个线程在更新共享状态时相互冲突。

1.2 可见性

可见性则更为微妙,它要对付内存缓存和编译器优化的各种反常行为。它必须确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的 。

作用:如果没有同步机制提供的这种可见性保证,线程看到的共享变量可能是修改前的值或不一致的值,这将引发许多严重问题。

原理:当对象获取锁时,它首先使自己的高速缓存无效,这样就可以保证直接从主内存中装入变量。 同样,在对象释放锁之前,它会刷新其高速缓存,强制使已做的任何更改都出现在主内存中。 这样,会保证在同一个锁上同步的两个线程看到在 synchronized 块内修改的变量的相同值。

一般来说,线程以某种不必让其他线程立即可以看到的方式(不管这些线程在寄存器中、在处理器特定的缓存中,还是通过指令重排或者其他编译器优化),不受缓存变量值的约束,但是如果开发人员使用了同步,那么运行库将确保某一线程对变量所做的更新先于对现有synchronized 块所进行的更新,当进入由同一监控器(lock)保护的另一个synchronized 块时,将立刻可以看到这些对变量所做的更新。类似的规则也存在于volatile变量上。

——volatile只保证可见性,不保证原子性!

1.3 何时要同步?

可见性同步的基本规则是在以下情况中必须同步:

  1. 读取上一次可能是由另一个线程写入的变量
  2. 写入下一次可能由另一个线程读取的变量

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

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

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

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

1.4 synchronize的限制

synchronized是不错,但它并不完美。它有一些功能性的限制:

  1. 它无法中断一个正在等候获得锁的线程;
  2. 也无法通过投票得到锁,如果不想等下去,也就没法得到锁;
  3. 同步还要求锁的释放只能在与获得锁所在的堆栈帧相同的堆栈帧中进行,多数情况下,这没问题(而且与异常处理交互得很好),但是,确实存在一些非块结构的锁定更合适的情况。

2、ReentrantLock

java.util.concurrent.lock 中的Lock 框架是锁定的一个抽象,它允许把锁定的实现作为 Java 类,而不是作为语言的特性来实现。这就为Lock 的多种实现留下了空间,各种实现可能有不同的调度算法、性能特性或者锁定语义。

ReentrantLock 类实现了Lock ,它拥有与synchronized 相同的并发性和内存语义,但是添加了类似锁投票定时锁等候可中断锁等候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。(换句话说,当许多线程都想访问共享资源时,JVM 可以花更少的时候来调度线程,把更多时间用在执行线程上。)

  1. class Outputter1 {
  2. private Lock lock = new ReentrantLock();// 锁对象
  3. public void output(String name) {
  4. lock.lock();      // 得到锁
  5. try {
  6. for(int i = 0; i < name.length(); i++) {
  7. System.out.print(name.charAt(i));
  8. }
  9. } finally {
  10. lock.unlock();// 释放锁
  11. }
  12. }
  13. }

区别:

需要注意的是,用sychronized修饰的方法或者语句块在代码执行完之后锁自动释放,而是用Lock需要我们手动释放锁,所以为了保证锁最终被释放(发生异常情况),要把互斥区放在try内,释放锁放在finally内!!

3、读写锁ReadWriteLock

上例中展示的是和synchronized相同的功能,那Lock的优势在哪里?

例如一个类对其内部共享数据data提供了get()和set()方法,如果用synchronized,则代码如下:

  1. class syncData {
  2. private int data;// 共享数据
  3. public synchronized void set(int data) {
  4. System.out.println(Thread.currentThread().getName() + "准备写入数据");
  5. try {
  6. Thread.sleep(20);
  7. } catch (InterruptedException e) {
  8. e.printStackTrace();
  9. }
  10. this.data = data;
  11. System.out.println(Thread.currentThread().getName() + "写入" + this.data);
  12. }
  13. public synchronized  void get() {
  14. System.out.println(Thread.currentThread().getName() + "准备读取数据");
  15. try {
  16. Thread.sleep(20);
  17. } catch (InterruptedException e) {
  18. e.printStackTrace();
  19. }
  20. System.out.println(Thread.currentThread().getName() + "读取" + this.data);
  21. }
  22. }

然后写个测试类来用多个线程分别读写这个共享数据:

  1. public static void main(String[] args) {
  2. //        final Data data = new Data();
  3. final syncData data = new syncData();
  4. //        final RwLockData data = new RwLockData();
  5. //写入
  6. for (int i = 0; i < 3; i++) {
  7. Thread t = new Thread(new Runnable() {
  8. @Override
  9. public void run() {
  10. for (int j = 0; j < 5; j++) {
  11. data.set(new Random().nextInt(30));
  12. }
  13. }
  14. });
  15. t.setName("Thread-W" + i);
  16. t.start();
  17. }
  18. //读取
  19. for (int i = 0; i < 3; i++) {
  20. Thread t = new Thread(new Runnable() {
  21. @Override
  22. public void run() {
  23. for (int j = 0; j < 5; j++) {
  24. data.get();
  25. }
  26. }
  27. });
  28. t.setName("Thread-R" + i);
  29. t.start();
  30. }
  31. }

运行结果:

  1. Thread-W0准备写入数据
  2. Thread-W0写入0
  3. Thread-W0准备写入数据
  4. Thread-W0写入1
  5. Thread-R1准备读取数据
  6. Thread-R1读取1
  7. Thread-R1准备读取数据
  8. Thread-R1读取1
  9. Thread-R1准备读取数据
  10. Thread-R1读取1
  11. Thread-R1准备读取数据
  12. Thread-R1读取1
  13. Thread-R1准备读取数据
  14. Thread-R1读取1
  15. Thread-R2准备读取数据
  16. Thread-R2读取1
  17. Thread-R2准备读取数据
  18. Thread-R2读取1
  19. Thread-R2准备读取数据
  20. Thread-R2读取1
  21. Thread-R2准备读取数据
  22. Thread-R2读取1
  23. Thread-R2准备读取数据
  24. Thread-R2读取1
  25. Thread-R0准备读取数据 //R0和R2可以同时读取,不应该互斥!
  26. Thread-R0读取1
  27. Thread-R0准备读取数据
  28. Thread-R0读取1
  29. Thread-R0准备读取数据
  30. Thread-R0读取1
  31. Thread-R0准备读取数据
  32. Thread-R0读取1
  33. Thread-R0准备读取数据
  34. Thread-R0读取1
  35. Thread-W1准备写入数据
  36. Thread-W1写入18
  37. Thread-W1准备写入数据
  38. Thread-W1写入16
  39. Thread-W1准备写入数据
  40. Thread-W1写入19
  41. Thread-W1准备写入数据
  42. Thread-W1写入21
  43. Thread-W1准备写入数据
  44. Thread-W1写入4
  45. Thread-W2准备写入数据
  46. Thread-W2写入10
  47. Thread-W2准备写入数据
  48. Thread-W2写入4
  49. Thread-W2准备写入数据
  50. Thread-W2写入1
  51. Thread-W2准备写入数据
  52. Thread-W2写入14
  53. Thread-W2准备写入数据
  54. Thread-W2写入2
  55. Thread-W0准备写入数据
  56. Thread-W0写入4
  57. Thread-W0准备写入数据
  58. Thread-W0写入20
  59. Thread-W0准备写入数据
  60. Thread-W0写入29

现在一切都看起来很好!各个线程互不干扰!等等。。读取线程和写入线程互不干扰是正常的,但是两个读取线程是否需要互不干扰??

对!读取线程不应该互斥!

我们可以用读写锁ReadWriteLock实现:

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

  1. class Data {
  2. private int data;// 共享数据
  3. private ReadWriteLock rwl = new ReentrantReadWriteLock();
  4. public void set(int data) {
  5. rwl.writeLock().lock();// 取到写锁
  6. try {
  7. System.out.println(Thread.currentThread().getName() + "准备写入数据");
  8. try {
  9. Thread.sleep(20);
  10. } catch (InterruptedException e) {
  11. e.printStackTrace();
  12. }
  13. this.data = data;
  14. System.out.println(Thread.currentThread().getName() + "写入" + this.data);
  15. } finally {
  16. rwl.writeLock().unlock();// 释放写锁
  17. }
  18. }
  19. public void get() {
  20. rwl.readLock().lock();// 取到读锁
  21. try {
  22. System.out.println(Thread.currentThread().getName() + "准备读取数据");
  23. try {
  24. Thread.sleep(20);
  25. } catch (InterruptedException e) {
  26. e.printStackTrace();
  27. }
  28. System.out.println(Thread.currentThread().getName() + "读取" + this.data);
  29. } finally {
  30. rwl.readLock().unlock();// 释放读锁
  31. }
  32. }
  33. }

测试结果:

  1. Thread-W1准备写入数据
  2. Thread-W1写入9
  3. Thread-W1准备写入数据
  4. Thread-W1写入24
  5. Thread-W1准备写入数据
  6. Thread-W1写入12
  7. Thread-W0准备写入数据
  8. Thread-W0写入22
  9. Thread-W0准备写入数据
  10. Thread-W0写入15
  11. Thread-W0准备写入数据
  12. Thread-W0写入6
  13. Thread-W0准备写入数据
  14. Thread-W0写入13
  15. Thread-W0准备写入数据
  16. Thread-W0写入0
  17. Thread-W2准备写入数据
  18. Thread-W2写入23
  19. Thread-W2准备写入数据
  20. Thread-W2写入24
  21. Thread-W2准备写入数据
  22. Thread-W2写入24
  23. Thread-W2准备写入数据
  24. Thread-W2写入17
  25. Thread-W2准备写入数据
  26. Thread-W2写入11
  27. Thread-R2准备读取数据
  28. Thread-R1准备读取数据
  29. Thread-R0准备读取数据
  30. Thread-R0读取11
  31. Thread-R1读取11
  32. Thread-R2读取11
  33. Thread-W1准备写入数据
  34. Thread-W1写入18
  35. Thread-W1准备写入数据
  36. Thread-W1写入1
  37. Thread-R0准备读取数据
  38. Thread-R2准备读取数据
  39. Thread-R1准备读取数据
  40. Thread-R2读取1
  41. Thread-R2准备读取数据
  42. Thread-R1读取1
  43. Thread-R0读取1
  44. Thread-R1准备读取数据
  45. Thread-R0准备读取数据
  46. Thread-R0读取1
  47. Thread-R2读取1
  48. Thread-R2准备读取数据
  49. Thread-R1读取1
  50. Thread-R0准备读取数据
  51. Thread-R1准备读取数据
  52. Thread-R0读取1
  53. Thread-R2读取1
  54. Thread-R1读取1
  55. Thread-R0准备读取数据
  56. Thread-R1准备读取数据
  57. Thread-R2准备读取数据
  58. Thread-R1读取1
  59. Thread-R2读取1
  60. Thread-R0读取1

与互斥锁定相比,读-写锁定允许对共享数据进行更高级别的并发访问。虽然一次只有一个线程(writer 线程)可以修改共享数据,但在许多情况下,任何数量的线程可以同时读取共享数据(reader 线程)

从理论上讲,与互斥锁定相比,使用读-写锁定所允许的并发性增强将带来更大的性能提高。

在实践中,只有在多处理器上并且只在访问模式适用于共享数据时,才能完全实现并发性增强。——例如,某个最初用数据填充并且之后不经常对其进行修改的 collection,因为经常对其进行搜索(比如搜索某种目录),所以这样的 collection 是使用读-写锁定的理想候选者。

4、线程间通信Condition

Condition可以替代传统的线程间通信,用await()替换wait(),用signal()替换notify(),用signalAll()替换notifyAll()。

——为什么方法名不直接叫wait()/notify()/nofityAll()?因为Object的这几个方法是final的,不可重写!

传统线程的通信方式,Condition都可以实现。

注意,Condition是被绑定到Lock上的,要创建一个Lock的Condition必须用newCondition()方法。

Condition的强大之处在于它可以为多个线程间建立不同的Condition

看JDK文档中的一个例子:假定有一个绑定的缓冲区,它支持 put 和 take 方法。如果试图在空的缓冲区上执行take 操作,则在某一个项变得可用之前,线程将一直阻塞;如果试图在满的缓冲区上执行 put 操作,则在有空间变得可用之前,线程将一直阻塞。我们喜欢在单独的等待 set 中保存put 线程和take 线程,这样就可以在缓冲区中的项或空间变得可用时利用最佳规划,一次只通知一个线程。可以使用两个Condition 实例来做到这一点。

——其实就是java.util.concurrent.ArrayBlockingQueue的功能

  1. class BoundedBuffer {
  2. final Lock lock = new ReentrantLock();          //锁对象
  3. final Condition notFull  = lock.newCondition(); //写线程锁
  4. final Condition notEmpty = lock.newCondition(); //读线程锁
  5. final Object[] items = new Object[100];//缓存队列
  6. int putptr;  //写索引
  7. int takeptr; //读索引
  8. int count;   //队列中数据数目
  9. //写
  10. public void put(Object x) throws InterruptedException {
  11. lock.lock(); //锁定
  12. try {
  13. // 如果队列满,则阻塞<写线程>
  14. while (count == items.length) {
  15. notFull.await();
  16. }
  17. // 写入队列,并更新写索引
  18. items[putptr] = x;
  19. if (++putptr == items.length) putptr = 0;
  20. ++count;
  21. // 唤醒<读线程>
  22. notEmpty.signal();
  23. } finally {
  24. lock.unlock();//解除锁定
  25. }
  26. }
  27. //读
  28. public Object take() throws InterruptedException {
  29. lock.lock(); //锁定
  30. try {
  31. // 如果队列空,则阻塞<读线程>
  32. while (count == 0) {
  33. notEmpty.await();
  34. }
  35. //读取队列,并更新读索引
  36. Object x = items[takeptr];
  37. if (++takeptr == items.length) takeptr = 0;
  38. --count;
  39. // 唤醒<写线程>
  40. notFull.signal();
  41. return x;
  42. } finally {
  43. lock.unlock();//解除锁定
  44. }
  45. }

优点:

假设缓存队列中已经存满,那么阻塞的肯定是写线程,唤醒的肯定是读线程,相反,阻塞的肯定是读线程,唤醒的肯定是写线程。

那么假设只有一个Condition会有什么效果呢?缓存队列中已经存满,这个Lock不知道唤醒的是读线程还是写线程了,如果唤醒的是读线程,皆大欢喜,如果唤醒的是写线程,那么线程刚被唤醒,又被阻塞了,这时又去唤醒,这样就浪费了很多时间。

时间: 2024-10-16 20:51:02

【Java线程】锁机制:synchronized、Lock、Condition的相关文章

Java 线程锁机制 -Synchronized Lock 互斥锁 读写锁

synchronized 是互斥锁: lock 更广泛,包含了读写锁 读写锁特点: 1)多个读者可以同时进行读2)写者必须互斥(只允许一个写者写,也不能读者写者同时进行)3)写者优先于读者(一旦有写者,则后续读者必须等待,唤醒时优先考虑写者) 互斥锁特点: 一次只能一个线程拥有互斥锁,其他线程只有等待 所谓互斥锁, 指的是一次最多只能有一个线程持有的锁. 在jdk1.5之前, 我们通常使用synchronized机制控制多个线程对共享资源的访问. 而现在, Lock提供了比synchronize

[置顶] 深入探析Java线程锁机制

今天在iteye上提了一个关于++操作和线程安全的问题,一位朋友的回答一言点醒梦中人,至此我对Java线程锁有了更加深刻的认识.在这里也做个总结供大家参考. 先看几段代码吧! 代码一: [java] view plaincopy public class TestMultiThread2 implements Runnable{ private static Object o = new Object(); private static Integer si = 0; private stati

【转载】Java中的锁机制 synchronized &amp; Lock

参考文章: http://blog.csdn.net/chen77716/article/details/6618779 目前在Java中存在两种锁机制:synchronized和Lock,Lock接口及其实现类是JDK5增加的内容,其作者是大名鼎鼎的并发专家Doug Lea.本文并不比较synchronized与Lock孰优孰劣,只是介绍二者的实现原理. 数据同步需要依赖锁,那锁的同步又依赖谁?synchronized给出的答案是在软件层面依赖JVM,而Lock给出的方案是在硬件层面依赖特殊的

java的锁机制——synchronized

转自:http://blog.csdn.net/yangzhijun_cau/article/details/6432216 一段synchronized的代码被一个线程执行之前,他要先拿到执行这段代码的权限,在java里边就是拿到某个同步对象的锁(一个对象只有一把锁): 如果这个时候同步对象的锁被其他线程拿走了,他(这个线程)就只能等了(线程阻塞在锁池等待队列中). 取到锁后,他就开始执行同步代码(被synchronized修饰的代码):线程执行完同步代码后马上就把锁还给同步对象,其他在锁池中

java的锁机制

一段synchronized的代码被一个线程执行之前,他要先拿到执行这段代码的权限,在java里边就是拿到某个同步对象的锁(一个对象只有一把锁): 如果这个时候同步对象的锁被其他线程拿走了,他(这个线程)就只能等了(线程阻塞在锁池等待队列中). 取到锁后,他就开始执行同步代码(被synchronized修饰的代码):线程执行完同步代码后马上就把锁还给同步对象,其他在锁池中等待的某个线程就可以拿到锁执行同步代码了.这样就保证了同步代码在统一时刻只有一个线程在执行. 众所周知,在Java多线程编程中

JAVA线程安全之synchronized关键字的正确用法

JAVA线程安全关于synchronized关键字的用法,今天才知道原来我一直错了.以为用了synchronized关键字包住了代码就可以线程同步安全了. 测试了下.发现是完全的错了.synchronized必须正确的使用才是真正的线程安全...虽然知道这种写法,一直以为却由于懒而用了错误的方法. 看来基础还没有打好.仍需复习加强!工作中犯这种错误是不可原谅的,要知道使用synchronized关键字的地方都是数据敏感的!汗一把... 先贴代码: [java] view plaincopypri

Java的锁机制--synchronsized关键字

引言 高并发环境下,多线程可能需要同时访问一个资源,并交替执行非原子性的操作,很容易出现最终结果与期望值相违背的情况,或者直接引发程序错误. 举个简单示例,存在一个初始静态变量count=0,两个线程分别对count进行100000次加1操作,期望的结果是200000,实际是这样的吗?写个程序跑下看看: 1234567891011121314151617181920212223242526272829303132333435363738 public class CountWithoutSyn

java 同步锁(synchronized)

java 同步锁(synchronized) 在java中,Synchronized就是一把锁,他可以锁定一个方法,也可以锁定一个方法,我擦,其实这两个东西就是一样的.块不就是一个没有名字的方法么,方法就是一个有名字的块.本文就用块来测试.所谓锁,就是原子操作,把这个锁定的块作为一个整体,就像你上厕所,拉了就要擦屁屁,当然你也可以不擦,如果你不在意出现的问题的话.信号量Semaphore和这个Synchronized 其实实现的功能差不多,不过效率不同,使用的方式也不同.Synchronized

Java线程同步(synchronized)——卖票问题

卖票问题通常被用来举例说明线程同步问题,在Java中,采用关键字synchronized关键字来解决线程同步的问题. Java任意类型的对象都有一个标志位,该标志位具有0,1两种状态,其开始状态为1,当某个线程执行了synchronized(object)语句后,object对象的标志位变为0状态,直到执行完整个synchronized语句中的代码块后,该对象的标志位又回到1状态. 当一个线程执行到synchronized(object)语句的时候,先检查object对象的标志位,如果为0状态,

java的线程同步机制synchronized关键字的理解

线程同步:               由于同一进程的多个线程共享同一片存储空间,在带来方便的同时,也带来了访问冲突这个严重的问题.Java语言提供了专门机制以解决这种冲突,有效避免了同一个数据对象被多个线程同时访问. 需要明确的几个问题: 1)synchronized关键字可以作为函数的修饰符,也可作为函数内的语句,也就是平时说的同步方法和同步语句块.如果 再细的分类,synchronized可作用于instance变量.object reference(对象引用).static函数和clas