美文网首页
JAVA多线程

JAVA多线程

作者: 五十米_深蓝 | 来源:发表于2019-01-13 18:11 被阅读9次

    1、主线程怎么捕获子线程的异常

    异常的实现机制:
    是严重依赖与线程的栈的。每个线程都有一个栈,线程启动后会在栈上安装一些异常处理
    帧,并形成一个链表的结构,在异常发生时通过该链表可以进行栈回滚.
    所以说,线程之间是不可能发生异常处理的交换关系的。
    

    因此,异常一定要在线程内部消化。也就是说主线程无法捕获子线程的异常;

    //子线程中捕获异常,在主线程中处理异常:
    using System;
    using System.Threading;
    namespace CatchThreadException
    {
        class Program
        {
            private delegate void ThreadExceptionEventHandler(Exception ex);
            private static ThreadExceptionEventHandler exceptionHappened;
            private static Exception exceptions;
            static void Main(string[] args)
            {
                exceptionHappened = new ThreadExceptionEventHandler(ShowThreadException);
                Thread t = new Thread(() =>
                    {
                        try
                        {
                            throw new Exception("我是子线程中抛出的异常!");
                        }
                        catch (Exception ex)
                        {
                            OnThreadExceptionHappened(ex);
                        }
                    }          
                );
                t.Start();
                t.Join();
                if (exceptions != null)
                {
                    Console.WriteLine(exceptions.Message);
                }
            }
            private static void ShowThreadException(Exception ex)
            {
                exceptions = ex;
            }
            private static void OnThreadExceptionHappened(Exception ex)
            {
                if (exceptionHappened != null)
                {
                    exceptionHappened(ex);
                }
            }
        }
    }
    
    

    2、现在有T1、T2、T3三个线程,你怎样保证T2在T1执行完后执行,T3在T2执行完后执行?
    thread.Join把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B。
    3、在Java中Lock接口比synchronized块的优势是什么?
    占有锁的线程释放锁一般会是以下情况之一:

    1、占有锁的线程执行完了该代码块,然后释放对锁的占有;
    2、占有锁线程执行发生异常,此时JVM会让线程自动释放锁;
    3、占有锁线程进入 WAITING 状态从而释放锁,例如在该线程中调用wait()方法等。
    

    Lock特性说明

    1、Lock是一个接口,是JDK层面的实现;而synchronized是Java中的关键字,是Java的内置特性,是JVM层面的实现;
    2、可以通过Lock得知线程有没有成功获取到锁 (解决方案:ReentrantLock)
    3、Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致死锁现象。
    4、Lock可以提高多个线程进行读操作的效率。
    

    相比synchronized,lock的优势

    1、lock能够响应中断 (解决方案:lockInterruptibly()),
    而使用synchronized关键字的情形下,假如占有锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,
    但是又没有释放锁,那么其他线程就只能一直等待,别无他法。这会极大影响程序执行效率。
    2、采用synchronized关键字实现同步的话,当多个线程都只是进行读操作时,
    也只有一个线程在可以进行读操作,其他线程只能等待锁的释放而无法进行读操作。
    ,Lock (解决方案:ReentrantReadWriteLock)可以使得多个线程都只是进行读操作时,线程之间不会发生冲突。
    3、可以通过Lock得知线程有没有成功获取到锁 (解决方案:ReentrantLock),但是synchronized无法办到的。
    

    4、Lock接口中有哪些方法?

    线程中断是什么?
    一个线程在未正常结束之前, 被强制终止是很危险的事情. 因为它可能带来完全预料不到的严重后果. 
    那么不能直接把一个线程搞挂掉, 但有时候又有必要让一个线程死掉, 或者让它结束某种等待的状态 该怎么办呢? 
    优雅的方法就是, 给那个线程一个中断信号, 让它自己决定该怎么办。
    如何中断一个线程:
    Thread类中的方法interrupt()
    
    public interface Lock {
        //获取锁。如果锁已被其他线程获取,则进行等待,如果采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。
        //因此,一般来说,使用Lock必须在try…catch…块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。
        void lock();
        //通过这个方法去获取锁时,能够响应中断,即中断线程的等待状态。
        void lockInterruptibly() throws InterruptedException;  // 可以响应中断
        //尝试获取锁,如果获取成功,则返回true;如果获取失败(即锁已被其他线程获取),则返回false
        boolean tryLock();
        //这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false
        boolean tryLock(long time, TimeUnit unit) throws InterruptedException;  // 可以响应中断
        //释放锁
        void unlock();
        //用于线程间的协作
        Condition newCondition();
    }
    //interrupt()方法只能中断阻塞过程中的线程而不能中断正在运行过程中的线程。
    
    1、ReentrantLock,即可重入锁。ReentrantLock是唯一实现了Lock接口的类;
    2、ReadWriteLock,是一个接口,在它里面只定义了两个方法:
     Lock readLock();与 Lock writeLock();
    一个用来获取读锁,一个用来获取写锁。
    

    4、在java中wait和sleep方法的不同?
    最大的不同是在等待时wait会释放锁,而sleep一直持有锁。Wait通常被用于线程间交互,sleep通常被用于暂停执行。
    5、用Java写代码来解决生产者——消费者问题?
    生产者、消费者有很多的实现方法:

    • 用wait() / notify()方法
    • 用Lock的多Condition方法
    • BlockingQueue阻塞队列方法
      可以发现在上面实现阻塞队列题中,BlockingQueue的实现基本都用到了类似的实现,将BlockingQueue的实现方式稍微包装一下就成了一个生产者-消费者模式了。
      6、用Java写一个会导致死锁的程序,你将怎么解决?
      /**
    • 简单死锁程序
    •  lockA、lockB分别是两个资源,线程A、B必须同是拿到才能工作
      
    •  但A线程先拿lockA、再拿lockB
      
    •  线程先拿lockB、再拿lockA
      

    */
    产生死锁的四个必要条件:

    • 互斥条件:一个资源每次只能被一个进程使用。
    • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
    • 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
    • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
    
    /**
     * 简单死锁程序
     *      lockA、lockB分别是两个资源,线程A、B必须同是拿到才能工作
     *      但A线程先拿lockA、再拿lockB
     *      线程先拿lockB、再拿lockA
     * @author xuexiaolei
     * @version 2017年11月01日
     */
    public class SimpleDeadLock {
        public static void main(String[] args) {
            Object lockA = new Object();
            Object lockB = new Object();
            A a = new A(lockA, lockB);
            B b = new B(lockA, lockB);
            a.start();
            b.start();
        }
     
        static class A extends Thread{
            private final Object lockA;
            private final Object lockB;
            A(Object lockA, Object lockB) {
                this.lockA = lockA;
                this.lockB = lockB;
            }
     
            @Override public void run() {
                synchronized (lockA){
                    try {
                        Thread.sleep(1000);
                        synchronized (lockB){
                            System.out.println("Hello A");
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
     
        static class B extends Thread{
            private final Object lockA;
            private final Object lockB;
            B(Object lockA, Object lockB) {
                this.lockA = lockA;
                this.lockB = lockB;
            }
     
            @Override public void run() {
                synchronized (lockB){
                    try {
                        Thread.sleep(1000);
                        synchronized (lockA){
                            System.out.println("Hello B");
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
    

    7、什么是原子操作,Java中的原子操作是什么?
    原子操作是不可分割的操作,一个原子操作中间是不会被其他线程打断的,所以不需要同步一个原子操作。
    多个原子操作合并起来后就不是一个原子操作了,就需要同步了。
    i++不是一个原子操作,它包含 读取-修改-写入 操作,在多线程状态下是不安全的。
    另外,java内存模型允许将64位的读操作或写操作分解为2个32位的操作,所以对long和double类型的单次读写操作并不是原子的,注意使用volitile使他们成为原子操作。
    8、Java中的volatile关键是什么作用?怎样使用它?在Java中它跟synchronized方法有什么不同?
    volatile关键字的作用是:保证变量的可见性。
    在java内存结构中,每个线程都是有自己独立的内存空间(此处指的线程栈)。当需要对一个共享变量操作时,线程会将这个数据从主存空间复制到自己的独立空间内进行操作,然后在某个时刻将修改后的值刷新到主存空间。这个中间时间就会发生许多奇奇怪怪的线程安全问题了,volatile就出来了,它保证读取数据时只从主存空间读取,修改数据直接修改到主存空间中去,这样就保证了这个变量对多个操作线程的可见性了。换句话说,被volatile修饰的变量,能保证该变量的 单次读或者单次写 操作是原子的。
    但是线程安全是两方面需要的 原子性(指的是多条操作)和可见性。volatile只能保证可见性,synchronized是两个均保证的。
    volatile轻量级,只能修饰变量;synchronized重量级,还可修饰方法。
    volatile不会造成线程的阻塞,而synchronized可能会造成线程的阻塞。
    9、什么是竞争条件(race condition)?你怎样发现和解决的?

    10、你将如何使用thread dump?你将如何分析Thread dump?

    11、为什么我们调用start()方法时会执行run()方法,为什么我们不能直接调用run()方法?
    当你调用start()方法时你将创建新的线程,并且执行在run()方法里的代码。但是如果你直接调用run()方法,它不会创建新的线程也不会执行调用线程的代码。
    简单点来说:
    new一个Thread,线程进入了新建状态;调用start()方法,线程进入了就绪状态,当分配到时间片后就可以开始运行了。
    start()会执行线程的相应准备工作,然后自动执行run()方法的内容。是真正的多线程工作。
    而直接执行run()方法,会把run方法当成一个mian线程下的普通方法去执行,并不会在某个线程中执行它,这并不是多线程工作。
    12、Java中你怎样唤醒一个阻塞的线程?
    如果线程因为调用wait()、sleep()、或者join()方法而导致的阻塞,你可以中断线程,并且通过抛出InterruptedException来唤醒它。
    13、在Java中CycliBarriar和CountdownLatch有什么区别?

    CyclicBarrier和CountDownLatch 都位于java.util.concurrent 这个包下;除此之外,此包下还有ConcorrenctHashMap 分段锁(将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问 ,由Segment数组结构和HashEntry数组结构组成,Segment是一种可重入锁ReentrantLock);CopyOnWriteArrayList(并发容器用于读多写少的并发场景,线程安全的 写时复制 ),ReentrantLock(可重入锁:线程可以进入它已经拥有的锁的同步代码块儿) 、callable(创建线程的三种方式之一)、ReentrantReadWriteLock (允许多个读线程同时访问)等;
    

    CycliBarriar(回环栅栏)与CountdownLatch总结:

    1、CountDownLatch和CyclicBarrier都能够实现线程之间的等待,只不过它们侧重点不同:
    2、CountDownLatch一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行;
    而CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;
    3、CountDownLatch是不能够重用的(内部计数器可重置),而CyclicBarrier是可以重用的(计数器值为0后不可再用)。
    

    CountdownLatch与CycliBarriar区别

    1、CyclicBarrier可重用的,因为内部计数器可重置;CountDownLatch不可重用,计数器值为0该CountDownLatch就不可再用。
    2、.CyclicBarrier唤醒等待线程虽然是唤醒全部,但等待线程是按顺序依次执行的;CountDownLatch是唤醒多个任务,抢占式执行;
    

    14、什么是不可变对象,它对写并发应用有什么帮助?
    immutable Objects(不可变对象)就是那些一旦被创建,它们的状态就不能被改变的Objects,每次对他们的改变都是产生了新的immutable的对象,而mutable Objects(可变对象)就是那些创建后,状态可以被改变的Objects.
    如何在Java中写出Immutable的类?

    1. immutable对象的状态在创建之后就不能发生改变,任何对它的改变都应该产生一个新的对象(String就是一个不可变对象)。 
    2. immutable类的所有的属性都应该是final的。 
    3. 对象必须被正确的创建,比如:对象引用在对象创建过程中不能泄露(leak)。 
    4. 对象应该是final的,以此来限制子类继承父类,以避免子类改变了父类的immutable特性。 
    5. 如果类中包含mutable类对象,那么返回给客户端的时候,返回该对象的一个拷贝,而不是该对象本身(该条可以归为第一条中的一个特例)
    使用Immutable类的好处: 
    1. Immutable对象是线程安全的,可以不用被synchronize就在并发环境中共享 
    2. Immutable对象简化了程序开发,因为它无需使用额外的锁机制就可以在线程间共享 
    3. Immutable对象提高了程序的性能,因为它减少了synchroinzed的使用 
    4. Immutable对象是可以被重复使用的,你可以将它们缓存起来重复使用,就像字符串字面量和整型数字一样。
    你可以使用静态工厂方法来提供类似于valueOf()这样的方法,它可以从缓存中返回一个已经存在的Immutable对象,而不是重新创建一个。
    

    15、用Java实现阻塞队列?
    分别用synchronized 和 wait/notify 实现;
    Java5中提供了BlockingQueue的方法,并且有几个实现;
    16、线程状态转换


    image.png

    16、线程互斥和同步?
    (1)互斥:多个线程之间有共享资源(shared resource)时会出现互斥现象。
    临界区的作用是在任何时刻一个共享资源只能供一个线程使用。
    在JAVA中使用关键字synchronized定义临界区,能对共享对象进行上锁操作。
    (2)同步:当线程A使用某个对象,而此对象又需要线程B修改后才能符合本线程的需要,此时线程A就要等待线程B完成修改工作。这种线程相互等待称为线程的同步。
    为实现同步,JAVA语言提供了wait()、notify()和notifyAll()三个方法供线程在临界区中使用。
    17、Java并发包有哪些类?
    通常所说的并发包也就是java.util.concurrent及其子包;
    (1)ConcurrentHashMap

    ConcurrentHashMap其实就是线程安全版本的hashMap。通过把整个Map分为N个Segment(HashTable),
    底层是一个segment数组和hashEntry数组,
    segment数组结构和hashmap结构类似,由数组和链表组成;
    hashEntry中存放键值对内容,每一个segment对应一个hashEntry数组,
    当一个线程要修改hashEntry中的某个元素时,segment就会加上一个可重用的锁,以此来解决多线程的冲突。
    

    (2)各种并发队列实现,如各种BlockingQueue实现;

    ConcurrentLinkedDeque:无界双端非阻塞队列;线程安全的队列。
    ConcurrentLinkedQueue:(无界单端非阻塞队列)基于链接节点的、线程安全的队列。并发访问不需要同步。因为它在队列的尾部添加元素并从头部删除它们;
    

    (4)CopyOnWriteArrayList

    CopyOnWriteArrayList是线程安全版本的ArrayList;
    CopyOnWrite容器即写时复制的容器。
    通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,
    而是先将当前容器进行Copy,复制出一个新的容器,
    然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。
    这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,
    因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
    

    (5)Executor框架

    可以创建各种不同类型的线程池,调度任务运行等。
    绝大部分情况下,不再需要自己从头实现线程池和任务调度器。
    

    18、并发编程-Executor框架


    image.png

    (1)简介:为灵活且强大的异步执行框架,其支持多种不同类型的任务执行策略,基于生产者-消费者模式,其提交任务的线程相当于生产者,执行任务的线程相当于消费者,并用Runnable来表示任务
    (2)ThreadPoolExecutor(线程池的真正实现)

    public ThreadPoolExecutor(int corePoolSize,//核心线程数
                              int maximumPoolSize,//最大线程数,可允许创建的线程数
                              long keepAliveTime,//如果线程数多于corePoolSize,则这些多余的线程的空闲时间超过keepAliveTime时将被终止
                              TimeUnit unit,//keepAliveTime参数的时间单位
                              BlockingQueue<Runnable> workQueue,//保存任务的阻塞队列,与线程池的大小有关
                              ThreadFactory threadFactory,//可选参数、使用ThreadFactory创建新线程,默认使用defaultThreadFactory创建线程
                              RejectedExecutionHandler handler) //可选参数、
    
    workQueue:保存任务的阻塞队列,与线程池的大小有关:
    1、当运行的线程数少于corePoolSize时,在有新任务时直接创建新线程来执行任务而无需再进队列;
    2、当运行的线程数等于或多于corePoolSize,在有新任务添加时则选加入队列,不直接创建线程
    3、当队列满时,在有新任务时就创建新线程
    
    handle:定义处理被拒绝任务的策略,默认使用ThreadPoolExecutor.AbortPolicy,
    任务被拒绝时将抛出RejectExecutorException
    

    (3)Executors(静态工厂创建线程池)
    提供了一系列静态工厂方法用于创建各种线程池

    1、newFixedThreadPool:
    创建可重用且固定线程数的线程池,如果线程池中的所有线程都处于活动状态,
    此时再提交任务就在队列中等待,直到有可用线程;
    如果线程池中的某个线程由于异常而结束时,线程池就会再补充一条新线程。
    2、newSingleThreadExecutor:
    创建一个单线程的Executor,如果该线程因为异常而结束就新建一条线程来继续执行后续的任务;
    3、newScheduledThreadPool:
    创建一个可延迟执行或定期执行的线程池;
    4、newCachedThreadPool:
    创建可缓存的线程池,如果线程池中的线程在60秒未被使用就将被移除,
    在执行新的任务时,当线程池中有之前创建的可用线程就重用可用线程,否则就新建一条线程。
    

    (4)Executor的生命周期
    ExecutorService提供了管理Eecutor生命周期的方法,ExecutorService的生命周期包括了:运行 关闭和终止三种状态。

    1、ExecutorService在初始化创建时处于运行状态;
    2、shutdown方法等待提交的任务执行完成并不再接受新任务,在完成全部提交的任务后关闭;
    3、shutdownNow方法将强制终止所有运行中的任务并不再允许提交新任务;
    

    18、线程池有哪些参数?

    线程池参数:
    1、CoreThreadNum:核心线程数
    2、MaxcoreThreadNum:最大线程数
    3、KeepAliveTime:线程数大于核心线程数时,多余线程空余时间超过KeepAliveTime将给终止;
    4、Unit:KeepAliveTime的单位
    5、workQuene:保存任务的阻塞队列,与线程池的大小有关
    6、ThreadFactory:可选参数、使用ThreadFactory创建新线程
    7、RejectedExecutionHandler:定义处理被拒绝任务的策略,默认使用ThreadPoolExecutor.AbortPolicy
    

    19、公平锁和非公平锁?
    公平锁是指多个线程同时尝试获取同一把锁时,获取锁的顺序按照线程达到的顺序,而非公平锁则允许线程“插队”。
    synchronized是非公平锁,而ReentrantLock的默认实现是非公平锁,但是也可以设置为公平锁:
    //ReentrantLock创建一个公平锁,构造传参true
    Lock lock = new ReentrantLock(true);
    20、Thread.sleep()方法需要捕获什么异常?

    Thread.sleep()是让线程休眠。在这种睡眠状态下,你可能调用interrupte来终止线程,
    这样就会抛出InterruptException,只有捕获异常进行处理,才能正确的终止线程。
    

    21、子线程正确退出的方式?

    1.设置退出标志,使线程正常退出,也就是当run()方法完成后线程终止;
    public class ThreadSafe extends Thread {
        public volatile boolean exit = false; 
            public void run() { 
            while (!exit){
                //do something
            }
        } 
    }
    
    2.使用interrupt()方法中断线程:
    捕获InterruptedException异常之后通过break来跳出循环,才能正常结束run方法。
    3.使用stop方法强行终止线程(不推荐使用,极端不安全的!)
    

    22、如何让三个线程按顺序执行?
    把线程放到栈里面、join()方法;
    23、读写锁

    ReentrantReadWriteLock也是一个接口(继承于Lock接口),提供了readLock和writeLock两种锁的操作机制(返回一个Lock对象);
    1、基本规则:  读读不互斥 读写互斥 写写互斥;
    2、其通过在重入锁ReentrantLock上维护一个读锁一个写锁实现的;
    3、
    

    24、Thread.yiled()

    Java线程中的Thread.yield( )方法,译为线程让步。
    当一个线程使用了这个方法之后,它就会把自己CPU执行的时间让掉,
    让自己或者其它的线程运行,注意是让自己或者其他线程运行,并不是单纯的让给其他线程。
    
    yield()的作用是让步。它能让当前线程由“运行状态”进入到“就绪状态”,从而让其它具有相同优先级的等待线
    程获取执行权;但是,并不能保证在当前线程调用yield()之后,其它具有相同优先级的线程就一定能获得执行
    权;也有可能是当前线程又进入到“运行状态”继续运行!
    
    举个例子:一帮朋友在排队上公交车,轮到Yield的时候,他突然说:我不想先上去了,咱们大家来竞赛上公
    交车。然后所有人就一块冲向公交车,有可能是其他人先上车了,也有可能是Yield先上车了。
    

    25、进程与线程的区别与联系?

    1、一个进程可以有多个线程,但至少有一个线程;而一个线程只能在一个进程的地址空间内活动。
    2、资源分配给进程,同一个进程的所有线程共享该进程所有资源。
    3、CPU分配给线程,即真正在处理器运行的是线程。
    4、线程在执行过程中需要协作同步,不同进程的线程间要利用消息通信的办法实现同步。
    

    26、乐观锁与悲观锁原理及实现

    1、乐观锁
    总是认为不会产生并发问题,每次去取数据的时候总认为不会有其他线程对数据进行修改,
    因此不会上锁,但是在更新时会判断其他线程在这之前有没有对数据进行修改,
    一般会使用版本号机制或CAS操作实现。
    (1)version方式:
    一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,
    当数据被修改时,version值会加一。
    当线程A要更新数据值时,在读取数据的同时也会读取version值,
    在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,
    否则重试更新操作,直到更新成功。
    (2)CAS操作方式:
    即compare and swap 或者 compare and set,涉及到三个操作数,数据所在的内存值,预期值,新值。
    当需要更新时,判断当前内存值与之前取到的值是否相等,若相等,则用新值更新,若失败则重试,
    一般情况下是一个自旋操作,即不断的重试。
    
    2、悲观锁
    总是假设最坏的情况,每次取数据时都认为其他线程会修改,所以都会加锁(读锁、写锁、行锁等),
    当其他线程想要访问数据时,都需要阻塞挂起。可以依靠数据库实现,如行锁、读锁和写锁都是在操作之前加锁,
    在Java中,synchronized的思想也是悲观锁。
    

    相关文章

      网友评论

          本文标题:JAVA多线程

          本文链接:https://www.haomeiwen.com/subject/asrgpftx.html