笔记整理-多线程与高并发

2023-10-29

多线程与高并发
目录
多线程与高并发 https://www.cnblogs.com/Zs-book1/p/14318992.html?share_token=641d3935-0525-44d5-a772-9764bf2fad2a
一、了解多线程
什么是进程?
什么是线程?
并发与并行的区别
临界区
学习线程必须知道的概念:
二、 线程的使用
三种方式的区别
线程的方法
线程的状态
三、Synchronized
JMM模型
volatile
synchronized
synchronized的使用
synchronized锁定的对象不能是基本类型和String类型
synchronized保证了可见性、原子性、有序性
synchroinzed是如何保证可见性的?
synchronized是如何保证原子性的?
什么是原子性?
synchronized如何保证原子性
synchronized保证有序性
synchronized锁升级过程
什么是重量级锁?
一个对象的内容
markword
锁升级
AtomicInteger
什么是CAS
CAS的工作原理
LongAdder
Unsafe
什么是Unsafe
Unsafe的使用
使用 Unsafe进行CAS操作
五、JUC中的锁
ReentrantLock
ReentrantLock的使用
ReentrantLock是可重入锁。
ReentrantLock的方法:
ReentrantLock和synchronized的区别:
Condition
condition方法
使用Condition
Object监视器和Condition的区别
使用Condition实现阻塞队列BlockingQueue
CountDownLatch
CyclicBarrier
CyclicBarrier与CountDownLatch的区别
Semaphore
Phaser
ReadWriteLock
LockSupport
AQS
ReentrantLock的实现:
CountDownLatch的实现:
六、强软弱虚四种引用以及ThreadLocal源码
强软弱虚引用
强引用
软引用
弱引用
虚引用
ThreadLocal源码
ThreadLocal造成的内存泄漏
七、集合容器
集合容器的分类
JUC下的容器
八、线程池
ThreadPoolExecutor
关于ThreadPoolExecutor
线程池的创建
线程工厂
ThreadGroup
自定义线程工厂
守护线程
线程池的拒绝策略RejectedExecutionHandler
自定义拒绝策略:
创建一个自定义线程工厂及拒绝策略的线程池
线程池的使用
线程池的大小设置
线程池的拓展
线程池源码阅读
ExecutorCompletionService
ForkJoinPool
九、 Disruptor
Disruptor的使用
定义事件
定义事件工厂
定义消费者
使用Disruptor
Disruptor的工作原理
disruptor的8种等待策略:
消费者处理异常
生产者类型
Disruptor对java8lambda的支持
一、了解多线程
什么是进程?
我们打开电脑上的qq时,点击qq.exe,电脑就会运行一个qq的程序,这个程序就叫做进程。
什么是线程?
当qq运行后, 我们可能会使用qq来打开多个聊天窗口进行聊天,那么每一个聊天窗口就算是一个线程。所以说,进程可以包括很多的线程。
线程和进程的区别?
进程是操作系统分配资源的最小单位,线程是CPU调度的最小单位。一个进程可以包括多个线程。
线程之间的执行是同时进行的,例如我们在qq聊天时,可以一边聊天,一边下载文件。此时下载文件和聊天这个两个操作就是两个线程。如果是一个线程的话,那么下载文件的过程中,我们就不能聊天了,只有等待文件下载完成之后我们才可以继续聊天,这就叫串形,而我们一边聊天一边下载就是并行。
说的再通俗一点:例如说我们现在想要打扫卫生,那么串形就是我自己一个人,就代表一个线程。我要先打扫卫生间,在打扫厨房,在打扫客厅,再打扫卧室… 因为我一个人,所以只能按照顺序先后执行,这样的话,就会非常的耗时间。那么还有一种方式就是我找几个朋友或者找几个家政保洁一起打扫。这样的话每一个人就相当于一个线程,大家一块打扫,你打扫你的,我打扫我的,互相之间并没有关联。此时打扫卫生总耗时就是耗时最多的一个人的时间,比如客厅空间比较大,那么打扫完整个房间的总耗时就是打扫客厅的时间,这就是多线程与单线程的区别。单线程也叫串形,多线程也叫并行。
并发与并行的区别
并发:电脑cpu是按照时间片来执行任务的,因此当电脑上运行着多个任务时,可能A任务执行一会儿,B任务执行一会儿,但是因为CPU的任务切换时间非常短,ns(纳秒)级别,因此在我们眼中看来,就像是A任务和B任务是一块执行的,也就是说A和B是并发执行的。并发说的是在同一个时间段内,多个事情在这个时间段内交替执行。
并行:当电脑上有多个核时,每个核都可以执行任务,因此假设电脑有两个核心的话,那么A任务在核心1上执行,B任务在核心2上执行,此时A和B任务是一块运行的,可以称为A和B是并行运行的。并行说的是多个事情在同一个时刻发生。
并行是并发的一种,都表示任务同步运行,因此也可以称为并发,但是并发不能称为并行。
下面以图片来理解并发和并行:

一个咖啡机就代表一个cpu核,上面的图一个咖啡机,排了两个队,那么这两个队交替到咖啡机接咖啡,交替前进就是并发。而下面两个咖啡机,每个咖啡机前都有一个队伍,这两个队伍是一起执行的,这两个队伍就叫做并行。
并发偏重于多个任务交替执行,这多个任务之间可能是串形执行的,而并行是真正意义上的同时执行。
临界区
临界区用来表示一种公共资源或者数据,可以被多个线程使用,但每次只能有一个线程使用它,一旦临界区被占用,那么其他线程过来想要获取这个资源就只能等待。
就比如办公室里的打印机,打印机就是一个公共资源。办公室里的每一个人都可以连接打印机打印文件,那么每一个电脑与打印机的连接就可以看成是一个线程。当需要打印东西时,如果一个人正在打印,那么此时他就独占了打印机这个资源,此时另一个人也想要打印东西,那么他就只能等待前一个人打印东西完成后,他就释放这个资源了,后面的人才可以连接打印自己的东西,只要前一个人没有打印完,就是还在用这个打印机,那么后面过来的人都要排队等着。等待获取这个公共资源。
学习线程必须知道的概念:

阻塞:阻塞的意思就是说线程在等待一个结果,在拿到这个结果前就在这等待着,CPU空转等待拿到结果后再继续,这个等待的过程叫做阻塞。例如:我们现在要去商店买一个玩具,但是这个玩具老板需要到仓库中找,我们就要在门口等着老板找到货后才能付款离开。等待的这个过程就是阻塞。


锁:加锁就是控制共享资源的使用权。例如一个8车道的高速公路,也就是说可以允许8辆车同时跑,这8个车道就可以看成是八个线程,而收费站就可以看成是共享资源,如果只有一个收费站的话,那么每次都只有一个车道的车辆可以通过这个收费站,在这辆车进入收费站出站之前,这辆车对这个收费站就是独占的,此时是不允许下一辆车进入的,那么这种情况就可以看成是这辆车获取了收费站这把锁。

其实,线程获取cpu也可以看成是获取锁,在一个线程获取CPU执行的过程中,其他的线程是等待的,只有当前线程的时间片用完了,那么释放锁,其他线程抢占CPU也就是获取锁。


死锁:多个线程之间互相挣抢锁,互不相让的过程。以下图理解:


A、B、C、D四辆小车都在这种情况下都无法继续行驶了。他们彼此之间相互占用了其他车辆的车道,如果大家都不愿意释放自己的车道,那么这个状况将永远持续下去,谁都不可能通过,这就是死锁。

二、 线程的使用
创建线程的三种方式:

方式1: 继承 Thread

import java.util.concurrent.TimeUnit;
/**

  • 通过继承方式创建线程

  • @author 赵帅

  • @date 2021/1/1
    */public class CreateMyThreadByExtendThread extends Thread {

    @Override
    public void run() {
    try {
    TimeUnit.SECONDS.sleep(3);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    System.out.println(“通过继承方式实现自定义线程,当前线程为:” + Thread.currentThread().getName());
    }

    public static void main(String[] args) {
    // 当前线程为主线程 main
    System.out.println("Thread.currentThread().getName() = " + Thread.currentThread().getName());

     // 创建一个新的线程并开启线程,打印新的线程名
     CreateMyThreadByExtendThread thread = new CreateMyThreadByExtendThread();
     thread.start();
    

    }
    }



方式2: 实现Runnable接口

import java.util.concurrent.TimeUnit;
/**

  • 通过实现Runnable接口方式

  • @author 赵帅

  • @date 2021/1/1
    */public class CreateMyThreadByImplRunnable implements Runnable {

    @Override
    public void run() {
    try {
    TimeUnit.SECONDS.sleep(3);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    System.out.println(“通过实现Runnable接口方式实现自定义线程,当前线程为:” + Thread.currentThread().getName());
    }

    public static void main(String[] args) {
    // 当前线程为主线程 main
    System.out.println("Thread.currentThread().getName() = " + Thread.currentThread().getName());

     // 创建一个新的线程并开启线程,打印新的线程名
     Thread thread = new Thread(new CreateMyThreadByImplRunnable());
     thread.start();
    

    }
    }


    方式3: Callable+Feature

    import java.util.concurrent.*;
    /**

  • 通过实现Callable接口方式

  • @author 赵帅

  • @date 2021/1/1
    */public class CreateMyThreadByImplCallable implements Callable {

    @Override
    public String call() throws Exception {
    try {
    TimeUnit.SECONDS.sleep(3);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    System.out.println(“通过实现Callable接口方式实现自定义线程,当前线程为:” + Thread.currentThread().getName());
    return “hello”;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
    // 当前线程为主线程 main
    System.out.println("Thread.currentThread().getName() = " + Thread.currentThread().getName());

     // callable接口实现的任务和Future接口或线程池一起使用
     CreateMyThreadByImplCallable callable = new CreateMyThreadByImplCallable();
    
     // 和Feature一起使用,Future表示未来,也就是说我执行这个任务,未来我去取任务执行的结果
     FutureTask<String> task = new FutureTask<>(callable);
     new Thread(task).start();
     System.out.println("main线程继续运行");
    
     // 等其他线程执行完了,再来拿task的结果,
     System.out.println("task.get() = " + task.get());
    
    
     // 使用线程池方式调用callable
     Future<String> submit = Executors.newSingleThreadExecutor().submit(callable);
     System.out.println("submit.get() = " + submit.get());
    

    }
    }

    三种方式的区别
    通过上面三种创建线程的方式,我们对线程的使用有了基本的了解 ,下面我们来分析这三种方式有什么区别:
    1.继承Thread: 通过继承方式创建线程,因为java单继承的特性,使用的限制就非常多了,使用不方便。
    2.实现Runnable: 因为单继承的限制,所以出现了Runnable,接口可以多实现,因此大大提高了程序的灵活性。但是无论是继承Thread还是实现Runnable接口,线程执行的方法都是 void 返回值。
    3.实现Callable: Callable接口就是为了解决线程没有返回值的问题,Callable接口有一个泛型类型,这个泛型就代表返回值的类型,使用Callable接口就可以开启一个线程取执行, Callable一般和Future接口同时使用,返回值为Future类型,可以通过Future接口的get方法拿执行结果。get()方法是一个阻塞的方法。
    相同点:都是通过Thread类的start()方法来开启线程。
    线程的方法

    start(): 开启线程,使线程从新建进入就绪状态


    sleep(): 睡眠,使当前线程休息, 需要指定睡眠时间,当执行sleep方法后进入阻塞状态,


    Join():加入线程,会将调用的线程加入当前线程。等待加入的线程执行完成后才会继续执行当前线程。

    /**

  • 线程方法示例

  • @author 赵帅

  • @date 2021/1/1
    */public class ThreadMethodDemo {
    public static void main(String[] args) throws InterruptedException {
    // 打印当前线程 main线程的线程名 main
    System.out.println("当前主线程线程名 = " + Thread.currentThread().getName());

     // 创建一个新的线程
     Thread thread = new Thread(() -> {
    
         // 线程进入睡眠状态
         try {
             Thread.sleep(1000L);
             System.out.println("当前线程名:" + Thread.currentThread().getName());
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
     });
     // 开启线程 thread
     thread.start();
    

// thread.join();
System.out.println(“主线程执行完毕”);
}
}

打开和关闭注释thread.join()可以发现输出结果是不一样的,使用join后会等待thread执行结束后再继续执行main方法。


wait(): 当前线程进入等待状态,让出CPU给其他线程,自己进入等待队列,等待被唤醒。


notify(): 唤醒等待队列中的一个线程,唤醒后会重新进入就绪状态,准备抢夺CPU。


notifyAll(): 唤醒等待队列中的所有线程,抢夺CPU。


yield(): 让出CPU。当前线程让出CPU给其他的线程执行,但是自己也会进入就绪状态参与CPU的抢夺,因此调用yield方法后,仍然可能继续获得CPU。

import java.util.concurrent.TimeUnit;
/**

  • 线程方法示例

  • @author 赵帅

  • @date 2021/1/1
    */public class ThreadMethodDemo {
    public static void main(String[] args) throws InterruptedException {
    // 打印当前线程 main线程的线程名 main
    System.out.println("当前主线程线程名 = " + Thread.currentThread().getName());

     Object obj = new Object();
    
     // 创建一个新的线程
     Thread thread = new Thread(() -> {
    
         // 线程进入睡眠状态
         try {
             synchronized (obj) {
                 obj.wait();
                 System.out.println("当前线程名:" + Thread.currentThread().getName());
             }
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
     });
     // 开启线程 thread
     thread.start();
    
     System.out.println("主线程执行完毕");
     // 等待thread进入等待状态释放锁,否则会产生死锁
     TimeUnit.SECONDS.sleep(6);
     synchronized (obj) {
         obj.notify();
     }
    

    }
    }

    注意:wait和notify只能在synchronized同步代码块中调用,否则会抛异常。
    Interrupt(): 中断线程,调用此方法后,会将线程的中断标志设置为true。
    线程的状态
    一个完整的线程的生命周期应该包含以下几个方面:
    1.新建(New):创建了一个线程,但是还没有调用start方法
    2.就绪(Runnable):调用了start()方法,线程准备获取CPU运行
    3.运行(Running):线程获取CPU成功,正在运行
    4.等待(Waiting):等待状态,和TimeWaiting一样都是等待状态, 不同的是Waiting是没有时间限制的等,而TimeWaiting会进入一个有时间限制的等。例如调用wait()方法后就会进入一个无限制的等,等待调用notify唤醒,而调用sleep( time)就会进入一个有时间限制的等。等待结束后(被唤醒或sleep时间到期)后就会重新进入就绪队列,等待获取CPU继续向下执行。
    5.阻塞(Blocked):多个线程再等待临界区资源时,进入阻塞状态。
    6.销毁(Teminated): 线程执行完毕,进入销毁状态,这个状态是不可逆的,是最终状态,当进入这个状态时,就代表线程执行结束了。
    以一张图来理解这几个状态:

简单介绍一个线程的生命周期:
当我们使用 new Thread()创建一个线程时,那么这个线程就处于创建状态;当我们调用start()方法后,此时线程就处于就绪状态(进入就绪状态后就不可能再进入创建状态了),但是调用start()方法后并不是说立马就会被CPU执行,而是会参与CPU的抢夺,当这个线程拿到CPU后,就会被执行。那么拿到CPU后就进入了运行状态。当调用了sleep或wait方法后,线程就进入了等待状态, 当等待状态被唤醒后,就会重新进入就绪队列等待获取CPU,当访问同步资源时或其他阻塞式操作时就会进入阻塞状态,阻塞状态结束重新进入就绪状态获取CPU。当线程运行完成后进入Teminate状态后,就代表线程执行结束了。
sleep操作不释放锁,wait操作释放锁。
三、Synchronized
synchronized 是java中的关键字,通过synchronized加锁保证多线程情况下临界区资源的安全访问。那么synchronized是如何实现的?
JMM模型
要了解synchronized的底层原理,首先需要了解java的内存模型。JMM内存模型是围绕线程的可见性、原子性、有序性建立的。java的内存模型分为堆内存和线程内存,也就是说java会对每一个线程都分配一块内存空间,线程内存主要存放堆栈信息和临时变量等。当创建一个对象时,如果用到了主内存中的变量,那么会将这个变量拷贝一份副本,在这个线程中对这个变量的所有操作,都是对这个副本操作的。以下面这个程序来证明这一点。
import java.util.concurrent.TimeUnit;
/**

  • 证明JMM模型中,线程对共享资源的操作,操作的是副本。

  • @author 赵帅

  • @date 2021/1/4
    */public class JMMTest {

    /**

    • 线程是否继续循环
      */
      private static volatile boolean running = true;

    public static void main(String[] args) throws InterruptedException {

     new Thread(() -> {
         while (running) {
    
         }
     }, "thread-0").start();
    
     TimeUnit.SECONDS.sleep(1);
     running = false;
     System.out.println("main线程结束");
    

    }

}
在上面这个程序中,我们定义了一个变量running,标记是否继续循环,在main方法中开启了一个线程,如果running=false时就会结束循环,结束这个线程。然后我们在main线程中将这个running更改为false。运行后发现线程并不会终止。说明在main线程中更改running的值后,thread-0线程中的running的值仍为true。这也就证明了main线程和thread-0线程的running变量并不是同一个变量。
总结一下上面的内容:
变量的定义都在主内存中。
每一个线程都有自己的工作内存,保存该线程使用到的变量的副本。
线程对共享变量的所有操作都在自己的工作内存中,不能直接操作主内存。
不同的线程之间不能访问其他线程工作内存中的变量,变量值的传递需要通过主内存来进行。
我们如何让main线程中的变量值修改后,thread-0就知道了呢?
对running变量添加volatile关键字修饰:private static volatile boolean running = true;
此时线程就能正常结束了。
volatile
volatile关键字有两个作用:

保证线程可见性: 通过缓存一致性协议实现。上面在running变量上添加volatile就是使用的保证线程可见性这个特性。当在变量上添加volatile之后,那么如果这个变量的值发生更改,就会将这个内存副本中变量的值,同步到主内存中去,主内存中的值发生更改,然后主内存将更改后的变量同步到其他的线程。这样就实现的线程间变量的可见性。


禁止指令重排序:通过内存屏障load-store实现。

指令重排序:一个类文件在编译后会转换成CPU能够识别的指令,CPU的速度很快,为了提升效率,会执行多条指令,CPU会对这多条指令进行优化。例如:

int a = 1;int b = 2;

变量a和变量b在定义上没有依赖关系,那么CPU在执行时可能会先执行b=2,再执行a=1。这都是CPU为了提升效率做的优化。当然发生重排序的概率非常小。但是这种情况是存在的。

经典问题:DCL单例是否需要添加volatile?

什么是DCL单例?

Double check lock 双重检查锁。

/**

  • @author 赵帅

  • @date 2021/1/5
    */public class DCLDemo {

    private static volatile DCLDemo INSTANCE;

    private String name = “hello”;

    private DCLDemo(){}

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

    上面代码就是DCL单例模式。那么INSTANCE要不要加volatile?

    首先我们来了解对象的创建过程:Object obj = new Object();

    创建这个对象会经历:

    1.为这个对象开辟一个内存区域
    2.复制对象引用
    3.利用对象引用调用构造方法
    4.将引用赋值给obj。
    那么如果发生指令重排序,即3、4两条指令的顺序变了。先将引用指向了对象,此时还没有执行第3条指令,即对象的构造方法还没有被执行,此时如果第二个线程调用了这个方法,那么在第一个判断,if(INSTANCE == null)判断结果为false,那么直接就返回了这个对象,这个对象是有问题的,就会出异常。
    所以DCL单例需要添加volatile。
    volatile是如何实现禁止指令重排序的?
    volatile内存屏障针对不同的操作系统会有不同的实现。只要是在每一个指令的前后添加内存屏障,前一条指令执行完才能执行后一条指令。主要通过lfence,sfence,mfence实现。在jvm层级的实现为:
    loadload、loadstore、storeload、storestore。
    更多的关于JMM内存模型会在后面的jvm中详细描述。
    synchronized
    在学习synchroinzed前,我们首先需要了解什么是线程安全性?
    当多个线程操作共享资源时,如果最终的结果与我们预想的一致,那么就是线程安全的,否则就是线程不安全的。
    看下面代码:
    /**

  • @author 赵帅

  • @date 2021/1/6
    */public class ThreadDemo {

    private int num = 0;

    public void fun() {
    for (int i = 0; i < 1000; i++) {
    num++;
    }
    }

    public static void main(String[] args) throws InterruptedException {
    ThreadDemo demo = new ThreadDemo();

     for (int i = 0; i < 10; i++) {
         new Thread(demo::fun).start();
     }
    
     TimeUnit.SECONDS.sleep(5);
     System.out.println("demo.num = " + demo.num);
    

    }
    }
    我们定义了一个num值为0;在fun方法中我们对这个num自增1000次;在main方法中我们启动了十个线程来调用这个方法,那么最终num的值应该是10*1000=10000。期望值是10000,但是执行方法后,无论执行几次最终的结果都不是期望值,因此这个类是线程不安全的。
    总结造成线程安全问题的主要原因:
    1.存在共享资源。
    2.存在多个线程同时操作共享资源。
    上面的问题如何解决?
    为了解决这个问题,我们需要保证在一个线程操作共享数据时,其他的线程不能操作这个数据。也就是保证同一时刻有且只有一个线程可以操作共享数据,其他线程必须等待这个线程处理完后再进行,这中方式叫做互斥锁。synchroinzed关键字可以实现这个操作。synchronized可以保证在同一时刻只有一个线程执行某个方法或某个代码块。
    synchronized的使用
    使用synchronzed解决上面的问题:
    import java.util.concurrent.TimeUnit;
    /**

  • @author 赵帅

  • @date 2021/1/6
    */public class ThreadDemo {

    private int num = 0;
    private final Object lock = new Object();

    public void fun() {
    synchronized (lock) {
    for (int i = 0; i < 1000; i++) {
    num++;
    }
    }
    }

    public static void main(String[] args) throws InterruptedException {
    ThreadDemo demo = new ThreadDemo();

     for (int i = 0; i < 10; i++) {
         new Thread(demo::fun).start();
     }
    
     TimeUnit.SECONDS.sleep(5);
     System.out.println("demo.num = " + demo.num);
    

    }
    }
    运行结果与期望值一致。synchronized在使用时必须锁定一个对象,可以像上面代码一样自己定义锁的对象,也可以使用如下方式:
    /**

  • @author 赵帅

  • @date 2021/1/7
    */public class SynchronizedDemo {

    /**

    • 加锁锁定的对象
      */
      private final Object lock = new Object();
      private static final Object STATIC_LOCK = new Object();

    /**

    • 方式1: 使用this关键字,锁定当前对象
      */
      public void fun1() {
      synchronized (this) {
      // do something
      }

      synchronized (lock) {
      // do something
      }
      }

    /**

    • 方式2:锁定方法
    • 在方法上加锁,这种方式与上面一样,都是锁定的当前对象
      */
      public synchronized void fun2(){}

    /**

    • 方式3:静态方法内加锁

    • 静态方法时无法使用this,只能锁定static修饰的对象,或者使用 类对象。

    • Synchronized.class 是Class对象
      */
      public static void fun3() {
      synchronized (SynchronizedDemo.class) {
      // do something
      }

      synchronized (STATIC_LOCK) {
      // do something
      }
      }

    /**

    • 方式4:锁定静态方法
    • 锁定静态方法时,与上面方式一样,锁定的是当前类对象
      */
      public static synchronized void fun4() {}
      }

多个线程必须竞争同一把锁,也就是说锁对象必须相同,下面这种方式是错误的:
public void fun1() {
final Object lock = new Object();
synchronized (lock) {
// do something
}
}
每个线程进来后都会创建一个新的锁对象,线程之间不存在锁竞争,那么锁就失去了作用,因此必须保证锁定同一个对象,多个线程竞争同一把锁。
synchronized锁定的对象不能是基本类型和String类型
使用如下代码做解释:
package com.xiazhi.thread;
import java.util.concurrent.TimeUnit;
/**

  • @author 赵帅

  • @date 2021/1/7
    */public class SynchronizedDemo2 {

    public Integer lock = 1;

    public void fun1() {
    synchronized (lock) {
    System.out.println(Thread.currentThread().getName() + “得到锁”);
    try {
    // 模拟执行业务代码耗时1秒
    TimeUnit.SECONDS.sleep(1);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    System.out.println(Thread.currentThread().getName() + “释放锁”);
    }
    }

    public static void main(String[] args) {
    SynchronizedDemo2 demo = new SynchronizedDemo2();
    for (int i = 0; i < 10; i++) {
    new Thread(demo::fun1).start();
    // demo.lock++; //1
    }

    }
    }

上面代码中定义了一个Integer类型的锁,并启动了十个线程,来调用fun1方法。我们期待的输出是这样的。
Thread-0得到锁Thread-0释放锁Thread-9得到锁Thread-9释放锁Thread-8得到锁Thread-8释放锁Thread-7得到锁Thread-7释放锁Thread-6得到锁Thread-6释放锁Thread-5得到锁Thread-5释放锁Thread-4得到锁Thread-4释放锁Thread-3得到锁Thread-3释放锁Thread-2得到锁Thread-2释放锁Thread-1得到锁Thread-1释放锁
线程之间因为竞争同一把锁有序执行,此时程序是可以正常运行的。但是一旦我们打开 demo.lock++这个注释,那么程序的结果就会变成这样:
Thread-0得到锁Thread-2得到锁Thread-1得到锁Thread-3得到锁Thread-4得到锁Thread-6得到锁Thread-7得到锁Thread-8得到锁Thread-9得到锁Thread-1释放锁Thread-4释放锁Thread-6释放锁Thread-0释放锁Thread-5得到锁Thread-2释放锁Thread-3释放锁Thread-7释放锁Thread-8释放锁Thread-9释放锁Thread-5释放锁
每一个线程都能拿到锁,这说明线程之间并不是在竞争同一把锁了。这是因为demo.lock++实际上执行的是`demo.lock = new Integer(demo.lock+1)。可以看到,创建了一个新的对象。我们打印一下锁对象的内存地址:
package com.xiazhi.thread;
import java.util.concurrent.TimeUnit;
/**

  • @author 赵帅

  • @date 2021/1/7
    */public class SynchronizedDemo2 {

    public Integer lock = 1;

    public void fun1() {
    synchronized (lock) {
    System.out.println(Thread.currentThread().getName() + “得到锁”);
    // 打印当前锁对象的内存地址
    System.out.println(“当前锁对象:” + System.identityHashCode(lock));
    try {
    // 模拟执行业务代码耗时1秒
    TimeUnit.SECONDS.sleep(1);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    System.out.println(Thread.currentThread().getName() + “释放锁”);
    }
    }

    public static void main(String[] args) {
    SynchronizedDemo2 demo = new SynchronizedDemo2();
    for (int i = 0; i < 10; i++) {
    new Thread(demo::fun1).start();
    demo.lock++;
    }

    }
    }
    查看运行结果:
    Thread-1得到锁
    Thread-2得到锁
    当前锁对象:245887817
    Thread-0得到锁
    当前锁对象:1443225064
    当前锁对象:245887817
    Thread-3得到锁
    当前锁对象:1443225064
    Thread-4得到锁
    当前锁对象:1468479040
    Thread-5得到锁
    当前锁对象:774263507
    Thread-6得到锁
    当前锁对象:1022687942
    Thread-7得到锁
    当前锁对象:166950465
    Thread-8得到锁
    当前锁对象:1694292106
    Thread-9得到锁
    当前锁对象:1694292106
    Thread-2释放锁
    Thread-0释放锁
    Thread-4释放锁
    Thread-3释放锁
    Thread-1释放锁
    Thread-6释放锁
    Thread-5释放锁
    Thread-8释放锁
    Thread-7释放锁
    Thread-9释放锁
    可以很明显的看到锁的对象一直在变化,而我们加锁的目的就是为了保证多个线程竞争同一把锁,现在是在竞争多把锁。线程之间就不存在竞争关系,都可以得到锁。所以不能使用Integer,其他的基本类型包装类型也是跟这个一样。所以说锁对象不能是基本类型包装类型。
    如果只是因为i++这个原因的话,或许我们会想如果用final修饰为不可变对象不就可以了么。例如下面这样:
    public final Integer lock = 1;
    这样的话,就保证了lock对象是不可变的。这样是不是就可以了?
    仍然不行。因为再Integer类内部维护着一个缓存池,缓存-128~127之间的值。
    /**

  • @author 赵帅

  • @date 2021/1/9
    */public class IntegerTest {
    public static void main(String[] args) {
    Integer var1 = 127;
    Integer var2 = 127;

     System.out.println(var1 == var2);// true
    
     Integer var3 = 128;
     Integer var4 = 128;
     System.out.println(var3 == var4);// false
    

    }
    }
    可以看到如果Integer的值在 -128~127之间的话,无论创建多少次,实际上使用的都会是一个对象。那么再使用中就会造成如下问题:
    我们首先来看不使用Integer做锁的时候, 程序的运行结果:
    package com.xiazhi.thread;
    import java.util.concurrent.TimeUnit;
    /**

  • @author 赵帅

  • @date 2021/1/9
    */public class SynchronizedDemo3 {

    static class A{
    private final Object lock = new Object();

     public void fun1() {
         synchronized (lock) {
             System.out.println(Thread.currentThread().getName() + "获取锁");
             // do something
             try {
                 TimeUnit.SECONDS.sleep(1);
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
             System.out.println(Thread.currentThread().getName() + "释放锁");
         }
     }
    

    }

    static class B{
    private final Object lock = new Object();

     public void fun1() {
         synchronized (lock) {
             System.out.println(Thread.currentThread().getName() + "获取锁");
             // do something
             try {
                 TimeUnit.SECONDS.sleep(1);
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
             System.out.println(Thread.currentThread().getName() + "释放锁");
         }
     }
    

    }

    public static void main(String[] args) {
    A a = new A();
    B b = new B();
    for (int i = 0; i < 5; i++) {
    new Thread(a::fun1, “Class A” + i).start();
    new Thread(b::fun1, “Class B” + i).start();
    }
    }
    }
    此时程序的运行结果是这样的:
    Class A0获取锁Class B0获取锁Class A0释放锁Class A4获取锁Class B0释放锁Class B4获取锁Class A4释放锁Class B4释放锁Class A3获取锁Class B3获取锁Class A3释放锁Class B3释放锁Class A2获取锁Class B2获取锁Class A2释放锁Class B2释放锁Class A1获取锁Class B1获取锁Class A1释放锁Class B1释放锁
    可以看到,ClassA的和ClassB之间是没有锁竞争的,类A的lock和类B的lock是两把锁,这样的话,这也类关联的线程其实是两个并行的线程。A和B之间互不影响。但是如果我们将类A和类B的锁对象修改:
    package com.xiazhi.thread;
    import java.util.concurrent.TimeUnit;
    /**

  • @author 赵帅

  • @date 2021/1/9
    */public class SynchronizedDemo3 {

    static class A{
    private final Integer lock = 1;

     public void fun1() {
         synchronized (lock) {
             System.out.println(Thread.currentThread().getName() + "获取锁");
             // do something
             try {
                 TimeUnit.SECONDS.sleep(1);
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
             System.out.println(Thread.currentThread().getName() + "释放锁");
         }
     }
    

    }

    static class B{
    private final Integer lock = 1;

     public void fun1() {
         synchronized (lock) {
             System.out.println(Thread.currentThread().getName() + "获取锁");
             // do something
             try {
                 TimeUnit.SECONDS.sleep(1);
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
             System.out.println(Thread.currentThread().getName() + "释放锁");
         }
     }
    

    }

    public static void main(String[] args) {
    A a = new A();
    B b = new B();
    for (int i = 0; i < 5; i++) {
    new Thread(a::fun1, “Class A” + i).start();
    new Thread(b::fun1, “Class B” + i).start();
    }
    }
    }
    此时再次运行程序,会发现A和B之间变成串形了,因为A和B都是用了Integer做锁,而且值一样,就变成了一把锁了。
    通过上面的分析,我们知道了为什么不允许使用基本包装类型来做锁对象。那么为什么也不允许String呢?
    原因与Integer缓存池一样,String创建的对象会进入常量池缓存。
    synchronized保证了可见性、原子性、有序性
    上面我们在讲volatile的可见性时的代码,如果我们讲代码这样更改:
    import java.util.concurrent.TimeUnit;
    /**

  • 证明JMM模型中,线程对共享资源的操作,操作的是副本。

  • @author 赵帅

  • @date 2021/1/4
    */public class JMMTest {

    /**

    • 线程是否继续循环
      */
      private static boolean running = true; //0

    public static void main(String[] args) throws InterruptedException {

     new Thread(() -> {
         while (running) {
             System.out.println("hello"); //1
         }
     }, "thread-0").start();
    
     TimeUnit.SECONDS.sleep(1);
     running = false;
     System.out.println("main线程结束");
    

    }

}

我们在1处添加代码,发现0处即使没有添加volatile,代码也是能正常结束的。为什么?
查看 System.out.println的源码:
public void println(String x) {
synchronized (this) {
print(x);
newLine();
}
}
发现使用了synchronized。synchronized是保证了可见性、原子性和有序性。
synchroinzed是如何保证可见性的?
synchronized获得锁之后会执行以下内容:
1.清空工作内存中共享变量的值
2.从主内存重新拷贝需要使用的共享变量的值
3.执行代码
4.将共享变量的最新值刷新到主内存数据
5.释放锁
从上面步骤可以看出,synchroinzed保证了线程可见性。
synchronized是如何保证原子性的?
什么是原子性?
原子性是指操作是不可分的,要么全部一起执行,要么都不执行。
synchronized如何保证原子性
查看synchronized的字节码原语,synchronized是通过monitorenter和monitorexit两个命令来操作的。线程1在执行monitorenter指令的时候,会对Monitor进行加锁,加锁后其他线程无法获得锁,除非线程1主动释放锁。即使在执行过程中,由于时间片用完,线程1放弃cpu,但是它并没有解锁,由于synchronized是可重入的,下一个时间片还是只能被他自己获取到,还是会继续执行代码,直到所有代码执行完,这就保证了原子性。
synchronized是可重入锁,什么是可重入锁?
查看下面一段代码:
package com.xiazhi.thread;
/**

  • @author 赵帅

  • @date 2021/1/9
    */public class SynchronizedDemo4 {

    public synchronized void fun1() {
    fun2();
    }

    public synchronized void fun2() {
    // do something
    }

    public static void main(String[] args) {
    SynchronizedDemo4 demo = new SynchronizedDemo4();
    demo.fun1();
    }
    }
    方法fun1和fun2都被synchronized修饰了,也就是说这两个方法都需要获得锁才可以执行,但是在fun1中调用了fun2方法,程序进入fun1时说明已经获得到this的锁了,之前我们说了,当锁被占用时,其他线程只有等待当前线程释放锁才可以拿到锁,但是现在线程已经拿到锁了,那么再次调用fun2是否能够调用成功?如果可以调用成功就说明这是个可重入锁。也就是说可重入锁就是指一个线程是否可以重复多次获得锁。
    synchronized保证有序性
    有序性是指程序执行的顺序按照代码的先后顺序执行。在并发时,程序的执行可能会出现乱序。给人的直观感觉就是:写在前面的代码,会在后面执行。但是synchronized提供了有序性保证,这其实和as-if-serial语义有关。
    as-if-serial语义是指不管怎么重排序(编译器和处理器为了提高并行度),单线程程序的执行结果都不能被改变。编译器和处理器无论如何优化,都必须遵守as-if-serial语义。只要编译器和处理器都遵守了这个语义,那么就可以认为单线程程序是按照顺序执行的,由于synchronized修饰的代码,同一时间只能被同一线程访问。那么可以认为是单线程执行的。所以可以保证其有序性。
    synchronized锁升级过程
    synchronized在jdk早期是重量级锁。
    什么是重量级锁?
    要解释重量级锁和轻量级锁的概念首先需要理解用户态和内核态的。
    一个对象的内容
    在java中,一个对象的内容主要分为以下几个部分:
    类型 jvm 32位长度 jvm64位长度
    markword 64位,8字节 64位,8字节
    class类指针长度 32位 64位,开启类指针压缩后为32位,默认开启
    属性长度 32位 64位,开启属性指针压缩后为32位,默认开启
    补齐 - -
    java内存地址按照8字节对齐,因此当对象的长度不足8的倍数是,会补齐到8的倍数。例如:
    Object obj = new Object();
    obj对象的大小 = markword(8字节)+Object类指针长度(8字节)+属性指针长度(object无属性,0字节)==16字节,16为8的倍数,所以不需要补齐。
    当开启类指针压缩时:
    obj对象的大小 = markword(8字节)+Object类指针长度(4字节,开启指针压缩)+属性指针长度(object无属性,0字节)==12字节。12不是8的倍数,所以补齐4个字节,最后类大小仍为16字节。
    markword
    我们之前说synchronized必须锁定一个对象,那么多个线程如何判断这个对象是否已经被占用了呢?当锁定这个对象时,会对这个对象添加一个标记,标记这个对象是否加锁。这个标记就放在markword中。
    锁升级
    因为早期的synchronized太重,每次都要调用内核态进行操作,效率太低了,因此为了提升效率,在后来的版本中对synchronized进行了优化,添加了锁升级的过程,锁升级过程中锁的状态就记录在锁对象的markword中。整个锁升级过程如下:
    无锁态:对象刚创建,还没有线程进来加锁。
    偏向锁:第一个线程进来后,升级为偏向锁。
    轻量级锁(自旋锁):当多个线程竞争这把锁时,升级为自旋锁。
    重量级锁:当线程自旋超过10次或等待线程数超过10,升级为重量级锁。
    锁升级过程与markword中内容对应关系如下:
    锁状态 25bit 4bit 1b

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

笔记整理-多线程与高并发 的相关文章

  • OSGi:将参数传递给特定包

    我有一个带有自定义 Main 类的自定义 jar 它启动 OSGi 框架并安装 启动捆绑包 这个主 jar 还包括一个属性文件 目标 我有一个包 A 它应该以某种方式获取主 jar 的该属性文件的属性 我的第一次尝试是在捆绑包 A 中定义一
  • 面试问题 - 在排序数组 X 中搜索索引 i,使得 X[i] = i

    昨天面试时 我被问到了以下问题 考虑一个 Java 或 C 数组X它已排序并且其中没有两个元素是相同的 如何最好地找到索引i这样该索引处的元素也是i 那是X i i 作为澄清 她还给了我一个例子 Array X 3 1 0 3 5 7 in
  • 如何通过keytool命令删除已经导入的证书/别名?

    我正在尝试通过 keytool 命令删除已导入的证书 keytool delete noprompt alias initcert keystore keycloak jks 但低于异常 keytool 错误 java lang Excep
  • 将 Spring Boot 应用程序部署到 Heroku 失败并显示“无效标志:--release -> [帮助 1]”

    当我尝试将代码部署到 Heroku 时 通过git push heroku master 我收到 Maven 错误 remote ERROR Failed to execute goal org apache maven plugins m
  • C# 中的 Culture 相当于 Java 中的 Locale 吗?

    C 使用文化的概念 这在操作上与 Java 中的 Locale 类似吗 或者底层概念是否存在显着差异 从文化而不是语言环境的角度进行工作是一种寻找正确抽象层次的尝试 从以类似方式做事的人群的角度来考虑事物 而不是谈论地理区域和语言 并有点疯
  • Android - Java - 发送 facebook 聊天消息的意图(facebook 禁用 xmpp)

    Facebook 已弃用 xmpp API 有没有办法打开意图 或将数据传递到fb 以在Android设备上发送聊天消息 设备上安装的 Facebook 和 Messenger 应用 谢谢 您需要将 uri 传递给意图 这里10000572
  • Spring Batch:比较数据库之间的数据

    我有两个数据库 Oracle 和 MySQL 目标是将Oracle表中的值保存到MySQL中 要求 MySQL表中不存在数据 但我在理解 Spring Batch 时遇到了困难 步骤中 它包含itemReader itemProcessor
  • 如何提高 Guice 启动时的性能

    好吧 我知道我的计算不客观等等 但无论如何 我讨厌在执行单元测试时等待这么多时间 我的 guice swing 应用程序需要大约 7 秒来初始化 这是一个简单的 IRC 客户端 在那一刻 没有打开连接 我什至还没有调用任何 java io
  • 从 Java 调用 Python 代码时出现问题(不使用 jython)

    我发现这是从 java 运行 使用 exec 方法 python 脚本的方法之一 我在 python 文件中有一个简单的打印语句 但是 我的程序在运行时什么也没做 它既不打印Python文件中编写的语句 也不抛出异常 程序什么都不做就终止了
  • SOAP Web 服务中的用户身份验证

    我提出了一个关于JAX WS 身份验证和授权 如何 https stackoverflow com questions 5314782 jax ws authentication and authorization how to 讨论了安全
  • JFreeChart MeterPlot

    我目前正在用java做Agent项目 在某些时候 我需要显示一个仪表 例如 电池电量 我的程序中有 5 个代理 每个代理都会创建自己的带有名称的仪表图 但不知何故他们没有更新数据集 或者他们正在更新数据集 只是它没有显示在仪表图上 任何想法
  • 如何迭代SparseArray?

    有没有办法迭代 Java SparseArray 适用于 Android 我用了sparsearray通过索引轻松获取值 我找不到 看来我找到了解决方案 我没有正确注意到keyAt index 功能 所以我会这样做 for int i 0
  • 在java中设置Process对象的安全性

    有人可以告诉我如何限制通过进程对象访问系统属性吗 如果我通过进程对象运行以下代码 我可以抛出安全异常吗 System getProperty user home 请告诉我如何为流程对象配置证券 在ProcessBuilder类文档中 环境方
  • 如何将我的自定义相机应用程序设置为默认应用程序?

    如果我使用以下代码 Intent takePictureIntent new Intent MediaStore ACTION IMAGE CAPTURE startActivityForResult takePictureIntent 1
  • 在调试模式下,哪些代码更改会自动反映在 Eclipse 中?

    我使用 eclipse 用于编写 调试 作为 IDE 在调试模式下 当我进行一些更改 例如初始化局部变量 时 它们会自动反映 但其他更改例如更改静态变量的值 有时我会收到一条消息 说我需要重新启动虚拟机 有时则不需要 现在的问题是哪些类型的
  • 用于将字符串与通配符模式进行匹配的递归函数

    所以我一整天都在试图解决这个作业 只是无法完成 以下函数接受 2 个字符串 第二个 不是第一个 可能包含 的 星号 An 是字符串的替换 空 1个字符或更多 它可以出现 仅在s2中 一次 两次 更多或根本不出现 它不能与另一个相邻 ab c
  • Spring Boot 健康执行器 - 什么时候上线?

    我找不到任何有关 Springs Health Actuator 何时返回 UP 状态的文档 你能依靠一切吗 Components正在初始化 会不会 Controller准备好满足请求了吗 为了测试应用程序上下文是否已加载 您可以执行此自定
  • PSQLException:错误:关系“TABLE_NAME”不存在

    我正在尝试在 PostgreSQL 8 4 2 DB 上运行休眠 每当我尝试运行简单的java代码时 例如 List
  • 1° 夏令时 Java 和 JS 表现出不同的行为

    假设巴西利亚 GMT 0300 夏令时于 21 10 2012 00 00 00 此时时钟应提前一小时 Java new Date 2012 1900 9 21 0 0 0 Sun Oct 21 01 00 00 BRST 2012 Chr
  • 文件构造函数说明

    我无法理解以下文件构造函数 public File String parent String child and public File File parent String child 参数有什么作用parent and child该文件

随机推荐

  • 电学基本概念

    电压 电流 电阻 功率 交流 直流 电压 电流 电阻功率之间关系
  • 如何简单理解概率分布函数和概率密度函数?

    本篇文章是在 应该如何理解概率分布函数和概率密度函数 的基础上整理来的 非常感谢原作者 目录 1 先从离散型随机变量和连续性随机变量说起 2 离散型随机变量的概率函数 概率分布和分布函数 2 1 概率函数和概率分布 2 1 1 概率函数 2
  • MyBatis分页PageHelper和RowBounds区别

    测试机器 笔记本 win7 cpu i5 4210M 8G内存 测试数据 单表 181w数据 mybatis常用分页组件PageHelper和RowBounds 1 性能对比 2 原理分析 PageHelper 物理分页 通过拦截器加 li
  • 用chrony使两台机器之间同步时间

    用chrony使两台机器之间同步时间 chrony服务部署 两台机器 a 第一台机器从阿里云同步时间 第二台机器从第一台机器同步时间 b 第一台服务器使用系统时间作为第二台服务器的时钟源 第一台服务器层级设置为6 a 在第一台机器的 etc
  • [Qt] [adb] Qt中通过adb给手机发送指令

    Qt中给手机发送adb指令 需要通过QProcess类来操作 通过adb device获取设备连接状态 void DeviceInfo findDevice 发送adb指令 QProcess processFindDev gt start
  • 固态硬盘usb测试软件,固态硬盘检测修复坏道三级OP设置软件HDAT2 5.3 ISO版

    HDAT2是用于测试或诊断硬盘 SSD和USB设备的程序 这里分享的是iso版 更新日期是2016年11月 你必须把它用ultraiso做成U盘启动盘 启动电脑后在dos环境下使用 这里介绍的主要功能是修复ssd的坏道 也就是Secure
  • 创建一个自定义插件,实现一个登录页面

    运行结果如下 话不多说 直接开整 div div
  • 05【掌握】 SpringBoot 清空Redis所有缓存

    package top yangbuyi system controller import org springframework beans factory annotation Autowired import org springfr
  • css3顺时,CSS3(transforms)

    transforms 主要包括了两个属性 transform 只可以转换 由盒子模型定位的元素 而根据经验也就是 具备了display block这个属性 由盒子模型定位元素 transform 指定作用在元素上的变形 取值为空格分隔的一些
  • Spring Cloud灰度部署

    1 背景 灰度部署 在我们系统发布生产环境时 有时为了确保新的服务逻辑没有问题 会让一小部分特定的用户来使用新的版本 比如客户端的内测版本 而其余的用户使用旧的版本 那么这个在Spring Cloud中该如何来实现呢 负载均衡组件使用 Sp
  • 【安卓学习之工具学习】网络通信测试工具-socket/post/get

    在安卓开发中 网络请求基本上都有用到 有时候我们通信出现问题 我们不知道是服务端问题还是客户端问题 就可以使用第三方的工具来测试 以保证能更好的确定出现bug的方向 当然也有时候接手别人开发的app 但又不知道里面的通信协议 也可以通过这个
  • Visio制图拷贝到word文档中显示不全、只显示一行

    问题描述 Visio 2016绘制技术流程图完毕 拷贝到Word 2016中时 只显示一行 最底部部分 上面其他部分只能看到最外边边框其他部分侵入上方文本区 且不显示 解决方案 选中要插入的行 设置行间距为1 5倍 我的问题是解决了 欢迎补
  • NVDLA系列之C-model:cvif<100>

    NV NVDLA cvif cpp pdp2cvif wr req b transport void NV NVDLA cvif pdp2cvif wr req b transport int ID nvdla dma wr req t p
  • JDBC中对url的一些理解

    大家都知道 用java来连接数据库 一般都是class forName 然后用DriverManager来生成一个Connection 生成Connection的method为 DriverManager getConnection 其中里
  • STM32_USART 串口通讯

    STM32 USART 串口通讯 通信方式的分类 按照通信方式分类 按照数据传送方向分类 通信接口及其说明如下图所示 STM32的串口通信 引脚接线 串口通讯的过程 串口发送数据 串口接收数据 USART中断请求 通信方式的分类 按照通信方
  • 【数据库1】mysql,DDL/DML/DQL,外键约束/多表/子查询,事务/连接池

    文章目录 1 mysql安装 存储 集合 内存 临时 IO流 硬盘 持久化 1 1 服务端 双击mysql installer community 5 6 22 0 msi 1 2 客户端 命令行输入mysql u 实际是如下安装路径的bi
  • 计算机怎么把硬盘分成几个,如何把电脑的一个盘的容量分给另外一个盘

    把电脑中的一个分区闲置容量分去给另外一个分区 今天就来分享如何把其中一个分区的闲置容量分去另外一个分区 以我的操作为例子 如我要把 F盘 的闲置2G空间 分给 G盘 如图所示 F盘是 4 99GB 的容量 G盘是 2 99GB 的容量 首先
  • uboot中启动linux内核的函数——do_bootm_linux函数解析

    1 do bootm linux函数解析 do bootm linux函数是专门启动linux内核的 包括以下功能 1 确认当前的机器码 可以从全局变量gd或者环境变量machid中获取 其中环境变量machid的优先级高于gd中的机器码
  • 禁欲28天!一宅男居然肝出如此详细Web安全学习笔记,学妹看完直接抽搐了!(持续中出)

    1 1 Web技术演化 1 1 1 简单网站 1 1 1 1 静态页面 Web技术在最初阶段 网站的主要内容是静态的 大多站点托管在ISP上 由文字和图片组成 制作和表现形式也是以表格为主 当时的用户行为也非常简单 基本只是浏览网页 1 1
  • 笔记整理-多线程与高并发

    多线程与高并发 目录 多线程与高并发 https www cnblogs com Zs book1 p 14318992 html share token 641d3935 0525 44d5 a772 9764bf2fad2a 一 了解多