JavaEE初阶=>为搭建网站,进行技术铺垫

JavaEE进阶=>搭建网站

初阶重点学习内容:多线程,网络原理

一.计算机是如何工作的

冯诺依曼体系结构 =>是一台计算机的最基本规则

1.CPU(中央处理器)

人类科技巅峰之作

2.80GHz  主频(基频) 

(速度:4.59GHz)睿频

CPU工作的快慢/运算速度的快慢,CPU工作频率也会变化

CPU的频率越高和核心数越多,就越好

1.1缓存cache,在CPU上开辟的存储数据的设备

1.2指令:

CPU上执行任务的基本单位,是0101构成的二进制机器语言.

CPU工作流程

C=>exe(硬盘,文件)=>双击exe(操作系统会把exe加载到内存中)

指令存储在内存中

2.存储器

内存 读写速度快,存储空间小,成本高,断电后数据丢失

硬盘 读写速度慢,存储空间大,成本低,断电后数据存在

u盘(U盘内部构造和固态硬盘类似,读写速度比固态硬盘慢一些)

光盘(光碟)

3.输入设备(键盘,鼠标...)

(触摸屏和网卡,既可以是输入也可以是输出)

4.输出设备(显示器,投影仪...)

指令通过二进制存储

指令和数据在同样的存储器上存储

2.操作系统

Windows 操作系统

Linux 程序员专属  在服务器,嵌入式设备上用

Mac OS/IOS 苹果公司系统

Android 本质上是Linux ,在Linux基础上做了很多改进,更适合手机端

嵌入式设备 :比如冰箱,空调,路由器...(搭载的计算机功能不多,性能不强)可以通过联网手机调用

操作系统功能:1.管理各种硬件设备2.为软件提供稳定的运行环境

System.out.println("hello world");       屏幕 =>硬件

在JVM调用C++版本的函数=>操作系统的API=>操作系统把字符串交给驱动程序=>驱动程序控制硬件完成显示工作

2.1进程

进程:(操作系统中非常重要的概念):计算机上运行起来的程序就叫进程"process" /任务"task";

操作系统中,进程是资源分配的基本单位

2.组织:使用一定的数据结构,把N个PCB串起来,一般用链表这样的方式组织

进程依赖的内存资源

进程依赖的硬盘资源(还涉及到其他外设.....)

2.2进程的调度

20个核心如何是上百个进程"同时"执行?

并行和并发统称为并发

2.3进程的状态

进程有多种状态,这里简单分为两种,就绪和阻塞

2.4进程的优先级

它决定了哪些进程优先安排CPU资源

每个进程都有自己独立的内存空间(进程的隔离性)

二.多线程初阶

线程概念

1.线程为了实现并发编程效果,解决多进程模型涉及的问题,

2.线程也叫轻量级进程,创建和销毁的开销比进程低很多

进程包含线程,进程是操作系统资源分配的基本单位,线程是操作系统调度执行的基本单位

一个进程中的若干线程共享操作系统的资源(内存 文件...)

每个线程都是一个"执行流"都可以放到CPU上调度执行,线程是操作系统调度执行的基本单位

"进程调度"过程,更准确说是"线程调度",一个进程可能包含一个线程,也可能包含N个线程(不能是0个),前面的"进程调度"可以想象成只有一个线程的进程,进行的调度

同一个进程的多个线程间,共用PCB的内存指针和文件描述符表,但有各自的状态,上下文,优先级和记账信息

线程比进程更轻量体现在,创建线程申请的资源少,

不过进程的第一个线程创建时,上述的资源申请都需要

当线程数目特多时,调度开销也会影响执行效率(因为cpu的核心数有上限)

多线程编程可能产生的问题

总结:(经典面试题:进程和线程间的关系)

多线程编程

开闭原则:对扩展开放,对修改关闭(修改可能会影响原有功能)

Java中向上转型的比较常见

写了由别人给自己调用的方法,称为"回调函数"(callback)

main线程是主线程

多线程编程中没有父线程/子线程之类的说法,多进程编程中有父进程/子进程的概念

while(true){}一直运行可能会将cpu吃满,会使CPU达到一个较高的温度,散热器风扇会加速运转

sleep可以控制时间让cpu休息一下,让当前线程进入阻塞状态

实际工作中,慎用sleep

第一种写法:

package thread;
//创建一个类继承自标准库的Thread类
class MyThread extends Thread{
    @Override
    public void run() {
        while(true){
            System.out.println("hello Thread");
            try {
                //换算成秒是1秒
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
public class Demo1 {
    public static void main(String[] args) throws InterruptedException {
    //创建Thread的实例
        Thread t = new MyThread();
        //启动线程
        t.start();
        while(true){
            System.out.println("hello main");
            Thread.sleep(1000);
        }
    }
}

针对main方法循环中的sleep和run()方法循环中的sleep抛出异常的选项问题的解释

所以run()方法中只能使用try catch进行捕获异常

线程创建的几种写法:

1.创建子类,继承Thread,重写run

2.实现Runnable接口

使用继承Thread的写法,"任务"和"线程"是绑定的,如果改成其他形式,需要大量修改代码

使用Runnable这样的写法并未和线程概念绑定,这样的任务可非常方便的迁移到其他载体上(更加低耦合)

第二种写法:

package thread;
//第二种实现多线程的写法
//runnable可搭载别的载体
class MyRunnable implements Runnable{
    @Override
    public void run() {
        while(true){
            System.out.println("hello Runnable");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
public class Demo2 {
    public static void main(String[] args) throws InterruptedException {
        Runnable r = new MyRunnable();
        Thread t = new Thread(r);
        t.start();
        while(true){
            System.out.println("hello main");
            Thread.sleep(1000);
        }
    }
}

3.继承Thread,使用匿名内部类

步骤:1.创建Thread的子类,该子类没有名字(匿名)方便,一次性

2.重写run 3.创建Thread子类的实例,并且使用t引用指向

第三种写法:

package thread;
//创建Thread匿名内部类
public class Demo3 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(){
            @Override
            public void run() {
                while(true){
                    System.out.println("hello thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        };
        //调用start()才是真正创建线程
        t.start();
        while(true){
            System.out.println("hello main");
            Thread.sleep(1000);
        }
    }
}

4.实现Runnable,使用匿名内部类

第四种写法:

package thread;

public class Demo4 {
    public static void main(String[] args) throws InterruptedException {
/*  第一种写法
      Runnable r = new Runnable(){
            @Override
            public void run() {

            }
        };
        Thread t = new Thread(r);*/
        //第二种写法
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                while(true){
                    System.out.println("hello thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        });
        t.start();
        while(true){
            System.out.println("hello main");
            Thread.sleep(1000);
        }
    }
}

第五种写法:Lambda表达式(推荐)

lambda本质上是一个匿名函数

package thread;
public class Demo5 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()->{
            while (true){
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        t.start();
        while(true){
            System.out.println("hello thread");
            Thread.sleep(1000);
        }
    }
}

为什么使用Thread,Runnable方法不需要导包?

它们都属于Java.lang包底下的,最基础最常用的

包将一组有关系的类放在一起,默认情况下不会引入到代码中,使当前代码的类比较少,不容易乱

Java约定,一个Thread对象和一个操作系统中的线程是"一 一对应"的关系,所以一个Thread对象,只能start一次.(创建多个Thread对象可以start多次,同样遵守上述原则)

后续学习Thread的其他方法/属性也是一 一 对应,确保Thread对象的各种方法属性,是在控制对应系统中的线程

显卡:

针对显卡(GPU)编程,由于硬件架构不同,它与cpu编程差距很大,为方便程序员对显卡编程,英伟达公司推出了一套框架Cuda,可以更方便的使用GPU进行编程,业界大佬对Cuda进一步封装,引入各种功能,尤其针对AI领域,进一步制作更高层次的框架,TensorFlow

Thread常见构造方法

线程组现在很少涉及,后续介绍"线程池"生态位上替代线程组

Thread几个常见属性

main线程包括代码手动创建的线程,默认都是前台线程

后台线程的设置:

package thread;
public class Demo7 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()->{
            for (int i = 0; i < 5; i++) {
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("t线程结束");
        });
        //在start之前设置后台线程
        t.setDaemon(true);
        t.start();
        //主线程调用start后,没什么需要执行的,接着主线程就结束了
        Thread.sleep(1000);
        System.out.println("主线程结束");
        //退出程序,指定退出码
        System.exit(1);
    }
}

1.启动一个线程:-start();

start方法本身,执行速度非常快

操作系统内部通过PCB来描述,通过链表组织,对Linux来说(Linux开源,Windows闭源)

Java代码=>.class文件,再通过JVM(C++写的)解释执行

带有native字样的方法,是在JVM上通过C++代码实现的,

通过调用操作系统的原生API,创建线程,根据当前操作系统进行区分

2.中断一个线程

对于Java来说,一个线程终止,就是这个线程的入口方法执行完毕,Java并不提供"强制终止"(所有让线程终止的方法,都围绕"入口方法结束")

代码实例:

package thread;
import java.util.Scanner;
public class Demo8 {
    private static boolean running = true;
    public static void main(String[] args) {
        Thread t = new Thread(()->{
            while(running){
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("t线程退出");
        });
        t.start();
        //主线程中让用户进行输入
        Scanner sc = new Scanner(System.in);
        System.out.println("请输入整数0,表示t线程终止:");
        int n = sc.nextInt();
        if(n == 0){
            running = false;
        }
    }
}

为什么running不能写作局部变量的解释

匿名内部类中

Java设定lambda表达式中捕获的变量必须是final/事实final的原因

第二种终止写法:

为什么不能使用t.isInterrupted的原因

当前设定下,可以针对Interrupt给出几种策略: 1.立即退出 2.稍等一下退出  3.不退出(忽略),       main想终止t但决定权在t的逻辑中

3.线程等待:

线程之间是随机执行的,为了让它结束具有确定性,通过干预两个线程的结束顺序,                           让后台线程 等待先结束的线程执行完           Thread =>join

t.join (join的阻塞等待时间是不确定的,取决于t何时退出)

任何两个线程都可以进行等待,t也可以等待main

代码演示:

package thread;
public class Demo10 {
    private static long result = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()->{
            for (int i = 0; i < 1000; i++) {
                result +=i;
            }
            System.out.println("t线程执行完毕");
        });
        t.start();
        //给t线程留有一定的执行时间,join等待t线程结束
        t.join();
        System.out.println(result);
    }
}

4.获取当前线程的引用

currentThread(在哪个线程中执行,就获取哪个线程的引用)                                                        Thread  名字 = Thread.currentThread;

5.休眠当前线程

sleep 阻塞,让线程不参与cpu调度

线程主动放弃cpu,在资源比较紧张的场景下会涉及到,可以降低CPU的使用率

6.线程的六种状态

线程安全问题(重点)

count++,这个代码对应三个指令1) load 把内存中的数据加载到寄存器中 ;                                  2)add把寄存器中的数据+1;                                                                                                              3)save 把寄存器中的数据写回内存

调度顺序是随机的                                                                                                                           可能会出现t1先load后,t2已经执行多次的情况,导致自增后的结果为1的情况,使最后的count值出现小于5W的情况

线程安全问题和随机调度直接相关

String 属于不可变对象,    1)如何实现?没有提供public 的set系列方法和final无关(final是为了禁止扩展(继承))                    2)String这么设计的原因:1.字符串常量池 2.计算hash 3.线程安全

枚举(enum)的概念和String不一样,枚举天然就是常量

erlang没有变量,没有if(使用模式匹配机制代替条件语句),没有循环(使用递归机制代替循环),          是函数式编程语言,以函数为主

rabbitmq消息队列,基于erlang实现

Go(2009)再被设计时,充分考虑多核心并发编程,内置协程,比线程更高效,通过channel机制,更方便的进行多个协程之间的协调

 锁的实现:核心原则:"互斥" "独占",关键步骤 加锁 解锁                                                                   锁机制本质是操作系统提供的功能

锁的关键字synchronized

别的语言中,加锁解锁是两个不同的方法:lock()    中间有一系列代码   unclock();

执行 load ,add ,save三个指令的过程中,该线程随时可以被从CPU上调度走,                                    如果其他线程也尝试进行加锁操作,就会产生阻塞,从而避免上述的三个指令过程中"被插队"

Java提供的锁=>操作系统的锁=>CPU对锁的支持(CPU执行的指令中包含了类似lock这样的指令)

锁内部的代码逻辑少,"锁的粒度小"

操作系统中还有一种机制,叫"临界区",某个线程进入临界区,会暂时关闭操作系统的调度器(这个线程会一口气在CPU上持续执行)直到出临界区,才恢复调度器(操作系统底层机制,java不提供)

代码演示:

package thread;
//线程安全问题演示与解决
public class Demo14 {
    private static long count = 0;
    private static Object locker = new Object();
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            for(int i = 0;i< 50000;i++){
                //锁机制,()中的对象要是同一个对象才能产生锁竞争
                synchronized(locker){
                    count++;
                }
            }
        });
        Thread t2 = new Thread(()->{
            for(int i = 0;i< 50000;i++){
                synchronized(locker){
                    count++;
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }
}

锁内部的代码逻辑多(不是代码行数),"锁的粒度大"

synchronized 通过{}控制加锁解锁,为了防止代码中忘记写解锁/写了代码未执行到 从而产生非常严重的问题

synchronized的其他写法

synchronized 可以修饰一个实例方法

synchronized 可以修饰一个静态方法(针对类对象加锁)

Demo16.class 反射:运行时获取到一个对象/类 内部结构,类里面有哪些属性,方法,名字,类型,public/private....,继承自哪个类,实现哪些接口....---->.java =>.class(二进制字节码文件)=>被JVM加载到内存中,会构造出一个特殊的对象,类对象.每个类都会在内存中有唯一 一个类对象(反射就是JVM提供了一组API可以从对象中获取到信息/进行一些操作)

标准库中的集合类,已学过的大部分都是线程不安全的(ArrayList,LinkedList,HashMap,Queue)不能多线程对这样的集合类对象进行修改  Vector,Hashtable 是线程安全的,核心方法都加了synchronized,会即将淘汰,因为无脑加锁

加锁的代价,使程序效率更低=>产生竞争=>产生阻塞,即使没有产生阻塞,加锁本身也可能调用到操作系统内核的逻辑,(确实存在线程安全的问题,再去加,不存在就不该加)

要知道 可重入锁的效果,记录持有锁的线程,哪里真正解锁

死锁

多线程编程中使用锁的非常经典的问题

1.一个线程一把锁,连续加锁多次(可重入锁直接解决)

2.两个线程两把锁(解释:循环依赖,A依赖B,B依赖A)

3.n个线程,m把锁(典型:哲学家就餐问题,当每个哲学家都拿起左手筷子,桌上就没有筷子了,就会陷入阻塞态,解锁不了)

由于多线程的"随机调度",实际执行顺序存在多种情况,有些情况没问题,有些有bug,写代码要确保所有情况都没问题

如何避免死锁:

死锁的四个必要条件(打破任意一个就可解除死锁)(背)

1.锁是互斥的(基本特点)  未来会接触到其他锁,有时互斥,有时不是...

2.锁不可抢占                                                                                                                                    A线程获取locker,此时B线程也想获取locker 把A的locker抢来了,B持有locker,A线程阻塞了(嵌入式开发,对实时要求高的操作系统上,任务优先级有明确要求,会出现这种特殊情况)

以上两点synchronized都改不了,是构成它的必要特性

3.请求和保持 (和代码结构有关)   A线程获取到locker1 的情况下,保持locker1 的状态(不释放),尝试获取locker2

4.循环等待/环路等待 (代码结构有关) eg:车钥匙锁家里,家钥匙锁车里

如何避免死锁:

volatile关键字

解决内存可见性引起的线程安全问题

内存可见性问题由编译器优化导致的,编译器优化:

javac =>.java =>.class

JVM 执行.class

此处编译器发现1.flag每次读到的都是相同的值 1s足以让这个循环上万次  2.编译器也并未发现哪里修改  虽然我们在另一个线程有修改,但编译器无法分析出另一个线程的执行时机,                      此处编译器就做了大胆判定:把load操作优化掉,后续循环只从寄存器/缓存中取flag值,                   此时如果在t2线程中修改flag,t1也就无法感知了(t1没在内存读,而从寄存器/缓存)

使用volatile关键字修饰某个变量 ,此时编译器就知道该变量"易变",后续针对这个变量的读写操作就不会设计优化了

主内存(main memory):平时说的内存 工作内存(work memory):寄存器+缓存 (学习细节因为面试)

wait&notify

由于多线程调度的随机性,join只能影响线程结束的顺序,所以引入了wait&notify

这个机制也可以应对线程"饿死"问题      wait&notify是Object类的方法,可以被任意对象调用

wait的释放锁和等待其他线程的通知(进入阻塞状态)必须是原子(一起执行)的

java要求notify 也得在synchronized中,操作系统原生api则没这个要求

wait和sleep的共同点:都可以让线程阻塞,都可以指定阻塞时间                                                          wait和sleep的区别:                                                                                                                             1.wait的设计是为了被notify,超时只是"后手"                                                                                     sleep的设计就是为了按照一定时间阻塞                                                                                             2.wait必须搭配锁使用,sleep则不需要                                                                                               3.wait一进来就会先释放锁,在获取锁,sleep放在锁内部,休眠时不会释放锁                                       4.wait虽然能通过interrupt唤醒,实际更希望通过notify唤醒(正常情况),notify唤醒之后,还可以随时再wait,再notify  sleep和interrupt就不是,Interrupt可能把线程终止掉

多线程编程案例:

1.单例模式:

一种设计模式,保证java进程中的某个类只有唯一 一个实例

执行效率:程序再计算机上运行的快慢  开发效率:程序员写代码时的快慢(目前开发比执行更重要,执行效率可通过升级硬件弥补,程序员的人力成本逐年上升)                                                                  编译器优化=>提高执行效率                                                                                                            设计模式(不止23种):可以提高开发效率  (不同编程语言存在不同设计模式)                                                                                      Instance可以表示多种含义:1.表示一台服务器  2.表示服务器主机上的一个服务器进程  3.表示某个类的一个对象                                单例模式应用场景饿汉式单例模式天然线程安全,getInstance只涉及读操作,多个线程对同一变量进行读取,没问题,    懒汉式单例模式不安全 ,它的修改操作不是原子的(java 的= 通常是原子的),条件判定和赋值放在一起,是完整的逻辑,不是原子的

指令重排序引起的问题 

 编译器的一种优化手段:调整指令执行顺序,使代码效率更高

完备的懒汉式单例模式代码:

package thread;
//懒汉式单例模式
class Singleton1{
    private static Object locker = new Object();
    //加volatile,关闭编译器优化,防止指令重排序产生的问题
    private volatile static Singleton1 instance;
    //懒汉式的关键在,将创建的时机推迟了,等第一次使用才创建
    public static Singleton1 getInstance(){
        //判断是否需要加锁
        if(instance == null){
            //加锁防止线程不安全问题
            synchronized(locker){
                //判断是否需要创建实例
                if(instance == null){
                    instance = new Singleton1();
                }
            }
        }
        return instance;
    }
    private Singleton1(){
    }
}

多线程代码中,不是加了锁就一定线程安全,也不是不加锁就一定线程不安全(具体问题具体分析)

工作后最高频使用的数据结构:ArrayList/数组   HashMap   Queue  (通用解决方案) Stack,LinkedList,TreeSet...使用非常少,特殊场景下的解决方案                                                    Python JS PHP Lua直接把ArrayList和HashMap内置到语法中了(内置类型)

阻塞队列(BlockingQueue)

:先进先出(FIFO)非常重要的数据结构                                                通过阻塞队列可以实现很多有用的效果(生产者消费者模型,实际开发中常见编程手法)生产者消费者模型,产生的作用:                                                                                                          1.降低资源竞争     2.解耦合

3.削峰填谷  "峰"指请求高峰,流量高峰                                         生产者消费者模型弊端:1.单个请求响应时间,可能受到影响 .2.服务器结构更为复杂.                      它更适合"异步"操作,不适合"同步"场景

Java标准库提供了阻塞队列

泛型在开发中会用到的场景:1.集合类 2.Spring统一封装返回结果

自己实现一个阻塞队列:

package fact.demo;
//自己实现一个阻塞队列
//暂时不考虑泛型,只保存String类型
class MyBlockingQueue{
    private String[] data ;
    //定义首尾下标
    private int head=0;
    private int tail=0;
    private int size =0;
    public MyBlockingQueue(int capacity){
        data = new String[capacity];
    }
    private Object locker = new Object();
    public void put(String elem) throws InterruptedException {
        synchronized(locker){
            while(size == data.length){
                //队列为满阻塞
                locker.wait();
            }
            //新元素放到tail所在位置
            data[tail] = elem;
            tail++;
            if(tail>=data.length){
                tail = 0;
            }
            size++;
            locker.notify();
        }
    }
    public String take() throws InterruptedException {
        synchronized(locker){
            while(size==0){
                //队列为空阻塞
                locker.wait();
            }
            //取出head位置的元素
            String ret = data[head];
            head++;
            if(head>=data.length){
                head = 0;
            }
            size--;
            locker.notify();
            return ret;
        }
    }
}
public class Demo25 {
    public static void main(String[] args) {
    MyBlockingQueue myBlockingQueue = new MyBlockingQueue(1000);
    Thread t1 = new Thread(()->{
        long n = 0;
        while(true){
            try {
               // myBlockingQueue.put(String.valueOf(n));
                myBlockingQueue.put(n+" ");
                System.out.println("生产了"+n);
                n++;
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    });
    Thread t2 = new Thread(()->{
        while(true){
            try {
                String n = myBlockingQueue.take();
                System.out.println("消费了"+n);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    });
    t1.start();
    t2.start();
    }
}

池:可能会用到,事先准备好(提高效率的手段)   常量池,线程池,进程池,内存池,连接池

线程池:

把线程提前创建好,放到一个位置,要用的时候去取,比从操作系统创建更快

引入线程池原因:

操作系统可视为两部分:1.内核(核心功能所在,eg:进程管理操作) 2.配套的应用程序(自带的画图板,计算机之类的)      应用程序(代码执行在用户态)      操作系统内核(代码执行在内核态)

eg:t.start()创建线程 1.一旦调用,先执行一系列逻辑(用户态) 2.start内部调用操作系统api(内核态)  3.回到用户态,继续往下执行(用户态) 这样的切换有一定开销

操作系统为了让应用程序有稳定运行环境,把CPU很多权限收紧了

采用线程池的方案,提前把线程通过系统api创建好,把这些Thread对象放到一个集合类中,后续使用,直接从集合类取(纯用户态代码)

标准库提供现成的线程池  java生态很丰富

 线程池的使用:2.submit方法,往池子中添加要执行的任务(Runnable)   submit(Runnable task)   1.构造方法(重点)(开发中用到,也是经典面试题) 这个问题是坐标两种表示形式不能构成重载的问题,可使用工厂模式解决

日常开发中,都认为新任务比较重要,上述策略都在尽力让新任务执行

线程池Executors工厂类使用代码演示:

public class Demo26 {
    public static void main(String[] args) {
        //可以自动扩容,都是非核心线程,没有核心线程
        //ExecutorService  executorService = Executors.newCachedThreadPool();
        //指定的数量都是核心线程
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 1000; i++) {
            //通过设置局部变量,使匿名内部类能进行变量捕获
            final int id = i;
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    String name = Thread.currentThread().getName();
                    System.out.println("hello"+name +","+id);
                }
            });
        }
        executorService.shutdown();
    }
}

定时器:

模拟实现定时器:

ThreadLocal

(面试常见问题,关心的是JVM/GC,不是多线程)                                                                                     定义线程级变量,(期望有一种变量,生命周期跟随线程,作用域也在线程中)

三.多线程进阶 (主要面试题,实际很少用)

1.锁策略

乐观锁VS悲观锁

重量级锁VS轻量级锁                                                 

挂起等待锁VS自旋锁(spin lock)

公平锁VS非公平锁                                                   

可重入锁(也称:递归锁)VS不可重入锁                                                          

普通互斥锁VS读写锁(实际开发比较常见)

面试题

2.CAS

是解决线程安全问题的另一种思路,加锁是普适方案

  compare and swap (比较且交换):比较一个内存和一个寄存器的值,                                              如果这两相同,就把内存和另一个寄存器的值进行交换

unsafe包,针对一些底层的操作,都可能是unsafe.(CAS在此包中)                                                      系统底层操作,操作步骤比较麻烦,注意事项也比较多

1.原子类(开发中比较实用的存在)

2.自旋锁

synchronized内部"自旋"就是靠CAS,基于CAS实现的是"轻量级锁".                                            使用CAS不必加锁(重量级锁),基于CAS的"无锁编程"(重量级锁)

3.ABA问题                    

CAS应对面试:1.CAS是啥2.应用场景(CAS如何实现锁)3.CAS的ABA问题是啥样的

实际开发中一般不会直接用到CAS,而是用到像原子类这样的封装CAS的操作

3.synchronized内部原理

1.锁升级(自适应过程)JVM内部实现逻辑

2.锁消除(针对synchronized进行的编译器优化)

3.锁粗化(锁的粒度,与加锁解锁之间有多少逻辑有关)

有多个任务要执行,加了多次锁,虽然留出空闲时间让别的线程获取,但增加了竞争次数,同时增加阻塞时间 ,锁粗化后,只加一次锁,整个执行过程只涉及一次竞争(还与编译器优化有关)

4.JUC(java.util.concurrent)的常见类

JUC面试中的"常见类"

Callable接口 

2.ReentrantLock(可重入锁)

3.信号量Semaphore

信号量就是一个"计数器",描述了"可用资源"的个数                                             在信号量中,如果数值已经是0了,继续进行P操作就会触发阻塞

4.CountDownLatch

5.线程安全的集合类

Vector,Stack,HashTable,是线程安全的(不建议用),其他的集合类不是线程安全的.

1.多线程环境下使用ArrayList

写时拷贝适用于比较小的数据

2.多线程环境下使用队列(简单了解)

1. ArrayBlockingQueue  基于数组实现的阻塞队列

2. LinkedBlockingQueue  基于链表实现的阻塞队列

3. PriorityBlockingQueue  基于堆实现的带优先级的阻塞队列

4. TransferQueue  最多只包含一个元素的阻塞队列

3.多线程环境下使用哈希表(常见面试题)              

关于AI

AI大语言模型针对全领域(ChatGPT,DeepSeek,Gemini,Grok,Kimi,豆包...)

相对的"小语言"针对某个特定知识领域  coze (字节的,AI agent的工作流) dify(开源社区)

AI agent"把大化小" 工作流是把若干个AI agent 串到一起,完成更复杂的工作

通过AI工作流:1.把每节课视频获取到,通过AI大模型提取语音,生成文字 2.通过大语言模型把完整文字进行提炼总结  3.让大语言模型通过MCP server控制git把课堂总结提交到gitee上

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐