zoukankan      html  css  js  c++  java
  • 从线程池到synchronized关键字详解

     

    转载:http://www.cnblogs.com/itdragon/p/8319183.html

     

    线程池 BlockingQueue synchronized volatile

    前段时间看了一篇关于"一名3年工作经验的程序员应该具备的技能"文章,倍受打击。很多熟悉而又陌生的知识让我怀疑自己是一个假的程序员。本章从线程池,阻塞队列,synchronized 和 volatile关键字,wait,notify方法实现线程之间的通讯,死锁,常考面试题。将这些零碎的知识整合在一起。如下图所示。

    学习流程图:
    学习流程图
    技术:Executors,BlockingQueue,synchronized,volatile,wait,notify
    说明:文章学习思路:线程池---->队列---->关键字---->死锁---->线程池实战
    源码:https://github.com/ITDragonBlog/daydayup/tree/master/ThreadBase

    线程池

    线程池,顾名思义存放线程的池子,可以类比数据库的连接池。因为频繁地创建和销毁线程会给服务器带来很大的压力。若能将创建的线程不再销毁而是存放在池中等待下一个任务使用,可以不仅减少了创建和销毁线程所用的时间,提高了性能,同时还减轻了服务器的压力。

    线程池的使用

    初始化线程池有五个核心参数,分别是 corePoolSizemaximumPoolSizekeepAliveTimeunitworkQueue。还有两个默认参数 threadFactory, handler
    corePoolSize:线程池初始核心线程数。初始化线程池的时候,池内是没有线程,只有在执行任务的时会创建线程。
    maximumPoolSize:线程池允许存在的最大线程数。若超过该数字,默认提示RejectedExecutionException异常
    keepAliveTime:当前线程数大于核心线程时,该参数生效,其目的是终止多余的空闲线程等待新任务的最长时间。即指定时间内将还未接收任务的线程销毁。
    unit:keepAliveTime 的时间单位
    workQueue:缓存任务的的队列,一般采用LinkedBlockingQueue。
    threadFactory:执行程序创建新线程时使用的工厂,一般采用默认值。
    handler:超出线程范围和队列容量而使执行被阻塞时所使用的处理程序,一般采用默认值。

    线程池工作流程

    开始,游泳馆来了一名学员,于是馆主安排一个教练负责培训这名学员;
    然后,游泳馆来了六名学员,可馆主只招了五名教练,于是有一名学员被安排到休息室等待;
    后来,游泳馆来了十六名学员,休息室已经满了,馆主核算了开支,预计最多可招十名教练;
    最后,游泳馆只来了十名学员,馆主对教练说,如果半天内接不到学员的教练就可以走了;
    结果,游泳馆没有学员,关闭了。
    在接收任务前,线程池内是没有线程。只有当任务来了才开始新建线程。当任务数大于核心线程数时,任务进入队列中等待。若队列满了,则线程池新增线程直到最大线程数。再超过则会执行拒绝策略。

    线程池的三种关闭

    shutdown: 线程池不再接收任务,等待线程池中所有任务完成后,关闭线程池。常用
    shutdownNow: 线程池不再接收任务,忽略队列中的任务,尝试中断正在执行的任务,返回未执行任务列表,关闭线程池。慎用
    awaitTermination: 线程池可以继续接收任务,当任务都完成后,或者超过设置的时间后,关闭线程池。方法是阻塞的,考虑使用

    线程池的种类

    1 newSingleThreadExecutor() 单线程线程池
    初始线程数和允许最大线程数都是一,keepAliveTime 也就失效了,队列是无界阻塞队列。该线程池的主要作用是负责缓存任务。

    2 newFixedThreadPool(n) 固定大小线程池
    初始线程数和允许最大线程数相同,且大小自定义,keepAliveTime 也就失效了,队列是无界阻塞队列。符合大部分业务要求,常用。

    3 newCachedThreadPool() 缓存无界线程池
    初始线程数为零,最大线程数为无穷大,keepAliveTime 60秒类终止空闲线程,队列是无缓冲无界队列。适合任务数不多的场景,慎用。

    import java.util.concurrent.ArrayBlockingQueue;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.ThreadPoolExecutor;
    import java.util.concurrent.TimeUnit;
    
    /**
     * 线程池
     * 优势,类比数据库的连接池
     * 1. 频繁的创建和销毁线程会给服务器带来很大的压力
     * 2. 若创建的线程不销毁而是留在线程池中等待下次使用,则会很大地提高效率也减轻了服务器的压力
     * 
     * 三种workQueue策略
     * 直接提交 SynchronousQueue
     * 无界队列 LinkedBlockingQueue
     * 有界队列 ArrayBlockingQueue
     * 
     * 四种拒绝策略
     * AbortPolicy : JDK默认,超出 MAXIMUM_POOL_SIZE 放弃任务抛异常 RejectedExecutionException
     * CallerRunsPolicy : 尝试直接调用被拒绝的任务,若线程池被关闭,则丢弃任务
     * DiscardOldestPolicy : 放弃队列最前面的任务,然后重新尝试执被拒绝的任务。若线程池被关闭,则丢弃任务
     * DiscardPolicy : 放弃不能执行的任务但不抛异常
     */
    public class ThreadPoolExecutorStu {
        
        // 线程池中初始线程个数
        private final static Integer CORE_POOL_SIZE = 3;
        // 线程池中允许的最大线程数
        private final static Integer MAXIMUM_POOL_SIZE = 8;
        // 当线程数大于初始线程时。终止多余的空闲线程等待新任务的最长时间
        private final static Long KEEP_ALIVE_TIME = 10L;
        // 任务缓存队列 ,即线程数大于初始线程数时先进入队列中等待,此数字可以稍微设置大点,避免线程数超过最大线程数时报错。或者直接用无界队列
        private final static ArrayBlockingQueue<Runnable> WORK_QUEUE = new ArrayBlockingQueue<Runnable>(5);
        
        public static void main(String[] args) {
            Long start = System.currentTimeMillis();
            /**
             * ITDragonThreadPoolExecutor 耗时 1503
             * ITDragonFixedThreadPool 耗时 505
             * ITDragonSingleThreadExecutor 语法问题报错,
             * ITDragonCachedThreadPool 耗时506
             * 推荐使用自定义线程池,或newFixedThreadPool(n)
             */
            ThreadPoolExecutor threadPoolExecutor = ITDragonThreadPoolExecutor();
            for (int i = 0; i < 8; i++) {   // 执行8个任务,若超过MAXIMUM_POOL_SIZE则会报错 RejectedExecutionException
                MyRunnableTest myRunnable = new MyRunnableTest(i);
                threadPoolExecutor.execute(myRunnable);
                System.out.println("线程池中现在的线程数目是:"+threadPoolExecutor.getPoolSize()+",  队列中正在等待执行的任务数量为:"+  
                        threadPoolExecutor.getQueue().size());
            }
            // 关掉线程池 ,并不会立即停止(停止接收外部的submit任务,等待内部任务完成后才停止),推荐使用。 与之对应的是shutdownNow,不推荐使用
            threadPoolExecutor.shutdown();  
            try {
                // 阻塞等待30秒关掉线程池,返回true表示已经关闭。和shutdown不同,它可以接收外部任务,并且还阻塞。这里为了方便统计时间,所以选择阻塞等待关闭。
                threadPoolExecutor.awaitTermination(30, TimeUnit.SECONDS);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("耗时 : " + (System.currentTimeMillis() - start));
        }
        
        // 自定义线程池,开发推荐使用
        public static ThreadPoolExecutor ITDragonThreadPoolExecutor() {
            // 构建一个,初始线程数量为3,最大线程数据为8,等待时间10分钟 ,队列长度为5 的线程池
            ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                    CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_TIME, TimeUnit.MINUTES, WORK_QUEUE);
            return threadPoolExecutor;
        }
        
        /**
         * 固定大小线程池
         * corePoolSize初始线程数和maximumPoolSize最大线程数一样,keepAliveTime参数不起作用,workQueue用的是无界阻塞队列
         */
        public static ThreadPoolExecutor ITDragonFixedThreadPool() {
            ExecutorService executor = Executors.newFixedThreadPool(8);
            ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executor;
            return threadPoolExecutor;
        }
        
        /**
         * 单线程线程池
         * 等价与Executors.newFixedThreadPool(1);
         */
        public static ThreadPoolExecutor ITDragonSingleThreadExecutor() {
            ExecutorService executor = Executors.newSingleThreadExecutor();
            ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executor;
            return threadPoolExecutor;
        }
        
        /**
         * 无界线程池
         * corePoolSize 初始线程数为零
         * maximumPoolSize 最大线程数无穷大
         * keepAliveTime 60秒类将没有被用到的线程终止
         * workQueue SynchronousQueue 队列,无容量,来任务就直接新增线程
         * 不推荐使用
         */
        public static ThreadPoolExecutor ITDragonCachedThreadPool() {
            ExecutorService executor = Executors.newCachedThreadPool();
            ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executor;
            return threadPoolExecutor;
        }
        
    }
    
    class MyRunnableTest implements Runnable {
        private Integer num;    // 正在执行的任务数
        public MyRunnableTest(Integer num) {
            this.num = num;
        }
        public void run() {
            System.out.println("正在执行的MyRunnable " + num);
            try {
                Thread.sleep(500);// 模拟执行事务需要耗时
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("MyRunnable " + num + "执行完毕");
        }
    }

    队列

    队列,是一种数据结构。大部分的队列都是以FIFO(先进先出)的方式对各个元素进行排序的(PriorityBlockingQueue是根据优先级排序的)。队列的头移除元素,队列的末尾插入元素。插入的元素建议不能为null。Queue主要分两类,一类是高性能队列 ConcurrentLinkedQueue;一类是阻塞队列 BlockingQueue。本章重点介绍BlockingQueue

    ConcurrentLinkedQueue

    ConcurrentLinkedQueue性能好于BlockingQueue。是基于链接节点的无界限线程安全队列。该队列的元素遵循先进先出的原则。不允许null元素。

    BlockingQueue

    ArrayBlockingQueue: 基于数组的阻塞队列,在内部维护了一个定长数组,以便缓存队列中的数据对象。并没有实现读写分离,也就意味着生产和消费不能完全并行。是一个有界队列
    LinkedBlockingQueue:基于列表的阻塞队列,在内部维护了一个数据缓冲队列(由一个链表构成),实现采用分离锁(读写分离两个锁),从而实现生产者和消费者操作的完全并行运行。是一个无界队列,也可以指定队列大小
    SynchronousQueue: 没有缓存的队列,生存者生产的数据直接会被消费者获取并消费。若没有数据就直接调用出栈方法则会报错。

    三种队列使用场景
    newFixedThreadPool 线程池采用的队列是LinkedBlockingQueue。其优点是无界可缓存,内部实现读写分离,并发的处理能力高于ArrayBlockingQueue
    newCachedThreadPool 线程池采用的队列是SynchronousQueue。其优点就是无缓存,接收到的任务均可直接处理,再次强调,慎用!
    并发量不大,服务器性能较好,可以考虑使用SynchronousQueue。
    并发量较大,服务器性能较好,可以考虑使用LinkedBlockingQueue。
    并发量很大,服务器性能无法满足,可以考虑使用ArrayBlockingQueue。系统的稳定最重要。

    import java.util.ArrayList;
    import java.util.List;
    import java.util.concurrent.ArrayBlockingQueue;
    import java.util.concurrent.ConcurrentLinkedQueue;
    import java.util.concurrent.LinkedBlockingQueue;
    import java.util.concurrent.SynchronousQueue;
    import java.util.concurrent.TimeUnit;
    import org.junit.Test;
    
    /**
     * 阻塞队列
     * ArrayBlockingQueue        :有界
     * LinkedBlockingQueue       :无界
     * SynchronousQueue          :无缓冲直接用
     * 非阻塞队列
     * ConcurrentLinkedQueue     :高性能
     */
    public class ITDragonQueue {
        
        /**
         * ArrayBlockingQueue : 基于数组的阻塞队列实现,在内部维护了一个定长数组,以便缓存队列中的数据对象。
         * 内部没有实现读写分离,生产和消费不能完全并行,
         * 长度是需要定义的,
         * 可以指定先进先出或者先进后出,
         * 是一个有界队列。
         */
        @Test
        public void ITDragonArrayBlockingQueue() throws Exception {  
            ArrayBlockingQueue<String> array = new ArrayBlockingQueue<String>(5); // 可以尝试 队列长度由3改到5  
            array.offer("offer 插入数据方法---成功返回true 否则返回false");  
            array.offer("offer 3秒后插入数据方法", 3, TimeUnit.SECONDS);  
            array.put("put 插入数据方法---但超出队列长度则阻塞等待,没有返回值");  
            array.add("add 插入数据方法---但超出队列长度则提示 java.lang.IllegalStateException"); //  java.lang.IllegalStateException: Queue full  
            System.out.println(array);
            System.out.println(array.take() + " 	还剩元素 : " + array);   // 从头部取出元素,并从队列里删除,若队列为null则一直等待
            System.out.println(array.poll() + " 	还剩元素 : " + array);   // 从头部取出元素,并从队列里删除,执行poll 后 元素减少一个
            System.out.println(array.peek() + " 	还剩元素 : " + array);   // 从头部取出元素,执行peek 不移除元素
        }
        
        /**
         * LinkedBlockingQueue:基于列表的阻塞队列,在内部维护了一个数据缓冲队列(该队列由一个链表构成)。
         * 其内部实现采用读写分离锁,能高效的处理并发数据,生产者和消费者操作的完全并行运行
         * 可以不指定长度,
         * 是一个无界队列。
         */
        @Test
        public void ITDragonLinkedBlockingQueue() throws Exception {
            LinkedBlockingQueue<String> queue = new LinkedBlockingQueue<String>();
            queue.offer("1.无界队列");
            queue.add("2.语法和ArrayBlockingQueue差不多");
            queue.put("3.实现采用读写分离");
            List<String> list = new ArrayList<String>();
            System.out.println("返回截取的长度 : " + queue.drainTo(list, 2));
            System.out.println("list : " + list);
        }
        
        /**
         * SynchronousQueue:没有缓冲的队列,生存者生产的数据直接会被消费者获取并消费。
         */
        @Test
        public void ITDragonSynchronousQueue() throws Exception {
            final SynchronousQueue<String> queue = new SynchronousQueue<String>();
            Thread thread1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        System.out.println("take , 在没有取到值之前一直处理阻塞  : " + queue.take());
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
            thread1.start();
            Thread.sleep(2000);
            Thread thread2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    queue.add("进值!!!");
                }
            });
            thread2.start();    
        }
    
        /**
         * ConcurrentLinkedQueue:是一个适合高并发场景下的队列,通过无锁的方式,实现了高并发状态下的高性能,性能好于BlockingQueue。
         * 它是一个基于链接节点的无界限线程安全队列。该队列的元素遵循先进先出的原则。头是最先加入的,尾是最后加入的,不允许null元素。
         * 无阻塞队列,没有 put 和 take 方法
         */
        @Test
        public void ITDragonConcurrentLinkedQueue() throws Exception {
            ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<String>();  
            queue.offer("1.高性能无阻塞");
            queue.add("2.无界队列");
            System.out.println(queue);
            System.out.println(queue.poll() + " 	  : " + queue);   // 从头部取出元素,并从队列里删除,执行poll 后 元素减少一个
            System.out.println(queue.peek() + " 	  : " + queue);   // 从头部取出元素,执行peek 不移除元素
        }
        
    }

    关键字

    关键字是为了线程安全服务的,哪什么是线程安全呢?当多个线程访问某一个类(对象或方法)时,这个对象始终都能表现出正确的行为,那么这个类(对象或方法)就是线程安全的
    线程安全的两个特性:原子性可见性。synchronized 同步,原子性。volatile 可见性。wait,notify 负责多个线程之间的通信。

    synchronized

    synchronized 可以在任意对象及方法上加锁,而加锁的这段代码称为"互斥区"或"临界区",若一个线程想要执行synchronized修饰的代码块,首先要
    step1 尝试获得锁
    step2 如果拿到锁,执行synchronized代码体内容
    step3 如果拿不到锁,这个线程就会不断的尝试获得这把锁,直到拿到为止,而且是多个线程同时去竞争这把锁。
    注*(线程多了也就是会出现锁竞争的问题,多个线程执行的顺序是按照CPU分配的先后顺序而定的,而并非代码执行的先后顺序)

    synchronized 可以修饰方法,修饰代码块,这些都是对象锁。若和static一起使用,则升级为类锁。
    synchronized 锁是可以重入的,当一个线程得到了一个对象的锁后,再次请求此对象时是可以再次得到该对象的锁。锁重入的机制,也支持在父子类继承的场景。
    synchronized 同步异步,一个线程得到了一个对象的锁后,其他线程是可以执行非加锁的方法(异步)。但是不能执行其他加锁的方法(同步)。
    synchronized 锁异常,当一个线程执行的代码出现异常时,其所持有的锁会自动释放。

    /**
     * synchronized 关键字,可以修饰方法,也可以修饰代码块。建议采用后者,通过减小锁的粒度,以提高系统性能。
     * synchronized 关键字,如果以字符串作为锁,请注意String常量池的缓存功能和字符串改变后锁是否的情况。
     * synchronized 锁重入,当一个线程得到了一个对象的锁后,再次请求此对象时是可以再次得到该对象的锁。
     * synchronized 同异步,一个线程获得锁后,另外一个线程可以执行非synchronized修饰的方法,这是异步。若另外一个线程执行任何synchronized修饰的方法则需要等待,这是同步
     * synchronized 类锁,用static + synchronized 修饰则表示对整个类进行加锁
     */
    public class ITDragonSynchronized {
        
        private void thisLock () {  // 对象锁  
            synchronized (this) {  
                System.out.println("this 对象锁!");  
            }  
        }  
          
        private void classLock () {  // 类锁  
            synchronized (ITDragonSynchronized.class) {  
                System.out.println("class 类锁!");  
            }  
        }  
          
        private Object lock = new Object();  
        private void objectLock () {  // 任何对象锁  
            synchronized (lock) {  
                System.out.println("object 任何对象锁!");  
            }  
        }  
          
        private void stringLock () {  // 字符串锁,注意String常量池的缓存功能  
            synchronized ("string") { // 用 new String("string")  t4 和 t5 同时进入。用string t4完成后,t5在开始
                try {  
                    for(int i = 0; i < 3; i++) {  
                        System.out.println("thread : " + Thread.currentThread().getName() + " stringLock !");  
                        Thread.sleep(500);       
                    }  
                } catch (InterruptedException e) {  
                    e.printStackTrace();  
                }  
            }  
        }  
          
        private String strLock = "lock";  // 字符串锁改变  
        private void changeStrLock () {  
            synchronized (strLock) {  
                try {  
                    System.out.println("thread : " + Thread.currentThread().getName() + " changeLock start !");  
                    strLock = "changeLock";  
                    Thread.sleep(500);  
                    System.out.println("thread : " + Thread.currentThread().getName() + " changeLock end !");  
                } catch (InterruptedException e) {  
                    e.printStackTrace();  
                }  
            }  
        }  
        
        private synchronized void method1() {  // 锁重入
            System.out.println("^^^^^^^^^^^^^^^^^^^^ method1");  
            method2();  
        }  
        private synchronized void method2() {  
            System.out.println("-------------------- method2");  
            method3();  
        }  
        private synchronized void method3() {  
            System.out.println("******************** method3");  
        }  
        
        private synchronized void syncMethod() {  
            try {  
                System.out.println(Thread.currentThread().getName() + " synchronized method!");  
                Thread.sleep(2000);  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
        }  
          
        // 若次方法也加上了synchronized,就必须等待t1线程执行完后,t2才能调用,两个synchronized块之间具有互斥性,synchronized块获得的是一个对象锁,锁定的是整个对象
        private void asyncMethod() {  
            System.out.println(Thread.currentThread().getName() + " asynchronized method!");  
        } 
          
        // static + synchronized 修饰则表示类锁,打印的结果是thread1线程先执行完,然后在执行thread2线程。若没有被static修饰,则thread1和 thread2几乎同时执行,同时结束
        private synchronized void classLock(String args) {
            System.out.println(args + "start......");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(args + "end......");
        }
        
        public static void main(String[] args) throws Exception {  
            final ITDragonSynchronized itDragonSynchronized = new ITDragonSynchronized();  
            System.out.println("------------------------- synchronized 代码块加锁 -------------------------");
            Thread thread1 = new Thread(new Runnable() {  
                @Override  
                public void run() {  
                    itDragonSynchronized.thisLock();  
                }  
            });  
            Thread thread2 = new Thread(new Runnable() {  
                @Override  
                public void run() {  
                    itDragonSynchronized.classLock();  
                }  
            });  
            Thread thread3 = new Thread(new Runnable() {  
                @Override  
                public void run() {  
                    itDragonSynchronized.objectLock();  
                }  
            });  
            thread1.start();  
            thread2.start();  
            thread3.start();  
            Thread.sleep(2000);
            System.out.println("------------------------- synchronized 字符串加锁 -------------------------");
            // 如果字符串锁,用new String("string") t4,t5线程是可以获取锁的,如果直接使用"string" ,若锁不释放,t5线程一直处理等待中  
            Thread thread4 = new Thread(new Runnable() {  
                @Override  
                public void run() {  
                    itDragonSynchronized.stringLock();  
                }  
            }, "t4");  
            Thread thread5 = new Thread(new Runnable() {  
                @Override  
                public void run() {  
                    itDragonSynchronized.stringLock();  
                }  
            }, "t5");  
            thread4.start();  
            thread5.start();  
              
            Thread.sleep(3000);
            System.out.println("------------------------- synchronized 字符串变锁 -------------------------");
            // 字符串变了,锁也会改变,导致t7线程在t6线程未结束后变开始执行,但一个对象的属性变了,不影响这个对象的锁。  
            Thread thread6 = new Thread(new Runnable() {  
                @Override  
                public void run() {  
                    itDragonSynchronized.changeStrLock();  
                }  
            }, "t6");  
            Thread thread7 = new Thread(new Runnable() {  
                @Override  
                public void run() {  
                    itDragonSynchronized.changeStrLock();  
                }  
            }, "t7");  
            thread6.start();  
            thread7.start(); 
            
            Thread.sleep(2000);
            System.out.println("------------------------- synchronized 锁重入 -------------------------");
            Thread thread8 = new Thread(new Runnable() {  
                @Override  
                public void run() {  
                    itDragonSynchronized.method1();  
                }  
            }, "t8");  
            thread8.start(); 
            Thread thread9 = new Thread(new Runnable() {  
                @Override  
                public void run() {  
                    SunClass sunClass = new SunClass();  
                    sunClass.sunMethod();  
                }  
            }, "t9");  
            thread9.start(); 
            
            Thread.sleep(2000);
            System.out.println("------------------------- synchronized 同步异步 -------------------------");
            Thread thread10 = new Thread(new Runnable() {  
                @Override  
                public void run() {  
                    itDragonSynchronized.syncMethod();  
                }  
            }, "t10");  
            Thread thread11 = new Thread(new Runnable() {  
                @Override  
                public void run() {  
                    itDragonSynchronized.asyncMethod();  
                }  
            }, "t11");  
            thread10.start(); 
            thread11.start(); 
            
            Thread.sleep(2000);
            System.out.println("------------------------- synchronized 同步异步 -------------------------");
            ITDragonSynchronized classLock1 = new ITDragonSynchronized();
            ITDragonSynchronized classLock2 = new ITDragonSynchronized();
            Thread thread12 = new Thread(new Runnable() {
                @Override
                public void run() {
                    classLock1.classLock("classLock1");
                }
            });
            thread12.start();
            Thread thread13 = new Thread(new Runnable() {
                @Override
                public void run() {
                    classLock2.classLock("classLock2");
                }
            });
            thread13.start();
        }  
        
        // 有父子继承关系的类,如果都使用了synchronized 关键字,也是线程安全的。  
        static class FatherClass {  
            public synchronized void fatherMethod(){  
                System.out.println("#################### fatherMethod");  
            }  
        }  
          
        static class SunClass extends FatherClass{  
            public synchronized void sunMethod() {  
                System.out.println("@@@@@@@@@@@@@@@@@@@@ sunMethod");  
                this.fatherMethod();
            }  
        }  
    }

    volatile

    volatile 关键字虽然不具备synchronized关键字的原子性(同步)但其主要作用就是使变量在多个线程中可见。也就是可见性。
    用法很简单,直接用来修饰变量。因为其不具备原子性,可以用Atomic类代替。美中不足的是多个Atomic类也不具备原子性,所以还需要synchronized来修饰。
    volatile 关键字工作原理
    每个线程都有自己的工作内存,如果线程需要用到一个变量的时,会从主内存拷贝一份到自己的工作内存中。从而提高了效率。每次执行完线程后再将变量从工作内存同步回主内存中。
    这样就存在一个问题,变量在不同线程中可能存在不同的值。如果用volatile 关键字修饰变量,则会让线程的执行引擎直接从主内存中获取值。
    volatile关键字

    import java.util.ArrayList;
    import java.util.List;
    import java.util.concurrent.atomic.AtomicInteger;
    
    /**
     * volatile 关键字主要作用就是使变量在多个线程中可见。
     * volatile 关键字不具备原子性,但Atomic类是具备原子性和可见性。
     * 美中不足的是多个Atomic类不具备原子性,还是需要synchronized 关键字帮忙。
     */
    public class ITDragonVolatile{
        
        private volatile boolean flag = true;  
        private static volatile int count;   
        private static AtomicInteger atomicCount = new AtomicInteger(0); // 加 static 是为了避免每次实例化对象时初始值为零
        
        //  测试volatile 关键字的可见性
        private void volatileMethod() {
            System.out.println("thread start !");  
            while (flag) {  // 如果flag为true则一直处于阻塞中,
            }  
            System.out.println("thread end !");
        }
        
        //  验证volatile 关键字不具备原子性
        private int volatileCountMethod() {
            for (int i = 0; i < 10; i++) {
                // 第一个线程还未将count加到10的时候,就可能被另一个线程开始修改。可能会导致最后一次打印的值不是1000
                count++ ;   
            }  
            return count;
        }
        
        //  验证Atomic类具有原子性
        private int atomicCountMethod() {
            for (int i = 0; i < 10; i++) {  
                atomicCount.incrementAndGet();  
            }  
            // 若最后一次打印为1000则表示具备原子性,中间打印的信息可能是受println延迟影响。
            return atomicCount.get();// 若最后一次打印为1000则表示具备原子性
        }
        
        // 验证多个 Atomic类操作不具备原子性,加synchronized关键字修饰即可
        private synchronized int multiAtomicMethod(){
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            atomicCount.addAndGet(1);
            atomicCount.addAndGet(2);
            atomicCount.addAndGet(3);
            atomicCount.addAndGet(4); 
            return atomicCount.get(); //若具备原子性,则返回的结果一定都是10的倍数,需多次运行才能看到结果
        }
      
        /**
         * volatile 关键字可见性原因
         * 这里有两个线程  :一个是main的主线程,一个是thread的子线程
         * jdk线程工作流程    :为了提高效率,每个线程都有一个工作内存,将主内存的变量拷贝一份到工作内存中。线程的执行引擎就直接从工作内存中获取变量。
         * So 问题来了      :thread线程用的是自己的工作内存,主线程将变量修改后,thread线程不知道。这就是数据不可见的问题。
         * 解决方法     :变量用volatile 关键字修饰后,线程的执行引擎就直接从主内存中获取变量。
         * 
         */
        public static void main(String[] args) throws InterruptedException {  
    //      测试volatile 关键字的可见性
            /*ITDragonVolatile itDragonVolatile = new ITDragonVolatile();  
            Thread thread = new Thread(itDragonVolatile);
            thread.start();
            Thread.sleep(1000);  // 等线程启动了,再设置值
            itDragonVolatile.setFlag(false);  
            System.out.println("flag : " + itDragonVolatile.isFlag());*/  
            
    //      验证volatile 关键字不具备原子性 和 Atomic类具有原子性
            final ITDragonVolatile itDragonVolatile = new ITDragonVolatile();
            List<Thread> threads = new ArrayList<Thread>();
            for (int i = 0; i < 100; i++) {
                threads.add(new Thread(new Runnable() {
                    @Override
                    public void run() {
                        // 中间打印的信息可能是受println延迟影响,请看最后一次打印的结果
                        System.out.println(itDragonVolatile.multiAtomicMethod());
                    }
                }));
            }
            for(Thread thread : threads){
                thread.start();
            }
        }  
          
        public boolean isFlag() {  
            return flag;  
        }  
      
        public void setFlag(boolean flag) {  
            this.flag = flag;  
        }  
    
    }

    wait,notify

    使用 wait/ notify 方法实现线程间的通信,模拟BlockingQueue队列。有两点需要注意:
    1)wait 和 notify 必须要配合 synchronized 关键字使用
    2)wait方法是释放锁的, notify方法不释放锁。
    线程通信概念:线程是操作系统中独立的个体,但这些个体如果不经过特殊的处理,就不能成为一个整体,线程之间的通信就成为整体的必用方法之一。

    import java.util.LinkedList;  
    import java.util.concurrent.atomic.AtomicInteger;
    
    /**
     * synchronized 可以在任意对象及方法上加锁,而加锁的这段代码称为"互斥区"或"临界区",一般给代码块加锁,通过减小锁的粒度从而提高性能。
     * Atomic* 是为了弥补volatile关键字不具备原子性的问题。虽然一个Atomic*对象是具备原子性的,但不能确保多个Atomic*对象也具备原子性。
     * volatile 关键字不具备synchronized关键字的原子性其主要作用就是使变量在多个线程中可见。
     * wait / notify
     * wait() 使线程阻塞运行,notify() 随机唤醒等待队列中等待同一共享资源的一个线程继续运行,notifyAll() 唤醒所有等待队列中等待同一共享资源的线程继续运行。
     * 1)wait 和 notify 必须要配合 synchronized 关键字使用
     * 2)wait方法是释放锁的, notify方法不释放锁
     */
    public class ITDragonMyQueue {
        
        //1 需要一个承装元素的集合   
        private LinkedList<Object> list = new LinkedList<Object>();  
        //2 需要一个计数器 AtomicInteger (保证原子性和可见性)
        private AtomicInteger count = new AtomicInteger(0);  
        //3 需要制定上限和下限  
        private final Integer minSize = 0;  
        private final Integer maxSize ;  
          
        //4 构造方法  
        public ITDragonMyQueue(Integer size){  
            this.maxSize = size;  
        }  
          
        //5 初始化一个对象 用于加锁  
        private final Object lock = new Object();  
          
        //put(anObject): 把anObject加到BlockingQueue里,如果BlockQueue没有空间,则调用此方法的线程被阻断,直到BlockingQueue里面有空间再继续.  
        public void put(Object obj){  
            synchronized (lock) {  
                while(count.get() == this.maxSize){  
                    try {  
                        lock.wait();        // 当Queue没有空间时,线程被阻塞 ,这里为了区分,命名为wait1
                    } catch (InterruptedException e) {  
                        e.printStackTrace();  
                    }  
                }  
                list.add(obj);              //1 加入元素  
                count.incrementAndGet();    //2 计数器累加  
                lock.notify();              //3 新增元素后,通知另外一个线程wait2,队列多了一个元素,可以做移除操作了。 
                System.out.println("新加入的元素为: " + obj);  
            }  
        }  
          
        //take: 取走BlockingQueue里排在首位的对象,若BlockingQueue为空,阻断进入等待状态直到BlockingQueue有新的数据被加入.  
        public Object take(){  
            Object ret = null;  
            synchronized (lock) {  
                while(count.get() == this.minSize){  
                    try {  
                        lock.wait();        // 当Queue没有值时,线程被阻塞 ,这里为了区分,命名为wait2
                    } catch (InterruptedException e) {  
                        e.printStackTrace();  
                    }  
                }  
                ret = list.removeFirst();   //1 做移除元素操作  
                count.decrementAndGet();    //2 计数器递减  
                lock.notify();              //3 移除元素后,唤醒另外一个线程wait1,队列少元素了,可以再添加操作了  
            }  
            return ret;  
        }  
          
        public int getSize(){  
            return this.count.get();  
        }  
          
        public static void main(String[] args) throws Exception{  
            final ITDragonMyQueue queue = new ITDragonMyQueue(5);  
            queue.put("a");  
            queue.put("b");  
            queue.put("c");  
            queue.put("d");  
            queue.put("e");  
            System.out.println("当前容器的长度: " + queue.getSize());  
            Thread thread1 = new Thread(new Runnable() {  
                @Override  
                public void run() {  
                    queue.put("f");  
                    queue.put("g");  
                }  
            },"thread1");  
            Thread thread2 = new Thread(new Runnable() {  
                @Override  
                public void run() {  
                    System.out.println("移除的元素为:" + queue.take());  // 移除一个元素后再进一个,而并非同时移除两个,进入两个元素。
                    System.out.println("移除的元素为:" + queue.take());  
                }  
            },"thread2");  
            thread1.start();  
            Thread.sleep(2000);
            thread2.start();  
        }  
    }

    死锁

    死锁是一个很糟糕的情况,锁迟迟不能解开,其他线程只能一直处于等待阻塞状态。比如线程A拥有锁一,却还想要锁二。线程B拥有锁二,却还想要锁一。两个线程互不相让,两个线程将永远等待。
    排查:
    第一步:控制台输入jps用于获得当前JVM进程的pid
    第二步:jstack pid 用于打印堆栈信息
    第三步:解读,"Thread-1" 是线程的名字,prio 是线程的优先级,tid 是线程id, nid 是本地线程id, waiting to lock 等待去获取的锁,locked 自己拥有的锁。

    "Thread-1" #11 prio=5 os_prio=0 tid=0x0000000055ff1800 nid=0x1bd4 waiting for monitor entry [0x0000000056e2e000]
       java.lang.Thread.State: BLOCKED (on object monitor)
            at com.itdragon.keyword.ITDragonDeadLock.rightLeft(ITDragonDeadLock.java:37)
            - waiting to lock <0x00000000ecfdf9d0> (a java.lang.Object)
            - locked <0x00000000ecfdf9e0> (a java.lang.Object)
            at com.itdragon.keyword.ITDragonDeadLock$2.run(ITDragonDeadLock.java:54)
            at java.lang.Thread.run(Thread.java:748)
    /**
     * 死锁: 线程A拥有锁一,却还想要锁二。线程B拥有锁二,却还想要锁一。两个线程互不相让,两个线程将永远等待。
     * 避免: 在设计阶段,了解锁的先后顺序,减少锁的交互数量。
     * 排查: 
     * 第一步:控制台输入 jps 用于获得当前JVM进程的pid
     * 第二步:jstack pid 用于打印堆栈信息 
     * "Thread-1" #11 prio=5 os_prio=0 tid=0x0000000055ff1800 nid=0x1bd4 waiting for monitor entry [0x0000000056e2e000]
     * - waiting to lock <0x00000000ecfdf9d0> - locked <0x00000000ecfdf9e0> 
     * "Thread-0" #10 prio=5 os_prio=0 tid=0x0000000055ff0800 nid=0x1b14 waiting for monitor entry [0x0000000056c7f000]
     * - waiting to lock <0x00000000ecfdf9e0> - locked <0x00000000ecfdf9d0> 
     * 可以看出,两个线程持有的锁都是对方想要得到的锁(得不到的永远在骚动),而且最后一行也打印了 Found 1 deadlock.
     */
    public class ITDragonDeadLock {
        
        private final Object left = new Object();
        private final Object right = new Object();
        
        public void leftRight(){
            synchronized (left) {
                try {
                    Thread.sleep(2000); // 模拟持有锁的过程
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (right) {
                    System.out.println("leftRight end!");
                }
            }
        }
        
        public void rightLeft(){
            synchronized (right) {
                try {
                    Thread.sleep(2000); // 模拟持有锁的过程
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (left) {
                    System.out.println("rightLeft end!");
                }
            }
        }
        
        public static void main(String[] args) {
            ITDragonDeadLock itDragonDeadLock = new ITDragonDeadLock();
            Thread thread1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    itDragonDeadLock.leftRight();
                }
            });
            thread1.start();
            Thread thread2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    itDragonDeadLock.rightLeft();
                }
            });
            thread2.start();
        }
    
    }

    多线程案例

    若有Thread1、Thread2、Thread3、Thread4四条线程分别统计C、D、E、F四个盘的大小,所有线程都统计完毕交给Thread5线程去做汇总,应当如何实现

    import java.io.File;
    import java.util.ArrayList;
    import java.util.Arrays;
    import java.util.concurrent.Callable;
    import java.util.concurrent.CompletionService;
    import java.util.concurrent.ExecutorCompletionService;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    /**
     * 若有Thread1、Thread2、Thread3、Thread4四条线程分别统计C、D、E、F四个盘的大小,所有线程都统计完毕交给Thread5线程去做汇总,应当如何实现?
     * 思考:汇总,说明要把四个线程的结果返回给第五个线程,若要线程有返回值,推荐使用callable。Thread和Runnable都没返回值
     */
    public class ITDragonThreads {  
        
        public static void main(String[] args) throws Exception {  
            // 无缓冲无界线程池
            ExecutorService executor = Executors.newFixedThreadPool(8); 
            // 相对ExecutorService,CompletionService可以更精确和简便地完成异步任务的执行
            CompletionService<Long> completion = new ExecutorCompletionService<Long>(executor);  
            CountWorker countWorker = null;  
            for (int i = 0; i < 4; i++) { // 四个线程负责统计
                countWorker = new CountWorker(i+1);  
                completion.submit(countWorker);  
            }  
            // 关闭线程池
            executor.shutdown();  
            // 主线程相当于第五个线程,用于汇总数据
            long total = 0;  
            for (int i = 0; i < 4; i++) { 
                total += completion.take().get(); 
            }  
            System.out.println(total / 1024 / 1024 / 1024 +"G");  
        }  
    }  
      
    class CountWorker implements Callable<Long>{  
        private Integer type;  
        public CountWorker() {
        }
        public CountWorker(Integer type) {
            this.type = type;
        }
    
        @Override  
        public Long call() throws Exception {  
            ArrayList<String> paths = new ArrayList<>(Arrays.asList("c:", "d:", "e:", "f:"));
            return countDiskSpace(paths.get(type - 1));  
        }  
        
        // 统计磁盘大小
        private Long countDiskSpace (String path) {  
            File file = new File(path);  
            long totalSpace = file.getTotalSpace();  
            System.out.println(path + " 总空间大小 : " + totalSpace / 1024 / 1024 / 1024 + "G");  
            return totalSpace;
        }  
    }  

    查考面试题

    1 常见创建线程的方式和其优缺点
    (1)继承Thread类 (2)实现Runnable接口
    优缺点:实现一个接口比继承一个类要灵活,减少程序之间的耦合度。缺点就是代码多了一点。

    2 start()方法和run()方法的区别
    start方法可以启动线程,而run方法只是thread的一个普通方法调用。

    3 多线程的作用
    (1)发挥多核CPU的优势,提高CPU的利用率(2)防止阻塞,提高效率

    4 什么是线程安全
    当多个线程访问某一个类(对象或方法)时,这个对象始终都能表现出正确的行为,那么这个类(对象或方法)就是线程安全的。

    5 线程安全级别
    (1)不可变(2)绝对线程安全(3)相对线程安全(4)线程非安全

    6 如何在两个线程之间共享数据
    线程之间数据共享,其实可以理解为线程之间的通信,可以用wait/notify/notifyAll 进行等待和唤醒。

    7 用线程池的好处
    避免频繁地创建和销毁线程,达到线程对象的重用,提高性能,减轻服务器压力。使用线程池还可以根据项目灵活地控制并发的数目。

    8 sleep方法和wait方法有什么区别
    sleep方法和wait方法都可以用来放弃CPU一定的时间,sleep是thread的方法,不会释放锁。wait是object的方法,会释放锁。

    总结

    1 线程池核心参数有 初始核心线程数,线程池运行最大线程数,空闲线程存活时间,时间单位,任务队列。
    2 队列是一种数据结构,主要有两类 阻塞队列BlockingQueue,和非阻塞高性能队列ConcurrentLinkedQueue。
    3 线程安全的两个特性,原子性和可见性。synchronized 关键字具备原子性。volatile 关键字具备可见性。
    4 单个Atomic类具备原子性和可见性,多个Atomic类不具备原子性,需要synchronized 关键字修饰。
    5 两个线程持有的锁都是对方想要得到的锁时容易出现死锁的情况,从设计上尽量减少锁的交互。

    本章到这里就结束了,涉及的知识点比较多,请参考流程图来学习。如有什么问题可以指出。喜欢的朋友可以点个"推荐"

  • 相关阅读:
    Spring中的Bean的配置形式
    使用外部属性文件配置Bean以及Bean的生命周期方法
    运行时找到main方法所在的类
    获取SpringMVC中所有RequestMapping映射URL信息
    RequestBody只能读取一次的问题
    接口标记为@ResponseBody却不进入ResponseBodyAdvice
    springboot打成jar包后无法解压
    Springboot打包执行源码解析
    关于base64的一个小细节
    Liquibase使用入门
  • 原文地址:https://www.cnblogs.com/shoshana-kong/p/10742459.html
Copyright © 2011-2022 走看看