一、重入锁

  • 锁作为并发共享数据,保证一致性的工具,在JAVA平台有多种实现,如synchronized(重量级) 和 ReentrantLock(轻量级)等等,这些已经写好提供的锁为我们开发提供了便利。
  • 重入锁,也叫做递归锁,指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。
  • 在JAVA环境下 ReentrantLock 和synchronized 都是 可重入锁

  • ReentrantLock

  1. public class TL005_Reentrant extends Thread {
  2. ReentrantLock lock = new ReentrantLock();
  3. public void get() {
  4. lock.lock();
  5. System.out.println(Thread.currentThread().getId());
  6. set();
  7. lock.unlock();
  8. }
  9. public void set() {
  10. lock.lock();
  11. System.out.println(Thread.currentThread().getId());
  12. lock.unlock();
  13. }
  14. @Override
  15. public void run() {
  16. get();
  17. }
  18. public static void main(String[] args) {
  19. TL005_Reentrant ss = new TL005_Reentrant();
  20. new Thread(ss).start();
  21. new Thread(ss).start();
  22. new Thread(ss).start();
  23. }
  24. }
  • synchronized
  1. public class Test_Reentrant implements Runnable {
  2. public synchronized void get() {
  3. System.out.println("name:" + Thread.currentThread().getName() + " get();");
  4. set();
  5. }
  6. public synchronized void set() {
  7. System.out.println("name:" + Thread.currentThread().getName() + " set();");
  8. }
  9. @Override
  10. public void run() {
  11. get();
  12. }
  13. public static void main(String[] args) {
  14. Test_Reentrant ss = new Test_Reentrant();
  15. new Thread(ss).start();
  16. new Thread(ss).start();
  17. new Thread(ss).start();
  18. new Thread(ss).start();
  19. }
  20. }

二、读写锁

  • 相比Java中的锁Lock实现,读写锁更复杂一些。假设你的程序中涉及到对一些共享资源的读和写操作,且写操作没有读操作那么频繁。在没有写操作的时候,两个线程同时读一个资源没有任何问题,所以应该允许多个线程能在同时读取共享资源。但是如果有一个线程想去写这些共享资源,就不应该再有其它线程对该资源进行读或写(也就是说:读-读能共存,读-写不能共存,写-写不能共存)。这就需要一个读/写锁来解决这个问题。Java5在java.util.concurrent包中已经包含了读写锁。

  • 示例如下:

  1. import java.util.HashMap;
  2. import java.util.Map;
  3. import java.util.concurrent.locks.Lock;
  4. import java.util.concurrent.locks.ReentrantReadWriteLock;
  5. public class Cache {
  6. static Map<String, Object> map = new HashMap<String, Object>();
  7. static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
  8. static Lock r = rwl.readLock();
  9. static Lock w = rwl.writeLock();
  10. // 获取一个key对应的value
  11. public static final Object get(String key) {
  12. r.lock();
  13. try {
  14. System.out.println("正在做读的操作,key:" + key + " 开始");
  15. Thread.sleep(100);
  16. Object object = map.get(key);
  17. System.out.println("正在做读的操作,key:" + key + " 结束");
  18. System.out.println();
  19. return object;
  20. } catch (InterruptedException e) {
  21. } finally {
  22. r.unlock();
  23. }
  24. return key;
  25. }
  26. // 设置key对应的value,并返回旧有的value
  27. public static final Object put(String key, Object value) {
  28. w.lock();
  29. try {
  30. System.out.println("正在做写的操作,key:" + key + ",value:" + value + "开始.");
  31. Thread.sleep(100);
  32. Object object = map.put(key, value);
  33. System.out.println("正在做写的操作,key:" + key + ",value:" + value + "结束.");
  34. System.out.println();
  35. return object;
  36. } catch (InterruptedException e) {
  37. } finally {
  38. w.unlock();
  39. }
  40. return value;
  41. }
  42. // 清空所有的内容
  43. public static final void clear() {
  44. w.lock();
  45. try {
  46. map.clear();
  47. } finally {
  48. w.unlock();
  49. }
  50. }
  51. public static void main(String[] args) {
  52. new Thread(new Runnable() {
  53. @Override
  54. public void run() {
  55. for (int i = 0; i < 10; i++) {
  56. Cache.put(i + "", i + "");
  57. }
  58. }
  59. }).start();
  60. new Thread(new Runnable() {
  61. @Override
  62. public void run() {
  63. for (int i = 0; i < 10; i++) {
  64. Cache.get(i + "");
  65. }
  66. }
  67. }).start();
  68. }
  69. }

三、悲观锁、乐观锁

3.1 悲观锁

  • 总是假设最坏的情况,每次取数据时都认为其他线程会修改,所以都会加锁(读锁、写锁、行锁等),当其他线程想要访问数据时,都需要阻塞挂起。可以依靠数据库实现,如行锁、读锁和写锁等,都是在操作之前加锁,在Java中,synchronized的思想也是悲观锁。

3.2 乐观锁

  • 总是认为不会产生并发问题,每次去取数据的时候总认为不会有其他线程对数据进行修改,因此不会上锁,但是在更新时会判断其他线程在这之前有没有对数据进行修改,一般会使用版本号机制或CAS操作实现。
  • version方式:一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。
  • 核心SQL语句
  1. update table set x=x+1, version=version+1 where id=#{id} and version=#{version};

3.3 CAS操作方式

  • CAS:Compare and Swap,即比较再交换。
  • 即compare and swap 或者 compare and set,涉及到三个操作数,数据所在的内存值,预期值,新值。当需要更新时,判断当前内存值与之前取到的值是否相等,若相等,则用新值更新,若失败则重试,一般情况下是一个自旋操作,即不断的重试。
  • jdk5增加了并发包java.util.concurrent.*,其下面的类使用CAS算法实现了区别于synchronouse同步锁的一种乐观锁。JDK 5之前Java语言是靠synchronized关键字保证同步的,这是一种独占锁,也是是悲观锁。

3.4 CAS算法理解

  • (1)与锁相比,使用CAS会使程序看起来更加复杂一些。但由于其非阻塞性,它对死锁问题天生免疫,并且,线程间的相互影响也远远比基于锁的方式要小。更为重要的是,使用无锁的方式完全没有锁竞争带来的系统开销,也没有线程间频繁调度带来的开销,因此,它要比基于锁的方式拥有更优越的性能。
  • (2)无锁的好处:
    • 第一,在高并发的情况下,它比有锁的程序拥有更好的性能;
    • 第二,它天生就是死锁免疫的。
    • 就凭借这两个优势,就值得我们冒险尝试使用无锁的并发。
  • (3)CAS算法的过程是这样:它包含三个参数CAS(V,E,N): V表示要更新的变量,E表示预期值,N表示新值。仅当V值等于E值时,才会将V的值设为N,如果V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。最后,CAS返回当前V的真实值。
  • (4)CAS操作是抱着乐观的态度进行的,它总是认为自己可以成功完成操作。当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。基于这样的原理,CAS操作即使没有锁,也可以发现其他线程对当前线程的干扰,并进行恰当的处理。
  • (5)简单地说,CAS需要你额外给出一个期望值,也就是你认为这个变量现在应该是什么样子的。如果变量不是你想象的那样,那说明它已经被别人修改过了。你就重新读取,再次尝试修改就好了。
  • (6)在硬件层面,大部分的现代处理器都已经支持原子化的CAS指令。在JDK 5.0以后,虚拟机便可以使用这个指令来实现并发操作和并发数据结构,并且,这种操作在虚拟机中可以说是无处不在。

3.5 CAS(乐观锁算法)

  • CAS比较与交换的伪代码可以表示为:
  1. do{
  2. 备份旧数据;
  3. 基于旧数据构造新数据;
  4. }while(!CAS( 内存地址,备份的旧数据,新数据 ))
  • CPU去更新一个值,但如果想改的值不再是原来的值,操作就失败,因为很明显,有其它操作先改变了这个值。
  • 就是指当两者进行比较时,如果相等,则证明共享数据没有被修改,替换成新值,然后继续往下运行;如果不相等,说明共享数据已经被修改,放弃已经所做的操作,然后重新执行刚才的操作。容易看出 CAS 操作是基于共享数据不会被修改的假设,采用了类似于数据库的 commit-retry 的模式。当同步冲突出现的机会很少时,这种假设能带来较大的性能提升。
  1. public final int getAndAddInt(Object o, long offset, int delta){
  2. int v;
  3. do {
  4. v = getIntVolatile(o, offset);
  5. } while (!compareAndSwapInt(o, offset, v, v + delta));
  6. return v;
  7. }
  8. /**
  9. * Atomically increments by one the current value.
  10. *
  11. * @return the updated value
  12. */
  13. public final int incrementAndGet() {
  14. for (;;) {
  15. //获取当前值
  16. int current = get();
  17. //设置期望值
  18. int next = current + 1;
  19. //调用Native方法compareAndSet,执行CAS操作
  20. if (compareAndSet(current, next))
  21. //成功后才会返回期望值,否则无线循环
  22. return next;
  23. }
  24. }

3.6 CAS缺点

  • CAS存在一个很明显的问题,即ABA问题。
  • 问题:如果变量V初次读取的时候是A,并且在准备赋值的时候检查到它仍然是A,那能说明它的值没有被其他线程修改过了吗?
  • 如果在这段期间曾经被改成B,然后又改回A,那CAS操作就会误认为它从来没有被修改过。针对这种情况,java并发包中提供了一个带有标记的原子引用类AtomicStampedReference,它可以通过控制变量值的版本来保证CAS的正确性。

四、原子类

4.1 概述

  • java.util.concurrent.atomic包:原子类的小工具包,支持在单个变量上解除锁的线程安全编程
  • 原子变量类相当于一种泛化的 volatile 变量,能够支持原子的和有条件的读-改-写操作。AtomicInteger 表示一个int类型的值,并提供了 get 和 set 方法,这些 Volatile 类型的int变量在读取和写入上有着相同的内存语义。它还提供了一个原子的 compareAndSet 方法(如果该方法成功执行,那么将实现与读取/写入一个 volatile 变量相同的内存效果),以及原子的添加、递增和递减等方法。AtomicInteger 表面上非常像一个扩展的 Counter 类,但在发生竞争的情况下能提供更高的可伸缩性,因为它直接利用了硬件对并发的支持。

  • CAS:Compare and Swap,即比较再交换。

  • jdk5增加了并发包java.util.concurrent.*,其下面的类使用CAS算法实现了区别于synchronouse同步锁的一种乐观锁。JDK 5之前Java语言是靠synchronized关键字保证同步的,这是一种独占锁,也是是悲观锁。

  • 如果同一个变量要被多个线程访问,则可以使用该包中的类

4.2 常用原子类

  • Java中的原子操作类大致可以分为4类:原子更新基本类型、原子更新数组类型、原子更新引用类型、原子更新属性类型。这些原子类中都是用了无锁的概念,有的地方直接使用CAS操作的线程安全的类型。

  • 1、基本类型,使用原子的方式更新基本类型

    • AtomicInteger:整型原子类
    • AtomicLong:长整型原子类
    • AtomicBoolean :布尔型原子类
  • 2、数组类型,使用原子的方式更新数组里的某个元素

    • AtomicIntegerArray:整型数组原子类
    • AtomicLongArray:长整型数组原子类
    • AtomicReferenceArray :引用类型数组原子类
  • 3、引用类型

    • AtomicReference:引用类型原子类
    • AtomicReferenceFieldUpdater:原子更新引用类型里的字段
    • AtomicMarkableReference :原子更新带有标记位的引用类型
  • 4、对象的属性修改类型

    • AtomicIntegerFieldUpdater:原子更新整型字段的更新器
    • AtomicLongFieldUpdater:原子更新长整型字段的更新器
    • AtomicStampedReference :原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。
    • AtomicMarkableReference:原子更新带有标记的引用类型。该类将 boolean 标记与引用关联起来,也可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。
  • 以AtomicInteger为例:

  1. import java.util.concurrent.atomic.AtomicInteger;
  2. public class Test_AtomicInteger implements Runnable {
  3. private static Integer count = 1;
  4. private static AtomicInteger atomic = new AtomicInteger();
  5. @Override
  6. public void run() {
  7. while (true) {
  8. int count = getCountAtomic();
  9. System.out.println(count);
  10. if (count >= 150) {
  11. break;
  12. }
  13. }
  14. }
  15. public synchronized Integer getCount() {
  16. try {
  17. Thread.sleep(50);
  18. } catch (Exception e) {
  19. }
  20. return count++;
  21. }
  22. public Integer getCountAtomic() {
  23. try {
  24. Thread.sleep(50);
  25. } catch (Exception e) {
  26. // TODO: handle exception
  27. }
  28. return atomic.incrementAndGet();
  29. }
  30. public static void main(String[] args) {
  31. Test_AtomicInteger test0001 = new Test_AtomicInteger();
  32. Thread t1 = new Thread(test0001);
  33. Thread t2 = new Thread(test0001);
  34. t1.start();
  35. t2.start();
  36. }
  37. }

五、分布式锁

  • 如果想在不同的jvm中保证数据同步,使用分布式锁技术。
  • 有数据库实现、缓存实现、Zookeeper分布式锁

【Java并发】锁机制的更多相关文章

  1. 深入理解 Java 并发锁

    本文以及示例源码已归档在 javacore 一.并发锁简介 确保线程安全最常见的做法是利用锁机制(Lock.sychronized)来对共享数据做互斥同步,这样在同一个时刻,只有一个线程可以执行某个方 ...

  2. Java的锁机制--synchronsized关键字

    引言 高并发环境下,多线程可能需要同时访问一个资源,并交替执行非原子性的操作,很容易出现最终结果与期望值相违背的情况,或者直接引发程序错误. 举个简单示例,存在一个初始静态变量count=0,两个线程 ...

  3. DB2默认的事务及并发锁机制

    今天有点时间,试验了一下DB2的并发锁机制,结果,和MSSQL的差不多:1.DB2的缺省行为,事务以可执行的SQL开始,以COMMIT或ROLLBACK结束:2.DB2缺省是否提交,以工具的不同而不同 ...

  4. java的锁机制

    一段synchronized的代码被一个线程执行之前,他要先拿到执行这段代码的权限,在Java里边就是拿到某个同步对象的锁(一个对象只有一把锁): 如果这个时候同步对象的锁被其他线程拿走了,他(这个线 ...

  5. Java常用锁机制简介

    在开发Java多线程应用程序中,各个线程之间由于要共享资源,必须用到锁机制.Java提供了多种多线程锁机制的实现方式,常见的有synchronized.ReentrantLock.Semaphore. ...

  6. lesson3:java的锁机制原理和分析

    jdk1.5之前,我们对代码加锁(实际是对象加锁),都是采用Synchronized关键字来处理,jdk1.5及以后的版本中,并发编程大师Doug Lea在concurrrent包中提供了Lock机制 ...

  7. 深入浅出 Java Concurrency 锁机制 : AQS

    转载:http://www.blogjava.net/xylz/archive/2010/07/06/325390.html 在理解J.U.C原理以及锁机制之前,我们来介绍J.U.C框架最核心也是最复 ...

  8. [java多线程] - 锁机制&同步代码块&信号量

    在美眉图片下载demo中,我们可以看到多个线程在公用一些变量,这个时候难免会发生冲突.冲突并不可怕,可怕的是当多线程的情况下,你没法控制冲突.按照我的理解在java中实现同步的方式分为三种,分别是:同 ...

  9. java的锁机制——synchronized

    一段synchronized的代码被一个线程执行之前,他要先拿到执行这段代码的权限,在java里边就是拿到某个同步对象的锁(一个对象只有一把锁): 如果这个时候同步对象的锁被其他线程拿走了,他(这个线 ...

  10. Redis学习笔记~Redis并发锁机制

    回到目录 redis客户端驱动有很多,如ServiceStack.Redis,StackExchange.Redis等等,下面我使用ServiceStack.Redis为例,介绍一下在redis驱动中 ...

随机推荐

  1. javaweb大文件上传

    本文主要关于利用html表单上传文件的后台代码实现. 需要用到两个工具类Apache commons-fileupload和commons-io. 注意要校验是否选择文件上传,最开始写的时候没有加上校 ...

  2. 将ByteBuffer保存成文件

    String dest = "d:/download/" + name; Path path = Paths.get(dest).getParent().toAbsolutePat ...

  3. js模块化编程之彻底弄懂CommonJS和AMD/CMD

    转自 http://www.cnblogs.com/chenguangliang/p/5856701.html

  4. 在Windows操作系统中安装MongoDB

    如何在Windows操作系统中安装MongoDB: https://docs.mongodb.com/manual/tutorial/install-mongodb-on-windows/ 启动Mon ...

  5. TOMCAT 安装教程 & 配置CGI & c语言exe

    TOMCAT安装 参考原文网址:百度经验http://jingyan.baidu.com/article/154b4631aad2bb28ca8f4191.html 1.下载安装JDK 网址:http ...

  6. C#规范整理·异常与自定义异常

    这里会列举在C#中处理CLR异常方面的规范,帮助大家构建和开发一个运行良好和可靠的应用系统. 前言   迄今为止,CLR异常机制让人关注最多的一点就是"效率"问题.其实,这里存在认 ...

  7. PTA(Advanced Level)1033.To Fill or Not to Fill

    With highways available, driving a car from Hangzhou to any other city is easy. But since the tank c ...

  8. 【AtCoder】AGC001

    AGC001 A - BBQ Easy 从第\(2n - 1\)个隔一个加一下加到1即可 #include <bits/stdc++.h> #define fi first #define ...

  9. Block Breaker HDU - 6699(深搜,水,写下涨涨记性)

    Problem Description Given a rectangle frame of size n×m. Initially, the frame is strewn with n×m squ ...

  10. 实现文件上下文管理(__enter__和___exit__)

    实现文件上下文管理(__enter__和__exit__) 我们知道在操作文件对象的时候可以这么写 with open('a.txt') as f: '代码块' 上述叫做上下文管理协议,即with语句 ...