一、广义上分为悲观锁和乐观锁,是一种设计思想,并不是指具体实现出来的锁,可以分为:
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,在切换为其他类型的锁时,除了锁标志位以外,其余部分的数据结构会发生改变,如下所示:
状态 | 存储内容 | 标志位 |
---|---|---|
无锁 | 实例的哈希值,实例年龄,偏向锁:0 | 01 |
偏向锁 | 获取资源的ID ,实例年龄,偏向锁:1 | 01 |
轻量级锁 | 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,可以在有一个写线程的同时有多个读线程。