java实践8各种锁的实现和解析(上篇)

java实践8各种锁的实现和解析(上篇)

  促使我写,这篇文章的起源是一个小伙伴,面试被问到java并发包下的ReentrantLock 各种锁、实现原理。网上有很多文章都有各种介绍、源码。但看完之后还是一头雾水,各种方法名很长,背着费劲,没有业务场景,不好理解。ReentrantLock虽然我用的不多,但我在实现锁方面,还是有一些体会(因为其他的一些场景,自己实现过集中方案,用其他的怕控制不好)。下面我会结合场景和代码,给大家分享一下我的理解。

1、什么是锁

  锁:本质上其实就是在内存中的一个数,来表示加锁和无锁状态,在多线程环境中,为了保证数据的正常,会用到锁。通过锁的机制,可以保证程序在多线程访问的情况下,数据不会错乱,避免程序出现未知异常。
   在我的理解中,java只提供了2种方法,就是synchronized和cas(具有原子性,可调用硬件级别的指令实现的同步原语)来实现锁。我两个我认为才是最重要的。其他的基本都是一些概念,我们要根据不同的业务场景来做不同的优化。java中各种锁、线程安全的类,都是基于他们两个来的。

  场景:提供对外API,当请求进入时把请求,处理请求,处理完后累加处理了多少个请求。

//模拟对外API接口
public class DuiwaiApi {
	int handlerNums = 0;
	//请求进来进行累加
	public void add() {
		//做一些事…
		//累加已处理请求数
		handlerNums++;
		System.out.println(Thread.currentThread().getName() + ",handlerNums=" + handlerNums);
	}
}
//模拟并发调用Main类
public class Main {
	static DuiwaiApi api = new DuiwaiApi();
	public static void main(String[] args) {
		Runnable r = new Runnable() {
			public void run() {
				// 调用对外API
				api.add();
			}
		};
		// 多线程 执行可以看到 对外API中的pv数据乱了
		for (int i = 0; i < 10; i++) {
			Thread t1 = new Thread(r);
			t1.start();
		}
	}
}

运行上面demo,没有锁,可以发现,处理请求数handlerNums数据乱了。

2、利用cas实现锁

  上面的程序handlerNums错乱了怎么办?我们可以用cas来实现个锁。使handlerNums可以正常累加。
场景变化:提供对外API,由于服务资源不够,同一时间只能处理同一个请求,当多个请求进入时,拿到锁的请求进行处理,并且累加计算handlerNums,没拿到锁的请求直接拒绝,并且不累加handlerNums。

//锁实现类
public class Suo1Simple {
	Unsafe unsafe;
	long markoffer;// mark在内存中的位置
	int mark = 0;// 标志位0没锁,1有锁

	public Suo1Simple() {
		// 获取unsafe
		try {
			Field f = Unsafe.class.getDeclaredField("theUnsafe");
			f.setAccessible(true);
			this.unsafe = (Unsafe) f.get(null);
			// 初始化 makr变量在内存中的位置
			markoffer = unsafe.objectFieldOffset(this.getClass().getDeclaredField("mark"));
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	public boolean lock() {
		//设置mark,为0(没锁)的话 设置为1(加锁)。
		boolean b = unsafe.compareAndSwapInt(this, markoffer, 0, 1);
		//返回true则加锁成功,返回false加锁失败
		return b;
	}

	public boolean unlock() {
		return unsafe.compareAndSwapInt(this, markoffer, 1, 0);
	}
}

重要:unsafe. compareAndSwapInt(Object o, long offset, int expected,int x),底层调用cas,是安全的。作用:在内存中的对象o, offset位置,放入x值。如果offset值为0,则放入成功返回true。如果offer位置的值不为0,返回false,赋值失败。

修改DuiWaiApi类
//对外API
public class DuiwaiApi {
	int handlerNums = 0;
	Suo1Simple suo = new Suo1Simple();
	//请求进来进行累加
	public void add() {
		// 加锁 如果成功则向下执行,否则不执行退出
		if (suo.lock()) {
			handlerNums++;
			System.out.println(Thread.currentThread().getName() + "拿到了锁开始执行,handlerNums=" + handlerNums);
			// 拿到锁的线程 执行完成后释放锁
			suo.unlock();
		} else {
			System.out.println(Thread.currentThread().getName() + "没拿到锁退出");
		}
	}
}

执行后,可以看到handlerNums能正常累加了,不会出现错乱的数据。一般情况就可以使用cas来实现锁。

3、实现可以阻塞的锁

 我们再把业务场景改变一下。虽然我们同时只能处理一个请求,但是没拿到锁的线程被拒绝了没有累加。这个锁能不能阻塞一下啊,前一个线程处理累加完,后面的线程也要进行处理累加。

修改刚才DuiwaiApi的代码,重写lock方法

// 锁
	public boolean lock() {
		while (true) {
			boolean b = unsafe.compareAndSwapInt(this, markoffer, 0, 1);
			if (b) {
				return b;
			}
		}
	}

可以看到我们在lock方法中增加了while语句。没拿到锁的线程会阻塞在lock中。一直去循环拿锁

3.1、自旋锁、乐观锁、不可重入锁

自旋锁:在lock方法中,多个线程会设置mark,设置失败的会一直循环去拿锁。这就是自旋锁。
乐观锁:调用lock方法,虽然会阻塞,但是没拿到锁的线程会一直循环去拿锁,他认为,拿到锁的线程会马上执行完毕,释放锁,这样他马上就能拿到锁了,不用休息,减少了线程切换。这就是乐观锁。

不可重入锁:大家注意对外api类中,调用了suo.lock方法。注意已经拿到锁的线程,再次调用lock线程(看下图),那么还会阻塞。这说明,我们实现的锁是不可重入的。 这个就是不可重入锁。

在这里插入图片描述

4、增加可重入功能

  在上面可以看到,拿到锁的线程,不能再次调用lock方法,否则会阻塞。那么我们来优化一下。增加可重入功能。当线程拿到锁后,我们记录一下拿到锁的线程,那么当拿到锁的线程多次 执行lock时,我们不阻塞直接放行。

修改锁的实现类
// 增加属性getsuoThread :记录获取锁的线程
private Thread getsuoThread = null;
//修改lock方法
public boolean lock() {
	while (true) {
		//判断是拿到锁的线程进来了,则直接返回true
		if (this.getsuoThread == Thread.currentThread()) {
			System.out.println(this.getsuoThread.getName() + "重入了");
			return true;
		}
		// 1、第一次会走这块 设置锁
		boolean b = unsafe.compareAndSwapInt(this, markoffer, 0, 1);
		if (b) {
			// 2、设置成功说明拿到了锁,则记录一下获取锁的线程,
			this.getsuoThread = Thread.currentThread();
			return b;
		}
	}
}
// 释放锁
public boolean unlock() {
	boolean b = unsafe.compareAndSwapInt(this, markoffer, 1, 0);
	this.getsuoThread = null;
	return b;
}

4.1可重入锁:

  注意代码中红色部分,当拿到锁的线程,再次进入lock后,会直接返回ture。这个就是可重入锁。
在这里插入图片描述

5、利用wait和notify实现可等待唤醒的锁

通过刚才的demo,可以看到如果每个线程,执行的都很快,能快速拿、快速执行并释放,那么我们线程不休息,循环去争抢锁,会减少线程的上线文切换,提高效率。但是 如果每个线程拿到锁后,执行的很慢怎么办?其他线程不休息,一直在争抢,那么会给系统带来很大的开销。
刚才的demo大家可以在duiwaiapi类中而增加Thread.sleep,并增加调用的线程数。

在这里插入图片描述在这里插入图片描述
修改后会发现不但程序慢,我们的系统也变的很卡,就是因为线程过多,一直在询问是否加锁成功,争抢cpu资源导致。这时,我们可以利用wait和notify来优化它。

修改锁实现类,重写lock和unlock方法(注意这里为减少代码行数,把可重入功能去掉了,大家理解后,可自行加入)

public boolean lock() {
		while (true) {
			boolean b = unsafe.compareAndSwapInt(this, markoffer, 0, 1);
			if (b) {
				return b;
			} else {
				//没拿到锁,程序进入等待,等拿到锁线程 进行唤醒再继续运行
				synchronized (this) {
					try {
						this.wait();
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			}
		}
	}

	// 释放锁
	public boolean unlock() {
		// 拿到锁的线程 执行完毕,唤醒其他线程继续执行
		synchronized (this) {
			// 修改标志位 无锁状态
			unsafe.compareAndSwapInt(this, markoffer, 1, 0);
			// 唤醒 线程继续执行wait下面的方法
			this.notify();
		}
		return true;
	}

可以看到lock方法,当执行到synchronized时,所以所有的线程都会被排队,然后线程会排队一个一个的执行。synchronized里面的内容。当其中一个线程执行到wait后,则此线程进入休息状态,等待拿到锁的线程释放时,会调用notify来唤醒。

小提示:wait/notify只是举个例子,我们也可以使用park/unpark(他也是使用unsafe实现的),或者siginal和await.

5.1悲观锁:

每次都会悲观的认为其他线程修改了数据,所以每次执行时都会上锁,其他线程会被阻塞休息,直到释放锁,不像乐观锁,不休息一直询问。synchronized修饰的方法或代码块都是悲观锁。

未完。java实践8各种锁的实现和解析(下篇)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值