目录

  • 前情引入
    • 简单介绍
    • 预备知识
  • 代码及详解
    • 简单代码
    • 基本解释
      • 生产者线程类
      • 消费者线程类
      • 测试类
      • 执行流程
      • 控制台输出
    • 自我提高
      • 问题一
      • 问题二
    • 升级代码
  • 总结

前情引入

做一些简单的认识和告知一些前置知识

简单介绍

生产者和消费者是一种特殊的业务需求的抽象,这种业务就是:需求和供给达到平衡关系,生产一个,就消费一个,或者是生产一部分,就消费一部分。

利用多线程,可以对这种业务需求进行简单的模拟和实现,主要是利用Object中的wait方法和notify方法。

注意,不能同时生产和消费,因为在多线程下,对共享的数据进行了修改,必须使用同步机制,不然会出现数据安全问题。

预备知识

首先对java中的多线程,有一定的认识。

再者呢,就是Object中的wait方法和notify方法的作用。

  • void wait():在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,导致当前线程等待。 简单来说,就是让当前线程进入阻塞,直到被唤醒,并且会释放调用当前线程占用的对象锁
  • void notify() :唤醒在此对象监视器上等待的单个线程。 如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。选择是任意性的,并在对实现做出决定时发生。线程通过调用其中一个 wait 方法,在对象的监视器上等待。简单来说,就是唤醒被wait方法进入阻塞的线程。
  • 注意:这两个方法都只能在同步代码块(包括同步方法)中使用
  • 顺带提一点,sleep虽然也能使当前线程进入阻塞状态,但是是不会释放锁和资源的

代码及详解

先上代码,再基本详解,最后再提高。

简单代码

import java.util.ArrayList;//使用wait和notify实现生产者和消费者模式
public class PCMode1
{public static void main(String[] args){ArrayList<Object> arrayList = new ArrayList<>();new Thread(new Producer1(arrayList), "Producer").start();new Thread(new Consumer1(arrayList), "Consumer").start();}
}@SuppressWarnings("all")
class Producer1 implements Runnable
{private final ArrayList arrayList;public Producer1(ArrayList arrayList){this.arrayList = arrayList;}@Overridepublic void run(){while (true){synchronized (arrayList){System.out.println(Thread.currentThread().getName()+"抢到了对象锁");if (arrayList.size() > 0){System.out.println("已有食物,请消费者消费!");System.out.println();try{ arrayList.wait(); } catch (InterruptedException e){ e.printStackTrace(); }} else{arrayList.add(0,"a");try{ Thread.sleep(100); } catch (InterruptedException e){ e.printStackTrace(); }System.out.println("已生产食物,请消费者消费!");arrayList.notify();}}}}
}@SuppressWarnings("all")
class Consumer1 implements Runnable
{private final ArrayList arrayList;public Consumer1(ArrayList arrayList){ this.arrayList = arrayList; }@Overridepublic void run(){while (true){synchronized (arrayList){System.out.println(Thread.currentThread().getName()+"抢到了对象锁");if (arrayList.size()>0){arrayList.remove(0);try{ Thread.sleep(100); } catch (InterruptedException e){ e.printStackTrace(); }System.out.println("消费者消费了一个食物,请生产者生产!");arrayList.notify();} else{System.out.println("没有食物,请生产者生产!!");System.out.println();try{ arrayList.wait(); }catch (InterruptedException e){ e.printStackTrace(); }}}}}
}

@SuppressWarnings(“all”) 这个注解是为了去掉代码中那些难看的提示,可以直接忽略。

能看懂的话,那就是大佬咯,大佬可以看看后面的提高。看不懂也没关系,我们来一步一步的分析。

基本解释

我把三个类写在一个java文件中的,一个是测试类,另外两个,分别是生产者线程类和消费者线程类。测试类就是公共类,其中有main方法,用来测试用的,没啥好说的,主要是两个线程类。

先说一下大概的思路,两个线程类里都一个ArrayList类型的变量,这两个线程都是共享的同一个变量,这就是那个共享的数据。

简单起见,我模拟的是生产一个,消费一个的情况。我将这个ArrayList作为一个容器,生产者每生产一个食物,就放进这个容器,然后等消费者来消费,消费一个之后,生产者又进行生产,如此往复……

生产者线程类

Producer1是生产者线程,之前说了,有一个ArrayList类型的成员变量,构造方法是为了给这个变量赋值。

重写线程任务的run方法,先来一个while(true)死循环,意思就是,生产者线程一直生产。然后是synchronized同步代码块,因为是对共享的数据进行修改操作,所以要使用同步机制,来保证数据的安全,synchronized的锁对象就是共享的对象:arrayList。synchronized的对象选取原则就是:想要那些线程排队执行,就选择一个这些线程共享的对象

进入synchronized代码块中做的第一件事情,我先打印了一句话,方便后面观察控制台的输出情况,然后是正事。

我们要进行生产,首先第一件要做的事情是什么?当然是判断arrayList这个容器里是不是已经有食物了。如果已经有食物了,咱就不能生产,得让消费者来消费是吧。如果没有,咱们才能生产食物,并且添加到容器。

34行到42行,就是容器中存在食物的逻辑,先打印提示语句,然后用arrayList这个对象调用wait方法。还记得这个方法的作用吗?会让当前线程进入阻塞,并且会释放占用的对象锁。释放了对象锁,那么消费者线程就可能拿到对象锁,然后进行消费。当然,我这里只说了一个大概,具体细节后面再分析。

43行到53行,就是容器中不存在食物,我们生产食物并添加进容器的逻辑。就是往arrayList中添加一个元素,然后模拟一下生产的消耗时间(主要是为控制台输出可控,不然控制台飞一般的跑),再打印提示信息,最后再调用arrayList的notify方法。还记得这个方法的作用吗?唤醒在此对象监视器上等待的单个线程。那此时谁在arrayList对象上等待呢?我们当前生产者线程在执行,那么肯定就是消费者线程进入了阻塞状态撒。而调用这个方法,就可以唤醒消费者线程,进行消费。

消费者线程类

消费者线程类的处理逻辑和生产者线程非常类似,只是处理的业务不同,一个是进行生产的,一个是进行消费的,我们来简单的过一遍。

一样的代码就不说了,一样的意思。
76行到85行,是容器中存在食物,进行消费的逻辑,先将容器中这个食物移除,然后在模拟一下耗时,最后调用arrayList的notify方法,因为我们已经消费了容器里的食物,现在要通知在等待中的生产者生产了。

87行到94行,是容器中不存食物的处理逻辑,很简单,打印控制信息,然后调用arrayList对象的wait方法,让当线程进入等待状态,等待生产者生产。

测试类

测试类里,就是创建了两个线程对象,然后将生产者线程和消费线程传了进去,然后启动。其实就算这样挨着挨着分析了代码,可能能还是会不清楚,我觉得最好的办法是:自己来跟着代码走一遍流程,我自己屡试不爽,我们一起来走走吧

执行流程
  1. 程序的执行从main函数开始,我先new了一个生产者线程并且启动,所以是生产者先抢到了arrayList的对象锁,然后开始生产食物……不对,虽然生产者线程先启动,但是如果在生产者线程还没有进入同步代码块,也就是还没有拿到arrayList的对象锁时,有没有可能消费者线程就启动了,并且拿到了CPU的执行权,先拿到了arrayList的对象锁呢?其实是完全有可能的,因为在没有进入同步代码块的时候,两个线程是共同抢夺CPU的执行权的,先启动的,不一定就能占到便宜。

  2. 那怎么办呢?我生产者还没生产呢,消费者就来消费了。别急,假设消费者先拿到了arrayList的对象锁,我们就随着消费者线程的逻辑往下走,进入了消费者线程,经过条件判断,直接就进入了else分支里,因为此时容器里是没有食物的,然后它干了一件什么事情?原地wait,直接就原地阻塞,而且还将拿到的对象锁给释放了。那生产者呢?开始arrayList的对象锁被消费者给拿走了,它肯定就一直在自己线程里的synchronized代码块外面等待,拿不到对象锁,它只能在synchronized代码块外面等待。它肯定在想,是哪个个天杀的,抢了我的对象锁,害我一直在这里等。消费者一旦释放了锁,而且此时消费者本身进入了阻塞状态,不会和生产者去抢arrayList的对象锁,所以一定是生产者拿到对象锁,然后进入synchronized代码块生产食物。

  3. 生产者线程拿到对象锁之后,就美滋滋的去生产食物去了,那是它存在的唯一使命。它还是很严谨的,先判断容器中是否已经存在食物,呀,没有,就进入了else分支了,先生产了一个食物,将其存入了容器中,然后再用notify方法唤醒了正在arrayList对象上等待的线程,然后自己再退出了同步代码块,释放了对象锁。(退出synchronized代码块,也是会释放锁的!

  4. 消费者被唤醒了,而且生产者释放了arrayList的对象锁,消费者终于可拿到对象锁,并且大吃特吃了。但是作为程序出生的吃货,虽然爱吃,但是也是严谨的。它也是先判断容器中是否有食物,没有的话,那还瞎费什么劲呢,赶紧睡一觉(wait方法),让生产者那家伙生产。但是这次它运气不错,容器里是有食物的,然后它饱餐了一顿,然后再用同样的方法(notify)唤醒正在arrayList对象上等待的线程,自己退出synchronized代码块的时候,将锁给释放了。

  5. 生产者又拿到对象锁了,……如此往复

当然,我这里只说了大致的流程,有疑惑的小伙伴可以多分析几遍。还有一些细节,我们在提高中分析。

控制台输出

为了看到效果,在运行的时候,我特意将消费者线程的创建放在前面的,让它先抢到对象锁的概率大一些。

自我提高

问题一
  1. 生产者/消费者释放了锁之后,可以再次拿到锁吗?再次拿到,会有什么影响吗?再次拿到的概率如何?为什么?

我的意思就是,在上面执行流程中第三步中,生产者最后将对象锁释放了,而且自己退出了synchronized代码块。但是别忘了,虽然生产者线程退出了同步代码块,但它还是一个正常执行的线程,并没有进入阻塞状态,它还是会和消费者抢锁的。

那会不会有问题?生产了之后生产者又抢到了锁。其实不会,就和我们在在执行流程第二步中的分析类似,即便生产者再次抢到了锁,但是此时容器中已经有食物了,它抢到了也会调用wait方法进入阻塞状态。生产者它此时的内心活动一定是:我靠,消费者那家伙还没吃?动作真慢,那我再睡会吧。然后消费者就能拿到对象锁,进行消费了。消费者再次抢到锁,情况也是类似,消费者前一次已经将食物消费了,再次抢到锁,发容器中已经没有食物了,就会调用arrayList对象wait方法,进入阻塞,释放锁。

其实上面程序的输出结果,也佐证了这一点。生产者总是在生产之后再次在控制台打印“已有食物,请消费者消费!”,消费者总是在消费之后,再次在控制台打印“没有食物,请生产者生产!!”,这其实就是因为再次抢到了锁,被wait方法进入阻塞之前打印的信息。

但是如果仔细分析,就会发现有问题。为什么每次都会再次抢到锁?每次都是,生产者先生产了一个食物,然后它再次抢到锁,再打印已有食物的提示信息。或者是,消费者先消费了一个食物,然后再次抢到了锁,并且打印没有食物的提示信息。这是为什么呢?按道理来说,他们俩都是有机会抢到锁的,为什么总是一个抢到两次,直到它自己被wait方法阻塞了,另一个线程才有执行的机会?如果你多刷几遍,可能会发现,偶尔一次,另一个还是后可能抢到锁的,只不过几率很小很小,以至于我一开始都怀疑我的代码有问题。

特意找到了这种情况,截图如下:(代码是上面的代码,没有动过哦)

要解释这个问题,就需要仔细分析wait和notify方法的执行时机了,也就是第二个问题,往下面看。

问题二
  1. 生产者/消费者被唤醒了之后,是马上就执行的吗?

我们思考一个场景:假设容器里现在是有食物的,生产者在31行阻塞,消费者拿到锁在执行。当消费者消费了食物之后,它就调用了arrayList对象的notify方法,之前被wait方法阻塞的生产者线程,此时就被唤醒了。那么问题就来了:消费者线程在执行,生产者线程也被唤醒了,就有两个线程同时在使用了共享对象的synchronized代码块里面

那么,synchronized还有用吗?还能保证共享数据的安全吗?java的设计者肯定不会允许这样的事情发生。我做了简单的测试,得出了结论:即便唤醒了arrayList对象上等待的线程,但是被唤醒的线程并不会第一时间执行,而是等待当前线程执行完毕,被唤醒的那个线程才可以继续执行。应该是,即便等待的线程被唤醒了,但是锁时被当前的线程占用着的,被唤醒的那个线程拿不到锁,所以无法执行。

也就是说,即便生产者将消费者唤醒了,但是由于arrayList的对象锁是在生产者手上的,所以消费者不能第一时间执行,必须等生产者自动退出了synchronized代码块,将锁给释放了,被唤醒的那个线程才能继续从上次等待的那个位置继续执行。所以,为什么两个线程连续抢到的概率为什么那么大的疑问,也能解释了。

因为当前线程将另外一个线程唤醒了之后,当前线程会继续执行,直到退出了synchronized代码块,退出了synchronized代码块,那么被唤醒的那个线程就会接着上次等待的地方继续执行,但是别人在执行的时候,当前这个线程也没闲着呀,他自己也会自己继续执行,直到再次遇到了synchronized关键字,此时arrayList的对象锁在被唤醒的那个线程手上,当前线程就只能卡在synchronized关键字这里。但是被唤醒那个线程马上就会退出synchronized代码块,一旦退出,由于当前线程之前就已经卡synchronized关键字这里了,所以当前线程马上就能获取到arrayList的对象锁,直到当前线程被wait方法给阻塞了,才没有能力去抢那个锁了。

那为什么有会出现,不是一个线程连续两次抢到锁,别的线程也能在中间抢到锁呢?这是因为java中的线程调度用的是抢占式,这个东西就和玄学一样,谁能抢到,不能确定。所以可能被唤醒的那个线程抢夺能力很强,从他被唤醒并且当前线程释放了对象锁之后,他一直在占用CPU的执行权,没有给当前线程留时间,被唤醒的那个线程就抢到了锁,但是这种几率很小,除非是刻意的去制造,增大概率。

升级代码

import java.util.Random;@SuppressWarnings("all")
public class PCMode
{public static void main(String[] args){Bun[] buns = new Bun[4];new Thread(new Consumer(buns),"consumer").start();new Thread(new Producer(buns),"producer").start();}
}@SuppressWarnings("all")
class Producer implements Runnable
{private Bun [] buns;private Random random = new Random();private String[] skins = {"冰皮儿","薄皮儿","厚皮儿"};private String[] stuffings = {"牛肉馅儿","大葱馅儿","韭菜馅儿","酱肉馅儿"};public Producer() { }public Producer(Bun[] buns){ this.buns = buns; }// 生产者生产包子@Overridepublic void run(){while (true)//死循环,一直生产{synchronized (buns)//涉及到了共享的成员变量,而且要对其修改,所以要用同步机制{System.out.println(Thread.currentThread().getName()+"抢到了对象锁");boolean isFull = true;//用来标记包子库是否已满for (int i = 0; i < buns.length;i++ )//尝试生产包子并将包子存入包子库{//如果有空位就可以生产包子并且存入if (buns[i] == null){isFull = false;//生产一个包子,随机馅儿和皮Bun bun = new Bun(skins[random.nextInt(skins.length)],stuffings[random.nextInt(stuffings.length)]);buns[i] = bun;//将该空位置上加上包子//模拟产生包子的耗时int time = random.nextInt(5);try{Thread.sleep(time*100);System.out.println("生产者生产一个“"+bun.toString()+"”,耗时:"+time+"百毫秒");//每生产一个包子,都唤醒包子库对象上所有的等待线程。分析和下面消费者的类似buns.notify();} catch (InterruptedException e){ e.printStackTrace(); }}}/*这里的分析和消费者那里类似,如果包子库已经满了,就自己进入等待状态,并且释放包子库对象上的锁。让消费者来执行*/if (isFull){try{System.out.println("包子铺满了,吃货快来吃!");System.out.println();buns.wait();//自己进入等待状态}catch (InterruptedException e){ e.printStackTrace(); }}}}}
}//生产者线程
@SuppressWarnings("all")
class Consumer implements Runnable
{private Bun [] buns; //包子库private Random random = new Random();public Consumer() { }public Consumer(Bun[] buns){ this.buns = buns; }//尝试消费包子@Overridepublic void run(){Bun bun = null;while (true) //死循环,一直消费{synchronized (buns)//涉及到了共享的成员变量,而且要对其修改,所以要用同步机制{System.out.println(Thread.currentThread().getName()+"抢到了对象锁");boolean isEmpty = true;//用来标记包子库是否为空for (int i = 0; i < buns.length; i++)//遍历包子库,消费包子{if (buns[i] != null)//不为null,就说明有包子,进行消费{isEmpty = false;//将标记标为falsebun = buns[i];//后面要用到这个包子对象buns[i] = null;//包子消费后,将其置为null//模拟消耗包子的耗时int time = random.nextInt(5);try{Thread.sleep(time * 100);System.out.println("消费者消费了一个“" + bun.toString() + "”,耗时:" + time + "百毫秒");}catch (InterruptedException e){ e.printStackTrace(); }/*每次消费一个包子后,都唤醒在仓库对象上等待的线程,但是由于对象锁还在当前线程,所以其他线程是不会执行的。当当前先线程跳出了synchronized代码块时,就将对象锁释放了,而生产者线程也是被唤醒了的,所以有可能也会抢到对象锁,但是抢到了也没事,因为条件判断又会使其进入等待,并且释放锁。但是不知道为什么,这种可能性很小很小,我刻意找了很久,只找到了一次*/buns.notify();}}/*如果一个包子都没有,自己进入等待状态。由于每次消费一个包子后,都唤醒了包子库对象上的所有线程,所以生产者线程早就在等待了。一旦当前线程(消费者线程)使用wait方法,释放了锁,并且自己进入了等待状态,立马就被生产者线程抢到。*/if (isEmpty){try{System.out.println("老板没包子了,快做包子!");System.out.println();buns.wait();}catch (Exception e){ e.printStackTrace(); }}}}}
}//包子类
@SuppressWarnings("all")
class Bun
{private String skin;//包子皮儿private String stuffing;//包子馅儿public Bun() { }public Bun(String skin, String stuffing){this.skin = skin;this.stuffing = stuffing;}public String getSkin(){ return skin; }public void setSkin(String skin){ this.skin = skin; }public String getStuffing(){ return stuffing; }public void setStuffing(String stuffing){ this.stuffing = stuffing; }@Overridepublic String toString(){ return skin+stuffing+"包子"; }
}

升级代码中,增加了仓库的容量,并且生产的时候,变成了随机生产的,模拟时间消耗也变成了随机性的。但其实核心的逻辑和最开始的是一样的,我这里就不分析了,大家有兴趣的可以去分析一下,我代码中写了很多注释,方便大家分析。

运行结果

在这个程序中,想去找那种特殊情况,就很难找了。

总结

模拟生产者和消费者,要记住几个要点

  1. while(true)死循环,因为生产者要一直生产,消费者要一直消费。

  2. synchronized,同步代码块,因为要对共享的数据进行修改,必须使用同步机制,而且wait和notify只能在同步代码块中使用。注意要用两个线程共享的对象,不用arrayList,用其他两个线程共享的对象也是可以的。比如用所有线程都共享的字符串,如果用字符串,那wait和notify方法也需要用那个字符串对象去调用。

  3. 还有一个很重要,但是不容易把握的要点,我详细说一下:就是wait方法和notify方法该在什么时候调用。比如说,我是生产者线程,根据容器里是否有食物,有两种状态,有或者没有。如果已经有,我该用wait方法阻塞当前线程,释放资源?还是该用notify方法唤醒在等待的线程呢?
    如果仅仅从字面上分析:如果已经有食物了,那我就自己就进入阻塞,释放锁对象。或者如果已经有食物了,我就唤醒在等待的线程(消费者线程)。两者好像都说得通。那如果没有呢,我生产一个食物存入容器之后,又该用那个方法呢?好像也都能说得通。
    针对这种情况,我发现了一个诀窍:要保证两次被同一个线程抢到了对象锁时,这个线程要进入阻塞状态,。我们再来分析上面的疑惑,如果是生产者,在容器中有没有食物的情况下,我调用了wait方法,将当前线程进入了阻塞状态。那么如果生产者两次抢到了对象锁,第二次进入的时候,容器里已经有食物了,因为第一次会生产并存入。那么第二次就不会进入“容器中没有食物”的那个选项,也就不能进入阻塞。所以我们选择在有食物的情况下,使用wait方法,进入阻塞状态。然后在另一种情况下,选择用notify方法唤醒等待的线程。消费者也可以用类似的方法判断。这种方法是可取的,但是不知道是否有其它更好的办法。

另外还有一点就是:当某个线程被另一个线程唤醒了,此时两个线程会在由同一个对象作锁的两个synchronized代码块里面,但是这两个线程并不会同时执行(同时执行的话,就可能会出现线程安全问题,那么synchronized也没有意义了),情况是:被唤醒的那个线程并不会第一时间执行,而是主动唤醒别的线程的那个线程先执行,直到释放了锁,被唤醒的那个线程才会接着上次休眠的地方继续执行。

有什么不对或不懂的地方,欢迎一起讨论

java多线程之——生产者和消费者(详解及提高)相关推荐

  1. java多线程中的join方法详解

    java多线程中的join方法详解 方法Join是干啥用的? 简单回答,同步,如何同步? 怎么实现的? 下面将逐个回答. 自从接触Java多线程,一直对Join理解不了.JDK是这样说的:join p ...

  2. Java多线程技术~生产者和消费者问题

    Java多线程技术~生产者和消费者问题 本文是上一篇文章的后续,详情点击该连接 线程通信 应用场景:生产者和消费者问题 假设仓库中只能存放一件产品,生产者将生产出来的产品放入仓库,消费者将仓库中产品取 ...

  3. Java多线程系列(六):深入详解Synchronized同步锁的底层实现

    谈到多线程就不得不谈到Synchronized,很多同学只会使用,缺不是很明白整个Synchronized的底层实现原理,这也是面试经常被问到的环节,比如: synchronized的底层实现原理 s ...

  4. Java多线程案例--生产者和消费者模型(送奶人和喝奶人的故事!)

    文章目录 一.进程和线程 1.进程 2.线程 3.进程与线程的区别 二.生产者和消费者模型 1.生产者消费者模式概述 2.奶箱类 3.生产者类 4.消费者类 三.测试 1.测试类(BoxDemo) 2 ...

  5. Kafka生产者与消费者详解

    什么是 Kafka Kafka 是由 Linkedin 公司开发的,它是一个分布式的,支持多分区.多副本,基于 Zookeeper 的分布式消息流平台,它同时也是一款开源的基于发布订阅模式的消息引擎系 ...

  6. Java多线程读写锁ReentrantReadWriteLock原理详解

    ReentrantLock属于排他锁,这些锁在同一时刻只允许一个线程进行访问,而读写锁在同一时刻可以允许多个线程访问,但是在写线程访问时,所有的读和其他写线程都被阻塞.读写锁维护了一对锁,一个读锁和一 ...

  7. java多线程之生产者和消费者问题

    线程通信:不同的线程执行不同的任务,如果这些任务有某种关系,线程之间必须能够通信,协调完成工作. 经典的生产者和消费者案例(Producer/Consumer): 分析案例: 1):生产者和消费者应该 ...

  8. Java多线程系列之“JUC集合“详解

    Java集合包 在"Java 集合系列01之 总体框架"中,介绍java集合的架构.主体内容包括Collection集合和Map类:而Collection集合又可以划分为List( ...

  9. Java 多线程断点下载文件_详解

    本文转载于:http://blog.csdn.net/ibm_hoojo/article/details/6838222 基本原理:利用URLConnection获取要下载文件的长度.头部等相关信息, ...

最新文章

  1. iis+php解析漏洞修复,IIS7.0畸形解析漏洞通杀0day
  2. 网络营销外包——网络营销外包公司如何做好电子商务网站优化?
  3. Python之pandas-profiling:pandas-profiling库的简介、安装、使用方法之详细攻略
  4. 【c/c++】刷算法题时常用的函数手册 持续更新--
  5. 最牛啤的java,没有之一~
  6. SAP 电商云 Spartacus UI 页面布局的设计原理
  7. 局域网内抢网速_路由器要不要每天重启?多亏宽带师傅透露,难怪网速一天比一天慢...
  8. Dev C++安装第三方库boost
  9. javascript 基础之事件(event)-------1
  10. 转:lnmp 搭建手册-黑一路人
  11. 微软Exchange Server 2010 SP1下载
  12. 记一次绕过安全狗与360艰难提权
  13. 持久层框架:Mybatis快速入门
  14. Autodesk AutoCAD 2018 for mac
  15. 12 个动画设计方法,帮助你快速实现炫酷的网页动画效果
  16. js 实现历史搜索记录功能
  17. Hrbust 2294 修建传送门【思维】
  18. 数据恢复原理与数据清除原理
  19. 电脑总是区域性白屏,求助各位大佬。
  20. unity 打砖块—休闲小游戏,摸鱼必备(完整代码)

热门文章

  1. 账外“公对私”结算薪资有风险,灵活用工助力企业税务合规
  2. 全网最详细chatgpt提示词,纯手工整理(二)
  3. 设计模式从入门到放弃
  4. windows server 2012/2016 设置多用户远程桌面
  5. 银行科技 | 招行CIO陈昆德:客户和科技是招行未来的两大核心主题
  6. 全面解析Discord安全问题
  7. HTTP 错误 403.14 - Forbidden Web 服务器被配置为不列出此目录的内容——错误代码:0x00000000
  8. 睡眠---全面少眠的时代,你睡得好吗?
  9. 天猫双11移动端交易额创全球移动电子商务新纪录
  10. 红米note5手机插u盘没反应_加上扩展坞iPad Pro就能插上翅膀变成生产工具了吗?...