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

专业旅游网站建设免费个人电子版简历下载

专业旅游网站建设,免费个人电子版简历下载,西安优化官网厂家,工程师报考网站线程安全是并发编程中的重要关注点#xff0c;应该注意到的是#xff0c;造成线程安全问题的主要诱因有两点#xff0c;一是存在共享数据(也称临界资源)#xff0c;二是存在多条线程共同操作共享数据。因此为了解决这个问题#xff0c;我们可能需要这样一个方案#xff0…线程安全是并发编程中的重要关注点应该注意到的是造成线程安全问题的主要诱因有两点一是存在共享数据(也称临界资源)二是存在多条线程共同操作共享数据。因此为了解决这个问题我们可能需要这样一个方案当存在多个线程操作共享数据时需要保证同一时刻有且只有一个线程在操作共享数据其他线程必须等到该线程处理完数据后再进行这种方式有个高尚的名称叫互斥锁即能达到互斥访问目的的锁也就是说当一个共享数据被当前正在访问的线程加上互斥锁后在同一个时刻其他线程只能处于等待的状态直到当前线程处理完毕释放该锁。在 Java 中关键字 synchronized可以保证在同一个时刻只有一个线程可以执行某个方法或者某个代码块(主要是对方法或者代码块中存在共享数据的操作)同时我们还应该注意到synchronized另外一个重要的作用synchronized可保证一个线程的变化(主要是共享数据的变化)被其他线程所看到(保证可见性完全可以替代Volatile功能)这点确实也是很重要的。synchronized的三种应用方式synchronized关键字最主要有以下3种应用方式下面分别介绍修饰实例方法作用于当前实例加锁进入同步代码前要获得当前实例的锁修饰静态方法作用于当前类对象加锁进入同步代码前要获得当前类对象的锁修饰代码块指定加锁对象对给定对象加锁进入同步代码库前要获得给定对象的锁。synchronized作用于实例方法所谓的实例对象锁就是用synchronized修饰实例对象中的实例方法注意是实例方法不包括静态方法如下public class AccountingSync implements Runnable{ //共享资源(临界资源) static int i0; /** * synchronized 修饰实例方法 */ public synchronized void increase(){ i; } Override public void run() { for(int j0;j1000000;j){ increase(); } } public static void main(String[] args) throws InterruptedException { AccountingSync instancenew AccountingSync(); Thread t1new Thread(instance); Thread t2new Thread(instance); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(i); } /** * 输出结果: * 2000000 */}上述代码中我们开启两个线程操作同一个共享资源即变量i由于i;操作并不具备原子性该操作是先读取值然后写回一个新值相当于原来的值加上1分两步完成如果第二个线程在第一个线程读取旧值和写回新值期间读取i的域值那么第二个线程就会与第一个线程一起看到同一个值并执行相同值的加1操作这也就造成了线程安全失败因此对于increase方法必须使用synchronized修饰以便保证线程安全。此时我们应该注意到synchronized修饰的是实例方法increase在这样的情况下当前线程的锁便是实例对象instance注意Java中的线程同步锁可以是任意对象。从代码执行结果来看确实是正确的倘若我们没有使用synchronized关键字其最终输出结果就很可能小于2000000这便是synchronized关键字的作用。这里我们还需要意识到当一个线程正在访问一个对象的 synchronized 实例方法那么其他线程不能访问该对象的其他 synchronized 方法毕竟一个对象只有一把锁当一个线程获取了该对象的锁之后其他线程无法获取该对象的锁所以无法访问该对象的其他synchronized实例方法但是其他线程还是可以访问该实例对象的其他非synchronized方法当然如果是一个线程 A 需要访问实例对象 obj1 的 synchronized 方法 f1(当前对象锁是obj1)另一个线程 B 需要访问实例对象 obj2 的 synchronized 方法 f2(当前对象锁是obj2)这样是允许的因为两个实例对象锁并不同相同此时如果两个线程操作数据并非共享的线程安全是有保障的遗憾的是如果两个线程操作的是共享数据那么线程安全就有可能无法保证了如下代码将演示出该现象public class AccountingSyncBad implements Runnable{ static int i0; public synchronized void increase(){ i; } Override public void run() { for(int j0;j1000000;j){ increase(); } } public static void main(String[] args) throws InterruptedException { //new新实例 Thread t1new Thread(new AccountingSyncBad()); //new新实例 Thread t2new Thread(new AccountingSyncBad()); t1.start(); t2.start(); //join含义:当前线程A等待thread线程终止之后才能从thread.join()返回 t1.join(); t2.join(); System.out.println(i); }}上述代码与前面不同的是我们同时创建了两个新实例AccountingSyncBad然后启动两个不同的线程对共享变量i进行操作但很遗憾操作结果是1452317而不是期望结果2000000因为上述代码犯了严重的错误虽然我们使用synchronized修饰了increase方法但却new了两个不同的实例对象这也就意味着存在着两个不同的实例对象锁因此t1和t2都会进入各自的对象锁也就是说t1和t2线程使用的是不同的锁因此线程安全是无法保证的。解决这种困境的的方式是将synchronized作用于静态的increase方法这样的话对象锁就当前类对象由于无论创建多少个实例对象但对于的类对象拥有只有一个所有在这样的情况下对象锁就是唯一的。下面我们看看如何使用将synchronized作用于静态的increase方法。synchronized作用于静态方法当synchronized作用于静态方法时其锁就是当前类的class对象锁。由于静态成员不专属于任何一个实例对象是类成员因此通过class对象锁可以控制静态 成员的并发操作。需要注意的是如果一个线程A调用一个实例对象的非static synchronized方法而线程B需要调用这个实例对象所属类的静态 synchronized方法是允许的不会发生互斥现象因为访问静态 synchronized 方法占用的锁是当前类的class对象而访问非静态 synchronized 方法占用的锁是当前实例对象锁看如下代码public class AccountingSyncClass implements Runnable{ static int i0; /** * 作用于静态方法,锁是当前class对象,也就是 * AccountingSyncClass类对应的class对象 */ public static synchronized void increase(){ i; } /** * 非静态,访问时锁不一样不会发生互斥 */ public synchronized void increase4Obj(){ i; } Override public void run() { for(int j0;j1000000;j){ increase(); } } public static void main(String[] args) throws InterruptedException { //new新实例 Thread t1new Thread(new AccountingSyncClass()); //new心事了 Thread t2new Thread(new AccountingSyncClass()); //启动线程 t1.start();t2.start(); t1.join();t2.join(); System.out.println(i); }}由于synchronized关键字修饰的是静态increase方法与修饰实例方法不同的是其锁对象是当前类的class对象。注意代码中的increase4Obj方法是实例方法其对象锁是当前实例对象如果别的线程调用该方法将不会产生互斥现象毕竟锁对象不同但我们应该意识到这种情况下可能会发现线程安全问题(操作了共享静态变量i)。synchronized同步代码块除了使用关键字修饰实例方法和静态方法外还可以使用同步代码块在某些情况下我们编写的方法体可能比较大同时存在一些比较耗时的操作而需要同步的代码又只有一小部分如果直接对整个方法进行同步操作可能会得不偿失此时我们可以使用同步代码块的方式对需要同步的代码进行包裹这样就无需对整个方法进行同步操作了同步代码块的使用示例如下public class AccountingSync implements Runnable{ static AccountingSync instancenew AccountingSync(); static int i0; Override public void run() { //省略其他耗时操作.... //使用同步代码块对变量i进行同步操作,锁对象为instance synchronized(instance){ for(int j0;j1000000;j){ i; } } } public static void main(String[] args) throws InterruptedException { Thread t1new Thread(instance); Thread t2new Thread(instance); t1.start();t2.start(); t1.join();t2.join(); System.out.println(i); }}从代码看出将synchronized作用于一个给定的实例对象instance即当前实例对象就是锁对象每次当线程进入synchronized包裹的代码块时就会要求当前线程持有instance实例对象锁如果当前有其他线程正持有该对象锁那么新到的线程就必须等待这样也就保证了每次只有一个线程执行i;操作。当然除了instance作为对象外我们还可以使用this对象(代表当前实例)或者当前类的class对象作为锁如下代码//this,当前实例对象锁synchronized(this){ for(int j0;j1000000;j){ i; }}//class对象锁synchronized(AccountingSync.class){ for(int j0;j1000000;j){ i; }}了解完synchronized的基本含义及其使用方式后下面我们将进一步深入理解synchronized的底层实现原理。synchronized底层语义原理Java 虚拟机中的同步(Synchronization)基于进入和退出管程(Monitor)对象实现 无论是显式同步(有明确的 monitorenter 和 monitorexit 指令,即同步代码块)还是隐式同步都是如此。在 Java 语言中同步用的最多的地方可能是被 synchronized 修饰的同步方法。同步方法 并不是由 monitorenter 和 monitorexit 指令来实现同步的而是由方法调用指令读取运行时常量池中方法的 ACC_SYNCHRONIZED 标志来隐式实现的关于这点稍后详细分析。下面先来了解一个概念Java对象头这对深入理解synchronized实现原理非常关键。如果对上面的执行结果还有疑问也先不用急我们先来了解Synchronized的原理再回头上面的问题就一目了然了。我们先通过反编译下面的代码来看看Synchronized是如何实现对代码块进行同步的public class SynchronizedDemo { public void method() { synchronized (this) { System.out.println(Method 1 start); } } }反编译结果image.png关于这两条指令的作用我们直接参考JVM规范中描述monitorenter Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:• If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.• If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.• If another thread already owns the monitor associated with objectref, the thread blocks until the monitors entry count is zero, then tries again to gain ownership.这段话的大概意思为每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态线程执行monitorenter指令时尝试获取monitor的所有权过程如下如果monitor的进入数为0则该线程进入monitor然后将进入数设置为1该线程即为monitor的所有者。如果线程已经占有该monitor只是重新进入则进入monitor的进入数加1.如果其他线程已经占用了monitor则该线程进入阻塞状态直到monitor的进入数为0再重新尝试获取monitor的所有权。monitorexitThe thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.这段话的大概意思为执行monitorexit的线程必须是objectref所对应的monitor的所有者。指令执行时monitor的进入数减1如果减1后进入数为0那线程退出monitor不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。通过这两段描述我们应该能很清楚的看出Synchronized的实现原理Synchronized的语义底层是通过一个monitor的对象来完成其实wait/notify等方法也依赖于monitor对象这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法否则会抛出java.lang.IllegalMonitorStateException的异常的原因。我们再来看一下同步方法的反编译结果源代码public class SynchronizedMethod { public synchronized void method() { System.out.println(Hello World!); } } 反编译结果image.png从反编译的结果来看方法的同步并没有通过指令monitorenter和monitorexit来完成(理论上其实也可以通过这两条指令来实现)不过相对于普通方法其常量池中多了ACC_SYNCHRONIZED标示符。JVM就是根据该标示符来实现方法的同步的当方法调用时调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置如果设置了执行线程将先获取monitor获取成功之后才能执行方法体方法执行完后再释放monitor。在方法执行期间其他任何线程都无法再获得同一个monitor对象。 其实本质上没有区别只是方法的同步是一种隐式的方式来实现无需通过字节码来完成。关于synchronized 可能需要了解的关键点synchronized的可重入性从互斥锁的设计上来说当一个线程试图操作一个由其他线程持有的对象锁的临界资源时将会处于阻塞状态但当一个线程再次请求自己持有对象锁的临界资源时这种情况属于重入锁请求将会成功在java中synchronized是基于原子性的内部锁机制是可重入的因此在一个线程调用synchronized方法的同时在其方法体内部调用该对象另一个synchronized方法也就是说一个线程得到一个对象锁后再次请求该对象锁是允许的这就是synchronized的可重入性。如下public class AccountingSync implements Runnable{ static AccountingSync instancenew AccountingSync(); static int i0; static int j0; Override public void run() { for(int j0;j1000000;j){ //this,当前实例对象锁 synchronized(this){ i; increase();//synchronized的可重入性 } } } public synchronized void increase(){ j; } public static void main(String[] args) throws InterruptedException { Thread t1new Thread(instance); Thread t2new Thread(instance); t1.start();t2.start(); t1.join();t2.join(); System.out.println(i); }}正如代码所演示的在获取当前实例对象锁后进入synchronized代码块执行同步代码并在代码块中调用了当前实例对象的另外一个synchronized方法再次请求当前实例锁时将被允许进而执行方法体代码这就是重入锁最直接的体现需要特别注意另外一种情况当子类继承父类时子类也是可以通过可重入锁调用父类的同步方法。注意由于synchronized是基于monitor实现的因此每次重入monitor中的计数器仍会加1。线程中断与synchronized线程中断正如中断二字所表达的意义在线程运行(run方法)中间打断它在Java中提供了以下3个有关线程中断的方法//中断线程(实例方法)public void Thread.interrupt();//判断线程是否被中断(实例方法)public boolean Thread.isInterrupted();//判断是否被中断并清除当前中断状态(静态方法)public static boolean Thread.interrupted();当一个线程处于被阻塞状态或者试图执行一个阻塞操作时使用Thread.interrupt()方式中断该线程注意此时将会抛出一个InterruptedException的异常同时中断状态将会被复位(由中断状态改为非中断状态)如下代码将演示该过程public class InterruputSleepThread3 { public static void main(String[] args) throws InterruptedException { Thread t1 new Thread() { Override public void run() { //while在try中通过异常中断就可以退出run循环 try { while (true) { //当前线程处于阻塞状态异常必须捕捉处理无法往外抛出 TimeUnit.SECONDS.sleep(2); } } catch (InterruptedException e) { System.out.println(Interruted When Sleep); boolean interrupt this.isInterrupted(); //中断状态被复位 System.out.println(interrupt:interrupt); } } }; t1.start(); TimeUnit.SECONDS.sleep(2); //中断处于阻塞状态的线程 t1.interrupt(); /** * 输出结果: Interruted When Sleep interrupt:false */ }}如上述代码所示我们创建一个线程并在线程中调用了sleep方法从而使用线程进入阻塞状态启动线程后调用线程实例对象的interrupt方法中断阻塞异常并抛出InterruptedException异常此时中断状态也将被复位。这里有些人可能会诧异为什么不用Thread.sleep(2000);而是用TimeUnit.SECONDS.sleep(2);其实原因很简单前者使用时并没有明确的单位说明而后者非常明确表达秒的单位事实上后者的内部实现最终还是调用了Thread.sleep(2000);但为了编写的代码语义更清晰建议使用TimeUnit.SECONDS.sleep(2);的方式注意TimeUnit是个枚举类型。ok~除了阻塞中断的情景我们还可能会遇到处于运行期且非阻塞的状态的线程这种情况下直接调用Thread.interrupt()中断线程是不会得到任响应的如下代码将无法中断非阻塞状态下的线程public class InterruputThread { public static void main(String[] args) throws InterruptedException { Thread t1new Thread(){ Override public void run(){ while(true){ System.out.println(未被中断); } } }; t1.start(); TimeUnit.SECONDS.sleep(2); t1.interrupt(); /** * 输出结果(无限执行): 未被中断 未被中断 未被中断 ...... */ }}虽然我们调用了interrupt方法但线程t1并未被中断因为处于非阻塞状态的线程需要我们手动进行中断检测并结束程序改进后代码如下public class InterruputThread { public static void main(String[] args) throws InterruptedException { Thread t1new Thread(){ Override public void run(){ while(true){ //判断当前线程是否被中断 if (this.isInterrupted()){ System.out.println(线程中断); break; } } System.out.println(已跳出循环,线程中断!); } }; t1.start(); TimeUnit.SECONDS.sleep(2); t1.interrupt(); /** * 输出结果: 线程中断 已跳出循环,线程中断! */ }}是的我们在代码中使用了实例方法isInterrupted判断线程是否已被中断如果被中断将跳出循环以此结束线程。综合所述可以简单总结一下中断两种情况一种是当线程处于阻塞状态或者试图执行一个阻塞操作时我们可以使用实例方法interrupt()进行线程中断执行中断操作后将会抛出interruptException异常(该异常必须捕捉无法向外抛出)并将中断状态复位另外一种是当线程处于运行状态时我们也可调用实例方法interrupt()进行线程中断但同时必须手动判断中断状态并编写中断线程的代码(其实就是结束run方法体的代码)。有时我们在编码时可能需要兼顾以上两种情况那么就可以如下编写public void run(){ try { //判断当前线程是否已中断,注意interrupted方法是静态的,执行后会对中断状态进行复位 while (!Thread.interrupted()) { TimeUnit.SECONDS.sleep(2); } } catch (InterruptedException e) { }}中断与synchronized事实上线程的中断操作对于正在等待获取的锁对象的synchronized方法或者代码块并不起作用也就是对于synchronized来说如果一个线程在等待锁那么结果只有两种要么它获得这把锁继续执行要么它就保存等待即使调用中断线程的方法也不会生效。演示代码如下public class SynchronizedBlocked implements Runnable{ public synchronized void f() { System.out.println(Trying to call f()); while(true) // Never releases lock Thread.yield(); } /** * 在构造器中创建新线程并启动获取对象锁 */ public SynchronizedBlocked() { //该线程已持有当前实例锁 new Thread() { public void run() { f(); // Lock acquired by this thread } }.start(); } public void run() { //中断判断 while (true) { if (Thread.interrupted()) { System.out.println(中断线程!!); break; } else { f(); } } } public static void main(String[] args) throws InterruptedException { SynchronizedBlocked sync new SynchronizedBlocked(); Thread t new Thread(sync); //启动后调用f()方法,无法获取当前实例锁处于等待状态 t.start(); TimeUnit.SECONDS.sleep(1); //中断线程,无法生效 t.interrupt(); }}我们在SynchronizedBlocked构造函数中创建一个新线程并启动获取调用f()获取到当前实例锁由于SynchronizedBlocked自身也是线程启动后在其run方法中也调用了f()但由于对象锁被其他线程占用导致t线程只能等到锁此时我们调用了t.interrupt();但并不能中断线程。等待唤醒机制与synchronized所谓等待唤醒机制本篇主要指的是notify/notifyAll和wait方法在使用这3个方法时必须处于synchronized代码块或者synchronized方法中否则就会抛出IllegalMonitorStateException异常这是因为调用这几个方法前必须拿到当前对象的监视器monitor对象也就是说notify/notifyAll和wait方法依赖于monitor对象在前面的分析中我们知道monitor 存在于对象头的Mark Word 中(存储monitor引用指针)而synchronized关键字可以获取 monitor 这也就是为什么notify/notifyAll和wait方法必须在synchronized代码块或者synchronized方法调用的原因。synchronized (obj) { obj.wait(); obj.notify(); obj.notifyAll(); }需要特别理解的一点是与sleep方法不同的是wait方法调用完成后线程将被暂停但wait方法将会释放当前持有的监视器锁(monitor)直到有线程调用notify/notifyAll方法后方能继续执行而sleep方法只让线程休眠并不释放锁。同时notify/notifyAll方法调用后并不会马上释放监视器锁而是在相应的synchronized(){}/synchronized方法执行结束后才自动释放锁。Java虚拟机对synchronized的优化锁的状态总共有四种无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争锁可以从偏向锁升级到轻量级锁再升级的重量级锁但是锁的升级是单向的也就是说只能从低到高升级不会出现锁的降级关于重量级锁前面我们已详细分析过下面我们将介绍偏向锁和轻量级锁以及JVM的其他优化手段这里并不打算深入到每个锁的实现和转换过程更多地是阐述Java虚拟机所提供的每个锁的核心优化思想毕竟涉及到具体过程比较繁琐如需了解详细过程可以查阅《深入理解Java虚拟机原理》。偏向锁偏向锁是Java 6之后加入的新锁它是一种针对加锁操作的优化手段经过研究发现在大多数情况下锁不仅不存在多线程竞争而且总是由同一线程多次获得因此为了减少同一线程获取锁(会涉及到一些CAS操作,耗时)的代价而引入偏向锁。偏向锁的核心思想是如果一个线程获得了锁那么锁就进入偏向模式此时Mark Word 的结构也变为偏向锁结构当这个线程再次请求锁时无需再做任何同步操作即获取锁的过程这样就省去了大量有关锁申请的操作从而也就提供程序的性能。所以对于没有锁竞争的场合偏向锁有很好的优化效果毕竟极有可能连续多次是同一个线程申请相同的锁。但是对于锁竞争比较激烈的场合偏向锁就失效了因为这样场合极有可能每次申请锁的线程都是不相同的因此这种场合下不应该使用偏向锁否则会得不偿失需要注意的是偏向锁失败后并不会立即膨胀为重量级锁而是先升级为轻量级锁。下面我们接着了解轻量级锁。轻量级锁倘若偏向锁失败虚拟机并不会立即升级为重量级锁它还会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的)此时Mark Word 的结构也变为轻量级锁的结构。轻量级锁能够提升程序性能的依据是“对绝大部分的锁在整个同步周期内都不存在竞争”注意这是经验数据。需要了解的是轻量级锁所适应的场景是线程交替执行同步块的场合如果存在同一时间访问同一锁的场合就会导致轻量级锁膨胀为重量级锁。自旋锁轻量级锁失败后虚拟机为了避免线程真实地在操作系统层面挂起还会进行一项称为自旋锁的优化手段。这是基于在大多数情况下线程持有锁的时间都不会太长如果直接挂起操作系统层面的线程可能会得不偿失毕竟操作系统实现线程之间的切换时需要从用户态转换到核心态这个状态之间的转换需要相对比较长的时间时间成本相对较高因此自旋锁会假设在不久将来当前的线程可以获得锁因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因)一般不会太久可能是50个循环或100循环在经过若干次循环后如果得到锁就顺利进入临界区。如果还不能获得锁那就会将线程在操作系统层面挂起这就是自旋锁的优化方式这种方式确实也是可以提升效率的。最后没办法也就只能升级为重量级锁了。锁消除消除锁是虚拟机另外一种锁的优化这种优化更彻底Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译又称即时编译)通过对运行上下文的扫描去除不可能存在共享资源竞争的锁通过这种方式消除没有必要的锁可以节省毫无意义的请求锁时间如下StringBuffer的append是一个同步方法但是在add方法中的StringBuffer属于一个局部变量并且不会被其他线程所使用因此StringBuffer不可能存在共享资源竞争的情景JVM会自动将其锁消除。重量级锁即synchronized一直等待线程施放锁后才可以拿到资源。本篇的主要参考资料 《Java编程思想》 《深入理解Java虚拟机》 《实战Java高并发程序设计》
http://www.zqtcl.cn/news/637801/

相关文章:

  • 做面包的公司网站alexa世界排名查询
  • 网站备案后下一步做什么263邮箱注册
  • 燕郊网站制作廊坊网站制作网站
  • 开网站建设网站如何做excel预览
  • p2p网站建设方案电商企业有哪些
  • 建设农场网站天元建设集团有限公司法定代表人
  • 论坛网站建设价格百度广告官网
  • 网站开发有哪些语言ps做登录网站
  • 网站怎么做百度关键字搜索国外服务器做网站不能访问
  • 如何选择品牌网站建设做网站容易吧
  • 广州建网站比较有名的公司提升学历英语翻译
  • php网站开发视频教程厦门网站建设公司首选乐振
  • 网站推广项目微信小程序登陆入口
  • 建设部监理协会网站微信公众平台开发微网站
  • 莆田cms建站模板现在可以做网站么
  • windows 建网站湖北省最新消息今天
  • 手机商场网站制作在线看网站源码
  • 云南建设厅网站房地产开发资质做哪一类网站能赚钱
  • 佛山优化网站关键词创作者服务平台
  • python做网站多少钱超级商城系统
  • 网站开发pc端和手机端长沙专业个人做网站哪家好
  • 永州网站建设收费标准天长网站开发
  • 做网站分辨率多少钱装修公司10强排名
  • 营销网站建设818gx在南宁做家教兼职的网站
  • 做杂志模板下载网站网站开发产品经理招聘
  • 深圳网站创建公司小程序代理怎么样
  • 所以免费爱做网站营销网站优化推广
  • 莆田网站制作设计东莞营销专业网站建设
  • joomla建站教程北京做网站ezhixi
  • 自己可以做拼单网站吗建设企业网站有哪些