Java学习——并发编程之锁的深入化

7 篇文章 0 订阅
5 篇文章 0 订阅

五、锁的深入化

锁是并发编程共享数据,保证数据一致性的工具。在Java中有多种实现,例如synchronized(重量级锁)、ReentrantLock(轻量级锁)等,这些锁为我们的来发提供了便利。下面我跟大家聊一聊Java中锁的相关知识。

1.重入锁

重入锁的概念:重入锁也叫递归锁。就是说同一线程中,外层函数获取了锁,可以传递给内层函数去使用,可重入性可以避险死锁现象。synchronized(重量级锁)、ReentrantLock(轻量级锁)都属于重入锁。下面写一个可重入锁的例子:

class Test implements Runnable{
	@Override
	public void run() {
		set();
	}
	
	//synchronized要在代码块执行完毕后才会释放锁
	public synchronized void set(){
		System.out.println("set方法");
		get();
	}
	
	public synchronized void get(){
		System.out.println("get方法");
	}
}

//synchronized(重量级) 和Lock锁(轻量级)——重入锁(具有递归性)
public class test01 {
	

	public static void main(String[] args) {
		
		Test test = new Test();
		
		Thread t1 = new Thread(test);
		
		t1.start();

	}
}

在这个例子中,set方法和get方法的锁是同一个,在这里我们假设synchronized锁不具有可重入性,那么get方法就必须要等待set方法释放锁后才能获取锁,这样在set方法中调用get方法必然会造成死锁现象(get方法一直在等待set方法执行完毕)。但是上面的代码并没有出错,说明synchronized锁具有可重入性,set方法中调用get方法,将set方法获取的锁传递给内层函数(get)。

2.读写锁

假设程序中涉及到对一些共享资源的读写操作,并且在没有做写入操作是,允许两个线程同时读入资源。这时就需要用到读写锁。读写锁允许多个线程同时读取资源,但是不允许多个线程同时进行写入操作或者同时读写操作,也就是说:读-读能共存,读-写不能共存,写-写不能共存。下面写一个读写锁的例子:

package com.zhu.test;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock;

public class test03 {
	
	private volatile Map<String,String> caChe = new HashMap<>();
	
	//新建一个读写锁
	private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
	
	//写锁
	private WriteLock writeLock = rwl.writeLock();
	//读锁
	private ReadLock readLock = rwl.readLock();
	
	//写入
	public void put(String key,String value){
		try {
			writeLock.lock();
			System.out.println("写入put方法key:" + key + ",value:" + value + "开始");
			//Thread.sleep(50);
			caChe.put(key, value);
			System.out.println("写入put方法key:" + key + ",value:" + value + "结束");
			
		} catch (Exception e) {
			// TODO: handle exception
		}finally {
			writeLock.unlock();
		}
	}
	
	//读取
	public String get(String key){
		try {
			readLock.lock();
			String value = caChe.get(key);
			System.out.println("读取get方法key:" + key + ",value:" + value + "开始");
			Thread.sleep(50);
			caChe.put(key, value);
			System.out.println("读取get方法key:" + key + ",value:" + value + "结束");
			return value;
		} catch (Exception e) {
			// TODO: handle exception
		}finally {
			readLock.unlock();
		}
		return null;
	}
	
	public static void main(String[] args) {
		
		test03 t = new test03();
		
		//写入线程
		Thread write = new Thread(new Runnable() {
			
			public void run() {
				for(int i = 0;i < 10;i++){
					t.put("i", i+"");
				}
				
			}
		});
		
		//读取线程
		Thread read = new Thread(new Runnable() {
			
			public void run() {
				for(int i = 0;i < 10;i++){
					t.get("i");
				}
				
			}
		});
		
		write.start();
		
		read.start();
	}

}

实验结果:

通过实验结果,我们可以发现,在写入资源时,我们调用写锁的lock()方法,写入结束调用unlock()方法;在读取资源时,我们调用读锁的lock()方法,写入结束调用unlock()方法,并且在写入操作没有完成之前,是不能进行读取操作的。

3.乐观锁/悲观锁

      乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度。悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。在Java中,synchronized的思想也是悲观锁。

       乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。从上面的描述我们可以看出,悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升。悲观锁在Java中的使用,就是利用各种锁乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新。

4.cas无锁机制

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

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

5.CAS算法理解

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

(2)无锁的好处:

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

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

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

(3)CAS算法的过程是这样:它包含三个参数CAS(V,E,N): V表示要更新的变量,E表示预期值,N表示新值。仅当V值等于E值时,才会将V的值设为N,如果V值和E值不同,则说明已经有其他线程做了更新,则将主内存的值刷新到本地内存,再去做比较,一直重试。最后,CAS返回当前V的真实值。

V=需要更新变量,主内存

E=预望值,本地内存

N=新值

如果V=E(主内存值与本地内存值一致),说明:没有被修改过,将V的值设置为N。

如果V!=E(主内存值与本地内存值不一致),已经被修改。

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

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

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值