1. 乐观锁 和 悲观锁
乐观锁:认为使用数据时,不会有别的线程修改数据,所以不用加锁。
适合于读操作远多于写操作的场景,因为它可以减少锁的开销,提高系统的并发能力。
判断的规则:
- 版本号机制Version
- 使用CAS算法,Java原子类中的递增操作需要CAS自旋实现
悲观锁:认为使用数据,一定会有别的线程修改数据,因此需要在获取加锁,确保数据不会被别的线程所修改。
适合写操作多的场景,先加锁可以保证写操作时的数据正确。
synchronized关键字 和 Lock的实现类都是悲观锁
2. 锁相关的案例
高并发时,同步调用应该去考虑所的性能损耗,能用无锁的数据的结构,就不要用锁;能锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁。(尽可能加锁的代码块小,避免在锁代码块中调用RPC方法)
class Phone //资源类
{
public static 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");
}
}
/**
* 题目:谈谈你对多线程锁的理解,8锁案例说明
* 口诀:线程 操作 资源类
* 8锁案例说明:
* 1 标准访问有ab两个线程,请问先打印邮件还是短信
* 2 sendEmail方法中加入暂停3秒钟,请问先打印邮件还是短信
* 3 添加一个普通的hello方法,请问先打印邮件还是hello
* 4 有两部手机,请问先打印邮件还是短信
* 5 有两个静态同步方法,有1部手机,请问先打印邮件还是短信
* 6 有两个静态同步方法,有2部手机,请问先打印邮件还是短信
* 7 有1个静态同步方法,有1个普通同步方法,有1部手机,请问先打印邮件还是短信
* 8 有1个静态同步方法,有1个普通同步方法,有2部手机,请问先打印邮件还是短信
*
* 笔记总结:
* 1-2
* * * 一个对象里面如果有多个synchronized方法,某一个时刻内,只要一个线程去调用其中的一个synchronized方法了,
* * * 其它的线程都只能等待,换句话说,某一个时刻内,只能有唯一的一个线程去访问这些synchronized方法
* * * 锁的是当前对象this,被锁定后,其它的线程都不能进入到当前对象的其它的synchronized方法
* 3-4
* * 加个普通方法后发现和同步锁无关
* * 换成两个对象后,不是同一把锁了,情况立刻变化。
*
* 5-6 都换成静态同步方法后,情况又变化
* 三种 synchronized 锁的内容有一些差别:
* 对于普通同步方法,锁的是当前实例对象,通常指this,具体的一部部手机,所有的普通同步方法用的都是同一把锁——>实例对象本身,
* 对于静态同步方法,锁的是当前类的Class对象,如Phone.class唯一的一个模板
* 对于同步方法块,锁的是 synchronized 括号内的对象
*
* * 7-8
* * 当一个线程试图访问同步代码时它首先必须得到锁,正常退出或抛出异常时必须释放锁。
* * *
* * * 所有的普通同步方法用的都是同一把锁——实例对象本身,就是new出来的具体实例对象本身,本类this
* * * 也就是说如果一个实例对象的普通同步方法获取锁后,该实例对象的其他普通同步方法必须等待获取锁的方法释放锁后才能获取锁。
* * *
* * * 所有的静态同步方法用的也是同一把锁——类对象本身,就是我们说过的唯一模板Class
* * * 具体实例对象this和唯一模板Class,这两把锁是两个不同的对象,所以静态同步方法与普通同步方法之间是不会有竞态条件的
* * * 但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁。
*/
public class Lock8Demo
{
public static void main(String[] args)//一切程序的入口
{
Phone phone = new Phone();
Phone phone2 = new Phone();
new Thread(() -> {
phone.sendEmail();
},"a").start();
//暂停毫秒,保证a线程先启动
try { TimeUnit.MILLISECONDS.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); }
new Thread(() -> {
//phone.sendSMS();
//phone.hello();
phone2.sendSMS();
},"b").start();
}
}
3. 多线程锁之公平锁和非公平锁
公平锁:是指多个线程按照申请锁的顺序来获取锁,类似排队买票,先来的人先买,这是公平的锁。即先来先得获取锁
Lock lock = new ReentrantLock(true)
非公平锁:是指多个线程获取锁的顺序不是按照申请锁的顺序,有可能后申请线程比先申请线程的优先获取锁,在高并发环境下,有可能造成优先级翻转或者饥饿状态(某个线程一直得不到锁)
即后来可能先获取锁
Lock lock = new ReentrantLock(false)
Lock lock = new ReentrantLock() //默认非公平锁
为什么ReetrantLock默认创建是非公平锁?
原因1:恢复挂起的线程到真正锁的获取是有时间差的,从开发人员1来看这个时间微乎其微,但是从CPU的角度看这个时间差存在明显,所以非公平锁更能充分的利用CPU的时间片,尽量减少CPU空闲状态时间。
原因2:使用多线程重要的考察点是线程切换的开销,当使用非公平,当1个线程请求获取同步状态,然后释放同步状态,所以刚释放锁的线程在此此刻再次获取同步状态的概率变得非常大,减少了线程的开销。
原因3:为了更高的吞吐量,非公平锁比较合适,因为节省了很多线程切换时间,吞吐量自然就上去了。
4. 可重入锁(递归锁)
可重入锁是指在同一个线程在外层获取锁时候,再进入该线程的内层方法自动获取锁(前提,锁的对象是同一个对象),不会因为之前已经获取还没有释放而阻塞。
Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点可一定程度避免死锁。
5. 死锁以及排查
死锁是指两个或两个以上的线程在执行过程中,因争抢资源而造成的一种互相等待的现象,如果没有外力的干涉,它们都无法继续推进下去。如果系统资源充足,进程的资源请求电动得到满足,死锁出现的可能性就很低,否则会争抢有限的资源而陷入死锁。每个线程都在等待其他线程释放锁,但是这些锁永远不会被释放。
死锁示例代码:
public class DeadLockDemo
{
public static void main(String[] args)
{
final Object objectA = new Object();
final Object objectB = new Object();
new Thread(() -> {
synchronized (objectA){
System.out.println(Thread.currentThread().getName()+"\t 自己持有A锁,希望获得B锁");
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
synchronized (objectB){
System.out.println(Thread.currentThread().getName()+"\t 成功获得B锁");
}
}
},"A").start();
new Thread(() -> {
synchronized (objectB){
System.out.println(Thread.currentThread().getName()+"\t 自己持有B锁,希望获得A锁");
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
synchronized (objectA){
System.out.println(Thread.currentThread().getName()+"\t 成功获得A锁");
}
}
},"B").start();
}
}
死锁的排查方法一(命令行):
显示Found 1 deadlock 发现一个死锁
死锁排查方法二(图形化):
6. ObjectMonitor
ObjectMonitor是Java中用于并发控制的一个关键概念。它通常被描述为一个同步工具或机制,每个Java对象都有一把内置的锁,即内部锁或Monitor锁。ObjectMonitor主要是在HotSpot虚拟机的底层C语言中实现的。
ObjectMonitor的主要功能包括:
- 线程等待:当锁已被其他线程获取时,期待获取锁的线程会进入Monitor对象的监控区(Entry Set)。
- 线程唤醒:当线程在待授权区(Wait Set)等待时,可以通过notify或notifyAll方法唤醒。
- 锁的重入:允许同一个线程多次获取同一把锁。
ObjectMonitor关键属性:
- owner:指向当前持有锁的线程,确保互斥性。
- _WaitSet:等待队列,包含调用
wait()
后等待的线程。 - _EntryList:入口队列,包含等待获取锁的线程。处于阻塞状态。
- _recursions:记录锁的重入次数,支持可重入性。
- count:用于记录该线程获取锁的次数。