总结 synchronized
Posted 随风的浪
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了总结 synchronized相关的知识,希望对你有一定的参考价值。
目录
synchronized的特性
1. 互斥
synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到同一个对象 synchronized 就会阻塞等待.
进入 synchronized 修饰的代码块, 相当于 加锁
退出 synchronized 修饰的代码块, 相当于 解锁
所有对象都可以作为 synchronized的锁(基本数据类型对象除外).
2. 刷新内存
synchronized 的工作过程 :
- 获得互斥锁
- 从主内存拷贝变量的最新副本到工作的内存
- 执行代码
- 将更改后的共享变量的值刷新到主内存
- 释放互斥锁
synchronized 也能保证内存可见性
3. 可重入
synchronized 对同一条线程来说是可重入的,不会出现自己把自己锁死的问题.
就是在锁里面再加一把锁, 并且两把锁的锁对象都是同一个, 一般来说要想进入第二把锁就得等第一把锁将锁对象释放了, 而第一把锁想释放就得进入并走出第二把锁, 这就产生了矛盾, 产生了死锁. 而 synchronized 并没有这样的问题.
synchronized的使用
synchronized 本质上要修改指定对象的 “对象头”. 从使用角度来看, synchronized 也势必要搭配一个具体的对象来使用.
1. 直接修饰普通方法
public class SynchronizedDemo
public synchronized void methond()
2. 修饰静态方法
public class SynchronizedDemo
public synchronized static void method()
3. 修饰代码块
锁当前对象
public class SynchronizedDemo
public void method()
synchronized (this)
锁类对象
public class SynchronizedDemo
public void method()
synchronized (SynchronizedDemo.class) //通过反射来获得对象
synchronized的锁机制
基本特点
- 开始时是乐观锁, 如果锁冲突频繁, 就转换为悲观锁.
- 开始是轻量级锁实现, 如果锁被持有的时间较长, 就转换成重量级锁.
- 实现轻量级锁的时候大概率用到自旋锁策略.
- 是一种不公平锁 (产生阻塞等待时, 不是按顺序来得到锁)
- 是一种可重入锁.
- 不是读写锁.
关键锁策略 : 锁升级
偏向锁不是真的 “加锁”, 只是打上一个 “标记”, 记录这个锁属于哪个线程.
如果后续没有其他线程来竞争该锁, 那么就不用进行其他操作了(避免了加锁解锁的开销)
如果后续其他线程来竞争这把锁了, 偏向锁就升级为自旋锁(轻量级锁), 如果竞争进一步激烈, 自旋不能快速获取到锁状态, 就会变为重量级锁.
synchronized总结
synchronized基础用法
- synchronized可以用于修饰类的实例方法、静态方法和代码块。它保护的是对象(包括类对象)而非代码,只要访问的是同一个对象的synchronized方法,即使是不同的代码,也会被同步顺序访问。
- 每个对象有一个锁(又叫监视器)和一个锁等待队列,锁只能被一个线程持有,其他试图获得同样锁的线程需要等待,执行synchronized实例方法的过程大概如下:
- 尝试获得锁,如果能够获得锁,继续下一步,否则加入锁等待队列,线程的状态变为BLOCKED,阻塞并等待唤醒
- 执行被锁住的方法或者代码块
- 释放锁,如果等待队列上有等待的线程,从中取一个并唤醒,如果有多个等待的线程,唤醒哪一个是不一定的,不保证公平性
- 一般在保护变量时,需要在所有访问该变量的方法上加上synchronized。
- 任何对象都可以作为synchronized锁的对象。
理解synchronized
可重入性
可重入是指:对同一个执行线程,它在获得了锁之后,在调用其他需要同样锁的代码时,可以直接调用。
可重入是通过记录锁的持有线程和持有数量来实现的,当调用被synchronized保护的代码时,检查对象是否已被锁,如果是,再检查是否被当前线程锁定,如果是,增加持有数量,如果不是被当前线程锁定,才加入等待队列,当释放锁时,减少持有数量,当数量变为0时才释放整个锁。
内存可见性
除了保证原子操作外,synchronized还有一个重要的作用,就是保证内存可见性,在释放锁时,所有写入都会写回内存,而获得锁后,都会从内存中读最新数据。
如果只是简单地操作变量的话,可以用volatile修饰该变量,替代synchronized以减少成本。
加了volatile之后,Java会在操作对应变量时插入一个cpu指令(又叫内存栅栏),保证读写到内存最新值,而非缓存的值。
死锁
死锁就是类似这种现象,比如, 有a, b两个线程,a持有锁A,在等待锁B,而b持有锁B,在等待锁A,a,b陷入了互相等待,最后谁都执行不下去。
避免死锁的方案:
- 应该尽量避免在持有一个锁的同时去申请另一个锁,如果确实需要多个锁,所有代码都应该按照相同的顺序去申请锁。
- 使用显式锁接口Lock,它支持尝试获取锁(tryLock)和带时间限制的获取锁方法,使用这些方法可以在获取不到锁的时候释放已经持有的锁,然后再次尝试获取锁或干脆放弃,以避免死锁。
死锁检查工具:Java自带的jstack命令
同步容器及其注意事项
同步容器
Collections类有一些方法,它们可以返回线程安全的同步容器,比如:
public static <T> Collection<T> synchronizedCollection(Collection<T> c) public static <T> List<T> synchronizedList(List<T> list) public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m)
它们是给所有容器方法都加上synchronized来实现安全的。当多个线程并发访问同一个容器对象时,不需要额外的同步操作,也不会出现错误的结果。
加了synchronized,所有方法调用变成了原子操作,但是也不是就绝对安全了,比如:
复合操作,比如先检查再更新
例如:
public V putIfAbsent(K key, V value){ V old = map.get(key); if(old!=null){ return old; } map.put(key, value); return null; }
假设map的每个方法都是安全的,但这个复合方法putIfAbsent是安全的吗?显然是否定的,这是一个检查然后再更新的复合操作,在多线程的情况下,可能有多个线程都执行完了检查这一步,都发现Map中没有对应的键,然后就会都调用put,而这就破坏了putIfAbsent方法期望保持的语义。
伪同步,比如同步错对象。
那给该方法加上synchronized就能实现安全吗?如下所示:
public synchronized V putIfAbsent(K key, V value){ V old = map.get(key); if(old!=null){ return old; } map.put(key, value); return null; }
答案是否定的!为什么呢?同步错对象了。putIfAbsent同步锁住的的是当前类的对象,如果该类还存在其他操作map的实例方法的话,那么它操作map时同步锁住的是map,两者是不同的对象。随意要解决这个问题应该给map加锁,如:
public V putIfAbsent(K key, V value){ synchronized(map){ V old = map.get(key); if(old!=null){ return old; } map.put(key, value); return null; } }
迭代
对于同步容器对象,虽然单个操作是安全的,但迭代并不是。遍历的同时容器如果发生了结构性变化,就会抛出ConcurrentModificationException异常,同步容器并没有解决这个问题,如果要避免这个异常,需要在遍历的时候给整个容器对象加锁
并发容器
除了以上这些注意事项,同步容器的性能也是比较低的,当并发访问量比较大的时候性能很差。所幸的是,Java中还有很多专为并发设计的容器类,比如:
-
- CopyOnWriteArrayList
- ConcurrentHashMap
- ConcurrentLinkedQueue
- ConcurrentSkipListSet
以上是关于总结 synchronized的主要内容,如果未能解决你的问题,请参考以下文章