Java中的多线程编程相关基础知识

多线程:

要了解多线程,必须先了解线程,要了解线程必须先了解进程。

进程:系统分配资源的最小单位。简单的说就是一个程序运行,如360程序,浏览器程序等

线程:线程是进程中执行运算的最小单位,是进程中的一个实体,是被系统独立调度和分派的基本单位。一个线程就是进程中的一个顺序控制流

理解他们可以借助工厂和工厂里的人来理解。

线程和进程的关系以及区别?

进程和线程的关系:

 (1)一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。

 (2)资源分配给进程,同一进程的所有线程共享该进程的所有资源。

 (3)处理机分给线程,即真正在处理机上运行的是线程。

 (4)线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。线程是指进程内的一个执行单元,也是进程内的可调度实体.

进程与线程的区别:

 (1)调度:线程作为操作系统调度和执行运行的基本单位,进程作为分配资源的基本单位

 (2)并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可并发执行

 (3)拥有资源:进程是拥有资源的基本单位,线程不拥有系统资源,但可以访问隶属于进程的资源。

 (4)系统开销:在创建或撤消进程时,由于系统都要为之分配和回收资源,导致系统的开销明显大于创建或撤消线程时的开销。线程的上下文切换要比进程的上下文切换快的多。

多线程编程的意义:合理利用CPU空闲时间,提高CPU的利用率,提高程序性能。

为啥可以提高CPU利用率?

现在计算机的主频都很高,IO操作速度很慢(在程序的控制范围之外,由硬件决定。程序等待IO操作完成,会使程序处于阻塞状态),所以程序的运行时间大多是是浪费在IO操作上,这个时候如果没有多线程,那么整个程序将停滞,CPU处理空闲状态。但是如果程序是多线程的,这个时候,CPU就会调度别的线程进行执行,使CPU处于运行状态,所以多线程可以提高CPU的利用率。

多线程编程,对于单核处理器,可以提高CPU的利用率。对于多核处理器,则可以充分发挥多核处理器的性能,每个线程在一个处理器上运行,实现真正的并发。单核处理器的多线程编程不是真正的并发,因为在一个时间点只能运行一个线程,多个线程轮流作业。但是可以提高CPU的利用率。单核处理器中,如果没有线程会阻塞,那么多线程是没有什么意义的。还不如单线程,因为多线程切换需要额外的开销,浪费资源。

Java多线程开发的弊端:多线程会让程序跑的更快,但是也有其弊端。想要写出安全的多线程程序时比较困难的,而且错误也比较难发现。到底需不需要多线程程序需要根据实际情况来决定。

Java中是线程驱动任务,所以多线程程序,需要有线程和任务。线程就是Thread对象,任务由run()描述。驱动就是线程调用run。

Java是一个多线程语言,因为Java程序在启动的时候,至少启动了两个线程,主线程和垃圾回收线程。

并发编程实际是将程序分成多个运行任务。通过多线程机制,每个任务由一个线程执行。

Java中实现多线程的方式:

1、 继承Thread类,重写run()方法。该方式不返回结果

2、 实现Runnable接口,重写run()方法。该方式不返回结果

3、 实现Callable<T>接口,重写call()方法(和线程池相联系)。该方式返回结果,结果以Future<T>形式给出。Callable<T>是一个泛型。该方式的用法和Runnable一样。提交任务使用submit()。因为execute是不返回结果的,从返回的结果获取数据,使用Future<T>中的方法get()。

常用的是第二种,第三种几乎不用。通过匿名内部类也可以实现继承或者实现接口。

为什么第二种用的较多?:

1、 可以解决Java中单继承的弊端,实现Runnable接口的类还可以再继承别的类,如果直接继承Thread类,则会让继承带来麻烦。

2、 使用Runnable接口可以使代码和数据分开,实现Runnable接口的类可以到处执行处理相同问题的情况,充分体现Java的面向对象设计想

Java中yeild(),sleep(),wait()的区别?

相同点:都是用来进行线程的切换。

yield():它的调用只是对线程调度器的一种建议,目前的线程已经将任务做的差不多了,可以进行线程切换。但是最终是否进行切换取决于线程调取器。即使切换了,也只是让其回到就绪状态且不释放锁,它可能会立即执行。只能使同优先级的线程得到执行

sleep():让线程先睡眠一会(进入阻塞状态),但不释放锁,让其他线程进行执行(低优先级,高优先级,同优先级都可以)。在这个睡眠期间,该线程肯定不会再执行。

wait():让线程进入等待状态,同时释放锁。

为什么wait(),notify(),notifyAll()等方法都定义在Object类中?

因为这些方法的调用是依赖于锁对象,而同步代码块的锁对象可以是任意对象。Object类是所有类的父类,所以只有将这些方法定义在object里面,任意对象才可以调用它们。

启动线程是run(),还是start()?它们的区别?

启动线程是start()。线程对象调用start(),jvm先初始化,线程进入就绪就绪状态,获得执行权,调用run()进行运行。简单的说,就是启动线程,调用run().

start()是先启动线程,然后调用run()。

run()只是用来封装被线程执行的代码。如果直接调用它,和调用普通的方法没有区别。

线程的优先级有10个等级,默认是5。优先级只是表明这个线程的重要性,线程调度器调度它的频率会高点,但是并不是说低优先级的线程得不到执行。只是频率低点。

线程的生命周期:新建,就绪,运行,阻塞,死亡

线程的状态转换图:线程的状态有,新建,就绪,运行,阻塞,死亡。其中阻塞分为几种情况:同步阻塞、等待阻塞、sleep(),IO操作造成的阻塞。这几种阻塞状态分别是线程在不同的情况下所造成的,如线程同步,线程协调,数据读取造成阻塞。处于阻塞状态的线程,线程调度器会忽略它们(即它们不参与CPU时间片的争夺)。线程离开阻塞状态,将变为就绪状态,将和其他线程共同进行CPU时间片的争夺。


Java的线程机制是抢占式。在Java中,将CPU的时间进行切片,各线程之间通过抢占的方式来获取CPU的时间片,从而线程在这个时间内完成部分任务。所以Java的线程调度器会周期性的中断线程,从而实现为每个线程分配时间。这种就造成调度器的不确定性。所有阻塞状态的线程,最终都要回到就绪状态,然后进行CPU时间片的抢夺。

线程池:其特点是池中的线程可自动重复利用,减少创建线程和销毁线程所花费的时间。尤其对于在短时间内创建大量线程,然后又销毁线程的情况。线程池也是并发包提供的功能。

使用线程池的优点:

1、减少在创建和销毁线程上所花的时间以及系统资源的开销

2、如不使用线程池,有可能造成系统创建大量线程而导致消耗完系统内存

3、可以简化编程。

Java自带的几种线程池:

1、 newCachedThreadPool创建一个可缓存的线程池。

2、 newFixedThreadPool  创建一个指定工作线程数量的线程池。

3、 newSingleThreadExecutor创建一个单线程化的Executor,即只创建唯一的工作者线程来执行任务,如果这个线程异常结束,会有另一个取代它,保证顺序执行。相当于2中线程的数量为1.

4、 newScheduleThreadPool创建一个定长的线程池,而且支持定时的以及周期性的任务执行

一般使用前两个线程池已经足够

1.CachedThreadPool的特点就是在线程池空闲时,即线程池中没有可运行任务时,它会释放工作线程,从而释放工作线程所占用的资源。但是,但当出现新任务时,又要创建一新的工作线程,又要一定的系统开销。并且,在使用CachedThreadPool时,一定要注意控制任务的数量,否则,由于大量线程同时运行,很有可能会造成系统瘫痪。默认可开辟的线程数量是Integer.MAX_VAULE

2.FixedThreadPool是一个典型且优秀的线程池,它具有提高程序效率和节省创建线程时所耗的开销的优点。但在线程池空闲时,即线程池中没有可运行任务时,它不会释放工作线程,还会占用一定的系统资源。

Executor是一个顶层接口,在它里面只声明了一个方法execute(Runnable){也可以是Callable接口,如果实现Callable接口,其也只能传给线程池。},返回值为void,参数为Runnable类型,从字面意思可以理解,就是用来执行传进去的任务的;

然后ExecutorService接口继承了Executor接口,并声明了一些方法:submit、invokeAll、invokeAny以及shutDown等;

抽象类AbstractExecutorService实现了ExecutorService接口,基本实现了ExecutorService中声明的所有方法;

然后ThreadPoolExecutor继承了类AbstractExecutorService。

在ThreadPoolExecutor类中有几个非常重要的方法:

execute(Runnable obj):向线程池提交任务,由线程池执行。无返回结果。

submit(Runnable obj):和execute一样,只是它有返回结果。其内部也是调了execute(),最后调用Future来获取任务结果。

shutdown():关闭线程池,即不能向线程池提交任务,等待任务执行完毕

shutdownNow():关闭线程池,即不能向线程池提交任务,尝试终止正在执行的任务

还有其他的一些方法来获得线程池的一些属性。

execute:其底层实现的大体思路:创建线程,启动线程,执行任务。只是并发包现在不需要程序员去做这些事情,已经帮助我们做好。我们只要提供一个任务就OK。由Executor(执行器)来管理线程对象,简化编程,是启动任务的优选方法。为客户端和任务执行提供了一个中介,现在不需要我们去执行任务,由这个中介去执行。这个中介就是线程池。代理模式的应用

 

多线程编程存在的问题:线程同步,线程安全,线程协调(线程通信),死锁

线程同步:线程调用某个方法,在操作完成前,其他的线程不能调用该方法(互斥),从而使线程按照预先的顺序进行执行(同步)。可以采用线程同步技术实现线程同步,线程同步的目的是使线程之间相互独立。这里的同步是协同步调的意思,不是同时操作。同步是在互斥基础上

线程安全:多个线程操作同一共享资源,在线程不同步的情况下,可能会造成数据不准确,产生脏数据,偏离预期目标。这样的线程是不安全的,为了使线程安全,只有采用线程同步,保证数据的唯一性和准确性。

线程协调:在有些情况下,除了要求线程同步,还可能要求线程之间相互协调配合,完成相应的操作。简单的说,就是多个线程相互配合完成某一任务。这里注重线程的配合,线程同步,注重的是线程之间的相互不干涉。通过等待唤醒机制实现线程协调。

不同步的根源:多线程中非同步问题主要出现在多个线程对同一域(变量)的读写上,如果域自身可以避免这个问题,则就不需要修改操作该域的方法。用final域,锁保护的域,volatile保护的域等都可以实现同步。如果对域只有读操作,那么不存在非同步问题。如果操作是原子性的,也可以避免同步问题。

线程同步技术:

实现线程同步的技术很多,最常用的是锁。下面分别介绍同步技术。

在讲线程同步技术之前,先说一下锁概念。每个对象都有一个内置锁,class也有一个锁。锁实际上是一个类似标志的东西(个人理解)。锁一般关联一个锁计数器,和获得锁的线程。同一个线程可以多次获得同一个锁,每获得一次,锁计数器加1,同理线程释放锁,锁计数器减1,锁计数器为0,锁被完全释放。

这里的锁分为公平锁和不公平锁,一般都是使用不公平锁。因为使用公平锁需要消耗更多的资源,付出更多的代价。

原子性:是指一次执行中不可中断的操作。如赋值操作等。i++不是原子操作,这个操作的过程是:先读取i的值,然后再加1,最后再赋值。这实际上可分为三个原子操作,这整个操作是可中断的。

使用锁同步代码的后果是:使代码具有原子性和可见性。

1、 synchronized(基于内置锁)

由该关键字修饰的方法,在线程调用对象中该方法前,先要获得此对象的锁,然后将方法锁住,接着才能调用,不能获得锁的对象将处于阻塞状态。这个锁会保护整个方法,即锁住整个方法。该关键字也可以修改静态方法,这样锁住的是整个类。相当于这个方法就是房间,线程从对象获得锁,然后将门锁上,由于锁的唯一性,这样别的线程就不不能进入房间。

2、 临界区(基于内置锁)

synchronized(this){

}

该方法锁住的只是这部分代码块。并不是锁住整个所方法。其他和第一种情况相似。其他线程可以调用该方法,但是在这个代码块处,将被阻塞。同步是一种高开销的操作,尽量减少同步的内容,一般没有必要同步整个方法,同步部分代码块即可。这样相对于地中方法来说,稍微高效一点。

3、 lock(重入锁)

这方法是Java的concurrent包—并发包提供的一种同步机制,其基本语义和synchronized方法或块相同,只是这里是显示上锁,释放锁。同时它还扩展了功能。添加了类似轮询锁、定时锁等候和可中断锁等候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。(换句话说,当许多线程都想访问共享资源时,JVM 可以花更少的时候来调度线程,把更多时间用在执行线程上)。除了扩展了功能之外,此方式相较与synchronized具有较好的伸缩性,其开销也比synchronized小的多,获得更高的吞吐量。它只是一个接口,它的实现类是ReentrantLock。使用时,一般遵循下面步骤:

创建对象:ReentrantLock lock = new ReentrantLock();

上锁:lock.lock();

释放锁:lock.unlock();在finally中释放锁,其实这步操作容易忽略,从而引起很难察觉的错误,出现死锁。

1.  Lock lock = new ReentrantLock();  

2.  lock.lock();  

3.  try {   

4.    // update object state  

5.  }  

6.  finally {  

7.    lock.unlock();   

8.  }  

虽然这种重入锁的方法比synchronized性能,功能上都好,但是实际中还是优先使用synchronized,因为其出错的可能性小,同时代码简洁。只有在确实需要使用lock的情况下,才使用的它。

4、 ThreadLocal(局部变量实现线程同步)

使用ThreadLocal管理变量,凡是使用该变量的线程都会获得该变量的副本,副本之间相互独立,每个线程分别在各自的副本上进行相应的操作,从而可以避免多线程中相同变量访问冲突的问题,解决同步问题。该方法是以空间换时间。前面的都是以时间换空间。

ThreadLocal 类的常用方法:

ThreadLocal() : 创建一个线程局部变量

get() : 返回此线程局部变量的当前线程副本中的值

initialValue() : 返回此线程局部变量的当前线程副本中的"初始值"

set(T value) : 将此线程局部变量的当前线程副本中的值设置为value

 

其内部实现是基于ThreadLocalMap来操作的。ThreadLocal内部有一个静态内部类,它是线程同步的根源,每个线程都有一个独立ThreadLocalMap副本,它存储的值只能被当前线程修改。它的键值对的键是ThreadLocal对象,值即是存储的值。从而实现线程同步。

5、 Volatile

是轻量级的同步机制。使用volatile修饰的变量,在各线程中具有可见性,或者说在各线程中具有一致性。(在这个线程中改变,在别的线程中会立即发生变化,始终保持最新状态)

volatile为域变量的多线程访问提供了一种免锁机制。volatile不会提供任何原子操作,它也不能用来修饰final类型的变量。

6、 Atomic--原子类

该方法不是基于语言特性,而是基于硬件的同步。该类是Java并发包提供的功能。借助硬件的相关命令来实现,不会出现阻塞。包里面提供了一组原子变量类。可以对基本数据、数组中的基本数据、对类中的基本数据进行操作,能够支持原子性操作。

实际最常用的还是前三个,只有高手才会能准确适用后面几个。

以上都是通过手动的方式来完成线程同步,实际上Java提供了很多能够实现同步方法,如并发包提供的同步集合类,Collectons提供的一些同步的方法。对于ArrayList,HashMap这样不安全的集合,可以使用Collections中的方法使其变成同步。

线程协调技术:

使用等待唤醒机制来实现线程协调,从而完成某一目标。线程协调的典型模型是:生产者-消费者模型。生产者产生数据资源(生产),消费者使用这些数据资源(消费)。

正常的思路:

生产者:

    先看是否有数据,如果有,则等待,否则生产数据,通知消费者进行消费

消费者:

    先看是否有数据,如果没有,则等待,否则消费,通知生产者生产数据。

为了使这两个线程可以配合正常使用,Java提供了一种等待唤醒机制。使用Object中的三种方法:wait(),notify(),notifyAll()

原始方法和Condition

wait()和notify()方法是Object类中的两个方法。这两个方法是非静态的,因为这两个方法是Object类中的方法,所以通过继承,Java中所有的实例都可以调用这两个方法。

   wait()方法:它让执行此代码的线程进入阻塞状态,同时释放对象锁。如果在处于挂起状态时,因为某些原因挂起被打断了,那么该方法就会抛出一个InterruptedException的异常,这个异常和sleep方法抛出的异常是同一个类型。

  notify()方法:唤醒因为在同一个对象上调用wait()而处于挂起状态的线程(线程将处于就绪状态,参与CPU时间片的争夺),让线程可以继续执行下去。

   同一个对象:因为在同一个对象上调用wait()方法而进入等待状态的线程,只能由另一个线程在同一个对象上调用notify方法来唤醒。举例来说,obj是一个指向类Object的实例的引用,线程1调用obj.wait()方法而进入挂起状态,如果想唤醒这个线程,只能够在指向同一个类Object实例上调用notify方法才可以

   调用方法的要求:必须在获得一个对象的锁的代码内部才能调用这个对象的wait()或者notify()方法。也就是说,如果要调用一个对象的wait()或者notify()方法,那么就首先需要使用synchronized获得这个对象的锁,否则程序将抛出一个IllegalMoniterStateException异常。它们基于锁操作。

   当调用wait()方法后线程马上释放对象的对象锁。

这里有一个notifyAll()方法,这个方法用来唤醒所有等待同一个对象锁的线程。notify()只能唤醒一个线程。确实有这种情况,例如有很多消费者要消费这个数据,但是刚开始都没有数据,那么这些消费者都将处于阻塞状态,直至生产者产生数据为止。

通过这样的等待唤醒机制,就可以让线程之间相互配合,完成相应的任务。但是上面的仅仅是最原始的方式,Java的并发包提供了与之相对应的Condition接口实现相同的功能,其方法有await(),signal(),signalAll()。分别对应上面的wait(),notify(),notify()。用法,分析都一样,它们更加高效和安全。但是该方法是依赖于lock接口,几种方法的使用也必须在lock(),unlock()之间。

private Lock lock = new ReentrantLock();

private Condition notFull = lock.newCondition();

private Condition notEmpty = lock.newCondition();

while(true){

   lock.lock();

     try {

       while(queue.size()== 0){

         try {

      System.out.println("队列空,等待数据");

          notEmpty.await();

          }catch (InterruptedException e) {

         e.printStackTrace();

         }

        }

       ueue.poll(); //每次移走队首元素

       notFull.signal();

       System.out.println("从队列取走一个元素,队列剩余"+queue.size()+"个元素");

       } finally{

         lock.unlock();

      }

     }

生产者-消费者队列

上面两种方法,都要在程序中使用锁,Java的并发包提供了更加安全,功能更强大,操作更简单的方法,这种方法在实际开发中,有利于快速搭建多线程开发程序。阻塞队列—blockingQueue,它更加安全高效的解决了数据传输问题,将数据存放在队列中。它只是一个接口,它的实现类有很多,也对应着相应的阻塞队列类型,常见用的有:ArrayBlockingQueue,LinkedBlockingQueue,PriorityBlockingQueue,SynchronizedQueue,DelayQueue。最常用的是ArrayBlockingQueue,LinkedBlockingQueue,基本上可以满足所有需求。前者是基于定长数组实现,两端采用相同的非公平锁(无法实现真正的并发),插入数据不会产生额外实例,后者是基于链表实现,两端采用不同的独立锁,插入数据会产生额外实例。它们的核心方法是存数据,取数据。

取数据:

take():取走BlockingQueue里排在首位的对象,若BlockingQueue为空,进入等待状态直到BlockingQueue有新的数据被加入。

poll(time):取走BlockingQueue里排在首位的对象,若不能立即取出,则可以等time参数规定的时间,取不到时返回null; (本方法不阻塞执行当前方法的线程)

 

存数据:

put(object):把Object加到BlockingQueue里,如果BlockQueue没有空间,则调用此方法的线程被阻塞,直到BlockingQueue里面有空间再继续

offer(object):表示如果可能的话,将Object加到BlockingQueue里,即如果BlockingQueue可以容纳,则返回true,否则返回false.(本方法不阻塞执行当前方法的线程)

 

使用这种方式编写程序,就像在编写非同步的多线程程序一样,只需要注重业务逻辑,不需要考虑线程安全等问题,并发包的内部都做了这样的事情,基本思想还是等待唤醒机制。

1、ArrayBlockingQueue

基于数组的阻塞队列实现,在ArrayBlockingQueue内部,维护了一个定长数组,以便缓存队列中的数据对象,这是一个常用的阻塞队列,除了一个定长数组外,ArrayBlockingQueue内部还保存着两个整形变量,分别标识着队列的头部和尾部在数组中的位置。

ArrayBlockingQueue在生产者放入数据和消费者获取数据,都是共用同一个锁对象,由此也意味着两者无法真正并行运行,这点尤其不同于LinkedBlockingQueue;按照实现原理来分析,ArrayBlockingQueue完全可以采用分离锁,从而实现生产者和消费者操作的完全并行运行。Doug Lea之所以没这样去做,也许是因为ArrayBlockingQueue的数据写入和获取操作已经足够轻巧,以至于引入独立的锁机制,除了给代码带来额外的复杂性外,其在性能上完全占不到任何便宜。ArrayBlockingQueue和LinkedBlockingQueue间还有一个明显的不同之处在于,前者在插入或删除元素时不会产生或销毁任何额外的对象实例,而后者则会生成一个额外的Node对象。这在长时间内需要高效并发地处理大批量数据的系统中,其对于GC的影响还是存在一定的区别。而在创建ArrayBlockingQueue时,我们还可以控制对象的内部锁是否采用公平锁,默认采用非公平锁。

2. LinkedBlockingQueue

基于链表的阻塞队列,同ArrayListBlockingQueue类似,其内部也维持着一个数据缓冲队列(该队列由一个链表构成),当生产者往队列中放入一个数据时,队列会从生产者手中获取数据,并缓存在队列内部,而生产者立即返回;只有当队列缓冲区达到最大值缓存容量时(LinkedBlockingQueue可以通过构造函数指定该值),才会阻塞生产者队列,直到消费者从队列中消费掉一份数据,生产者线程会被唤醒,反之对于消费者这端的处理也基于同样的原理。而LinkedBlockingQueue之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。

作为开发者,我们需要注意的是,如果构造一个LinkedBlockingQueue对象,而没有指定其容量大小,LinkedBlockingQueue会默认一个类似无限大小的容量(Integer.MAX_VALUE),这样的话,如果生产者的速度一旦大于消费者的速度,也许还没有等到队列满阻塞产生,系统内存就有可能已被消耗殆尽了。

 

死锁:线程1占用对象1,等待对象2的锁,同时线程2占用对象2,等待对象1的锁,就这样一直等待下去,从而出现死锁情况。死锁--多个线程等待不可能释放的锁,从而导致程序不能正常运行。处于等待状态的线程即是阻塞状态,这里的等待不是因为wait()的原因。

导致死锁的根源:在于不适当地运用synchronized关键词来管理线程对特定对象的访问。synchronized关键词的作用是,确保在某个时刻只有一个线程被允许执行特定的代码块,因此,被允许执行的线程首先必须拥有对对象的排他性的访问权。当线程访问对象时,线程会给对象加锁,而这个锁导致其它也想访问同一对象的线程被阻塞,直至第一个线程释放它加在对象上的锁。

由于这个原因,在使用synchronized关键词时,很容易出现两个线程互相等待对方做出某个动作的情形。也就容易出现死锁。多个synchronized嵌套,就容易出现死锁。循环容易造成死锁。

如何避免死锁

1、让所有的线程按照同样的顺序获得一组锁。这种方法消除了相互等待对方资源的问题。

2、将多个锁组成一组并放到同一个锁下。前面Java线程死锁的例子中,可以创建一个银器对象的锁。于是在获得刀或叉之前都必须获得这个银器的锁。

3、将那些不会阻塞的可获得资源用变量标志出来。当某个线程获得银器对象的锁时,就可以通过检查变量来判断是否整个银器集合中的对象锁都可获得。如果是,它就可以获得相关的锁,否则,就要释放掉银器这个锁并稍后再尝试。

 

何时使用多线程

1、通过网络(例如,与 Web 服务器、数据库或远程对象)进行通信。 (分布式事务处理)

2、执行需要较长时间因而可能导致 UI 冻结的本地操作。 (图像呈现,数据操纵,排序,搜索等)

3、区分各种优先级的任务。

4、提高应用程序启动和初始化的性能。


备注:这些知识Java多线程编程的一些基础知识,想要深入理解多线程编程,请阅读Java虚拟机-JVM中的内存模型,或者参考博客:http://blog.csdn.net/ccit0519/article/details/11241403

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值