JAVA多线程

2019-01-13  本文已影响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写代码来解决生产者——消费者问题?
生产者、消费者有很多的实现方法:

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


/**
 * 简单死锁程序
 *      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的思想也是悲观锁。
上一篇下一篇

猜你喜欢

热点阅读