精华内容
参与话题
问答
  • Java锁机制

    千次阅读 2018-05-16 00:03:00
    对于熟悉java多线程并发的人来说,java锁机制是不可逃避的话题。那么什么是java锁机制,以及什么时候使用java的锁呢?让我们看看以下几种场景吧!一、同步锁案例:假设现在我们现在有很多人去商店买衣服,因为我们每...

    对于熟悉java多线程并发的人来说,java锁机制是不可逃避的话题。那么什么是java锁机制,以及什么时候使用java的锁呢?

    让我们看看以下几种场景吧!

    一、同步锁

    案例:假设现在我们现在有很多人去商店买衣服,因为我们每个人买衣服是可能同时执行的,不可能商店要求同一时刻只能有一个顾客买衣服,因此我们需要为每一个顾客买衣服设置一个线程;假设买衣服的整个过程有“挑选”、“试衣服”、“结账”三个过程,那么初步代码实现是这样的:

    public class Shopping {
    	public static void main(String[] args) {
    		Task task = new Task();
    		new Thread(task).start();
    		new Thread(task).start();
    	}
    }
    class Task implements Runnable{
    	public void run(){
    		System.out.println("顾客正在挑选衣服...");
    		try {
    			Thread.sleep(2000);
    		} catch (InterruptedException e) {
    			e.printStackTrace();
    		}
    		System.out.println("顾客正在试衣服...");
    		try {
    			Thread.sleep(2000);
    		} catch (InterruptedException e) {
    			e.printStackTrace();
    		}
    		System.out.println("顾客正在结账...");
    	}
    }

    执行结果:

    顾客正在挑选衣服...
    顾客正在挑选衣服...
    顾客正在试衣服...
    顾客正在试衣服...
    顾客正在结账...

    顾客正在结账...

    这样的结果大家是不是觉得有点别扭呢?我们挑选衣服、结账还可以一起,那没问题,但是试衣服就有点尴尬了,万一大家性别不同呢?。。。所以这时候要是有一把锁在我们换衣服的时候能够把试衣间锁上,一个人试完了再到下一个那样就不会乱套了,那么怎么做呢?其实java提供了一个关键字synchronized来修饰我们多个线程需要同步执行的代码段或方法,所谓同步就是一个一个排队干的意思,具体语法如下:

    synchronized(监视器){

        需要执行的代码

    }

    //监视器:java里面的任意一个对象,但是要求该对象对于需要同步执行的线程来说是同一个对象即可。

    那么以上程序就可以这么干了:

    public class Shopping {
    	public static void main(String[] args) {
    		Task task = new Task();
    		new Thread(task).start();
    		new Thread(task).start();
    	}
    }
    class Task implements Runnable{
    	public void run(){
    		......................................
    		synchronized(this){
    			System.out.println("顾客正在试衣服...");
    			try {
    				Thread.sleep(2000);
    			} catch (InterruptedException e) {
    				e.printStackTrace();
    			}
    		}
    		............................
    	}
    }

    结果:

    顾客正在挑选衣服...
    顾客正在挑选衣服...
    顾客正在试衣服...
    顾客正在结账...
    顾客正在试衣服...

    顾客正在结账...

    是不是发现试衣服这件事错开了呢?那么这就是java里面的同步锁机制:当一个线程执行synchronized修饰的代码段时,会给监视器上一把锁,当另一个线程执行同样的被synchronized修饰的任务时,该线程就需要关注该监视器有没有上锁,没有才能执行,否则就阻塞。

    二、互斥锁

    案例:假设有这样一个场景,对于一家人来说每个人都可以同时吃饭同时看电视,但是有一个家规就是吃饭的时候谁也不准看电视,那么怎样实现代码模拟呢?看下面:

    public class Shopping {
    	public static void main(String[] args) {
    		Task task = new Task();
    		new Thread(task).start();
    		new Thread(task).start();
    	}
    }
    class Task implements Runnable{
    	public void run(){
    		synchronized(this){
    			System.out.println("吃饭...");
    			try {
    				Thread.sleep(2000);
    			} catch (InterruptedException e) {
    				e.printStackTrace();
    			}
    		}
    		synchronized(this){
    			System.out.println("看电视...");
    			try {
    				Thread.sleep(2000);
    			} catch (InterruptedException e) {
    				e.printStackTrace();
    			}
    		}
    	}
    }

    结果:

    吃饭...
    看电视...
    吃饭...

    看电视...

    执行后会发现即使是不同的线程吃饭和看电视也是交替而不是同时执行的。这就是典型的互斥锁:当两个synchronized锁的对象(监视器)是同一个,修饰的代码片段不同时,那么这些代码片段所代表的任务是互斥的。

    三、死锁

    概念:当每个线程都持有自己的锁,但是都在请求对方释放锁时,就会造成死锁现象。

    案例:

    public class Test {
    	public static void main(String[] args) {
    		Task task = new Task();
    		Thread t1 = new Thread(){
    			public void run(){
    				task.methodA();
    			}
    		};
    		Thread t2 = new Thread(){
    			public void run(){
    				task.methodB();
    			}
    		};
    		
    		t1.start();
    		t2.start();
    	}
    }
    class Task{
    	private Object a = new Object();
    	private Object b = new Object();
    	public void run(){
    		
    	}
    	
    	public void methodA(){
    		System.out.println("开始执行A方法");
    		try {
    			Thread.sleep(2000);
    		} catch (InterruptedException e) {
    			e.printStackTrace();
    		}
    		synchronized(a){
    			System.out.println("对A方法上锁");
    			try {
    				Thread.sleep(2000);
    			} catch (InterruptedException e) {
    				e.printStackTrace();
    			}
    			System.out.println("开始调用B方法");
    			methodB();
    			try {
    				Thread.sleep(2000);
    			} catch (InterruptedException e) {
    				e.printStackTrace();
    			}
    		}
    		System.out.println("A方法执行完毕");
    	}
    	public void methodB(){
    		System.out.println("开始执行B方法");
    		try {
    			Thread.sleep(2000);
    		} catch (InterruptedException e) {
    			e.printStackTrace();
    		}
    		synchronized(b){
    			System.out.println("对B方法上锁");
    			try {
    				Thread.sleep(2000);
    			} catch (InterruptedException e) {
    				e.printStackTrace();
    			}
    			System.out.println("开始调用A方法");
    			methodA();
    			try {
    				Thread.sleep(2000);
    			} catch (InterruptedException e) {
    				e.printStackTrace();
    			}
    		}
    		System.out.println("B方法执行完毕");
    	}
    }

    结果:

    开始执行A方法
    开始执行B方法
    对A方法上锁
    对B方法上锁
    开始调用B方法
    开始执行B方法
    开始调用A方法

    开始执行A方法

    执行到这里后两个线程t1和t2会进入僵持状态,由于双方都是在持有自己锁的同时请求对方释放锁,因此这就形成了典型的死锁现象:这里是因为滥用锁对象造成的,大家可以将两个锁对象(监视器)换成同一个对象,程序就会向下继续运行,但是该程序继续向下运行并没有什么意义,这里只是为了说明死锁形成的简单场景。

    以上就是java里面典型的各种锁,有兴趣可以研究一下。

    展开全文
  • 【7】Java锁机制

    万次阅读 2019-11-26 20:49:47
    知识点3:悲观、乐观 1、场景 2、悲观与乐观 (1)悲观 (2)乐观 知识点4:原子类 1、为什么会有原子类 2、如果同一个变量要被多个线程访问,则可以使用该包中的类 3、CAS无锁模式 (1)什么...

    目录

    知识点1:重入锁

    知识点2:读写锁

    知识点3:悲观锁、乐观锁

    1、场景

    2、悲观锁与乐观锁

    (1)悲观锁

    (2)乐观锁

    知识点4:原子类

    1、为什么会有原子类

    2、如果同一个变量要被多个线程访问,则可以使用该包中的类

    3、CAS无锁模式

    (1)什么是CAS

    (2)CAS算法理解

    4、常用原子类

    5、CAS(乐观锁算法)的基本假设前提

    6、CAS缺点

    知识点5:分布式锁


    知识点1:重入锁

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

    重入锁,也叫做递归锁,指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。
    在JAVA环境下 ReentrantLock(显式锁、轻量级锁)和Synchronized (内置锁、重量级锁)都是 可重入锁。

    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:读写锁

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

    知识点3:悲观锁、乐观锁

    1、场景

    当多个请求同时操作数据库时,首先将订单状态改为已支付,在金额加上200,在同时并发场景查询条件下,会造成重复通知。

    SQL:

    Update

    2、悲观锁与乐观锁

    (1)悲观锁

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

    (2)乐观锁

    总是认为不会产生并发问题,每次去取数据的时候总认为不会有其他线程对数据进行修改,因此不会上锁,但是在更新时会判断其他线程在这之前有没有对数据进行修改,一般会使用版本号机制或CAS操作实现。

     

     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,涉及到三个操作数,数据所在的内存值,预期值,新值。当需要更新时,判断当前内存值与之前取到的值是否相等,若相等,则用新值更新,若失败则重试,一般情况下是一个自旋操作,即不断的重试。


    知识点4:原子类

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

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

    1、为什么会有原子类

    CAS:Compare and Swap,即比较再交换。

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

    2、如果同一个变量要被多个线程访问,则可以使用该包中的类

    AtomicBoolean

    AtomicInteger

    AtomicLong

    AtomicReference

    3、CAS无锁模式

    (1)什么是CAS

    CAS:Compare and Swap,即比较再交换。

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

    (2)CAS算法理解

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

    (2)无锁的好处:

    第一,在高并发的情况下,它比有锁的程序拥有更好的性能;

    第二,它天生就是死锁免疫的。

    就凭借这两个优势,就值得我们冒险尝试使用无锁的并发。

    (3)CAS算法的过程是这样:它包含三个参数CAS(V,E,N): V表示要更新的变量,E表示预期值,N表示新值。仅当V值等于E值时,才会将V的值设为N,如果V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。最后,CAS返回当前V的真实值。

    (4)CAS操作是抱着乐观的态度进行的,它总是认为自己可以成功完成操作。当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。基于这样的原理,CAS操作即使没有锁,也可以发现其他线程对当前线程的干扰,并进行恰当的处理。

    (5)简单地说,CAS需要你额外给出一个期望值,也就是你认为这个变量现在应该是什么样子的。如果变量不是你想象的那样,那说明它已经被别人修改过了。你就重新读取,再次尝试修改就好了。

    (6)在硬件层面,大部分的现代处理器都已经支持原子化的CAS指令。在JDK 5.0以后,虚拟机便可以使用这个指令来实现并发操作和并发数据结构,并且,这种操作在虚拟机中可以说是无处不在。

    4、常用原子类

    Java中的原子操作类大致可以分为4类:原子更新基本类型、原子更新数组类型、原子更新引用类型、原子更新属性类型。这些原子类中都是用了无锁的概念,有的地方直接使用CAS操作的线程安全的类型。

    AtomicBoolean

    AtomicInteger

    AtomicLong

    AtomicReference

    public class Test0001 implements Runnable {
    	private static Integer count = 1;
    	private static AtomicInteger atomic = new AtomicInteger();
    
    	@Override
    	public void run() {
    		while (true) {
    			int count = getCountAtomic();
    			System.out.println(count);
    			if (count >= 150) {
    				break;
    			}
    		}
    	}
    
    	public synchronized Integer getCount() {
    		try {
    			Thread.sleep(50);
    		} catch (Exception e) {
    			// TODO: handle exception
    		}
    
    		return count++;
    	}
    
    	public Integer getCountAtomic() {
    		try {
    			Thread.sleep(50);
    		} catch (Exception e) {
    			// TODO: handle exception
    		}
    		return atomic.incrementAndGet();
    	}
    
    	public static void main(String[] args) {
    		Test0001 test0001 = new Test0001();
    		Thread t1 = new Thread(test0001);
    		Thread t2 = new Thread(test0001);
    		t1.start();
    		t2.start();
    	}
    
    }
    

    5、CAS(乐观锁算法)的基本假设前提

    CAS比较与交换的伪代码可以表示为:

    do{   
          
    备份旧数据;  
          
    基于旧数据构造新数据;  
    }while(!CAS(
    内存地址,备份的旧数据,新数据 ))  

     

    (上图的解释:CPU去更新一个值,但如果想改的值不再是原来的值,操作就失败,因为很明显,有其它操作先改变了这个值。)

    就是指当两者进行比较时,如果相等,则证明共享数据没有被修改,替换成新值,然后继续往下运行;如果不相等,说明共享数据已经被修改,放弃已经所做的操作,然后重新执行刚才的操作。容易看出 CAS 操作是基于共享数据不会被修改的假设,采用了类似于数据库的 commit-retry 的模式。当同步冲突出现的机会很少时,这种假设能带来较大的性能提升。

    public final int getAndAddInt(Object o, long offset, int delta) {
            int v;
            do {
                v = getIntVolatile(o, offset);
            } while (!compareAndSwapInt(o, offset, v, v + delta));
            return v;
        }
    
    /** 
    	 * Atomically increments by one the current value. 
    	 * 
    	 * @return the updated value 
    	 */  
    	public final int incrementAndGet() {  
    	    for (;;) {  
    	        //获取当前值  
    	        int current = get();  
    	        //设置期望值  
    	        int next = current + 1;  
    	        //调用Native方法compareAndSet,执行CAS操作  
    	        if (compareAndSet(current, next))  
    	            //成功后才会返回期望值,否则无线循环  
    	            return next;  
    	    }  
    	}  
    

    6、CAS缺点

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

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

    如果在这段期间曾经被改成B,然后又改回A,那CAS操作就会误认为它从来没有被修改过。针对这种情况,java并发包中提供了一个带有标记的原子引用类AtomicStampedReference,它可以通过控制变量值的版本来保证CAS的正确性。

     


    知识点5:分布式锁

    如果想在不同的jvm中保证数据同步,使用分布式锁技术。

    有数据库实现、缓存实现、Zookeeper分布式锁;

     

    展开全文
  • 深入解析Java锁机制

    千次阅读 2018-11-20 08:48:58
    转载自:...amp;mid=2247485524&amp;idx=1&amp;sn=2807a248ab60ce21b22dc07ec1b0ee0c&amp;chksm=fbb281aaccc508bc404611ee11b057bf4b3e02fbbb2916c472fe586cf9ee989eab...

    转载自:https://mp.weixin.qq.com/s?__biz=MzU0OTE4MzYzMw==&mid=2247485524&idx=1&sn=2807a248ab60ce21b22dc07ec1b0ee0c&chksm=fbb281aaccc508bc404611ee11b057bf4b3e02fbbb2916c472fe586cf9ee989eab2be1c84e49&mpshare=1&scene=1&srcid=#rd

    前言

    Java提供了种类丰富的锁,每种锁因其特性的不同,在适当的场景下能够展现出非常高的效率。本文旨在对锁相关源码(本文中的源码来自JDK 8)、使用场景进行举例,为读者介绍主流锁的知识点,以及不同的锁的适用场景。

    Java中往往是按照是否含有某一特性来定义锁,我们通过特性将锁进行分组归类,再使用对比的方式进行介绍,帮助大家更快捷的理解相关知识。下面给出本文内容的总体分类目录:

    1. 乐观锁 VS 悲观锁

    乐观锁与悲观锁是一种广义上的概念,体现了看待线程同步的不同角度。在Java和数据库中都有此概念对应的实际应用。

    先说概念。对于同一个数据的并发操作,悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。Java中,synchronized关键字和Lock的实现类都是悲观锁。

    而乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)。

    乐观锁在Java中是通过使用无锁编程来实现,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。

    根据从上面的概念描述我们可以发现:

    • 悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。

    • 乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。

    光说概念有些抽象,我们来看下乐观锁和悲观锁的调用方式示例:

    通过调用方式示例,我们可以发现悲观锁基本都是在显式的锁定之后再操作同步资源,而乐观锁则直接去操作同步资源。那么,为何乐观锁能够做到不锁定同步资源也可以正确的实现线程同步呢?我们通过介绍乐观锁的主要实现方式 “CAS” 的技术原理来为大家解惑。

    CAS全称 Compare And Swap(比较与交换),是一种无锁算法。在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。java.util.concurrent包中的原子类就是通过CAS来实现了乐观锁。

    CAS算法涉及到三个操作数:

    • 需要读写的内存值 V。

    • 进行比较的值 A。

    • 要写入的新值 B。

    当且仅当 V 的值等于 A 时,CAS通过原子方式用新值B来更新V的值(“比较+更新”整体是一个原子操作),否则不会执行任何操作。一般情况下,“更新”是一个不断重试的操作。

    之前提到java.util.concurrent包中的原子类,就是通过CAS来实现了乐观锁,那么我们进入原子类AtomicInteger的源码,看一下AtomicInteger的定义:

    根据定义我们可以看出各属性的作用:

    • unsafe: 获取并操作内存的数据。

    • valueOffset: 存储value在AtomicInteger中的偏移量。

    • value: 存储AtomicInteger的int值,该属性需要借助volatile关键字保证其在线程间是可见的。

    接下来,我们查看AtomicInteger的自增函数incrementAndGet()的源码时,发现自增函数底层调用的是unsafe.getAndAddInt()。但是由于JDK本身只有Unsafe.class,只通过class文件中的参数名,并不能很好的了解方法的作用,所以我们通过OpenJDK 8 来查看Unsafe的源码:

    根据OpenJDK 8的源码我们可以看出,getAndAddInt()循环获取给定对象o中的偏移量处的值v,然后判断内存值是否等于v。如果相等则将内存值设置为 v + delta,否则返回false,继续循环进行重试,直到设置成功才能退出循环,并且将旧值返回。整个“比较+更新”操作封装在compareAndSwapInt()中,在JNI里是借助于一个CPU指令完成的,属于原子操作,可以保证多个线程都能够看到同一个变量的修改值。

    后续JDK通过CPU的cmpxchg指令,去比较寄存器中的 A 和 内存中的值 V。如果相等,就把要写入的新值 B 存入内存中。如果不相等,就将内存值 V 赋值给寄存器中的值 A。然后通过Java代码中的while循环再次调用cmpxchg指令进行重试,直到设置成功为止。

    CAS虽然很高效,但是它也存在三大问题,这里也简单说一下:

    1. ABA问题。CAS需要在操作值的时候检查内存值是否发生变化,没有发生变化才会更新内存值。但是如果内存值原来是A,后来变成了B,然后又变成了A,那么CAS进行检查时会发现值没有发生变化,但是实际上是有变化的。ABA问题的解决思路就是在变量前面添加版本号,每次变量更新的时候都把版本号加一,这样变化过程就从“A-B-A”变成了“1A-2B-3A”。

    JDK从1.5开始提供了AtomicStampedReference类来解决ABA问题,具体操作封装在compareAndSet()中。compareAndSet()首先检查当前引用和当前标志与预期引用和预期标志是否相等,如果都相等,则以原子方式将引用值和标志的值设置为给定的更新值。

    2. 循环时间长开销大。CAS操作如果长时间不成功,会导致其一直自旋,给CPU带来非常大的开销。

    3. 只能保证一个共享变量的原子操作。对一个共享变量执行操作时,CAS能够保证原子操作,但是对多个共享变量操作时,CAS是无法保证操作的原子性的。

    Java从1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作。

    2. 自旋锁 VS 适应性自旋锁

    在介绍自旋锁前,我们需要介绍一些前提知识来帮助大家明白自旋锁的概念。

    阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长。

    在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失。如果物理机器有多个处理器,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃CPU的执行时间,看看持有锁的线程是否很快就会释放锁。

    而为了让当前线程“稍等一下”,我们需让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销。这就是自旋锁。

     

    自旋锁本身是有缺点的,它不能代替阻塞。自旋等待虽然避免了线程切换的开销,但它要占用处理器时间。如果锁被占用的时间很短,自旋等待的效果就会非常好。反之,如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源。所以,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是10次,可以使用-XX:PreBlockSpin来更改)没有成功获得锁,就应当挂起线程。

    自旋锁的实现原理同样也是CAS,AtomicInteger中调用unsafe进行自增操作的源码中的do-while循环就是一个自旋操作,如果修改数值失败则通过循环来执行自旋,直至修改成功。

    自旋锁在JDK1.4.2中引入,使用-XX:+UseSpinning来开启。JDK 6中变为默认开启,并且引入了自适应的自旋锁(适应性自旋锁)。

    自适应意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。

    在自旋锁中 另有三种常见的锁形式:TicketLock、CLHlock和MCSlock,本文中仅做名词介绍,不做深入讲解,感兴趣的同学可以自行查阅相关资料。

    3. 无锁 VS 偏向锁 VS 轻量级锁 VS 重量级锁

    这四种锁是指锁的状态,专门针对synchronized的。在介绍这四种锁状态之前还需要介绍一些额外的知识。

    首先为什么Synchronized能实现线程同步?

    在回答这个问题之前我们需要了解两个重要的概念:“Java对象头”、“Monitor”。

    Java对象头

    synchronized是悲观锁,在操作同步资源之前需要给同步资源先加锁,这把锁就是存在Java对象头里的,而Java对象头又是什么呢?

    我们以Hotspot虚拟机为例,Hotspot的对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)。

    Mark Word:默认存储对象的HashCode,分代年龄和锁标志位信息。这些信息都是与对象自身定义无关的数据,所以Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。

    Klass Point:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

    Monitor

    Monitor可以理解为一个同步工具或一种同步机制,通常被描述为一个对象。每一个Java对象就有一把看不见的锁,称为内部锁或者Monitor锁。

    Monitor是线程私有的数据结构,每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个monitor关联,同时monitor中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。

    现在话题回到synchronized,synchronized通过Monitor来实现线程同步,Monitor是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的线程同步。

    如同我们在自旋锁中提到的“阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长”。这种方式就是synchronized最初实现同步的方式,这就是JDK 6之前synchronized效率低的原因。这种依赖于操作系统Mutex Lock所实现的锁我们称之为“重量级锁”,JDK 6中为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”。

    所以目前锁一共有4种状态,级别从低到高依次是:无锁、偏向锁、轻量级锁和重量级锁。锁状态只能升级不能降级。

    通过上面的介绍,我们对synchronized的加锁机制以及相关知识有了一个了解,那么下面我们给出四种锁状态对应的的Mark Word内容,然后再分别讲解四种锁状态的思路以及特点:

    无锁

    无锁没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。

    无锁的特点就是修改操作在循环内进行,线程会不断的尝试修改共享资源。如果没有冲突就修改成功并退出,否则就会继续循环尝试。如果有多个线程修改同一个值,必定会有一个线程能修改成功,而其他修改失败的线程会不断重试直到修改成功。上面我们介绍的CAS原理及应用即是无锁的实现。无锁无法全面代替有锁,但无锁在某些场合下的性能是非常高的。

    偏向锁

    偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。

    在大多数情况下,锁总是由同一线程多次获得,不存在多线程竞争,所以出现了偏向锁。其目标就是在只有一个线程执行同步代码块时能够提高性能。

    当一个线程访问同步代码块并获取锁时,会在Mark Word里存储锁偏向的线程ID。在线程进入和退出同步块时不再通过CAS操作来加锁和解锁,而是检测Mark Word里是否存储着指向当前线程的偏向锁。引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令即可。

    偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态。撤销偏向锁后恢复到无锁(标志位为“01”)或轻量级锁(标志位为“00”)的状态。

    偏向锁在JDK 6及以后的JVM里是默认启用的。可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,关闭之后程序默认会进入轻量级锁状态。

    轻量级锁

    是指当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。

    在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,然后拷贝对象头中的Mark Word复制到锁记录中。

    拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock Record里的owner指针指向对象的Mark Word。

    如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,表示此对象处于轻量级锁定状态。

    如果轻量级锁的更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行,否则说明多个线程竞争锁。

    若当前只有一个等待线程,则该线程通过自旋进行等待。但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。

    重量级锁

    升级为重量级锁时,锁标志的状态值变为“10”,此时Mark Word中存储的是指向重量级锁的指针,此时等待锁的线程都会进入阻塞状态。

    整体的锁状态升级流程如下:

    综上,偏向锁通过对比Mark Word解决加锁问题,避免执行CAS操作。而轻量级锁是通过用CAS操作和自旋来解决加锁问题,避免线程阻塞和唤醒而影响性能。重量级锁是将除了拥有锁的线程以外的线程都阻塞。

    4. 公平锁 VS 非公平锁

    公平锁是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。公平锁的优点是等待锁的线程不会饿死。缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大。

    非公平锁是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景。非公平锁的优点是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程。缺点是处于等待队列中的线程可能会饿死,或者等很久才会获得锁。

    直接用语言描述可能有点抽象,这里作者用从别处看到的一个例子来讲述一下公平锁和非公平锁。

     

    如上图所示,假设有一口水井,有管理员看守,管理员有一把锁,只有拿到锁的人才能够打水,打完水要把锁还给管理员。每个过来打水的人都要管理员的允许并拿到锁之后才能去打水,如果前面有人正在打水,那么这个想要打水的人就必须排队。管理员会查看下一个要去打水的人是不是队伍里排最前面的人,如果是的话,才会给你锁让你去打水;如果你不是排第一的人,就必须去队尾排队,这就是公平锁。

    但是对于非公平锁,管理员对打水的人没有要求。即使等待队伍里有排队等待的人,但如果在上一个人刚打完水把锁还给管理员而且管理员还没有允许等待队伍里下一个人去打水时,刚好来了一个插队的人,这个插队的人是可以直接从管理员那里拿到锁去打水,不需要排队,原本排队等待的人只能继续等待。如下图所示:

    接下来我们通过ReentrantLock的源码来讲解公平锁和非公平锁。

    根据代码可知,ReentrantLock里面有一个内部类Sync,Sync继承AQS(AbstractQueuedSynchronizer),添加锁和释放锁的大部分操作实际上都是在Sync中实现的。它有公平锁FairSync和非公平锁NonfairSync两个子类。ReentrantLock默认使用非公平锁,也可以通过构造器来显示的指定使用公平锁。

    下面我们来看一下公平锁与非公平锁的加锁方法的源码:

     

    通过上图中的源代码对比,我们可以明显的看出公平锁与非公平锁的lock()方法唯一的区别就在于公平锁在获取同步状态时多了一个限制条件:hasQueuedPredecessors()。

    再进入hasQueuedPredecessors(),可以看到该方法主要做一件事情:主要是判断当前线程是否位于同步队列中的第一个。如果是则返回true,否则返回false。

    综上,公平锁就是通过同步队列来实现多个线程按照申请锁的顺序来获取锁,从而实现公平的特性。非公平锁加锁时不考虑排队等待问题,直接尝试获取锁,所以存在后申请却先获得锁的情况。

    5. 可重入锁 VS 非可重入锁

    可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。下面用示例代码来进行分析:

    在上面的代码中,类中的两个方法都是被内置锁synchronized修饰的,doSomething()方法中调用doOthers()方法。因为内置锁是可重入的,所以同一个线程在调用doOthers()时可以直接获得当前对象的锁,进入doOthers()进行操作。

    如果是一个不可重入锁,那么当前线程在调用doOthers()之前需要将执行doSomething()时获取当前对象的锁释放掉,实际上该对象锁已被当前线程所持有,且无法释放。所以此时会出现死锁。

    而为什么可重入锁就可以在嵌套调用时可以自动获得锁呢?我们通过图示和源码来分别解析一下。

    还是打水的例子,有多个人在排队打水,此时管理员允许锁和同一个人的多个水桶绑定。这个人用多个水桶打水时,第一个水桶和锁绑定并打完水之后,第二个水桶也可以直接和锁绑定并开始打水,所有的水桶都打完水之后打水人才会将锁还给管理员。这个人的所有打水流程都能够成功执行,后续等待的人也能够打到水。这就是可重入锁。

    但如果是非可重入锁的话,此时管理员只允许锁和同一个人的一个水桶绑定。第一个水桶和锁绑定打完水之后并不会释放锁,导致第二个水桶不能和锁绑定也无法打水。当前线程出现死锁,整个等待队列中的所有线程都无法被唤醒。

     

    之前我们说过ReentrantLock和synchronized都是重入锁,那么我们通过重入锁ReentrantLock以及非可重入锁NonReentrantLock的源码来对比分析一下为什么非可重入锁在重复调用同步资源时会出现死锁。

    首先ReentrantLock和NonReentrantLock都继承父类AQS,其父类AQS中维护了一个同步状态status来计数重入次数,status初始值为0。

    当线程尝试获取锁时,可重入锁先尝试获取并更新status值,如果status == 0表示没有其他线程在执行同步代码,则把status置为1,当前线程开始执行。如果status != 0,则判断当前线程是否是获取到这个锁的线程,如果是的话执行status+1,且当前线程可以再次获取锁。而非可重入锁是直接去获取并尝试更新当前status的值,如果status != 0的话会导致其获取锁失败,当前线程阻塞。

    释放锁时,可重入锁同样先获取当前status的值,在当前线程是持有锁的线程的前提下。如果status-1 == 0,则表示当前线程所有重复获取锁的操作都已经执行完毕,然后该线程才会真正释放锁。而非可重入锁则是在确定当前线程是持有锁的线程之后,直接将status置为0,将锁释放。

    6. 独享锁 VS 共享锁

    独享锁和共享锁同样是一种概念。我们先介绍一下具体的概念,然后通过ReentrantLock和ReentrantReadWriteLock的源码来介绍独享锁和共享锁。

    独享锁也叫排他锁,是指该锁一次只能被一个线程所持有。如果线程T对数据A加上排它锁后,则其他线程不能再对A加任何类型的锁。获得排它锁的线程即能读数据又能修改数据。JDK中的synchronized和JUC中Lock的实现类就是互斥锁。

    共享锁是指该锁可被多个线程所持有。如果线程T对数据A加上共享锁后,则其他线程只能对A再加共享锁,不能加排它锁。获得共享锁的线程只能读数据,不能修改数据。

    独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。

    下图为ReentrantReadWriteLock的部分源码:

     

    我们看到ReentrantReadWriteLock有两把锁:ReadLock和WriteLock,由词知意,一个读锁一个写锁,合称“读写锁”。再进一步观察可以发现ReadLock和WriteLock是靠内部类Sync实现的锁。Sync是AQS的一个子类,这种结构在CountDownLatch、ReentrantLock、Semaphore里面也都存在。

    在ReentrantReadWriteLock里面,读锁和写锁的锁主体都是Sync,但读锁和写锁的加锁方式不一样。读锁是共享锁,写锁是独享锁。读锁的共享锁可保证并发读非常高效,而读写、写读、写写的过程互斥,因为读锁和写锁是分离的。所以ReentrantReadWriteLock的并发性相比一般的互斥锁有了很大提升。

    那读锁和写锁的具体加锁方式有什么区别呢?在了解源码之前我们需要回顾一下其他知识。

    在最开始提及AQS的时候我们也提到了state字段(int类型,32位),该字段用来描述有多少线程获持有锁。

    在独享锁中这个值通常是0或者1(如果是重入锁的话state值就是重入的次数),在共享锁中state就是持有锁的数量。但是在ReentrantReadWriteLock中有读、写两把锁,所以需要在一个整型变量state上分别描述读锁和写锁的数量(或者也可以叫状态)。于是将state变量“按位切割”切分成了两个部分,高16位表示读锁状态(读锁个数),低16位表示写锁状态(写锁个数)。如下图所示:

    了解了概念之后我们再来看代码,先看写锁的加锁源码:

    • 这段代码首先取到当前锁的个数c,然后再通过c来获取写锁的个数w。因为写锁是低16位,所以取低16位的最大值与当前的c做与运算( int w = exclusiveCount(c); ),高16位和0与运算后是0,剩下的就是低位运算的值,同时也是持有写锁的线程数目。

    • 在取到写锁线程的数目后,首先判断是否已经有线程持有了锁。如果已经有线程持有了锁(c!=0),则查看当前写锁线程的数目,如果写线程数为0(即此时存在读锁)或者持有锁的线程不是当前线程就返回失败(涉及到公平锁和非公平锁的实现)。

    • 如果写入锁的数量大于最大数(65535,2的16次方-1)就抛出一个Error。

    • 如果当且写线程数为0(那么读线程也应该为0,因为上面已经处理c!=0的情况),并且当前线程需要阻塞那么就返回失败;如果通过CAS增加写线程数失败也返回失败。

    • 如果c=0,w=0或者c>0,w>0(重入),则设置当前线程或锁的拥有者,返回成功!

    tryAcquire()除了重入条件(当前线程为获取了写锁的线程)之外,增加了一个读锁是否存在的判断。如果存在读锁,则写锁不能被获取,原因在于:必须确保写锁的操作对读锁可见,如果允许读锁在已被获取的情况下对写锁的获取,那么正在运行的其他读线程就无法感知到当前写线程的操作。

    因此,只有等待其他读线程都释放了读锁,写锁才能被当前线程获取,而写锁一旦被获取,则其他读写线程的后续访问均被阻塞。写锁的释放与ReentrantLock的释放过程基本类似,每次释放均减少写状态,当写状态为0时表示写锁已被释放,然后等待的读写线程才能够继续访问读写锁,同时前次写线程的修改对后续的读写线程可见。

    接着是读锁的代码:

    可以看到在tryAcquireShared(int unused)方法中,如果其他线程已经获取了写锁,则当前线程获取读锁失败,进入等待状态。如果当前线程获取了写锁或者写锁未被获取,则当前线程(线程安全,依靠CAS保证)增加读状态,成功获取读锁。读锁的每次释放(线程安全的,可能有多个读线程同时释放读锁)均减少读状态,减少的值是“1<<16”。所以读写锁才能实现读读的过程共享,而读写、写读、写写的过程互斥。

    此时,我们再回头看一下互斥锁ReentrantLock中公平锁和非公平锁的加锁源码:

     

    我们发现在ReentrantLock虽然有公平锁和非公平锁两种,但是它们添加的都是独享锁。根据源码所示,当某一个线程调用lock方法获取锁时,如果同步资源没有被其他线程锁住,那么当前线程在使用CAS更新state成功后就会成功抢占该资源。而如果公共资源被占用且不是被当前线程占用,那么就会加锁失败。所以可以确定ReentrantLock无论读操作还是写操作,添加的锁都是都是独享锁。

     

    结语

    本文Java中常用的锁以及常见的锁的概念进行了基本介绍,并从源码以及实际应用的角度进行了对比分析。限于篇幅以及个人水平,没有在本篇文章中对所有内容进行深层次的讲解。

    其实Java本身已经对锁本身进行了良好的封装,降低了研发同学在平时工作中的使用难度。但是研发同学也需要熟悉锁的底层原理,不同场景下选择最适合的锁。而且源码中的思路都是非常好的思路,也是值得大家去学习和借鉴的。

    展开全文
  • JAVA锁机制

    2018-11-23 23:09:59
    总是认为不会产生并发问题,每次去取数据的时候总认为不会有其他线程对数据进行修改,因此不会上,但是在更新时会判断其他线程在这之前有没有对数据进行修改,一般会使用版本号机制或CAS操作实现。  version方式...

    乐观锁

    总是认为不会产生并发问题,每次去取数据的时候总认为不会有其他线程对数据进行修改,因此不会上锁,但是在更新时会判断其他线程在这之前有没有对数据进行修改,一般会使用版本号机制或CAS操作实现。

     version方式:一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。

    核心SQL语句

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

    对于上面修改用户帐户信息的例子而言,假设数据库中帐户信息表中有一个 version 字段,当前值为 1 ;而当前帐户余额字段( balance )为 $100 。

    1 操作员 A 此时将其读出( version=1 ),并从其帐户余额中扣除 $50( $100-$50 )。

    2 在操作员 A 操作的过程中,操作员B 也读入此用户信息( version=1 ),并从其帐户余额中扣除 $20 ( $100-$20 )。

    3 操作员 A 完成了修改工作,将数据版本号加一( version=2 ),连同帐户扣除后余额( balance=$50 ),提交至数据库更新,此时由于提交数据版本大于数据库记录当前版本,数据被更新,数据库记录 version 更新为 2 。

    4 操作员 B 完成了操作,也将版本号加一( version=2 )试图向数据库提交数据( balance=$80 ),但此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为 2 ,数据库记录当前版本也为 2 ,不满足 “ 提交版本必须大于记录当前版本才能执行更新 “ 的乐观锁策略,因此,操作员 B 的提交被驳回。

    这样,就避免了操作员 B 用基于 version=1 的旧数据修改的结果覆盖操作员A 的操作结果的可能。

    CAS操作方式:即compare and swap 或者 compare and set,涉及到三个操作数,数据所在的内存值,预期值,新值。当需要更新时,判断当前内存值与之前取到的值是否相等,若相等,则用新值更新,若失败则重试,一般情况下是一个自旋操作,即不断的重试。

    优点

    从上面的例子可以看出,乐观锁机制避免了长事务中的数据库加锁开销(操作员 A和操作员 B 操作过程中,都没有对数据库数据加锁),大大提升了大并发量下的系统整体性能表现。

    缺点

    需要注意的是,乐观锁机制往往基于系统中的数据存储逻辑,因此也具备一定的局限性,如在上例中,由于乐观锁机制是在我们的系统中实现,来自外部系统的用户余额更新操作不受我们系统的控制,因此可能会造成脏数据被更新到数据库中。在系统设计阶段,我们应该充分考虑到这些情况出现的可能性,并进行相应调整(如将乐观锁策略在数据库存储过程中实现,对外只开放基于此存储过程的数据更新途径,而不是将数据库表直接对外公开)。

    悲观锁

    顾名思义就是采用一种悲观的态度来对待事务并发问题,我们认为系统中的并发更新会非常频繁,并且事务失败了以后重来的开销很大,这样以来,我们就需要采用真正意义上的锁来进行实现。悲观锁的基本思想就是每次一个事务读取某一条记录后,就会把这条记录锁住,这样
    其它的事务要想更新,必须等以前的事务提交或者回滚解除锁。

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

    重入锁

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

    重入锁,也叫做递归锁,指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。
    在JAVA环境下 ReentrantLock 和synchronized 都是 可重入锁

    读写锁

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

    公平锁

     公平和非公平锁的队列都基于锁内部维护的一个双向链表,表节点Node的值就是每一个节点请求当前锁的线程。公平锁则在于每次都是依次从队首取值。

    锁的方式是基于以下几点:

    表节点Node和状态state的 volatile关键字。

    非公平锁

    在等待锁的过程中,如果有任意新的线程妄图获取锁,都是有很大的几率直接获取到锁的。

    公平锁先到先得,按序进行,非公平锁就是不排队直接拿,失败再说。 

    自旋锁

    自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗

    但是线程自旋是需要消耗cup的,说白了就是让cup在做无用功,线程不能一直占用cup自旋做无用功,所以需要设定一个自旋等待的最大时间。

    如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会导致其它争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。

    自旋锁的优缺点

    自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起操作的消耗!

    但是如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适合使用自旋锁了,因为自旋锁在获取锁前一直都是占用cpu做无用功,占着XX不XX,线程自旋的消耗大于线程阻塞挂起操作的消耗,其它需要cup的线程又不能获取到cpu,造成cpu的浪费。

    自旋锁时间阈值

    自旋锁的目的是为了占着CPU的资源不释放,等到获取到锁立即进行处理。但是如何去选择自旋的执行时间呢?如果自旋执行时间太长,会有大量的线程处于自旋状态占用CPU资源,进而会影响整体系统的性能。因此自旋的周期选的额外重要!

    JVM对于自旋周期的选择,jdk1.5这个限度是一定的写死的,在1.6引入了适应性自旋锁,适应性自旋锁意味着自旋的时间不在是固定的了,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定,基本认为一个线程上下文切换的时间是最佳的一个时间,同时JVM还针对当前CPU的负荷情况做了较多的优化

    1. 如果平均负载小于CPUs则一直自旋

    2. 如果有超过(CPUs/2)个线程正在自旋,则后来线程直接阻塞

    3. 如果正在自旋的线程发现Owner发生了变化则延迟自旋时间(自旋计数)或进入阻塞

    4. 如果CPU处于节电模式则停止自旋

    5. 自旋时间的最坏情况是CPU的存储延迟(CPU A存储了一个数据,到CPU B得知这个数据直接的时间差)

    6. 自旋时会适当放弃线程优先级之间的差异

    自旋锁的开启

    JDK1.6中-XX:+UseSpinning开启; 
    JDK1.7后,去掉此参数,由jvm控制;

     

    展开全文
  • Java 锁机制

    千次阅读 2018-07-21 17:37:50
    当没有竞争的时候,系统会默认使用偏斜。JVM 利用CAS(compare and swap)在 对象头的第一部分(mark word)设置 偏向线程ID,表示对象偏向于这个线程。 因为大部分并发场景下面 对象 生命周期 中最多被一个线程...
  • java锁机制

    2016-04-14 08:02:23
    1.java锁种类及相关概念 1、自旋锁 2、自旋锁的其他种类 3、阻塞锁 4、可重入锁 5、读写锁 6、互斥锁 7、悲观锁 8、乐观锁 9、公平锁 10、非公平锁 11、偏向锁 12、对象锁 13、线程锁 14、锁粗化 15、轻量级锁 16、...
  • java锁机制

    万次阅读 多人点赞 2011-05-19 15:22:00
    一段synchronized的代码被一个线程执行之前,他要先拿到执行这段代码的权限,在java里边就是拿到某个同步对象的(一个对象只有一把); 如果这个时候同步对象的被其他线程拿走了,他(这个线程)就只能...
  • 锁机制-java面试

    千次阅读 2018-04-08 00:00:00
    何为同步?JVM规范规定JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,但两者的实现细节不一样。代码块同步是使用monitorenter和monitorexit指令实现,而方法同步是使用另外一种方式实现的,细节在JVM...
  • 悲观锁和乐观锁机制 - Java基础

    千次阅读 2020-08-22 15:15:17
    知识的广度来自知识的深度,学习如果不成体系...悲观锁和乐观锁机制 - Java基础一、资源和加锁1、场景描述2、演示案例二、锁的概念简介1、锁机制简介2、悲观锁3、乐观锁4、机制对比三、Lock基础案例1、Lock方法说明2、.
  • 有些业务逻辑在执行过程中要求对数据进行排他性的访问,于是需要通过一些机制保证在此过程中数据被锁住不会被外界修改,这就是所谓的锁机制。 Hibernate支持悲观锁和乐观锁两种锁机制。悲观锁,顾名思义悲观的认为在...
  • 锁机制保证了程序不会出现,脏读,冲突等情况。先介绍下,悲观锁和乐观锁的基本描述。悲观锁正如其名,当出现在多用户的并发环境中时, 它对数据出现并发冲突,持保守态度(悲观)。它假定一定出现冲突,所以在数据...
  • java锁机制Synchronized

    千次阅读 2009-03-27 22:12:00
    这些房间有上的(synchronized方法), 和不上之分(普通方法)。房门口放着一把钥匙(key),这把钥匙可以打开所有上的房间。另外我把所有想调用该对象方法的线程比喻成想进入这房子某个 房间的人。所有的...
  • Java锁机制

    2018-04-30 09:26:24
    Java机制: synchronized 在修饰代码块的时候需要一个reference对象作为的对象. 在修饰方法的时候默认是当前对象作为的对象. 在修饰类时候默认是当前类的Class对象作为的对象. 线程同步的方法:...
  • Java中常用的锁机制

    万次阅读 多人点赞 2018-03-29 23:43:10
    在计算机科学中,(lock)或互斥(mutex)是一种同步机制,用于在有许多执行线程的环境中强制对资源的访问限制。旨在强制实施互斥排他、并发控制策略。 通常需要硬件支持才能有效实施。这种支持通常采取一个或...
  • 在计算机科学中,(lock)或互斥(mutex)是一种同步机制,用于在有许多执行线程的环境中强制对资源的访问限制。旨在强制实施互斥排他、并发控制策略。 通常需要硬件支持才能有效实施。这种支持通常采取一个或多...
  • 浅谈Java锁机制

    千次阅读 2018-08-04 11:35:55
    这两天一直在准备面试,看了很多篇关于的介绍的博客,今天就总结一下。 首先需要知道几个名词: 公平/非公平 可重入 独享/共享 互斥/读写 乐观/悲观 分段 偏向/轻量级/重量级 ...
  • java锁机制详解

    千次阅读 2015-03-16 13:18:31
    这些房间有上的(synchronized方法), 和不上之分(普通方法)。房门口放着一把钥匙(key),这把钥匙可以打开所有上的房间。另外我把所有想调用该对象方法的线程比喻成想进入这房子某个 房间的人。所有的东西就...
  • 详解 Java 常用的四种锁机制优缺点

    千次阅读 2018-11-26 14:09:33
    多线程的并发问题一直困扰着大家,Java提供了多种多线程锁机制的实现方式,接下来的话题将分为四个部分给大家讲解他们的优缺点和原理(Synchronized、ReentrantLock、Semaphore、AtomicInteger),每种机制都有优...
  • Java锁机制 自旋锁(spinlock)剖析

    千次阅读 2016-09-13 22:16:37
    Spinlock 介绍线程通过 busy-wait-loop 方式来获取, 任何时刻只有一个线程能够获得, 其它线程忙等待知道获得。应用场景spinlock 不会有线程状态切换,所以响应更快。使用spinlock时, 临界区要尽量短,不要...
  • 从synchronized举例,在java1.6当中为了减少获得和释放带来的性能消耗而引入的偏向和轻量级,以及的存储结构和升级过程。 ·对于普通同步方法,是当前实例对象。 ·对于静态同步方法,是当前类的...
  • 一篇文章即可读懂java中的锁机制

    万次阅读 多人点赞 2019-03-27 09:45:16
    java中的分为以下(其实就是按照的特性和设计来划分): 1、公平/非公平 2、可重入 3、独享/共享 4、互斥/读写 5、乐观/悲观 6、分段 7、偏向/轻量级/重量级 8、自旋...

空空如也

1 2 3 4 5 ... 20
收藏数 74,521
精华内容 29,808
关键字:

锁机制