乐观锁和悲观锁
悲观锁:
认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改,synchronized和Lock的实现类都是悲观锁,适合写操作多的场景,先加锁可以保证写操作时数据正确,显示的锁定之后再操作同步资源-----狼性锁
乐观锁:
认为自己在使用数据的时候不会有别的线程修改数据或资源,不会添加锁,Java中使用无锁编程来实现,只是在更新的时候去判断,之前有没有别的线程更新了这个数据,如果这个数据没有被更新,当前线程将自己修改的数据成功写入,如果已经被其他线程更新,则根据不同的实现方式执行不同的操作,比如:放弃修改、重试抢锁等等。判断规则有:版本号机制Version,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。-----适合读操作多的场景,不加锁的特性能够使其读操作的性能大幅提升,乐观锁则直接去操作同步资源,是一种无锁算法,得之我幸不得我命---佛系锁
synchronized
- 作用于实例方法,当前实例加锁,进入同步代码块前要获得当前实例的锁;
- 作用于代码块,对括号里配置的对象加锁
- 作用于静态方法,当前类加锁,进去同步代码前要获得当前类对象的锁
class Phone {
public synchronized void sendEmail() {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("------sendEmail");
}
public synchronized void sendSMS() {
System.out.println("------sendSMS");
}
public void hello() {
System.out.println("------hello");
}
}
/**
* 现象描述:
* 1 标准访问ab两个线程,请问先打印邮件还是短信? --------先邮件,后短信 共用一个对象锁
* 2. sendEmail钟加入暂停3秒钟,请问先打印邮件还是短信?---------先邮件,后短信 共用一个对象锁
* 3. 添加一个普通的hello方法,请问先打印普通方法还是邮件? --------先hello,再邮件
* 4. 有两部手机,请问先打印邮件还是短信? ----先短信后邮件 资源没有争抢,不是同一个对象锁
* 5. 有两个静态同步方法,一步手机, 请问先打印邮件还是短信?---------先邮件后短信 共用一个类锁
* 6. 有两个静态同步方法,两部手机, 请问先打印邮件还是短信? ----------先邮件后短信 共用一个类锁
* 7. 有一个静态同步方法 一个普通同步方法,请问先打印邮件还是短信? ---------先短信后邮件 一个用类锁一个用对象锁
* 8. 有一个静态同步方法,一个普通同步方法,两部手机,请问先打印邮件还是短信? -------先短信后邮件 一个类锁一个对象锁
*/
public class Lock8Demo {
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(() -> {
phone.sendEmail();
}, "a").start();
try {
TimeUnit.MILLISECONDS.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
phone.sendSMS();
}, "b").start();
}
}
结论:
- 对于普通同步方法,锁的是当前实例对象,通常指this,所有的同步方法用的都是同一把锁--->实例对象本身
- 对于静态同步方法,锁的时当前类的Class对象
- 对于同步方法块,锁的时synchronized括号内的对象
公平锁和非公平锁
公平锁:
- 是指多个线程按照申请锁的顺序来获取锁,这里类似于排队买票,先来的人先买,后来的人再队尾排着,这是公平的
- ----- Lock lock = new ReentrantLock(true)---表示公平锁,先来先得。
非公平锁:
- 是指多个线程获取锁的顺序并不是按照申请的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发环境下,有可能造成优先级反转或者饥饿的状态(某个线程一直得不到锁)
- ---- Lock lock = new ReentrantLock(false)---表示非公平锁,后来的也可能先获得锁,默认为非公平锁。
可重入锁
概念:
- 是指在同一线程在外层方法获取到锁的时侯,在进入该线程的内层方法会自动获取锁(前提,锁对象的是同一个对象),不会因为之前已经获取过还没释放而阻塞---------优点之一就是可一定程度避免死锁。
可重入锁种类:
- 隐式锁(即synchronized关键字使用的锁),默认是可重入锁
- 在一个synchronized修饰的方法或者代码块的内部调用本类的其他synchronized修饰的方法或者代码块时,是永远可以得到锁。
- 显式锁(即Lock)也有ReentrantLock这样的可重入锁
public class ReEntryLockDemo {
public static void main(String[] args) {
final Object o = new Object();
/**
* ---------------外层调用
* ---------------中层调用
* ---------------内层调用
*/
new Thread(() -> {
synchronized (o) {
System.out.println("---------------外层调用");
synchronized (o) {
System.out.println("---------------中层调用");
synchronized (o) {
System.out.println("---------------内层调用");
}
}
}
}, "t1").start();
/**
* 注意:加锁几次就需要解锁几次
* ---------------外层调用
* ---------------中层调用
* ---------------内层调用
*/
Lock lock = new ReentrantLock();
new Thread(() -> {
lock.lock();
try {
System.out.println("---------------外层调用");
lock.lock();
try {
System.out.println("---------------中层调用");
lock.lock();
try {
System.out.println("---------------内层调用");
} finally {
lock.unlock();
}
} finally {
lock.unlock();
}
} finally {
lock.unlock();
}
}, "t2").start();
}
}
死锁及排查
概念:
死锁是指两个或两个以上的线程在执行过程中,因抢夺资源而造成的一种互相等待的现象,若无外力干涉,则它们无法再继续推进下去。
产生原因:
- 系统资源不足
- 进程运行推进顺序不合适
- 系统资源分配不当
case:
/**
* @author Guanghao Wei
* @create 2023-04-10 16:20
*/
public class DeadLockDemo {
static Object a=new Object();
static Object b=new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (a){
System.out.println("t1线程持有a锁,试图获取b锁");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (b){
System.out.println("t1线程获取到b锁");
}
}
},"t1").start();
new Thread(() -> {
synchronized (b){
System.out.println("t2线程持有a锁,试图获取a锁");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (a){
System.out.println("t2线程获取到a锁");
}
}
},"t2").start();
}
}
写锁(独占锁)/读锁(共享锁)
读写锁(readers-writer lock),看英文可以顾名思义,在执行加锁操作时需要额外表明读写意图,复数读者之间并不互斥,而写者则要求与任何人互斥。读写锁不需要特殊支持就可以直接用之前提到的几个东西实现,比如可以直接用两个 spinLock 或者两个 mutex 实现:
void 以读者身份加锁(rwlock) {
加锁(rwlock.保护当前读者数量的锁);
rwlock.当前读者数量 += 1;
if (rwlock.当前读者数量 == 1) {
加锁(rwlock.保护写操作的锁);
}
解锁(rwlock.保护当前读者数量的锁);
}
void 以读者身份解锁(rwlock) {
加锁(rwlock.保护当前读者数量的锁);
rwlock.当前读者数量 -= 1;
if (rwlock.当前读者数量 == 0) {
解锁(rwlock.保护写操作的锁);
}
解锁(rwlock.保护当前读者数量的锁);
}
void 以写者身份加锁(rwlock) {
加锁(rwlock.保护写操作的锁);
}
void 以写者身份解锁(rwlock) {
解锁(rwlock.保护写操作的锁);
}
如果整个场景中只有一个读者、一个写者,那么其实可以等价于直接使用互斥器。不过由于读写锁需要额外记录读者数量,花销要大一点。
自旋锁spinLock
概念:
自旋锁(spinlock)很好理解。对自旋锁加锁的操作,你可以认为是类似这样的:
while (抢锁(lock) == 没抢到) {}
只要没有锁上,就不断重试。显然,如果别的线程长期持有该锁,那么你这个线程就一直在 while while while 地检查是否能够加锁,浪费 CPU 做无用功。
仔细想想,其实没有必要一直去尝试加锁,因为只要锁的持有状态没有改变,加锁操作就肯定是失败的。所以,抢锁失败后只要锁的持有状态一直没有改变,那就让出 CPU 给别的线程先执行好了。这就是互斥器(mutex)也就是题目里的互斥锁(不过个人觉得既然英语里本来就不带 lock,就不要称作锁了吧)。对互斥锁加锁的操作你可以认为是类似这样的:
while (抢锁(lock) == 没抢到) { 本线程先去睡了请在这把锁的状态发生改变时再唤醒(lock);}