多线程-synchronized(一)

在这里插入图片描述

乐观锁和悲观锁

悲观锁

认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。

synchronized关键字和Lock的实现类都是悲观锁

适合写操作多的场景,先加锁可以保证写操作时数据正确,显式的锁定之后再操作同步资源

乐观锁

认为自己在使用数据时不会有别的线程修改数据或资源,在Java中是通过使用无锁编程来实现,只是在更新数据的时候去判断,之前有没有別的线程更新了这个数据。所以不会添加锁。

如果这个数据没有被更新,当前线程将自己修改的数据成功写入。
如果这个数据已经被其它线程更新,则根据不同的实现方式执行不同的操作,比如放弃修改、重试抢锁等等

适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。
乐观锁则直接去操作同步资源,是一种无锁算法,属于那种得之我幸不得我命,再努力就行的佛性锁

乐观锁一般有两种实现方式:
1、采用Version版本号机制
2、CAS(Compare-and-Swap,即比较并替换)算法实现

八种锁运行案例,锁的到底是什么

【强制】高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁。
说明:尽可能使加锁的代码块工作量尽可能的小,避免在锁代码块中调用 RPC 方法。

以下八种案例就是围绕【能用对象锁,不要用类锁】来说明

1、标准访问有ab两线程,先打印谁

启用两个不同的线程去调用,并再第二个线程调用前,休眠一段时间,那么到底是什么执行顺序呢?

class Phone //资源类
{
    public synchronized void sendEmail() {
        System.out.println("-----sendEmail");
    }

    public synchronized void sendSMS() {
        System.out.println("-----sendSMS");
    }

}
public class TestDemo {
    public static void main(String[] args) {
        Phone phone = new Phone();
        new Thread(() -> {
            phone.sendEmail();
        }, "a").start();

        try {
            TimeUnit.MILLISECONDS.sleep(200);
        } catch (Exception e) {
            e.printStackTrace();
        }

        new Thread(() -> {
            phone.sendSMS();
        }, "b").start();

    }
}

效果:
在这里插入图片描述

2、sendEmail方法加暂停3s,先打印谁

class Phone //资源类
{
    public synchronized void sendEmail() {
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("-----sendEmail");
    }

    public synchronized void sendSMS() {
        System.out.println("-----sendSMS");
    }

}
public class TestDemo {
    public static void main(String[] args) {
        Phone phone = new Phone();
        new Thread(() -> {
            phone.sendEmail();
        }, "a").start();

        try {
            TimeUnit.MILLISECONDS.sleep(200);
        } catch (Exception e) {
            e.printStackTrace();
        }

        new Thread(() -> {
            phone.sendSMS();
        }, "b").start();

    }
}

效果图
在这里插入图片描述

3、添加一个普通hello方法,先打印谁

class Phone //资源类
{
    public synchronized void sendEmail() {
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("-----sendEmail");
    }

    public synchronized void sendSMS() {
        System.out.println("-----sendSMS");
    }

    public void hello(){
        System.out.println("-----hello");
    }

}
public class TestDemo {
    public static void main(String[] args) {
        Phone phone = new Phone();
        new Thread(() -> {
            phone.sendEmail();
        }, "a").start();

        try {
            TimeUnit.MILLISECONDS.sleep(200);
        } catch (Exception e) {
            e.printStackTrace();
        }

        new Thread(() -> {
            //phone.sendSMS();
            phone.hello();
        }, "b").start();

    }
}

效果:
在这里插入图片描述

4、有2个手机,先打印谁

class Phone //资源类
{
    public synchronized void sendEmail() {
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("-----sendEmail");
    }

    public synchronized void sendSMS() {
        System.out.println("-----sendSMS");
    }

    public void hello(){
        System.out.println("-----hello");
    }

}
public class TestDemo {
    public static void main(String[] args) {
        Phone phone = new Phone();
        Phone phone2 = new Phone();
        new Thread(() -> {
            phone.sendEmail();
        }, "a").start();

        try {
            TimeUnit.MILLISECONDS.sleep(200);
        } catch (Exception e) {
            e.printStackTrace();
        }

        new Thread(() -> {
            //phone.sendSMS();
            //phone.hello();
            phone2.sendSMS();
        }, "b").start();

    }
}

效果图:
在这里插入图片描述

5、2个静态同步方法,1台手机,先打印谁

class Phone //资源类
{
    public static synchronized void sendEmail() {
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("-----sendEmail");
    }

    public static synchronized void sendSMS() {
        System.out.println("-----sendSMS");
    }

    public void hello(){
        System.out.println("-----hello");
    }

}
public class TestDemo {
    public static void main(String[] args) {
        Phone phone = new Phone();
        Phone phone2 = new Phone();
        new Thread(() -> {
            phone.sendEmail();
        }, "a").start();

        try {
            TimeUnit.MILLISECONDS.sleep(200);
        } catch (Exception e) {
            e.printStackTrace();
        }

        new Thread(() -> {
            phone.sendSMS();
            //phone.hello();
            //phone2.sendSMS();
        }, "b").start();

    }
}

效果图
在这里插入图片描述

6、2个静态同步方法,2台手机,先打印谁

class Phone //资源类
{
    public static synchronized void sendEmail() {
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("-----sendEmail");
    }

    public static synchronized void sendSMS() {
        System.out.println("-----sendSMS");
    }

    public void hello(){
        System.out.println("-----hello");
    }

}
public class TestDemo {
    public static void main(String[] args) {
        Phone phone = new Phone();
        Phone phone2 = new Phone();
        new Thread(() -> {
            phone.sendEmail();
        }, "a").start();

        try {
            TimeUnit.MILLISECONDS.sleep(200);
        } catch (Exception e) {
            e.printStackTrace();
        }

        new Thread(() -> {
            //phone.sendSMS();
            //phone.hello();
            phone2.sendSMS();
        }, "b").start();

    }
}

效果图
在这里插入图片描述

7、1个静态同步方法,1个普通同步方法,先打印谁

class Phone //资源类
{
    public static synchronized void sendEmail() {
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("-----sendEmail");
    }

    public  synchronized void sendSMS() {
        System.out.println("-----sendSMS");
    }

    public void hello(){
        System.out.println("-----hello");
    }

}
public class TestDemo {
    public static void main(String[] args) {
        Phone phone = new Phone();
        Phone phone2 = new Phone();
        new Thread(() -> {
            phone.sendEmail();
        }, "a").start();

        try {
            TimeUnit.MILLISECONDS.sleep(200);
        } catch (Exception e) {
            e.printStackTrace();
        }

        new Thread(() -> {
            phone.sendSMS();
            //phone.hello();
            //phone2.sendSMS();
        }, "b").start();

    }
}

效果图
在这里插入图片描述

8、1个静态同步方法,1个普通同步方法,2个手机,先打印谁

class Phone //资源类
{
    public static synchronized void sendEmail() {
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("-----sendEmail");
    }

    public  synchronized void sendSMS() {
        System.out.println("-----sendSMS");
    }

    public void hello(){
        System.out.println("-----hello");
    }

}
public class TestDemo {
    public static void main(String[] args) {
        Phone phone = new Phone();
        Phone phone2 = new Phone();
        new Thread(() -> {
            phone.sendEmail();
        }, "a").start();

        try {
            TimeUnit.MILLISECONDS.sleep(200);
        } catch (Exception e) {
            e.printStackTrace();
        }

        new Thread(() -> {
            //phone.sendSMS();
            //phone.hello();
            phone2.sendSMS();
        }, "b").start();

    }
}

效果图
在这里插入图片描述

八锁原理总结

首先得先记住一个原则,synchronized是悲观锁,再用悲观锁的理念去理解以下就比较明白了

1-2:一个对象里面如果有多个synchronized方法,某一时刻,只要有一个线程去调用其中一个synchronized方法,其他线程只能等待,换句话说,某一时刻内,只能有唯一的线程去访问这些synchronized方法,synchronized锁的是当前对象this,被锁定后,其他线程都不能进入到当前对象的其他synchronized方法。由此可见,new phone(),这是锁的对象锁

3-4:一个加锁,一个不加锁,同一对象调用,不存在锁争夺情况;两个不同对象调用同一把锁,但是synchronized锁的是对象,两者之间也不存在锁争抢情况,所以加普通方法后发现和同步锁无关,换乘两个对象后,不是同一把锁,情况立刻变化

5-6:都换成静态同步方法后,synchronized锁的内容有一些差别;对于普通同步方法,锁的是当前对象,通常指this,也就是new Phone(),锁的右边实例化出来的对象;静态同步方法,锁的是当前类的class对象,也就是Phone phone=new Phone(),最左边的Phone,Phone.class模板,再直白点就是LIst list =new ArrayList(),静态同步方法锁的是List,是类锁

7-8:当一个线程试图访问同步代码时,它首先必须得到锁,正常退出或抛出异常时必须释放锁

  • 所有的普通同步方法用的都是同一把锁一实例对象本身,就是new出来的具体实例对象本身,本类this也就是说如果一个实例对象的普通同步方法获取锁后,该实例对象的其他普通同步方法必须等待获取锁的方法释放锁后才能获取锁。
  • 所有的静态同步方法用的也是同一把锁一类对象本身,就是我们说过的唯一板class具体实例对象this和唯一模板class,这两把锁是两个不同的对象,所以静态同步方法与普通同步方法之问是不会有意态条件的但是一旦一个静态同步方法队取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁。

以上来个简短点的总结:

  • 作用于实例方法,当前实例加锁,进入同步代码前要获取当前实例的锁
  • 作用于代码块,对括号里配置的对象加锁
  • 作用于静态方法,当前类加锁,进去同步代码前要获得当前类对象的锁

从字节码角度分析synchronized实现

javap -c xxxx.class反编译
javap -v xxx.class 输出附加信息(包括行号、本地变量表、反汇编等信息)

同步代码块层面

public class LockSyncDemo {
    Object object = new Object();

    public void m1(){
        synchronized (object){
            System.out.println("-----hello");
        }
    }
    public static void main(String[] args) {

    }
}

点击main运行,再找到文件路径输入命令
在这里插入图片描述
在这里插入图片描述

反编译得到字节码如图:
在这里插入图片描述
图上我们可以看到m1打印的方法附近有monitor,有开始跟退出,那么我们就知道原来synchronized底层是用monitor来控制一退一出,但是为什么下面还有个monitorexit呢?

多出来的monitorexit其实就有点像throw,如果按照正常逻辑monitorenter进,monitorexit出,是没问题了,但是如果同步代码块里发生了异常,锁就无法释放,所以就要有个异常处理的,如果发生异常就由最后一个monitorexit退出,并抛出异常,如下图
在这里插入图片描述
但是一定是一个monitorenter两个monitorexit吗?不一定,极端情况如下
在这里插入图片描述
在这里插入图片描述
只有一个monitorenter一个monitorexit,两个athrow

同步方法层面

public class LockSyncDemo {
    Object object = new Object();

    public void m1() {
        synchronized (object) {
            System.out.println("-----hello");
            throw new RuntimeException("----exp");
        }
    }

    public synchronized void m2() {
        System.out.println("-----hello synchronized m2");
    }

    public static void main(String[] args) {

    }
}

在这里插入图片描述
有个ACC_SYNCHRONIZED,这个就是c++里面用来标识同步方法

静态同步方法

在这里插入图片描述

在这里插入图片描述
反编译知道static,在字节码里面是ACC_STATIC来区分,就以这个来区分是否静态同步方法锁的是类还是对象

反编译synchronized锁的是什么

首先得知道一个知识,synchronized锁是由管程(Monitor)来实现的。
在这里插入图片描述
在这里插入图片描述

在jdk8中,在HotSport虚拟集中,monitor采用ObjectMonitor实现,那么我们来看看c++中是怎么来实现的
在这里插入图片描述
主要是这块,这就是为什么一个对象加上synchronized就能被锁住的原因
每个对象天生都带着一个对象监视器
每一个被锁住的对象都会和monitor关联起来
owner,就代表着这个锁是谁的
在这里插入图片描述

公平锁和非公平锁

ReentrantLock 默认非公平锁


class Ticket{
    private int number = 50;
    ReentrantLock lock = new ReentrantLock();

    public void sale() {
        lock.lock();
        try {
            if (number > 0) {
                System.out.println(Thread.currentThread().getName() + "卖出第:" + (number--) + "\t 还剩下" + number);
            }
        } finally {
            lock.unlock();
        }
    }
}

public class TicketDemo {
    public static void main(String[] args) {
        final Ticket ticket = new Ticket();
        new Thread(()-> {
            for (int i = 0; i < 55; i++) {
                ticket.sale();
            }
        },"a").start();
        new Thread(()-> {
            for (int i = 0; i < 55; i++) {
                ticket.sale();
            }
        },"b").start();
        new Thread(()-> {
            for (int i = 0; i < 55; i++) {
                ticket.sale();
            }
        },"c").start();
    }


}

在这里插入图片描述
基本都是a跟c卖票,b没有抢到票卖

那么想要公平点,别都a、c卖票,b也想卖票,那么怎么改呢,在ReentrantLock里面传true,就是公平锁机制,这时候我们来看看

ReentrantLock lock = new ReentrantLock(true);

在这里插入图片描述
可以看到,刚开始都是a抢到了票,后面基本是轮询方式abc轮着卖票了,那么我们来个总结

公平锁:是指多个线程按照申请锁的顺序来获取锁,这里类似排队买票,先来的人先买后来的人在队尾排着,这是公平的Lock lock =new ReemtrantLock(true);//true 表示公平锁,先来先得

非公平锁:是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发环境下,有可能造成优先级翻转或者饥饿的状态(某个线程一直得不到锁)Lock lock = new ReentrantLock(false);//false 表示非公平锁,后来的也可能先获得锁Lock lock =new ReentrantLock();//默认非公平锁

为什么会有公平锁/非公平锁的设计?

  • 恢复挂起的线程到真正锁的获取还是有时问差的,从开发人员来看这个时问微乎其微,但是从CPU的角度来看,这个时问差存在的还是很明显的。
    所以非公平锁能更充分的利用CPU的时间片,尽量减少CPU空状态时间

  • 使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当1个线程请求锁获取同步状态,然后释放同步状态,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得非常大,所以就减少了线程的开销

什么时候用公平锁/非公平锁?

看业务场景而定,拿卖票来说,我的主要目的是卖票,卖票卖的越快越好,我不关心谁的能力卖的多卖得少,那么就采用非公平锁(减少线程开销、减少cpu空闲状态时间)

如果为了更高的吞吐量,很显然非公平锁是比较合适的,因为节省跟多线程切换时问,吞吐量自然就上去了:否则那就用公平锁,大家公平使用。

预埋AQS

为什么new ReentrantLock(true) 和new ReentrantLock()就能实现公平锁和非公平锁呢,这里就涉及到AQS,关于AQS会专门出一篇文章来说明,在这里先混个眼熟

在这里插入图片描述

可重入锁(又名递归锁)

是指在同一个线种在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。

如果是1个有 synchronized 修饰的递归调用方法,程序第2次进入被自己阻塞了岂不是天大的笑话,出现了作茧自缚。
所以Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。
在这里插入图片描述

结合上图理解,锁的都是object对象,那么如果出现多个锁的情况下,进入内部方法锁的时候,无需等待上一把锁还没释放就直接进入

可重入锁的种类

隐式和显式锁

隐式锁(即synchronized关键字使用的锁)默认是可重入锁

指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。简单的来说就是:在一个synchronized修饰的方法或代码块的内部调用本类的其他synchronized修饰的方法或代码块时,是永远可以得到锁的

案例说明

代码块重入锁:
在这里插入图片描述
方法重入锁:
在这里插入图片描述
以上都无死锁情况发生,程序正常运行

Synchronized的重入的实现机理

为什么任何一个对象都可以成为一个锁?上面也讲过可以通过看c++底层字节码objectMonitor.hpp文件,有几个主要属性
在这里插入图片描述
每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。
当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虛拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1。

在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么 java 虚拟机可以将其计数器加1,否则需要等待。直至持有线程释放该锁。

当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已被释放。

显式锁(即Lock)也有ReentrantLock这样的可重入锁

这里就需要注意和synchronized不同,synchronized是java自身帮我们实现好了的,ReentrantLock是需要我们自己控制,锁了几次就要释放几次

案例演示

在这里插入图片描述
可重入效果如图,正常执行,那么如果不按照加锁释放锁呢?我注释掉一个锁释放看看
在这里插入图片描述
执行起来很正常,也无报错,但是目前只是一个线程,如果我多加一个线程呢
在这里插入图片描述
此时问题出现:
1、主程序一直无法结束
2、t3线程无法执行,一直卡在t2

由于加锁和释放锁的次数不一致,所以就会导致一直在等待
所以进行显式加锁一定要记得,lock和unlock一 一配对

死锁及排查

死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那它们都将无法推进下去,如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。
在这里插入图片描述
那么死锁产生主要原因有哪些呢?

  • 系统资源不足
  • 进程运行推进的顺序不合适
  • 资源分配不当

案例演示

按照上面的架构图,搞个demo
在这里插入图片描述

这里就产生了死锁

死锁排查

根据上面死锁案例来排查,有两种,jdk自带的jstack和jconsole
在这里插入图片描述

jstack 纯命令方式

输入jps -l 获取进程号,然后再jstack进程号
在这里插入图片描述
就能发现提示语写着,发现一个死锁,a锁着f548,b等着f548
在这里插入图片描述

jconsole图形化方式

cmd jconsole
在这里插入图片描述
选择自己写的类,点击连接
在这里插入图片描述
选择线程,点击检测死锁
在这里插入图片描述
就能看到死锁信息
在这里插入图片描述

其他锁

  • 写锁(独占锁)/读锁(共享锁)
  • 自旋锁SpinLock
  • 无锁->独占锁->读写锁->邮戳锁
  • 无锁->偏向锁->轻量锁->重量锁

以上锁本章节暂不讲解,这些内容将等把线程中断、JMM、AQS、CAS、volatile等出完后,再以单篇逐个详细说明,因为会涉及到更加深层次的内容,在这里也是先混个眼熟,可以先订阅JUC并发编程的专栏,JUC相关内容都会放这里面

总结

指针指向monitor对象(也称为管程或监视器锁)的起始地址。每个对象都存在着一个monitor与之关联,当一个 monitor 被某个线程持有后,它便处于锁定状态。在Java虚拟机(HotSpot)中,monitor是由ObiectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的)

在这里插入图片描述
在这里插入图片描述
就先说到这 \color{#008B8B}{ 就先说到这} 就先说到这
在下 A p o l l o \color{#008B8B}{在下Apollo} 在下Apollo
一个爱分享 J a v a 、生活的小人物, \color{#008B8B}{一个爱分享Java、生活的小人物,} 一个爱分享Java、生活的小人物,
咱们来日方长,有缘江湖再见,告辞! \color{#008B8B}{咱们来日方长,有缘江湖再见,告辞!} 咱们来日方长,有缘江湖再见,告辞!

在这里插入图片描述

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值