美文网首页Java技术专栏
Java多线程知识小抄集(二)

Java多线程知识小抄集(二)

作者: 朱小厮 | 来源:发表于2018-01-16 01:00 被阅读42次

    本文主要整理笔者遇到的Java多线程的相关知识点,适合速记,故命名为“小抄集”。本文没有特别重点,每一项针对一个多线程知识做一个概要性总结,也有一些会带一点例子,习题方便理解和记忆。

    16. 读写锁ReentrantReadWriteLock

    读写锁表示也有两个锁,一个是读操作相关的锁,也称为共享锁;另一个是写操作相关的锁,也叫排它锁。也就是多个读锁之间不互斥,读锁与写锁互斥,写锁与写锁互斥。在没有Thread进行写操作时,进行读取操作的多个Thread都可以获取读锁,而进行写入操作的Thread只有在获取写锁后才能进行写入操作。即多个Thread可以同时进行读取操作,但是同一时刻只允许一个Thread进行写入操作。(lock.readlock.lock(), lock.readlock.unlock, lock.writelock.lock, lock.writelock.unlock)

    17. Timer的使用

    JDK中的Timer类主要负责计划任务的功能,也就是在指定时间开始执行某一任务。Timer类的主要作用就是设置计划任务,但封装任务的类却是TimerTask类(public abstract class TimerTask extends Object implements Runnable)。可以通过new Timer(true)设置为后台线程。

    有以下几个方法:

    • void schedule(TimerTask task, Date time):在指定的日期执行某一次任务。如果执行任务的时间早于当前时间则立刻执行。
    • void schedule(TimerTask task, Date firstTime, long period):在指定的日期之后,按指定的间隔周期性地无限循环地执行某一任务。如果执行任务的时间早于当前时间则立刻执行。
    • void schedule(TimerTask task, long delay):以当前时间为参考时间,在此基础上延迟指定的毫秒数后执行一次TimerTask任务。
    • void schedule(TimerTask task, long delay, long period):以当前时间为参考时间,在此基础上延迟指定的毫秒数,再以某一间隔无限次数地执行某一任务。
    • void scheduleAtFixedRate(TimerTask task, Date firstTime, long period):下次执行任务时间参考上次任务的结束时间,且具有“追赶性”。

    TimerTask是以队列的方式一个一个被顺序执行的,所以执行的时间有可能和预期的时间不一致,因为前面的任务有可能消耗的时间较长,则后面的任务运行时间也会被延迟。
    TimerTask类中的cancel方法的作用是将自身从任务队列中清除。
    Timer类中的cancel方法的作用是将任务队列中的全部任务清空,并且进程被销毁。

    Timer的缺陷:Timer支持基于绝对时间而不是相对时间的调度机制,因此任务的执行对系统时钟变化很敏感,而ScheduledThreadPoolExecutor只支持相对时间的调度。Timer在执行所有定时任务时只会创建一个线程。如果某个任务的执行时间过长,那么将破坏其他TimerTask的定时精确性。Timer的另一个问题是,如果TimerTask抛出了一个未检查的异常,那么Timer将表现出糟糕的行为。Timer线程并不波或异常,因此当TimerTask抛出为检测的异常时将终止定时线程。

    JDK5或者更高的JDK中已经很少使用Timer.

    18. 线程安全的单例模式

    建议不要采用DCL的写法,建议使用下面这种写法:

    public class LazyInitHolderSingleton {  
            private LazyInitHolderSingleton() {  
            }  
    
            private static class SingletonHolder {  
                    private static final LazyInitHolderSingleton INSTANCE = new LazyInitHolderSingleton();  
            }  
    
            public static LazyInitHolderSingleton getInstance() {  
                    return SingletonHolder.INSTANCE;  
            }  
    }  
    

    或者这种:

    public enum SingletonClass
    {
        INSTANCE;
    }
    

    19. 线程组ThreadGroup

    为了有效地对一些线程进行组织管理,通常的情况下事创建一个线程组,然后再将部分线程归属到该组中,这样可以对零散的线程对象进行有效的组织和规划。参考以下案例:

            ThreadGroup tgroup = new ThreadGroup("mavelous zzh");
            new Thread(tgroup, new Runnable(){
                @Override
                public void run()
                {
                    System.out.println("A: Begin: "+Thread.currentThread().getName());
                    while(!Thread.currentThread().isInterrupted())
                    {
    
                    }
                    System.out.println("A: DEAD: "+Thread.currentThread().getName());
                }}).start();;
            new Thread(tgroup, new Runnable(){
                @Override
                public void run()
                {
                    System.out.println("B: Begin: "+Thread.currentThread().getName());
                    while(!Thread.currentThread().isInterrupted())
                    {
    
                    }
                    System.out.println("B: DEAD: "+Thread.currentThread().getName());
                }}).start();;
            System.out.println(tgroup.activeCount());
            System.out.println(tgroup.getName());
            System.out.println(tgroup.getMaxPriority());
            System.out.println(tgroup.getParent());
            TimeUnit.SECONDS.sleep(5);
            tgroup.interrupt();
    

    输出:

    A: Begin: Thread-0
    2
    mavelous zzh
    10
    B: Begin: Thread-1
    java.lang.ThreadGroup[name=main,maxpri=10]
    B: DEAD: Thread-1
    A: DEAD: Thread-0
    

    20. 多线程的异常捕获UncaughtExceptionHandler

    setUncaughtExceptionHandler()的作用是对指定线程对象设置默认的异常处理器。

            Thread thread = new Thread(new Runnable(){
                @Override
                public void run()
                {
                    int a=1/0;
                }
            });
            thread.setUncaughtExceptionHandler(new UncaughtExceptionHandler(){
                @Override
                public void uncaughtException(Thread t, Throwable e)
                {
                    System.out.println("线程:"+t.getName()+" 出现了异常:"+e.getMessage());
                }
            });
            thread.start();
    

    输出:线程:Thread-0 出现了异常:/ by zero
    setDefaultUncaughtExceptionHandler()方法对所有线程对象设置异常处理器。

            Thread thread = new Thread(new Runnable(){
                @Override
                public void run()
                {
                    int a=1/0;
                }
            });
            Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler(){
                @Override
                public void uncaughtException(Thread t, Throwable e)
                {
                    System.out.println("线程:"+t.getName()+" 出现了异常:"+e.getMessage());
                }
            });
            thread.start();
    

    输出同上,注意两者之间的区别。如果既包含setUncaughtExceptionHandler又包含setDefaultUncaughtExceptionHandler那么会被setUncaughtExceptionHandler处理,setDefaultUncaughtExceptionHandler则忽略。更多详细信息参考《JAVA多线程之UncaughtExceptionHandler——处理非正常的线程中止

    21.ReentrantLock与synchonized区别

    1. ReentrantLock可以中断地获取锁(void lockInterruptibly() throws InterruptedException)
    2. ReentrantLock可以尝试非阻塞地获取锁(boolean tryLock())
    3. ReentrantLock可以超时获取锁。通过tryLock(timeout, unit),可以尝试获得锁,并且指定等待的时间。
    4. ReentrantLock可以实现公平锁。通过new ReentrantLock(true)实现。
    5. ReentrantLock对象可以同时绑定多个Condition对象,而在synchronized中,锁对象的的wait(), notify(), notifyAll()方法可以实现一个隐含条件,如果要和多于一个的条件关联的对象,就不得不额外地添加一个锁,而ReentrantLock则无需这样做,只需要多次调用newCondition()方法即可。

    22. 使用多线程的优势

    更多的处理器核心;更快的响应时间;更好的编程模型。

    23. 构造线程

    一个新构造的线程对象是由其parent线程来进行空间分配的,而child线程继承了parent线程的:是否为Daemon、优先级、加载资源的contextClassLoader以及InheritableThreadLocal(参考第12条),同时还会分配一个唯一的ID来标志这个child线程。

    24. 使用多线程的方式

    extends Thread 或者implements Runnable

    25. 读写锁

    读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞。读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排它锁有了很大的提升。Java中使用ReentrantReadWriteLock实现读写锁,读写锁的一般写法如下(修改自JDK7中的示例):

        class RWDictionary {
        private final Map<String, Object> m = new TreeMap<String, Object>();
        private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
        private final Lock r = rwl.readLock();
        private final Lock w = rwl.writeLock();
    
        public Object get(String key)
        {
            r.lock();
            try
            {
                return m.get(key);
            }
            finally
            {
                r.unlock();
            }
        }
    
        public String[] allKeys()
        {
            r.lock();
            try
            {
                return (String[]) m.keySet().toArray();
            }
            finally
            {
                r.unlock();
            }
        }
    
        public Object put(String key, Object value)
        {
            w.lock();
            try
            {
                return m.put(key, value);
            }
            finally
            {
                w.unlock();
            }
        }
    
        public void clear()
        {
            w.lock();
            try
            {
                m.clear();
            }
            finally
            {
                w.unlock();
            }
        }
     }
    

    26.锁降级

    锁降级是指写锁降级成读锁。如果当前线程拥有写锁,然后将其释放,最后获取读锁,这种分段完成的过程不能称之为锁降级。锁降级是指把持住(当前拥有的)写锁,再获取到读锁,最后释放(先前拥有的)写锁的过程。参考下面的示例:

        private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
        private final Lock r = rwl.readLock();
        private final Lock w = rwl.writeLock();
        private volatile static boolean update = false;
    
        public void processData()
        {
            r.lock();
            if(!update)
            {
                //必须先释放读锁
                r.unlock();
                //锁降级从写锁获取到开始
                w.lock();
                try
                {
                    if(!update)
                    {
                        //准备数据的流程(略)
                        update = true;
                    }
                    r.lock();
                }
                finally
                {
                    w.unlock();
                }
                //锁降级完成,写锁降级为读锁
            }
    
            try
            {
                //使用数据的流程(略)
            }
            finally
            {
                r.unlock();
            }
        }
    

    锁降级中的读锁是否有必要呢?答案是必要。主要是为了保证数据的可见性,如果当前线程不获取读锁而是直接释放写锁,假设此刻另一个线程(T)获取了写锁并修改了数据,那么当前线程无法感知线程T的数据更新。如果当前线程获取读锁,即遵循锁降级的步骤,则线程T将会被阻塞,直到当前线程使用数据并释放读锁之后,线程T才能获取写锁进行数据更新。


    PS:消息中间件(Kafka、RabbitMQ)交流可加微信:hiddenzzh
    欢迎支持《RabbitMQ实战指南》以及关注微信公众号:朱小厮的博客。


    欢迎关注

    相关文章

      网友评论

        本文标题:Java多线程知识小抄集(二)

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