006-Java 锁的深入理解
in 001-并发编程 with 0 comment

006-Java 锁的深入理解

in 001-并发编程 with 0 comment

1 Lock接口

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

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队列来完成资源获取线程排队工作

1.3.1 AQS API

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

AQS提供如下三个方法来访问和修改同步状态:

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 独占锁和共享锁的区别

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操作实现,本质没有锁,效率比较高,无阻塞,无等待,重试

实现方式

update table set x=x+1, version=version+1 where id=#{id} and version=#{version};

2.4 悲观锁

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

2.5 synchronized

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

2.6 分布式锁

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

2.7 自旋锁和互斥锁的区别

  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上进行忙等待并不停的进行锁请求,直到得到这个锁为止。
  1. 临界区有IO操作
  2. 临界区代码复杂或者循环量大
  3. 临界区竞争非常激烈
  4. 单核处理器

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

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

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

3 CAS

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

与锁相比,使用比较交换(下文简称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