【多线程】线程锁概念及两种实现

一、相关概念

  • 自旋锁
    对某数据的修改如果失败,不会放弃当前CPU执行权,而是循环使用CAS进行尝试修改直到成功。
  • 悲观锁
    假设会发生并发操作,因此对数据的相关操作从读开始就上锁
  • 乐观锁
    假设不会有并发操作,在修改数据时如果发现数据不一致了,则读取最新数据再尝试进行修改。自旋锁就是一种乐观锁
  • 独享锁
    即排它锁,同时只有一个线程可持有
  • 共享锁
    多个线程也可加持同一把共享锁,如读锁,多线程可读
  • 可重入锁/不可重入锁
    线程拿到一把锁后,是否可以自由进入由同一把锁同步的其他代码中
  • 公平锁/非公平锁
    线程争抢锁资源是否按先到先得原则,如果是则是公平锁,否则是非公平锁

二、锁的实现

1、同步关键字synchronized

基于对象监视器,每个对象都对应一个监视器,同时只有一个线程可以锁定这个监视器,其他尝试锁定的线程都会被阻塞。synchronized上锁与解锁都是jvm层面的,会自动释放锁。下面看下代码实现:

  • 对象锁
public class SynTest1 {
	public static void main(String[] args) {
		SynTest1 obj = new SynTest1();
		new Thread(new Runnable() {
			@Override
			public void run() {
				obj.method();
			}
		}).start();

		new Thread(new Runnable() {
			@Override
			public void run() {
				obj.method();
			}
		}).start();
	}

	private synchronized void method() {
		System.out.println("method() run with...." + Thread.currentThread());
		try {
			Thread.sleep(3000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println("method() end with...." + Thread.currentThread());
	}
}

输出结果:
method() run with…Thread[Thread-0,5,main]
method() end with…Thread[Thread-0,5,main]
method() run with…Thread[Thread-1,5,main]
method() end with…Thread[Thread-1,5,main]

  • 类锁
public class SynTest1 {
	public static void main(String[] args) {
		// SynTest1 obj = new SynTest1();
		new Thread(new Runnable() {
			@Override
			public void run() {
				SynTest1.method();
			}
		}).start();

		new Thread(new Runnable() {
			@Override
			public void run() {
				SynTest1.method();
			}
		}).start();
	}

	private static synchronized void method() {
		System.out.println("method() run with...." + Thread.currentThread());
		try {
			Thread.sleep(3000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println("method() end with...." + Thread.currentThread());
	}
}

输出结果:
method() run with…Thread[Thread-0,5,main]
method() end with…Thread[Thread-0,5,main]
method() run with…Thread[Thread-1,5,main]
method() end with…Thread[Thread-1,5,main]

同时,synchronized 还可以用在代码块上:

public class SynTest1 {
	public static void main(String[] args) {
		SynTest1 obj = new SynTest1();
		new Thread(new Runnable() {
			@Override
			public void run() {
				obj.method();
			}
		}).start();

		new Thread(new Runnable() {
			@Override
			public void run() {
				obj.method();
			}
		}).start();
	}
	private void method() {
		synchronized (this) {//这里的this指代调用该方法的对象
			System.out.println("method() run with...." + Thread.currentThread());
			try {
				Thread.sleep(3000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println("method() end with...." + Thread.currentThread());
		}
	}
}

synchronized实现的是一种悲观锁、独享锁,被synchronized加持的一段操作都是需要获得锁才能进行操作的,且同时只有一个线程可进行操作。同时synchronized也是一种重入锁,结合代码我们理解下什么是可重入锁:

public class SynTest2 {
	static SynTest2 obj = new SynTest2();
	public static void main(String[] args) {
		new Thread(new Runnable() {
			@Override
			public void run() {
				obj.method(1);
			}
		}).start();
	}
	private synchronized void method(int leval) {
		System.out.println("in door-" + leval);
		try {
			Thread.sleep(3000);
			if (leval == 1) {
				obj.method(2);
			} else {
				System.out.println("get it in door-" + leval);
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println("out door-" + leval);
	}
}

输出结果:
in door-1
in door-2
get it in door-2
out door-2
out door-1
就像我们拿一把钥匙回家一样,先打开入户门,还可以拿这把钥匙打开卧室门然后躺在柔软的床上。

2、synchronized实现原理

对象在内存中存储的的形式中有一部分为头信息(Mark Word),标记了对象和锁相关的信息,synchronized实现就是基于其中锁的标志位实现的。简化存储内容为:

是否偏向锁锁标志位锁状态
001无锁
101无锁
00CAS轻量级锁
10重量级锁

偏向锁状态是jvm底层的一种优化,默认是1开启状态,jvm会先“偷懒”认为是无多线程操作的,即无锁状态。当线程去获取锁是首先看偏向锁中是否存储有线程id,没有则存储当前线程id,即首次有线程访问,当单线程情况下回去比对该线程id,相同则认为是单线程操作,即不需要加锁。一旦有其他线程访问,查看到的线程id与自己不符,则会升级为多线程操作,去CAS操作设置锁标志位为加锁状态,释放锁则是恢复锁标志位操作。但CAS自旋是很消耗CPU的,在自旋一定时间后就会转为同步等待状态,标志位为重量级锁。

3、Lock锁

通过Lock接口实现类API来获取锁释放锁,功能更加强大灵活,但不会主动释放锁,需要编程实现。常用实现有ReentrantLock、ReentrantReadWriteLock。核心实现类:

方法描述
lock获取锁,如果有其他锁占用则阻塞等待
lockInterruptibly获取锁过程中可中断
tryLock非阻塞式获取,立即返回或者指定尝试一定时间
unlock释放锁
  • ReentrantLock:可重入锁,独享锁、支持公平与非公平锁
public class LockTest1 {
	private static final Lock lock = new ReentrantLock();
	public static void main(String[] args) {
		test1();
	}
	private static void test1() {
		lock.lock();
		try {
			System.out.println("do something...");
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			lock.unlock();
		}
	}
}
  • ReentrantReadWriteLock:读写锁,用于读多写少场景优化
public class LockTest2 {
	private static final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
	public static void main(String[] args) {
		LockTest2 lt = new LockTest2();
		new Thread(new Runnable() {
			@Override
			public void run() {
				lt.read(Thread.currentThread());
			}
		}).start();
		new Thread(new Runnable() {
			@Override
			public void run() {
				lt.read(Thread.currentThread());
			}
		}).start();
		new Thread(new Runnable() {
			@Override
			public void run() {
				lt.write(Thread.currentThread());
			}
		}).start();
	}
	private static void read(Thread t) {
		rwLock.readLock().lock();// 获取读锁并上锁,共享锁,多个线程可获取
		long start = System.currentTimeMillis();
		while (System.currentTimeMillis() - start < 1) {
			System.out.println(t.getName() + "正在读取数据...");
		}
		rwLock.readLock().unlock();
	}
	private static void write(Thread t) {
		rwLock.writeLock().lock();//独享锁,只有一个线程能同时访问
		long start = System.currentTimeMillis();
		while (System.currentTimeMillis() - start < 1) {
			System.out.println(t.getName() + "正在写数据...");
		}
		rwLock.writeLock().unlock();
	}
}

输出结果较长,可自行运行,可以看到线程0和线程1会交叉读取数据,而线程2写数据操作只有当读操作全部完毕才会执行。

4、锁降级理解

持有写锁过程中在获取到读锁,再将写锁释放的过程,多应用于缓存处理中。

public class LockTest3 {
	private static Map<String, Object> map = new HashMap<String, Object>();
	private static final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
	private static Object get(String id) {
		Object value = null;
		// 开启读锁,先从缓存中读取
		rwLock.readLock().lock();
		try {
			if ((value = map.get(id)) == null) {// 缓存失效
				// 查询db读取数据,但访问量过大,这里db压力扛不住就会发生缓存雪崩
				// 解决方法:读锁-》写锁,使得只有一个线程去db读取数据,其他线程坐等结果
				rwLock.readLock().unlock();// 先释放之前的读锁才能加写锁
				rwLock.writeLock().lock();// 此时大量访问线程会在此处阻塞
				try {// 再次检查缓存,会有一个线程去库里读取回来,设置到缓存中
					if ((value = map.get(id)) == null) {
						// select value from...
					}
					map.put(id, value);
					// 在释放写锁之前考虑下,如果有其他线程修改这个值,则会到时数据不一致,此时需要加读锁降级写锁,保证数据不可修改
					rwLock.readLock().lock();

				} finally {
					rwLock.writeLock().unlock();// 释放写锁
				}
			}
		} finally {
			rwLock.readLock().unlock();// 最后释放读锁
		}
		return value;
	}
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值