JAVA多线程和线程池

java并发 专栏收录该内容
3 篇文章 0 订阅

目录

1、线程状态

(1) 新建状态

(2) 就绪状态

(3) 运行状态

(4) 阻塞状态

(5) 死亡状态

2、线程优先级

3、同步工具synchronized、wait、notify

4、创建线程

(1) 实现 Runnable 接口

(2) 继承 Thread 类

(3) 通过 Callable 和 Future 创建线程

5、三种创建方式的区别

6、线程池

(1) 什么是线程池

(2) 为什么要有线程池

(3) 线程池可以干什么

(4) 线程池的创建

7、线程池的实现类(ThreadPoolExecutor)

(1) 直接提交队列

(2) 有界的任务队列

(3) 无界的任务队列

(4) 优先任务队列

(5) 几种常见的包装线程池类

(6) 拒绝策略

8、ThreadLocal 类

9、线程安全

10、synchronized 和 lock 的区别


1、线程状态

线程状态转换

(1) 新建状态

        使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。

(2) 就绪状态

        当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。

(3) 运行状态

        如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。

(4) 阻塞状态

        如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:

  • 等待阻塞,运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。wait()释放锁

  • 同步阻塞,线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。

  • 其他阻塞,通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。sleep()不释放锁

(5) 死亡状态

        一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。

2、线程优先级

        每一个 Java 线程都有一个优先级,这样有助于操作系统确定线程的调度顺序。Java 线程的优先级是一个整数,其取值范围是1(Thread.MIN_PRIORITY)到10(Thread.MAX_PRIORITY)。默认情况下,每一个线程都会分配一个优先级 NORM_PRIORITY(5)。具有较高优先级的线程对程序更重要,并且应该在低优先级的线程之前分配处理器资源。但是,线程优先级不能保证线程执行的顺序,而且非常依赖于平台。

3、同步工具synchronized、wait、notify

        他们是应用于同步问题的人工线程调度工具。讲其本质,首先就要明确monitor的概念,Java中的每个对象都有一个监视器,来监测并发代码的重入。在非多线程编码时该监视器不发挥作用,反之如果在synchronized 范围内,监视器发挥作用。

        wait/notify必须存在于synchronized块中。并且,这三个关键字针对的是同一个监视器(某对象的监视器)。这意味着wait之后,其他线程可以进入同步块执行。

        当某代码并不持有监视器的使用权时去wait或notify,会抛出java.lang.IllegalMonitorStateException。也包括在synchronized块中去调用另一个对象的wait/notify,因为不同对象的监视器不同,同样会抛出此异常。

        synchronized单独使用:

  • 代码块:如下,在多线程环境下,synchronized块中的方法获取了lock实例的monitor,如果实例相同,那么只有一个线程能执行该块内容

public class Thread1 implements Runnable { 
    Object lock; 
    public void run() { 
        synchronized(lock){ 
            //TODO 
        }
    }
}
  • 直接用于方法: 相当于上面代码中用lock来锁定的效果,实际获取的是Thread1类的monitor。更进一步,如果修饰的是static方法,则锁定该类所有实例。

public class Thread1 implements Runnable { 
    public synchronized void run() {
        //TODO 
    }
}

        多线程的内存模型:main memory(主存)、working memory(线程栈),在处理数据时,线程会把值从主存load到本地栈,完成操作后再save回去(volatile关键词的作用:每次针对该变量的操作都激发一次load and save)。

        针对多线程使用的变量如果不是volatile或者final修饰的,很有可能产生不可预知的结果(另一个线程修改了这个值,但是之后在某线程看到的是修改之前的值)。其实道理上讲同一实例的同一属性本身只有一个副本。但是多线程是会缓存值的,本质上,volatile就是不去缓存,直接取值。在线程安全的情况下加volatile会牺牲性能。

4、创建线程

        Java 提供了三种创建线程的方法:实现 Runnable 接口、继承 Thread 类、通过 Callable 和 Future 创建线程。

线程创建方法

(1) 实现 Runnable 接口

public class Test {
    public static void main(String[] args) { 
        MyRunnable runnable = new MyRunnable(); 
        Thread thread = new Thread(runnable); 
        thread.start();
    } 
} 
class MyRunnable implements Runnable{ 
    public MyRunnable() {
        //TODO
    } 
    @Override public void run() { 
        //TODO
    } 
}

(2) 继承 Thread 类

public class Test { 
    public static void main(String[] args) { 
        MyThread thread = new MyThread(); 
        thread.start(); 
    } 
} 
class MyThread extends Thread{ 
    public MyThread(){ 
        //TODO
    } 
    @Override public void run() { 
        //TODO
    } 
}

        Thread类相关方法:

//当前线程客转让CPU控制权,让别的就绪状态线程运行(切换)
public static Thread.yield()

//暂停一段时间
public static Thread.slepp()

//在一个项城中调用other.join(),将等待other线程执行完后才继续本线程
public join()

//后两个函数皆可以被打断
public interrupte()

        关于中断:

        它并不像stop方法那样会中断一个正在运行的线程。线程会不时地检测中断标识位,以判断线程是否应该被中断(中断标识值是否为true)。终端只会影响到wait状态、sleep状态和join状态。被打断的线程会抛出InterruptedException。Thread.interrupted()检查当前线程是否发生中断,返回boolean。synchronized在获锁的过程中是不能被中断的。

        中断是一个状态!interrupt()方法只是将这个状态置为true而已。所以说正常运行的程序不去检测状态,就不会终止,而wait等阻塞方法会去检查并抛出异常。

        interrupt():设置当前中断标志位为true;

        interrupted():检查当前线程是否发生中断(即中断标志位是否为true)

        设置中断标志位后,只能通过wait()、sleep()、join()判断标志位,若标志位为true,会抛出InterruptedException异常,捕获异常后,手动中断线程或进行其他操作。

        不能使用try/catch来捕获异常!需使用自定义异常处理器捕获异常,步骤如下:

1.定义异常处理器。实现 Thread.UncaughtExceptionHandler的uncaughtException方法

//第一步:定义符合线程异常处理器规范的"异常处理器",实现Thread.UncaughtExceptionHandler规范

class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler{
    //Thread.UncaughtExceptionHandler.uncaughtException()会在线程因未捕获的异常而临近死亡时被调用
    @Override
    public void uncaughtException(Thread t, Throwable e) {
        System.out.println("caught:"+e);
    }
}
2.定义使用该异常处理器的线程工厂

//第二步:定义线程工厂。线程工厂用来将任务附着给线程,并给该线程绑定一个异常处理器

class HanlderThreadFactory implements ThreadFactory{
    @Override
    public Thread newThread(Runnable r) {
        System.out.println(this+"creating new Thread");
        Thread t = new Thread(r);
        System.out.println("created "+t);
        t.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler()); //设定线程工厂的异常处理器
        System.out.println("eh="+t.getUncaughtExceptionHandler());
        return t;
    }
}

        三、四步为测试:

3.定义一个任务,让其抛出一个异常

//第三步:我们的任务可能会抛出异常。显示的抛出一个exception

class ExceptionThread implements Runnable{
    @Override
    public void run() {
        Thread t = Thread.currentThread();
        System.out.println("run() by "+t);
        System.out.println("eh = "+t.getUncaughtExceptionHandler());
        throw new RuntimeException();
    }
}
4.调用实验

//第四步:使用线程工厂创建线程池,并调用其execute方法

public class ThreadExceptionUncaughtExceptionHandler{
    public static void main(String[] args){
        ExecutorService exec = Executors.newCachedThreadPool(new HanlderThreadFactory());
        exec.execute(new ExceptionThread());
    }
}

(3) 通过 Callable 和 Future 创建线程

        上述两种创建线程的方法,在执行完任务之后无法获取执行结果。如果需要获取执行结果,就必须通过共享变量或者使用线程通信的方式来达到效果。而Callable和Future可以在任务执行完毕之后得到任务执行结果。通过以下四种方法创建线程:

  • 创建 Callable 接口的实现类,并实现 call() 方法,该 call() 方法将作为线程执行体,并且有返回值。

  • 创建 Callable 实现类的实例,使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封装了该 Callable 对象的 call() 方法的返回值。

  • 使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程。

  • 调用 FutureTask 对象的 get() 方法来获得子线程执行结束后的返回值。

5、三种创建方式的区别

        (1) 继承Thread类创建的线程可以拥有自己独立的类成员变量,但是实现Runnable接口创建线程共享实现接口类的成员变量。两中方式创建线程都要重写run方法,run方法是线程的执行体。(Thread和runnable均可以实现单独资源和共享资源)

        Eg.

        a、Thread类:

        启动两个线程,每个线程拥有单独的成员变量

class MyThread extends Thread{
    //TODO 
} 
new MyThread().start(); 
new MyThread().start();

        启动两个线程,两个线程共享成员变量

MyThread m = new MyThread(); 
new Thread(m).start(); 
new Thread(m).start();

        b、Runnable接口:

        启动两个线程,共同享有成员变量

class MyThread implements Runnable { 
    //TODO
}

MyThread m = new MyThread(); 
new Thread(m).start(); 
new Thread(m).start();

        启动两个线程,每个线程拥有单独的成员变量

MyThread myThread = new MyThread(); 
MyThread myThread2 = new MyThread(); 
new Thread(myThread).start(); 
new Thread(myThread2).start();

        (2) 在继承Thread类创建进程中可以通过使用this获得当前进程的对象,但是在实现Runnable接口创建线程的途径中可以使用Thread.currentThread()方式来获得当前进程。

        (3) 第三种方式是较为复杂的一种。Callable接口是一个与Runnable接口十分相似的接口。在Runnable中run方法为线程的执行体,但是在Callable接口中call方法是线程的执行体。下面是两个接口实现执行体的不同:

  • call方法有返回值,但是run方法没有

  • call方法可以生命抛出异常

        所以可以说Callable接口是Runnable接口的增强版本。

        (4)  FutureTask类实现了Runnable和Future接口。和Callable一样都是泛型。

        (5)  Future接口是对Callable任务的执行结果进行取消,查询是否完成,获取结果的。下面是这个接口的几个重要方法:

  • boolean cancel(boolean myInterruptRunning),试图取消Future与Callable关联的任务

  •  V get(), 返回Callable任务中call方法的返回值,调用该方法会导致程序阻塞,必须等到子线程结束才会有返回值。这里V表示泛型

  • V get(long timeout, TimeUnit  unit), 返回Callable中call方法的返回值,该方法让程序最多阻塞timeout毫秒的时间,或者直到unit时间点。如果在指定的时间Callable的任务没有完成就会抛出异常TimeoutEexception

  • boolean  isCancelled(), 如果Callable中的任务被取消,则返回true,否则返回false

  • boolean isDone(),如果Callable中的任务被完成,则返回true,否则返回false

6、线程池

(1) 什么是线程池

        线程池,其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源

(2) 为什么要有线程池

        在java中,如果每个请求到达就创建一个新线程,开销是相当大的。在实际使用中,创建和销毁线程花费的时间和消耗的系统资源都相当大,甚至可能要比在处理实际的用户请求的时间和资源要多的多。除了创建和销毁线程的开销之外,活动的线程也需要消耗系统资源。如果在一个jvm里创建太多的线程,可能会使系统由于过度消耗内存或"切换过度"而导致系统资源不足。为了防止资源不足,需要采取一些办法来限制任何给定时刻处理的请求数目,尽可能减少创建和销毁线程的次数,特别是一些资源耗费比较大的线程的创建和销毁,尽量利用已有对象来进行服务。

(3) 线程池可以干什么

        线程池主要用来解决线程生命周期开销问题和资源不足问题。通过对多个任务重复使用线程,线程创建的开销就被分摊到了多个任务上了,而且由于在请求到达时线程已经存在,所以消除了线程创建所带来的延迟。这样,就可以立即为请求服务,使用应用程序响应更快;另外,通过适当的调整线程中的线程数目可以防止出现资源不足的情况。

(4) 线程池的创建

        线程池都是通过线程池工厂创建,再调用线程池中的方法获取线程,再通过线程去执行任务方法。

  • Executors:线程池创建工厂类

  • public static ExecutorService newFixedThreadPool(int nThreads):返回线程池对象

  • ExecutorService:线程池类

  • Future<?> submit(Runnable task):获取线程池中的某一个线程对象,并执行

  • Future 接口:用来记录线程任务执行完毕后产生的结果。线程池创建与使用

        a、使用Runnable接口创建线程池

  • 创建线程池对象

  • 关闭线程池

  • 提交 Runnable 接口子类对象

  • 创建 Runnable 接口子类对象

public static void main(String[] args) {
    //创建线程池对象  参数5,代表有5个线程的线程池
    ExecutorService service = Executors.newFixedThreadPool(5);

    //创建Runnable线程任务对象
    TaskRunnable task = new TaskRunnable();
        
    //从线程池中获取线程对象
    service.submit(task);
    System.out.println("----------------------");
        
    //再获取一个线程对象
    service.submit(task);
        
    //关闭线程池
    service.shutdown();
}

        b、使用Callable接口创建线程池

        ExecutorService:线程池类

        <T> Future<T> submit(Callable<T> task):获取线程池中的某一个线程对象,并执行线程中的 call() 方法

        Future 接口:用来记录线程任务执行完毕后产生的结果。线程池创建与使用

  • 创建线程池对象

  • 创建 Callable 接口子类对象

  • 提交 Callable接口子类对象

  • 关闭线程池

public static void main(String[] args) {
       
    ExecutorService service = Executors.newFixedThreadPool(3);
    TaskCallable c = new TaskCallable();
        
    //线程池中获取线程对象,调用run方法
    service.submit(c);
        
    //再获取一个
    service.submit(c);
        
    //关闭线程池
    service.shutdown();
}

7、线程池的实现类(ThreadPoolExecutor)

        ThreadPoolExecutor 类继承了 AbstractExecutorService 类,而 AbstractExecutorService 类实现了 ExecutorService 接口。所以上述线程池创建的方法可以将创建的线程池(例如newFixedThreadPool)赋给 ExecutorService。

        ThreadPoolExecutor 构造函数如下:

ThreadPoolExecutor ( int corePoolSize, // 线程池中的线程数量

                    int maximumPoolSize, // 线程池中的最大线程数量

                    long keepAliveTime, // 当线程池线程数量超过corePoolsize时,多余的空闲线程的存活时间,即超过corePoolSize的空闲线程,在keepAliveTime时间内会被销毁
                    
                    TimeUnit unit, // keepAliveTime的单位

                    BlockingQueue<Runnable> workQueue, // 任务队列,被提交但尚未被执行的任务

                    ThreadFactory threadFactory, // 线程工厂,用于创建线程,一般用默认的线程工厂即可

                    RejectedExecutionHandler handler) // 拒绝策略。当任务太多来不及处理时,如何拒绝任务

        关键参数:workQueue

(1) 直接提交队列

        由 SynchronousQueue 实现,一种无缓冲的等待队列。

        SynchronousQueue 没有容量,即没有等待队列,总是将新任务提交给线程去执行,当没有空闲线程时,就新增一个线程,当线程数达到最大值maximumPoolSize时,即无法再新增线程时,则执行拒绝策略。

(2) 有界的任务队列

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

        当有新任务需要执行时,如果线程池的实际线程数小于 corePoolSize,则会新增一个线程,若大于 corePoolSize,则会将新任务加入等待队列。若队列已满,则在总线程数不大于maximumPoolSize 的前提下,新增一个线程,若大于 maximumPoolSize,则执行拒绝策略。也就是说,只有当等待队列满了的时候,才可能将线程数增加到 corePoolSize 以上。也就是说,除非系统非常繁忙,否则线程数量基本维持在 corePoolSize。

(3) 无界的任务队列

        由 LinkedBlockingQueue 实现,其内部维护了一个链表(如果没有指定长度,则默认容量为无穷大),LinkedBlockingQueue 之所以能够高效的处理并发数据,是因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。作为开发者,我们需要注意的是,如果构造一个 LinkedBlockingQueue 对象,而没有指定其容量大小,LinkedBlockingQueue 会默认一个类似无限大小的容量(Integer.MAX_VALUE),这样的话,如果生产者的速度一旦大于消费者的速度,也许还没有等到队列满阻塞产生,系统内存就有可能已被消耗殆尽了。

        与有界队列相比,除非系统资源耗尽,否则无界队列不存在任务入队失败的情况,即无界队列的长度是无穷大。当有新的任务需要执行时,若线程池的实际数量小于corePoolSize,则会新增一个线程,且线程池的最大线程数为corePoolSize。若生产者的速度远小于消费者的速度,则等待队列会快速增长,直至系统资源耗尽。

(4) 优先任务队列

        由 PriorityBlockingQueue 实现,其内部维护了一个数组,优先级的判断通过构造函数传入的 Comparator 对象来决定,需要注意的是 PriorityBlockingQueue 并不会阻塞数据生产者,而只会在没有可消费的数据时,阻塞数据的消费者。因此使用的时候要特别注意,生产者生产数据的速度绝对不能快于消费者消费数据的速度,否则时间一长,会最终耗尽所有的可用堆内存空间。在实现 PriorityBlockingQueue 时,内部控制线程同步的锁采用的是公平锁。

这是一个有优先级的无界队列。

(5) 几种常见的包装线程池类

  • newFixedThreadPool:设置 corePoolSize 和 maximumPoolSize 相等,使用无界的任务队列(LinkedBlockingQueue)

  • newSignalThreadExecutor:newFixedThreadPool 的一种特殊形式,即 corePoolSize为1

  • newCachedThreadPool:设置 corePoolSize 为0,maximumPoolSize 为无穷大,使用直接提交队列(SynchronousQueue)

(6) 拒绝策略

  • AbortPolicy:直接抛出异常,阻止系统正常工作

  • CallerRunsPolicy:由调用线程直接执行当前任务,可能会造成任务提交线程(即调用线程)的性能急剧下降

  • DiscardOldestPolicy:丢弃等待队列头的一个任务,并再次提交该任务

  • DiscardPolicy:丢弃提交任务,即什么都不做

8、ThreadLocal 类

        用处:保存线程的独立变量。对一个线程类(继承自Thread),当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。常用于用户登录控制,如记录session信息。

        实现:每个Thread都持有一个TreadLocalMap类型的变量(该类是一个轻量级的Map,功能与map一样,区别是桶里放的是entry而不是entry的链表。功能还是一个map。)以本身为key,以目标为value。主要方法是get()和set(T a),set之后在map里维护一个threadLocal -> a,get时将a返回。ThreadLocal是一个特殊的容器。

        ThreadLocal和线程同步机制都是为了解决多线程中相同变量的访问冲突问题。同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

        #########################################################################

        每个Thread持有一个ThreadLocalMap,key-value,key为弱引用,value为强引用,解决方法间共享和实例线程间隔离;静态变量只能解决方法间共享

        某一个类的多个实例线程,多个类的多个实例线程

        ThreadLocal解决的是某类的多个实例线程间的隔离问题,ThreadLocal属于一个类,是一个类的私有变量。

        普通的私有变量可以保证不同类之间相互隔离,不能保证同一个类不同实例间的相互隔离,而ThreadLocal却可以。例如,

public class ThreadRunnableDemo implement Runnable{ 
    private int a; 
    private ThreadLocal<String> b; 
}

        (1) 分别使用不同 Runnable 生成 Thread

Thread t1 = new Thread(new ThreadRunnableDemo ()); 
Thread t2 = new Thread(new ThreadRunnableDemo ());

        (2) 用同一个 Runnable 生成 Thread

ThreadRunnableDemo t = new ThreadRunnableDemo (); 
Thread t1 = new Thread(t); 
Thread t2 = new Thread(t);

        对于上述两种情况:

        a、(1) 中可保证变量a是线程间隔离的(即线程安全的);而 (2) 中却不可以,因为他们用同一个Runnable去生成Thread,由于变量a是属于Runnable的,所以会产生线程安全问题。

        b、(1)(2)中 ThreadLocal 是线程安全的,即使使用同一个Runnable生成Thread,他们也各自使用一个变量副本。

9、线程安全

        Synchronized、volatile、原子类、Lock锁

10、synchronized 和 lock 的区别

        Synchronized是关键字;不支持等待超时中断;读操作之间仍然是互斥的,不能同时进行;不需要手动释放锁

        Lock是一个类;可以只等待一定的时间或者能够响应中断;读操作之间不互斥,可以同时进行;需要手动释放锁

 

  • 5
    点赞
  • 0
    评论
  • 27
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值