Java多线程知识总结

多线程知识总结

在日常开发过程中,作为一个需要经常编写代码的码农,多线程的知识在开始进入编程工作时,或许多线程的知识并不必要,可当深入代码的海洋,总是会遇到一些使用场景,传统的单线程编程无法应对,此时就需要采用多线程技术处理复杂的应用场景,进而获得更好的用户体验。

另外,在因特网上下载一个文件时,多会提示如下的内容:

延迟下载,可以理解为,快速响应,延迟下载。本质上就是多线程的一种下载。

 在bm审查期间,和同事合作,抽空优化了同事开发的考勤项目,通过代码重构手法以及多线程的使用,程序的运行耗时从一分钟多降低到了2秒钟,强有力的保障了用户的体验,而且使得页面超时问题不再出现。在这个过程中,还是要多感谢自己的工作经验,其中有两个较为显著的地方:

  1. 代码重构手法,用以理解代码的组织,通过调整结构使得关键耗时节点得以明晰
  2. 多线程机制的使用,关键是找到多线程机制的使用地方,在处理考勤业务时核心时处理多个电子表格并行处理,并且将处理每个电子表格中的一条记录也并行,通过两处并行将处理速度优化了很多倍,也增加对多CPU的利用率
  3. Lambda并行处理,之前一直在同事中宣贯Lambda的用法,甚至有点滥用的迹象,但没有办法,为了推广必须矫枉过正。另外一个较为关键的是就是Lambda并行流处理,这使得Java的多线程触发更加灵活方便,一两行代码的变化就能使程序的变化非常的显著。真正实现了四两拨千斤的作用。

也正是体会到了多线程的强大,因此自己在bm审查期间花了两天的时间,认真的总结了多线程的概念,通过阅读**《Java 疯狂讲义》**深入的理解多线程的方方面面,通过阅读的过程,自己对于多线程的认识和理解确实到了一个新的高度。本文就是基于在阅读时整理的XMind,梳理成文。以期能对多线程有帮助的程序员有所帮助。

线程概述

在线程概述中,较为重要的是理解线程和进程的概念以及理解上的区别。

进程

其实理解起来也并不复杂,进程的概念是相对宏观的,进程可以理解成QQ和微信。当我们在日常工作生活中,QQ和微信两个程序时互不干扰的,这是两个进程,而不是两个线程。但在QQ程序内部,却有可能存在多个线程,比如一个线程和某个人聊天,另外一个线程则是批量下载QQ中的图片。

  • 操作系统的多任务就是多进程。
  • 一个进程可以拥有多个线程。
  • 操作系统创建一个进程时,必须为该进程分配独立的内存空间,并分配大量的相关资源。

线程

  • 单线程程序: 只有一个顺序的执行流,在Java世界中,该线程为main函数所在的线程
  • 多线程的程序:可以包括多个顺序执行流,多个顺序执行流之间互不干扰。每个顺序执行流就是一个线程

线程的优点如下所示:

线程的创建和启动

线程的创建和启动,指的是在Java的世界里,如何通过编程实现一个线程的创建,并启动该线程实现具体的业务。

Thread

 Java 提供了编写一个类来继承Thread,然后覆写run方法,然后调用start方法来启动线程。这时这个类就会以另一个线程的方式来运行run方法里面的代码。另一种是编写一个类来实现Runnable接口,然后实现接口方法run,然后创造一个Thread对象,把实现了Runnable接口的类当做构造参数,传入Thread对象,最后该Thread对象调用start方法。
 这里的start方法是一个有启动功能的方法,该方法内部回调run方法。所以,只有调用了start方法才会启动另一个线程,直接调用run方法,还是在同一个线程中执行run,而不是在另一个线程执行run
此外,start方法只是告诉虚拟机,该线程可以启动了,也就说该线程在就绪的状态,但不代表调用start就立即运行了,这要等待JVM来决定什么时候执行这个线程。也就是说,如果有两个线程A,B ,A先调用start,B后调用start,不代表A线程先运行,B线程后运行。这都是由JVM决定了,可以认为是随机启动。

public class ExampleThread extends Thread {
    @Override
    public void run() {
        super.run();
        System.out.println("这是一个继承自Thread的ExampleThread");
    }
}

Runnable

另一种,实现了Runnable接口

public class ExampleRunable  implements Runnable{

    public void run() {
        System.out.println("这是实现Runnable接口的类");
    }
}

package java.lang;

/**
 * The <code>Runnable</code> interface should be implemented by any
 * class whose instances are intended to be executed by a thread. The
 * class must define a method of no arguments called <code>run</code>.
 * <p>
 * This interface is designed to provide a common protocol for objects that
 * wish to execute code while they are active. For example,
 * <code>Runnable</code> is implemented by class <code>Thread</code>.
 * Being active simply means that a thread has been started and has not
 * yet been stopped.
 * <p>
 * In addition, <code>Runnable</code> provides the means for a class to be
 * active while not subclassing <code>Thread</code>. A class that implements
 * <code>Runnable</code> can run without subclassing <code>Thread</code>
 * by instantiating a <code>Thread</code> instance and passing itself in
 * as the target.  In most cases, the <code>Runnable</code> interface should
 * be used if you are only planning to override the <code>run()</code>
 * method and no other <code>Thread</code> methods.
 * This is important because classes should not be subclassed
 * unless the programmer intends on modifying or enhancing the fundamental
 * behavior of the class.
 *
 * @author  Arthur van Hoff
 * @see     java.lang.Thread
 * @see     java.util.concurrent.Callable
 * @since   JDK1.0
 */
@FunctionalInterface
public interface Runnable {
    /**
     * When an object implementing interface <code>Runnable</code> is used
     * to create a thread, starting the thread causes the object's
     * <code>run</code> method to be called in that separately executing
     * thread.
     * <p>
     * The general contract of the method <code>run</code> is that it may
     * take any action whatsoever.
     *
     * @see     java.lang.Thread#run()
     */
    public abstract void run();
}

从上述类定义可以看出,Runnable类注解了函数式接口,因此可以通过Lambda的格式来实现Runnable。这也是当前较为常用的实现方式。

你不知道的Runnable接口,深度解析Runnable接口

JDK8中Lambda深入理解和Stream实践

可以通过上文深入理解Lambda来实现Runnable,语法很凝练,也很方便。

另一个重要的好处就是,因为Runnable是一个接口,在自定义类实现该接口的同时,仍然可以继承其他类,而不像Thread类,由于Java的单继承,是无法继承其他的类

Callable和Future

 当实现Callable接口之后,要重写接口中的call方法,call方法中的代码作为线程执行体,此时的call方法可以有返回值。

​ Callable接口是Java5中新增的接口,不是Runnable接口的子接口,所以Callable对象不能直接作为Thread对象的Target,于是Java5中提供了Future接口来代表Callable接口里call方法的返回值,并为Future接口提供了一个FutureTask实现类,它实现了Future接口和Runnable接口,可以作为Thread类的Target。所以在创建Callable

接口实现类之后,要用FutureTask来包装Callable对象(实现手动装箱)。然后用FutureTask对象作为Target。

package java.util.concurrent;

/**
 * A task that returns a result and may throw an exception.
 * Implementors define a single method with no arguments called
 * {@code call}.
 *
 * <p>The {@code Callable} interface is similar to {@link
 * java.lang.Runnable}, in that both are designed for classes whose
 * instances are potentially executed by another thread.  A
 * {@code Runnable}, however, does not return a result and cannot
 * throw a checked exception.
 *
 * <p>The {@link Executors} class contains utility methods to
 * convert from other common forms to {@code Callable} classes.
 *
 * @see Executor
 * @since 1.5
 * @author Doug Lea
 * @param <V> the result type of method {@code call}
 */
@FunctionalInterface
public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}

Java多线程的三种实现方式(重点看Collable接口实现方式)

对比

线程的生命周期

线程状态

线程转换图

IllegalThreadStateException

在多线程编程中常见的异常,常见发生的时机为如下图所示:

控制线程

控制线程的主要内容如下图所示:

join

join() 的作用:让“主线程”等待“子线程”结束之后才能继续运行。

// 主线程
public class Father extends Thread {
    public void run() {
        Son s = new Son();
        s.start();
        s.join();
        ...
    }
}
// 子线程
public class Son extends Thread {
    public void run() {
        ...
    }
}

上面的有两个类Father(主线程类)和Son(子线程类)。因为Son是在Father中创建并启动的,所以,Father是主线程类,Son是子线程类。
在Father主线程中,通过new Son()新建“子线程s”。接着通过s.start()启动“子线程s”,并且调用s.join()。在调用s.join()之后,Father主线程会一直等待,直到“子线程s”运行完毕;在“子线程s”运行完毕之后,Father主线程才能接着运行。 这也就是我们所说的“join()的作用,是让主线程会等待子线程结束之后才能继续运行”!

Java多线程系列–“基础篇”08之 join()

后台线程

线程睡眠

由于sleep()方法是Thread类的方法,因此它不能改变对象的锁。所以当在一个Synchronized方法中调用sleep()时,线程虽然休眠了,但是对象的机锁没有被释放,其他线程仍然无法访问这个对象。而wait()方法则会在线程休眠的同时释放掉机锁,其他线程可以访问该对象。

​ wait()方法和notify()方法:当一个线程执行到wait()方法时(线程休眠且释放机锁),它就进入到一个和该对象相关的等待池中,同时失去了对象的机锁。当它被一个notify()方法唤醒时,等待池中的线程就被放到了锁池中。该线程从锁池中获得机锁,然后回到wait()前的中断现场。

​ yield()方法是停止当前线程,让同等优先权的线程运行。如果没有同等优先权的线程,那么Yield()方法将不会起作用。 join()方法使当前线程停下来等待,直至另一个调用join方法的线程终止。线程的在被激活后不一定马上就运行,而是进入到可运行线程的队列中。

改变线程优先级

我们可以设置线程的优先级来让CPU尽可能的将执行的资源给优先级高的线程。Java设置了1-10这10个优先级,又有三个静态变量来提供三个优先级:

    /**
     * The minimum priority that a thread can have.
     */
    public final static int MIN_PRIORITY = 1;

   /**
     * The default priority that is assigned to a thread.
     */
    public final static int NORM_PRIORITY = 5;

    /**
     * The maximum priority that a thread can have.
     */
    public final static int MAX_PRIORITY = 10;

我们可以通过setPriority来设置线程的优先级,可以直接传入上诉三个静态变量,也可以直接传入1-10的数字。设置后线程就会有不同的优先级。如果我们不设置优先级,会是什么情况?
线程的优先级是有继承的特性,如果我们在A线程中启动了B线程,则AB具有相同的优先级。一般我们在main线程中启动线程,就和main线程有一致的优先级。main线程的优先级默认是5。

下面说一下优先级的一些规则:

  1. 优先级高的线程一般会比优先级低的线程获得更多的CPU资源,但是不代表优先级高的任务一定先于优先级低的任务先执行完。因为不同优先级的线程中run方法内容可能不一样。
  2. 优先级高的线程一定会比优先级低的线程执行的快。如果两个线程是一样的run方法,但是优先级不一样,确实优先级高的线程先执行完。

线程同步

线程同步大抵上是线程最核心也是最重要的方面了。

​ 因为当我们有多个线程要同时访问一个变量或对象时,如果这些线程中既有读又有写操作时,就会导致变量值或对象的状态出现混乱,从而导致程序异常。举个例子,如果一个银行账户同时被两个线程操作,一个取100块,一个存钱100块。假设账户原本有0块,如果取钱线程和存钱线程同时发生,会出现什么结果呢?取钱不成功,账户余额是100.取钱成功了,账户余额是0.那到底是哪个呢?很难说清楚。因此多线程同步就是要解决这个问题。

[java]线程–同步代码块

Java 多线程同步的五种方法

线程同步的基本内容如下图所示:

线程安全问题

同步代码块

如果同步方法是需要执行一个很长时间的任务,那么多线程在排队处理同步方法时就会等待很久,但是一个方法中,其实并不是所有的代码都需要同步处理的,只有可能会发生线程不安全的代码才需要同步。这时,可以采用synchronized来修饰语句块让关键的代码进行同步。用synchronized修饰同步块,其格式如下:

synchronized(对象){
	//语句块
}

这里的对象,可以是当前类的对象this,也可以是任意的一个Object对象,或者间接继承自Object的对象,只要保证synchronized修饰的对象被多线程访问的是同一个,而不是每次调用方法的时候都是新生成就就可以。但是特别注意String对象,因为JVM有String常量池的原因,所以相同内容的字符串实际上就是同一个对象,在用同步语句块的时候尽可能不用String。
下面,看一个例子来说明同步语句块的用法和与同步方法的区别:

public class LongTimeTask {
    private String getData1;
    private String getData2;

    public void doLongTimeTask(){
        try{
            System.out.println("begin task");
            Thread.sleep(3000);
            String privateGetData1 = "长时间处理任务后从远程返回的值 1 threadName=" + Thread.currentThread().getName();
            String privateGetData2 = "长时间处理任务后从远程返回的值 2 threadName=" + Thread.currentThread().getName();

            synchronized (this){
                getData1 = privateGetData1;
                getData2 = privateGetData2;
            }

            System.out.println(getData1);
            System.out.println(getData2);
            System.out.println("end task");
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}

public class LongTimeServiceThreadA extends Thread{

    private LongTimeTask task;
    public LongTimeServiceThreadA(LongTimeTask task){
        super();
        this.task = task;
    }

    @Override
    public void run() {
        super.run();
        CommonUtils.beginTime1 = System.currentTimeMillis();
        task.doLongTimeTask();
        CommonUtils.endTime1 = System.currentTimeMillis();
    }
}

public class LongTimeServiceThreadB extends Thread{

    private LongTimeTask task;
    public LongTimeServiceThreadB(LongTimeTask task){
        super();
        this.task = task;
    }

    @Override
    public void run() {
        super.run();
        CommonUtils.beginTime2 = System.currentTimeMillis();
        task.doLongTimeTask();
        CommonUtils.endTime2 = System.currentTimeMillis();
    }
}

测试的代码如下:

public class LongTimeServiceThreadATest extends TestCase {

    public void testRun() throws Exception {
        LongTimeTask task = new LongTimeTask();
        LongTimeServiceThreadA threadA = new LongTimeServiceThreadA(task);
        threadA.start();

        LongTimeServiceThreadB threadB = new LongTimeServiceThreadB(task);
        threadB.start();

        try{
            Thread.sleep(1000 * 10);
        }catch (InterruptedException e){
            e.printStackTrace();
        }

        long beginTime = CommonUtils.beginTime1;
        if (CommonUtils.beginTime2 < CommonUtils.beginTime1){
            beginTime = CommonUtils.beginTime2;
        }

        long endTime = CommonUtils.endTime1;
        if (CommonUtils.endTime2 < CommonUtils.endTime1){
            endTime = CommonUtils.endTime2;
        }
        System.out.println("耗时:" + ((endTime - beginTime) / 1000));

        Thread.sleep(1000 * 20);
    }

}

结果如下:

begin task
begin task
长时间处理任务后从远程返回的值 1 threadName=Thread-1
长时间处理任务后从远程返回的值 2 threadName=Thread-1
end task
长时间处理任务后从远程返回的值 1 threadName=Thread-1
长时间处理任务后从远程返回的值 2 threadName=Thread-1
end task
耗时:3

两个线程并发处理耗时任务只用了3s, 因为只在赋值的时候进行同步处理,同步语句块以外的部分都是多个线程异步处理的。
下面,说一下同步语句块的一些特性:

  1. 当多个线程同时执行synchronized(x){}同步代码块时呈同步效果。
  2. 当其他线程执行x对象中的synchronized同步方法时呈同步效果。
  3. 当其他线程执行x对象中的synchronized(this)代码块时也呈现同步效果。

细说一下每个特性,第一个特性上面的例子已经阐述了,就不多说了。第二个特性,因为同步语句块也是对象锁,所有当对x加锁的时候,x对象内的同步方法也呈现同步效果,当x为this的时候,该对象内的其他同步方法也要等待同步语句块执行完,才能执行。第三个特性和上面x为this是不一样的,第三个特性说的是,x对象中有一个方法,该方法中有一个synchronized(this)的语句块的时候,也呈现同步效果。即A线程调用了对x加锁的同步语句块的方法,B线程在调用该x对象的synchronized(this)代码块是有先后的同步关系。

上面说同步语句块比同步方法在某些方法中执行更有效率,同步语句块还有一个优点,就是如果两个方法都是同步方法,第一个方法无限在执行的时候,第二个方法就永远不会被执行。这时可以对两个方法做同步语句块的处理,设置不同的锁对象,则可以实现两个方法异步执行。

注意同步监视器的使用流程为:加锁、修改、释放锁

[java]线程–同步代码块

同步方法

其实,无论是同步代码块和同步方法,同步监视器都是存在的,只不过同步方法的同步监视器是this,即调用该方法的对象。

释放同步监视器的锁定

同步锁

  1. 即Lock接口及其子接口所代表的对象;
  2. 它是一种显式锁,它自己本身就是同步监视对象,它的用法是和try-final块搭配使用:

死锁

线程通信

线程通信

线程通信的主要内容如下:

传统的线程通信

wait(),notify(),notifyAll()

Object提供了三个方法wait(), notify(), notifyAll()在线程之间进行通信,以此来解决线程间执行顺序等问题。

  • wait():释放当前线程的同步监视控制器,并让当前线程进入阻塞状态,直到别的线程发出notify将该线程唤醒。
  • notify():唤醒在等待控制监视器的其中一个线程(随机)。只有当前线程释放了同步监视器锁(调用wait)之后,被唤醒的线程才有机会执行。
  • notifyAll():与上面notify的区别是同时唤醒多个等待线程。

值得注意的是这三个方法是属于Object而不是属于Thread的,但是调用的时候必须用同步监视器来调用,

  • 对于synchronized修饰的同步方法,由于方法所在类对象(this)就是同步监视器,因此可以直接在同步方法中调用这三个方法;
  • 对于同步代码块,synchronized(obj) { … },则需要用空号钟的obj来调用。

生产者-消费者问题模型

在经典的生产者-消费者问题中,需要使用线程通信来解决。

假设有这么一个场景,有一个线程需要存钱进一个账户,有多个线程需要从这个账户取钱,要求是每次必须先存钱之后才能取钱,而且取钱之后必须存钱,

存钱和取钱不能同时发生两次,而是要保持顺序不变,如何实现这个需求呢。

下面是用同步方法结合线程通信的方式来实现的思路,

  • 首先在Account类中定义两个同步方法,deposit和draw用来确保存款和取款操作的原子性。
  • 在Account类中定义用标识符flag, 由deposit和draw共用。初始值为false,表示只能存款。 如果为false,表示只能取款。
  • 定义一个存款线程类,去调用Account类的同步方法deposit,在deposit中先对flag进行判断,如果不为false,则调用wait阻塞存款线程,等待取款线程发出notice。存款完成之后,将flag改为true.
  • 定义一个取款线程类,去调用Account类的同步方法draw,在draw中先对flag进行判断,如果不为true,则调用wait阻塞取款线程,等待存线程发出notice。取款完成之后,将flag改为false.
  • 定义测试类,同时启动一个(或多个)存款线程进行存款,同时启动多个取款线程去取款,存款(取款)线程之间不会有先后顺序,但是存款和取款直接会有严格的先后顺序,这就解决了生产者消费者问题

其中实现的内容为了阅读方便,如下所示:

使用Condition控制线程通信

如果程序使用lock来同步线程的话,就要使用condition来进行线程通信。

在lock同步线程中,lock 对象就是一个显示的同步监视器,但是这个显示的同步监视器不直接阻塞或者通知线程,而是通过condition——lock对象通过调用newCondition方法返回一个与lock关联的condition对象,由condition对象来控制线程阻塞(await)和发出信号(single)唤醒其他线程。

与synchronized同步线程方式对应的是,conditions方式也提供了三个方法,

  • await:类似于synchronized隐式同步控制器对象调用的wait方法,可以阻塞当前线程,直到在别的线程中调用了condition的singal方法唤醒该线程。

  • signal:随机唤醒一个被await阻塞的线程。注意只有在当前线程已经释放lock同步监视器之后,被唤醒的其他线程才有机会执行。

  • signalAll:与上面类似,但是是唤醒所有线程。

阻塞队列控制线程通信

BlockingQueue是JAVA5提供的一个队列接口,但这个队列并不是用作一个容器,而是作为线程的同步工具。

它可以很好地解决生产者消费者问题,而且比前面提到的两种方式更为灵活,

BlockingQueue的特征是,

当生产者线程试图向BlockingQueue存入元素时,如果队列已满,生产者线程将会阻塞,

当消费者线程试图从BlockingQueue取出元素时,如果队列为空,消费者线程将会阻塞

对比前面线程通信的例子,synchronized同步方法/代码块和lock+condition方式中,都只能控制生产者和消费者按固定顺序执行,

但BlockingQueue则是可以通过集合中的元素个数(商品数量)来控制线程执行顺序,通过调整集合容量可以控制线程切换的条件。

集合(商品)为空时,消费者阻塞,只能执行生产者线程;集合(商品)已满时,生产者阻塞,只能执行消费者线程。

线程组和未处理的线程

线程组ThreadGroup分析详解 多线程中篇(三)

一个线程组核心的信息是:名称、优先级、是否守护、父线程组、子线程组

线程池

线程池的基本内容如下:

其中Lambda并行发生时,使用的便是ForkJoinPool线程池。

多线程

与数据库连接池相似,线程池在系统启动时即创建大量空闲的线程,程序将一个Runnable和一个Callable对象传给线程池,线程池就会启动一个空闲的线程来执行它们的run()或call()方法,当run()或call()方法执行结束后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个Runnable对象的run()或call()方法

系统启动一个新线程的的成本比较高。

Java 8改进的线程池

Executors

Executors类

Executors类是生成线程池的工厂类

一、四种线程池

Java通过Executors提供四种线程池,分别为:

1、newSingleThreadExecutor

创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

2、newFixedThreadPool

创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

3、newScheduledThreadPool

创建一个可定期或者延时执行任务的定长线程池,支持定时及周期性任务执行。

4、newCachedThreadPoo

创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

Executor接口

ExecutorService接口

ScheduledExecutorService接口

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RBlP551a-1604758713723)(scheduledhttps://i-blog.csdnimg.cn/blog_migrate/9d7c34e597d37aacfc8ba0e7b3d5701d.png)]

使用步骤

Java 8增强的ForkJoinPool

线程相关类

线程相关类基本内容如下:

ThreadLocal

包装线程不安全的集合

线程安全的集合类

总结

这篇文章,到这里也就结束了,最近笔者的腰很不舒服,尤其是长时间坐着就更加难受,希望看到这篇文章的各位程序员照顾好自己的身体,在自己最佳的时间里做最好的工作,拥有好的身体和心情,开开心的工作。

​ 2020年11月7日22:04:43于AUX

参考

多线程.xmind

多线程-本文原始markdown

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值