当前位置: 首页 > news >正文

多线程(进阶)

在这里插入图片描述

🎉🎉🎉写在前面:
博主主页:🌹🌹🌹戳一戳,欢迎大佬指点!
目标梦想:进大厂,立志成为一个牛掰的Java程序猿,虽然现在还是一个小菜鸟嘿嘿
-----------------------------谢谢你这么帅气美丽还给我点赞!比个心-----------------------------

在这里插入图片描述


多线程进阶

  • 一,常见锁策略
    • 1.1,乐观锁,悲观锁
    • 1.2,读写锁,普通互斥锁
    • 1.3,重量级锁,轻量级锁
    • 1.4,自旋锁,挂起等待锁
    • 1.5,公平锁,非公平锁
    • 1.6,可重入锁,不可重入锁
    • 1.7,synchronized总结
  • 二,CAS
    • 2.1,CAS的应用场景
      • 2.1.1,实现原子类
      • 2.1.2,实现自旋锁
  • 三,ABA问题
  • 四,synchronized原理
    • 4.1,synchronized优化手段
      • 4.1.1,锁升级
      • 4.1.2,锁消除
      • 4.1.3,锁粗化
  • 五,Callable接口
  • 六,JUC(Java.util.concurrent)中的常见类
    • 6.1,ReentrantLock
    • 6.2,原子类
    • 6.3,Semaphore信号量
    • 6.2,CountDownLatch
  • 七,线程安全的集合类
    • 7.1,多线程下使用ArrayList
    • 7.2,多线程下使用队列
    • 7.3,多线程下使用哈希表
      • 7.3.1,HashTable
      • 7.3.2,ConcurrentHashMap
  • 八,死锁


一,常见锁策略

1.1,乐观锁,悲观锁

乐观锁:假设数据一般情况下不会出现并发冲突,所以在对数据进行操作的时候,会直接去操作,然后在操作完对数据进行提交的时候(写回内存),再通过一定的机制来验证数据是否存在了冲突。
悲观锁:总是假设最坏的情况,就是总是存在并发冲突的问题,所以每次操作都是先尝试加锁后再进行操作。

synchronized初始使用乐观锁策略,在发现锁竞争比较频繁的时候,就会自动切换成悲观锁策略。初始使用乐观锁策略,如果真的恰好没有冲突存在,那么就直接减去了加锁的步骤,可以节省一定的时间和资源,直至乐观锁尝试提交数据几次都失败发现存在冲突的时候,这个时候再调整为悲观锁策略,去加锁进行处理。


那么,乐观锁是如何检测到存在访问冲突的呢,这里引入一个版本号来进行标识。

在这里插入图片描述


1.2,读写锁,普通互斥锁

普通互斥锁。例如我们的sunchronized,只要是多个线程之间竞争同一把锁,那么就会有锁竞争,从而产生阻塞等待。 而读写锁相对于普通的互斥锁,会根据你的操作意图(读or写)来决定。读写锁存在两种加锁情况,加读锁和加写锁。例如:

1,多个线程同时进行读数据,直接读取即可,也就是读锁与读锁之间不会存在互斥的问题。【不存在线程安全问题】
2,多个线程同时进行修改数据,需要互斥进行操作,也就是写锁与写锁之间会存在锁竞争。【存在线程安全问题】
3,多个线程,有的读取数据,有的修改数据,需要互斥进行操作,也就是读锁与写锁之间存在竞争。【存在线程安全问题】

读写锁的机制就相当于是把读操作与写操作区分对待了。在实际的开发环境中,其实大部分情况下读的操作场景会多于写的操作场景,所以利用读写锁,就可以减少大量的锁竞争,优化了效率。


1.3,重量级锁,轻量级锁

在这里插入图片描述


1.4,自旋锁,挂起等待锁

自旋锁:循环的尝试加锁,无限循环,直至获取到锁为止。获取锁会很及时,但是这个时候CPU的占用率就比较高。
挂起等待锁:尝试获取锁失败之后,就会挂起等待,之后再尝试进行加锁。这个时候CPU占用率就比较低,可以去做别的事情,之后再来获取锁,只是相对于自旋锁可能没有那么及时的获取到锁。

自旋锁是轻量级锁的一种典型实现,挂起等待锁是重量级锁的一种典型实现


1.5,公平锁,非公平锁

公平锁与非公平锁,首先这里先要颠覆大家对于公平的定义,这里什么叫做公平:那就是先到先得,讲究一个先来后到。

公平锁:先到的线程则先获取到锁,后到的线程则后面才能获取到。
非公平锁:不遵守先来后到,先到的与后到的线程都有同等的机会获取到锁。


操作系统默认的随机调度,也就是非公平锁,如果想要实现公平锁,需要依赖额外的数据结构来实现。


1.6,可重入锁,不可重入锁

同一个线程针对同一把锁连续加锁两次,会造成死锁的是不可重入锁,不会死锁的是可重入锁。


1.7,synchronized总结

在这里插入图片描述


synchronized不是读写锁,是非公平锁,是可重入锁。synchronized这种智能的调整机制是JVM为我们实现好的自动优化策略,用来适应不同的场景。


二,CAS

CAS:全称就是 compare and swap,比较与交换的意思,它可以完成变量在满足条件下进行赋值的同时保证这是一个原子性的操作即线程安全。

操作机制:

把内存中的值val和CPU寄存器A中的值进行比较,如果值相等(也就是满足条件),就把另一个寄存器B中的值和内存中的值val进行交换。其实这里的核心就是赋值是利用交换实现的,我们的目的就是将寄存器B中的值放到内存中,也就是赋值操作。

上述的这个操作是一个硬件指令完成的,所以也就是线程安全的。并且相对于加锁,CAS的开销也小一些,效率更高。但是CAS的使用面肯定是比加锁要小很多的。


2.1,CAS的应用场景

2.1.1,实现原子类

之前在学习synchronized的时候,在多线程环境下进行count++我们是需要加锁的,因为是线程不安全的,加锁保证了安全,但是效率会打折扣。但是基于CAS实现保证原子性的++操作,既可以保证原子性,达到线程安全的目的,也能提高效率。


在这里插入图片描述


在这里插入图片描述


2.1.2,实现自旋锁

自旋锁是纯用户态的轻量级锁,当发现锁被其他线程占用之后,会返回检测是否锁已经释放,可以达到第一时间获取到锁的目的。但是如果当前锁竞争比较冲突的情况下,自旋锁会比较占用CPU资源。


在这里插入图片描述


三,ABA问题

什么是ABA问题:假设存在两个线程t1,t2,存在一个共享的变量val,然后t1准备去修改这个val值,但是这个点t2线程将val的值由A变成了B,然后又变回了A,但是站在t1线程的角度,CAS也检测不到这个值原来是变过的,那这个时候到底还修不修改呢?就有疑问了。

在大多数的情况下,类似于t2线程的这种前后反复横跳的修改,一般不会影响到t1线程修改的正确性。但是万事没有绝对,这里总归是一个隐含的bug,在某些特殊场景下可能就会有问题了。


在这里插入图片描述


那么如何解决ABA问题呢?在单纯比较值的基础之上,加上一个记录来记录下内存中数据的变化就好了。这个记录可以是另外的弄一个内存,来保存变量的修改次数(版本号)或者是上次的修改时间。无论是修改时间还是版本号,都是只增不减的,通过这种办法,就可以做到区分该变量到底是没改过还是改了又变回来的。

在这里插入图片描述


四,synchronized原理

4.1,synchronized优化手段

4.1.1,锁升级

【1,偏向锁】

第一次加锁会进入偏向锁的状态,偏向锁的核心就在于它不是真的加锁,只是给偏向锁头加了一个标记,标识它当前是属于哪个线程,后续如果存在锁竞争了,才会真的加锁。

在这里插入图片描述


偏向锁是最特殊的一种状态,如果存在了锁竞争,就会升级为轻量级锁,如果锁竞争激烈,那么就会进一步升级为重量级锁,这么一个层层升级的过程就叫做锁升级。


4.1.2,锁消除

编译器+JVM自动判定,如果在你的代码里面,发现某处不必加锁了,然后你写了synchronized,那么就会自动给你把锁给去掉。

另外注意,锁消除是一种优化行为,所以在判定的时候也不可能每次都十分的准确,只有当100%拿捏的准这里锁确实可以消除那么就会给你消除掉,不然在无法肯定的情况下是不会进行锁消除的。

偏向锁虽然不是真加锁,只是改了一个标志位,但是即使这一点开销,在锁消除下也会给你弄掉。


4.1.3,锁粗化

锁粗化指的是锁的粒度。锁的粒度指的是synchronized所对应的代码块里面代码的多少。包含的代码少,锁的粒度就低,包含的代码多,锁的粒度就高。

在这里插入图片描述


本身加锁是为了能够保证线程安全,但是同一块的逻辑,如果通过一个锁(前提是多个任务是一个锁对象,可以粗化到一起),范围大一点,就可以达到保证线程安全的目的话,内部就不需要多次的加锁解锁,那样会带来额外的由于锁竞争带来的开销。

另外,锁粗化也不是越粗越好,因为粒度太大,锁的代码就多,这其实是不利于我们的并发执行的。


五,Callable接口

Callable接口类似于Runnable接口,都是用来描述任务的,只不过它是带返回值的,可以用来解决那些希望返回结果的场景。

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class Demo14 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //使用Callable定义一个任务
        Callable<Integer> callable = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                int sum = 0;
                for (int i = 0; i <= 10000; i++) {
                    sum += i;
                }
                return sum;
            }
        };

        FutureTask<Integer> futureTask = new FutureTask<>(callable);

        //Thread不能直接传入Callable接口,没有这个构造方法,需要一个中间类
        Thread t = new Thread(futureTask);
        t.start();

        //获取线程的计算结果 get方法会阻塞,直至call方法计算完毕,get才会返回
        System.out.println(futureTask.get());//输出50005000
    }
}

在这里插入图片描述


说到这里,我们又学习到了一种新的创建线程的方式,总结一下创建线程执行任务的方式如下:

1,写一个类继承Thread(也可使用匿名内部类)

2,写一个类实现Runnable接口(也可使用匿名内部类)

3,lambda表达式

4,创建线程池

5,使用Callable接口


六,JUC(Java.util.concurrent)中的常见类

6.1,ReentrantLock

可重入互斥锁,这个锁的定位其实和Synchronized差不多,但是也有功能是Synchronized做不到的。

ReentrantLock的三个核心用法:

public class Demo15 {
    public static void main(String[] args) {
        ReentrantLock reentrantLock = new ReentrantLock();

        try {
            //1.加锁,获取不到就死等
            //reentrantLock.lock();

            //2,尝试获取锁,成功就加上,失败就放弃,不会死等
            reentrantLock.tryLock();
        }finally {
            //3,解锁
            reentrantLock.unlock();
        }
    }
}

至于这里为什么要使用try{}finally{}进行包裹,那是因为ReentrantLock不像Synchronized那样最终无论如何都会执行解锁操作,这里是需要手动解锁的,所以万一你中间出个什么岔子代码根本来执行到解锁或者你给忘了,那么这里就永远不会解锁了。而放到finally块里面就一定会执行到解锁。


【ReentrantLock 和 synchronized 的区别 :】

1,Synchronized使用时不需要手动释放锁,但是ReentrantLock是需要手动释放锁的。
2,synchronized 在申请锁失败时, 会死等. ReentrantLock 可以通过 trylock 的方式等待一段时间就放弃 。
public class Demo15 {
    public static void main(String[] args) {
        Lock reentrantLock = new ReentrantLock();
        try {
            reentrantLock.tryLock(1000, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

3,Synchronized是非公平锁,ReentrantLock默认是非公平锁,但是可以设置为公平锁。
ReentrantLock reentrantLock = new ReentrantLock(true);//传入true,就可设置为公平锁

4,Synchronized搭配wait/notify使用,唤醒机制只能随机唤醒一个线程。但是ReentrantLock搭配Condition 类实现等待-唤醒,可以做到精准的唤醒某个等待线程。
5,synchronized 是一个关键字, 是 JVM 内部实现的(大概率是基于 C++ 实现). ReentrantLock 是标准库的一个类, 在 JVM 外实现的(基于 Java 实现) 【最不关键的区别】

6.2,原子类

原子类的内部都是基于CAS实现的,所以效率会比加锁++的效率高很多。这些原子类都在java.util.concurrent.atomic下,主要是适用于计数之类的场景。

在这里插入图片描述


现在以AtomicInteger为例:

public class Demo16 {
    public static void main(String[] args) throws InterruptedException {
        AtomicInteger count = new AtomicInteger(0);//传入一个你要操作的值
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 1000; i++) {
                count.getAndIncrement();//这个相当于count++
                //怎么区分看increment以及decrement的位置就好
//                count.incrementAndGet();//这个相当于++count
//                count.getAndDecrement();//这个相当于count--
//                count.decrementAndGet();//这个相当于--count
            }
        });

        Thread t2 = new Thread(()->{
            for (int i = 0; i < 1000; i++) {
                count.getAndIncrement();//这个相当于count++

            }
        });

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println(count.get());//输出2000
    }
}

6.3,Semaphore信号量

信号量这个概念是由迪杰斯特拉这个大佬提出的。

在这里插入图片描述


看到这里,其实我们就可以认为信号量是一种更加广义的锁,我们之前学的锁就是信号量为1的一个特殊的锁。

public class Demo17 {
    public static void main(String[] args) throws InterruptedException {
        //构造的时候要指定计数器的值,即可用资源的个数
        Semaphore semaphore = new Semaphore(3);
        //执行P操作
        semaphore.acquire();
        System.out.println("P操作 申请资源");
        semaphore.acquire();
        System.out.println("P操作 申请资源");
        semaphore.acquire();
        System.out.println("P操作 申请资源");

        //到这里就会阻塞 除非semaphore.release()进行V操作释放资源
        semaphore.acquire();
        System.out.println("P操作 申请资源");


    }
}

6.2,CountDownLatch

能够同时等待N个任务执行结束。

public class Demo18 {
    public static void main(String[] args) throws InterruptedException {
        //有十个选手参赛
        CountDownLatch countDownLatch = new CountDownLatch(10);
        for(int i = 0;i < 10;i++){
            //创建10个线程来执行任务
            Thread t = new Thread(()->{
                System.out.println("选手出发!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("选手到达!" + Thread.currentThread().getName());
                countDownLatch.countDown();//每到达一个选手计数器减1
            });
            t.start();
        }

        //await()会进行阻塞等待,直至计数器减为0,才会解除阻塞
        countDownLatch.await();
        System.out.println("全部选手到达,比赛结束!");
    }
}

在这里插入图片描述


应用场景比如要完成某个任务,但是需要向多个不同的服务器分别请求数据,那么这个时候这个任务的完成标准就是所有的服务器数据都请求到才算,那么这里就可以运用CountDownLatch 来阻塞等待这些请求执行完毕。


七,线程安全的集合类

大部分的集合类都是线程不安全的。Vector,Stack,HashTable是线程安全的,但是基于它们的加锁机制可能不太好,所以也就不太推荐使用。


7.1,多线程下使用ArrayList

(1),自己通过Synchronized,ReentrantLock来加锁,达到线程安全的目的。

(2),使用Collections.synchronizedList(new ArrayList),这是标准库提供的一个基于Synchronized实现的线程安全的List类

(3),使用CopyOnWriteArrayList。使用场景较为有限,一般写操作较为频繁的场景下不用。

在这里插入图片描述


正是因为中间是要依靠拷贝,所以当你的写操作频繁,或者说数据量特别大的情况下,拷贝就会消耗比较大的资源,那么也就不太推荐使用CopyOnWriteList。


7.2,多线程下使用队列

这个主要就是阻塞队列的各种实现与应用。


7.3,多线程下使用哈希表

多线程下使用哈希表可以有 HashTable,ConcurrentHashMap。

7.3.1,HashTable

对于HashTable来说,不推荐使用。它的加锁方式就是直接在方法上加锁,也就是相当于锁对象是this,那么对于一个HashTable对象而言,整个哈希表就只有一把锁。所以在多线程下,无论是你进行什么操作,都会有频繁的锁竞争。

7.3.2,ConcurrentHashMap

相对于HashTable,ConcurrentHashMap做出了许多的优化。【面试题】

(1),锁的粒度的优化。

在这里插入图片描述


(2),只对写操作加锁,读操作不加锁。

这算是一种比较激进的优化策略,这种分操作类型决定是否加锁的方式,可以进一步的降低锁冲突的次数。

在这里插入图片描述


另外对于读操作,也是使用了volatile来保证我们读取到的数据是即时的,即从内存读取。


(3),充分利用了CAS特性

比如在维护哈希表的数据的个数上,都是通过CAS来实现的,而不是加锁。包括还有一些地方是通过CAS实现的轻量级锁来实现的。Synchronized虽然内部也做了很多的优化,但是到底是底层JVM的自动优化,我们自己是不可控的。

总体来说,ConcurrentHashMap的设计思想就是能不加锁就不加锁。


(4),对于扩容机制:化整为零

先对于HashTable来说,当put()元素的时候,发现当前负载因子已经超过了阈值,那么就要触发扩容机制。扩容的方法就是申请一个新的数组,然后把原有元素全部拷贝过去。

那么这种做法就会有一个问题,如果说数据量特别大,那么在你put()的那个时候,扩容的时间就会比较久,那么对于这个put()操作所花的时间也会很久,那这如果是在实际开发中向服务器提交数据的情况,很可能这一下请求就超时了,未能成功提交。


那么ConcurrentHashMap的化整为零的思路就是申请一个新数组,在扩容期间,新的数组与旧的数组同时存在。

1,扩容期间,每次都是会搬动一小部分元素到新的数组里面。后续只要是操作CurrentHashMap的线程都会参与这个搬运元素的过程。

2,这个期间,插入元素就直接往新数组里面插入,查询元素就新数组与旧数组一起查,删除元素就直接删了也不用搬运。

3,当最终所有元素都搬运完全之后,旧的数组就被释放掉了。


【面试题:HashMap,HashTable,ConcurrentHashMap三者的区别?】

1,首先,从线程安全的角度下看,HashMap是只能在单线程使用的,在多线程下是线程不安全的。但是HashTable,ConcurrentHashMap在多线程下是线程安全的。

2,HashMap的key是可以为null的,但是HashTable,ConcurrentHashMap是不可以为null的。

3,重点说说HashTable,ConcurrentHashMap二者区别,后者相对于前者在锁的粒度,只对写操作解锁,内部利用CAS特性,以及扩容机制上都进行了优化。

扩展:ConcurrentHashMap旧版本的加锁方式是分段加锁,也就是好几个链表用同一把锁,现在新版本是一个链表一把锁,将锁冲突的概率降到了最低。


八,死锁

死锁是最严重的bug之一,一个线程或者多个线程都在等待某个资源的释放,导致线程无期限的被阻塞,程序无法正常的终止。


以前所说的几个死锁的情况:

1,一个线程,针对一把锁,连续加锁两次。如果该锁是可重入锁还好,但是如果是不可重入锁,那么就会死锁。

2,两个线程,两把锁,在释放第一把锁之前想要获取第二把锁,只不过两个线程之间加锁的顺序刚好僵住了。

public class Demo19 {
    public static Object locker1 = new Object();
    public static Object locker2 = new Object();
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            System.out.println("t1 线程尝试获取锁locker1");
            synchronized (locker1){
                System.out.println("t1 线程成功获取锁locker1");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                System.out.println("t1 线程尝试获取锁locker2");
                synchronized (locker2){
                    System.out.println("t1 线程成功获取锁locker2");
                }
            }
        });


        Thread t2 = new Thread(()->{
            System.out.println("t2 线程尝试获取锁locker2");
            synchronized (locker2){
                System.out.println("t2 线程成功获取锁locker2");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                System.out.println("t2 线程尝试获取锁locker1");
                synchronized (locker1){
                    System.out.println("t2 线程成功获取锁locker1");
                }
            }
        });

        t1.start();
        t2.start();

        t1.join();
        t2.join();
    }
}

在这里插入图片描述


可以看到t1线程尝试获取locker2没有成功,t2线程尝试获取locker1没有成功,因为中间出现了死锁。


3,多个线程多把锁,哲学家就餐问题

在这里插入图片描述


【如何解决死锁?】

产生死锁的四个必要条件:

1,互斥使用:当线程1占用锁A之后,线程2就是用不了。

2,不可抢占:当线程1占用锁A之后,线程2不能直接把锁抢过来,除非线程1主动释放。

3,请求与保持:当有多把锁的时候,线程1在持有锁A的情况下,还想持有锁B。

4,循环等待:线程1等待线程2释放锁,线程2等待线程3释放锁,线程3又等待线程1释放锁,前后套成了一个环。


上面四个都是形成死锁的四个必要条件,所以只要破坏其中的任意一个条件,死锁就迎刃而解。前两个都是锁的基本特性,我们改变不了,所以只能从后面两个入手解决。

请求与保持这个条件有可能打破,主要是取决于你代码的写法,如果你获取第二个锁之前就把第一个锁释放了,那么就不会有死锁的问题了(两个锁不是嵌套着),不过在某些需求场景下,可能不允许你释放掉第一个锁,那么就还是解决不了问题,所以不具有普适性。

所以,最有把握破坏的条件就是循环等待。

【如何破坏循环等待条件?】

约定好加锁的顺序,最常用的技术就是锁排序,也就是对锁进行编号,然后多个线程来获取锁的时候就要按照固定的编号从小到大来获取锁。

public class Demo19 {
    public static Object locker1 = new Object();
    public static Object locker2 = new Object();
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            System.out.println("t1 线程尝试获取锁locker1");
            synchronized (locker1){
                System.out.println("t1 线程成功获取锁locker1");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                System.out.println("t1 线程尝试获取锁locker2");
                synchronized (locker2){
                    System.out.println("t1 线程成功获取锁locker2");
                }
            }
        });


        Thread t2 = new Thread(()->{
            System.out.println("t2 线程尝试获取锁locker1");
            synchronized (locker1){
                System.out.println("t2 线程成功获取锁locker1");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                System.out.println("t2 线程尝试获取锁locker2");
                synchronized (locker2){
                    System.out.println("t2 线程成功获取锁locker2");
                }
            }
        });

        t1.start();
        t2.start();

        t1.join();
        t2.join();
    }
}

在这里插入图片描述


例如上面这里,我们约定t1线程先获取locker1,然后再获取locker2,然后t2线程也是先获取locker1,再获取locker2。这样就不会出现死锁的问题了。

当然,还有一个银行家算法也可以解决死锁的问题,只不过这个算法比较复杂,在实际的开发环境中用的比较少。


今天分享就这么多了啰,大家如果觉得写的不错的话还请点点赞咯,十分感谢呢!🥰🥰🥰
在这里插入图片描述

相关文章:

  • 基于affine+sift特征提取的图像配准算法matlab仿真
  • 【DLY-310端子排型电流继电器】
  • STM32:GPIO输入(硬件部分)(内含实验现象+按键介绍+传感器模块介绍+硬件电路)
  • 【XGBoost】第 5 章:XGBoost 揭幕
  • Spring声明式基于注解的缓存(3-精进篇)
  • 怎么入门网络安全,学这两类证书就够了NISP或CISP
  • 探究MYSQL之索引
  • Linux环境搭建与登陆
  • WEB自动化测试(5)—— Cypress-元素交互
  • 图片速览 Deep Clustering for Unsupervised Learning of Visual Features
  • HCIA网络基础9-VRP文件系统管理
  • springboot整合mycat实现读写分离
  • iOS 16 SwiftUI 4.0 列表(List)项分隔线变短的原因及解决
  • 创邻科技入选Gartner全球《图数据库管理系统市场指南》代表厂商
  • OpenHarmony如何控制屏幕亮度
  • 分享一款快速APP功能测试工具
  • 345-反转字符串中的元音字母
  • javascript面向对象之创建对象
  • java中的hashCode
  • Lucene解析 - 基本概念
  • node学习系列之简单文件上传
  • Python - 闭包Closure
  • React 快速上手 - 07 前端路由 react-router
  • weex踩坑之旅第一弹 ~ 搭建具有入口文件的weex脚手架
  • 聊一聊前端的监控
  • 面试遇到的一些题
  • 网络应用优化——时延与带宽
  • 我这样减少了26.5M Java内存!
  • LevelDB 入门 —— 全面了解 LevelDB 的功能特性
  • shell使用lftp连接ftp和sftp,并可以指定私钥
  • 扩展资源服务器解决oauth2 性能瓶颈
  • ​linux启动进程的方式
  • # 深度解析 Socket 与 WebSocket:原理、区别与应用
  • ### Error querying database. Cause: com.mysql.jdbc.exceptions.jdbc4.CommunicationsException
  • #我与Java虚拟机的故事#连载07:我放弃了对JVM的进一步学习
  • $HTTP_POST_VARS['']和$_POST['']的区别
  • (2/2) 为了理解 UWP 的启动流程,我从零开始创建了一个 UWP 程序
  • (4)STL算法之比较
  • (6)设计一个TimeMap
  • (Matlab)遗传算法优化的BP神经网络实现回归预测
  • (第27天)Oracle 数据泵转换分区表
  • (附源码)springboot掌上博客系统 毕业设计063131
  • (学习日记)2024.03.12:UCOSIII第十四节:时基列表
  • (转)VC++中ondraw在什么时候调用的
  • (转)德国人的记事本
  • (转)视频码率,帧率和分辨率的联系与区别
  • *_zh_CN.properties 国际化资源文件 struts 防乱码等
  • .apk文件,IIS不支持下载解决
  • .bat批处理(一):@echo off
  • .desktop 桌面快捷_Linux桌面环境那么多,这几款优秀的任你选
  • .NET 2.0中新增的一些TryGet,TryParse等方法
  • .net core MVC 通过 Filters 过滤器拦截请求及响应内容
  • .net core 实现redis分片_基于 Redis 的分布式任务调度框架 earth-frost
  • .net专家(高海东的专栏)
  • @data注解_一枚 架构师 也不会用的Lombok注解,相见恨晚