`
BrokenDreams
  • 浏览: 249076 次
  • 性别: Icon_minigender_1
  • 来自: 北京
博客专栏
68ec41aa-0ce6-3f83-961b-5aa541d59e48
Java并发包源码解析
浏览量:97973
社区版块
存档分类
最新评论

Java与锁的一些简单总结

阅读更多

Java与锁的一些简单总结

作者:大飞

 

  • 前言
       从开始写Java到现在,从开始不知道锁是什么,怎么用,更不知道为什么要用。到现在能够在必要的场景下正确的使用一些锁。这过程中经历了对锁的不断尝试和理解,这篇文章就来做一下Java里面关于锁的一些简单的总结。
       有错误的地方请指正,有没提到的内容请补充!
 
  • 在Java中,什么情况下需要使用锁?
       首先我们可以回顾一下之前的编码经历,什么情况下要使用锁呢?抛开那些加锁方法和代码块,我们按照内存分布的方式来理解下,如果多个程序会访问同一块儿内存中的数据,这种情况下可能需要加锁。这块儿共享内存可以对应到我们Java中的全局变量、共享资源等等,总之就是大家(Java线程)共用的。
 
       先举一个反例,看看下面的方法:
	public static String getString(){
		StringBuilder builder = new StringBuilder();
		builder.append('s');
		builder.append('h');
		builder.append('i');
		builder.append('t');
		return builder.toString();
	}
       首先我们在方法中创建了一个实例,这个实例被分配到Java堆内存中。我们知道在Java内存分布中,栈内存是每个线程各自私有的,而堆内存是所有线程共用的,所以上面程序中的builder对应的对象处于所有线程共用的内存区域中,理论上可能被多个线程访问到。
       但是,仔细思考一下,尽管builder对象被分配到了堆内存,但builder这个引用(类似句柄)只有在当前方法中有效。而且多个线程调用这个方法时,每个线程都会去new一个实例,都会有自己的builder引用(引用存在于线程的栈中),所以对于每个线程来说,只有它自己可以访问在堆内存中分配的builder,所以builder是不共享资源,上面的方法不需要加锁。
       延伸:
       这种情况也称为栈封闭,可以认为是线程安全的,不需要加锁。而且,这种情况下new的对象实例一定会分配在堆内存里面么?JVM中可能采取一些优化手段,比如逃逸分析(Escape Analysis),基于逃逸分析可能会进行栈上分配。也就是说,JVM会检测到,builder这个引用的生命周期只存在于上述方法范围中,不可能逃逸到方法外,所以可能就会直接将builder引用指向的对象分配到栈上了。
 
       上面我们看到的是不存在共享资源的情况下,不需要锁的例子。下面再看一个存在共享资源的例子:
	public static final Map<String, String> MAP;
	
	static{
		Map<String, String> temp = new HashMap<String, String>();
		temp.put("a", "1");
		temp.put("b", "2");
		MAP = Collections.unmodifiableMap(temp);
	}
	
	public static String getName(){
		String px = "tim-";
		return px + MAP.get("a");
	}
       很明显MAP是一个全局的共享资源,但是getName方法仍然不用加锁,为什么呢?因为虽然MAP是共享资源,但是它是不可变的,可能有多个线程访问它,但没有线程会修改它,所以这里也不需要加锁。
       延伸:
       我们会将这种不可变的域定义为常量,也就是说整个程序的运行过程中都不会去修改(写)这个常量。但是,就算是常量也要初始化吧,初始化也是写的过程,那怎么保证在初始化的时候不会有其他Java线程来读这个常量呢?所以一般常量都会由final修饰,可以保证安全发布(也就是说初始化过程中不会被其他线程读到),更多信息可以看下JSR-133中关于final域的重排规则。
 
       最后再看一个例子:
	public static final Map<String, String> MAP_U;
	
	static{
		Map<String, String> temp = new HashMap<String, String>();
		temp.put("a", "1");
		temp.put("b", "2");
		MAP_U = Collections.synchronizedMap(temp);
	}
	
	public static String getNameU(){
		String px = "tim-";
		return px + MAP_U.get("a");
	}
       很明显MAP_U是一个全局的共享资源,而且是可变的,我们不能保证其他线程不去修改MAP_U中的数据,所以访问这个共享数据的时候需要加锁。尽管我们方法中没有显示加锁,但MAP_U是一个线程安全的Map,get方法中已经加锁。
       延伸:
       尽管上面的MAP_U是线程安全的Map,但在某个复合操作下(比如判断没有则添加)还得额外加锁,如果需要原子的复合操作,请参见ConcurrenMap接口中提供的一些原子复合操作。
 
       总结一下:当存在共享资源,且有线程会修改(写)这个共享资源时,那么对这个共享资源的访问(读写)都需要加锁。
 
  • 如果获取锁失败,会有那些行为?
       废话,获取锁失败后当然要等待了。
       但具体怎么等待呢?有哪些细节?
       我们知道Java中有很多锁,具体的等待细节也有所不同,但大体上等待方式可分两种:自旋等待阻塞等待。从较底层的层面来说,自旋等待相当于当前的程序(进程)还在被调度执行,处理器还会执行程序的指令,但是这些指令表达的意思都是一直在不断(循环)尝试获取锁,直到获取成功,所以自旋等待也称为忙等待;而阻塞等待相当于当前程序不会被调度器调度了,处理器也不在执行它的指令了,一直到其他程序释放了锁,将其唤醒,它才会去再次尝试获取锁。
       延伸:
       Java线程一般都会映射到操作系统的进程,比如在Linux平台,Java线程会映射到Linux的线程(轻量级进程)。在Linux内核中,进程会由调度器来进行调度。这里简单说下CFS调度器,系统中所有可执行的进程在linux内核中组成一棵红黑树,CFS会从红黑树选择进程来调度。当进程阻塞后,会被从红黑树中转移到等待队列中,直到进程被唤醒,再次从等待队列中转移到红黑树中,才有可能再次被调度。
 
       总结一下:如果程序获取锁失败,无外乎两种情况:程序继续被调度执行(不断重试);程序阻塞,不会被调度。
 
  • 具体的锁是什么?
       前面了解了锁的作用和一些行为,那么具体的锁是什么呢?
       我们通过看一些Java中的锁机制来体会一下,首先最常用的就是synchronized关键字。
       synchronized关键字给我们更多的感觉是语法层面的同步,只要有这个关键字,方法或者代码块儿中的代码就是线程安全的。但具体的锁是什么??
       先看两个synchronized例子:
	private Map<String, String> cache = new HashMap<>();
	public synchronized void put(String k, String v){
		cache.put(k, v);
	}
	
	public void putV(String k, String v){
		synchronized (cache) {
			cache.put(k, v);
		}
	}
       其实我们关注的锁,是一个对象,这里就叫锁对象吧。我们知道,用synchronized修饰实例方法的话,就相当于synchronized(this);修饰静态方法的话,就相当于synchronized(this.class)。可见,synchronized相关的代码最后都可以归结为是synchronized(Object)的形式,那么这个Object其实就是锁对象。
       一般锁对象可以是我们要访问的共享资源对象本身,也可以是专门定义的一个锁对象,总之得是一个公共的对象,所有访问其保护资源的线程都能访问到的对象。
       好吧,看看下面这个程序有什么问题:
	public void putVV(String k, String v){
		Object lock = new Object();
		synchronized (lock) {
			cache.put(k, v);
		}
	}
     
 
       其次,ReentrantLock也是比较常用的锁机制。
       相比synchronized关键字,ReentrantLock更容易理解。它本身就是一个锁(锁对象),我们会自然而然的创建好这个锁对象,然后执行加锁解锁等操作,不会像使用synchronized那样有时候不知道自己在干啥。
 
       最后,在Java中有时候会使用一些基于CAS操作的自旋锁机制。
       这些操作其实也是基于对一个数值的CAS等操作来进行加锁解锁过程,这个数值就相当于是锁对象。
 
       总结一下:具体的锁可以看成是一个对象,这个对象会被能访问由锁保护资源的所有线程访问到。
 
  • Java中提供了哪些锁?
       synchronized:
       内置锁,可重入。内部做了一些细致的优化,获取锁过程为:偏向锁->轻量级锁->自旋锁->重量级锁。
       ReentrantLock:
       基于AQS的可重入锁,比synchronized更加灵活。
       ReentrantReadWriteLock:
       基于AQS的可重入的读写锁。
       SequenceLock:
       基于AQS的可重入的顺序锁,在乐观读方法上要比ReentrantReadWriteLock高效一些。(这个类在jsr166e的extra包里发现,但貌似没出现在jdk里)
       StampedLock:
       不可重入的优化读写锁,针对乐观读做了优化,一般用于构建内部并发组件。
       cas:
       程序中可以通过CAS操作来构建一些锁,比如jdk1.7中ForkJoin框架中使用的scanGuard包含的顺序锁、jdk1.8中Striped64的cellsBusy锁等。
       基于AQS构建的同步机制:
       这些同步机制和锁机制也有着很多联系。
 
       总结一下:Java中提供了各种各样的锁,合适的场景使用合适的锁。
 
  • 使用锁有哪些注意事项?
       1.正确的使用锁。
          该用的时候用,不该用的时候不用。不要因为确定不了是否会出现并发问题,就把所有的方法都加上锁,要仔细分析可能出现并发的地方,在需要的时候加锁;也不要意识不到并发问题,让一些被共享的资源在无锁保护下裸奔,常见的比如使用一个公共的Random实例。
       2.使用合适的锁。
          前面我们简单介绍了那么多锁,在实际使用时要使用合适的锁。
       3.锁的一些优化。
          尽量减少锁的范围,值锁可能产生并发问题的代码;尽量减少锁的粒度,避免一些不必要的竞争,比如ConcurrentHashMap中的方式;按照实际情况控制锁行为,比如实际等待锁的时间很短(就是说等待时间比上下文切换时间还短),就没有必要阻塞,可以自旋一下。如果自旋超过一定次数,都让其进入阻塞状态,避免消耗过多的处理器资源。
 
  • JVM在这方面做了哪些优化?
       1.锁消除。
       看个例子:
	public static String getStr(){
		StringBuffer buffer = new StringBuffer();
		buffer.append("1");
		buffer.append("2");
		buffer.append("3");
		return buffer.toString();
	}
       前面已经提到过,这种情况属于栈封闭。同时我们知道,StringBuffer是一个线程安全的类,所有方法都是同步的。但很明显,这里的同步都是不必要的,所以JVM很可能会将代码中产生同步相关指令消除掉。
 
       2.锁粗化。
       看个例子:
	public static StringBuffer getStr(){
		StringBuffer buffer = new StringBuffer();
		buffer.append("1");
		buffer.append("2");
		buffer.append("3");
		return buffer;
	}
       这种情况下,很明显buffer已经逃逸到方法外,没办法进行锁消除。但里面的3个append方法都会各自加锁解锁,实际上加一次锁(包含3个append)也可以,所以JVM很可能会将代码中的3个加锁解锁操作合并成一个。
 
       3.偏向锁。
       简单的说,就是在JVM内部,如果一个对象作为synchronized的锁对象,当一个线程获取这个锁时,会将线程id保存到这个锁对象的对象头上,当紧接着下一次申请获取这个锁的线程还是之前的线程时,只需要比较对象头中的线程id,不需要做其他锁相关的操作了。
 
 
       OK!简单总结到这里。  
分享到:
评论
3 楼 liangzi4454 2016-01-13  
liangzi4454 写道
上边写这段代码:public static final Map<String, String> MAP;  编译无法通过,因为变量声明为final后,必须进行初始化
,这里是我看错了,在下边初始化的,不好意思
2 楼 liangzi4454 2016-01-13  
上边写这段代码:public static final Map<String, String> MAP;  编译无法通过,因为变量声明为final后,必须进行初始化
1 楼 pcgreat 2015-12-24  
写的不错 学到些东西

相关推荐

    java 面试题 总结

    20、EJB与JAVA BEAN的区别? Java Bean 是可复用的组件,对Java Bean并没有严格的规范,理论上讲,任何一个Java类都可以是一个Bean。但通常情况下,由于Java Bean是被容器所创建(如Tomcat)的,所以Java Bean应具有...

    java多线程核心技术

    结合大量实例,全面讲解Java多线程编程中的并发访问、线程间通信、锁等最难突破的核心技术与应用实践 Java多线程无处不在,如服务器、数据库、应用。多线程可以有效提升计算和处理效率,大大提升吞吐量和可伸缩性,...

    java concurrent source code

    结合大量实例,全面讲解Java多线程编程中的并发访问、线程间通信、锁等最难突破的核心技术与应用实践 封底 Java多线程无处不在,如服务器、数据库、应用。多线程可以有效提升计算和处理效率,大大提升吞吐量和可...

    Java并发编程原理与实战

    线程安全性问题简单总结.mp4 线程之间的通信之wait notify.mp4 通过生产者消费者模型理解等待唤醒机制.mp4 Condition的使用及原理解析.mp4 使用Condition重写waitnotify案例并实现一个有界队列.mp4 深入解析...

    java 多线程设计模式 进程详解

    简单的同步例子 一个网络服务器类 AsyncInputStream类 使用TCPServer和AsynclnputStream 总结 第六章 Java线程调度 线程调度概述 何时调度是重要的 调度和线程优先级 常见的调度实现 本地调度支持 其他线程调度...

    Java中volatile关键字的总结.docx

    Java 语言包含两种内在的同步机制:同步块(或方法)和 volatile 变量,相比于synchronized(synchronized通常称为重量级锁),volatile更轻量级,因为它不会引起线程上下文的切换和调度。但是volatile 变量的同步性...

    龙果java并发编程完整视频

    第28节线程安全性问题简单总结00:15:34分钟 | 第29节线程之间的通信之wait/notify00:32:12分钟 | 第30节通过生产者消费者模型理解等待唤醒机制00:20:50分钟 | 第31节Condition的使用及原理解析00:17:40分钟 | 第...

    JAVA入门1.2.3:一个老鸟的JAVA学习心得 PART1(共3个)

    11.2.5 总结:类的修饰符 297 11.3 方法的修饰符 297 11.3.1 方法的访问控制符 298 11.3.2 public:没有限制的修饰符 299 11.3.3 protected:仅对子类和同包的类可见 300 11.3.4 默认控制符:仅在本包中可见 ...

    龙果 java并发编程原理实战

    第28节线程安全性问题简单总结00:15:34分钟 | 第29节线程之间的通信之wait/notify00:32:12分钟 | 第30节通过生产者消费者模型理解等待唤醒机制00:20:50分钟 | 第31节Condition的使用及原理解析00:17:40分钟 | 第...

    Java入门1·2·3:一个老鸟的Java学习心得.PART3(共3个)

    11.2.5 总结:类的修饰符 297 11.3 方法的修饰符 297 11.3.1 方法的访问控制符 298 11.3.2 public:没有限制的修饰符 299 11.3.3 protected:仅对子类和同包的类可见 300 11.3.4 默认控制符:仅在本包中可见 ...

    整理后java开发全套达内学习笔记(含练习)

    synchronized (关键字) 同步(锁) ['siŋkrәnaiz] Thread [java] 线程 [θred] throw (关键字) throws (关键字) [θrәu] 抛出(异常) transient (关键字) 瞬变;临时的['trænziәnt]'(可序列化) valid 正确的,...

    Java 并发编程原理与实战视频

    第28节线程安全性问题简单总结00:15:34分钟 | 第29节线程之间的通信之wait/notify00:32:12分钟 | 第30节通过生产者消费者模型理解等待唤醒机制00:20:50分钟 | 第31节Condition的使用及原理解析00:17:40分钟 | 第...

    个人总结的深入java多线程开发

    看完《think in java》多线程章节,自己写的多线程文档,还结合了其他的相关网络资料。 线程 一. 线程池 1)为什么要使用线程池 2 2)一个具有线程池的工作队列 3 3)使用线程池的风险: 4 4)有效使用线程池的原则 5...

    涵盖了90%以上的面试题

    Java中的异常处理机制的简单原理和应用。 java socket java序列化 JVM加载class文件的原理 双亲委派模型 为什么要自定义类加载器 如何自定义类加载器 什么是GC 内存泄漏和内存溢出 Java的内存模型(JVM的内存划分) ...

    java并发编程

    第28节线程安全性问题简单总结00:15:34分钟 | 第29节线程之间的通信之wait/notify00:32:12分钟 | 第30节通过生产者消费者模型理解等待唤醒机制00:20:50分钟 | 第31节Condition的使用及原理解析00:17:40分钟 | 第...

    2018年面试实战总结.zip

    2018/10/06 周六 下午 17:17 97,111 分布式锁简单入门以及三种实现方式介 绍.docx 2019/04/29 周一 下午 15:58 18,097 图灵群面试题.docx 2018/09/12 周三 上午 11:21 52,736 工厂模式实现.doc 2018/10/03 周三 下午...

    Java 虚拟机面试题全面解析(干货)

    Java 虚拟机面试题全面解析,《深入理解Java虚拟机》干货版,自己总结,希望能够帮助大家,免费下载~什么是类加载机制? 虚拟机和物理机的区别是什么? 运行时栈帧结构 Java方法调用 什么是方法调用? Java的方法调用,...

    java面试题,180多页,绝对良心制作,欢迎点评,涵盖各种知识点,排版优美,阅读舒心

    【数据库】乐观锁与悲观锁的区别 107 【数据库】数据库的三范式 107 【数据库】inner/left/right/full join的区别 109 【数据库】哪些字段该添加索引,哪些不添加? 109 【数据库】分页查询语句怎么写? 110 Mysql...

Global site tag (gtag.js) - Google Analytics