Synchronized
Synchronized 的两种用法
- 对象锁
1.1 方法锁
1.2 同步代码块锁 - 类锁
2.1 静态方法锁
2.1 锁对象为Class
对象锁用法
同步代码块锁
Object lock1 = new Object();
Object lock2 = new Object();
synchronized(lock1){
...
}
synchronized(lock2){
...
}
同一把锁会互斥。 不同的锁会同步
方法锁
public synchronized void methon(){
}
synchronized 直接修饰普通方法,锁对象默认是this。 普通方法不包括静态方法
类锁
概念:
1) java 有很多对象,但只有一个Class对象
2) 本质,所谓的类锁,不过是Class对象的锁而已
3) 类锁只能在同一时刻被一个对象拥有
1. 形式1: synchronized 加在static方法上
public static synchornized void methon(){
...
}
这个方法在全局上同步。
2. 形式2: synchronized(*.class) 代码块
synchronized(x.class){
...
}
同步代码块中直接添加class对象,不同的实例依然要串行的执行
synchronized 的缺陷
- 锁的释放情况少,锁的释放只能等待当前线程释放锁,或者出现异常时由JVM释放。其他情况synchronized都不会释放锁,遇到线程阻塞或者io阻塞,会导致其他线程等待时间过长,整体效率就低。
- 不能设置超时时间,synchronized不能设置持有锁的超时时间。
- 不能中断,不能中断一个线程获取锁。
- 不够灵活,加锁和释放锁的时机单一,每个锁仅有单一的条件。(读写锁更灵活,读的时候不加锁,写的时候加锁。)
- 无法知道是否成功的获取了锁。(lock可以知晓是否成功获取锁,成功了做什么,失败了做什么)
lock锁
public static void main(String[] args) {
Lock lock = new ReentrantLock();
lock.lock(); // 加锁
lock.unlock(); // 释放锁
try {
boolean isTake = lock.tryLock(); // 尝试获取锁,true 获得,false 没有获得
lock.tryLock(1000, TimeUnit.SECONDS); // 在设置的时间内去获取这把锁,超时则放弃
}catch (Exception e){
e.printStackTrace();
}
}
锁的核心思想
- 一把锁只能同时被一个线程获取,没有拿到锁的线程必须等待。
- 每个实例都对应有自己的一把锁,不同实例之间互不影响; 例外: 锁对象是*.class以及synchronized修饰的是static方法的时候,所有对象共用同一把锁
- 无论方法正常执行完毕或者抛出异常,都会释放锁
synchronized使用的注意点
synchronized使用的注意点:
1、锁的信息是保存在对象头中的、作用域不易过大,影响性能、避免死锁
2、如何选择Lock和synchronized关键字
1)建议都不使用,可以使用java.util.concurrent包中的Automic类、countDown等类
2)优先使用现成工具,如果没有就优先使用synchronized关键字,好处是写尽量少的代码就能实现功能。如果需要灵活的加解锁机制,则使用Lock接口
面试常考
-
两个线程同时访问一个对象的同步方法。
答: 此时两个线程争抢的是同一把锁,所以会相互等待。 -
两个线程访问的是两个对象的同步方法。
答: 此时两个线程争抢的不是同一把锁,不用相互等待。 -
两个线程访问的是synchronized 的静态方法。
答: 静态方法在全局上是同步的,两个线程 争抢的是同一把锁,所以会互相等待。 -
同时访问同步方法和非同步方法。
答:此时不需要争抢锁,两个方法不用互相等待。 -
访问同一个对象的不同的普通同步方法
答: 同一个对象的不同普通同步方法实际上使用的是同一个锁对象this。 争抢同一把锁,所以会互相等待。 -
同时访问静态synchronized和非静态synchronized方法
答:静态synchronized方法持有的锁是class的类锁,非静态synchronized方法持有的是this对象锁,争抢的不是同一把锁,不用互相等待。 -
抛出异常后会释放锁
答:synchronized遇到异常后,由jvm主动释放锁
基本类型注意点
在多线程中使用long 或者 double 等类型是不安全的(JVM允许将64位的读写操作分为两个32位的操作),除非用关键字volatile 或者用锁保护起来。
当且仅当满足以下所有条件时,才应该使用volatile变量:
1. 对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程能更新变量。
2. 该变量不会与其他状态变量一起纳入不变性条件中。
3. 在访问变量时不需要加锁。
注:加锁机制可以确保可见性和原子性,volatile只能确保可见性
StampedLock
java8在java.util.concurrent.locks新增的一个API。
ReentrantReadWriteLock 在沒有任何读写锁时,才可以取得写入锁,这可用于实现了悲观读取(Pessimistic Reading,悲观的任务总是有读取和写入同时发生,一定会造成写入新数据另一边读到旧数据的情况,所以悲观锁需要等待到没有读写锁时才能获取到锁,否则就一直等待),如果执行中进行读取时,经常可能有另一执行要写入的需求,为了保持同步,ReentrantReadWriteLock 的读锁可以在此时使用。然而,如果读取执行情况很多,写入很少的情况下,由于写锁需要在没有读写锁的情况下才能获得写入锁,所以写锁可能会使写入线程遭遇饥饿(Starvation)问题,也就是写入线程迟迟无法竞争到锁从而一直处于等待状态。
StampedLock控制锁有三种模式(写,读,乐观读),一个StampedLock状态是由版本和模式两个部分组成,锁获取方法返回一个数字作为票据stamp,它用相应的锁状态表示并控制访问,数字0表示没有写锁被授权访问。在读锁上分为悲观锁和乐观锁。
所谓的乐观读模式,也就是若读的操作很多,写的操作很少的情况下,你可以乐观地认为,写入与读取同时发生几率很少,因此不悲观地使用完全的读取锁定,程序可以查看读取资料之后,是否遭到写入执行的变更,再采取后续的措施(重新读取变更信息,或者抛出异常) ,这一个小小改进,可大幅度提高程序的吞吐量。