了解Java中的锁,看这一篇就够了!

1 Lock接口

锁是用来控制多个线程访问同一个共享资源的方式,通常来讲,一个锁能防止多个线程同时访问共享资源,在Lock接口出来以前,Java是经过synchronized关键字来实现锁的功能,而Java1.5以后,并发包新增了Lock接口(以及相关实现类)用来实现锁的功能,它提供了与synchronized关键字相似的同步功能,只是在使用方式上有所不一样,须要显式的获取锁和释放锁。虽然缺乏了隐式的便捷性,但却拥有了锁获取和释放的可操做性,可中断的获取因此及超时获取锁的的同步特性java

1.1 Lock接口提供的synchronized不具有的特性:

特性 描述
尝试非阻塞式获取锁 当前线程尝试获取锁,若是这一刻没有被其余线程获取到,则成功获取并持有锁
能被中断的获取锁 与synchronized关键字不一样,获取到锁的线程可以响应中断,当获取到锁的线程被中断时,中断异常会被抛出,同时锁会被释放
超时获取锁 在指定的截止时间以前获取到锁,入伙截止时间到了仍旧没法获取锁,则返回

1.2 Lock接口 API

方法名称 描述
void lock() 获取锁,调用该方法当前线程将会获取锁,当锁获取到时,从该方法返回
void lockInterruptibly() throws InterruptedException() 可中断的获取锁,和lock()方法的不一样之处在于该方法可响应中断,即在锁的获取中和中断当前线程
boolean tryLock() 尝试非阻塞的获取锁,调用该方法马上返回,若是可以获取则返回true,不然返回false
boolean tryLock(long time,TimeUnit unit) throws InterruptedException() 超时的获取锁,当前线程在如下三种状况会返回:1.当前线程在超时时间内获取到锁 2. 当前线程在超时时间内被中断 3. 超时时间结束,返回false
void unlock() 释放锁
Condition newCondition() 获取等待通知组件,该组件和当前的锁绑定,当前线程只有得到了锁,才能调用该组件的wait()方法,而调用后,当前线程释放锁

1.3 AbstractQueueSynchronized(队列同步器)

如下简称AQS AQS是用来构建锁和其余同步组件的基础框架,它使用了一个int成员变量表示同步状态,经过内置的FIFO队列来完成资源获取线程排队工做redis

1.3.1 AQS API

AQS给予模板方法设计模式设计的,也就是说,使用者须要继承AQS并重写指定的方法进行实现算法

AQS提供以下三个方法来访问和修改同步状态:数据库

  • getState():获取当前线程的同步状态。
  • getState(int newState):设置当前线程同步状态。
  • compareAndSetState(int expect,int update):使用CAS设置当前状态,该方法可以保证状态设置的原子性

1.3.2 AQS可重写的方法

方法名称 描述
protected boolean tryAcquire(int arg) 独占式的获取同步状态,实现该方法须要查询当前状态并判断同步状态是否符合预期,而后再进行CAS设置同步状态
protected boolean tryRelease(int arg) 独占式释放同步状态,等待获取同步状态的线程将有机会获取同步状态
protected int tryAcquireShared(int arg) 共享式获取同步状态,返回大于等于0的值,表示获取成功,反之获取失败
protected boolean tryReleaseShared(int arg) 共享式释放同步状态
protected boolean isHeldExclusively() 当前同步器是否在独占模式下被线程占用,通常该方法表示是否被当前线程所独占

1.3.3 AQS提供的模板方法

方法名称 描述
void acquire(int arg) 独占式获取同步状态,若是当前线程获取同步状态成功,则该方法返回,不然,将进入同步队列等待,该方法将会调用重写的tryAcquire(int arg)方法
void acquireInterruptibly(int arg) 与acquire(int arg)相同,可是该方法响应中断,当前线程未获取到同步状态而进入同步队列中,若是当前线程被中断,则该方法抛出异常并返回
boolean tryAcquireNanos(int arg,long nanos) 在acquireInterruptibly(int arg)基础上增长了超时限制,若是当前线程在超时时间内没有获取到同步状态,那么将会返回false,若是获取到了则返回true
void acquireShared(int arg) 共享式的获取同步状态,若是当前线程未获取到同步状态,将会进入同步队列中进行等待,与独占式的区别主要在于同一时刻能够有多个线程获取到同步状态
void acquireSharedInterruptibly(int arg) 与acquireInterruptibly(int arg)相同,该方法可响应中断
boolean tryAcquireSharedNanos(int arg,long nanos) 在acquireSharedInterruptibly(int arg)基础上增长了超时限制
boolean release(int arg) 独占式的释放同步状态,该方法会在释放同步状态以后,将同步队列中的第一个节点包含的线程唤醒
boolean releaseShared(int arg) 共享式的释放同步状态
Collection getQueueThreads() 获取等待在同步队列上的线程集合

1.3.4 独占锁和共享锁的区别

  • 独占锁,顾名思义,就是在同一时刻只能有一个线程获取到锁,而其余的线程只能在同步队列中等待,只有获取锁的线程释放了锁,后继线程才能获取到锁
  • 共享锁就是在同一时刻能够有多个线程获取锁
  • 独享锁与共享锁也是经过AQS来实现的,经过实现不同的方法,来实现独享或者共享

2 常见的锁

2.1 重入锁

重入锁,也叫作递归锁,指的是同一线程外层函数得到锁以后,内层递归函数仍然有获取该锁的代码,但不受影响。编程

在JAVA环境下ReentrantLock和sypnchronized都是可重入锁设计模式

public class Test implements Runnable {
	public synchronized void get() {
		System.out.println("name:" + Thread.currentThread().getName() + " get();");
		set();
	}
	public synchronized void set() {
		System.out.println("name:" + Thread.currentThread().getName() + " set();");
	}
	@Override
	public void run() {
		get();
	}
	public static void main(String[] args) {
		Test ss = new Test();
		new Thread(ss).start();
		new Thread(ss).start();
		new Thread(ss).start();
		new Thread(ss).start();
	}
}
复制代码
public class Test02 extends Thread {
	ReentrantLock lock = new ReentrantLock();
	public void get() {
		lock.lock();
		System.out.println(Thread.currentThread().getId());
		set();
		lock.unlock();
	}
	public void set() {
		lock.lock();
		System.out.println(Thread.currentThread().getId());
		lock.unlock();
	}
	@Override
	public void run() {
		get();
	}
	public static void main(String[] args) {
		Test ss = new Test();
		new Thread(ss).start();
		new Thread(ss).start();
		new Thread(ss).start();
	}
}
复制代码

2.2 读写锁

相比Java中的锁(Locks in Java)里Lock实现,读写锁更复杂一些。假设你的程序中涉及到对一些共享资源的读和写操做,且写操做没有读操做那么频繁。在没有写操做的时候,两个线程同时读一个资源没有任何问题,因此应该容许多个线程能在同时读取共享资源。可是若是有一个线程想去写这些共享资源,就不该该再有其它线程对该资源进行读或写(译者注:也就是说:读-读能共存,读-写不能共存,写-写不能共存)。 这就须要一个读/写锁来解决这个问题。Java5在java.util.concurrent包中已经包含了读写锁。尽管如此,咱们仍是应该了解其实现背后的原理。缓存

public class Cache {
	static Map<String, Object> map = new HashMap<String, Object>();
	static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
	static Lock r = rwl.readLock();
	static Lock w = rwl.writeLock();
	// 获取一个key对应的value
	public static final Object get(String key) {
		r.lock();
		try {
			System.out.println("正在作读的操做,key:" + key + " 开始");
			Thread.sleep(100);
			Object object = map.get(key);
			System.out.println("正在作读的操做,key:" + key + " 结束");
			System.out.println();
			return object;
		} catch (InterruptedException e) {
		} finally {
			r.unlock();
		}
		return key;
	}
	// 设置key对应的value,并返回旧有的value
	public static final Object put(String key, Object value) {
		w.lock();
		try {
			System.out.println("正在作写的操做,key:" + key + ",value:" + value + "开始.");
			Thread.sleep(100);
			Object object = map.put(key, value);
			System.out.println("正在作写的操做,key:" + key + ",value:" + value + "结束.");
			System.out.println();
			return object;
		} catch (InterruptedException e) {
		} finally {
			w.unlock();
		}
		return value;
	}
	// 清空全部的内容
	public static final void clear() {
		w.lock();
		try {
			map.clear();
		} finally {
			w.unlock();
		}
	}
	public static void main(String[] args) {
		new Thread(new Runnable() {
			@Override
			public void run() {
				for (int i = 0; i < 10; i++) {
					Cache.put(i + "", i + "");
				}
			}
		}).start();
		new Thread(new Runnable() {
			@Override
			public void run() {
				for (int i = 0; i < 10; i++) {
					Cache.get(i + "");
				}
			}
		}).start();
	}
}
复制代码

2.3 乐观锁

老是认为不会产生并发问题,每次去取数据的时候总认为不会有其余线程对数据进行修改,所以不会上锁,可是在更新时会判断其余线程在这以前有没有对数据进行修改,通常会使用版本号机制或CAS操做实现,本质没有锁,效率比较高,无阻塞,无等待,重试安全

实现方式服务器

  • version方式:通常是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加1。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,不然重试更新操做,直到更新成功。 核心SQL语句
update table set x=x+1, version=version+1 where id=#{id} and version=#{version};
复制代码
  • CAS操做方式:即compare and swap 或者 compare and set,涉及到三个操做数,数据所在的内存值,预期值,新值。当须要更新时,判断当前内存值与以前取到的值是否相等,若相等,则用新值更新,若失败则重试,通常状况下是一个自旋操做,即不断的重试。

2.4 悲观锁

老是假设最坏的状况,每次取数据时都认为其余线程会修改,因此都会加锁(读锁、写锁、行锁等),当其余线程想要访问数据时,都须要阻塞挂起。能够依靠数据库实现,如行锁、读锁和写锁等,都是在操做以前加锁,在Java中,synchronized的思想也是悲观锁。属于重量级锁,会阻塞,会等待并发

2.5 synchronized

  • 优势
  1. 具备可重入性,保证原子性和可见性
  • 缺点
  1. 锁的本质是重量级锁,开销大,不能禁止重排序,产生阻塞,效率低下

2.6 分布式锁

若是想在不一样的jvm中保证数据同步,使用分布式锁技术。有数据库实现、缓存redis实现、Zookeeper分布式锁

2.7 自旋锁和互斥锁的区别

  • 自旋锁(Spin lock) 自旋锁与互斥锁有点相似,只是自旋锁不会引发调用者睡眠,若是自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是 否该自旋锁的保持者已经释放了锁,"自旋"一词就是所以而得名。其做用是为了解决某项资源的互斥使用。由于自旋锁不会引发调用者睡眠,因此自旋锁的效率远 高于互斥锁。虽然它的效率比互斥锁高,可是它也有些不足之处:
  1. 自旋锁一直占用CPU,他在未得到锁的状况下,一直运行--自旋,因此占用着CPU,若是不能在很短的时 间内得到锁,这无疑会使CPU效率下降。
  2. 在用自旋锁时有可能形成死锁,当递归调用时有可能形成死锁,调用有些其余函数也可能形成死锁,如 copy_to_user()、copy_from_user()、kmalloc()等。

所以咱们要慎重使用自旋锁,自旋锁只有在内核可抢占式或SMP的状况下才真正须要,在单CPU且不可抢占式的内核下,自旋锁的操做为空操做。自旋锁适用于锁使用者保持锁时间比较短的状况下。

  • 两种锁的加锁原理
  1. 互斥锁:线程会从sleep(加锁)——>running(解锁),过程当中有上下文的切换,cpu的抢占,信号的发送等开销。
  2. 自旋锁:线程一直是running(加锁——>解锁),死循环检测锁的标志位,机制不复杂。 互斥锁属于sleep-waiting类型的锁。例如在一个双核的机器上有两个线程(线程A和线程B),它们分别运行在Core0和 Core1上。假设线程A想要经过pthread_mutex_lock操做去获得一个临界区的锁,而此时这个锁正被线程B所持有,那么线程A就会被阻塞 (blocking),Core0 会在此时进行上下文切换(Context Switch)将线程A置于等待队列中,此时Core0就能够运行其余的任务(例如另外一个线程C)而没必要进行忙等待。而自旋锁则否则,它属于busy-waiting类型的锁,若是线程A是使用pthread_spin_lock操做去请求锁,那么线程A就会一直在 Core0上进行忙等待并不停的进行锁请求,直到获得这个锁为止。
  • 两种锁的区别 互斥锁的起始原始开销要高于自旋锁,可是基本是一劳永逸,临界区持锁时间的大小并不会对互斥锁的开销形成影响,而自旋锁是死循环检测,加锁全程消耗cpu,起始开销虽然低于互斥锁,可是随着持锁时间,加锁的开销是线性增加。
  • 两种锁的应用 互斥锁用于临界区持锁时间比较长的操做,好比下面这些状况均可以考虑
  1. 临界区有IO操做
  2. 临界区代码复杂或者循环量大
  3. 临界区竞争很是激烈
  4. 单核处理器

至于自旋锁就主要用在临界区持锁时间很是短且CPU资源不紧张的状况下,自旋锁通常用于多核的服务器。

2.8 公平锁和非公平锁的区别

非公平锁:在等待锁的过程当中,若是有人以新的线程妄图获取锁,都是有很大概率直接获取到锁的。白话文:公平锁是先到先得,按序进行,非公平锁就是不排队直接拿,失败再说。

3 CAS

Compare and Swap,即比较再交换。jdk5增长了并发包java.util.concurrent.*,其下面的类使用CAS算法实现了区别于synchronouse同步锁的一种乐观锁。JDK 5以前Java语言是靠synchronized关键字保证同步的,这是一种独占锁,也是是悲观锁。

  • CAS算法理解

与锁相比,使用比较交换(下文简称CAS)会使程序看起来更加复杂一些。但因为其非阻塞性,它对死锁问题天生免疫,而且,线程间的相互影响也远远比基于锁的方式要小。更为重要的是,使用无锁的方式彻底没有锁竞争带来的系统开销,也没有线程间频繁调度带来的开销,所以,它要比基于锁的方式拥有更优越的性能。

  • 无锁的好处:
  1. 在高并发的状况下,它比有锁的程序拥有更好的性能;
  2. 它天生就是死锁免疫的。
  • 优势 效率比较高,无阻塞,无等待,重试
  • 缺点:
  1. 会产生ABA问题:由于CAS须要在操做值的时候,检查值有没有发生变化,若是没有变化则更新,可是若是一个值原来是A,变成了B,又变成了A,那么CAS检查时发现它的值没有发生变化,但实际上发生了变化:A->B->A的过程
  2. 循环时间长,开销大:自旋CAS若是长时间不成功,会给CPU带来很大的执行开销
  3. 只能保证一个共享变量的原子操做:当对一个共享变量操做时,咱们能够采用CAS的方式来保证原子操做,可是对多个共享变量操做时,循环CAS就没法保证操做的原子性

4 原子类

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

若是同一个变量要被多个线程访问,则可使用该包中的类 AtomicBoolean AtomicInteger AtomicLong AtomicReference

相关文章
相关标签/搜索