java随笔-多线程(2)

1.线程池

线程池的工作原理

Java线程池主要用于管理线程组及其运行状态,以便Java虚拟机更好地利用CPU资源.其工作原理为:JVM先根据用户的参数创建一定数量的可运行的线程任务,并将其放入队列中,在线程创建后启动这些任务,如果正在运行的线程数量超过了最大线程数量,则超出数量的线程排队等候,在有任务执行完毕后,线程池调度器会发现有可用的线程,进而再次从队列中取出任务并执行.

线程池的工作流程

Java线程池时通过Executor框架实现的,在该框架中用到了Executor,Executors,ExecutorService,ThreadPoolExecutor,Callable,Future,FutureTask这几个核心类.具体的继承关系如下:

Executor框架
其中,ThreadPoolExecutor是构建线程的核心方法.它的具体参数如下:

序号参数说明
1corePoolSize线程池中核心线程的数量
2maximumPoolSize线程池中最大线程的数量
3keepAliveTime当前线程数量超过corePoolSize时,空闲线程的存活时间
4unitkeepAliveTime的时间单位
5workQueue任务队列,被提交但尚未被执行的任务存放的地方
6threadFactory线程工厂,用于创建线程,可使用默认的线程工厂或自定义线程工厂
7handler由于任务过多或其他原因导致线程池无法处理时的任务拒绝策略

Java线程池的工作流程为:线程池刚被创建时,只是向系统申请一个用于执行线程队列和管理线程池的线程资源.在调用execute()添加一个任务时,线程池会按照以下流程执行任务.

  • 如果正在运行的线程数量少于corePoolSize,线程池就会立刻创建线程并执行该线程任务.
  • 如果正在运行的线程数量大于等于corePoolSize,该任务就将被放入阻塞队列中.
  • 在阻塞队列已满且正在运行的线程数量少于maximumPoolSize时,线程池将拒绝执行该线程任务并抛出RejectExecutionException异常.
  • 在线程任务执行完毕后,该任务将被从线程池队列中移除,线程池将从队列中取出下一个线程任务继续执行.
  • 在线程处于空闲状态的时间超过keepAliveTime时,正在运行的线程数量超过coolPoolSize,该线程将会被认定为空闲线程并停止.因此在线程池中所有线程任务都执行完毕后,线程池会收缩到corePoolSize大小.
    具体的流程如下:
    线程池的运行流程

5种常用的线程池

Java定义了Executor接口并在该接口种定义了execute()方法用于执行了一个线程任务,然后通过ExecutorService实现Executor接口并执行具体的线程操作.ExecutorService接口有多个实现类可以用于创建不同的线程池.它们分别是:

  • newCachedThreadPool
  • newScheduledThreadPool
  • newSingleThreadExecutor
  • newWorkStealingPool
  • newFIxedThreadPool

2.锁

Java中的锁主要用于保障线程在多并发情况下数据的一致性.在多线程编程中为了保障数据的一致性,我们通常需要在使用对象或者调用方法之前加锁,这时如果有其他的线程也需要使用该对象或者调用该方法,则首先要获得锁对象.如果某个线程发现锁正在被其他线程使用,就会进入阻塞队列等待锁的释放,直到其他线程执行完成并释放锁.这种方式保证了在同一时刻只有一个线程持有该对象的锁并修改该对象,从而保障数据的安全.

举个例子,当我们从银行取款时,同一时刻,一张卡上只能由一个人使用,才能保证数据安全.换言之,如果一张卡被多个人用于取款,系统在结算剩余金额之前被其他人抢先又用了一次取款操作,那么很有可能导致最后的余额和实际取出的钱总和大于这张卡的原金额.下面将设计一个简单的算法模拟实现这一点.

考虑一个AccountRunnableTest类,表示账户,线程创建采用实现runnable接口的方式,它只有一个参数,即余额,和它的get,set方法,声明一个带有余额这个参数的构造方法.如下所示:

public class AccountRunnableTest implements Runnable {
    int balance;


    public AccountRunnableTest(int balance) {
        this.balance = balance;
    }

    public int getBalance() {
        return balance;
    }

    public void setBalance(int balance) {
        this.balance = balance;
    }
   }

在这个类中需要重写run()方法,run方法的内容如下:

@Override
    public void run() {
        //1.获得账户的余额
        int temp = this.getBalance();
        //2.判断余额是否大于200,如果是,则执行出钞操作
        if(temp >= 200){
            System.out.println("正在出钞,请稍候...");
            try {
                //3.当前线程睡眠5s
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //4.将余额更新,取款操作完成
            temp -= 200;
            this.setBalance(temp);
            System.out.println("取款成功!剩余金额为"+this.getBalance());
        }else{
            System.out.println("取款失败,余额不足!");
        }

这个run方法就是取钱操作.
在主方法中,实例化账户类,设置余额为1000.建立两个子线程,分别调用start方法,也就是分别从中取走200.观察最后的余额是多少.代码如下:

public static void main(String[] args) {
        AccountRunnableTest art = new AccountRunnableTest(1000);

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

        t1.start();
        t2.start();

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("最终的账户余额为:"+art.getBalance());
    }

结果如下:
在这里插入图片描述
原本两个线程分别取走了账户里的200块,应该还剩下600块,可实际上账户余额显示为800元.分析其中的原因.主要是run方法里设置了休眠五秒的操作.而且这个操作在账户类更新最新余额之前.也就是说,在用户1取钱以后,账户类并没有立即更新余额,这个时候第二个用户就已经开始取钱了,这时余额还是1000,当用户1取完了钱,还剩800.而用户2取完了钱,账户余额也是800,并且用户二的账户余额覆盖了前面的账户余额的数据,它是在1000的基础上进行的,而非第一个用户取走200以后的800余额上进行的.这种情况也成为线程不安全.
为了防止这种情况,最好的办法就是限制同一时刻同一个账户的取款人数.当一个用户在取款时,其他需要取款的用户必须等他取完,余额更新完了以后才可以取款.实现这种方式的方法有两种,synchronized修饰和lock.

synchronized

synchronized关键字用于为Java对象,方法,代码块提供线程安全的操作.synchronized属于独占式的悲观锁.(关于悲观锁将在之后介绍),同时属于可重入锁.在使用synchronized修饰对象时,同一时刻只能有一个线程对该对象进行访问;在synchronized修饰方法,代码块时,同一时刻只能有一个线程执行该方法体或代码块,其他线程只有等待当前线程执行完毕并释放锁资源后才能访问该对象或执行同步代码块.

synchronized作用范围

  • synchronized作用于成员变量和非静态方法时,锁住的是对象的实例,即this对象.
  • synchronized作用于静态方法时,锁住的是Class实例,因为静态方法属于Class而不属于对象.
  • synchronized作用于一个代码块时,锁住的是所有代码块中配置的对象.

结合上述,利用synchronized优化代码,也存在这三种上锁方式:

  1. 对Accountable的run方法用synchronized修饰;
  2. 这个稍微复杂点,最好把取钱的方法写成一个静态方法,然后在run方法中调用这个方法.对这个静态方法用sychronized修饰;
  3. 再声明一个类,比如demo{},这个类可以没有任何实质性内容,在账户类中声明一个demo对象,将这个对象作为锁,用synchronized修饰run方法里的代码块.
    以第三种方法为例,优化如下:
ackage com.lagou.sychronized_design.accountRunnableTest;

/**
 * 在使用sychronized修饰对象时,同一时刻只能有一个线程对该对象进行访问,在synchronized修饰方法,代码块时,同一时刻
 * 只能由一个线程执行该方法体或代码块,其他线程只能等待当前线程执行完毕并且释放锁资源后才能访问改对象或执行同步代码块.
 */
public class AccountRunnableTest3 implements Runnable {

    int balance;

    private Demo demo = new Demo();

    public AccountRunnableTest3(int balance) {
        this.balance = balance;
    }

    public int getBalance() {
        return balance;
    }

    public void setBalance(int balance) {
        this.balance = balance;
    }

    @Override
    public void run() {
        synchronized (demo) {
            int temp = this.getBalance();
            if(temp >= 200){
                System.out.println("正在出钞,请稍候...");
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                temp -= 200;
                this.setBalance(temp);
                System.out.println("取款成功!剩余金额为"+this.getBalance());
            }else{
                System.out.println("取款失败,余额不足!");
            }
        }
    }
    public static void main(String[] args) {
        AccountRunnableTest3 art3 = new AccountRunnableTest3(1000);

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

        t1.start();
        t2.start();

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("账户余额为:"+art3.getBalance());
    }

    class Demo{};
}

运行结果如下:
在这里插入图片描述
如此一来,就解决了线程不安全问题.

ReentrantLock

从Java5开始提供了更强大的线程同步机制—使用显式定义的同步锁对象来实现。
java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。
该接口的主要实现类是ReentrantLock类,该类拥有与synchronized相同的并发性,在以后的线程
安全控制中,经常使用ReentrantLock类显式加锁和释放锁

ReentrantLock不但提供了synchronized对锁的操作功能,还提供了诸如可响应中断锁,可轮询锁请求,定时锁等避免多线程死锁的方法.
ReentrantLock有显式的操作过程,何时加锁,何时释放锁都在程序员的控制之下.具体的使用流程时定义一个ReentrantLock,在需要加锁的地方通过lock方法加锁,等资源使用完成后再通过unlock方法释放锁.
未完待续…
在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值