• JAVA面试考点解析(10) -- 多线程、高并发


    12.线程并发库和线程池的作用

    简单了解过,JDK5中增加了并发库,java.util.concurrent中提供了对线程优化.管理的各项操作,该包提供了线程的运行,线程池的创建,线程生命周期的控制.
    线程池:java.util.concurrent.Executors创建四种线程池
    newCachedThreadPool 创建非固定数量,可缓存的线程池,若线程池超过处理需要,可灵活回收空线程,若没有线程可回收,则建新线程
    newFixedThreadPool固定线程池,底层是无界队列,可控制最大并发数,超出的线程会在队列中等待
    newScheduledThreadPool定时执行线程池,支持定时及周期性任务执行
    newSingleThreadExecutor单线程化的线程池,只会用唯一的工作线程来执行任务,保证所有任务按照顺序执行
    线程池的好处:

    限定线程的个数,不会导致由于线程过多导致系统运行缓慢或崩溃
    线程池每次都不需要去创建和销毁,节约了资源
    线程池不需要每次都去创建,相应时间更快.

    88.秒杀

    秒杀业务最主要的操作:减库存和添加订单 
    1.第一种方式:数据库(中小型网站) 
    Update goods set num=num-1 where id=id; 
    Insert orders values() where goods.id=goodsid
    为了保持数据的一致性,需要使用事务
    优化: 
    ①数据库服务器和秒杀服务器在同一个机房 
    ②避免使用事务,使用数据库的存储过程来完成业务单元 
    存储过程逻辑:避免使用事务 
    (1)判断库存(num) 
    (2)如果num>0,执行update和insert操作 
    (3)如果num<0,秒杀结束
    2.第二种方式:缓存(redis)(中大型网站) 
    将秒杀商品和订单都存在redis中,用户下单时,num>0?下单(num-1):秒杀结束;

    57、Thread类的sleep()方法和对象的wait()方法都可以让线程暂停执行,它们有什么区别? 
    答:sleep()方法(休眠)是线程类(Thread)的静态方法,调用此方法会让当前线程暂停执行指定的时间,将执行机会(CPU)让给其他线程,但是对象的锁依然保持,因此休眠时间结束后会自动恢复(线程回到就绪状态,请参考第66题中的线程状态转换图)。wait()是Object类的方法,调用对象的wait()方法导致当前线程放弃对象的锁(线程暂停执行),进入对象的等待池(wait pool),只有调用对象的notify()方法(或notifyAll()方法)时才能唤醒等待池中的线程进入等锁池(lock pool),如果线程重新获得对象的锁就可以进入就绪状态。

    补充:可能不少人对什么是进程,什么是线程还比较模糊,对于为什么需要多线程编程也不是特别理解。简单的说:进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,是操作系统进行资源分配和调度的一个独立单位;线程是进程的一个实体,是CPU调度和分派的基本单位,是比进程更小的能独立运行的基本单位。线程的划分尺度小于进程,这使得多线程程序的并发性高;进程在执行时通常拥有独立的内存单元,而线程之间可以共享内存。使用多线程的编程通常能够带来更好的性能和用户体验,但是多线程的程序对于其他程序是不友好的,因为它可能占用了更多的CPU资源。当然,也不是线程越多,程序的性能就越好,因为线程之间的调度和切换也会浪费CPU时间。时下很时髦的Node.js就采用了单线程异步I/O的工作模式。
    58、线程的sleep()方法和yield()方法有什么区别? 
    答: 
    ① sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会; 
    ② 线程执行sleep()方法后转入阻塞(blocked)状态,而执行yield()方法后转入就绪(ready)状态; 
    ③ sleep()方法声明抛出InterruptedException,而yield()方法没有声明任何异常; 
    ④ sleep()方法比yield()方法(跟操作系统CPU调度相关)具有更好的可移植性。

    59、当一个线程进入一个对象的synchronized方法A之后,其它线程是否可进入此对象的synchronized方法B? 
    答:不能。其它线程只能访问该对象的非同步方法,同步方法则不能进入。因为非静态方法上的synchronized修饰符要求执行方法时要获得对象的锁,如果已经进入A方法说明对象锁已经被取走,那么试图进入B方法的线程就只能在等锁池(注意不是等待池哦)中等待对象的锁。

    60、请说出与线程同步以及线程调度相关的方法。 
    答: 
    - wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁; 
    - sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理InterruptedException异常; 
    - notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且与优先级无关; 
    - notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态;

    提示:关于Java多线程和并发编程的问题,建议大家看我的另一篇文章《关于Java并发编程的总结和思考》。
    补充:Java 5通过Lock接口提供了显式的锁机制(explicit lock),增强了灵活性以及对线程的协调。Lock接口中定义了加锁(lock())和解锁(unlock())的方法,同时还提供了newCondition()方法来产生用于线程之间通信的Condition对象;此外,Java 5还提供了信号量机制(semaphore),信号量可以用来限制对某个共享资源进行访问的线程的数量。在对资源进行访问之前,线程必须得到信号量的许可(调用Semaphore对象的acquire()方法);在完成对资源的访问后,线程必须向信号量归还许可(调用Semaphore对象的release()方法)。
    下面的例子演示了100个线程同时向一个银行账户中存入1元钱,在没有使用同步机制和使用同步机制情况下的执行情况。

    银行账户类:
    /**
    * 银行账户
    * @author 骆昊
    *
    */
    public class Account {
    private double balance; // 账户余额

    /**
    * 存款
    * @param money 存入金额
    */
    public void deposit(double money) {
    double newBalance = balance + money;
    try {
    Thread.sleep(10); // 模拟此业务需要一段处理时间
    }
    catch(InterruptedException ex) {
    ex.printStackTrace();
    }
    balance = newBalance;
    }

    /**
    * 获得账户余额
    */
    public double getBalance() {
    return balance;
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    存钱线程类:
    /**
    * 存钱线程
    * @author 骆昊
    *
    */
    public class AddMoneyThread implements Runnable {
    private Account account; // 存入账户
    private double money; // 存入金额

    public AddMoneyThread(Account account, double money) {
    this.account = account;
    this.money = money;
    }

    @Override
    public void run() {
    account.deposit(money);
    }

    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    测试类:
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;

    public class Test01 {

    public static void main(String[] args) {
    Account account = new Account();
    ExecutorService service = Executors.newFixedThreadPool(100);

    for(int i = 1; i <= 100; i++) {
    service.execute(new AddMoneyThread(account, 1));
    }

    service.shutdown();

    while(!service.isTerminated()) {}

    System.out.println("账户余额: " + account.getBalance());
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    在没有同步的情况下,执行结果通常是显示账户余额在10元以下,出现这种状况的原因是,当一个线程A试图存入1元的时候,另外一个线程B也能够进入存款的方法中,线程B读取到的账户余额仍然是线程A存入1元钱之前的账户余额,因此也是在原来的余额0上面做了加1元的操作,同理线程C也会做类似的事情,所以最后100个线程执行结束时,本来期望账户余额为100元,但实际得到的通常在10元以下(很可能是1元哦)。解决这个问题的办法就是同步,当一个线程对银行账户存钱时,需要将此账户锁定,待其操作完成后才允许其他的线程进行操作,代码有如下几种调整方案:

    在银行账户的存款(deposit)方法上同步(synchronized)关键字
    /**
    * 银行账户
    * @author 骆昊
    *
    */
    public class Account {
    private double balance; // 账户余额

    /**
    * 存款
    * @param money 存入金额
    */
    public synchronized void deposit(double money) {
    double newBalance = balance + money;
    try {
    Thread.sleep(10); // 模拟此业务需要一段处理时间
    }
    catch(InterruptedException ex) {
    ex.printStackTrace();
    }
    balance = newBalance;
    }

    /**
    * 获得账户余额
    */
    public double getBalance() {
    return balance;
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    在线程调用存款方法时对银行账户进行同步
    /**
    * 存钱线程
    * @author 骆昊
    *
    */
    public class AddMoneyThread implements Runnable {
    private Account account; // 存入账户
    private double money; // 存入金额

    public AddMoneyThread(Account account, double money) {
    this.account = account;
    this.money = money;
    }

    @Override
    public void run() {
    synchronized (account) {
    account.deposit(money); 
    }
    }

    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    通过Java 5显示的锁机制,为每个银行账户创建一个锁对象,在存款操作进行加锁和解锁的操作
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;

    /**
    * 银行账户

    * @author 骆昊
    *
    */
    public class Account {
    private Lock accountLock = new ReentrantLock();
    private double balance; // 账户余额

    /**
    * 存款

    * @param money
    * 存入金额
    */
    public void deposit(double money) {
    accountLock.lock();
    try {
    double newBalance = balance + money;
    try {
    Thread.sleep(10); // 模拟此业务需要一段处理时间
    }
    catch (InterruptedException ex) {
    ex.printStackTrace();
    }
    balance = newBalance;
    }
    finally {
    accountLock.unlock();
    }
    }

    /**
    * 获得账户余额
    */
    public double getBalance() {
    return balance;
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    按照上述三种方式对代码进行修改后,重写执行测试代码Test01,将看到最终的账户余额为100元。当然也可以使用Semaphore或CountdownLatch来实现同步。

    61、编写多线程程序有几种实现方式? 
    答:Java 5以前实现多线程有两种实现方法:一种是继承Thread类;另一种是实现Runnable接口。两种方式都要通过重写run()方法来定义线程的行为,推荐使用后者,因为Java中的继承是单继承,一个类有一个父类,如果继承了Thread类就无法再继承其他类了,显然使用Runnable接口更为灵活。

    补充:Java 5以后创建线程还有第三种方式:实现Callable接口,该接口中的call方法可以在线程执行结束时产生一个返回值,代码如下所示:
    import java.util.ArrayList;
    import java.util.List;
    import java.util.concurrent.Callable;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.Future;


    class MyTask implements Callable<Integer> {
    private int upperBounds;

    public MyTask(int upperBounds) {
    this.upperBounds = upperBounds;
    }

    @Override
    public Integer call() throws Exception {
    int sum = 0; 
    for(int i = 1; i <= upperBounds; i++) {
    sum += i;
    }
    return sum;
    }

    }

    class Test {

    public static void main(String[] args) throws Exception {
    List<Future<Integer>> list = new ArrayList<>();
    ExecutorService service = Executors.newFixedThreadPool(10);
    for(int i = 0; i < 10; i++) {
    list.add(service.submit(new MyTask((int) (Math.random() * 100))));
    }

    int sum = 0;
    for(Future<Integer> future : list) {
    // while(!future.isDone()) ;
    sum += future.get();
    }

    System.out.println(sum);
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    62、synchronized关键字的用法? 
    答:synchronized关键字可以将对象或者方法标记为同步,以实现对对象和方法的互斥访问,可以用synchronized(对象) { … }定义同步代码块,或者在声明方法时将synchronized作为方法的修饰符。在第60题的例子中已经展示了synchronized关键字的用法。

    63、举例说明同步和异步。 
    答:如果系统中存在临界资源(资源数量少于竞争资源的线程数量的资源),例如正在写的数据以后可能被另一个线程读到,或者正在读的数据可能已经被另一个线程写过了,那么这些数据就必须进行同步存取(数据库操作中的排他锁就是最好的例子)。当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并且不希望让程序等待方法的返回时,就应该使用异步编程,在很多情况下采用异步途径往往更有效率。事实上,所谓的同步就是指阻塞式操作,而异步就是非阻塞式操作。

    64、启动一个线程是调用run()还是start()方法? 
    答:启动一个线程是调用start()方法,使线程所代表的虚拟处理机处于可运行状态,这意味着它可以由JVM 调度并执行,这并不意味着线程就会立即运行。run()方法是线程启动后要进行回调(callback)的方法。

    65、什么是线程池(thread pool)? 
    答:在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源。在Java中更是如此,虚拟机将试图跟踪每一个对象,以便能够在对象销毁后进行垃圾回收。所以提高服务程序效率的一个手段就是尽可能减少创建和销毁对象的次数,特别是一些很耗资源的对象创建和销毁,这就是”池化资源”技术产生的原因。线程池顾名思义就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程不用自行创建,使用完毕不需要销毁线程而是放回池中,从而减少创建和销毁线程对象的开销。 
    Java 5+中的Executor接口定义一个执行线程的工具。它的子类型即线程池接口是ExecutorService。要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,因此在工具类Executors面提供了一些静态工厂方法,生成一些常用的线程池,如下所示: 
    - newSingleThreadExecutor:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。 
    - newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。 
    - newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。 
    - newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。 
    - newSingleThreadExecutor:创建一个单线程的线程池。此线程池支持定时以及周期性执行任务的需求。

    第60题的例子中演示了通过Executors工具类创建线程池并使用线程池执行线程的代码。如果希望在服务器上使用线程池,强烈建议使用newFixedThreadPool方法来创建线程池,这样能获得更好的性能。

    66、线程的基本状态以及状态之间的关系? 
    答: 

    说明:其中Running表示运行状态,Runnable表示就绪状态(万事俱备,只欠CPU),Blocked表示阻塞状态,阻塞状态又有多种情况,可能是因为调用wait()方法进入等待池,也可能是执行同步方法或同步代码块进入等锁池,或者是调用了sleep()方法或join()方法等待休眠或其他线程结束,或是因为发生了I/O中断。


    67、简述synchronized 和java.util.concurrent.locks.Lock的异同? 
    答:Lock是Java 5以后引入的新的API,和关键字synchronized相比主要相同点:Lock 能完成synchronized所实现的所有功能;主要不同点:Lock有比synchronized更精确的线程语义和更好的性能,而且不强制性的要求一定要获得锁。synchronized会自动释放锁,而Lock一定要求程序员手工释放,并且最好在finally 块中释放(这是释放外部资源的最好的地方)。

    68、ThreadLocal的用途

    ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。ThreadLocal,顾名思义是线程的一个本地化对象,当工作于多线程中的对象使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程分配一个独立的变量副本,所以每一个线程都可以独立的改变自己的副本,而不影响其他线程所对应的副本。从线程的角度看,这个变量就像是线程的本地变量。

    ThreadLocal类非常简单好用,只有四个方法,能用上的也就是下面三个方法:
    - void set(T value):设置当前线程的线程局部变量的值。
    - T get():获得当前线程所对应的线程局部变量的值。
    - void remove():删除当前线程中线程局部变量的值。

    ThreadLocal是如何做到为每一个线程维护一份独立的变量副本的呢?在ThreadLocal类中有一个Map,键为线程对象,值是其线程对应的变量的副本,自己要模拟实现一个ThreadLocal类其实并不困难,代码如下所示:

    import java.util.Collections;
    import java.util.HashMap;
    import java.util.Map;

    public class MyThreadLocal<T> {
    private Map<Thread, T> map = Collections.synchronizedMap(new HashMap<Thread, T>());

    public void set(T newValue) {
    map.put(Thread.currentThread(), newValue);
    }

    public T get() {
    return map.get(Thread.currentThread());
    }

    public void remove() {
    map.remove(Thread.currentThread());
    }
    }

  • 相关阅读:
    json
    mysql
    第九节 三元表达式,列表生成式
    mysql数据库
    小练习
    22
    22
    100句美文
    22
    22
  • 原文地址:https://www.cnblogs.com/newbie27/p/10835828.html
Copyright © 2020-2023  润新知