并发编程之Java锁

1、重入锁

锁做为并发共享数据,保证一致性的工具,在JAVA平台有多种实现(如 synchronized(重量级) 和 ReentrantLock(轻量级)等等 ) 。这些已经写好提供的锁为咱们开发提供了便利。java

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

在JAVA环境下 ReentrantLock 和synchronized 都是 可重入锁算法

synchronized:sql

public class Test implements Runnable {
	public  synchronized void get() {
		System.out.println("name:" + Thread.currentThread().getName() + " get();");
	}

	public synchronized  void set() {
		System.out.println("name:" + Thread.currentThread().getName() + " set();");
		get();
	}

	@Override
	public void run() {
		set();
	}

	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();
	}
}

ReentrantLock:数据库

public class Test02 extends Thread {
	ReentrantLock lock = new ReentrantLock();
	public void get() {
		try{
			lock.lock();
			System.out.println(Thread.currentThread().getId());
		}catch (Exception e){

		}finally {
			lock.unlock();
		}
	}
	public void set() {
		try{
			lock.lock();
			System.out.println(Thread.currentThread().getId());
			get();
		}catch (Exception e){

		}finally {
			lock.unlock();
		}
	}
	@Override
	public void run() {
		set();
	}
	public static void main(String[] args) {
		Test ss = new Test();
		new Thread(ss).start();
		new Thread(ss).start();
		new Thread(ss).start();
	}

}

正常状况下在函数自行完毕后会释放锁,以上两种方式均在已得到锁的函数中从新调用了须要锁的函数,若是没有可重入性,那么调用新的须要锁的函数时,将会形成死锁。可重用锁在递归调用中很是重要。编程

2、读写锁

程序中涉及到对一些共享资源的读和写操做,且写操做没有读操做那么频繁。在没有写操做的时候,两个线程同时读一个资源没有任何问题,因此应该容许多个线程能在同时读取共享资源。安全

可是若是有一个线程想去写这些共享资源,就不该该再有其它线程对该资源进行读或写(也就是说:读-读能共存,读-写不能共存,写-写不能共存)。数据结构

这就须要一个读/写锁来解决这个问题。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) {
		
		try {
			r.lock();
			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) {
		
		try {
			w.lock();
			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() {
		try {
			w.lock();
			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();
	}
}

3、悲观锁、乐观锁

一、乐观锁

老是认为不会产生并发问题,每次去取数据的时候总认为不会有其余线程对数据进行修改,所以不会上锁,可是在更新时会判断其余线程在这以前有没有对数据进行修改,通常会使用版本号机制或CAS操做实现。ide

version方式:通常是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程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,涉及到三个操做数,数据所在的内存值,预期值,新值。当须要更新时,判断当前内存值与以前取到的值是否相等,若相等,则用新值更新,若失败则重试,通常状况下是一个自旋操做,即不断的重试。

二、悲观锁

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

4、原子类

java.util.concurrent.atomic包:原子类的小工具包,支持在单个变量上解除锁的线程安全编程

原子变量类至关于一种泛化的 volatile 变量,可以支持原子的和有条件的读-改-写操做。

AtomicInteger 表示一个int类型的值,并提供了 get 和 set 方法,这些 Volatile 类型的int变量在读取和写入上有着相同的内存语义。它还提供了一个原子的 compareAndSet 方法(若是该方法成功执行,那么将实现与读取/写入一个 volatile 变量相同的内存效果),以及原子的添加、递增和递减等方法。AtomicInteger 表面上很是像一个扩展的 Counter 类,但在发生竞争的状况下能提供更高的可伸缩性,由于它直接利用了硬件对并发的支持。

为何会有原子类 CAS:Compare and Swap,即比较再交换。

jdk5增长了并发包java.util.concurrent.*,其下面的类使用CAS算法实现了区别于synchronouse同步锁的一种乐观锁。相似于mysql中的version字段的乐观锁

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

  • AtomicBoolean
  • AtomicInteger
  • AtomicLong
  • AtomicReference

5、CAS无锁模式

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

一、CAS算法理解

  • (1)与锁相比,使用比较交换(下文简称CAS)会使程序看起来更加复杂一些。但因为其非阻塞性,它对死锁问题天生免疫,而且,线程间的相互影响也远远比基于锁的方式要小。更为重要的是,使用无锁的方式彻底没有锁竞争带来的系统开销,也没有线程间频繁调度带来的开销,所以,它要比基于锁的方式拥有更优越的性能。
  • (2)无锁的好处: 第一,在高并发的状况下,它比有锁的程序拥有更好的性能; 第二,它天生就是死锁免疫的。
  • (3)CAS算法的过程是这样:它包含三个参数CAS(V,E,N): V表示要更新的变量,E表示预期值,N表示新值。仅当V值等于E值时,才会将V的值设为N,若是V值和E值不一样,则说明已经有其余线程作了更新,则当前线程什么都不作。最后,CAS返回当前V的真实值。 (V表示要更新的变量 主线程的值 主内存,E表示预期值 本地内存 工做内存,N表示新值)
  • (4)CAS操做是抱着乐观的态度进行的,它老是认为本身能够成功完成操做。当多个线程同时使用CAS操做一个变量时,只有一个会胜出,并成功更新,其他均会失败。失败的线程不会被挂起,仅是被告知失败,而且容许再次尝试,固然也容许失败的线程放弃操做。基于这样的原理,CAS操做即便没有锁,也能够发现其余线程对当前线程的干扰,并进行恰当的处理。
  • (5)简单地说,CAS须要你额外给出一个指望值,也就是你认为这个变量如今应该是什么样子的。若是变量不是你想象的那样,那说明它已经被别人修改过了。你就从新读取,再次尝试修改就行了。
  • (6)在硬件层面,大部分的现代处理器都已经支持原子化的CAS指令。在JDK 5.0之后,虚拟机即可以使用这个指令来实现并发操做和并发数据结构,而且,这种操做在虚拟机中能够说是无处不在。

二、CAS缺点

CAS存在一个很明显的问题,即ABA问题。

问题:若是变量V初次读取的时候是A,而且在准备赋值的时候检查到它仍然是A,那能说明它的值没有被其余线程修改过了吗?

若是在这段期间曾经被改为B,而后又改回A,那CAS操做就会误认为它历来没有被修改过。针对这种状况,java并发包中提供了一个带有标记的原子引用类AtomicStampedReference,它能够经过控制变量值的版原本保证CAS的正确性。

我的博客 蜗牛

相关文章
相关标签/搜索