第六部分 Java高级开发(二)——多线程

本文详细介绍了Java中的多线程概念,包括进程与线程的区别、Java线程的实现方式(Thread类继承与Runnable接口实现)、Callable接口、线程操作方法、线程同步、线程优先级、守护线程、线程池和生产者消费者模型等。通过实例解析了各种线程状态、同步机制、锁的优化及线程池的创建与关闭。着重讨论了线程池中的核心类ThreadPoolExecutor及其参数配置,并探讨了如何合理配置线程池以提高系统性能。
摘要由CSDN通过智能技术生成

6.2 多线程

6.2.1 进程和线程

进程:在操作系统中一个程序的执行周期;

线程:一个程序同时执行多个任务,每个任务成为一个线程。

相比于进程,线程的创建、撤销的开销比进程小很多,更加“轻量级”。没有进程就没有线程、进程一旦终止线程也一并终止。

多进程与多线程区别:本质区别在于,每个进程拥有自己的一整套变量,而线程则共享数据。共享变量使得线程之间的通信比进程之间通信更有效、更方便。

多线程的表现:高并发(访问的线程量非常大,会导致服务器内存不够用,程序资源竞争,无法处理新的请求)

线程状态

创建:创建一个进程,但是不会立即执行,转到就绪状态;

就绪:通过操作系统调度可以到达运行状态;

阻塞:出现I/O等其他资源没有就位而进入的状态;

运行:就绪的线程获得CPU的运行时间权限进入执行状态;

终止:线程完成相应任务进入终止状态。

6.2.2 Java线程的实现

Java中通过Thread类(java.lang.Thread)的直接继承,之后覆写该类中的run方法(相当于main方法)

public class MyThread extends Thread {
   
private String msg;
    public
MyThread(String msg) {
       
this.msg = msg;
   
}
   
@Override
   
public void run() {
        System.
out.println(msg);
   
}
}

想要使用java的多线程特性,要使用Thread的start方法

public class TestThread {

    public static void main(String[] args) {

        MyThread thread1 = new MyThread("thread A");

        MyThread thread2 = new MyThread("thread B");

        MyThread thread3 = new MyThread("thread C");

        thread1.run();

        thread2.run();

        thread3.run();

        System.out.println();

        thread1.start();

        thread2.start();

        thread3.start();

    }

}

在start()方法中抛出IllegalThreadStateException异常,按照原有的处理方式,应当在调用处进行异常处理,而此处没有处理也不会报错,因此是一个RunTimeException,这个异常的产生只是因为你重复启动了线程才会产生。所以,每一个线程对象只能够启动一次。

在start()方法中调用了start0()方法,而这个方法是一个只声明而未实现的方法同时使用native关键字进行定义。native指的是调用本机的原生系统函数。

Thread 类有个 registerNatives 本地方法,该方法主要的作用就是注册一些本地方法供 Thread 类使用,如start0(),stop0() 等等,可以说,所有操作本地线程的本地方法都是由它注册的。这个方法放在一个 static 语句块中,当该类被加载到 JVM 中的时候,它就会被调用,进而注册相应的本地方法。而本地方法 registerNatives 是定义在 Thread.c 文件中的。Thread.c 是个很小的文件,它定义了各个操作系统平台都要用到的关于线程的公用数据和操作。

6.2.3 Runnable接口实现多线程

Thread类的核心功能是进行线程的启动。如果一个类为了实现多线程直接去继承Thread类就会有但继承局限。在java中又提供有另外一种实现模式:Runnable接口。

public class TestThread {
   
public static void main(String[] args) {
        Runnable runnable = () -> System.
out.println("Hello World");
        new
Thread(runnable).start();
   
}
}

 

public class TestThread {
       
public static void main(String[] args) {
           
new Thread(new Runnable() {
               
@Override
               
public void run() {
                    System.
out.println("Hello World");
                
}
            }).start()
;
       
}
}

 

Thread与Runnable区别

首先从使用形式来讲,明显使用Runnable实现多线程要比继承Thread类要好,因为可以避免但继承局限。

在多线程的处理上使用的就是代理设计模式。实际上在开发之中使用Runnable还有一个特点:使用Runnable实现的多线程的程序类可以更好的描述出程序共享的概念(并不是说Thread不能)

Runnable实现的多线程的程序类可以更好的描述出程序共享的概念。

6.2.4 Callable

这个开发包主要是进行高并发编程使用的,包含很多在高并发操作中会使用的类。在这个包里定义有一个新的接口Callable

Runnable中的run()方法没有返回值,它的设计也遵循了主方法的设计原则:线程开始了就别回头。但是很多时候需要一些返回值,例如某些线程执行完成后可能带来一些返回结果,这种情况下就只能利用Callable来实现多线程。

public class MyThread implements Callable {
   
private int  ticket = 10;
    public static void
main(String[] args) {
        Callable callable =
new MyThread();
       
FutureTask<String> task =new FutureTask<>(callable);
       
Thread thread1 = new Thread(task);
       
thread1.start();
       
Thread thread2 = new Thread(task);
       
thread2.start();
       
Thread thread3 = new Thread(task);
       
thread3.start();
        try
{
            System.
out.println(task.get());
       
} catch (InterruptedException e) {
            e.printStackTrace()
;
       
} catch (ExecutionException e) {
            e.printStackTrace()
;
       
}
    }
   
@Override
   
public synchronized String call() throws Exception {
       
while(this.ticket>0){
           
this.ticket--;
           
System.out.println(this.ticket);
       
}
       
return "Out of tickets";
   
}
}

6.2.5 线程中的常用操作方法

分类

方法名

效果

线程命名与取得

public Thread(Runnable target,String name)

构造方法,创建线程的时候设置名称

public final synchronized void setName(String name)

设置线程名称

public final String getName()

获得线程名称

public static native Thread currentThread();

获取当前线程本体

线程状态

public static native void sleep(long millis) throws InterruptedException

线程休眠:让线程暂缓执行,时间过了之后恢复执行,但是会交出CPU,且不会释放锁。时间单位是毫秒。线程会进入阻塞状态,之后回到就绪状态。

public static native void yield();

暂停正在执行的线程对象,并执行其他线程,交出CPU,不会释放锁,不能具体控制交出CPU的时间,也只能让拥有相同优先级线程有获取CPU执行时间的机会,会使得线程直接回到就绪状态

public final synchronized void join() throws InterruptedException

等待该线程终止。意思就是如果在主线程中调用该方法时就会让主线程休眠,让调用该方法的线程run方法先执行完毕之后在开始执行主线程。

线程终止

public void interrupt()

给受阻塞的线程发出一个中断信号,这样受阻线程就得以退出阻塞的状态。

public final void stop()

已经被弃用。强制使线程退出。会有产生残废数据的可能性。

public boolean isInterrupted()

 

 

注意:interrupt()方法并不会立即执行中断操作;具体而言,这个方法只会给线程设置一个为true的中断标志(中断标志只是一个布尔类型的变量),而设置之后,则根据线程当前的状态进行不同的后续操作。如果,线程的当前状态处于非阻塞状态,那么仅仅是线程的中断标志被修改为true而已;如果线程的当前状态处于阻塞状态,那么在将中断标志设置为true后,还会有如下三种情况之一的操作: 

如果是wait、sleep以及jion三个方法引起的阻塞,那么会将线程的中断标志重新设置为false,并抛出一个InterruptedException;

如果在中断时,线程正处于非阻塞状态,则将中断标志修改为true,而在此基础上,一旦进入阻塞状态,则按照阻塞状态的情况来进行处理;例如,一个线程在运行状态中,其中断标志被设置为true之后,一旦线程调用了wait、jion、sleep方法中的一种,立马抛出一个InterruptedException,且中断标志被程序会自动清除,重新设置为false。

6.2.6 线程优先级

优先级是一个等级,等级高的线程有更高的可能被优先执行。注意只是可能而已。

在Thread类中提供有如下优先级方法:

设置优先级:public final void setPriority(int newPriority)

取得优先级:public final int getPriority()

对于优先级设置的内容可以通过Thread类的几个常量来决定

1. 最高优先级:public final static int MAX_PRIORITY = 10;

2. 中等优先级:public final static int NORM_PRIORITY = 5;(默认优先级,main方法也是这个优先级)

3. 最低优先级:public final static int MIN_PRIORITY = 1;

线程具有继承性:线程A内创建的新的线程B称为线程A的子线程,AB两个线程的优先级是一致的。

6.2.7 守护线程

守护线程是一种特殊的线程,它属于是一种陪伴线程。

Java 中有两种线程:用户线程和守护线程。可以通过isDaemon()方法来区别它们:如果返回false,则说明该线程是“用户线程”;否则就是“守护线程”。也可以使用setDaemon()方法来设定某个线程是否是守护线程。但是这个设置必须要在线程开始运行之前设置才能生效。典型的守护线程就是垃圾回收线程。只要当前JVM进程中存在任何一个非守护线程没有结束,守护线程就在工作;只有当最后一个非守护线程结束时,守护线程才会随着JVM一同停止工作。主线程main是用户线程。

6.2.8 线程的同步

同步指的是所有的线程不是一起进入到方法中执行,而是按照顺序一个一个进来。

synchronized处理同步问题

关键字synchronized 可以修饰代码块或者方法

使用同步代码块 : 如果要使用同步代码块必须设置一个要锁定的对象,所以一般可以锁定当前对象:this

synchronized (this){
   
// TODO    
    }

这种方法可以保证进入代码块的线程有且只有一个。只需要把同步的代码放入代码块中,整体代码的性能会提高。

使用同步方法:同步的是对象的方法,直接在方法上加synchronized关键字即可。

同步虽然可以保证数据完整性,但是执行速度会收到影响。

synchronized优化

1、CAS(乐观锁策略)

利用比较交换来鉴别线程是否冲突,一旦出现冲突就重试当前操作指导没有冲突。设函数CAS(V,O,N),其中V表示内存中的实际值,O表示预期的值(旧值),N表示更新的值。若V==O,表明这个数据可能没有被其他线程修改,这个线程可以修改内存,此时可以把N赋值给V;如果V≠O,旧值O不是最新值,不能将N赋值给V,只能返回最新值V。当多个线程使用CAS时有且只有一个线程会成功更新,其余会失败。失败的线程可以选择重试或者挂起。

CAS整体操作是原子性的,整体的实现需要硬件指令集的支持,在JDK1.5之后虚拟机才可以使用处理器提供的CMPXCHG指令实现。

如果没有CAS的主要问题在存在线程竞争的情况下会出现线程阻塞和唤醒锁带来的性能问题,因为这是一种互斥同步(阻塞同步)。CAS并不是武断的将线程挂起,当CAS操作失败后会进行一定的尝试,而非进行耗时的挂起唤醒的操作,因此也叫做非阻塞同步。

然而CAS也存在一定的问题,首当其冲的就是ABA问题。考虑CAS函数的三个变量为CAS(A,B,A)即一个旧值A变成了B,然后再变成A,刚好在做CAS检查是发现旧值并没有变化,但是实际上的确发生了变化。解决方案为添加版本号,对版本号采用CAS处理。在JDK1.5后的atomic包中提供了AtomicStampedReference来解决ABA问题。

CAS如果出现冲突会可能发生重试,这个过程被称为自旋,自旋由于进程持续处于运行情况,会浪费大量的处理器资源。解决方式为:在自旋的时候会动态调整自旋的时长(循环次数),这个处理逻辑是由JVM提供的。

自旋状态的副作用:会使得处于自旋状态的进程更有可能优先获得资源,内建的synchronized锁无法实现公平机制,Lock体系可以实现公平锁。

2、Java头对象

在同步的时候是获取对象的monitor,即获取到对象的锁。对象的锁无非就是类似对对象的一个标志,那么这个标志就是存放在Java对象的对象头。Java对象头里的Mark Word里默认的存放的对象的Hashcode,分代年龄和锁标记位。32位JVM Mark Word默认存储结构为:

锁状态

25bit

4bit

1bit是否是偏向锁

2bit锁标志位

无锁状态

对象的hashCode

对象分代年龄

0

01

图在Mark Word会默认存放hasdcode,年龄值以及锁标志位等信息。 Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。

3、偏向锁

偏向锁:从始至终只有一个线程请求某一把锁,是四种状态中最乐观的一种锁。主要目的是降低线程请求锁的难度。

在获取偏向锁时,首先先在对象头和栈桢中的锁记录中存储锁偏向的线程ID,以后这个线程在进入/退出同步快是不需要进行CAS操作来加解锁,只需要简单的测试对象头的Mark Word中是否存储着指向当前线程的偏向锁。如果测试成功,表示线程获得锁反之就要测试MarkWord中偏向锁的标识是否设置成1,如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。

在释放偏向锁时,只有出现竞争才释放锁,当其他县城尝试竞争偏向锁时,需要等待全局安全点(没有正在执行的字节码)。首先先暂停偏向锁的线程,然后检查持有偏向锁的线程是否存活,如果线程不处于活动状态,则将对象头设置成无锁状态;如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。

偏向锁的关闭:偏向锁在JDK6之后是默认启用的,但是它在应用程序启动几秒钟之后才激活,如有必要可以使用JVM参数来关闭延迟:-XX:BiasedLockingStartupDelay=0。如果你确定应用程序里所有的锁通常情况下处于竞争状态,可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,那么程序默认会进入轻量级锁状态。

4、轻量级锁

多个线程在不同的时间段请求同一把锁,也就是说没有锁竞争。针对这种情况,JVM采用了轻量级锁,来避免线程的阻塞以及唤醒。

轻量级锁的加锁:需要考虑的是加锁的时间。线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。

轻量级锁的解锁和膨胀:轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。

因为自旋会消耗CPU,为了避免无用的自旋(比如获得锁的线程被阻塞住了),一旦锁升级成重量级锁,就不会再恢复到轻量级锁状态。当锁处于这个状态下,其他线程试图获取锁时,都会被阻塞住,当持有锁的线程释放锁之后会唤醒这些线程,被唤醒的线程就会进行新一轮的夺锁之争。

5、重量级锁

重量级锁是JVM中最为基础的锁实现。在这种状态下,JVM虚拟机会阻塞加锁失败的线程,并且在目标锁被释放的时候,唤醒这些线程。

Java线程的阻塞以及唤醒,都是依靠操作系统来完成的。举例来说,对于符合posix接口的操作系统(如macOS和绝大部分的Linux),上述操作通过pthread的互斥锁(mutex)来实现的。此外,这些操作将涉及系统调用,需要从操作系统的用户态切换至内核态,其开销非常之大。

为了尽量避免昂贵的线程阻塞、唤醒操作,JVM会在线程进入阻塞状态之前,以及被唤醒之后竞争不到锁的情况下,进入自旋状态,在处理器上空跑并且轮询锁是否被释放。如果此时锁恰好被释放了,那么当前线程便无须进入阻塞状态,而是直接获得这把锁。

6、锁粗化

锁粗化就是将多次连接在一起的加锁、解锁操作合并为一次,将多个连续的锁扩展成为一个范围更大的锁。

class MyThread  {
   
private static StringBuilder sb = new StringBuilder();
public static void
main(String[] args)  {
       
synchronized (sb){
           
sb.append("a");
           
sb.append("b");
           
sb.append("c");
       
}
    }   
}

7、锁消除

锁消除即删除不必要的加锁操作。根据代码逃逸技术,如果判断到一段代码中,堆上的数据不会逃逸出当前线程,那么可以认为这段代码是线程安全的,不必要加锁。

class MyThread  {
   
public static void main(String[] args)  {
        StringBuilder sb =
new StringBuilder();
       
sb.append("a").append("b").append("c");
   
}
}

 

Lock

在JDK1.5中,Lock的性能是比synchronized高的,在使用中先创建一个private的Lock对象,将需要同步处理的代码上下使用Lock.lock()和Lock.unlock()包裹即可。官方表示,他们更支持synchronized,在未来的版本中还有优化余地,所以还是提倡在synchronized能实现需求的情况下,优先考虑使用synchronized来进行同步。

import java.util.concurrent.locks.Lock;
import
java.util.concurrent.locks.ReentrantLock;

class
MyThread implements Runnable {
   
private int ticket = 10;
    private
Lock ticketLock = new ReentrantLock() ;
   
@Override
   
public void run() {
       
for (int i = 0; i < 10; i++) {
           
ticketLock.lock();
            try
{
               
if (this.ticket > 0) { // 还有票
                   
try {
                        Thread.sleep(
20);
                   
} catch (InterruptedException e) {
                        e.printStackTrace()
;
                   
} // 模拟网络延迟
                   
System.out.println(Thread.currentThread().getName() + ",还有" +
                           
this.ticket-- + " 张票");
               
}
            }
finally {
               
ticketLock.unlock();
           
}
        }
    }

   
public static void main(String[] args) {
        MyThread mt =
new MyThread();
       
Thread t1 = new Thread(mt, "黄牛A");
       
Thread t2 = new Thread(mt, "黄牛B");
       
Thread t3 = new Thread(mt, "黄牛C");
       
t1.setPriority(Thread.MIN_PRIORITY);
       
t2.setPriority(Thread.MAX_PRIORITY);
       
t3.setPriority(Thread.MAX_PRIORITY);
       
t1.start();
       
t2.start();
       
t3.start();
   
}
}

6.2.9 死锁

同步的本质在于:一个线程等待另外一个线程执行完毕执行完成后才可以继续执行。但是如果现在相关的几个线程彼此之间都在等待着,那么就会造成死锁。

public class DeadLock {
   
private static Pen pen = new Pen() ;
    private static
Book book = new Book() ;
    public static void
main(String[] args) {
       
new DeadLock().deadLock();
   
}
    
public void deadLock() {
        Thread thread1 =
new Thread(new Runnable() { // 笔线程
           
@Override
           
public void run() {
               
synchronized (pen) {
                    System.
out.println(Thread.currentThread()+" :我有笔,我就不给你");
                    synchronized
(book) {
                        System.
out.println(Thread.currentThread()+" :把你的本给我!");
                   
}
                }
            }
        }
,"Pen") ;

       
Thread thread2 = new Thread(new Runnable() { // 本子线程
           
@Override
           
public void run() {
               
synchronized (book) {
                    System.
out.println(Thread.currentThread()+" :我有本子,我就不给你!");
                    synchronized
(pen) {
                        System.
out.println(Thread.currentThread()+" :把你的笔给我!");
                   
}
                }

            }
        }
,"Book") ;         thread1.start();
       
thread2.start();
   
}
}

死锁一旦出现之后,整个程序就将中断执行,所以死锁属于严重性问题。过多的同步会造成死锁,对于资源的上锁一定要注意不要成"环"。

6.2.10 ThreadLocal

ThreadLocal 用于提供线程局部变量,在多线程环境可以保证各个线程里的变量独立于其它线程里的变量。也就是说 ThreadLocal 可以为每个线程创建一个【单独的变量副本】,相当于线程的 private static 类型变量。

ThreadLocal 的作用和同步机制有些相反:同步机制是为了保证多线程环境下数据的一致性;而 ThreadLocal 是保证了多线程环境下数据的独立性。

6.2.11 生产者消费者模型

生产者消费者模型类似于早期电脑对声音的处理方式。早起电脑对声音的输出是CPU直接和喇叭产生连接,CPU需要精确的控制喇叭输出的时间、音高等内容,喇叭再进行输出。这样不仅会使得声音的输出效果有限,也会大量的浪费CPU的资源。

后来就出现了声卡,将CPU发送出来对声音的指令送到声卡中, CPU不对喇叭直接控制,CPU直接把声音的相关信息送到声卡,声卡中是一个缓冲区,平衡了喇叭和CPU之间的供求关系。

生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。

这个阻塞队列就是用来给生产者和消费者解耦的。纵观大多数设计模式,都会找一个第三者出来进行解耦,如工厂模式的第三者是工厂类,模板模式的第三者是模板类。在学习一些设计模式的过程中,如果先找到这个模式的第三者,能帮助我们快速熟悉一个设计模式。

wait()/notify()方法

wait方法是Object类中的方法,使得线程停止运行,用来将当前线程放入“预执行队列”,并且在wait()所在的代码处停止执行,直到收到通知或者被中断。wait方法只能在同步方法/同步块中调用。如果调用时没有持有适当的锁就会抛出异常。wait方法执行后,当前线程释放锁,线程与其他线程竞争重新获取锁。

public static void main(String[] args) {
   
final Object object = new Object();
    synchronized
(object){
        System.
out.println("Start!");
        try
{
            object.wait()
;
       
} catch (InterruptedException e) {
            e.printStackTrace()
;
       
}
        System.
out.println("End!");
   
}
}

sleep和wait的区别

1、sleep在Thread类中,wait在Object类中。

2、sleep在指定时间后自动唤醒,不释放对象锁;wait需要等待notify/notifyAll方法后唤醒,在代码执行完后会释放对象锁。

 

notify方法使得停止的线程继续运行,是Object类中的方法。方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。如果有多个线程等待,则有线程规划器随机挑选出一个呈wait状态的线程。

在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。

public static void main(String[] args) {
   
final Object object = new Object();
    synchronized
(object){
        System.
out.println("Start!");
        new
Thread(new Runnable() {
           
@Override
           
public void run() {
               
try {
                    Thread.sleep(
1000);
                    synchronized
(object){
                       
object.notify();
                   
}
                }
catch (InterruptedException e) {
                    e.printStackTrace()
;
               
}
            }
        }).start()
;
        try
{
            object.wait()
;
       
} catch (InterruptedException e) {
            e.printStackTrace()
;
       
}
        System.
out.println("End!");
   
}
}

notifyAll方法可以一次性唤醒所有的等待线程

6.2.12 线程池

线程池的创建:

public ThreadPoolExecutor(int corePoolSize,
                          int
maximumPoolSize,
                          long
keepAliveTime,
                         
TimeUnit unit,
                         
BlockingQueue<Runnable> workQueue,
                         
RejectedExecutionHandler handler)

相关参数:

1)corePoolSize(线程池的基本大小):当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有基本线程。

2)runnableTaskQueue(任务队列):用于保存等待执行的任务的阻塞队列。可以选择以下几个阻塞队列。

·ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按FIFO(先进先出)原则对元素进行排序。

·LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。

·SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于Linked-BlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。

PriorityBlockingQueue:一个具有优先级的无限阻塞队列。

3)maximumPoolSize(线程池最大数量):线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是,如果使用了无界的任务队列这个参数就没什么效果。

4) keepAliveTime(线程活动保持时间):线程池的工作线程空闲后,保持存活的时间。所以,如果任务很多,并且每个任务执行的时间比较短,可以调大时间,提高线程的利用率。

5) TimeUnit(线程活动保持时间的单位):可选的单位有天(DAYS)、小时(HOURS)、分钟(MINUTES)、毫秒(MILLISECONDS)、微秒(MICROSECONDS,千分之一毫秒)和纳秒(NANOSECONDS,千分之一微秒)。

6) RejectedExecutionHandler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。在JDK 1.5中Java线程池框架提供了以下4种策略。

·AbortPolicy:直接抛出异常。(默认采用此策略)

·CallerRunsPolicy:只用调用者所在线程来运行任务。

·DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。

·DiscardPolicy:不处理,丢弃掉。

 

ThreadPoolExecutor threadPoolExecutor =
       
new ThreadPoolExecutor(3,5,2000,TimeUnit.MILLISECONDS,
                new
LinkedBlockingDeque<Runnable>());

 

要想合理地配置线程池,就必须首先分析任务特性,可以从以下几个角度来分析。

任务的性质:CPU密集型任务、IO密集型任务和混合型任务。

任务的优先级:高、中和低。

任务的执行时间:长、中和短。

任务的依赖性:是否依赖其他系统资源,如数据库连接。

性质不同的任务可以用不同规模的线程池分开处理。CPU密集型任务应配置尽可能小的线程,如配置Ncpu+1个线程的线程池。由于IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如2*Ncpu。混合型的任务,如果可以拆分,将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐量将高于串行执行的吞吐量。如果这两个任务执行时间相差太大,则没必要进行分解。

可以通过Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数。

如果各个任务的优先级存在差异,可以使用PriorityBlockingQueue来处理,可以让优先级高的任务先执行。但是如果一直有优先级高的任务提交到队列里,那么优先级低的任务可能永远不能执行。

执行时间不同的任务可以交给不同规模的线程池来处理,或者可以使用优先级队列,让执行时间短的任务先执行。依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,等待的时间越长,则CPU空闲时间就越长,那么线程数应该设置得越大,这样才能更好地利用CPU。

 

向线程池提交任务:

可以使用两个方法向线程池提交任务,分别为execute()和submit()方法。execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功。

 

import java.util.concurrent.*;
class
RunnableThread implements Runnable {
   
@Override
   
public void run() {
       
for (int i = 0; i < 50; i++) {
            System.
out.println(Thread.currentThread().getName() + "" + i);
       
}
    }
}

public class Test {
   
public static void main(String[] args){
        RunnableThread runnableThread =
new RunnableThread();
       
ThreadPoolExecutor threadPoolExecutor =
               
new ThreadPoolExecutor(3,5,2000,TimeUnit.MILLISECONDS,
                        new
LinkedBlockingDeque<Runnable>());
        for
(int i = 0; i < 5; i++) {
            threadPoolExecutor.execute(runnableThread)
;
       
}
    }
}

 

submit()方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。

 

线程池的关闭:

可以通过调用线程池的shutdown或shutdownNow方法来关闭线程池。它们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。但是它们存在一定的区别。

shutdownNow首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表。shutdown只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程。只要调用了这两个关闭方法中的任意一个,isShutdown方法就会返回true。当所有的任务都已关闭后,才表示线程池关闭成功,这时调用isTerminaed方法会返回true。至于应该调用哪一种方法来关闭线程池,应该由提交到线程池的任务特性决定,通常调用shutdown方法来关闭线程池,如果任务不一定要执行完,则可以调用shutdownNow方法。

 

在Java中,使用线程来异步执行任务。Java线程的创建与销毁需要一定的开销,如果我们为每一个任务创建一个新线程来执行,这些线程的创建与销毁将消耗大量的计算资源。同时,为每一个任务创建一个新线程来执行,这种策略可能会使处于高负荷状态的应用最终崩溃。

Java的线程既是工作单元,也是执行机制。从JDK 5开始,把工作单元与执行机制分离开来。工作单元包括Runnable和Callable,而执行机制由Executor框架提供。

Java线程(java.lang.Thread)被一对一映射为本地操作系统线程。Java线程启动时会创建一个本地操作系统线程;当该Java线程终止时,这个操作系统线程也会被回收。操作系统会调度所有线程并将它们分配给可用的CPU。在上层,Java多线程程序通常把应用分解为若干个任务,然后使用用户级的调度器(Executor框架)将这些任务映射为固定数量的线程;在底层,操作系统内核将这些线程映射到硬件处理器上。

 

Executor框架最核心的类是ThreadPoolExecutor,它是线程池的实现类。通过Executor框架的工具类Executors,

可以创建3种类型的ThreadPoolExecutor。

1. 创建无大小限制的线程池:public static ExecutorService newCachedThreadPool()

2. 创建固定大小的线程池:public static ExecutorService newFixedThreadPool(int nThreads)

3. 单线程池:public static ExecutorService newSingleThreadExecutor()

 

FixedThreadPool被称为可重用固定线程数的线程池。

public static ExecutorService newFixedThreadPool(int nThreads) {

return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,

new LinkedBlockingQueue<Runnable>());

 

FixedThreadPool使用无界队列LinkedBlockingQueue作为线程池的工作队列(队列的容量为Integer.MAX_VALUE)。使用无界队列作为工作队列会对线程池带来如下影响。

1)当线程池中的线程数达到corePoolSize后,新任务将在无界队列中等待,因此线程池中 的线程数不会超过corePoolSize。

2)由于1,使用无界队列时maximumPoolSize将是一个无效参数。

3)由于1和2,使用无界队列时keepAliveTime将是一个无效参数。

4)由于使用无界队列,运行中的FixedThreadPool(未执行方法shutdown()或 shutdownNow())不会拒绝任务(不会调用RejectedExecutionHandler.rejectedExecution方法)。

FixedThreadPool适用于为了满足资源管理的需求,而需要限制当前线程数量的应用场合,适用于负载比较重的服务器。

 

SingleThreadExecutor是使用单个worker线程的Executor。

SingleThreadExecutor的corePoolSize和maximumPoolSize被设置为1。其他参数与FixedThreadPool相同。SingleThreadExecutor使用无界队列LinkedBlockingQueue作为线程池的工作队列(队列的容量为Integer.MAX_VALUE)。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值