`
yangmingjiayou
  • 浏览: 112645 次
  • 性别: Icon_minigender_1
  • 来自: 深圳
社区版块
存档分类
最新评论

Java 多线程同步问题的探究 (摘自于网上)

阅读更多

   Java 多线程同步问题的探究 (摘自于网上)

Java 多线程同步问题的探究(一、线程的先来后到)
时间:2010-03-29 20:18    来源:未知    作者:admin

关于线程的同步,一般有以下解决方法:
1. 在需要同步的方法的方法签名中加入synchronized关键字。
2. 使用synchronized块对需要进行同步的代码段进行同步。
3. 使用JDK 5中提供的java.util.concurrent.lock包中的Lock对象。
另外,为了解决多个线程对同一变量进行访问时可能发生的安全性问题,我们不仅可以采用同步机制,更可以通过JDK 1.2中加入的ThreadLocal来保证更好的并发性。
本篇中,将详细的讨论Java多线程同步机制,并对ThreadLocal做出探讨。
大致的目录结构如下:
一、线程的先来后到——问题的提出:为什么要有多线程同步?Java多线程同步的机制是什么?
二、给我一把锁,我能创造一个规矩——传统的多线程同步编程方法有哪些?他们有何异同?
三、Lock来了,大家都让开—— Java并发框架中的Lock详解。
四、协作,互斥下的协作——Java多线程协作(wait、notify、notifyAll)
五、你有我有全都有—— ThreadLocal如何解决并发安全性?
六、总结——Java线程安全的几种方法对比。
一、线程的先来后到
我们来举一个Dirty的例子:某餐厅的卫生间很小,几乎只能容纳一个人如厕。为了保证不受干扰,如厕的人进入卫生间,就要锁上房门。我们可以把卫生间想象成是共享的资源,而众多需要如厕的人可以被视作多个线程。假如卫生间当前有人占用,那么其他人必须等待,直到这个人如厕完毕,打开房门走出来为止。这就好比多个线程共享一个资源的时候,是一定要分出先来后到的。

有人说:那如果我没有这道门会怎样呢?让两个线程相互竞争,谁抢先了,谁就可以先干活,这样多好阿?但是我们知道:如果厕所没有门的话,如厕的人一起涌向厕所,那么必然会发生争执,正常的如厕步骤就会被打乱,很有可能会发生意想不到的结果,例如某些人可能只好被迫在不正确的地方施肥……正是因为有这道门,任何一个单独进入如厕的人都可以顺利的完成他们的如厕过程,而不会被干扰,甚至发生以外的结果。这就是说,如厕的时候要讲究先来后到
那么在Java 多线程程序当中,当多个线程竞争同一个资源的时候,如何能够保证他们不会产生“打架”的情况呢?有人说是使用同步机制。没错,像上面这个例子,就是典型的同步案例,一旦第一位开始如厕,则第二位必须等待第一位结束,才能开始他的如厕过程。一个线程,一旦进入某一过程,必须等待正常的返回,并退出这一过程,下一个线程才能开始这个过程。这里,最关键的就是卫生间的门。其实,卫生间的门担任的是资源锁的角色,只要如厕的人锁上门,就相当于获得了这个锁,而当他打开锁出来以后,就相当于释放了这个锁。
也就是说,多线程的线程同步机制实际上是靠锁的概念来控制的。那么在Java程序当中,锁是如何体现的呢?
让我们从JVM的角度来看看锁这个概念:
在Java程序运行时环境中,JVM需要对两类线程共享的数据进行协调:
1)保存在堆中的实例变量
2)保存在方法区中的类变量
这两类数据是被所有线程共享的。
(程序不需要协调保存在Java 栈当中的数据。因为这些数据是属于拥有该栈的线程所私有的。)
在java虚拟机中,每个对象和类在逻辑上都是和一个监视器相关联的。
对于对象来说,相关联的监视器保护对象的实例变量。
对于类来说,监视器保护类的类变量。
(如果一个对象没有实例变量,或者一个类没有变量,相关联的监视器就什么也不监视。)
为了实现监视器的排他性监视能力,java虚拟机为每一个对象和类都关联一个锁。代表任何时候只允许一个线程拥有的特权。线程访问实例变量或者类变量不需锁。
但是如果线程获取了锁,那么在它释放这个锁之前,就没有其他线程可以获取同样数据的锁了。(锁住一个对象就是获取对象相关联的监视器)
类锁实际上用对象锁来实现。当虚拟机装载一个class文件的时候,它就会创建一个java.lang.Class类的实例。当锁住一个对象的时候,实际上锁住的是那个类的Class对象。

一个线程可以多次对同一个对象上锁。对于每一个对象,java虚拟机维护一个加锁计数器,线程每获得一次该对象,计数器就加1,每释放一次,计数器就减 1,当计数器值为0时,锁就被完全释放了。
java编程人员不需要自己动手加锁,对象锁是java虚拟机内部使用的。
在java程序中,只需要使用synchronized块或者synchronized方法就可以标志一个监视区域。当每次进入一个监视区域时,java 虚拟机都会自动锁上对象或者类。
看到这里,我想你们一定都疲劳了吧?o(∩_∩)o...哈哈。让我们休息一下,但是在这之前,请你们一定要记着:
当一个有限的资源被多个线程共享的时候,为了保证对共享资源的互斥访问,我们一定要给他们排出一个先来后到。而要做到这一点,对象锁在这里起着非常重要的作用。
如果你想知道更多细节,请接着看本系列的第二篇吧。
Java 多线程同步问题的探究(二、给我一把锁,我能创造一个规矩)
时间:2010-03-29 20:20    来源:未知    作者:admin
核心提示:在上一篇中,我们讲到了多线程是如何处理共享资源的,以及保证他们对资源进行互斥访问所依赖的重要机制:对象锁。 本篇中,我们来看一看传统的同步实现方式以及这背后的原理。 很多人都知道,在Java多线程编程中,有一个重要的关键字,synchronized。但是很多
在上一篇中,我们讲到了多线程是如何处理共享资源的,以及保证他们对资源进行互斥访问所依赖的重要机制:对象锁。
本篇中,我们来看一看传统的同步实现方式以及这背后的原理。
很多人都知道,在Java多线程编程中,有一个重要的关键字,synchronized。但是很多人看到这个东西会感到困惑:“都说同步机制是通过对象锁来实现的,但是这么一个关键字,我也看不出来Java程序锁住了哪个对象阿?“没错,我一开始也是对这个问题感到困惑和不解。不过还好,我们有下面的这个例程:
  public class ThreadTest extends Thread {
      private int threadNo;
      public ThreadTest(int threadNo) {
          this.threadNo = threadNo;
     }
      public static void main(String[] args) throws Exception {

         for (int i = 1; i < 10; i++) {
             new ThreadTest(i).start();
             Thread.sleep(1);
         }
    }
 
     @Override
     public synchronized void run() {
        for (int i = 1; i < 10000; i++) {
             System.out.println("No." + threadNo + ":" + i);
         }
     }
 }

这个程序其实就是让10个线程在控制台上数数,从1数到9999。理想情况下,我们希望看到一个线程数完,然后才是另一个线程开始数数。但是这个程序的执行过程告诉我们,这些线程还是乱糟糟的在那里抢着报数,丝毫没有任何规矩可言。
但是细心的读者注意到:run方法还是加了一个synchronized关键字的,按道理说,这些线程应该可以一个接一个的执行这个run方法才对阿。
但是通过上一篇中,我们提到的,对于一个成员方法加synchronized关键字,这实际上是以这个成员方法所在的对象本身作为对象锁。在本例中,就是以ThreadTest类的一个具体对象,也就是该线程自身作为对象锁的。一共十个线程,每个线程持有自己 线程对象的那个对象锁。这必然不能产生同步的效果。换句话说,如果要对这些线程进行同步,那么这些线程所持有的对象锁应当是共享且唯一的!
我们来看下面的例程:

public class ThreadTest2 extends Thread {
  private int threadNo;
   private String lock;

  public ThreadTest2(int threadNo, String lock) {
   this.threadNo = threadNo;
   this.lock = lock;
 }


  public static void main(String[] args) throws Exception {
 String lock = new String("lock");
  for (int i = 1; i < 10; i++) {
  new ThreadTest2(i, lock).start();
  Thread.sleep(1);
 }
 }
  public void run() {
  synchronized (lock) {
  for (int i = 1; i < 10000; i++) {
               System.out.println("No." + threadNo + ":" + i);
         }
       }
      }
  }

 

我们注意到,该程序通过在main方法启动10个线程之前,创建了一个String类型的对象。并通过ThreadTest2的构造函数,将这个对象赋值给每一个ThreadTest2线程对象中的私有变量lock。根据Java方法的传值特点,我们知道,这些线程的lock变量实际上指向的是堆内存中的同一个区域,即存放main函数中的lock变量的区域。
程序将原来run方法前的synchronized关键字去掉,换用了run方法中的一个synchronized块来实现。这个同步块的对象锁,就是 main方法中创建的那个String对象。换句话说,他们指向的是同一个String类型的对象,对象锁是共享且唯一的!
于是,我们看到了预期的效果:10个线程不再是争先恐后的报数了,而是一个接一个的报数。
再来看下面的例程:

 public class ThreadTest3 extends Thread {
   private int threadNo;
   private String lock;
   public ThreadTest3(int threadNo) {
   this.threadNo = threadNo;
    }  
 public static void main(String[] args) throws Exception {
    //String lock = new String("lock");
     for (int i = 1; i < 20; i++) {
             new ThreadTest3(i).start();
             Thread.sleep(1);
        }
     }

public static synchronized void abc(int threadNo) {
      for (int i = 1; i < 10000; i++) {
    System.out.println("No." + threadNo + ":" + i); 
    }
  }

 public void run() {
 abc(threadNo);
  }
}


细心的读者发现了:这段代码没有使用main方法中创建的String对象作为这10个线程的线程锁。而是通过在run方法中调用本线程中一个静态的同步方法abc而实现了线程的同步。我想看到这里,你们应该很困惑:这里synchronized静态方法是用什么来做对象锁的呢?
我们知道,对于同步静态方法,对象锁就是该静态放发所在的类的Class实例,由于在JVM中,所有被加载的类都有唯一的类对象,具体到本例,就是唯一的 ThreadTest3.class对象。不管我们创建了该类的多少实例,但是它的类实例仍然是一个!
这样我们就知道了:

1、对于同步的方法或者代码块来说,必须获得对象锁才能够进入同步方法或者代码块进行操作;
2、如果采用method级别的同步,则对象锁即为method所在的对象,如果是静态方法,对象锁即指method所在的Class对象(唯一);
3、对于代码块,对象锁即指synchronized(abc)中的abc;
4、因为第一种情况,对象锁即为每一个线程对象,因此有多个,所以同步失效,第二种共用同一个对象锁lock,因此同步生效,第三个因为是
static因此对象锁为ThreadTest3的class 对象,因此同步生效。

 
如上述正确,则同步有两种方式,同步块和同步方法(为什么没有wait和notify?这个我会在补充章节中做出阐述)
如果是同步代码块,则对象锁需要编程人员自己指定,一般有些代码为synchronized(this)只有在单态模式才生效;
(本类的实例有且只有一个)

如果是同步方法,则分静态和非静态两种。


静态方法则一定会同步,非静态方法需在单例模式才生效,推荐用静态方法(不用担心是否单例)。
所以说,在Java多线程编程中,最常见的synchronized关键字实际上是依靠对象锁的机制来实现线程同步的。
我们似乎可以听到synchronized在向我们说:“给我一把锁,我能创造一个规矩”。

下一篇中,我们将看到JDK 5提供的新的同步机制,也就是大名鼎鼎的Doug Lee提供的Java Concurrency框架。
Java 多线程同步问题的探究(三、Lock来了,大家都让开【1. 认识重入锁】)
文章分类:Java编程
在上一节中,
我们已经了解了Java多线程编程中常用的关键字synchronized,以及与之相关的对象锁机制。这一节中,让我们一起来认识JDK 5中新引入的并发框架中的锁机制。

我想很多购买了《Java程序员面试宝典》之类图书的朋友一定对下面这个面试题感到非常熟悉:

问:请对比synchronized与java.util.concurrent.locks.Lock 的异同。
答案:主要相同点:Lock能完成synchronized所实现的所有功能
     主要不同点:Lock有比synchronized更精确的线程语义和更好的性能。synchronized会自动释放锁,而Lock一定要求程序员手工释放,并且必须在finally从句中释放。

恩,让我们先鄙视一下应试教育。

言归正传,我们先来看一个多线程程序。它使用多个线程对一个Student对象进行访问,改变其中的变量值。我们首先用传统的synchronized 机制来实现它:
public   class  ThreadDemo  implements  Runnable {
     class  Student {
         private   int  age  =   0 ;
         public   int  getAge() {
             return  age;
        }
         public   void  setAge( int  age) {
             this .age  =  age;
        }
    }
   
    Student student  =   new  Student();
     int  count  =   0 ;
     public   static   void  main(String[] args) {
        ThreadDemo td  =   new  ThreadDemo();
        Thread t1  =   new  Thread(td,  " a " );
        Thread t2  =   new  Thread(td,  " b " );
        Thread t3  =   new  Thread(td,  " c " );
        t1.start();
        t2.start();
        t3.start();
    }
     public   void  run() {
        accessStudent();
    }

     public   void  accessStudent() {
        String currentThreadName  =  Thread.currentThread().getName();
        System.out.println(currentThreadName  +   "  is running! " );
         synchronized  ( this ) { // (1)使用同一个ThreadDemo对象作为同步锁
            System.out.println(currentThreadName  +   "  got lock1@Step1! " );
             try  {
                count ++ ;
                Thread.sleep( 5000 );
            }  catch  (Exception e) {
                e.printStackTrace();
            }  finally  {
                System.out.println(currentThreadName  +   "  first Reading count: "   +  count);
            }

        }
      
        System.out.println(currentThreadName  +   "  release lock1@Step1! " );
         synchronized  ( this ) { // (2)使用同一个ThreadDemo对象作为同步锁
            System.out.println(currentThreadName  +   "  got lock2@Step2! " );
             try  {
                Random random  =   new  Random();
                 int  age  =  random.nextInt( 100 );
                System.out.println( " thread  "   +  currentThreadName  +   "  set age to: "   +  age);
                 this .student.setAge(age);
                System.out.println( " thread  "   +  currentThreadName  +   "  first  read age is: "   +   this .student.getAge());
                Thread.sleep( 5000 );
            }  catch  (Exception ex) {
                ex.printStackTrace();
            }  finally {
                System.out.println( " thread  "   +  currentThreadName  +   "  second read age is: "   +   this .student.getAge());
            }

        }
        System.out.println(currentThreadName  +   "  release lock2@Step2! " );
    }
}
转载注明出处:http://x- spirit.iteye.com/、http: //www.blogjava.net/zhangwei217245/
运行结果:
a is running!
a got lock1@Step1!
b is running!
c is running!
a first Reading count: 1
a release lock1@Step1!
a got lock2@Step2!
thread a set age to: 76
thread a first  read age is: 76
thread a second read age is: 76
a release lock2@Step2!
c got lock1@Step1!
c first Reading count: 2
c release lock1@Step1!
c got lock2@Step2!
thread c set age to: 35
thread c first  read age is: 35
thread c second read age is: 35
c release lock2@Step2!
b got lock1@Step1!
b first Reading count: 3
b release lock1@Step1!
b got lock2@Step2!
thread b set age to: 91
thread b first  read age is: 91
thread b second read age is: 91
b release lock2@Step2!
成功生成(总时间: 30  秒)

显然,在这个程序中,由于两段synchronized块使用了同样的对象做为对象锁,所以JVM优先使刚刚释放该锁的线程重新获得该锁。这样,每个线程执行的时间是10秒钟,并且要彻底把两个同步块的动作执行完毕,才能释放对象锁。这样,加起来一共是 30秒。
我想一定有人会说:如果两段synchronized块采用两个不同的对象锁,就可以提高程序的并发性,并且,这两个对象锁应该选择那些被所有线程所共享的对象。

那么好。我们把第二个同步块中的对象锁改为student(此处略去代码,读者自己修改),程序运行结果为:
a is running!
a got lock1@Step1!
b is running!
c is running!
a first Reading count: 1
a release lock1@Step1!
a got lock2@Step2!
thread a set age to: 73
thread a first  read age is: 73
c got lock1@Step1!
thread a second read age is: 73
a release lock2@Step2!
c first Reading count: 2
c release lock1@Step1!
c got lock2@Step2!
thread c set age to: 15
thread c first  read age is: 15
b got lock1@Step1!
thread c second read age is: 15
c release lock2@Step2!
b first Reading count: 3
b release lock1@Step1!
b got lock2@Step2!
thread b set age to: 19
thread b first  read age is: 19
thread b second read age is: 19
b release lock2@Step2!
成功生成(总时间: 21  秒)

从修改后的运行结果来看,显然,由于同步块的对象锁不同了,三个线程的执行顺序也发生了变化。在一个线程释放第一个同步块的同步锁之后,第二个线程就可以进入第一个同步块,而此时,第一个线程可以继续执行第二个同步块。这样,整个执行过程中,有10秒钟的时间是两个线程同时工作的。另外十秒钟分别是第一个线程执行第一个同步块的动作和最后一个线程执行第二个同步块的动作。相比较第一个例程,整个程序的运行时间节省了1/3。细心的读者不难总结出优化前后的执行时间比例公式:(n+1)/2n,其中n为线程数。如果线程数趋近于正无穷,则程序执行效率的提高会接近50%。而如果一个线程的执行阶段被分割成m个 synchronized块,并且每个同步块使用不同的对象锁,而同步块的执行时间恒定,则执行时间比例公式可以写作:((m- 1)n+1)/mn那么当m趋于无穷大时,线程数n趋近于无穷大,则程序执行效率的提升几乎可以达到100%。(显然,我们不能按照理想情况下的数学推导来给BOSS发报告,不过通过这样的数学推导,至少我们看到了提高多线程程序并发性的一种方案,而这种方案至少具备数学上的可行性理论支持。)
转载注明出处:http://x- spirit.iteye.com/、http: //www.blogjava.net/zhangwei217245/
可见,使用不同的对象锁,在不同的同步块中完成任务,可以使性能大大提升。

很多人看到这不禁要问:这和新的Lock框架有什么关系?

别着急。我们这就来看一看。

synchronized块的确不错,但是他有一些功能性的限制:
1. 它无法中断一个正在等候获得锁的线程,也无法通过投票得到锁,如果不想等下去,也就没法得到锁。
2.synchronized 块对于锁的获得和释放是在相同的堆栈帧中进行的。多数情况下,这没问题(而且与异常处理交互得很好),但是,确实存在一些更适合使用非块结构锁定的情况。
转载注明出处:http://x- spirit.iteye.com/、http: //www.blogjava.net/zhangwei217245/
java.util.concurrent.lock 中的 Lock 框架是锁定的一个抽象,它允许把锁定的实现作为 Java 类,而不是作为语言的特性来实现。这就为 Lock 的多种实现留下了空间,各种实现可能有不同的调度算法、性能特性或者锁定语义。

JDK 官方文档中提到:
ReentrantLock是“一个可重入的互斥锁 Lock,它具有与使用 synchronized  方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大。
ReentrantLock 将由最近成功获得锁,并且还没有释放该锁的线程所拥有。当锁没有被另一个线程所拥有时,调用 lock 的线程将成功获取该锁并返回。如果当前线程已经拥有该锁,此方法将立即返回。可以使用 isHeldByCurrentThread() 和 getHoldCount() 方法来检查此情况是否发生。 ”

简单来说,ReentrantLock有一个与锁相关的获取计数器,如果拥有锁的某个线程再次得到锁,那么获取计数器就加1,然后锁需要被释放两次才能获得真正释放。这模仿了 synchronized 的语义;如果线程进入由线程已经拥有的监控器保护的 synchronized 块,就允许线程继续进行,当线程退出第二个(或者后续) synchronized 块的时候,不释放锁,只有线程退出它进入的监控器保护的第一个 synchronized 块时,才释放锁。
转载注明出处:http://x- spirit.iteye.com/、http: //www.blogjava.net/zhangwei217245/
ReentrantLock  类(重入锁)实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,但是添加了类似锁投票、定时锁等候和可中断锁等候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。(换句话说,当许多线程都想访问共享资源时,JVM 可以花更少的时候来调度线程,把更多时间用在执行线程上。)

我们把上面的例程改造一下:
public   class  ThreadDemo  implements  Runnable {

     class  Student {

         private   int  age  =   0 ;

         public   int  getAge() {
             return  age;
        }

         public   void  setAge( int  age) {
             this .age  =  age;
        }
    }
    Student student  =   new  Student();
     int  count  =   0 ;
    ReentrantLock lock1  =   new  ReentrantLock( false );
    ReentrantLock lock2  =   new  ReentrantLock( false );

     public   static   void  main(String[] args) {
        ThreadDemo td  =   new  ThreadDemo();
         for  ( int  i  =   1 ; i  <=   3 ; i ++ ) {
            Thread t  =   new  Thread(td, i  +   "" );
            t.start();
        }
    }

     public   void  run() {
        accessStudent();
    }

     public   void  accessStudent() {
        String currentThreadName  =  Thread.currentThread().getName();
        System.out.println(currentThreadName  +   "  is running! " );
        lock1.lock(); // 使用重入锁
        System.out.println(currentThreadName  +   "  got lock1@Step1! " );
         try  {
            count ++ ;
            Thread.sleep( 5000 );
        }  catch  (Exception e) {
            e.printStackTrace();
        }  finally  {
            System.out.println(currentThreadName  +   "  first Reading count: "   +  count);
            lock1.unlock();
            System.out.println(currentThreadName  +   "  release lock1@Step1! " );
        }

        lock2.lock(); // 使用另外一个不同的重入锁
        System.out.println(currentThreadName  +   "  got lock2@Step2! " );
         try  {
            Random random  =   new  Random();
             int  age  =  random.nextInt( 100 );
            System.out.println( " thread  "   +  currentThreadName  +   "  set age to: "   +  age);

             this .student.setAge(age);

            System.out.println( " thread  "   +  currentThreadName  +   "  first  read age is: "   +   this .student.getAge());

            Thread.sleep( 5000 );
        }  catch  (Exception ex) {
            ex.printStackTrace();
        }  finally  {
            System.out.println( " thread  "   +  currentThreadName  +   "  second read age is: "   +   this .student.getAge());
            lock2.unlock();
            System.out.println(currentThreadName  +   "  release lock2@Step2! " );
        }

    }
}


从上面这个程序我们看到:
对象锁的获得和释放是由手工编码完成的,所以获得锁和释放锁的时机比使用同步块具有更好的可定制性。并且通过程序的运行结果(运行结果忽略,请读者根据例程自行观察),我们可以发现,和使用同步块的版本相比,结果是相同的。
转载注明出处:http://x- spirit.iteye.com/、http: //www.blogjava.net/zhangwei217245/
这说明两点问题:
1. 新的ReentrantLock的确实现了和同步块相同的语义功能。而对象锁的获得和释放都可以由编码人员自行掌握。
2. 使用新的ReentrantLock,免去了为同步块放置合适的对象锁所要进行的考量。
3. 使用新的ReentrantLock,最佳的实践就是结合try/finally块来进行。在try块之前使用lock方法,而在finally中使用unlock方法。
转载注明出处:http://x- spirit.iteye.com/、http: //www.blogjava.net/zhangwei217245/
细心的读者又发现了:

在我们的例程中,创建ReentrantLock实例的时候,我们的构造函数里面传递的参数是false。那么如果传递 true又回是什么结果呢?这里面又有什么奥秘呢?

Java 多线程同步问题的探究(四、协作,互斥下的协作——Java多线程协作(wait、notify、notifyAll))
文章分类:Java编程
Java监视器支持两种线程:互斥和协作。

前面我们介绍了采用对象锁和重入锁来实现的互斥。这一篇中,我们来看一看线程的协作。

举个例子:有一家汉堡店举办吃汉堡比赛,决赛时有3个顾客来吃,3个厨师来做,一个服务员负责协调汉堡的数量。为了避免浪费,制作好的汉堡被放进一个能装有10个汉堡的长条状容器中,按照先进先出的原则取汉堡。如果容器被装满,则厨师停止做汉堡,如果顾客发现容器内的汉堡吃完了,就可以拍响容器上的闹铃,提醒厨师再做几个汉堡出来。此时服务员过来安抚顾客,让他等待。而一旦厨师的汉堡做出来,就会让服务员通知顾客,汉堡做好了,让顾客继续过来取汉堡。

这里,顾客其实就是我们所说的消费者,而厨师就是生产者。容器是决定厨师行为的监视器,而服务员则负责监视顾客的行为。

在JVM中,此种监视器被称为等待并唤醒监视器。
转载注明出处:http://x- spirit.iteye.com/、http: //www.blogjava.net/zhangwei217245/
在这种监视器中,一个已经持有该监视器的线程,可以通过调用监视对象的wait方法,暂停自身的执行,并释放监视器,自己进入一个等待区,直到监视器内的其他线程调用了监视对象的notify方法。当一个线程调用唤醒命令以后,它会持续持有监视器,直到它主动释放监视器。而这之后,等待线程会苏醒,其中的一个会重新获得监视器,判断条件状态,以便决定是否继续进入等待状态或者执行监视区域,或者退出。

请看下面的代码:
     public   class  NotifyTest {
        private   String flag  =   " true " ;
     
          class  NotifyThread  extends  Thread{
              public  NotifyThread(String name) {
                  super (name);
            }
              public   void  run() {     
                 try  {
                    sleep( 3000 ); // 推迟3秒钟通知 
              }  catch  (InterruptedException e) {
                    e.printStackTrace();
               }
                    flag  =   " false " ;
                  flag.notify();
           }
       };
   
         class  WaitThread  extends  Thread {
            public  WaitThread(String name) {
                 super (name);
           }
             public   void  run() {
                
                    while  (flag != " false " ) {
                        System.out.println(getName()  +   "  begin waiting! " );
                         long  waitTime  =  System.currentTimeMillis();
                         try  {
                            flag.wait();
                        }  catch  (InterruptedException e) {
                            e.printStackTrace();
                        }
                       waitTime  =  System.currentTimeMillis()  -  waitTime;
                        System.out.println( " wait time : " + waitTime);
                    }
                    System.out.println(getName()  +   "  end waiting! " );
               
            }
        }
   
         public   static   void  main(String[] args)  throws  InterruptedException {
            System.out.println( " Main Thread Run! " );
            NotifyTest test  =   new  NotifyTest();
            NotifyThread notifyThread  = test. new  NotifyThread( " notify01 " );
            WaitThread waitThread01  =  test. new  WaitThread( " waiter01 " );
            WaitThread waitThread02  =  test. new  WaitThread( " waiter02 " );
            WaitThread waitThread03  =  test. new  WaitThread( " waiter03 " );
            notifyThread.start();
            waitThread01.start();
            waitThread02.start();
            waitThread03.start();
        }
    
    } 

转载注明出处:http://x- spirit.iteye.com/、http: //www.blogjava.net/zhangwei217245/
这段代码启动了三个简单的wait线程,当他们处于等待状态以后,试图由一个notify线程来唤醒。
转载注明出处:http://x- spirit.iteye.com/、http: //www.blogjava.net/zhangwei217245/
运行这段程序,你会发现,满屏的java.lang.IllegalMonitorStateException,根本不是你想要的结果。
转载注明出处:http://x- spirit.iteye.com/、http: //www.blogjava.net/zhangwei217245/
请注意以下几个事实:
   1. 任何一个时刻,对象的控制权(monitor)只能被一个线程拥有。
   2. 无论是执行对象的wait、notify还是notifyAll方法,必须保证当前运行的线程取得了该对象的控制权(monitor)。
   3. 如果在没有控制权的线程里执行对象的以上三种方法,就会报java.lang.IllegalMonitorStateException异常。
   4. JVM基于多线程,默认情况下不能保证运行时线程的时序性。

也就是说,当线程在调用某个对象的wait或者notify方法的时候,要先取得该对象的控制权,换句话说,就是进入这个对象的监视器。
转载注明出处:http://x- spirit.iteye.com/、http: //www.blogjava.net/zhangwei217245/
通过前面对同步的讨论,我们知道,要让一个线程进入某个对象的监视器,通常有三种方法:
转载注明出处:http://x- spirit.iteye.com/、http: //www.blogjava.net/zhangwei217245/
1: 执行对象的某个同步实例方法
2: 执行对象对应的同步静态方法
3: 执行对该对象加同步锁的同步块

显然,在上面的例程中,我们用第三种方法比较合适。

于是我们将上面的wait和notify方法调用包在同步块中。
   1 .              synchronized  (flag) {
    2 .                 flag  =   " false " ;
    3 .                 flag.notify();
    4 .             } 

转载注明出处:http://x- spirit.iteye.com/、http: //www.blogjava.net/zhangwei217245/
   1 .              synchronized  (flag) {
    2 .                  while  (flag != " false " ) {
    3 .                     System.out.println(getName()  +   "  begin waiting! " );
    4 .                      long  waitTime  =  System.currentTimeMillis();
    5 .                      try  {
    6 .                         flag.wait();
    7 .                     }  catch  (InterruptedException e) {
    8 .                         e.printStackTrace();
    9 .                     }
   10 .                     waitTime  =  System.currentTimeMillis()  -  waitTime;
   11 .                     System.out.println( " wait time : " + waitTime);
   12 .                 }
   13 .                 System.out.println(getName()  +   "  end waiting! " );
   14 .             } 

 

但是,运行这个程序,我们发现事与愿违。那个非法监视器异常又出现了。。。
转载注明出处:http://x- spirit.iteye.com/、http: //www.blogjava.net/zhangwei217245/
我们注意到,针对flag的同步块中,我们实际上已经更改了flag对对象的引用: flag="false";
转载注明出处:http://x- spirit.iteye.com/、http: //www.blogjava.net/zhangwei217245/
显然,这样一来,同步块也无能为力了,因为我们根本不是针对唯一的一个对象在进行同步。

我们不妨将flag封装到JavaBean或者数组中去,这样用JavaBean对象或者数组对象进行同步,就可以达到既能修改里面参数又不耽误同步的目的。
1 .  private    String flag[]  =  { " true " };
 
   1 .          synchronized  (flag) {
    2 .             flag[ 0 ]  =   " false " ;
    3 .             flag.notify();
    4 .         } 

   1 .                   synchronized  (flag) {
    2 .                 flag[ 0 ]  =   " false " ;
    3 .                 flag.notify();
    4 .             } synchronized  (flag) {
    5 .                  while  (flag[ 0 ] != " false " ) {
    6 .                     System.out.println(getName()  +   "  begin waiting! " );
    7 .                      long  waitTime  =  System.currentTimeMillis();
    8 .                      try  {
    9 .                         flag.wait();
   10 .                         
   11 .                     }  catch  (InterruptedException e) {
   12 .                         e.printStackTrace();
   13 .                     } 


运行这个程序,看不到异常了。但是仔细观察结果,貌似只有一个线程被唤醒。利用jconsole等工具查看线程状态,发现的确还是有两个线程被阻塞的。这是为啥呢?

程序中使用了flag.notify()方法。只能是随机的唤醒一个线程。我们可以改用flag.notifyAll()方法。这样,所有被阻塞的线程都会被唤醒了。

最终代码请读者自己修改,这里不再赘述。

好了,亲爱的读者们,让我们回到开篇提到的汉堡店大赛问题当中去,来看一看厨师、服务生和顾客是怎么协作进行这个比赛的。

首先我们构造故事中的三个次要对象:汉堡包、存放汉堡包的容器、服务生
public   class  Waiter { // 服务生,这是个配角,不需要属性。
}
 
    class  Hamberg {
         // 汉堡包
         private   int  id; // 汉堡编号
         private  String cookerid; // 厨师编号
         public  Hamberg( int  id, String cookerid){
             this .id  =  id;
             this .cookerid  =  cookerid;
            System.out.println( this .toString() + " was made! " );
        }

        @Override
         public  String toString() {
             return   " # " + id + "  by  " + cookerid;
        }
       
    }
 
    class  HambergFifo {
         // 汉堡包容器
        List < Hamberg >  hambergs  =   new  ArrayList < Hamberg > (); // 借助ArrayList来存放汉堡包
         int  maxSize  =   10 ; // 指定容器容量

         // 放入汉堡
         public   < T  extends  Hamberg >   void  push(T t) {
            hambergs.add(t);
        }

         // 取出汉堡
         public  Hamberg pop() {
            Hamberg h  =  hambergs.get( 0 );
            hambergs.remove( 0 );
             return  h;
        }

         // 判断容器是否为空
         public   boolean  isEmpty() {
             return  hambergs.isEmpty();
        }

         // 判断容器内汉堡的个数
         public   int  size() {
             return  hambergs.size();
        }

         // 返回容器的最大容量
         public   int  getMaxSize() {
             return   this .maxSize;
        }
    }

接下来我们构造厨师对象:
    class  Cooker  implements  Runnable {
         // 厨师要面对容器
        HambergFifo pool;
         // 还要面对服务生
        Waiter waiter;

         public  Cooker(Waiter waiter, HambergFifo hambergStack) {
             this .pool  =  hambergStack;
             this .waiter  =  waiter;
        }
         // 制造汉堡
         public   void  makeHamberg() {
             // 制造的个数
             int  madeCount  =   0 ;
             // 因为容器满,被迫等待的次数
             int  fullFiredCount  =   0 ;
             try  {
               
                 while  ( true ) {
                    // 制作汉堡前的准备工作
                    Thread.sleep( 1000 );
                     if  (pool.size()  <  pool.getMaxSize()) {
                         synchronized  (waiter) {
                            // 容器未满,制作汉堡,并放入容器。
                            pool.push( new  Hamberg( ++ madeCount,Thread.currentThread().getName()));
                            // 说出容器内汉堡数量
                            System.out.println(Thread.currentThread().getName()  +   " : There are  "   +  pool.size()  +   "  Hambergs in all " );
                            // 让服务生通知顾客,有汉堡可以吃了
                            waiter.notifyAll();
                            System.out.println( " ### Cooker: waiter.notifyAll() : Hi! Customers, we got some new Hambergs! " );
                        }
                    }  else  {
                         synchronized  (pool) {
                             if  (fullFiredCount ++   <   10 ) {
                                // 发现容器满了,停止做汉堡的尝试。
                                System.out.println(Thread.currentThread().getName()  +   " : Hamberg Pool is Full, Stop making hamberg " );
                                System.out.println( " ### Cooker: pool.wait() " );
                                // 汉堡容器的状况使厨师等待
                                pool.wait();
                            }  else  {
                                 return ;
                            }
                        }

                    }
                   
                    // 做完汉堡要进行收尾工作,为下一次的制作做准备。
                    Thread.sleep( 1000 );

                }
            }  catch  (Exception e) {
                madeCount -- ;
                e.printStackTrace();
            }
        }

         public   void  run() {

            makeHamberg();

        }
    }

接下来,我们构造顾客对象:
    class  Customer  implements  Runnable {
         // 顾客要面对服务生
        Waiter waiter;
         // 也要面对汉堡包容器
        HambergFifo pool;
         // 想要记下自己吃了多少汉堡
         int  ateCount  =   0 ;
         // 吃每个汉堡的时间不尽相同
         long  sleeptime;
         // 用于产生随机数
        Random r  =   new  Random();

         public  Customer(Waiter waiter, HambergFifo pool) {
             this .waiter  =  waiter;
             this .pool  =  pool;
        }

         public   void  run() {

             while  ( true ) {

                 try  {
                     // 取汉堡
                    getHamberg();
                     // 吃汉堡
                    eatHamberg();
                }  catch  (Exception e) {
                     synchronized  (waiter) {
                        System.out.println(e.getMessage());
                         // 若取不到汉堡,要和服务生打交道
                         try  {
                            System.out.println( " ### Customer: waiter.wait(): Sorry, Sir, there is no hambergs left, please wait! " );
                            System.out.println(Thread.currentThread().getName()  +   " : OK, Waiting for new hambergs " );
                             // 服务生安抚顾客,让他等待。
                            waiter.wait();
                             continue ;
                        }  catch  (InterruptedException ex) {
                            ex.printStackTrace();
                        }
                    }
                }
            }
        }

         private   void  eatHamberg() {
             try  {
                 // 吃每个汉堡的时间不等
                sleeptime  =  Math.abs(r.nextInt( 3000 ))  *   5 ;
                System.out.println(Thread.currentThread().getName()  +   " : I'm eating the hamberg for  "   +  sleeptime  +   "  milliseconds " );
               
                Thread.sleep(sleeptime);
            }  catch  (Exception e) {
                e.printStackTrace();
            }
        }

         private   void  getHamberg()  throws  Exception {
            Hamberg hamberg  =   null ;

             synchronized  (pool) {
                 try  {
                     // 在容器内取汉堡
                    hamberg  =  pool.pop();

                    ateCount ++ ;
                    System.out.println(Thread.currentThread().getName()  +   " : I Got  "   +  ateCount  +   "  Hamberg  "   +  hamberg);
                    System.out.println(Thread.currentThread().getName()  +   " : There are still  "   +  pool.size()  +   "  hambergs left " );


                }  catch  (Exception e) {
                    pool.notifyAll();
                    System.out.println( " ### Customer: pool.notifyAll() " );
                     throw   new  Exception(Thread.currentThread().getName()  +   " : OH MY GOD!!!! No hambergs left, Waiter![Ring the bell besides the hamberg pool] " );

                }
            }
        }
    }

转载注明出处:http://x- spirit.iteye.com/、http: //www.blogjava.net/zhangwei217245/
最后,我们构造汉堡店,让这个故事发生:
转载注明出处:http://x- spirit.iteye.com/、http: //www.blogjava.net/zhangwei217245/
public   class  HambergShop {

    Waiter waiter  =   new  Waiter();
    HambergFifo hambergPool  =   new  HambergFifo();
    Customer c1  =   new  Customer(waiter, hambergPool);
    Customer c2  =   new  Customer(waiter, hambergPool);
    Customer c3  =   new  Customer(waiter, hambergPool);
    Cooker cooker  =   new  Cooker(waiter, hambergPool);

     public   static   void  main(String[] args) {
        HambergShop hambergShop  =   new  HambergShop();
        Thread t1  =   new  Thread(hambergShop.c1,  " Customer 1 " );
        Thread t2  =   new  Thread(hambergShop.c2,  " Customer 2 " );
        Thread t3  =   new  Thread(hambergShop.c3,  " Customer 3 " );
        Thread t4  =   new  Thread(hambergShop.cooker,  " Cooker 1 " );
        Thread t5  =   new  Thread(hambergShop.cooker,  " Cooker 2 " );
        Thread t6  =   new  Thread(hambergShop.cooker,  " Cooker 3 " );
        t4.start();
        t5.start();
        t6.start();
         try  {
            Thread.sleep( 10000 );
        }  catch  (Exception e) {
        }

        t1.start();
        t2.start();
        t3.start();
    }
}

运行这个程序吧,然后你会看到我们汉堡店的比赛进行的很好,只是不知道那些顾客是不是会被撑到。。。
 
读到这里,有的读者可能会想到前面介绍的重入锁ReentrantLock。有的读者会问:如
Java 多线程同步问题的探究(五、你有我有全都有—— ThreadLocal如何解决并发安全性?)
文章分类:Java编程
前面我们介绍了Java当中多个线程抢占一个共享资源的问题。但不论是同步还是重入锁,都不能实实在在的解决资源紧缺的情况,这些方案只是靠制定规则来约束线程的行为,让它们不再拼命的争抢,而不是真正从实质上解决他们对资源的需求。

在JDK 1.2当中,引入了java.lang.ThreadLocal。它为我们提供了一种全新的思路来解决线程并发的问题。但是他的名字难免让我们望文生义:本地线程?

什么是本地线程?
本地线程开玩笑的说:不要迷恋哥,哥只是个传说。

其实ThreadLocal并非Thread at Local,而是LocalVariable in a Thread。

根据WikiPedia上的介绍,ThreadLocal其实是源于一项多线程技术,叫做Thread Local Storage,即线程本地存储技术。不仅仅是Java,在C++、C#、.NET、Python、Ruby、Perl等开发平台上,该技术都已经得以实现。

当使用ThreadLocal维护变量时,它会为每个使用该变量的线程提供独立的变量副本。也就是说,他从根本上解决的是资源数量的问题,从而使得每个线程持有相对独立的资源。这样,当多个线程进行工作的时候,它们不需要纠结于同步的问题,于是性能便大大提升。但资源的扩张带来的是更多的空间消耗,ThreadLocal就是这样一种利用空间来换取时间的解决方案。

说了这么多,来看看如何正确使用ThreadLocal。

通过研究JDK文档,我们知道,ThreadLocal中有几个重要的方法:get()、set()、remove()、initailValue(),对应的含义分别是:
返回此线程局部变量的当前线程副本中的值、将此线程局部变量的当前线程副本中的值设置为指定值、移除此线程局部变量当前线程的值、返回此线程局部变量的当前线程的“初始值”。
转载注明出处:http://x- spirit.iteye.com/、http: //www.blogjava.net/zhangwei217245/
还记得我们在第三篇的上半节引出的那个例子么?几个线程修改同一个Student对象中的age属性。为了保证这几个线程能够工作正常,我们需要对Student的对象进行同步。
下面我们对这个程序进行一点小小的改造,我们通过继承Thread来实现多线程:
/**
 *
 *  @author  x-spirit
  */
public   class  ThreadDemo3  extends  Thread{

     private  ThreadLocal < Student >  stuLocal  =   new  ThreadLocal < Student > ();

     public  ThreadDemo3(Student stu){
        stuLocal.set(stu);
    }

     public   static   void  main(String[] args) {
        Student stu  =   new  Student();
        ThreadDemo3 td31  =   new  ThreadDemo3(stu);
        ThreadDemo3 td32  =   new  ThreadDemo3(stu);
        ThreadDemo3 td33  =   new  ThreadDemo3(stu);
        td31.start();
        td32.start();
        td33.start();
    }

    @Override
     public   void  run() {
        accessStudent();
    }

     public   void  accessStudent() {

        String currentThreadName  =  Thread.currentThread().getName();
        System.out.println(currentThreadName  +   "  is running! " );
        Random random  =   new  Random();
         int  age  =  random.nextInt( 100 );
        System.out.println( " thread  "   +  currentThreadName  +   "  set age to: "   +  age);
        Student student  =  stuLocal.get();
        student.setAge(age);
        System.out.println( " thread  "   +  currentThreadName  +   "  first  read age is: "   +  student.getAge());
         try  {
            Thread.sleep( 5000 );
        }  catch  (InterruptedException ex) {
            ex.printStackTrace();
        }
        System.out.println( " thread  "   +  currentThreadName  +   "  second read age is: "   +  student.getAge());

    }
}
转载注明出处:http://x- spirit.iteye.com/、http: //www.blogjava.net/zhangwei217245/

貌似这个程序没什么问题。但是运行结果却显示:这个程序中的3个线程会抛出3个空指针异常。读者一定感到很困惑。我明明在构造器当中把Student对象set进了ThreadLocal里面阿,为什么run起来之后居然在调用stuLocal.get()方法的时候得到的是NULL呢?
转载注明出处:http://x- spirit.iteye.com/、http: //www.blogjava.net/zhangwei217245/
带着这个疑问,让我们深入到JDK的代码当中,去一看究竟。
转载注明出处:http://x- spirit.iteye.com/、http: //www.blogjava.net/zhangwei217245/
原来,在ThreadLocal中,有一个内部类叫做ThreadLocalMap。这个ThreadLocalMap并非java.util.Map的一个实现,而是利用java.lang.ref.WeakReference实现的一个键-值对应的数据结构其中,key是ThreadLocal类型,而value是Object类型,我们可以简单的视为HashMap<ThreadLocal,Object>。

而在每一个Thread对象中,都有一个ThreadLocalMap的引用,即Thread.threadLocals。而ThreadLocal的set方法就是首先尝试从当前线程中取得ThreadLocalMap(以下简称Map)对象。如果取到的不为null,则以ThreadLocal对象自身为key,来取Map中的value。如果取不到Map对象,则首先为当前线程创建一个ThreadLocalMap,然后以ThreadLocal对象自身为key,将传入的value放入该Map中。
    ThreadLocalMap getMap(Thread t) {
         return  t.threadLocals;
    }  

     public   void  set(T value) {
        Thread t  =  Thread.currentThread();
        ThreadLocalMap map  =  getMap(t);
         if  (map  !=   null )
            map.set( this , value);
         else
            createMap(t, value);
    }


而get方法则是首先得到当前线程的ThreadLocalMap对象,然后,根据ThreadLocal对象自身,取出相应的value。当然,如果在当前线程中取不到ThreadLocalMap对象,则尝试为当前线程创建ThreadLocalMap对象,并以ThreadLocal对象自身为key,把initialValue()方法产生的对象作为value放入新创建的ThreadLocalMap中。
   public  T get() {
        Thread t  =  Thread.currentThread();
        ThreadLocalMap map  =  getMap(t);
         if  (map  !=   null ) {
            ThreadLocalMap.Entry e  =  map.getEntry( this );
             if  (e  !=   null )
                 return  (T)e.value;
        }
         return  setInitialValue();
    }

     private  T setInitialValue() {
        T value  =  initialValue();
        Thread t  =  Thread.currentThread();
        ThreadLocalMap map  =  getMap(t);
         if  (map  !=   null )
            map.set( this , value);
         else
            createMap(t, value);
         return  value;
    }
  
     protected  T initialValue() {
         return   null ;
    }


这样,我们就明白上面的问题出在哪里:我们在main方法执行期间,试图在调用ThreadDemo3的构造器时向ThreadLocal置入Student对象,而此时,以ThreadLocal对象为key,Student对象为value的Map是被放入当前的活动线程内的。也就是Main线程。而当我们的3个ThreadDemo3线程运行起来以后,调用get()方法,都是试图从当前的活动线程中取得ThreadLocalMap对象,但当前的活动线程显然已经不是Main线程了,于是,程序最终执行了ThreadLocal原生的initialValue()方法,返回了null。
转载注明出处:http://x- spirit.iteye.com/、http: //www.blogjava.net/zhangwei217245/
讲到这里,我想不少朋友一定已经看出来了:ThreadLocal的initialValue()方法是需要被覆盖的。
转载注明出处:http://x- spirit.iteye.com/、http: //www.blogjava.net/zhangwei217245/
于是,ThreadLocal的正确使用方法是:将ThreadLocal以内部类的形式进行继承,并覆盖原来的initialValue()方法,在这里产生可供线程拥有的本地变量值。
这样,我们就有了下面的正确例程:
/**
 *
 *  @author  x-spirit
  */
public   class  ThreadDemo3  extends  Thread{

     private  ThreadLocal < Student >  stuLocal  =   new  ThreadLocal < Student > (){

        @Override
         protected  Student initialValue() {
             return   new  Student();
        }

    };

     public  ThreadDemo3(){
      
    }

     public   static   void  main(String[] args) {
        ThreadDemo3 td31  =   new  ThreadDemo3();
        ThreadDemo3 td32  =   new  ThreadDemo3();
        ThreadDemo3 td33  =   new  ThreadDemo3();
        td31.start();
        td32.start();
        td33.start();
    }

    @Override
     public   void  run() {
        accessStudent();
    }

     public   void  accessStudent() {

        String currentThreadName  =  Thread.currentThread().getName();
        System.out.println(currentThreadName  +   "  is running! " );
        Random random  =   new  Random();
         int  age  =  random.nextInt( 100 );
        System.out.println( " thread  "   +  currentThreadName  +   "  set age to: "   +  age);
        Student student  =  stuLocal.get();
        student.setAge(age);
        System.out.println( " thread  "   +  currentThreadName  +   "  first  read age is: "   +  student.getAge());
         try  {
            Thread.sleep( 5000 );
        }  catch  (InterruptedException ex) {
            ex.printStackTrace();
        }
        System.out.println( " thread  "   +  currentThreadName  +   "  second read age is: "   +  student.getAge());

    }
}


可见,要正确使用ThreadLocal,必须注意以下几点:
转载注明出处:http://x- spirit.iteye.com/、http: //www.blogjava.net/zhangwei217245/
1. 总是对ThreadLocal中的initialValue()方法进行覆盖。
转载注明出处:http://x- spirit.iteye.com/、http: //www.blogjava.net/zhangwei217245/
2. 当使用set()或get()方法时牢记这两个方法是对当

分享到:
评论

相关推荐

    Termux (Android 5.0+).apk.cab

    Termux (Android 5.0+).apk.cab

    基于go、vue开发的堡垒机系统(运维安全审计系统)全部资料+详细文档.zip

    【资源说明】 基于go、vue开发的堡垒机系统(运维安全审计系统)全部资料+详细文档.zip 【备注】 1、该项目是个人高分项目源码,已获导师指导认可通过,答辩评审分达到95分 2、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用! 3、本项目适合计算机相关专业(人工智能、通信工程、自动化、电子信息、物联网等)的在校学生、老师或者企业员工下载使用,也可作为毕业设计、课程设计、作业、项目初期立项演示等,当然也适合小白学习进阶。 4、如果基础还行,可以在此代码基础上进行修改,以实现其他功能,也可直接用于毕设、课设、作业等。 欢迎下载,沟通交流,互相学习,共同进步!

    葡萄城手册,快速上手,灵活报表

    制作报表

    基于C++与Qt的金山培训大作业源码汇总

    本项目为金山培训大作业源码汇总,采用C++与Qt技术构建,包含401个文件,涵盖106个C++源文件、72个头文件、41个PNG图片、27个项目文件以及HTML、JavaScript、CSS等多种文件类型。项目包含四个主要模块:KVector向量库、命令行会议系统、KSvg绘图板和KHttp音乐播放器。尽管最终未能入选,但展现了作者在C++编程和Qt框架应用方面的扎实功底和努力。

    (26408240)STM32F103+四个VL53L0代码(2020新).zip

    内容来源于网络分享,如有侵权请联系我删除。另外如果没有积分的同学需要下载,请私信我。

    基于课程设计:C语言爬虫、详细文档+全部资料+高分项目.zip

    【资源说明】 基于课程设计:C语言爬虫、详细文档+全部资料+高分项目.zip 【备注】 1、该项目是个人高分项目源码,已获导师指导认可通过,答辩评审分达到95分 2、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用! 3、本项目适合计算机相关专业(人工智能、通信工程、自动化、电子信息、物联网等)的在校学生、老师或者企业员工下载使用,也可作为毕业设计、课程设计、作业、项目初期立项演示等,当然也适合小白学习进阶。 4、如果基础还行,可以在此代码基础上进行修改,以实现其他功能,也可直接用于毕设、课设、作业等。 欢迎下载,沟通交流,互相学习,共同进步!

    (176629254)杭州电子科技大学自动控制原理期末试卷答案 复习ppt

    《自动控制原理》是自动化及相关专业的重要课程,涵盖了控制系统的基础理论和分析方法。这份资料集是针对杭州电子科技大学自动控制原理课程的期末复习资源,包含了试卷答案和复习PPT,对于学习者来说是一份非常宝贵的参考资料。 我们来看文件"9自控原理第五章习题参考答案.doc",它提供了第五章的习题解答。第五章通常涉及根轨迹法,这是分析线性系统稳定性的一种图形方法。通过绘制根轨迹,我们可以直观地理解系统动态性能的变化,例如系统的稳定性、超调量和调节时间等。 接着,"1第一章作业(答案).docx"涵盖了课程的初步概念,如控制系统的基本组成部分、控制系统的定义以及开环与闭环控制的区别。第一章的内容通常包括控制系统的基本模型,如传递函数和信号流图。 "10测试4.docx"和"13第7章测试.docx"可能是关于控制系统设计和分析的测试题目,可能涉及到频率响应分析或状态空间模型等内容。第七章常常讨论系统稳定性分析,比如奈奎斯特稳定判据或劳斯判据。 "7测试1.docx"可能包含有关拉普拉斯变换和控制系统动态特性的问题,这是控制系统分析的基础工具。 "4自动控制第二章习题答案.pdf"提供了第二章习题

    066 - 直播逗大哥话术.docx

    066 - 直播逗大哥话术

    AOP项目demo 案例

    AOP项目demo 案例

    皮带输送线3D+2DCAD+加工件标准件清单BOMsw2016可编辑全套技术资料100%好用.zip

    皮带输送线3D+2DCAD+加工件标准件清单BOMsw2016可编辑全套技术资料100%好用.zip

    154-基于stm32单片机花样流水灯设计Proteus仿真+源程序.zip

    154-基于stm32单片机花样流水灯设计Proteus仿真+源程序.zip

    鲸鱼WOA-XGboost拟合预测建模模型,数据格式多维自变量输入,单维因变量输出,直接替数据就可以使用,程序内注释详细

    鲸鱼WOA-XGboost拟合预测建模模型,数据格式多维自变量输入,单维因变量输出,直接替数据就可以使用,程序内注释详细

    基于springboot的学生综合成绩测评系统源码(java毕业设计完整源码).zip

    项目均经过测试,可正常运行! 环境说明: 开发语言:java JDK版本:jdk1.8 框架:springboot 数据库:mysql 5.7/8 数据库工具:navicat 开发软件:eclipse/idea

    RocketDock-v1.3.5

    RocketDock-v1.3.5是一款专为Windows 7设计的桌面工具,它模仿Mac OS X的Dock功能,通过直观的图标排列提高应用启动效率。该工具提供快捷启动、动画效果、自定义设置以及扩展性,使用户能够根据个人喜好调整Dock栏并减少桌面杂乱。安装和设置过程简单,配有详细的说明文件,帮助用户更好地了解和使用工具。

    基于springboot的会员制医疗预约服务管理信息系统源码(java毕业设计完整源码+LW).zip

    会员制医疗预约服务管理信息系统的作用,可以提高会员制医疗预约服务管理的工作人员的效率,协助他们对会员制医疗预约服务信息进行统一管理,为管理者提供信息储存和查询搜索系统。一个良好的会员制医疗预约服务管理信息系统可以实现对会员制医疗预约服务的精细化管理:对在线会员制医疗预约服务管理流程的全过程进行电子化操作,其主要作用是管理和控制会员制医疗预约服务所有的信息,分析库存数据,使工作人员对会员制医疗预约服务管理信息系统进行监管,根据系统所提供的相应信息,采取适当的措施,及时补救管理中的漏洞,提高在线会员制医疗预约服务管理的工作效率,使得在线会员制医疗预约服务管理变的更加系统和规范。 环境说明: 开发语言:java JDK版本:jdk1.8 框架:springboot 数据库:mysql 5.7/8 数据库工具:navicat 开发软件:eclipse/idea

    (2368806)CCNA中文版PPT

    **CCNA(思科认证网络助理工程师)是网络技术领域中的一个基础认证,它涵盖了网络基础知识、IP编址、路由与交换技术等多个方面。以下是对CCNA中文版PPT中可能涉及的知识点的详细说明:** ### 第1章 高级IP编址 #### 1.1 IPv4地址结构 - IPv4地址由32位二进制组成,通常分为四段,每段8位,用点分十进制表示。 - 子网掩码用于定义网络部分和主机部分,如255.255.255.0。 - IP地址的分类:A类、B类、C类、D类(多播)和E类(保留)。 #### 1.2 子网划分 - 子网划分用于优化IP地址的分配,通过借用主机位创建更多的子网。 - 子网计算涉及掩码位数选择,以及如何确定可用的主机数和子网数。 - CIDR(无类别域间路由)表示法用于更有效地管理IP地址空间。 #### 1.3 私有IP地址 - 为了节省公网IP地址,私有IP地址被用于内部网络,如10.0.0.0/8,172.16.0.0/12,192.168.0.0/16。 #### 1.4 广播地址 - 每个网络都有一个特定的广播地址,所有数据包都会发送到这个地址以达到同一网络内的所有设备。

    基于springboot的藏区特产销售平台源码(java毕业设计完整源码+LW).zip

    项目实现了对特产信息管理、特产分类管理、特产分类管理、特产评分管理、系统管理、订单管理等业务进行管理。 环境说明: 开发语言:java JDK版本:jdk1.8 框架:springboot 数据库:mysql 5.7/8 数据库工具:navicat 开发软件:eclipse/idea

    “个人事务云服务”:日常事务管理系统的云计算应用

    随着21世纪网络和计算机技术的飞速发展,它们已经与我们的日常生活紧密融合。当前网络的运行速度已经达到千兆级别,覆盖范围广泛,深入到生活的每一个角落。这一进步推动了管理系统的发展,使得远程处理事务、远程工作信息管理和实时追踪工作状态成为可能。网上管理系统以其前所未有的体验满足了新时代的工作需求,因此得到了大力发展。 本系统是一个个人日常事务管理系统,利用计算机和网络技术开发的在线平台,能够实现日常安排、消费记录和重要提醒设置等功能。系统采用SSM框架和Vue技术,数据库使用MySQL,开发环境为Eclipse。系统用户角色包括普通用户和管理员,功能涵盖个人中心管理、用户管理、日常安排管理、消费记录管理和重要提醒管理。用户可以记录消费、安排日常事务和设置重要提醒,而管理员则负责管理用户信息和基础数据信息。该系统不仅方便了用户和管理员,还提高了个人事务管理的效率,更适应现代人的生活方式。

    基于java+springboot+mysql+微信小程序的考研资料分享系统 源码+数据库+论文(高分毕业设计).zip

    项目已获导师指导并通过的高分毕业设计项目,可作为课程设计和期末大作业,下载即用无需修改,项目完整确保可以运行。 包含:项目源码、数据库脚本、软件工具等,该项目可以作为毕设、课程设计使用,前后端代码都在里面。 该系统功能完善、界面美观、操作简单、功能齐全、管理便捷,具有很高的实际应用价值。 项目都经过严格调试,确保可以运行!可以放心下载 技术组成 语言:java 开发环境:idea、微信开发者工具 数据库:MySql5.7以上 部署环境:maven 数据库工具:navicat

    如何使用Python和PIL库生成带竖排文字的封面图像

    如何使用Python和PIL库生成带竖排文字的封面图像

Global site tag (gtag.js) - Google Analytics