• JAVA多线程笔记


    创建

    1. 接口创建:

      Thread A = Thread(Runnable threadOb,String threadName); 
      

      threadOb是实现了Runnable的类实例,称为目标对象

    ​ 使用同一目标对象创建的线程,目标对象的成员变量自然就是这些线程共享的数据单元。

    //这里的票是共享数据,一个线程改变后另一个线程也会更新
    public class Ticket implements Runnable{
        private int ticket = 10;
        ...
    }
    public class SellTicket {
        public static void main(String[] args) {
            Ticket t = new Ticket();
            new Thread(t).start();
            new Thread(t).start();
        }
    }
    
    1. 继承创建:

      ThreadClass A = new ThreadClass(String threadName);
      

      类名为ThreadClass的类继承Thread

    2. Callable,Furture

      以后用到时再补充,Callable接口代表一段可以调用并返回结果的代码,Future接口表示异步任务,是还没有完成的任务给出的未来结果。Callable用于产生结果,Future用于获取结果。future模式:并发模式的一种,可以有两种形式,即无阻塞和阻塞,分别是isDone和get。其中Future对象用来存放该线程的返回值以及状态。Future对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果,必要时可以通过get方法获取执行结果,该方法会阻塞直到任务返回结果。

    常用方法

    1. //为当前线程命名
      a.setname(" ");  
      
      //在目标对象的run方法中调用获取当前线程的名字,确定是哪个线程在占用cpu
      Thread.currentThread().getName(); 
      
      //线程新建和死亡,调用a.start到死亡前线程均为Alive(), 做判断语句
      a.isAlive()==true 
      
    2. Thread.yield( )

      ​ 使当前线程从执行状态(运行状态)变为可执行态(就绪状态)。cpu会从众多的可执行态里选择,也就是说,当前也就是刚刚的那个线程还是有可能会被再次执行到的。yield不会保证把执行权移交,他只会显式地让cpu重新调度一次。

    3. 目标对象与线程关系

      • 完全解耦:只能在目标对象的run方法种调用getname来获取线程信息

      • 弱耦合:线程作为目标对象的成员

        //主线程中:
        House house = new House();
        house.dog.start();
        //目标对象house中:
        Thread dog;
        House(){  
            dog = new Thread(this); 
            dog.setname("xxx");
        }
        public void run(){
            Thread t = Thread.currentThread();
            if(t == dog){
                ...
            }
        }
        

    Tips

    • Thread t; t.toString方法是在主线程中执行的

    • dog.start();  //先执行dog的run方法,先执行start先开始调度
      cat.start();  //大约2毫秒后可能执行cat的run方法,取决cpu的调度
      //在目标对象的run方法中执行下列语句时,随时可能发生中断,因此需要解决线程安全问题
      if(t == dog){	
          System...;   //可能发生中断
      	water ++ ;   //可能发生中断
      }
      
    • Thread thread = new Thread(target);
      thread.start();
      //在线程thread的run方法中再新建名为thread的线程对象
      public void run(){
          thread = new Thread(target); //此时原来的线程实体并不会被回收掉,垃圾实体和当前实体都在工作
      	...
      }
      
    • 如果同一个方法内同时有两个或更多线程,则每个线程有自己的局部变量拷贝

    synchronized

    1. java的内置锁:多线程的锁,其实本质上就是给一块内存空间的访问添加访问权限,因为Java中是没有办法直接对某一块内存进行操作,又因为Java是面向对象的语言,一切皆对象,所以具体的表现就是某一个对象承担锁的功能,每一个对象都可以是一个锁。内置锁,使用方式就是使用 synchronized 关键字,synchronized 方法或者 synchronized 代码块。线程进入同步代码块或方法的时候会自动获得该锁,在退出同步代码块或方法时会释放该锁。获得内置锁的唯一途径就是进入这个锁的保护的同步代码块或方法。 java内置锁是一个互斥锁,这就是意味着最多只有一个线程能够获得该锁,当线程A尝试去获得线程B持有的内置锁时,线程A必须等待或者阻塞,直到线程B释放这个锁,如果B线程不释放这个锁,那么A线程将永远等待下去。对象的内置锁和对象的状态之间是没有内在的关联的,虽然大多数类都将内置锁用做一种有效的加锁机制,但对象的域并不一定通过内置锁来保护。当获取到与对象关联的内置锁时,并不能阻止其他线程访问该对象,当某个线程获得对象的锁之后,只能阻止其他线程获得同一个锁。之所以每个对象都有一个内置锁,是为了免去显式地创建锁对象。所以synchronized只是一个内置锁的加锁机制,当某个方法加上synchronized关键字后,就表明要获得该内置锁才能执行,并不能阻止其他线程访问不需要获得该内置锁的方法

      • 对象锁(方法锁)
        • 对象锁是用于对象实例方法或一个对象实例
        • 类的对象实例可以有很多个,一个类不同对象实例的对象锁互不干扰
        • 一个类的对象锁和另一个类的对象锁是没有关联的
      //当两个并发线程访问同一个对象(t1)中的synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。即A先执行完语句块1,B再执行
      //TIPS: 并非一定A先执行完语句块1,B再执行,也有可能B先执行完语句块2,A再执行。这是因为java编译器在编译成字节码的时候,会对代码进行一个重排序,也就是说,编译器会根据实际情况对代码进行一个合理的排序,编译前代码写在前面,在编译后的字节码不一定排在前面,所以这种运行结果是正常的)下面的例子类同
      
      public class Thread1 implements Runnable {  
          public void run() {  
              synchronized (this) {  
                  //语句块1
              }  
          }  
        
          public static void main(String[] args) {  
              Thread1 t1 = new Thread1();  
              Thread ta = new Thread(t1, "A");  
              Thread tb = new Thread(t1, "B");  
              ta.start();  
              tb.start();  
          }  
      }  
      
      //当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块,即结果是两线程交替执行语句块1和语句块2。注意这里ta和tb的目标对象并非同一个,只是在run方法中调用了同一个对象myt2
      
      public class Thread2 {  
          public void m4t1() {  
              synchronized (this) {  
                  //语句块1
              }  
          }  
        
          public void m4t2() {  
              //语句块2
          }  
        
          public static void main(String[] args) {  
              final Thread2 myt2 = new Thread2();  
              Thread t1 = new Thread(new Runnable() {  //匿名类
                  public void run() {  
                      myt2.m4t1();  
                  }  
              }, "t1");  
              Thread t2 = new Thread(new Runnable() {  
                  public void run() {  
                      myt2.m4t2();  
                  }  
              }, "t2");  
              t1.start();  
              t2.start();  
          }  
      } 
      
      //当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对该object中所有其它synchronized(this)同步代码块或同步方法的访问将被阻塞(如注释)。即结果为t1先执行完语句块1,t2再执行语句块2
      
      public class Thread2 {  
          public void m4t1() {  
              synchronized (this) {  
                  //语句块1
              }  
          }  
        
          public void m4t2() {  
              synchronized (this) {  
                  //语句块2
              } 
          }  
          /*
          public synchronized void m4t2() {  
          	//语句块2
          }*/ 
          
          public static void main(String[] args) {  
              final Thread2 myt2 = new Thread2();  
              Thread t1 = new Thread(new Runnable() {  //匿名类
                  public void run() {  
                      myt2.m4t1();  
                  }  
              }, "t1");  
              Thread t2 = new Thread(new Runnable() {  
                  public void run() {  
                      myt2.m4t2();  
                  }  
              }, "t2");  
              t1.start();  
              t2.start();  
          }  
      } 
      

      以上规则对其它对象锁同样适用

      //关于synchronized(this)的位置:模式1等于模式2,模式3可减小锁粒度
      
      public synchronized void work(){
          ...
      }
      public void work(){
          synchronized(this){
          ...
          }
      }
      public void work(){
          ... //大量不涉及线程安全的计算或者IO工作
          synchronized(this){
          ...
          }
          ...
      }   
      
      • 类锁
        • 类锁是用于类的静态方法或者一个类的class对象上的。类锁只是一个概念上的东西,并不是真实存在的,它只是用来帮助我们理解锁定实例方法和静态方法的区别
        • 每个类只有一个class对象
    //类锁修饰方法和代码块的效果和对象锁是一样的,因为类锁只是一个抽象出来的概念,只是为了区别静态方法的特点,因为静态方法是所有对象实例共用的,所以对应着synchronized修饰的静态方法的锁也是唯一的,所以抽象出来个类锁。因此t1执行完语句块1后,t2才能执行
    
    public class Thread2 {  
        //类锁的修饰静态方法
    	private static synchronized void method1(){
    		//语句块1
    	}
    	//类锁修饰代码块
    	private void method2(){  
        	synchronized(Thread2.class){
            	//语句块2
        	}   
    	} 
        public static void main(String[] args) {  
            final Thread2 myt2 = new Thread2();  
            Thread t1 = new Thread(new Runnable() {  //匿名类
                public void run() {  
                    myt2.method1();  
                }  
            }, "t1");  
            Thread t2 = new Thread(new Runnable() {  
                public void run() {  
                    myt2.method2();  
                }  
            }, "t2");  
            t1.start();  
            t2.start();  
        }  
    } 
    
    //synchronized同时修饰静态方法和实例方法,但是运行结果是交替进行的,这证明了类锁和对象锁是两个不一样的锁,控制着不同的区域,它们是互不干扰的。同样,线程获得对象锁的同时,也可以获得该类锁,即同时获得两个锁,这是允许的。
    public class TestSynchronized   
    {    
        public synchronized void test1()   
        {    
    		//语句块1 
        }    
          
        public static synchronized void test2()   
        {    
             //语句块2   
        }    
          
        public static void main(String[] args)   
        {    
             final TestSynchronized myt2 = new TestSynchronized();    
             Thread t1 = new Thread(new Runnable() {  
                public void run() {  
                    myt2.method1();  
                }  
            }, "t1");  
            Thread t2 = new Thread(new Runnable() {  
                public void run() {  
                    myt2.method2();  
                }  
            }, "t2");  
            test1.start();    
            test2.start();    
        }   
    }  
    

    3.可重入机制:其中对于对象锁,当一个对象拥有锁之后,访问一个加了对象锁的方法,而该方法中又调用了该类中其他加了对象锁的方法,那么这个时候是不会阻塞住的。这是java通过可重入锁机制实现的。可重入锁指的是当一个对象拥有对象锁之后,可以重复获取该锁。因为synchronized块是可重入的,所以当你访问一个对象锁的方法的时候,在该方法中继续访问其他对象锁方法是不会被阻塞的

    4. synchronized的缺陷:当某个线程进入同步方法获得对象锁,那么其他线程访问这里对象的同步方法时,必须等待或者阻塞,这对高并发的系统是致命的,这很容易导致系统的崩溃。如果某个线程在同步方法里面发生了死循环,那么它就永远不会释放这个对象锁,那么其他线程就要永远的等待,这是一个致命的问题。

    sleep,interrupt

    • 同步方法(语句块)中调用sleep方法,cpu会被分配给其他线程。其他进程无法访问此同步方法(语句块),需等此线程结束该方法(块)的访问才能进行访问

    • //吵醒正在休眠的线程student,导致student会catch中断异常,结束休眠,重新排队等待cpu资源
      student.interrupt();
      catch(InterruptedException e) { // 如果由interrupt()唤醒则从这里继续执行
      	...  // thread interrupted during sleep or wait      
      }
      

    wait,notify,notifyall

    • 只能在同步块或同步方法中使用, wait()、notify()和notifyAll()方法是本地方法,并且为final方法,无法被重写

    • 应用场景:一个线程使用的同步方法中用到某个变量,这个变量又需要其他线程修改后才符合本线程的需要,需要使用wait()方法,比如买票没有零钱找给他时,他需要允许后面的人买票

    • notify()和notifyAll()方法只是唤醒等待该对象的monitor的线程,并不决定哪个线程能够获取到monitor

    • 调用wait后中断本线程的执行,使本线程等待,暂时让出cpu使用权,并允许其他线程使用这个同步方法。其他线程如果使用这个同步方法无需等待,当它使用完该方法时需要notifyAll所有使用该方法而等待的线程结束等待,丛刚才中断处继续执行,遵循“先中断先执行”。

      //同步方法中使用wait,notify
      public synchronized void method( args ){
          if(args == ...){...}
          else if(args == ... ){
          	while(共享变量满足...){
      		try{
      			wait();
                  ...
      		} catch {}
      	}
          else ...;
          notifyAll(); //执行结束通知
      } 
      
    • //wait放while中而不放if中的原因
      
      synchronized (monitor) {
      //  判断条件谓词是否得到满足
      	if(!locked) {
      		monitor.wait(); //当线程从wait中唤醒时,那么将直接执行处理wait后的代码,但这时候可能出现
      	}			 //另外一种可能,使得条件谓词已经不满足处理业务逻辑的条件了
      	//  处理其他的业务逻辑
      }
      
      synchronized (monitor) {
      //  判断条件谓词是否得到满足
      	while(!locked) {
      		monitor.wait(); //条件不满足出不去
      	}
      	//  处理其他的业务逻辑
      }
      
    • 生产者和消费者

      public class ProducerConsumerTest {
            public static void main(String[] args) {
                  Tray t = new Tray();  //t为交易托盘
                  Producer p1 = new Producer(t, 1);  //生产者线程p1的id为1
                  Consumer c1 = new Consumer(t, 2);
                  p1.start();
                  c1.start();
            } 
      }
      
      public class Producer extends Thread {
            private Tray tray;             
            private int id;
            public Producer(Tray t, int id) {
            	tray = t;             
      	  	this.id = id;            
            }
            public void run() {
                 int value;
                 for (int i = 0; i < 10; i++) 
                   for(int j = 0; j < 10; j++ ) {
                      value = i*10+j;
                      tray.put(value);
                      System.out.println("Producer #" + this.id   + " put: ("+value+ ").");
                      try { sleep((int)(Math.random() * 100));  }
                      catch (InterruptedException e) { }
                   };
             }   
       }
      
      public class Consumer extends Thread {
          private Tray tray;
          private int id;
          public Consumer(Tray t, int id) {
              tray = t;         
              this.id = id;    
          }
          public void run() {
              int value = 0;
              for (int i = 0; i < 10; i++) {
                  value = tray.get();
                  System.out.println("Consumer #" + this.id + " got: " + value);
              }   
          } 
      }
      
      //这里的get和put,get和get,put和put均互斥,即任一时刻只能有唯一的一个get或put方法在进行
      public class Tray {
          private int value;      
          private boolean full = false; 
          public synchronized int get() {
              while (full == false) { //盘里没满的时候不能取走
                  try {  
                  	wait(); //wait让出当前Tray对象的锁     
                  } catch (InterruptedException e) {}        
              }
              // 此时盘满了,消费者把盘里的东西都拿走
              full = false;  //把盘清空,否则消费者会无限消费下去
              notifyAll();  //注意notifyAll执行后,当前线程仍会执行下面的return语句
              return value;  //退出临界区,其他等待线程获得执行机会。
          }
          public synchronized void put(int v) {
              while (full == true) { //盘里满的时候生产者不能继续生产
                  try {  
                      wait();   
                  } catch (InterruptedException e) { }        
              }
              //此时盘没满,生产者生产把盘填满
              value = v;
              full = true;  //此时盘里置满,确保不会无限生产下去
              notifyAll(); 
          }
      }
      
      拓展:
      1.消费者不知道生产者会生产多少货物 - 只要有新的货物,就去消费
      public class Tray {
          private ArrayList<Integer> values;
          private int limit;
          public Tray(int vol){
          	values = new ArrayList<Integer>();
          	limit = vol;
          }
          public synchronized boolean isEmpty(){
                 return values.isEmpty();
          }
      }
          
      2.消费者的消费速度和生产速度匹配不上 - 需要使用队列来管理货物(缓冲区策略)
       public synchronized void put(int v){
             while(values.size()>=limit){try{wait();}catch (InterruptedException e){…}
             values.add(new Integer(v));
             notifyAll();
       }
       public synchronized int get(){
             while(values.size()==0){try{wait();}catch (InterruptedException e){…}
             int v=values.remove(values.size()-1).intValue();
             notifyAll();
             return v;
       }
       
      3.没有生产者怎么办 - 事先给消费者准备好了货物,消费完就game over
      for(…)t.put(…);//备好货
      Consumer c1 = new Consumer(t, 1);
      Consumer c2 = new Consumer(t, 2);
      Consumer c3 = new Consumer(t, 3);
      c1.start(); c2.start();c3.start();
      
      public class Consumer extends Thread {
          private Tray tray;…
          public void run(){
               int value;
               while(!tray.isEmpty()){
               value = tray.get();
               System.out.println("Consumer #" + this.id + " got: " + value);
          }
      }
      
    • 客户端请求和服务端响应

      //Handler来专门处理客户端和服务端的,对于客户端有2个方法就是发送请求和等待服务端响应,对于服务端同样2个方法那就是等待客户端请求和响应客户端
      public class Handler {
          private boolean isClientRequest=false; //表示当前是否有请求
          public void sendRequest(){
              synchronized (this){
                  isClientRequest = true;
                  this.notifyAll(); //告诉下面的waitRequest退出,服务端程序继续执行下面的代码
              }
          }
          public void waitResponse() throws InterruptedException {
              synchronized (this){
                  while (isClientRequest){
                      this.wait();
                  }
              }
          }
      
          public void receiveRequest(){
              synchronized (this) {
                  isClientRequest = false;
                  this.notifyAll();
              }
          }
          public void waitRequest() throws InterruptedException {
              synchronized (this){
                  while (!isClientRequest){
                      this.wait();
                  }
              }
          }
      }
      
      //客户端先发送请求,但是先等待1s为了让服务端处于等待的效果,发送请求后就处于等待状态直到服务端的响应
      public class Client implements Runnable {
          private Handler handler;
      
          public Client(Handler handler) {
              this.handler = handler;
          }
      
          public void run() {
              try {    //当前线程未被打断
                  while (!Thread.interrupted()) {
                      System.out.println("客户端发送请求");
                      TimeUnit.SECONDS.sleep(1);
                      this.handler.sendRequest();//第二步
                      System.out.println("等待服务端的响应");
                      this.handler.waitResponse();//第三步
                  }
              } catch (InterruptedException e) {
      
              }
              System.out.println("客户端已经完成请求");
          }
      }
      
      //服务端首先处于等待状态,收到客户端请求后立马进行处理,处理完毕之后再次等待客户端的请求
      public class Server implements Runnable {
          public Handler handler;
      
          public Server(Handler handler) {
              this.handler = handler;
          }
      
          public void run() {
              try {
                  while (!Thread.interrupted()) {
                      System.out.println("等待客户端请求");
                      this.handler.waitRequest();//第一步
                      System.out.println("处理客户端请求");
                      TimeUnit.SECONDS.sleep(1); //处理用1s时间
                      this.handler.receiveRequest();//第四步
                  }
              } catch (InterruptedException e) {
      
              }
              System.out.println("服务端处理已经完成");
          }
      }
      
    • Condition

      • Condition1的await()、signal()这种方式实现线程间协作更加安全和高效
      • 阻塞队列实际上是使用了Condition来模拟线程间协作
      • Condition是个接口,基本的方法就是await()和signal()方法
      • Condition依赖于Lock接口,生成一个Condition的基本代码是lock.newCondition()
      • 调用Condition的await()和signal()方法,都必须在lock保护之内,就是说必须在lock.lock()和lock.unlock之间才可以使用
        • Conditon中的await()对应Object的wait()
        • Condition中的signal()对应Object的notify()
        • Condition中的signalAll()对应Object的notifyAll()
      public class Test {
          private int queueSize = 10;
          private PriorityQueue<Integer> queue = new PriorityQueue<Integer>(queueSize);
          private Lock lock = new ReentrantLock();
          private Condition notFull = lock.newCondition();
          private Condition notEmpty = lock.newCondition();
           
          public static void main(String[] args)  {
              Test test = new Test();
              Producer producer = test.new Producer();
              Consumer consumer = test.new Consumer();
                
              producer.start();
              consumer.start();
          }
            
          class Consumer extends Thread{
              @Override
              public void run() {
                  consume();
              }
                
              private void consume() {
                  while(true){
                      lock.lock();
                      try {
                          while(queue.size() == 0){
                              try {
                                  System.out.println("队列空,等待数据");
                                  notEmpty.await(); //等待非空
                              } catch (InterruptedException e) {
                                  e.printStackTrace();
                              }
                          }
                          queue.poll();  //每次移走队首元素
                          notFull.signal(); //通知非满
                      } finally{
                          lock.unlock();
                      }
                  }
              }
          }
            
          class Producer extends Thread{
              @Override
              public void run() {
                  produce();
              }
                
              private void produce() {
                  while(true){
                      lock.lock();
                      try {
                          while(queue.size() == queueSize){
                              try {
                                  System.out.println("队列满,等待有空余空间");
                                  notFull.await(); //等待非满
                              } catch (InterruptedException e) {
                                  e.printStackTrace();
                              }
                          }
                          queue.offer(1);     //每次插入一个元素
                          notEmpty.signal();  //通知非空
                      } finally{
                          lock.unlock();
                      }
                  }
              }
          }
      }
      

    线程联合

    一个线程A在占有cpu资源时,一旦联合B线程,那么A将立即中断执行,等待A联合的B执行完毕,A再重新排队等待CPU以恢复执行。

    t.join(time):
    当前线程等待time规定的时间,或者在期间内t执行完毕,当前线程才能继续下面的执行

    public class example{
        public static void main(String []args){
            ThreadJoin a = new ThreadJoin();
            Thread customer = new ThreadJoin(a);
            Thread cakeMaker = new ThreadJoin(a);
            customer.setname("顾客");
            cakeMaker.setname("蛋糕师");
            a.setJoinThread(cakeMaker); //把蛋糕师线程作为顾客线程执行过程中的联合线程
            customer.start();
        }
    }
    
    public class ThreadJoin implements Runnable{
        Cake cake;
        Thread joinThread;
        public void setJoinThread(Thread t){
            joinThread = t;
        }
        
        @Override
        public void run(){
            if(Thread.currentThread().getName().equals("顾客")){
                Sys("顾客"+"等待"+joinThread.getName()+"制作生日蛋糕");
                try{
                    joinThread.start();
                    joinThread.join(); //当前线程开始等待joinThread结束
                } catch(InterruptedException e){}
                Sys("顾客买了"+cake.name+"价钱:"+cake.price);
            } else if(Thread.currentThread()==joinThread){
                Sys("开始制作蛋糕");
                try{ Thread.sleep(2000); } catch(){}
                cake = new Cake("生日蛋糕",158);
                Sys(joinThread.getName() + "制作完毕");
            }
        }
        
        Class Cake{  //内部类
            int price;
            String name; 
            Cake(String name,int price){
                this.name = name;
                this.price = price;
            }
        }
    }
    

    守护线程

    • 守护线程:当程序中所有非守护线程都结束时,守护线程立刻结束(无论是否执行完)。

    • 非守护线程:用户线程默认是非守护线程。

    • a.setDaemon(true); 将线程对象a设置为守护线程

    单例模式

    • 场景:当系统需要一个全局对象进行协调,单例模式是一种非常有效的全局对象保存和访问方法,避免把全局对象到处传递。对象只创建一个实例,并且提供一个全局的访问点。
    • 用法:
      1. 将构造方法定义为Private,只有通过该类提供的静态方法来得到该类的唯一实例
      2. 提供一个静态方法,当调用这个方法时,如果类持有的全局对象引用不为空就返回这个引用,否则就创建实例,并保存好。(静态方法的访问:类名.方法名)
    public class Singleton {
    
        private static volatile Singleton singleton;
    
        private Singleton() {}
    
        public static Singleton getInstance() {
            if (singleton == null) { //对实例方法的访问不应该被阻塞,否则效率低
                synchronized (Singleton.class) {  //在实例化时进行阻塞防止创建多个
                    if (singleton == null) {   //阻塞多个线程同时创建
                        singleton = new Singleton();
                    }
                }
            }
            return singleton;
        }
    }
    

    观察者模式(待更新)

    volatile

    阻塞队列

    原子类

    https://www.jianshu.com/p/84c75074fa03

    参考资料

    1.https://www.cnblogs.com/dolphin0520/p/3932906.html

    2.https://www.cnblogs.com/dolphin0520/p/3920373.html

    3.https://www.cnblogs.com/wxd0108/p/5479442.html

    4.https://www.jianshu.com/p/84c75074fa03

    5.Java实用教程2

    注:部分内容有从各种博文中摘录,但时间过长未记录出处,如有需要请联系备注

  • 相关阅读:
    Android Studio运行Hello World程序
    WPF,回车即是tab
    phpmyadmin上在某数据库里创建函数
    thinkphp项目部署在phpstudy里的nginx上
    《原创视频》牛腩学docker简记
    visual studio添加docker支持简记
    edge 浏览器中数字显示为链接
    JSON.net 在实体类中自定义日期的格式
    让easyui 的alert 消息框中的确定按钮支持空格键
    修复百度编辑器(UM)禁用时上传图片按钮还可点击的BUG;
  • 原文地址:https://www.cnblogs.com/Red-Revolution/p/10692194.html
Copyright © 2020-2023  润新知