JUC(一)

一、JUC

JUC是java.util.conccurent包的简称,juc提供了很多关于线程安全的集合实现,以及各种锁的实现。
juc视频教程

二、多线程相关知识

1.并发

同一台处理器上 “同时” 处理多个任务,为什么同时需要打引号呢?因为实际上同一时刻实际上只有一个事件在发生。处理器是通过不断切换占用cpu时间片的任务来实现同时处理多个任务的。
在这里插入图片描述

2.并行

多台处理器同时处理多个任务,同一时刻,每个处理器都在处理不同的任务。
在这里插入图片描述

3.进程

是并发执行的程序在执行过程中系统分配和管理资源的基本单位

4.线程

包含在进程中,是cpu调用的最小单元,也称轻量级进程

5.管程

实际上指的是锁(monitor),管程只会允许一个线程进入去获取共享资源

6.进程VS线程

  • 内存空间及系统资源
    进程之间拥有独立的空间、系统资源,同一个进程中的线程则是共享进程的内存空间、系统资源
  • 健壮性
    一个进程崩溃之后不会影响其它进程,同一个进程中的线程崩溃后会导致进程中的其它线程受到影响。

7.用户线程

一般情况下不做特殊说明,线程默认都是用户线程

8.守护线程

守护线程是一个服务线程,他服务于用户线程,如果用户线程结束了,守护线程也就结束了。
在java中,jvm的gc垃圾回收线程就是守护线程,当主线城结束后,垃圾回收线程也会结束。
下面用一个例子来演示守护线程
创建一个线程类,在其中执行打印操作1-100

/**
 * @author Watching
 * * @date 2023/4/29
 * * Describe:
 */
public class MyThread extends Thread{
    @Override
    public void run() {
        for(int i = 1;i<= 100;i++){
            System.out.println(getName()+" helloWord " + i);
        }
    }
}

main方法,创建两个线程,一个女神线程,一个舔狗线程,女神先开始打印,1s后舔狗开始打印

/**
 * @author Watching
 * * @date 2023/4/29
 * * Describe:
 */
public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        MyThread myThread1 = new MyThread();
        MyThread myThread2 = new MyThread();
        myThread1.setName("女神");
        myThread2.setName("舔狗");
        myThread2.setDaemon(true);
        myThread1.start();//女神开始打印
        Thread.sleep(1000);
        myThread2.start();//舔狗开始打印
    }
}

查看结果:发现舔狗没有打印完100次就结束了,因为他打印17次之后发现女神线程结束了,所以舔狗线程也就结束了。
在这里插入图片描述

三、CompletableFuture

可以查看我另外一篇文章
CompletableFuture

四、锁

1.悲观锁VS乐观锁

  • 悲观锁
    认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。synchronized和lock接口都是悲观锁,适合用于写多的操作。
  • 乐观锁
    认为自己在使用数据的时候不会有其它线程修改数据,所以不会添加锁,只有在更新数据的时候才会去判断,这个数据相比于自己查到的时候有没有被修改。
    如果数据相比于自己查到的时候没有被修改,则将数据修改为自己线程的数据。
    反之,可以选择放弃修改数据,或者自旋重新尝试修改数据。
    通常会采用版本号机制cas算法
    适合用于读多的操作。

2.synchronized(1.6)

sychronized是java提供的一个关键字,在jdk1.6之前它是一把重量级锁
第一个线程获取锁之后(在底层获取monitor这把锁),然后其他未获取到锁的线程会被添加到一个就绪队列中(其中还存在一个等待队列,线程处于阻塞状态时会被放入等待队列,等待被唤醒之后放入就绪队列),等待锁被释放后再次去竞争这把锁。

由于涉及到线程的状态切换,比较耗费系统资源,所以synchronized这把锁的代价很大。
在这里插入图片描述

2.0、管程(monitor)(待完善)

下面就来详细讲一下synchronized在底层是如何获取monitor这把锁的

2.1、synchronized作用在代码块时

它的底层是通过monitorentermonitorexit指令来实现的。

monitorenter:

每个对象都是一个监视器锁(monitor),当monitor被占用时就会处于锁定状态,线程执行
monitorenter指令时尝试获取monitor的所有权,过程如下:

如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor
的所有者。如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1。如果其
他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获
取monitor的所有权。

monitorexit:

执行monitorexit的线程必须是objectref所对应的monitor持有者。
指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个monitor的所有权。
monitorexit指令出现了两次,第1次为同步正常退出释放锁,第2次为发生异常退出释放锁。

2.2、方法的同步

并没有通过 monitorenter 和 monitorexit 指令来完成,不过相对于普通方法,其常量
池中多了 ACC_SYNCHRONIZED 标示符。JVM就是根据该标示符来实现方法的同步的:
当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。

2.3、总结

两种同步方式本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。两个指令的执行是JVM通过调用操作系统的互斥原语mutex来实现,被阻塞的线程会被挂起、等待重新调度,会导致“用户态和内核态”两个态之间来回切换,对性能有较大影响。

2.4、javap -v

为了更直观的看见monitorenter、monitorexit、ACC_SYNCHRONIZED这些指令和标识,我们可以使用java提供的javap -v指令将字节码文件反编译。
这里是包含同步代码块、同步方法、静态同步方法的类:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package com.bilibili.juc.locks;

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

    public LockSyncDemo() {
    }

//同步代码块
    public void m1() {
        synchronized(this.object){}

        try {
            System.out.println("----hello synchronized code block");
            throw new RuntimeException("-----exp");
        } finally {
            ;
        }
    }
//同步方法
    public synchronized void m2() {
        System.out.println("----hello synchronized m2");
    }
//静态同步类
    public static synchronized void m3() {
        System.out.println("----hello static synchronized m3");
    }

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

将其字解码反编译:
在这里插入图片描述

m1
在这里插入图片描述
m2、m3
在这里插入图片描述

3.公平锁Vs非公平锁

3.1公平锁

公平锁是指几个线程共同竞争锁,如果竞争到了锁,则占有,没有竞争到锁的线程会被依次存放在一个FIFO的双向链表中,等占有锁的线程执行完任务释放锁之后,排在双向链表头结点后一个的线程被唤醒再来获取锁。如此反复。

双向链表图:
head处存的是站有锁的线程的信息
在这里插入图片描述

3.2非公平锁

非公平锁是指几个线程共同竞争锁,如果锁已经被一个线程占用了,后续的线程不会像公平锁一样直接依次存放在FIFO的双向链表中,而是每次来一个线程都要尝试获取锁,如果没获取到则依次存入双向链表,如果获取到了那就直接占有锁,尽管双向链表中还有线程在等待,可以理解为插队了。

在java锁的实现如synchronized和ReentrantLock中,synchronized是非公平锁,而ReentrantLock默认是非公平锁。
为什么都要优先使用非公平锁呢?
因为这涉及到一个效率问题,公平锁中如果锁当前被占用,后续的线程会在双向队列中阻塞等待,锁被释放后,再从双向队列中唤醒下一个线程,这就涉及到了上下文切换,核心态到用户态的切换,效率更低。
而非公平锁,在当前锁被占用的情况下,后续线程来了会直接尝试获取锁,如果恰好锁被释放了,后续线程就可以直接获取锁执行操作,避免了上下文切换。

4.重入锁

重入锁是指线程已经获取了锁,这个线程再次获取这把锁不会阻塞。

4.1synchronized (隐式重入

java中的sychronized和Lock都是重入锁实现。
示例:
在方法reEntryM1()中使用同一个线程嵌套使用Synchronized,结果正常执行

package com.bilibili.juc.locks;
/**
 * @auther zzyy
 * @create 2022-01-18 18:09
 */
public class ReEntryLockDemo {
    public static void main(String[] args) {
        reEntryM1();
    }
    private static void reEntryM1() {
        final Object object = new Object();

        new Thread(() -> {
            synchronized (object) {
                System.out.println(Thread.currentThread().getName() + "\t ----外层调用");
                synchronized (object) {
                    System.out.println(Thread.currentThread().getName() + "\t ----中层调用");
                    synchronized (object) {
                        System.out.println(Thread.currentThread().getName() + "\t ----内层调用");
                    }
                }
            }
        }, "t1").start();
    }
}

在这里插入图片描述

4.2 实现原理

在前面讲synchronized关键字的时候我们提到了monitor这个底层的锁,其实在底层的ObjectMonitor.hpp(锁对象monitor)中有很多属性,其中有几个关键属性列在下面表格中。

在这里插入图片描述
注意看到有一个属性叫 _recursions,当同一个线程获取已经被该线程获取到的锁时,该锁对象会调用monitorenter并且_recursions+1,当释放锁时,调用monitorexit并且_recursions-1。如果recursions=0了,则当前锁已经完全被释放了。
注意:
但是在判断当前锁是否已经被线程占用了不是用_recursions字段,而是判断_count字段是否尾0,monitorenter,_count++; monitorexit,_count–;
现在我们按照这个思路来解释一下上面的代码:
在这里插入图片描述

4.3 Lock (显示重入

lock接口需要手动释放锁,为了避免死锁,每次lock()之后最好在finally中unlock()

    static Lock lock = new ReentrantLock();
    public static void main(String[] args) {
        ReEntryLockDemo reEntryLockDemo = new ReEntryLockDemo();
        new Thread(() -> {
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + "\t ----come in外层调用");
                lock.lock();
                try {
                    System.out.println(Thread.currentThread().getName() + "\t ----come in内层调用");
                } finally {
                    lock.unlock();
                }
            } finally {
                // 由于加锁次数和释放次数不一样,第二个线程始终无法获取到锁,导致一直在等待。
                lock.unlock();// 正常情况,加锁几次就要解锁几次
            }
        }, "t1").start();
    }

加锁和解锁需要一一对应
在这里插入图片描述

4.4 死锁产生的原因以及排查手法

死锁产生的原因可以用一句话概括:我占用着你需要的资源,你占用着我需要的资源,并且彼此都需要两个资源才能完成任务。
代码演示:
线程A此时需要锁对象objectB,但是锁对象objectB被B线程占用着。
线程B此时需要锁对象objectA,但是锁对象objectA被A线程占用着。

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();
    }
}

运行结果:
死锁了。在这里插入图片描述
那么在开发中遇到死锁我们该如何排查呢?
方式一:命令终端
①jps -l
找到当前运行的进程编号 15108
在这里插入图片描述
②jstack 15108
找到对应信息
在这里插入图片描述
在信息的最下面还会告诉存在几个死锁
在这里插入图片描述
方式二:图形化
①jconsole打开java监管图形化界面
在这里插入图片描述
②找到进程
在这里插入图片描述
找到以下模块,可以看见死锁的线程。
在这里插入图片描述

5.synchronized加锁解锁流程图

解锁步骤recursions应该是- -,写错了
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值