JUC线程学习

AQS相关

AQS全称是 AbstractQueuedSynchronizer ,翻译叫抽象队列同步器,说到抽象其实就是一个模板,或者是框架,理解为半成品模板都可以。

原理的话,可以这么讲,他的目的就是保证多个线程来夺取资源嘛,然后在AQS里面资源的具体实现就是一个state 的变量,然后就是为了实现了一个同步,他还维护了一个队列,就是让那些没有获取到锁的线程在里面排队,为了方便操作,里面有一个头节点和尾节点指向第一个和最后一个线程。
这就是一个简单的理解,这种结构的话是跟那个CLH一样了,但是AQS的底层跟CLH是有些不一样的,就比如说,线程获取到锁的操作在CLH里面是自旋获取,这种就是会浪费cpu资源,AQS对其做了一定的改进,对于没有获取锁的线程,会阻塞然后等待被唤醒。

并发 锁机制 线程池练手题
模拟10个线程对int 变量 i 各加10w次,累计100w次,观察最后的结果是多少?并思考现象背后的原理

最后的结果是远远小于100w的,背后就是多个线程间没有实现对资源的合理修改,没有实现原子性,顺序性,可见性中的条件。

-> 这时候可以说一说JMM,Java内心模型,为什么会这样,就是由于,他很多次只修改了本地的副本,然后把修改提交给了主内存区域,并且可见性也没有保证,造成大量无效的i++。

如何解决并发执行i++的线程安全问题?

使用锁来解决

经典的锁,syncronized ,ReentreaLock,或者使用原子变量AtomicInteger都可以。

手动实现一个饿汉模式单例,DCL 双重检查锁形式,并且逐行代码分析,为什么要使用volatile关键字,用到了哪些特性?为什么要双重检查?如果不进行双重检查可能会有哪些问题?
class Singleton {
    private static Singleton instance;

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

首先为什么要用volatile 关键字,这里涉及到一个指令重排序的问题,一般来说JVM创建一个对象,正常是走

1.为对象分配内存,2.初始化对象,3让指针指向这个对象。

在单线程下,jvm会在不影响结果的前提下为了效率,将其中的一些顺序进行调换,可能就变成了1 , 3 , 2

这时候出现一个问题,就是我这个对象还没有被初始化,里面的属性还没有,就已被指向了。

在多线程环境下,如果一个线程执行到3,另一个线程刚好执行到第一次检查,发现instance不为null,就直接返回instance,此时得到的Singleton实例其实是未初始化的。这就有问题了。


这时候为了解决这个问题,Java语言就实现了一种名为内存屏障的东西,分别就有 读读,读写,写写屏障,

恰好volatile 就是内存屏障的一种实现,他保证了可见性和有序性。

再提一嘴,JMM就是为了实现这一类屏障啥的而出现的概念,标准。

为什么需要两次检查?

先说结论,再说原因:

第一次是为了提高效率,避免每次有并发的时候都可能出现的阻塞现象

第一次是为了保证单例,避免出现极端情况,两个线程创建了两个对象, 第一个对象被覆盖。


再次总结,volatile 的作用其实是防止出现指令重排序,导致前期可能会有线程拿到一个未初始化的单例对象。

总结一下原因:

第一次检查会出现的情况就是,前提是已经创建好了单例,并且把一个检查去掉了哈,第一个线性先进入到了锁里面,这时第二个甚至更多的线程,来获取锁的时候就会被阻塞。

第二个情况:我们把第二个检查给去掉,此时有两个线程极快的通过了第一次校验,然后第一个线程进去了,创建单例,此时第二个线程就阻塞获取锁了,接着第一个线程创建好了单例,把锁放回去了, 第二个线程又能进去了,又创建了单例对象,并且把指针指向他创建的单例对象了。

学习博客:Java高频面试题:在DCL单例写法中,为什么主要做两次检查?_单例模式为什么要双重检查-CSDN博客
高并发下双重检测锁DCL指令重排问题剖析_dcl双重检查锁-CSDN博客

用至少2种方法实现三个线程顺序打印a,b,c ,并且能够说明线程之间唤醒的时序流程

我记得是有一个wait() 和nofity的方法,能够唤醒,之前有写过但是忘记了。

简单复习一下,粘贴一下以前写的关键代码 ,这是两个线程循环执行100次的代码

synchronized (lock){
    for (int i = 0; i < 50; i++) {
        try{
            System.out.println("ThreadA " + i);
            Thread.sleep(1000);
            lock.notify(); // 唤醒B线程
            lock.wait(); // 等待线程 ,同时释放锁 ,不手动也释放也行
        } catch (InterruptedException e){
            throw new RuntimeException(e);
        }

    }
    lock.notifyAll(); // 唤醒所有线程
}
/***************************************************************************/
synchronized(lock){
	System.out.println("a");
	Thread.sleep(1000);
    lock.notify();
}
//说明,这是a的实现,b的实现可以就是把"a"改为"b" ,1000 改为2000

这是第一种,lock其实就是一个对象,这种是用了对象锁,wait()就会释放掉锁。

第二种,利用经典的ReentranLock 来实现, ,关键代码

reentrantLock.lock();
try{
    System.out.println("a");
    Thread.sleep(1000);
    reentrantLock.unlock();
} catch (InterruptedException e) {
    throw new RuntimeException(e);
}

//说明,这是a的实现,b的实现可以就是把"a"改为"b" ,1000 改为2000
思考一个场景,假设说现在不定期有200+任务需要执行,你会如何接受这些任务?是来一个任务就创建一个线程,还是把线程存起来一起使用?第二种方式就叫做线程池的方式

尝试手动创建一个线程池并且提交一些耗时的任务,比如说读取磁盘上的文件。

并写代码对比重复创建新线程和使用线程池复用线程的时间效率。

线程池的一些关键参数,自己尝试debug,跟踪线程池的提交任务流程,核心线程池-->阻塞队列-->最大队列

,并且尝试将阻塞队列的大小设置为无界队列,一直添加任务直到堆栈溢出为止。

线程生命周期学习

New 状态: 跟普通对象没有什么区别,线程此时不存在,

Runnable 状态(也可称之为Ready): 调用了start()方法才真正在JVM中创建了一个线程,Runable的线程只能进入Running状态(在图里面有所体现,可以带着这一点去看图)或者意外终止。

Runing 状态 : 这个最好理解了,就是在cpu在执行你这个线程的任务

还有几种状态未补充,终止状态,阻塞状态,等待状态,超时等待状态。

这种线程状态转换图我见过很多张,看过多种解释,这些解释大体都相同,但是有一些细微的差异,为了让自己不迷糊,今天自己总结一下。首先先贴两个图,

我们记忆这种状态的时候重点记状态,不用耗费很多力气去记住状态之间的转换的方法。

首先是系统的线程状态,其实现在Java的线程就是在操作系统线程哈哈,以前是绿色线程也被称做用户线程,因为能做的事情比较少,权限小,效率慢,所以改为操作系统线程了。

第二章也来自于网上

这里我说明几点,这两张图非常相似

  1. 几乎只有一个状态的不同,就是一个是Blocked 一个是waiting
  2. Java中把Runable 和Running 统称未 Active 状态,没有做区分 。下面那张图称之为RUNNABLE ,然后把Runable 称之为Ready ,(你品,你细品,这你不总结一下,不得给绕晕?

接下贴一张书上的图 深入浅出Java多线程

好了再来总结一下这张图吧。

我们这样子记住这些状态,这几张图共同的地方,也就是共识

NEW 起床- > RUNABLE( READY 摸鱼 - RUNNING 干活) -> TERMIATED 下班

起床 -> 上班 -> 下班 : 上班是摸鱼和干活交替执行的 ,有没有很形象哈哈哈哈哈哈,面试可不能这样讲啊哈

对对这些是共识嘛,也就是大体的概念,然后讲一下不同的地方:

  1. BLOCKED (阻塞状态) : 没抢到锁得时候,会进入
  2. WAITING (等待状态) : 等着别人叫才起来干活
  3. TIMED_WAITING (超时等待状态) : 定了闹钟,过一会,自己叫自己起来干活

sleep() wait() join() park() unpark() 的区别
关于sleep()和wait()可以看看这边文章java sleep()释放锁吗_Java中sleep()与wait()区别(涉及类锁相关概念)-CSDN博客
看完后自己的理解:

  1. sleep() 是线程Thread的方法 ,不会释放锁,哪里都可以用
  2. wait() 是Object的方法,跟锁有关,它会释放锁,只能在获取锁的时候使用(sychronized代码块里)
  3. 一般我们平时嗷,sleep(1000) ,这样子使用,进入超时等地状态,到时候自动起来,而wait() 的话需要另一个线程调用这个对象的notify() 唤醒方法,把别人叫起来。

join() 就是加入一个一个线程,手动通知主线程,等子线程执行了完了再往下面走

public static void main(String[] args) {
    Runnable runnable = new Runnable() {
			@Override
			public void run() {
				System.out.println("子线程执行");
			}
		};
    Thread thread1 = new Thread(runnable);
    Thread thread2 = new Thread(runnable);
    thread1.start();
    thread2.start();
    try {
        //主线程开始等待子线程thread1,thread2
        thread1.join();
        thread2.join();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    //等待两个线程都执行完(不活动)了,才执行下行打印
  `  System.out.println("执行完毕")`;;
}

代码来自博客 Java中join()方法原理及使用教程_java join方法-CSDN博客

OKOK,先这样了,后续想到了再补充了..

并发理论学习

原子性

一系列操作要么全部执行,要么全部不执行

在数据库也有这个概念 ACIO(原子性,一致性,隔离性,永久性 -> 面试可以提 其他三个都是为了实现这个一致性的。)

可见性学习

找到一个比较好的学习文章 缓存一致性协议-MESI是什么?-腾讯云开发者社区-腾讯云

MESI 是各个状态的简写

  1. M (Modified) 修改状态 :
  2. E (Exclusive) 独享,互斥:
  3. S (Shared) 共享
  4. I (Invalid) 无效 :
    其实我们学习一个东西就是为了搞懂(他解决了什么问题,他是怎么解决的?)

好看完这个文章第一段,就是说了他是一个缓存一致性协议。

原文引用:内存屏障是什么? 本文是继上文的解决内存不一致的另一种实现方式。

MESI是未了解决内存不一致(缓存一致性)的另一种实现方式,我们已经知道了内存屏障,也是有各种协议,什么happenBefore等等,来保证内存一致性。那这个MESI定义的几种状态可以先看一下

状态

描述

监听任务

M 修改 (Modified)

该Cache line有效,数据被修改了,和内存中的数据不一致,数据只存在于本Cache中。

缓存行必须时刻监听所有试图读该缓存行相对就主存的操作,这种操作必须在缓存将该缓存行写回主存并将状态变成S(共享)状态之前被延迟执行。

E 独享、互斥 (Exclusive)

该Cache line有效,数据和内存中的数据一致,数据只存在于本Cache中。

缓存行也必须监听其它缓存读主存中该缓存行的操作,一旦有这种操作,该缓存行需要变成S(共享)状态。

S 共享 (Shared)

该Cache line有效,数据和内存中的数据一致,数据存在于很多Cache中。

缓存行也必须监听其它缓存使该缓存行无效或者独享该缓存行的请求,并将该缓存行变成无效(Invalid)。

I 无效 (Invalid)

该Cache line无效。

好,我看完之后再看一遍这个表,协议定义的几种状态,然后对应了一种监听任务对吧。

那如果要我自己去实现这个协议,保证内存的一致性,此时我们知道他的状态描述,我们怎么去定义监听任务呢?

这里我把表格里的话做一个口语化。方便理解。

如果是修改状态的话,看他的定义,只有我这个资源是有效的,并且我是在改这个数据的,那其他的线程要读这个资源,肯定要在我后面吧,等我改完在去读

如果是独享状态的话,看他的定义,就是我现在和主内存的数据是一致的, 并且只有我和主内存是一致的,其他的要读是吧,好,你读了这个数据,你那也有一个副本,那我这个数据就不是独享的了,要把他改为共享状态。

如果是共享状态的话:看他的定义,这个数据很多地方都有,都是共享的,那如果有线程要去修改他了,或者说他要把这个数据变成独享的了,那我就得知道他这个操作啊,监听他的操作啊,如果有这个操作,那我本地的数据是不是就不是最新的了,就没有用了,就是无效的状态了。

有这样一个思路之后在看上面那个表格,感觉就很好理解了。

好好好,学完MESI协议,咱来回顾一些内存屏障呗,感觉非常相似啊,他两都是为了保证缓存一致性他们有啥关联呢?我也不太懂,网上找到一个资料专门讲解了这个问题,此处粘贴一下链接 16 | 内存模型:有了MESI为什么还需要内存屏障?-极客时间

  • 29
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值