Java进阶 —— 多线程并发

前言

        在系统学完Java的面向对象编程之后,我们需要认真地来学习Java并发编程,我们在学习计算机操作系统的时候也都了解过进程、线程和协程的概念。在这篇文章中荔枝主要会梳理有关线程创建、线程生命周期、同步锁和死锁、线程通信和线程池的知识,并给出相应的精简示例,希望能帮助有需要的小伙伴们哈哈哈~~~


文章目录

前言

一、基础概念

二、创建线程的三种方式

2.1 通过继承Thread类来启用

2.2 实现Runnable接口来实现

2.3 实现Callable接口

三、Thread类的相关方法

四、生命周期

五、同步锁和死锁

5.1 同步锁 

5.1.1 synchronized加锁的两种方式 

5.1.2 Lock

5.2 死锁

六、线程通信

6.1 传统的线程通信

6.2 使用Condition来控制线程通信

6.3 使用阻塞队列来控制线程通信

七、线程池

7.1 ExecutorService类使用示例

7.2 Java8中的ForkJoinPool

总结


一、基础概念

进程 

        我们知道CPU是主机上的中央核心处理器,CPU的核数代表着主机能在一个瞬间同时并行处理的任务数,单核CPU只能在内存中并发处理任务。而在现有的操作系统中,几乎都支持进程这个概念。进程是程序的在内存中的一次执行过程,具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位

线程

        线程在程序中是独立的、并发的执行流,与分隔的进程相比隔离性会更小,线程之间共享内存、文件句柄和其它的进程应有的状态。线程比进程具有更高的性能,这是由于同一进程中的线程具有共性。简单理解,多线程是进程中并行执行的多个子程序。

并发性和并行的区别

        并行是指在同一时刻,有多条指令在多个处理器上同时执行;而并发是指在同一时刻只能执行,但是通过多进程快速轮换执行可以达到同时执行的效果。CPU主频就代表着这些进程之间频繁切换的速度。


二、创建线程的三种方式

2.1 通过继承Thread类来启用

Java语言中JVM允许程序运行多个线程并通过java.lang.Thread类来实现。

Thread类的特性
        每个线程都是通过某个特定Thread对象的run()方法来完成操作的,经常把run()方法的主体称为线程体,并通过该Thread对象的start()方法来启动线程。

流程:

  1. 定义子类继承Thread类;
  2. 子类中重写Thread类中的run方法;
  3. 创建Thread子类对象,即创建了线程对象;
  4. 调用线程对象start方法:启动线程,调用run方法。

 具体代码示例

首先构建一个继承Thread类的子类

//继承Thread类的方式实现多线程
public class TestThread extends Thread{
    @Override
    public void run(){
        System.out.println("多线程运行的代码");
    }
}

 调用线程

public class Test{
    public static void main(String[]args){
        Thread t = new TestThread();
        t.start();   //启动线程
    }
}

2.2 实现Runnable接口来实现

流程

  1. 定义子类,实现Runnable接口。
  2. 子类中重写Runnable接口中的run方法。
  3. 通过Thread类含参构造器创建线程对象。
  4. 将Runnable接口的子类对象作为实际参数传递给Thread类的构造方法中。
  5. 调用Thread类的start方法:开启线程,调用Runnable子类接口的run方法。 

 实现Runnable接口

public class TestRunnable implements Runnable{
    @Override
    public void run(){
        System.out.println("实现Runnable接口运行多线程");
    }
}

实现多线程

public class Test{
    public static void main(String[]args){
        Thread t = new Thread(new TestRunnable);
        //带有线程名称的实例化线程对象。可以通过Thread.currentThread().getName()获取
        //Thread t = new Thread(new TestRunnable,"the FirstThread");
        t.start();   //启动线程
    }
}

与继承Thread类的区别

  • 继承Thread:线程代码存放Thread子类run方法中。重写run方法
  • 实现Runnable:线程代码存在接口的子类的run方法。实现run方法 

实现Runnable接口方法的好处

        实现Runnable接口方法通过继承Runnable接口避免了当继承的局限性,同时也使得多个线程可以同时共享一个接口实现类的对象,非常适合多个相同线程来处理同一份资源。 

2.3 实现Callable接口

        在前面通过实现Runnable接口创建多线程时,Thread类的作用就是把run方法包装成线程的执行体。而从Java5以后,Java提供了一个Callable接口中的call()方法作为线程执行体,同时call()方法可以有返回值,也可以抛出异常。

public class Test{
    public static void main(String[]args){
        //创建callable对象
        ThirdThread tt = new ThirdThread();
        //使用FutureTask来包装Callable对象
        FutureTask task = new FutureTask((Callable)()->{
            ...
            ...
        });
        new Thread(task,"有返回值的线程").start();
        try{
            //获取线程返回值
            System.out.println("子线程的返回值" + task.get());
        }catch (EXception ex){
            ex.printStackTrace();    
        }
    }
}

Callable接口实现类和Runnable接口实现类的区别在于是否有参数返回! 


三、Thread类的相关方法

常用方法如下:

  • void start():启动线程,并执行对象的run(0方法
  • run():线程在被调度时执行的操作
  • String getName():返回线程的名称
  • void setName(String name):设置该线程名称
  • static currentThread():返回当前线程 
public class Test{
    public static void main(String[]args){
        TestRun r1 = new TestRun();
        Thread t1 = new Thread(r1);
        //为线程设置名称
        t1.setName("线程t1");

        t1.start();   //启动线程
        System.out.println(t1.getName()); //若没指定,系统默认给出的线程名称是Thread-0....
    }
}
public class TestRun implements Runnable{
    @Override
    public void run(){
        System.out.println("实现Runnable接口运行多线程");
    }
}

线程优先级

线程的优先级设置增加了线程的执行顺序靠前的概率,是用一个数组1-10来表示的,默认的优先级是5。涉及的方法有:getPriority()和setPriority()

//获取优先级
t1.getPriority();
//设置优先级
t1.setPriority(10);

线程让步

static void yield()线程让步,即暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程,若队列中没有同优先级的线程,则跳过。

Thread.yield();

线程阻塞

join():当某个程序执行流中调用其他线程的join()方法时,调用线程将被阻塞,直到join()方法加入的join线程执行完为止。

try{
    //获取线程返回值
    t1.join();
}catch (EXception ex){
    ex.printStackTrace();    
}

线程睡眠

try{
    Thread.sleep(1000);//当前线程睡眠1000毫秒
}catch(InterruptedException e)(
    e.printStackTrace();
}

线程生命结束

t1.stop();

判断当前线程是否存活

t1.isAlive();

四、生命周期

线程从创建、启动到死亡经历了一个完整的生命周期,在线程的生命周期中一般要经历五种状态:新建——就绪——运行——阻塞——死亡。

  • 新建:当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态;
  • 就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,也就是在执行.start()方法后;
  • 运行:当就绪的线程被调度并获得处理器资源时,便进入运行状态,run()方法定义了线程的操作和功能,此时run()方法的代码开始执行;
  • 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态;
  • 死亡:线程完成了它的全部工作或线程被提前强制性地中止 。

 ​​​​​Java进阶 —— 多线程并发_第1张图片

线程可能以如下三种方法结束:

  • run或call方法执行完成后
  • 线程抛出一个未捕获的Exception或Error
  • 直接调用了stop()方法

五、同步锁和死锁

5.1 同步锁 

        多线程模式的提出势必就会带来线程同步的问题,在保证数据一致性上,我们需要为线程加上同步锁。Java中对于多线程安全的问题提出了同步机制,即在方法声明的时候加入synchronized关键字来修饰或者直接使用synchronized来锁一个demo

5.1.1 synchronized加锁的两种方式 

synchronized同步锁关键字修饰 

//使用synchronized同步锁关键字修饰需要同步执行的方法体
public synchronized void drawing(int money){
    需要同步执行的代码
}

注意:

        在普通方法上加同步锁synchronized,锁的是整个对象,不是某一个方法。如果是不同对象的话那么就是不同的锁。静态的方法加synchronized对于所有的对象都是同一个锁

synchronized锁一段demo

使用这种方法来锁指向this的代码块使用的都是同一个同步锁。如果改成方法对象的话比如Account对象的话就是不同的同步锁。

synchronized(this){ //表示当前的对象的代码块被加了synchronized同步锁
    demo...
}

5.1.2 Lock

        相比于上面的synchronized相应的锁操作,Lock提供了更为广泛的锁操作。其中包括ReadWriteLock(读写锁)和ReentrantLock(可重入锁),ReadWriteLock提供了ReentrantReadWriteLock的实现类。在Java8中引入了一个新的StampedLock类替代了传统的ReentrantReadWriteLock并给出了三种锁模式:Write、ReadOptimistic和Reading。

ReentrantLock 实现demo 

class x{
    //定义锁对象
    private final ReentrantLock lock = new ReentrantLock();
    //...
    //定义需要保证线程安全的方法
    public void m(){
        lock.lock();
        try{
            //需要保证线程安全的demo
        }
        finally{
            lock.unlock();
        }
    }
}

5.2 死锁

不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
解决方法

  • 专门的算法、原则,比如加锁顺序一致
  • 尽量减少同步资源的定义,尽量避免锁未释放的场景

六、线程通信

        当我们手动开启并在控制台中输出两个线程的运行过程的时候,程序并不能每次都准确的控制两个线程的轮换执行的先后次序,所以Java中也提供了一些机制来保证线程的协调运行。在传统的Java中,基于同步锁synchronized关键字提供了借助于Object类的wait()、notify()和notifyAll()方法来控制线程的阻塞情况,而之后也出现了基于Condition和阻塞队列BlockingQueue来控制线程阻塞的情况。

6.1 传统的线程通信

Object类中提供的wait()、notify()和notifyAll()方法必须由一个同步监视器对象来调用,所以这三种方法必须基于同步锁synchronized关键字

  • wait():该方法会导致当前线程进入等待状态,直到其它的线程调用notify()或notifyAll()方法来唤醒该线程,wait方法有三种形式:不带时间参数(等待唤醒)、带毫秒时间参数(时间到自动唤醒)和带毫微秒的时间参数(时间到自动唤醒)。调用wait方法当前线程会释放对同步监视器的锁定。
  • notify():唤醒该同步监视器上等待的单个线程,这种选择是按照优先级最高的来唤醒结束其等待状态。
  • notifyAll():唤醒等待的所有线程。
//使用时直接调用方法就行,但必须是在有synchronized修饰的方法内去调用才可
wait();
notify();
notifyAll();

6.2 使用Condition来控制线程通信

        对于程序不使用synchronized关键字来保证同步锁,而是采用Lock对象来保证同步,Java中提供了Condition类来保证线程通信。Contidion类中提供了类似于synchronized关键字中的三种方法:await()、signal()和signalAll(),替代了同步监视器的功能。

  • await():类似于wait方法,会使得当前线程进入等待状态,直到其它线程调用signal()或signalAll()来唤醒。
  • signal():唤醒单个线程。
  • signalAll():唤醒多个线程。
//显示定义Lock对象
Lock lock = new ReentrantLock();
//获取Condition
Condition cond = lock.newCondition();
//需要同步的方法中加锁
public void fun(){
    //加锁过程
    lock.lock();
    try{
        if(条件) cond.await(); //线程进入等待
        else{
            //唤醒其他线程
            cond.signalAll();
        }
    }catch(InterruptedException e){
        e.printStrackTrace();
    }finally{
        //锁的释放
        lock.unlock();
    }
}

6.3 使用阻塞队列来控制线程通信

        除了上述两种方法,Java5中还提供了BlockingQueue接口来作为线程同步的工具。它的工作原理是这样滴:当生产者往BlockingQueue接口中放入元素直至接口队列满了,线程阻塞;消费者从BlockingQueue接口队列中取元素直至队列空了,线程阻塞。BlockingQueue接口继承了Queue接口并提供了如下三组方法。

  •  在队列尾部添加元素:add(E e)、offer(E e)、put(E e),当队列已满的时候,这三个方法分别会抛出异常、返回false和阻塞线程。
  • 在队列头部删除并返回删除元素:remove()、poll()、take()方法。当该队列已空时,这三个方法分别会抛出异常、返回false和阻塞线程。
  • 在队列头部取出但不删除元素:element()和peek(),当该队列已空时,分别会抛出异常和返回false

在Java7之后,阻塞队列出现了新增,分别是:ArrayBlockingQueue、LinkedBlockingQueue、priorityBlockingQueue、SynchornizedQueue和DelayQueue这五个类。 


七、线程池

        系统启动一个新线程的成本是比较高的,尤其是当系统本身已经有大量的并发线程时,会导致系统性能急剧下降,甚至会导致JVM崩溃,因此我们通常采用线程池来维护系统的并发线程。与数据库连接池类似的时,线程池在系统启动时即创建大量空闲的线程,程序将一个Runnable对象或Callable对象传给线程池,线程池就会启动后一个空闲的线程来执行它们的run()或call()方法,当运行结束后,该线程不会死亡而是返回线程池中进入空闲等待状态。Java进阶 —— 多线程并发_第2张图片

        ExecutorService代表尽快执行线程的线程池,程序只需要将一个Runnable对象或Callable对象传给线程池,就会尽快执行线程任务;ScheduledExecutorService代表可在指定延迟后或周期性地执行线程任务的线程池。

7.1 ExecutorService类使用示例

使用线程池的步骤如下:

  • 调用Executors类的静态工厂方法调用创建一个ExecutorService对象,该对象就代表着一个线程池;
  • 创建Runnable实现类或Callable实现类的实例,作为线程执行的任务;
  • 调用ExecutorService对象的submit()方法来提交Runnable或者Callable对象实例;
  • 结束任务时,调用ExecutorService对象的shutdown()方法来关闭线程池;
//开启6个线程的线程池
ExecutorService pool = Executors.newFixedThreadPool(6);
//创建Runnable实现类
Runnable target = ()->{...}

//提交线程任务到线程池
pool.submit();

//关闭线程
pool.shutdown();

        用完一个线程池后,应该调用该线程池的shutdown()方法,该方法将启动线程池的关闭序列并不再接受新任务,线程池中的任务依次执行完毕后线程死亡;或者调用线程池的shutdownNow()方法来直接停止所有正在执行的活动任务。

7.2 Java8中的ForkJoinPool

        计算机发展到现在其实基本的硬件都支持多核CPU,为了更好地利用硬件设备的资源,Java中提供了一个ForkJoinPool来支持将一个任务拆分成多个小任务并行计算。ForkJoinPool是ExecutorService的实现类,是一个特殊的线程池。

构造器的两种方法

  • ForkJoinPool(int num):创建一个包含num个并行线程的ForkJoinPool;
  • ForkJoinPool():以Runtime.availableProcessors()方法的返回值作为parallelism参数(上面我写成了num)来创建改线程池

实现通用池的两个静态方法

  • ForkJoinPool commonPool():改方法返回一个通用池,通用池的状态不会受到shutdown()等方法的影响,System.exit(0)除外。
  • int getCommonPoolParallelism():该方法返回通用池的并行级别

注意:

        ForkJoinPool.submit(ForkJoinTask task) ,其中ForkJoinTask代表着一个可以并行和合并的任务,他有两个抽象的子类:RecursiveAction和RecursiveTask,分别代表着有返回值和无返回值的任务。

class PrintTask extends RecursiveAction{
    ...
    @Override
    protected void compute(){
        ......
        //分割任务
        PrintTask t1 = new PrintTask(start,middle);
        PrintTask t2 = new PrintTask(middle,end);
        //并行执行子任务
        t1.fork();
        t2.fork();
    }
}

public class Test{
    public static void main(String[]args) throws Exception{
        //实例化通用池对象
        ForkJoinPool pool = new ForkJoinPool();
        pool.submit(new PrintTask(0,1000));
        //线程等待完成
        pool.awaitTermination(2,TimeUnit.SECONDS);
        //关闭线程池
        pool.shutdown();
    }
}

总结

        现有的所有企业都采用的是多线程并发的方式来开发的,也要求我们能够应对在高并发场景下保证系统服务的高可用的要求,所以多线程和异步编程我们必须牢牢掌握。这几章可能会比较枯燥,难度也会比较大,荔枝也是啃了一段时间嘿嘿嘿,在学这部分之前一定要把面向对象学好,要不然会晕哈哈哈~~~

今朝已然成为过去,明日依然向往未来!我是小荔枝,在技术成长的路上与你相伴,码文不易,麻烦举起小爪爪点个赞吧哈哈哈~~~ 比心心♥~~~

你可能感兴趣的:(Java开发,java,多线程,并发,线程池)