Java中的多线程如何理解——精简

Posted 石原里美的微笑

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java中的多线程如何理解——精简相关的知识,希望对你有一定的参考价值。

目录

引言

线程安全 

实战模拟 

线程同步 

方式一:同步代码块 

方式二:同步方法 

方式三:Lock锁 

线程池 

线程池处理Runnable任务 

线程池处理Callable任务 

Executors的工具类构建线程池对象 


引言

        通过前面的学习,我们已经学会了线程是如何创建的以及线程的常用方法,接下来呢,我们将要深入性了解线程中的知识,主要是线程安全,线程同步,线程池三个知识点。我相信大家通过这节课的简单地学习,就可以大概地掌握了线程吧!好了,废话不多说,我们开始今天的学习吧!

线程安全 

        首先我们应该了解的是什么是“线程安全”问题呢?通俗易懂的讲的话,那就是“假设在某地有一瓶水,石原里美和工藤静香都很渴,想要去喝这瓶水,然而当这两个线程同时启动的时候,二人都会去拿这瓶水,并且同时判断这瓶水是否还在?在这瓶水未被取走之前,二人的判断都是true,因此二人都能够取到这瓶水,可是明明只有一瓶水,却可以让两个人都取到水,这很明显是与现实生活中的情况是不符合的。”这就是我们需要处理的“线程安全”问题。

线程安全问题出现的原因:

  • 存在多线程并发
  • 同时访问共享资源
  • 存在修改共享资源 

实战模拟 

问题描述:

仍然是前面的例子,现有两人需要喝水,分别是石原里美和工藤静香,二者共享同一瓶水,若对方喝掉这瓶水,则另一方则没有水可以喝。

具体操作:

1、提供一个Account类并创建它,作为二人的共享水资源账户;

public class Account 
    private int num;//代表水的数量

    public Account() 
    

    public Account(int num) 
        this.num = num;
    

    public int getNum() 
        return num;
    

    public void setNum(int num) 
        this.num = num;
    

2、定义一个线程类,并且该线程可以处理Account对象;

public class ThreadAccount extends Thread
    private Account account;

    public ThreadAccount() 
    

    public ThreadAccount(Account account) 
        this.account = account;
    

    public Account getAccount() 
        return account;
    

    public void setAccount(Account account) 
        this.account = account;
    

    @Override
    public void run() 
        Thread thread = Thread.currentThread();
        if(account.getNum()>0)
            System.out.println(thread.getName()+"已经成功获得了这瓶水!");
            account.setNum(account.getNum()-1);
            System.out.println("此时还剩下"+account.getNum()+"瓶水");
        else 
            System.out.println("水资源不够,已经无法取出");
        
    

3、创建两个线程,并传入同一个Account对象;

public static void main(String[] args) throws Exception 
        Account account = new Account(1);
        ThreadAccount threadAccount1 = new ThreadAccount(account);
        ThreadAccount threadAccount2 = new ThreadAccount(account);
        Thread thread1 = new Thread(threadAccount1,"石原里美");
        Thread thread2 = new Thread(threadAccount1,"工藤静香");
        thread1.start();
        thread2.start();
    

4、启动两个线程,同时获取同一瓶水。 

 通过运行结果,我们可以清晰的看出来存在很大的问题,当第一个人石原里美获得这瓶水的时候,水资源账户中已经没有水了,所以工藤静香是不能够取到水资源的,然而工藤静香仍然获得了水,且剩下了-1瓶水,这很明显是不符合现实情况的,那么我们该如何解决呢?

线程同步 

        为了能够解决刚才出现的问题,我们可以考虑使用线程同步,让多个线程实现先后依次访问共享资源。其核心思想是:加锁,把共享资源进行上锁,每次只能一个线程进入访问完毕以后解锁,然后其他线程才能进来。通俗易懂的讲就是:“假如两个线程同时开始访问共享资源,在访问之前,谁先拿到钥匙打开这把锁,谁才能访问该共享资源。而另一个线程则只能在共享资源外等待钥匙空下来。”接下来介绍几种方式来解决该问题。

方式一:同步代码块 

作用:把出现线程安全问题的核心代码给上锁

原理:每次只能一个线程进入,执行完毕后自动解锁,其他进程才可以进来执行

synchronized (同步锁对象)
            操作共享资源的代码(核心代码)

 锁对象要求:理论上,锁对象只要对于当前同时执行的线程来说是同一个对象即可。

synchronized ("喝水") //喝水是对于两个线程的不变对象
            if(account.getNum()>0)
                System.out.println(thread.getName()+"已经成功获得了这瓶水!");
                account.setNum(account.getNum()-1);
                System.out.println("此时还剩下"+account.getNum()+"瓶水");
            else 
                System.out.println("水资源不够,已经无法取出");
            
        

具体的操作就是将之前的run()方法中的核心代码加锁,即可实现多个线程依次访问共享资源

 其中需要注意的一点是,同步锁对象等同于打开共享资源的一把钥匙,所以应该对多个线程是同一个对象,并且要规范命名,若同步锁对象命名相同,则会影响另一组线程对共享资源的访问。对于实例方法建议使用this作为锁对象;对于静态方法建议使用字节码(类名.class)对象作为锁对象。

方式二:同步方法 

作用:把出现线程问题的核心方法给上锁。

原理:每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行。

修饰符 synchronized 返回值类型 方法名称(形参列表)
        操作共享资源的代码

 与方式一的不同之处在于,前者是封装共享资源的核心代码,后者则是封装整个方法

public synchronized void LockWater()
            if(account.getNum()>0)
                System.out.println(thread.getName()+"已经成功获得了这瓶水!");
                account.setNum(account.getNum()-1);
                System.out.println("此时还剩下"+account.getNum()+"瓶水");
            else 
                System.out.println("水资源不够,已经无法取出");
            
 

如果方法是实例方法:同步方法默认用this作为锁对象,但是代码要高度面向对象;

如果方法是静态方法:同步方法默认用类名.class作为锁对象。

方式三:Lock锁 

  • 为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock,更加灵活、方便
  • Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁操作
  • Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来构建锁对象
方法名称说明
public ReentrantLock( )获得Lock锁的实现类对象
void lock( )获得锁
void unlock( )释放锁
//在使用之前先定义一个锁对象
private Lock lock = new ReentrantLock();
//定义过锁对象之后,即可以调用其API
lock.lock();
//共享资源代码
lock.unlock();

线程池 

线程池是一个可以复用线程的技术;如果不使用线程池的话,用户每发起一个请求,后台就创建一个新线程来处理,下次新任务来了又要创建新线程,而创建新线程的开销很大,这样会严重影响系统性能。

线程池的实现原理:

在创建线程池时,设定该线程池固定存在N个核心线程用于处理任务,另外会有一个任务队列提供给任务排队等待,在任务队列中的前N个任务则是交给核心线程去处理,在没有空余线程的时候,其余任务则在任务队列中等待。 

如何得到线程池对象?

  • 方式一:使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象;
  • 方式二:使用Executors(线程池的工具类)调用方法返回不同特点的线程对象。

 ThreadPoolExecutor构造器的参数说明

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

参数一:指定线程池的线程数量(核心线程):corePoolSize                        (不能小于0)

参数二:指定线程池可支持的最大线程数:maximumPoolSize      (最大数量>=核心线程)

参数三:指定临时线程的最大存活时间:keepAliveTime                                 (不能小于0)

参数四:指定存活时间的单位(秒、分、时、天):unit                                (时间单位)

参数五:指定任务队列:workQueue                                                                (不能为null)

参数六:指定用哪个线程工厂创建线程:threadFactory                                  (不能为null)

参数七:指定线程忙,任务满的时候,新任务来了怎么办:handler                (不能为null)

        为了更好的去理解多线程:我们可以假设核心线程数量为3个,最大线程数为5个,那么该线程池可以创建的临时线程数为5-3=2个线程,临时线程的最大存活时间是指其被创建之后不处理任务之后的存活时间,时间单位结合实际即可,任务队列设置为5个,参数六字面意思理解即可,参数七则是规定线程池不能再接收任务的时候如何处理。接下来通过这两个问题加深理解。

临时线程什么时候创建?

  • 新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程。
  • (此时3个核心线程在处理任务,并且任务队列中已经有5个任务在等待了,然而仍然有任务过来,此时即可开始创建临时线程处理任务)

什么时候会开始拒绝任务?

  • 核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始任务拒绝。
  • (当3个核心线程和2个临时线程都在处理任务,且任务队列满的情况下,将会开始拒绝接收任务,由参数七决定该如何处理,由此我们也可以得出该线程池任务处理的最大数量为:3+2+5=10,即核心线程+临时线程+任务队列)

线程池处理Runnable任务 

ThreadPoolExecutor创建线程池对象:

ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(3,5,10, TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(5), Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());

 ExecutorService常用方法

常用方法说明
void executor(Runnable command)执行任务/命令,没有返回值。一般用来执行Runnable任务
Future<T>submit(Callable<T> task)执行任务,返回未来任务对象获取线程结果,一般拿来执行Callable任务
void shutdown( )等任务执行完毕后关闭线程池
List<Runnable shutdownNow>立刻关闭,停止正在执行的任务,并返回队列中未执行的任务

参数七新任务拒绝策略

策略说明
ThreadPoolExecutor.AbortPolicy丢弃任务,并抛出RejectExecutionException异常,默认的策略
ThreadPoolExecutor.DiscardPolicy丢弃任务,但是不抛出异常,不推荐
ThreadPoolExecutor.DiscardOldPolicy抛弃队列中等待最久的任务,然后把当前任务加到队列中
ThreadPoolExecutor.CallerRunsPolicy由主线程负责调用任务的run()方法从而绕过线程池直接执行
//main方法
public static void main(String[] args) 
        ThreadPoolExecutor pool = new ThreadPoolExecutor(2,4,10, TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(3), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
        Runnable target = new MyThreadRunnable();
        //核心线程+任务队列=5,所以5个以内不需要创建临时线程
        pool.execute(target);
        pool.execute(target);
        pool.execute(target);
        pool.execute(target);
        pool.execute(target);
        //当任务数量达到5个的时候,接收任务的时候就需要创建临时线程
        pool.execute(target);
        pool.execute(target);
        //当超过7个线程时,再次接收任务时,则会拒绝
        pool.execute(target);
    
//实现Runnable接口
public class MyThreadRunnable implements Runnable
    @Override
    public void run() 
        System.out.println(Thread.currentThread().getName()+"正在处理任务");
        Thread.sleep(10000000);//避免线程快速处理任务,无法达到实验效果
    

//输出结果:
//pool-1-thread-2正在处理任务
//pool-1-thread-4正在处理任务
//pool-1-thread-3正在处理任务
//pool-1-thread-1正在处理任务

线程池处理Callable任务 

Future<T>submit(Callable<T> task)执行任务,返回未来任务对象获取线程结果,一般拿来执行Callable任务
//main方法
public class MoreThread 
    public static void main(String[] args) throws Exception
        ThreadPoolExecutor pool = new ThreadPoolExecutor(2,4,10, TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(3), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
        Future<String> f1 = pool.submit(new MyThreadCallable());
        Future<String> f2 = pool.submit(new MyThreadCallable());
        Future<String> f3 = pool.submit(new MyThreadCallable());
        Future<String> f4 = pool.submit(new MyThreadCallable());
        System.out.println(f1.get());
        System.out.println(f2.get());
        System.out.println(f3.get());
        System.out.println(f4.get());
    

//线程实现Callable接口
public class MyThreadCallable implements Callable<String> 
    @Override
    public String call() throws Exception 
        return Thread.currentThread().getName()+"正在工作中~";
    

//输出结果:仅由两个核心线程完成即可
//pool-1-thread-1正在工作中~
//pool-1-thread-2正在工作中~
//pool-1-thread-2正在工作中~
//pool-1-thread-2正在工作中~

Executors的工具类构建线程池对象 

Executors:线程池的工具类通过调用方法返回不同类型的线程池对象。

方法名称

说明

public static ExecutorsService newCachedThreadPool( )线程数量随着任务增加而增加,如果线程任务执行完毕且空闲了一段时间则会被回收掉。
public static ExecutorsService newFixedThreadPool(int nThread)创建固定线程数量的线程池,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程替代它。
public static ExecutorsService newSingleThreadExecutor( )创建只有一个线程的线程池对象,如果该线程出现异常而结束,那么线程池会补充一个新线程。
public static ScheduledExecutorsService newScheduledThreadPool(int corePoolSize)创建一个线程池,可以实现在给定的延迟后运行任务,或者定期执行任务。

注意:Executors的底层其实也是基于线程池的实现类ThreadPoolExecutor创建线程池对象。

ExecutorService pool1 = Executors.newFixedThreadPool(5);
pool.execute(new MyThreadRunnable());
pool.execute(new MyThreadRunnable());
pool.execute(new MyThreadRunnable());

虽然Executors使用起来会很方便,但是仍然是存在风险的,因此还是推荐前面使用的线程池创建方式。

(1)FixedThreadPool和SingleThreadExecutor:

允许的请求队列长度为Integer.Max_VALUE,可能会堆积大量的请求,从而导致oom。

(2)CachedThreadPool和ScheduledThreadPool:

允许的创建线程数量为Integer.Max.VALUE,可能会创建大量的线程,从而导致oom。

创作不易,给个三连 

以上是关于Java中的多线程如何理解——精简的主要内容,如果未能解决你的问题,请参考以下文章

java中的线程如何理解——精简

java 多线程

Java 并发编程解析 | 如何正确理解Java领域中的多线程模型,主要用来解决什么问题?

java中线程锁的概念

可能是东半球最好的多线程讲义!

java多线程的入门小记