zoukankan      html  css  js  c++  java
  • 多线程学习(第三天)线程间通信

    一、volatile 与 synchronized

      java多线程支持每个线程拥有对象的拷贝,这样每个线程内部就是独立的java运行环境。但是这样存在问题,共享内存中的对象或变量,在线程内对其拷贝进行修改后,其他线程读取的数据则为脏数据。

      volatile:作用就是告诉程序,当线程修改拷贝后,需要将修改后的内容同步到内存中,其他线程读取数据时,需要到内存中同步。(缺点:影响效率)

      synchronized:可以修饰方法或代码块,保证多线程的情况,同一时间只允许一个线程执行该代码。

      当一个线程获取到锁之后,其他线程进入同步队列,线程状态编程BLOCKED。当线程执行完成后,会进行释放操作,并唤醒所有等待的线程。

      代码验证:

    package com.example.thread.state;
    
    public class MyThread implements Runnable {
    
        public static void main(String[] args) {
            MyThread myThread = new MyThread();
            Thread one = new Thread(myThread, "线程1");
            Thread two = new Thread(myThread, "线程2");
            Thread three = new Thread(myThread, "线程3");
            Thread four = new Thread(myThread, "线程4");
    
            one.start();
            two.start();
            three.start();
            four.start();
    
            while(true) {
                System.out.println("线程1状态:"+one.getState());
                System.out.println("线程2状态:"+two.getState());
                System.out.println("线程3状态:"+three.getState());
                System.out.println("线程4状态:"+four.getState());
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    
        @Override
        public void run() {
            synchronized (this) {
                try {
                    System.out.println(Thread.currentThread().getName()+"抢到了锁");
                    Thread.sleep(2000);
                    System.out.println(Thread.currentThread().getName()+"释放锁");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    Main

      结果状态:

    线程1状态:RUNNABLE
    线程2状态:RUNNABLE
    线程3状态:RUNNABLE
    线程4状态:RUNNABLE
    线程2抢到了锁
    线程1状态:BLOCKED
    线程2状态:TIMED_WAITING
    线程3状态:BLOCKED
    线程4状态:BLOCKED
    线程1状态:BLOCKED
    线程2状态:TIMED_WAITING
    线程3状态:BLOCKED
    线程4状态:BLOCKED
    线程1状态:BLOCKED
    线程2状态:TIMED_WAITING
    线程3状态:BLOCKED
    线程4状态:BLOCKED
    线程2释放锁
    线程4抢到了锁
    线程1状态:BLOCKED
    线程2状态:TERMINATED
    线程3状态:BLOCKED
    线程4状态:TIMED_WAITING
    线程1状态:BLOCKED
    线程2状态:TERMINATED
    线程3状态:BLOCKED
    线程4状态:TIMED_WAITING
    线程1状态:BLOCKED
    线程2状态:TERMINATED
    线程3状态:BLOCKED
    线程4状态:TIMED_WAITING
    线程1状态:BLOCKED
    线程2状态:TERMINATED
    线程3状态:BLOCKED
    线程4状态:TIMED_WAITING
    线程4释放锁
    线程1抢到了锁
    线程1状态:TIMED_WAITING
    线程2状态:TERMINATED
    线程3状态:BLOCKED
    线程4状态:TERMINATED
    线程1状态:TIMED_WAITING
    线程2状态:TERMINATED
    线程3状态:BLOCKED
    线程4状态:TERMINATED
    线程1状态:TIMED_WAITING
    线程2状态:TERMINATED
    线程3状态:BLOCKED
    线程4状态:TERMINATED
    线程1状态:TIMED_WAITING
    线程2状态:TERMINATED
    线程3状态:BLOCKED
    线程4状态:TERMINATED
    线程1释放锁
    线程3抢到了锁
    线程1状态:TERMINATED
    线程2状态:TERMINATED
    线程3状态:TIMED_WAITING
    线程4状态:TERMINATED
    线程1状态:TERMINATED
    线程2状态:TERMINATED
    线程3状态:TIMED_WAITING
    线程4状态:TERMINATED
    线程1状态:TERMINATED
    线程2状态:TERMINATED
    线程3状态:TIMED_WAITING
    线程4状态:TERMINATED
    线程1状态:TERMINATED
    线程2状态:TERMINATED
    线程3状态:TIMED_WAITING
    线程4状态:TERMINATED
    线程3释放锁
    线程1状态:TERMINATED
    线程2状态:TERMINATED
    线程3状态:TERMINATED
    线程4状态:TERMINATED

    二、等待/通知机制

      等待(wait):

      wait的使用前提是,线程必须获取当前对象的锁。所以wait的使用必须在synchronized中。

      通知(notify):

      notify唤醒一个等待当前锁的线程。(多个线程等待的时候,随机唤醒一个)。synchronized中使用。

    wait和sleep比较
    wait:当前线程会释放锁
    sleep:不释放锁

      下面一洗车喷漆为例

    public class Car {
    
        /**
         * INIT:初始化
         * WASHED: 洗完
         * COLORED:染完色
         */
        private String state;
    
        public Car(String state) {
            this.state = state;
        }
    
        public String getState() {
            return state;
        }
    
        public void setState(String state) {
            this.state = state;
        }
    
    }
    package com.example.thread.wait.carworker;
    
    import com.example.thread.SleepUtil;
    
    /**
     * 给车上色
     */
    public class CarColor implements Runnable{
        Car car;
        public CarColor(Car c) {
            this.car = c;
        }
        
        @Override
        public void run() {
    
            synchronized (car) {
    
                while (true) {
    
                    System.out.println(Thread.currentThread().getName() + "准备上色");
    
                    if ("WASHED".equals(car.getState())) {
    
                        System.out.println("上色中。。。。。。");
                        SleepUtil.sleep(3000);
                        car.setState("COLORED");
                        car.notify();
                        System.out.println(Thread.currentThread().getName() + "完成上色");
    
                    } else {
                        try {
                            car.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
    
                    SleepUtil.sleep(1000);
                }
            }
    
        }
    }
    /**
     * 洗车
     */
    public class CarWash implements Runnable {
    
        private Car car;
    
        public CarWash(Car c) {
            this.car = c;
        }
    
        
        @Override
        public void run() {
    
            synchronized (car) {
    
                while(true) {
                    System.out.println(Thread.currentThread().getName() + "准备洗车");
    
                    if ("INIT".equals(car.getState()) || "COLORED".equals(car.getState())) {
    
                        System.out.println("洗车中。。。。。。");
                        SleepUtil.sleep(3000);
                        car.setState("WASHED");
                        car.notify();
                        System.out.println(Thread.currentThread().getName() + "完成洗车");
    
                    } else {
                        System.out.println(Thread.currentThread().getName() + "等待洗车");
                        try {
                            car.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    SleepUtil.sleep(1000);
                }
            }
        }
    }
    import com.example.thread.SleepUtil;
    
    public class CarWorker {
    
        public static void main(String[] args) {
    
            Car car = new Car("INIT");
    
            CarWash carWash = new CarWash(car);
            CarColor carColor = new CarColor(car);
    
            Thread a = new Thread(carWash, "洗车位1");
            Thread b = new Thread(carWash, "洗车位2");
            Thread c = new Thread(carColor, "喷漆位");
    
            a.start();
            b.start();
            c.start();
    
            while(true) {
                SleepUtil.sleep(1000);
                System.out.println("---------------"+a.getName()+"->"+a.getState());
                System.out.println("---------------"+b.getName()+"->"+b.getState());
                System.out.println("---------------"+c.getName()+"->"+c.getState());
            }
    
        }
    
    }

    执行结果:

    洗车位1准备洗车
    洗车中。。。。。。
    ---------------洗车位1->TIMED_WAITING
    ---------------洗车位2->BLOCKED
    ---------------喷漆位->BLOCKED
    ---------------洗车位1->TIMED_WAITING
    ---------------洗车位2->BLOCKED
    ---------------喷漆位->BLOCKED
    洗车位1完成洗车
    ---------------洗车位1->TIMED_WAITING
    ---------------洗车位2->BLOCKED
    ---------------喷漆位->BLOCKED
    洗车位1准备洗车
    洗车位1等待洗车
    洗车位2准备洗车
    洗车位2等待洗车
    喷漆位准备上色
    上色中。。。。。。
    ---------------洗车位1->WAITING
    ---------------洗车位2->WAITING
    ---------------喷漆位->TIMED_WAITING
    ---------------洗车位1->WAITING
    ---------------洗车位2->WAITING
    ---------------喷漆位->TIMED_WAITING
    ---------------洗车位1->WAITING
    ---------------洗车位2->WAITING
    ---------------喷漆位->TIMED_WAITING
    喷漆位完成上色
    ---------------洗车位1->BLOCKED
    ---------------洗车位2->WAITING
    ---------------喷漆位->TIMED_WAITING
    喷漆位准备上色
    ---------------洗车位1->TIMED_WAITING
    ---------------洗车位2->WAITING
    ---------------喷漆位->WAITING
    洗车位1准备洗车
    洗车中。。。。。。
    ---------------洗车位1->TIMED_WAITING
    ---------------洗车位2->WAITING
    ---------------喷漆位->WAITING
    ---------------洗车位1->TIMED_WAITING
    ---------------洗车位2->WAITING
    ---------------喷漆位->WAITING
    ---------------洗车位1->TIMED_WAITING
    ---------------洗车位2->WAITING
    ---------------喷漆位->WAITING
    洗车位1完成洗车
    ---------------洗车位1->TIMED_WAITING
    ---------------洗车位2->BLOCKED
    ---------------喷漆位->WAITING
    洗车位1准备洗车
    洗车位1等待洗车
    ---------------洗车位1->WAITING
    ---------------洗车位2->TIMED_WAITING
    ---------------喷漆位->WAITING
    洗车位2准备洗车
    洗车位2等待洗车
    ---------------洗车位1->WAITING
    ---------------洗车位2->WAITING
    ---------------喷漆位->WAITING
    
    Process finished with exit code -1

    经典范式:

    等待方遵循如下原则。
    1)获取对象的锁。
    2)如果条件不满足,那么调用对象的wait()方法,被通知后仍要检查条件。
    3)条件满足则执行对应的逻辑。
    对应的伪代码如下。
    synchronized(对象) {
    while(条件不满足) {
    对象.wait();
    } 对
    应的处理逻辑
    }
    通知方遵循如下原则。
    1)获得对象的锁。
    2)改变条件。
    3)通知所有等待在对象上的线程。
    对应的伪代码如下。
    synchronized(对象) {
    改变条件
    对象.notifyAll();
    }

     三、Thread.join

      当前线程里如果存在其他线程A调用join()方法后,那么当前线程会等待线程A执行完之后,继续执行。

      场景:在很多情况下,主线程创建并启动子线程,如果子线程中要进行大量的耗时运算,主线程将可能早于子线程结束。如果主线程需要知道子线程的执行结果时,就需要等待子线程执行结束了。主线程可以sleep(xx),但这样的xx时间不好确定,因为子线程的执行时间不确定,join()方法比较合适这个场景。

      

    import com.example.thread.SleepUtil;
    
    public class JoinThread extends Thread{
    
        Thread thread;
    
        public Thread getThread() {
            return thread;
        }
    
        public void setThread(Thread thread) {
            this.thread = thread;
        }
    
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "待执行");
            SleepUtil.sleep(1000);
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "执行");
        }
    }
    public class Main {
    
        public static void main(String[] args) {
            Thread threadPre = new JoinThread();
            for (int i = 0; i < 10; i++) {
                JoinThread joinThread = new JoinThread();
                joinThread.setThread(threadPre);
    
                joinThread.start();
                threadPre = joinThread;
            }
            System.out.println("ENDING......");
        }
    
    }

    执行结果:

    Thread-1执行
    Thread-2执行
    Thread-3执行
    Thread-4执行
    Thread-5执行
    Thread-6执行
    Thread-7执行
    Thread-8执行
    Thread-9执行
    Thread-10执行

     状态变化:

     状态验证代码:

    public class JoinThread extends Thread{
    
        Thread thread;
    
        public Thread getThread() {
            return thread;
        }
    
        public void setThread(Thread thread) {
            this.thread = thread;
        }
    
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "待执行");
            try {
                if (thread != null)
                    thread.join();
                SleepUtil.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "执行");
        }
    }
    public static void main(String[] args) {
    
            JoinThread joinThread1 = new JoinThread();
            JoinThread joinThread2 = new JoinThread();
            JoinThread joinThread3 = new JoinThread();
    
            joinThread2.setThread(joinThread1);
            joinThread3.setThread(joinThread2);
            joinThread1.start();
            joinThread2.start();
            joinThread3.start();
    
            while (true) {
                System.out.println(joinThread1.getName()+"->"+joinThread1.getState());
                System.out.println(joinThread2.getName()+"->"+joinThread2.getState());
                System.out.println(joinThread3.getName()+"->"+joinThread3.getState());
                SleepUtil.sleep(500);
            }
        }

    运行结果:

    Thread-0->RUNNABLE
    Thread-1->RUNNABLE
    Thread-2->RUNNABLE
    Thread-0待执行
    Thread-1待执行
    Thread-2待执行
    Thread-0->TIMED_WAITING
    Thread-1->WAITING
    Thread-2->WAITING
    Thread-0->TIMED_WAITING
    Thread-1->WAITING
    Thread-2->WAITING
    Thread-0->TIMED_WAITING
    Thread-1->WAITING
    Thread-2->WAITING
    Thread-0执行
    Thread-0->TERMINATED
    Thread-1->TIMED_WAITING
    Thread-2->WAITING
    Thread-0->TERMINATED
    Thread-1->TIMED_WAITING
    Thread-2->WAITING
    Thread-0->TERMINATED
    Thread-1->TIMED_WAITING
    Thread-2->WAITING
    Thread-0->TERMINATED
    Thread-1->TIMED_WAITING
    Thread-2->WAITING
    Thread-1执行
    Thread-0->TERMINATED
    Thread-1->TERMINATED
    Thread-2->TIMED_WAITING

     四、ThreadLocation

      线程私有局部变量的存储器,确保每个线程处理自己变量的副本,互不干扰。

    代码演示:

    public class ThreadLocationThread implements Runnable {
    
        ThreadLocal<Long> longThreadLocal = new ThreadLocal<Long>();
    
        Long aaa = 0L;
    
        @Override
        public void run() {
            if (longThreadLocal.get() == null) {
                longThreadLocal.set(0L);
            }
            longThreadLocal.set(longThreadLocal.get()+1);
            aaa++;
            System.out.println("longThreadLocal:"+longThreadLocal.get());
            System.out.println(aaa);
    
        }
    
        public static void main(String[] args) {
            ThreadLocationThread threadLocationThread = new ThreadLocationThread();
            new Thread(threadLocationThread).start();
            new Thread(threadLocationThread).start();
            new Thread(threadLocationThread).start();
            new Thread(threadLocationThread).start();
            new Thread(threadLocationThread).start();
            new Thread(threadLocationThread).start();
    
        }
    }

    运行结果:

    longThreadLocal:1  --互不干扰
    longThreadLocal:1
    2
    2
    longThreadLocal:1
    longThreadLocal:1
    5
    5
    longThreadLocal:1
    6
    longThreadLocal:1
    6

    ThreadLocation原理

      查看源码ThreadLocal的set方法:

       public void set(T value) {
            Thread t = Thread.currentThread();
         //ThreadLocalMap为ThreadLocation内部类 ThreadLocalMap map
    = getMap(t); //获取当前线程的Map对象 if (map != null)
           //set方法为Thread当前对象的ThreadLocal实例(每个线程实例都不一样,所以map中存储的值保证每个线程只能存储自己对象的value),value为要存储的值 map.set(
    this, value); else createMap(t, value); }

     使用场景:

    • 处理较为复杂的业务时,使用ThreadLocal代替参数的显示传递。
    • ThreadLocal可以用来做数据库连接池保存Connection对象,这样就可以让线程内多次获取到的连接是同一个了(Spring中的DataSource就是使用的ThreadLocal)。
    • 管理Session会话,将Session保存在ThreadLocal中,使线程处理多次处理会话时始终是一个Session。

    五、应用实例

      等待超时模式

      用一个方法等待一段时间(一般来定一个时间段),如果方法能定的时间段之内得到果,那么将果立刻返回,反之,超返回默认结

    import com.example.thread.SleepUtil;
    
    import java.util.ArrayList;
    import java.util.Date;
    import java.util.List;
    
    public class WaitTimeoutThread implements Runnable {
    
        List<String> list = new ArrayList<>();
    
        public static void main(String[] args) {
            WaitTimeoutThread waitTimeoutThread = new WaitTimeoutThread();
    
            new Thread(waitTimeoutThread).start();
            new Thread(waitTimeoutThread).start();
            new Thread(waitTimeoutThread).start();
            new Thread(waitTimeoutThread).start();
            waitTimeoutThread.testWait();
        }
    
    
        public String get() {
    
            if (list.size() <= 0) {
                return null;
            }
    
            String result = list.get(0);
            list.remove(0);
            return result;
        }
    
        @Override
        public void run() {
    
            while(true) {
                testRead(10000);
                SleepUtil.sleep(5000);
            }
    
        }
    
        public synchronized void testRead(long timeout1) {
            System.out.println(Thread.currentThread().getName()+"->"+new Date()+"取数据开始");
            long start = System.currentTimeMillis();
    
            //等待30秒
            Long waitLong = timeout1;
            long remaining = 1000L;
            boolean timeout = false;
            long end = start + waitLong;
    
            String result = get();
    
            while(result == null && !timeout) {
                try {
                    //休息1秒钟
                    wait(remaining);
                    result = get();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                timeout = System.currentTimeMillis() > end;
                if (timeout) {
                    result = "超时了";
                }
            }
            System.out.println(Thread.currentThread().getName()+"->"+new Date()+result);
        }
    
        public String testWait() {
            while (true) {
                SleepUtil.sleep(3000);
                this.list.add(System.currentTimeMillis() + "");
                System.out.println("------------------------------>"+new Date()+"生成一个数据");
            }
        }
    }

    结果:

    Thread-0->Thu May 13 14:22:05 CST 2021取数据开始
    Thread-3->Thu May 13 14:22:05 CST 2021取数据开始
    Thread-2->Thu May 13 14:22:05 CST 2021取数据开始
    Thread-1->Thu May 13 14:22:05 CST 2021取数据开始
    ------------------------------>Thu May 13 14:22:08 CST 2021生成一个数据
    Thread-3->Thu May 13 14:22:08 CST 20211620886928046
    ------------------------------>Thu May 13 14:22:11 CST 2021生成一个数据
    Thread-0->Thu May 13 14:22:11 CST 20211620886931063
    Thread-3->Thu May 13 14:22:13 CST 2021取数据开始
    ------------------------------>Thu May 13 14:22:14 CST 2021生成一个数据
    Thread-3->Thu May 13 14:22:14 CST 20211620886934063
    Thread-1->Thu May 13 14:22:15 CST 2021超时了
    Thread-2->Thu May 13 14:22:15 CST 2021超时了
    Thread-0->Thu May 13 14:22:16 CST 2021取数据开始
    ------------------------------>Thu May 13 14:22:17 CST 2021生成一个数据
    Thread-0->Thu May 13 14:22:17 CST 20211620886937064
    Thread-3->Thu May 13 14:22:19 CST 2021取数据开始
    ------------------------------>Thu May 13 14:22:20 CST 2021生成一个数据
    Thread-3->Thu May 13 14:22:20 CST 20211620886940066
    Thread-1->Thu May 13 14:22:20 CST 2021取数据开始
    Thread-2->Thu May 13 14:22:20 CST 2021取数据开始

     六、线程池技术及示例

      频繁的创建线程,销毁线程是比较耗费系统资源。

      线程池技很好地解决问题,它建了若干数量的线程,并且不能由用直接对线程的行控制,在个前提下重复使用固定或较为固定数目的线程来完成任行。

      这样做的好是,一方面,消除了建和消亡线程的系统资源开,另一方面,面对过量任的提交能的劣化

      线程池例子代码:

    import com.example.thread.SleepUtil;
    
    import java.util.*;
    import java.util.concurrent.atomic.AtomicLong;
    
    public class DefaultThreadPool<Job extends JobService> implements ThreadPool<Job> {
    
        //线程池最大线程数
        private static final int MAX_WORKER_NUMBERS = 10;
    
        //线程池默认线程数
        private static final  int DEFAULT_WORKER_NUMBERS = 5;
    
        //线程池最小线程数
        private static final int MIN_WORKER_NUMBERS  = 1;
    
        // 线程编号生成
        private AtomicLong threadNum = new AtomicLong();
    
        //工作线程队列
        private final LinkedList<Job> jobs = new LinkedList<>();
        //工作者线程队列
        private final List<Worker> workers = Collections.synchronizedList(new ArrayList<>());
    
        public DefaultThreadPool(int num){
            initWorkers(num);
        }
    
        @Override
        public void execute(Job job) {
            synchronized (jobs) {
                jobs.add(job);
                jobs.notify();
            }
        }
    
        @Override
        public void shutdown() {
            for(Worker worker: workers) {
                worker.shutdown();
            }
        }
    
        @Override
        public void addWorkers(int num) {
            synchronized (jobs) {
                    initWorkers(num);
            }
    
        }
    
        @Override
        public void removeWorkers(int num) {
            synchronized (jobs) {
                for (Worker worker : workers) {
                    worker.shutdown();
                    workers.remove(worker);
                }
            }
        }
    
        @Override
        public int getJobSize() {
            return jobs.size();
        }
    
        /**
         * 初始化线程池,可用线程
         * @param num
         */
        public void initWorkers(int num) {
            for (int i = 0;i < num; i++) {
                Worker worker = new Worker();
                Thread thread = new Thread(worker, "Thread-Worker-"+threadNum.incrementAndGet());
                this.workers.add(worker);
                thread.start();
            }
        }
    
        /**
         * 负责消费任务
         */
        class Worker implements Runnable {
            //是否工作(线程同步)
            private volatile boolean running = true;
    
            @Override
            public void run() {
                while(running) {
                    Job job = null;
                    synchronized (jobs) {
                        if (jobs.isEmpty()) {
                            //任务列表为空
                            try {
                                jobs.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                        job = jobs.removeFirst();
                    }
                    if (job != null) {
                        //???????? job.run();
                        System.out.println(Thread.currentThread().getName()+"开始执行");
                        job.exe();
                    }
                }
            }
            public void shutdown() {
                running = false;
            }
        }
    
        public static void main(String[] args) {
            ThreadPool pool = new DefaultThreadPool(5);
            for(int i = 0;i < 110;i++) {
                SleepUtil.sleep(3000);
                String tmp = i+"";
                pool.execute(() -> {
                    System.out.println("hello"+tmp);
                });
            }
        }
    }
    public interface JobService {
        public void exe();
    }
    public interface ThreadPool<Job extends JobService>  {
    
        //执行一个job
        void execute(Job job);
    
        //关闭线程池
        void shutdown();
    
        //增加线程
        void addWorkers(int num);
    
        //较少线程
        void removeWorkers(int num);
    
        //获得正在等待的执行的线程数量
        int getJobSize();
    
    }

    执行效果:

    Connected to the target VM, address: '127.0.0.1:14108', transport: 'socket'
    Thread-Worker-1开始执行
    hello0
    Thread-Worker-2开始执行
    hello1
    Thread-Worker-3开始执行
    hello2
    Thread-Worker-4开始执行
    hello3
    Thread-Worker-5开始执行
    hello4
    Thread-Worker-1开始执行
    hello5
    Thread-Worker-2开始执行
    hello6
    Thread-Worker-3开始执行
    hello7
    Thread-Worker-4开始执行
    hello8
    Thread-Worker-5开始执行
    hello9

     

  • 相关阅读:
    数据结构 字符串的长度
    滚动条
    git push 一直卡在 writing objects 然后 就提交失败 提示:unexpected-disconnect-while-reading-sideband-packet
    vue中的防抖和节流
    html5中output元素详解
    手写 apply call bind 三个方法
    js中的陷阱!!!
    display:inline-block元素之间空隙的产生原因和解决办法
    git push到Gitee的时候上传不成功,可能是本地文件夹与远程仓库不同步
    axios没有实现jsonp这个功能,基于axios自己扩展一个
  • 原文地址:https://www.cnblogs.com/guanhao0114/p/14756106.html
Copyright © 2011-2022 走看看