• 多线程1


    进程与线程的区别:

    进程
    正在运行的程序,是系统进行资源分配和调用的独立单位
    每一个进程都有它自己的内存空间和系统资源
    线程
    进程中的单个顺序控制流,是一条执行路径
    一个进程如果只有一条执行路径,则是单线程程序
    一个进程内有多条执行路径,则是多线程程序
    一个进程内可以执行多个任务,每个任务就是一个线程

    多线程的意义
    单进程计算机只能做一件事,现在计算机同一时间段内可以执行多个任务,提高CPU的利用率
    多线程的意义
    多线程的存在,不是提高程序的执行速度,其实是为了提高应用程序的使用率
    程序执行是抢占CPU的资源,CPU的执行权
    多个进程抢这些资源,而其中的某一个进程如果执行路径比较多,就会有更高的几率抢到
    无法保证线程在什么时候抢到资源,线程执行有随机性

    并发:逻辑上同时发生,指在某个时间内同时运行多个程序。
    并行:物理上同时发生,指在某个时间点运行多个程序。、
    Java程序运行原理
        java命令启动java虚拟机,启动JVM,等于启动一个应用程序,也就是启动一个进程。
        该进程会自动启动一个主线程,然后主线程去调用某个类的main方法,所以main方法运行在主线程中,
        再次之前程序都是单线程的。
        JVM是多线程,垃圾回收线程也要先东西,否则会很容易出现内存溢出。
        最少启动了主线程和垃圾回收线程两个线程。
    

    线程是依赖进程存在的,所以要先调用一个进程。进程由系统创建的,所以我们应该调用系统功能创建一个进程。
    Java是不能调用系统功能的,所以我们没有办法直接实现多线程。Java可以调用C/C++写好的程序实现多线程,由C/C++调用系统功能创建进程,然后由Java去调用这样的东西,然后提供一些类供我们使用,就可以实现多线程程序。
    实现多线程的方式

    1. 继承Thread
    public class ThreadJava extends Thread {
    	//重写run()方法
    	public void run() {
    
    	}
    }
    
    1. 实现Runnable接口
    public class RunnableJava Implements Runnable{
    	//重写run()方法
    	public void run(){
    
    	}
    }
    

    执行多线程
    如果是执行调用run()方法的话就会作为普通类执行,而不是多线程进行执行。

    //继承Thread类的执行方式
    	ThreadJava tj = new ThreadJava();
    	tj.start()
    
    //实现Runnable接口的执行方式
    	RunnableJava rj = new RunnableJava();
    	Thread t = new Thread(rj);
    	t.start();
    

    Thread

    /*
     * 两个线程分别得到i的值,分别执行run()方法体
     * */
    public class newTest extends Thread{
    	private int i = 100;
    	public void run() {
    		while (true) {
    			if (i > 0) {
    				System.out.println("i的值为: " + (i--) + "--------" + Thread.currentThread().getName());
    			}
    		}
    	}
    	public static void main(String[] srgs) {
    		newTest nt = new newTest();
    		newTest nt2 = new newTest();
    		nt.start();
    		nt2.start();
    	}
    }
    
    /*
     * 两个线程共用i
     * */
    public class newTest extends Thread{
    	private static int i = 100;
    	public void run() {
    		while (true) {
    			if (i > 0) {
    				System.out.println("i的值为: " + (i--) + "--------" + Thread.currentThread().getName());
    			}
    		}
    	}
    	public static void main(String[] srgs) {
    		newTest nt = new newTest();
    		newTest nt2 = new newTest();
    		nt.start();
    		nt2.start();
    	}
    }
    

    这里主要看i是类变量还是成员变量,类变量任意类对象都可以对它进行更改,成员变量因为对象的不同而不同。
    实现Runnable接口
    程序一:下面程序每次输出i的值减一

    public class RunnableJava implements Runnable{
         private int i = 100;
         private Object obj = new Object();
         public void run() {
               while (true) {
                    synchronized (obj) {
                          obj.notifyAll();
                          if (i > 0) {
                               System.out.println("i的值为: " + (i--) + "--------" + Thread.currentThread().getName());
                          }
                          try {
                               Thread.sleep(1000);
                          } catch (InterruptedException e) {
                               // TODO Auto-generated catch block
                               e.printStackTrace();
                          }
                          try {
                               obj.wait(1000);
                          } catch (InterruptedException e) {
                               // TODO Auto-generated catch block
                               e.printStackTrace();
                          }
                    }
               }
         }
    }
    //---------------------------------------------------------------------------------------------------------------------------------
               RunnableJava rj1 = new RunnableJava();
               Thread t1 = new Thread(rj1, "线程rj1 ---------- 线程一");
               Thread t2 = new Thread(rj1, "线程rj1 ---------- 线程二");
               Thread t3 = new Thread(rj1, "线程rj1 ---------- 线程三");
               t1.start();
               t2.start();
               t3.start();
    

    程序二:如果建两个Runnable的实现类对象分别传入多个Thread中,如果想让两个Runnable分别计算输出i的值,i不能用static修饰,锁对象可用static修饰,也可以不用。不过这里是有区别的,不用static代表分别传入到Thread的Runnable的两个内存每次只能有一个Thread访问,使用static的话同一只能有一个线程访问两个Runnable内存中的一块,也就是只能执行一个Runnable的run()方法

    public class RunnableJava implements Runnable{     
    	private int i = 100;    
    	private static Object obj = new Object();   
    	public void run() {         
    		while (true) {                
    			synchronized (obj) {                    
    				obj.notifyAll();                   
    				if (i > 0) {                         
    					System.out.println("i的值为: " + (i--) + "--------" + Thread.currentThread().getName());          
    					}                     
    				try {                          
    					Thread.sleep(1000);                      
    					} catch (InterruptedException e) {                          
    					// TODO Auto-generated catch block                          
    						e.printStackTrace();                    
    						}                    
    				try {                          
    					obj.wait(1000);                
    					} catch (InterruptedException e) {                          
    						e.printStackTrace();                     
    					}             
    				}         
    			}  
    		}
    	}
    
     
    public class RunnableJava implements Runnable{
         private int i = 100;
         private Object obj = new Object();
         public void run() {
               while (true) {
                    synchronized (obj) {
                          obj.notifyAll();
                          if (i > 0) {
                               System.out.println("i的值为: " + (i--) + "--------" + Thread.currentThread().getName());
                          }
                          try {
                               Thread.sleep(1000);
                          } catch (InterruptedException e) {
                               // TODO Auto-generated catch block
                               e.printStackTrace();
                          }
                          try {
                               obj.wait(1000);
                          } catch (InterruptedException e) {
                               // TODO Auto-generated catch block
                               e.printStackTrace();
                          }
                    }
               }
         }
    }
    
               RunnableJava rj1 = new RunnableJava();
               RunnableJava rj2 = new RunnableJava();
               Thread t1 = new Thread(rj1, "线程rj1 ---------- 线程一");
               Thread t2 = new Thread(rj1, "线程rj1 ---------- 线程二");
               Thread t3 = new Thread(rj1, "线程rj1 ---------- 线程三");
               t1.start();
               t2.start();
               t3.start();
               Thread t4 = new Thread(rj2, "线程rj2 ---------- 线程四");
               Thread t5 = new Thread(rj2, "线程rj2 ---------- 线程五");
               Thread t6 = new Thread(rj2, "线程rj2 ---------- 线程六");
               t4.start();
               t5.start();
               t6.start();
    

    程序三:如果建两个Runnable的实现类对象分别传入多个Thread中,如果想让两个Runnable共同计算输出j的值,i不和锁对象必须用static修饰

    public class RunnableJava implements Runnable{
         private static int i = 100;
         private static Object obj = new Object();
         public void run() {
               while (true) {
                    synchronized (obj) {
                          obj.notifyAll();
                          if (i > 0) {
                               System.out.println("i的值为: " + (i--) + "--------" + Thread.currentThread().getName());
                          }
                          try {
                               Thread.sleep(1000);
                          } catch (InterruptedException e) {
                               // TODO Auto-generated catch block
                               e.printStackTrace();
                          }
                          try {
                               obj.wait(1000);
                          } catch (InterruptedException e) {
                               // TODO Auto-generated catch block
                               e.printStackTrace();
                          }
                    }
               }
         }
    }
    

    这里要是静态变量的知识,static修饰的变量是属于类的,不管你new几个对象,都只会有这一个对象。所以
    程序一,只有一个Runnable对象传入Thread,访问的是这个Runnable对象的内存,这里只有一个i和锁对象,所以不需要使用static修饰;
    程序二,有两个Runnable对象分别传入Thread,这里有两块内存,要分别计算输出i 的值,i就必须每个对象给一个,但是锁对象可以是唯一的,也可以不是唯一的。锁对象static修饰代表同一时间只能有一个run()方法执行操作,不用static修饰代表同一时间段传入两个Runnable对象的两个线程可以同时被执行;
    程序三,i的值和锁对象都使用static修饰,代表不管有几个Runnable对象传入多少个Thread中,变量i从100递减到1只会执行一次,和程序一结果相同。
    上面就是看内存是否访问的是同一个变量,使用的锁对象是否是同一个对象,锁住之后的方法块每次都只能一个得到锁对象的线程可以执行。
    线程同步synchronized
    通过synchronized关键字实现线程同步,synchronized是不被继承的
    1、synchronized同步代码块,同步代码块传入的锁对象可以是任意对象

    //线程必须得到锁才能执行同步代码块,否则无法执行同步代码块
    public class SynchronizedTest2 implements Runnable {
    	private static int i = 0;
    	private static int j = 100;
    	//锁对象不能为空值,定义为类变量则为所有类对象共用
    	private static Object obj1 = new Object();
    	private static Object obj2 = new Object();
    
    	@Override
    	public void run() {
    		while(true) {
    			synchronized (obj1) {
    				if(i < 100) {
    					System.out.println(Thread.currentThread().getName() + "---------" + (i++));
    				}
    			}
    			
    			synchronized (obj2) {
    				if(j > 0) {
    					System.out.println(Thread.currentThread().getName() + "---------" + (j--));
    				}
    			}
    		}
    		
    	}
    
    }
    

    2、synchronized修饰成员方法,修饰成员方法是锁对象问对象本身,也就是this

    /*
     * synchronized修饰普通方法对象锁是本身,即this
     * */
    public class SynchronizedMethod2 implements Runnable{
    
    	public synchronized void method() {
    		System.out.println("synchronized修饰普通方法" + Thread.currentThread().getName());
    	}
    	
    	public void run() {
    		// TODO Auto-generated method stub
    		while(true) {
    			this.method();
    			synchronized(this) {
    				System.out.println("synchronized修饰同步代码块" + Thread.currentThread().getName());
    			}
    		}
    	}
    	
    	public static void main(String[] args) {
    		SynchronizedMethod2 sm = new SynchronizedMethod2();
    		Thread t1 = new Thread(sm, "线程一");
    		SynchronizedMethod2 sm2 = new SynchronizedMethod2();
    		//传入sm2的时候锁是this sm2
    		//传入sm的时候锁是this  sm
    		//Thread t2 = new Thread(sm2, "线程二");
    		Thread t2 = new Thread(sm, "线程二");
    		t1.start();
    		t2.start();
    	}
    }
    
    

    3、synchronized修饰静态方法,修饰静态方法的锁对象是类本身的.class文件

    /* synchronized(类.class)锁住的是整个类
     * synchronized修饰静态方法和同步代码块,在两个方法体中都sleep()使得其他线程有机会得到执行
     * 不管创建了多少个Runnable对象传入到不同的Thread中,每次只能有一个Thread执行synchronized(类.class)代码块,同synchronized修饰的静态方法
     * */
    public class SynchronizedMethod implements Runnable {
    	private static int i = 20;
    	private int j = 0;
    	private static Object obj = new Object();
    	public synchronized static void method1() {
    		//SynchronizedMethod.class.notifyAll();
    		System.out.println("synchronized修饰静态方法:" + "---" + Thread.currentThread().getName());
    		if (i > 0) {
    			System.out.println("静态方法中i的值: " + (i--) + "---" + Thread.currentThread().getName());
    
    			System.out.println("静态方法执行完");
    //			try {
    //				Thread.sleep(1000);
    //			} catch (InterruptedException e) {
    //				// TODO Auto-generated catch block
    //				e.printStackTrace();
    //			}
    //			try {
    //				SynchronizedMethod.class.wait();
    //			} catch (InterruptedException e1) {
    //				// TODO Auto-generated catch block
    //				e1.printStackTrace();
    //			}
    		}
    	}
    
    	public void run() {
    		// 方法的调用放在while()循环外只会执行method1()方法
    		// method1();
    		while (true) {
    			method1();
    			//同步代码块锁对象为类的class文件,则是锁住整个类的,其他线程必须等代码块执行完释放锁才能继续执行
    			synchronized (SynchronizedMethod.class) {
    			//把同步代码块锁对象换成obj,线程之间互无关系,可以任意执行自己对象的锁
    			//synchronized (obj) {
    				//SynchronizedMethod.class.notifyAll();
    				System.out.println("synchronized使用类class文件修饰同步代码块" + "---" + Thread.currentThread().getName());
    				if (j < 20) {
    					System.out.println("同步代码块中j的值: " + (j++) + "---" + Thread.currentThread().getName());
    					System.out.println("同步代码块执行完");
    					try {
    						Thread.sleep(1000);
    					} catch (InterruptedException e) {
    						// TODO Auto-generated catch block
    						e.printStackTrace();
    					}
    //					try {
    //						SynchronizedMethod.class.wait();
    //					} catch (InterruptedException e1) {
    //						// TODO Auto-generated catch block
    //						e1.printStackTrace();
    //					}
    				}
    			}
    		}
    	}
    
    }
    

    4、synchronized修饰run()方法,代表某个线程只能有一个run()方法执行

    public class SynchronizedRun implements Runnable{
    
    	public static void main(String[] args) {
    		// TODO Auto-generated method stub
    		SynchronizedRun sr = new SynchronizedRun();
    		Thread t1 = new Thread(sr, "线程一");
    		//SynchronizedRun sr2 = new SynchronizedRun();
    		Thread t2 = new Thread(sr, "线程二");
    		t1.start();
    		t2.start();
    	}
    
    	//等于synchronized(this),可以保证一个线程只能有一个run()方法在运行
    	//synchronized(this)在没有释放锁的情况下只有一个线程能够执行这个方法
    	public synchronized void run() {
    		// TODO Auto-generated method stub
    		System.out.println("多线程run方法" + Thread.currentThread().getName());
    	}
    
    }
    

    synchronized锁住的是括号里的对象,而不是代码。对于非static的synchronized方法,锁的就是对象本身也就是this。就上面程序二的类,创建两个Runnable对象,分别传入到Thread中,每一个Runnable对象的同步代码块有一个锁对象obj,两个Runnable对象传到的Thread多线程可以同时执行同步代码块中的代码。而同一个Runnable对象下的多线程之间互斥,谁得到锁谁执行。所以synchronized锁住的是对象不是代码块。
    static synchronized方法,static方法可以直接类名加方法名调用,方法中无法使用this,所以它锁的不是this,而是类的Class对象,所以,static synchronized方法也相当于全局锁,相当于锁住了整个代码段。同步代码块如果想要全局锁可以传入类本身的class文件。
    线程同步Lock
    1、lock方法,没有获取锁一直等待

    public class LockJava implements Runnable {
    	private static int i = 100;
    	private static Lock lock = new ReentrantLock();
    //	private static Condition c = lock.newCondition();
    
    	public void run() {
    		while(true) {
    			lock.lock();
    			try {
    //				c.signalAll();
    				if(i > 0) {
    					System.out.println(Thread.currentThread().getName() + "----" + (i--));
    //					try {
    //						c.await();
    //					} catch (InterruptedException e) {
    //						// TODO Auto-generated catch block
    //						e.printStackTrace();
    //					}
    				}
    			}finally {
    				lock.unlock();
    			}
    		}
    	}
    }
    

    2、tryLock()方法
    tryLock()方法返回一个boolean值,判断是否可以获取锁,并立即返回结果。所以tryLock()和lock()方法一样使用的话如果没有获取锁只会返回false,但是在unLock()的时候会报错,因为并没有获取锁。不过报错的情况并不是使程序终止

    /*
     * IllegalMonitorStateException异常
     * 线程一运行获得lock1,等待,
     * 线程二运行获得lock2,等待,
     * 线程一执行lock2锁锁住的部分,并不能获取锁,
     * */
    public class DeathLock1 {
    	static Lock lock1 = new ReentrantLock();
    	static Lock lock2 = new ReentrantLock();
    	public static void main(String[] args) {
    		new Thread(new Runnable() {
    			public void run() {
    				while(true) {
    					System.out.println(Thread.currentThread().getName() + "开始运行:");
    					lock1.lock();
    					try {
    						System.out.println(Thread.currentThread().getName() + "get  lock1");
    						try {
    							Thread.sleep(1000);
    						} catch (InterruptedException e) {
    							// TODO Auto-generated catch block
    							e.printStackTrace();
    						}
    						//lock2.lock();
    						//这里并没有获得锁
    						lock2.tryLock();
    						try {
    							System.out.println(Thread.currentThread().getName() + "get lock2");
    						}finally {
    							//这里会报IllegalMonitorStateException错误
    							//因为此时锁不在这里
    							lock2.unlock();
    						}		
    					}finally {
    						lock1.unlock();
    					}
    				}			
    			}			
    		}, "线程一").start();
    		
    		new Thread(new Runnable() {
    			@Override
    			public void run() {
    				// TODO Auto-generated method stub
    				while(true) {
    					System.out.println(Thread.currentThread().getName() + "开始运行:");
    					lock2.lock();
    					try {
    						System.out.println(Thread.currentThread().getName() + "get  lock2");
    						
    						try {
    							Thread.sleep(1000);
    						} catch (InterruptedException e) {
    							e.printStackTrace();
    						}
    						//lock1.lock();
    						lock1.tryLock();
    						try {
    							//boolean booleanlock1 = lock1.tryLock();
    							System.out.println(Thread.currentThread().getName() + "get lock1");
    						}finally {
    							//没有获取锁,会报错
    							lock1.unlock();
    						}	
    					}finally {
    						System.out.println("线程二释放lock2");
    						lock2.unlock();
    					}
    				}			
    			}
    				
    			
    			
    		}, "线程二").start();
    		
    	}
    }
    

    可以使用if语句,tryLock()作为条件,获取了就执行,不获取就不执行

    public class DeathLock1 {
    	static Lock lock1 = new ReentrantLock();
    	static Lock lock2 = new ReentrantLock();
    	static Condition  c1 = lock1.newCondition();
    	static Condition  c2 = lock2.newCondition();
    	public static void main(String[] args) {
    
    		new Thread(new Runnable() {
    			public void run() {
    				while (true) {
    					System.out.println(Thread.currentThread().getName() + "开始运行:");
    					lock1.lock();
    					try {
    						System.out.println(Thread.currentThread().getName() + "get  lock1");
    						try {
    							Thread.sleep(1000);
    						} catch (InterruptedException e) {
    							e.printStackTrace();
    						}
    						//线程二释放了lock2,这里就可以获取
    						if (lock2.tryLock()) {
    							try {
    								System.out.println(Thread.currentThread().getName() + "get lock2");
    								try {
    									Thread.sleep(1000);
    								} catch (InterruptedException e) {
    									e.printStackTrace();
    								}
    							} finally {
    								lock2.unlock();
    							}
    						}
    					} finally {
    						System.out.println("线程一释放了lock1");
    						lock1.unlock();
    					}
    				}
    			}
    		}, "线程一").start();
    
    		new Thread(new Runnable() {
    			@Override
    			public void run() {
    				// TODO Auto-generated method stub
    					System.out.println(Thread.currentThread().getName() + "开始运行:");
    					lock2.lock();
    					try {
    						System.out.println(Thread.currentThread().getName() + "get  lock2");
    						try {
    							Thread.sleep(1000);
    						} catch (InterruptedException e) {
    							e.printStackTrace();
    						}
    					} finally {
    						System.out.println("线程二释放了lock2");
    						lock2.unlock();
    					}
    			}
    
    		}, "线程二").start();
    
    	}
    }
    

    tryLock()不是lock()那种使用方式

    lock.tryLock() //这里返回的是boolean,这里不会报错
    ...
    lock.unlock() //这里没有获取锁的话会报错
    

    3.lockInterruptibly,没有当前线程没有被中断,则获取锁
    允许在等待时由其它线程调用等待线程的Thread.interrupt方法来中断等待线程的等待而直接返回,这时不用获取锁,而会抛出一个InterruptedException。而ReentrantLock.lock方法不允许Thread.interrupt中断,即使检测到Thread.isInterrupted,一样会继续尝试获取锁,失败则继续休眠。只是在最后获取锁成功后再把当前线程置为interrupted状态。
    线程同步读写锁

    public class LockWriterReaderJava {
    	private static int i = 0;
    	private static ReentrantReadWriteLock  lock = new ReentrantReadWriteLock();
    	private static ReentrantReadWriteLock.ReadLock  read = lock.readLock();
    	private static ReentrantReadWriteLock.WriteLock write = lock.writeLock();
    	public static void main(String[] args) {
    		// TODO Auto-generated method stub
    		
    		//读,可以随意访问
    		new Thread(new Runnable() {
    
    			@Override
    			public void run() {
    				// TODO Auto-generated method stub
    				while(true) {
    					write.lock();
    					try {
    						if(i < 100) {
    							System.out.println(Thread.currentThread().getName() + (i++) + "----i增加1");
    						}
    					}finally {
    						write.unlock();
    					}
    					read.lock();
    					try {
    						if(i < 100) {
    							System.out.println(Thread.currentThread().getName() + i);
    						}
    					}finally {
    						read.unlock();
    					}
    				}
    			}
    			
    		}, "线程一").start();
    		
    		//写,每次只能一个线程进行访问
    		new Thread(new Runnable() {
    
    			@Override
    			public void run() {
    				// TODO Auto-generated method stub
    				while(true) {
    					read.lock();
    					try {
    						if(i < 100) {
    							System.out.println(Thread.currentThread().getName() + i);
    						}
    					}finally {
    						read.unlock();
    					}
    				}
    			}
    			
    		}, "线程二").start();
    		
    	}
    
    }
    

    写锁可以“降级”为读锁;读锁不能“升级”为写锁。在线程持有读锁的情况下,该线程不能取得写锁(因为获取写锁的时候,如果发现当前的读锁被占用,就马上获取失败,不管读锁是不是被当前线程持有)在线程持有写锁的情况下,该线程可以继续获取读锁(获取读锁时如果发现写锁被占用,只有写锁没有被当前线程占用的情况才会获取失败)。
    死锁

    /*
    	 * 线程一开始运行: 线程一get到obj1 线程二开始运行: 线程二get到obj2
    	 * 
    	 * 线程一运行获得obj1,到sleep()方法,这个时候 线程二运行获得obj2,到sleep()方法
    	 * 此时线程一等待线程二释放obj2锁继续运行,线程二等到obj1继续运行,都没等到,所以死锁。
    	 */
    	public static void main(String[] args) {
    		// TODO Auto-generated method stub
    		new Thread(new Runnable() {
    
    			@Override
    			public void run() {
    				// TODO Auto-generated method stub
    				while (true) {
    					System.out.println(Thread.currentThread().getName() + "开始运行:");
    					synchronized (obj1) {
    						System.out.println(Thread.currentThread().getName() + "get到obj1");
    						try {
    							Thread.sleep(1000);
    						} catch (InterruptedException e) {
    							// TODO Auto-generated catch block
    							e.printStackTrace();
    						}
    						synchronized (obj2) {
    							System.out.println(Thread.currentThread().getName() + "get到obj2");
    							try {
    								Thread.sleep(1000);
    							} catch (InterruptedException e) {
    								// TODO Auto-generated catch block
    								e.printStackTrace();
    							}
    						}
    					}
    				}
    			}
    
    		}, "线程一").start();
    
    		new Thread(new Runnable() {
    
    			@Override
    			public void run() {
    				// TODO Auto-generated method stub
    				while (true) {
    					System.out.println(Thread.currentThread().getName() + "开始运行:");
    					synchronized (obj2) {
    						System.out.println(Thread.currentThread().getName() + "get到obj2");
    						try {
    							Thread.sleep(1000);
    						} catch (InterruptedException e) {
    							// TODO Auto-generated catch block
    							e.printStackTrace();
    						}
    						synchronized (obj1) {
    							System.out.println(Thread.currentThread().getName() + "get到obj1");
    							try {
    								Thread.sleep(1000);
    							} catch (InterruptedException e) {
    								// TODO Auto-generated catch block
    								e.printStackTrace();
    							}
    						}
    					}
    				}
    			}
    
    		}, "线程二").start();
    	}
    }
    

    线程调度
    线程状态:

  • 相关阅读:
    ZipArchive 的使用
    Bootstrap使用心得
    SQL SERVER 级联删除
    ASP.NET 使用C#代码设置页面元素中的样式或属性
    GDI+中发生一般性错误之文件被占用
    .Net 中资源的使用方式
    一张图全解析个性化邮件那么重要
    看天猫EDM营销学企业EDM营销
    细数EDM营销中存在的两大盲点
    如何进行EDM邮件内容的撰写
  • 原文地址:https://www.cnblogs.com/changzuidaerguai/p/9310082.html
Copyright © 2020-2023  润新知