Java的各种锁

一、广义上分为悲观锁和乐观锁,是一种设计思想,并不是指具体实现出来的锁,可以分为:
1.悲观锁:永远都假设最坏的情况:每次对数据时操作时都会有另一线程前来修改当前数据,所以每次对数据进行操作时都会上锁,这时如果有其他线程对当前数据进行修改,只能等待或阻塞直到锁解开,例如Synchronized的重量级锁、ReentrantLock,按照实现方式这两个也可以叫做阻塞锁。

2.乐观锁:每次操作数据时都认为不会有其他线程来修改数据,但在最终更新数据时会检测一下数据是否已经被更新,比如数据库抢单时设置version字段,除此之外,乐观锁也可以用CAS算法实现,比如几个线程竞争的抢占一个资源,就可以用compareAndSet()来实现,compareAndSet()函数是CAS算法的具体实现,并且也是原子操作,compareAndSet()有两个参数,第一个是旧值,第二个是要设置的新值。通过旧值,可以知道目的地址的值是否发生过修改,如果没有则设置为新值并返回true,否则返回false。

二、通过抢占资源失败后线程是通过阻塞等待还是通过CAS等待可以分为:
1.阻塞锁:几个线程抢占一个资源时,如果某个线程抢占到,其他线程只能处于阻塞状态,直到资源释放,再次竞争资源,一直这样循环下去直至所有线程执行完毕。例如:

		Object object = new Object();


		Runnable runnable = new Runnable() {
			@Override
			public void run() {
				// TODO Auto-generated method stub
				synchronized (object) {
					System.out.println(Thread.currentThread().getName() + "获取到锁");
					try {
						Thread.sleep(10000);
					} catch (Exception e) {
						// TODO: handle exception
					}
					System.out.println(Thread.currentThread().getName() + "释放锁");
				}
			}
		};
		new Thread(runnable).start();

		Runnable runnable2 = new Runnable() {
			@Override
			public void run() {
				// TODO Auto-generated method stub
				synchronized (object) {
					System.out.println(Thread.currentThread().getName() + "获取到锁");
				}
			}
		};
		Thread t2 = new Thread(runnable2);
		t2.start();

		Runnable runnable3 = new Runnable() {

			@Override
			public void run() {
				// TODO Auto-generated method stub
				while (true) {
					try {
						Thread.sleep(1000);
					} catch (Exception e) {
						// TODO: handle exception
					}
					System.out.println("t2状态:" + t2.getState());
				}
			}
		};
		new Thread(runnable3).start();

输出如下:

Thread-0获取到锁
t2状态:BLOCKED
t2状态:BLOCKED
Thread-0释放锁
Thread-1获取到锁
t2状态:TERMINATED

2.自旋锁:在等待获取锁的时候,线程不会阻塞,而是不断循环尝试用CAS获取锁。例如Java中的TicketLock。另外,自己也可以根据这种思想实现一个简单的线程安全的int栈:

import java.util.concurrent.atomic.AtomicReference;

public class CustomStack {

	AtomicReference<Node> top = new AtomicReference<Node>();

	public void push(int i) {
		Node newTop = new Node(i);
		Node oldTop;
		do {
			oldTop = top.get();
			newTop.next = oldTop;
		} while (!top.compareAndSet(oldTop, newTop));

	}

	public Node pop() {
		Node newTop;
		Node oldTop;
		Node res;
		do {
			oldTop = top.get();
			res = oldTop;
			newTop = oldTop.next;
		} while (!top.compareAndSet(oldTop, newTop));
		return res;
	}
}

class Node {
	int data;
	Node next;

	public Node(int i) {
		data = i;
	}
}

其中,在AtomicReference类中,使用了volatile关键字使变量对每个线程可见。

push()过程分析:线程1准备放入一个新的头结点,所以取得此时top的头结点,并将此旧的头结点作为新的头结点的next,然后通过compareAndSet()进行判断:如果此时top中的头结点仍然是刚刚取出时的旧的头结点,则说明线程1操作top这段时间没有其他线程修改过头结点,所以可以设置新值,反之则说明头结点已经被改变,所以需要重新获取当前的头结点,再次重新比对、设置,直至设置成功。

自旋锁中,线程可以不阻塞,所以节省了操作系统频繁调度线程的时间,但缺点是cpu占用较高,所以超过一定时间仍然没有获取到资源后,仍然需要进入阻塞状态。

三、根据在竞争资源时采用的方式可以定义为三种,synchronized的三种状态可以分别对应:
首先要看一下Java对象头,Java对象头包括Mark Word和Class Address。
Mark Word:在默认(无锁)状态下,存储实例的哈希值,实例年龄,是否为偏向锁标志位以及锁标志位,此时处于无锁状态,锁标志位用01表示,偏向锁标志位为0,在切换为其他类型的锁时,除了锁标志位以外,其余部分的数据结构会发生改变,如下所示:

状态存储内容标志位
无锁实例的哈希值,实例年龄,偏向锁:001
偏向锁获取资源的ID ,实例年龄,偏向锁:101
轻量级锁Lock Record(锁记录) 的指针00
重量级锁mutex lock(重量级锁)的指针10

Class Address:保存向类的相关信息所在的地址,指明当前对象是哪一个类的实例。

在JDK6以前,synchronized使用系统底层的Mutex Lock来实现锁,这种锁的状态转换时间较长,如果执行的代码比较简单,那么锁的状态转换时间甚至会高于真正执行代码的时间,所以在JDK6后,引入了偏向锁和轻量级锁。

1.偏向锁:如果总共只有一个线程重复的执行并获取资源,例如线程池中的SingleThreadExecutor的单例执行方式,那么此时(注意是此时)不会再有资源争夺的情况,如果使用重量级锁也就没有意义了,所以此时修改标志位,从无锁状态改为采用偏向锁。假设经过一段时间后,突然有一个新的其他进程来尝试获取资源,则偏向锁变为轻量级锁。
2.轻量级锁:轻量级锁认为存在竞争,但竞争很轻,例如只有两个线程,通过自旋的方式即可解决。进入轻量级锁时,在当前线程对应的栈帧中分配一个Lock Record空间,然后线程将目标对象的Mark Work复制到Lock Record中,然后使用CAS将目标对象的Mark Word修改为指向自己的Lock Record的指针,如果成功,则表明获取到对象所,可以执行synchronized的代码块,如果自旋到一定次数后仍然未获取到锁,则上升至重量级锁。
3.重量级锁:即用系统底层的Mutex Lock来实现的锁,在锁的状态转换时需要一定时间。

四、根据线程中的函数能否递归调用分为可重入锁和不可重入锁:
ReentrantLock和Synchronized都是可重入锁,可重入锁也叫作递归所,意思就是在同一线程中重复获取锁不会产生死锁,例如:

public class LockTest {

	static ReentrantLock reentrantLock = new ReentrantLock();

	public static void method1() {
		reentrantLock.lock();
		method2();
		System.out.println("this is method1");
		reentrantLock.unlock();
	}

	public static void method2() {
		reentrantLock.lock();
		System.out.println("this is method2");
		reentrantLock.unlock();
	}

	public static void main(String args[]) {
		method1();
	}
}

如果是不可重入锁,在主线程中method1()调用method2()时,method2()需要获取锁,所以等待method1()将锁释放,但method1()并没有执行到unlock(),所以发生死锁。

可重入锁实现原理:
锁本身会记录获取到锁的线程和一个起始为0的计数器,线程尝试获取锁时,锁首先判断尝试获取锁的线程是否为当前记录的线程,如果是,计数器加一,继续执行同步区代码;如果不是当前线程,则会让尝试获取锁的线程等待。
线程释放锁时,锁会将计数器减一,直到重复释放到计数器为0时,真正的释放锁,唤醒其他的等待的线程。

四、根据多个线程能否共享一把锁可以分为独享锁和共享锁:
独享锁:只能由一个线程持有,例如ReentrantLock。
共享锁:可以由多个线程持有,例如ReentrantReadWriteLock,可以在有一个写线程的同时有多个读线程。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值