美文网首页
java多线程

java多线程

作者: 让我们荡起双桨呀 | 来源:发表于2020-04-03 17:28 被阅读0次

    JAVA线程实现/创建方式

    Thread类本质上是实现了Runnable接口的一个实例,代表一个线程的实例。启动一个线程的唯一方法就是通过Thread类的start()实例方法。start()方法是一个native方法,它将启动一个新线程,并执行run()方法。

    public class MyThread extends Thread{
     public void run(){
     System.out.println("MyThread.run()");
     }
    }
    //启动MyThread,需要首先实例化一个Thread,并传入自己的MyThread实例:
    MyThread myThread = new MyThread();
    Thread thread = new Thread(myThread);
    thread.start();
    //事实上,当传入一个Runnable target参数给Thread后,Thread的run()方法就会调用target.run()
    public void run(){
     if (target != null){
     target.run();
     }
    }
    

    ExecutorService、Callable< Class >、Future有返回值线程

    有返回值的任务必须实现Callable接口,类似的,无返回值的任务必须Runnnable接口。执行Callable任务后,可以获取一个Future的对象,在该对象上调用get就可以获取到Callable任务返回的Object了,再结合线程池接口ExecutorService就可以实现传说中有返回结果的多线程了。

    //创建一个线程池
    ExecutorService pool = Executors.newFixedThreadPool(taskSize);
    //创建多个有返回值的任务
    List<Future> list = new ArrayList<>();
    for(int i = 0; i < taskSize; i++){
     Callable c = new MyCallable(i + "");
     //执行任务并获取Future对象
     Future f = pool.submit(c);
     list.add(f);
    }
    //关闭线程池
    pool.shutdown();
    //获取所有并发任务的运行结果
    for(Future f: list){
     System.out.println("res: " + f.get().toString());
    }
    

    基于线程池的方式

    线程和数据库连接这些资源都是非常宝贵的,那么每次需要的时候创建,不需要的时候销毁,是非常浪费资源的,那么我们可以使用缓存的策略,也就是使用线程池。

    //创建线程池
    ExecutorService threadPool = Executors.newFixedThreadPool(10);
    while(true){
     threadPool.executor(new Runnable(){ //提交多个线程,并执行
     @Override
     public void run(){
     System.out.println(Thread.currentThread().getName() + "is running...");
     try{
     Thread.sleep(3000);
     }catch(InterruptedException e){
     e.printStackTrace();
     }
     }
     })
    }
    

    4种线程池

    java里面线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而是一个执行线程的工具。真正的线程池接口是ExecutorService。

    newCachedThreadPool

    创建要给可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。对于执行很多短期异步任务的程序而言,这些线程池通常可提高程序性能。调用executor将重用以前的构造线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓冲中移除那些已有60秒未被使用的线程。因此,长时间保持空闲的线程池不会使用任何资源。

    newFixedThreadPool

    创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。在任意点,在大多数nThreads线程会处于处理任务的活动状态。如果在所有线程处于活动状态时提交附加任务,则在有可用线程之前,附加任务将在队列中等待。如果在关闭前的执行期间由于失败而导致任何线程终止,那么一个新线程将代替它执行后续的任务(如果需要)。在某个线程被显示地关闭之前,池中的线程将一直存在。

    newSchedulerThreadPool

    创建一个线程池,它可安排在给定延迟后运行命令或者定期执行。

    ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3);
    scheduledThreadPool.schedule(new Runnable(){
     @Override
     public void run(){
     System.out.println("延迟三秒");
     }
    }, 3, TimeUnit.SECONDS);
    scheduledThreadPool.scheduleAtFixedRate(new Runnable(){
     @Override
     public void run(){
     System.out.println("延迟1秒后每三秒执行一次");
     }
    }, 1, 3, TimeUnit.SECONDS);
    

    newSingleThreadExecutor

    Executors.newSingleThreadExecutor()返回一个线程池(这个池中只有一个线程),这个线程池可以在线程死后(或发生异常后)重新启动一个线程来替代原来的线程继续执行下去!

    线程生命周期(状态)

    当线程被创建并启动后,它既不是一启动就进去了执行状态,也不是一直处于执行状态。

    在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)五种状态。尤其是当线程启动以后,它不可能一直“霸占”着CPU独自运行,所以CPU需要在多条线程之间切换,于是线程状态也会有多次在运行、阻塞之间切换。

    新建状态(New)

    当程序使用 new 关键字创建了一个线程之后,该线程就处于新建状态,此时仅由JVM为其分配内存,并初始化其成员变量值

    就绪状态(Runnable)

    当线程对象调用了start()方法之后,该线程处于就绪状态,java虚拟机会为其创建方法调用栈和程序计数器,等待调度运行。

    运行状态(Running)

    如果处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,则该线程处于运行状态。

    阻塞状态(Blocked)

    阻塞状态是指线程因为某种原因放弃了CPU使用权,也即让出了cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得cpu timeslice转到运行(running)状态。则色的情况分三种:

    等待阻塞(o.wait -> 等待队列)

    运行(running)的线程执行o.wait(),JVM会把线程放入等待队列(waitting queue)中

    同步阻塞(lock -> 锁池)

    运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中

    其他阻塞(sleep/join)

    运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。

    线程死亡(dead)

    线程会一下面三种方式结束,结束后就是死亡状态。

    正常状态

    1. run()或call()方法执行完成,线程正常结束

    异常结束

    1. 线程抛出了一个未捕获的Exception或Error

    调用stop

    1. 直接调用该线程的stop()方法来结束该线程---该方法通常容易导致死锁,不推荐使用
    1585892415324.png

    终止线程四种方式

    1. 正常运行结束

    2. 使用退出标志退出线程

      一般run()方法执行完,线程就会正常结束,然而,常常有些线程时伺服线程。他们需要长时间运行,只有在外部某些条件满足的情况下,才能关闭这些线程。使用一个变量来控制循环,例如:最直接的方法就是设置一个boolean类型的标志,并通过设置这个标志为true或false来控制while循环是否退出,代码示例:

    public class ThreadSafe extends Thread{
         public volatile boolean exit = false;
           public void run(){
           while(! exit){
           //do something
           }
         }
    }
    

    定义了一个退出标志exit,当exit为true时,while循环退出,exit的默认值为false。在定义exit时,使用了一个java关键字volatile,这个关键字的目的是使exit同步,也就是说在同一时刻只能由一个线程来修改exit的值。

    1. Interrupt方法结束线程

      使用interrupt()方法来中断线程有两种情况:

      1. 线程处于阻塞状态:如使用了sleep,同步锁的wait,socket种的receiver,accept等方法时,会使线程处于阻塞状态。当调用线程的interrupt()方法时,会抛出InterruptException异常。阻塞中的那个方法抛出这个异常,通过代码捕获异常,然后break跳出循环,从而让我们有机会结束这个线程的执行。通过很多人认为只要调用interrupt方法线程就会结束,实际上是错的,一定要先捕获InterruptException异常之后通过break来跳出循环,才能正常结束run方法。

      2. 线程未处于阻塞状态:使用isInterrupted()判断线程的中断标志来退出循环。当使用interrupt()方法时,中断标志就会置为true,和使用自定义的标志来控制循环是一样的道理。

    public class ThreadSafe extends Thread{
             public void run(){
               while(!isInterrupted){ //非阻塞过程中通过判断中断标志来退出
                 try{
                   Thread.sleep(5000); //阻塞过程捕获中断异常来退出
                 }catch(InterruptedException e){
                   e.printStackTrace();
                   break; //捕获到异常之后,执行break跳出循环
                }
             }
         }
     }
    
    1. stop方法终止线程(线程不安全)

      程序中可以直接使用Thread.stop()来强行终止线程,但是stop方法是很危险的,就像突然关闭计算机电源,而不是按正常程序关机一样,可能会产生不可预料的结果,不安全主要是:Thread.stop()调用之后,创建子线程的线程就会抛出ThradDeathError的错误,并且会释放子线程所持有的锁。一般任何进行加锁的代码块,都是为了保护数据的一致性,如果在调用Thread.stop()后导致了该线程所持有的锁突然释放(不可控制),那么被保护数据就有可能呈现不一致性,其他线程在使用这些破坏的数据时,有可能导致一些很奇怪的应用程序错误。因此,并不推荐使用stop方法来终止线程。

    sleep与wait区别

    1. 对于sleep方法,属于Thread类中的。而wait方法,则属于Object类中的。

    2. sleep方法导致了程序暂停执行指定的时间,让出cpu给其他线程,但是它的监控状态依然保持着,当指定的时间到了又会自动恢复运行状态

    3. 在调用sleep方法的过程中,线程不会释放对象锁

    4. 而在调用wait方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态

    start与run区别

    1. start()方法来启动线程,真正实现了多线程运行,这时无需等待run方法体代码执行完毕,可以直接继续执行下面的代码

    2. 通过调用Thread类的start方法来启动一个线程,这时此线程是处于就绪状态,并没有运行

    3. 方法run称为线程体,它包含了要执行的这个线程的内容,线程就进入了运行状态,开始运行run函数当中的代码,run方法运行结束,此线程终止。然后cpu再调度其他线程

    java后台线程

    1. 定义:守护线程--也称“服务线程”,它是后台线程,他又一个特性,即为用户线程提供公共服务,在没有用户线程可服务时会自动离开

    2. 优先级:守护线程的优先级比较低,用于为系统中的其他对象和线程服务

    3. 设置:通过setDaemon(true)来设置线程为“守护线程”;将一个用户线程设置为守护线程的方式是在线程对象创建之前用线程对象的setDaemon方法

    4. 在Daemon线程中产生的新线程也是Daemon的

    5. 线程则是JVM级别的,以Tomcat为例,如果你在Web应用中启动一个线程,这个线程的生命周期并不会和Web应用程序保持同步。也就是说,即使你停止了Web应用,这个线程依旧是活跃的

    6. example:垃圾回收线程就是一个经典的守护线程,当我们的程序中不再有任何运行的Thread,程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是JVM上仅剩的线程时,垃圾回收线程会自动离开。它始终在低级别的状态运行,用于实时监控和管理系统中可回收资源

    7. 生命周期:守护线程时运行在后台的一种特殊线程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。也就是说守护线程不依赖于终端,但是依赖于系统,与系统“同生共死”。当JVM中所有的线程都是守护线程的时候,JVM就可以退出了,如果还有一个或以上的非守护线程则JVM不会退出

    java锁

    乐观锁

    乐观锁是一种乐观思想,即任务读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为被人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写操作。

    java中的乐观锁基本都是通过CAS操作实现的,CAS是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。

    悲观锁

    悲观锁就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人像读写这个数据就会block直到拿到锁。java中悲观锁就是Synchronized,AQS框架下的锁则是先尝试cas乐观锁去获取锁,获取不到,才会转换为悲观锁,如RetreenLock。

    自旋锁

    自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。

    线程自旋是需要消耗cpu的,说白了就是让cpu在做无用功,如果一直获取不到锁,那线程也不能一直占用cpu自旋做无用功,所以需要设定一个自旋等待的最大时间。

    如果持有锁的线程执行的时间超过自旋等待的最大时间仍没有释放锁,就会导致其他争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。

    自旋锁的优缺点

    自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能大幅度的提升,因为自旋锁的消耗小于线程阻塞挂起再唤醒的操作的消耗,这些操作会导致线程发生两次上下文切换!

    但是如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适合使用自旋锁了,因为自旋锁在获取锁前一直都是占用cpu做无用功,同时有大量线程在竞争一个锁,会导致获取锁的时间很长,线程自旋的消耗大于线程阻塞挂起操作的消耗,其他需要cpu的线程有布恩那个获取cpu,造成cpu的浪费,所以这种情况下我们要关闭自旋锁;

    自旋锁的时间阈值(1.6引入了适应性自旋锁)

    自旋锁的母的是为了占着cpu的资源不放,等到获取锁立即进行处理。但是如何去选择自旋的执行时间呢?如果自旋执行时间太长,会有大量的线程处于自旋状态占用cpu资源,进而会影响整体系统的性能,因此自旋的周期额外重要!

    JVM对于自旋周期的选择,jdk1.5这个限度时一定的写死的,在1.6引入了适应性自旋锁,适应性自旋锁意味着自选时间不再是固定的了,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定的,基本认为一个线程上下文切换的时间是最佳的一个时间,同时JVM还针对当前CPU的负荷情况做了较多的优化,如果平均负载小于CPUs则一直自旋,如果有超过(CPUs/2)个线程正在自旋,则后来线程直接阻塞,如果正在自旋的线程发现Owner发生了变化则延迟自旋时间(自旋计数)或进入阻塞,如果CPU处于节电模式则停止自旋,自旋时间的最坏情况是CPU的存储延迟(CPU A存储了一个数据,到CPU B得知这个数据直接的时间差),自旋时会适当放弃线程优先级之间的差异。

    自旋锁的开启

    JDK1.6中-XX:+UseSpinning开始;

    -XX:PreBlockSpin=10为自旋次数

    JDK1.7后,去掉此参数,由Jvm控制;

    Synchronized同步锁

    synchronized它可以把任意一个非NULL的对象当作锁,他属于独占式的悲观锁,同时属于可重入锁。

    synchronized作用范围

    1. 作用于方法时,锁住的时对象的实例(this)

    2. 当作用于静态方法时,锁住的是Class实例,又因为Class的相关数据存储在永久代PermGen(jdk1.8则是metaspace),永久代是全局共享的,因此静态方法锁相当于类的一个全局锁,会锁所有调用该方法的线程

    3. synchronized作用于一个对象实例时,锁住的是所有以该对象为锁的代码块。他有多个队列,当多线程一起访问某个对象监视器的时候,对象监视器会将这些线程存储在不同的容器中。

    synchronized核心组件

    1. wait set:哪些调用wait方法被阻塞的线程被放置在这里

    2. contention list:竞争队列,所有的请求锁的线程首先被放在这个竞争队列中

    3. entry list:contention list中那些有资格成为候选资源的线程被移动到entry list中

    4. ondeck:任意时刻,最多只有一个线程正在竞争锁资源,该线程被称为ondeck

    5. !owner:当前释放锁的线程

    synchronized实现


    1585905524490.png
    1. JVM每次从队列的尾部取出一个数据用于锁竞争候选者(OnDeck),但是并发情况下,ContentionList会被大量的并发线程进行CAS访问,为了降低对尾部元素的竞争,JVM会将一部分线程移动到EntryList种作为候选竞争线程。
    2. Owner线程会在unlock时,将ContentionList种的部分线程迁移到EntryList种,并指定EntryList种的某个线程为OnDeck线程(一般是最先进去的那个线程)
    3. 。。。

    线程基本方法
    线程相关的基本方法有wait、notify、notifyAll、sleep、join、yield等。


    image.png
    1. 线程等待(wait)
      调用该方法的线程进入WAITING状态,只有等待另外线程的通知或被中断才会返回,需要注意的是调用wait()方法之后,会释放对象的锁。因此,wait方法一般用在同步方法或同步代码块种。
    2. 线程睡眠(sleep)
      sleep导致当前线程睡眠,与wait方法不同的是sleep不会释放锁,sleep(long)会导致线程进入TIMED-WAIRTING状态,而wait方法会导致当前线程进入WAITING状态。
    3. 线程让步(yield)
      yield会使当前线程让出CPU执行时间片,与其他线程一起重新竞争CPU时间片。一般情况下,优先级高的线程有更大的可能性成功竞争得到CPU时间片,但这又不是绝对的,有的操作系统对线程优先级并不敏感。
    4. 线程中断(interrupt)
      中断一个线程,其本意是给这个线程一个通知信号,会影响这个线程内部的一个中断标识位。这个线程本身并不会因此而改变状态(如阻塞、终止等)。
      1.调用interrupt方法并不会中断一个正在运行的线程,也就是说处于Running状态的线程并不会因为被中断而被终止,仅仅改变了内部维护的中断标识位而已。
      2.若调用sleep而是线程处于TIMED-WAITING状态,这时调用interrupt方法,会抛出InterruptedException,从而使线程提前结束TIMED-WAITING状态。
      3.许多声明抛出InterruptedException的方法(如Thread.sleep(long mills)方法),抛出异常前,都会清除中断标识位,所以抛出异常后,调用isInterrupted()方法将会放回false。
      4.中断状态时线程固有的一个标识位,可以通过此标识位安全的终止线程。比如,你想终止一个线程thread的时候,可以调用thread.interrupt()方法,在线程run方法内部可以根据thread.isInterrupted()的值来优雅的终止线程。
    5. join等待其他线程终止

    join方法,等待其他线程终止,在当前线程中调用一个线程的join方法,则当前线程转为阻塞状态,等到另一个线程结束,当前线程再由阻塞状态变为就绪状态,等待cpu的调度。
    为什么要用join方法?
    很多情况下,主线程生成并启动了子线程,需要用到子线程返回的结果,也就是需要主线程再子线程结束后再结束,这时候就要用到join方法。

    System.out.println(Thread.currentThread().getName() + "线程运行开始!");
     Thread6 thread1 = new Thread6();
     thread1.setName("线程 B");
     thread1.join();
    System.out.println("这时 thread1 执行完毕之后才能执行主线程");
    

    线程唤醒(notify)
    Object类中的notify方法,唤醒在此对象监视器上等待的单个线程,如果所有线程都在此对象上等待,则会选择唤醒其中一个线程,选择是任意的,并在对实现做出决定时发生,线程通过调用其中要给wait方法,在对象的监视器上等待,知道当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程,被唤醒的线程将以常规方式与在该对象上主动同步的其他所有的线程进行竞争,类似的方法还有notifyAll,唤醒在此监视器上所有的线程。
    其他方法:
    1.sleep:强迫一个线程睡眠n毫秒
    2.isAlive:判断一个线程是否存活
    3.join:等待线程终止
    4.activeCount:程序中活跃的线程数
    5.enumerate:枚举程序中线程
    6.isDaemon:一个线程是否为守护线程
    7.currentThread:得到当前线程
    8.setDaemon:设置一个线程为守护线程,(用户线程和守护线程的区别在于,是否等待主线程依赖于主线程结束而结束)
    9.wait:强迫一个线程等待
    10.setName:为线程设置一个名称
    11.notify:通知一个线程继续运行
    12.setPriority:设置一个线程的优先级
    13.getPriority:获得一个线程的优先级

    线程上下文切换
    巧妙地利用了时间片轮转的方式。cpu给每个任务都服务一定的时间,然后把当前任务的状态保存下来,来加载下一个任务,继续服务下一个任务,任务的状态保存及再加载,这段过程就叫做上下文切换,时间片轮转的方式使多个任务在同一颗cpu上执行变成了可能。


    image.png

    进程:是指一个程序运行的实例,在Linux系统中,线程就是能并行运行并且与它们的父进程共享一个地址空间和其他资源的轻量级的进程。
    上下文:是指某一时间点cpu寄存器和程序计数器的内容
    寄存器:是cpu内部的数量较少但是速度很快的内存(与之对应的是cpu外部相对很慢的ram主内存)。寄存器通过对常用值(通常是运算的中间值)的快速访问来提高计算机程序运行的速度
    程序计数器:

    相关文章

      网友评论

          本文标题:java多线程

          本文链接:https://www.haomeiwen.com/subject/rtjkphtx.html