多线程四大经典案例及java多线程的实现

Posted bug 郭

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多线程四大经典案例及java多线程的实现相关的知识,希望对你有一定的参考价值。

目录

本节要点

  • 了解一些线程安全的案例
  • 学习线程安全的设计模型
  • 掌握单例模式,阻塞队列,生产在消费者模型

单例模式

我们知道多线程编程,因为线程的随机调度会出现很多线程安全问题! 而我们的java有些大佬针对一些多线程安全问题的应用场景,设计了一些对应的解决方法和案例,就是解决这些问题的一些套路,被称为设计模式,供我们学习和使用!

单例模式是校招最常考的一个设计模式之一!!!

什么是单例模式呢?

单例模式能保证某个类在程序中只存在唯一一份实例, 而不会创建出多个实例.
这一点在很多场景上都需要. 比如 JDBC 中的 DataSource 实例就只需要一个

单例模式的具体实现方法又分为饿汉懒汉两种!
而这里所说的饿并不是贬义词!
饿汉指的是在创建一个类的时候就将实例创建好!比较急!
懒汉指的是在需要用到实例的时候再去创建实例!比较懒!

饿汉模式

饿汉模式联系实际生活中例子:
就是一个人性子比较急,也许一件事情的期限还有好久,而他却把事情早早干完!

因为我们单例模式只能有一个实例
那如何去保证一个实例呢?
我们会马上想到类中用static修饰的类属性,它只有一份!保证了单例模式的基本条件!

显然生活中这样的人很优秀,但是我们的计算机如果这样却不太好!
因为cpu和内存的空间有限,如果还不需要用到该实例,却创建了实例,那不就增加了内存开销,显然不科学.但事实问题也不大!

class Singleton
    //饿汉模式, static 创建类时,就创建好了类属性的实例!
    //private 这里的instance实例只有一份!!!
    private static Singleton instance = new Singleton();
    //私有的构造方法!保证该实例不能再创建
    private Singleton()
    
    //提供一个方法,外界可以获取到该实例!
    public static Singleton getInstance() 
        return instance;
    

我们可以看到这里饿汉模式,当多个线程并发时,并没有出现线程不安全问题,因为这里的设计模式只是针对了读操作!!! 而单例模式的更改操作,需要看懒汉模式!

懒汉模式

联系实际中的例子就是.就是这个人比较拖延,有些事情不得不做的时候,他才会去做完!

//懒汉模式(线程不安全版本)
class Singleton1
    //懒汉模式, static 创建类时,并没有创建实例!
    //private 保证这里的instance实例只有一份!!!
    private static Singleton1 instance = null;
    //私有的构造方法!保证该实例不能再创建
    private Singleton1()
    
    //提供一个方法,外界可以获取到该实例!
    public static Singleton1 getInstance() 
        if(instance==null)//需要时再创建实例!
            instance = new Singleton1();
        
        return instance;
    

我们分析一下上述代码,该模式,对singleton进行了修改,而我们知道多线程的修改可能会出现线程不安全问题!
当我们多个线程同时对该变量进行访问时!

我们将该代码的情况分成两种,一种是初始化前要进行读写操作,初始化后只需要进行读操作!

  • instance未初始化化前
    多个线程同时进入getInstance方法!那就会创建很多次instance实例!
    联系之前的变量更改内存cpu的操作:

    显然很多线程进行了无效操作!!!也会触发内存不可见问题!!!
  • instance初始化后,进行的读操作,就像上面的饿汉模式一样,并没有线程安全问题!

我们下面进行多次优化

//优化1
class Singleton2
    //懒汉模式, static 创建类时,并没有创建实例!
    //private 保证这里的instance实例只有一份!!!
    private static Singleton2 instance = null;
    //私有的构造方法!保证该实例不能再创建
    private Singleton2()
    
    //提供一个方法,外界可以获取到该实例!
    public static Singleton2 getInstance() 
        synchronized (Singleton.class) //对读写操作进行加锁!
            if(instance==null)//需要时再创建实例!
                instance = new Singleton2();
            
            return instance;
        
    

我们将Singleton类对象加锁后,显然避免了刚刚的一些线程安全问题!但是出现了新的问题!

  • instance初始化前
    在初始化前,我们很好的将读写操作进行了原子封装,并不会造成线程不安全问题!
  • instance初始化后
    然而初始化后的每次读操作却并不好,当我们多个线程进行多操作时,很多线程就会造成线程阻塞,代码的运行效率极具下降!

我们如何保证,线程安全的情况下又保证读操作不会进行加锁,锁竞争呢?

我们可以间代码的两种情况分别处理!

//优化二
class Singleton2
    //懒汉模式, static 创建类时,并没有创建实例!
    //private 保证这里的instance实例只有一份!!!
    private static Singleton2 instance = null;
    //私有的构造方法!保证该实例不能再创建
    private Singleton2()
    
    //提供一个方法,外界可以获取到该实例!
    public static Singleton2 getInstance() 
        if(instance==null)//如果未初始化就进行加锁操作!
            synchronized (Singleton.class) //对读写操作进行加锁!
                if(instance==null)//需要时再创建实例!
                    instance = new Singleton2();
                
            
        
        //已经初始化后直接读!!!
        return instance;
    

我们看到这里可能会有疑惑,咋为啥要套两个if啊,把里面的if删除不行吗!!!
我们来看删除后的效果:

//删除里层if
class Singleton2
    //懒汉模式, static 创建类时,并没有创建实例!
    //private 保证这里的instance实例只有一份!!!
    private static Singleton2 instance = null;
    //私有的构造方法!保证该实例不能再创建
    private Singleton2()
    
    //提供一个方法,外界可以获取到该实例!
    public static Singleton2 getInstance() 
        if(instance==null)//如果未初始化就进行加锁操作!
            synchronized (Singleton.class) //对读写操作进行加锁!
                    instance = new Singleton2();
            
        
        //已经初始化后直接读!!!
        return instance;
    

在删除里层的if后:
我们发现当有多个线程进行了第一个if判断后,进入的线程中有一个线程锁竞争拿到了锁!而其他线程就在这阻塞等待,直到该锁释放后,又有线程拿到了该锁,而这样也就多次创建了instance实例,显然不可!!!

所以这里的两个if都有自己的作用缺一不可!
第一个if:
判断是否要进行加锁初始化
第二个if:
判断该线程实例是否已经创建!

//最终优化版
class Singleton2
    //懒汉模式, static 创建类时,并没有创建实例!
    //private 保证这里的instance实例只有一份!!!
    //volatile 保证内存可见!!!避免编译器优化!!!
    private static volatile Singleton2 instance = null;
    //私有的构造方法!保证该实例不能再创建
    private Singleton2()
    
    //提供一个方法,外界可以获取到该实例!
    public static Singleton2 getInstance() 
        if(instance==null)//如果未初始化就进行加锁操作!
            synchronized (Singleton.class) //对读写操作进行加锁!
	            if(instance==null)
	                instance = new Singleton2();
	            
            
        
        //已经初始化后直接读!!!
        return instance;
    

而我们又发现了一个问题,我们的编译器是会对代码进行优化操作的!如果很多线程对第一个if进行判断,那cpu老是在内存中拿instance的值,就很慢,编译器就不开心了,它就优化直接将该值存在寄存器中,而此操作是否危险,如果有一个线程将该实例创建!那就会导致线程安全问题! 而volatile关键字保证了instanse内存可见性!!!

总结懒汉模式

  • if 外层保证未初始化前加锁,创建实例. 里层if保证实例创建唯一一次
  • synchronized加锁,保证读写原子性
  • volatile保证内存可见性,避免编译器优化

阻塞队列

什么是阻塞队列?
顾名思义是队列的一种!
也符合先进先出的特点!
阻塞队列特点:

当队列为空时,读操作阻塞
当队列为满时,写操作阻塞

阻塞队列一般用在多线程中!并且有很多的应用场景!
最典型的一个应用场景就是生产者消费者模型

生产者消费者模型

我们知道生产者和消费者有着供需关系!
而开发中很多场景都会有这样的供需关系!
比如有两个服务器AB
A是入口服务器直接接受用户的网络请求
B应用服务器对A进行数据提供

在通常情况下如果一个网站的访问量不大,那么AB服务器都能正常使用!
而我们知道,很多网站当很多用户进行同时访问时就可能挂!
我们知道,A入口服务器和B引用服务器此时耦合度较高!
当一个挂了,那么另一个服务器也会出现问题!

而当我们使用生产者消费者模型就很好的解决了上述高度耦合问题!我们在他们中间加入一个阻塞队列即可!


当增加就绪队列后,我们就不用担心AB的耦合!
并且AB进行更改都不会影响到对方! 甚至将改变服务器,对方也无法察觉!
而阻塞队列还保证了,服务器的访问速度,不管用户量多大! 这些数据都会先传入阻塞队列,而阻塞队列如果满,或者空,都会线程阻塞! 也就不存在服务器爆了的问题!!!
也就是起到了削峰填谷的作用!不管访问量一时间多大!就绪队列都可以保证服务器的速度!

标准库中的就绪队列

我们java中提供了一组就绪队列供我们使用!

BlockingQueue


BlockingQueue 是一个接口. 真正实现的类是 LinkedBlockingQueue.
put 方法用于阻塞式的入队列,
take 用于阻塞式的出队列.
BlockingQueue 也有 offer, poll, peek 等方法, 但是这些方法不带有阻塞特性.

//生产着消费者模型
public class Test2 
    public static void main(String[] args) throws InterruptedException 
        //创建一个阻塞队列
        BlockingQueue<Integer> blockingQueue = new LinkedBlockingQueue<Integer>();
        Thread customer = new Thread(() -> //消费者
            while (true) 
                try 
                    int value = blockingQueue.take();
                    System.out.println("消费元素: " + value);
                 catch (InterruptedException e) 
                    e.printStackTrace();
                
            
        , "消费者");
        customer.start();
        Thread producer = new Thread(() -> //生产者
            Random random = new Random();
            while (true) 
                try 
                    int num = random.nextInt(1000);
                    System.out.println("生产元素: " + num);
                    blockingQueue.put(num);
                    Thread.sleep(1000);
                 catch (InterruptedException e) 
                    e.printStackTrace();
                
            
        , "生产者");
        producer.start();
        customer.join();
        producer.join();
    

阻塞队列实现

虽然java标准库中提供了阻塞队列,但是我们想自己实现一个阻塞队列!

我们就用循环队列实现吧,使用数组!

//循环队列
class MyblockingQueue
    //阻塞队列
    private int[] data = new int[100];
    //队头
    private int start = 0;
    //队尾
    private int tail = 0;
    //元素个数, 用于判断队列满
    private int size = 0;
    public void put(int x)
        //入队操作
        if(size==data.length)
            //队列满
            return;
        
        data[tail] = x;
        tail++;//入队
        if(tail==data.length)
            //判断是否需要循环回
            tail=0;
        
        size++; //入队成功加1
    
    public Integer take()
        //出队并且获取队头元素
        if(tail==start)
            //队列为空!
            return null;
        
        int ret = data[start]; //获取队头元素
        start++; //出队
        if(start==data.length)
            //判断是否要循环回来
            start = 0;
        
       // start = start % data.length;//不建议可读性不搞,效率也低
        size--;//元素个数减一
        return ret;
    




我们已经创建好了一个循环队列,目前达不到阻塞的效果!
而且当多线程并发时有很多线程不安全问题!
而我们知道想要阻塞,那不得加锁,不然哪来的阻塞!

//阻塞队列
class MyblockingQueue
    //阻塞队列
    private int[] data = new int[100];
    //队头
    private int start = 0;
    //队尾
    private int tail = 0;
    //元素个数, 用于判断队列满
    private int size = 0;
    //锁对象
    Object locker = new Object();

    public void put(int x)
       synchronized (locker)//对该操作加锁
           //入队操作
           if(size==data.length)
               //队列满 阻塞等待!!!直到put操作后notify才会继续执行
               try 
                   locker.wait();
                catch (InterruptedException e) 
                   e.printStackTrace();
               
           
           data[tail] = x;
           tail++;//入队
           if(tail==data.length)
               //判断是否需要循环回
               tail=0;
           
           size++; //入队成功加1
           //入队成功后通知take 如果take阻塞
           locker.notify();//这个操作线程阻塞并没有副作用!
       
    
    public Integer take()
        //出队并且获取队头元素
        synchronized (locker)
            if(size==0)
                //队列为空!阻塞等待 知道队列有元素put就会继续执行该线程
                try 
                    locker.wait();
                 catch (InterruptedException e) 
                    e.printStackTrace();
                
            
            int ret = data[start]; //获取队头元素
            start++; //出队
            if(start==data.length)
                //判断是否要循环回来
                start = 0;
            
            // start = start % data.length;//不建议可读性不搞,效率也低
            size--;//元素个数减一
            locker.notify();//通知 put 如果put阻塞!
            return ret;
        
    

//测试代码
public class Test3 
    public static void main(String[] args) 
            MyblockingQueue queue = new MyblockingQueue();
            Thread customer = new Thread(()->
                int i = 0;
                while (true)
                    System.out.println("消费了"+queue.take());
                
            );

                    Thread producer = new Thread(()->
                        Random random = new Random();
                        while (true)
                            int x = random.nextInt(100);
                            System.out.println("生产了"+x);
                            queue.put(x);
                            try 
                                Thread.sleep(100);
                             catch (InterruptedException e) 
                                e.printStackTrace();
                            
                        
                    );
                    customer.start();
                    producer.start();
    


可以看到通过waitnotify的配和,我就实现了阻塞队列!!!

定时器

定时器是什么

定时器也是软件开发中的一个重要组件. 类似于一个 “闹钟”. 达到一个设定的时间之后, 就执行某个指定好的代码.

也就是说定时器有像joinsleep等待功能,不过他们是基于系统内部的定时器,
而我们要学习的是在java给我们提供的定时器包装类,用于到了指定时间就执行代码!
并且定时器在我们日常开发中十分常用!

java给我们提供了专门一个定时器的封装类Timerjava.util包下!

Timer定时器

Timer类下有一个schedule方法,用于安排指定的任务和执行时间!
也就达到了定时的效果,如果时间到了,就会执行task!

  • schedule 包含两个参数.
  • 第一个参数指定即将要执行的任务代码,
  • 第二个参数指定多长时间之后执行 (单位为毫秒).
//实例
import java.util.Timer;
import java.util.TimerTask;
public class Demo1 
    public static void main(String[] args) 
        //在java.util.Timer包下
        Timer timer = new Timer();
        //timer.schedule()方法传入需要执行的任务和定时时间
        //Timer内部有专门的线程负责任务的注册,所以不需要start
        timer.schedule(new TimerTask() 
            @Override
            public void run() 
                System.out.println("hello Timer!");
            
        ,3000);
        //main线程
        System.out.println("hello main!");
    


我们可以看到我们只需要创建一个Timer对象,然后调用schedule返回,传入你要执行的任务,和定时时间便可完成!

定时器实现

我们居然知道java中定时器的使用,那如何自己实现一个定时器呢!

我们可以通过Timer中的源码,然后进行操作!

Timer内部需要什么东西呢!

我们想想Timer的功能!
可以定时执行任务!(线程)
可以知道任务啥时候执行(时间)
可以将多个任务组织起来对比时间执行

  • 描述任务
    也就是schedule方法中传入的TimerTake
    创建一个专门表示定时器中的任务
class MyTask
    //任务具体要干啥
    private Runnable runnable;
    //任务执行时间,时间戳
    private long time;
    ///delay是一个时间间隔
    public MyTask(Runnable runnable,long delay)
            this.runnable = runnable;
            time = System.currentTimeMillis()+delay;
    
    public void run() //描述任务!
        runnable.run();
    

  • 组织任务
    组织任务就是将上述的任务组织起来!
    我们知道我们的任务需要在多线程的环境下执行,所以就需要有线程安全,阻塞功能的数据结构!并且我们的任务到了时间就需要执行,也就是需要时刻对任务排序!
    所以我们采用PriorityBlockingQueue优先级队列!阻塞!

    但是这里我们使用了优先级队列,我们需要指定比较规则,就是让MyTask实现Comparable接口,重写 compareTo方法,指定升序排序,就是小根堆!
    1. 多线程的创建方式

    (1)、继承 Thread类:但Thread本质上也是实现了Runnable 接口的一个实例,它代表一个线程的实例,并且,启动线程的唯一方法就是通过 Thread 类的 start()实例方法。start()方法是一个 native 方法,它将启动一个新线程,并执行run()方法。这种方式实现多线程很简单,通过自己的类直接extend Thread,并复写run()方法,就可以启动新线程并执行自己定义的run()方法。例如:继承Thread类实现多线程,并在合适的地方启动线程

     

    1.public class MyThread extends Thread {   
    2.  public void run() {   
    3.   System.out.println("MyThread.run()");   
    4.  }   
    5.}  
     
    6.MyThread myThread1 = new MyThread();   
    7.MyThread myThread2 = new MyThread();   
    8.myThread1.start();   
    9.myThread2.start();

    (2)、实现Runnable接口的方式实现多线程,并且实例化Thread,传入自己的Thread实例,调用run( )方法 

    1.public class MyThread implements Runnable {   
    2.  public void run() {   
    3.   System.out.println("MyThread.run()");   
    4.  }   
    5.} 
    6.MyThread myThread = new MyThread();   
    7.Thread thread = new Thread(myThread);   
    8.thread.start(); 

    (3)、使用ExecutorService、Callable、Future实现有返回结果的多线程:ExecutorService、Callable、Future这 个 对 象 实际 上 都是属 于 Executor 框 架中 的 功 能 类。 想 要详细 了 解 Executor 框架 的 可 以 访问http://www.javaeye.com/topic/366591 ,这里面对该框架做了很详细的解释。返回结果的线程是在JDK1.5中引入的新特征,确实很实用,有了这种特征我就不需要再为了得到返回值而大费周折了,而且即便实现了也可能漏洞百出。可返回值的任务必须实现Callable接口,类似的,无返回值的任务必须Runnable接口。执行Callable任务后,可以获取一个 Future 的对象,在该对象上调用 get 就可以获取到 Callable 任务返回的 Object 了,再结合线程池接口ExecutorService就可以实现传说中有返回结果的多线程了。下面提供了一个完整的有返回结果的多线程测试例子,在JDK1.5下验证过没问题可以直接使用。代码如下:

    1.import java.util.concurrent.*;   
    2.import java.util.Date;   
    3.import java.util.List;   
    4.import java.util.ArrayList;   
    56./**  
    7.* 有返回值的线程  
    8.*/   
    9.@SuppressWarnings("unchecked")   
    10.public class Test {   
    11.public static void main(String[] args) throws ExecutionException,   
    12.    InterruptedException {   
    13.   System.out.println("----程序开始运行----");   
    14.   Date date1 = new Date();   
    1516.   int taskSize = 5;   
    17.   // 创建一个线程池   
    18.   ExecutorService pool = Executors.newFixedThreadPool(taskSize);   
    19.   // 创建多个有返回值的任务   
    20.   List<Future> list = new ArrayList<Future>();   
    21.   for (int i = 0; i < taskSize; i++) {   
    22.    Callable c = new MyCallable(i + " ");   
    23.    // 执行任务并获取 Future 对象   
    24.    Future f = pool.submit(c);   
    25.    // System.out.println(">>>" + f.get().toString());   
    26.    list.add(f);   
    27.   }   
    28.   // 关闭线程池   
    29.   pool.shutdown();   
    3031.   // 获取所有并发任务的运行结果   
    32.   for (Future f : list) {   
    33.    // 从 Future 对象上获取任务的返回值,并输出到控制台   
    34.    System.out.println(">>>" + f.get().toString());   
    35.   }   
    3637.   Date date2 = new Date();   
    38.   System.out.println("----程序结束运行----,程序运行时间【"   
    39.     + (date2.getTime() - date1.getTime()) + "毫秒】");   
    40.}   
    41.}   
    4243.class MyCallable implements Callable<Object> {   
    44.private String taskNum;   
    4546.MyCallable(String taskNum) {   
    47.   this.taskNum = taskNum;   
    48.}   
    4950.public Object call() throws Exception {   
    51.   System.out.println(">>>" + taskNum + "任务启动");   
    52.   Date dateTmp1 = new Date();   
    53.   Thread.sleep(1000);   
    54.   Date dateTmp2 = new Date();   
    55.   long time = dateTmp2.getTime() - dateTmp1.getTime();   
    56.   System.out.println(">>>" + taskNum + "任务终止");   
    57.   return taskNum + "任务返回运行结果,当前任务时间【" + time + "毫秒】";   58.}   
    59.} 

    2. 在 java 中 wait 和 sleep 方法的不同? 

    最大的不同是在等待时wait会释放锁,而sleep一直持有锁。wait通常被用于线程间交互,sleep通常被用于暂停执行。

    3. synchronized 和 volatile 关键字的作用
    一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
    1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是
    立即可见的。
    2)禁止进行指令重排序。
    volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;
    synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
    1.volatile仅能使用在变量级别;
    synchronized则可以使用在变量、方法、和类级别的
    2.volatile仅能实现变量的修改可见性,并不能保证原子性;
    synchronized则可以保证变量的修改可见性和原子性
    3.volatile不会造成线程的阻塞;
    synchronized可能会造成线程的阻塞。
    4.volatile标记的变量不会被编译器优化;
    synchronized标记的变量可以被编译器优化

    4. 分析线程并发访问代码解释原因 

    1. public class Counter { 
    2.  private volatile int count = 0; 
    3.  public void inc(){ 
    4.   try { 
    5.    Thread.sleep(3); 
    6.   } catch (InterruptedException e) { 
    7.    e.printStackTrace(); 
    8.   } 
    9.   count++; 
    10.  } 
    11.  @Override 
    12.  public String toString() { 
    13.   return "[count=" + count + "]"; 
    14.  } 
    15. }  
    16. //---------------------------------华丽的分割线----------------------------- 
    17. public class VolatileTest { 
    18.  public static void main(String[] args) { 
    19.   final Counter counter = new Counter(); 
    20.   for(int i=0;i<1000;i++){ 
    21.    new Thread(new Runnable() { 
    22.     @Override 
    23.     public void run() { 
    24.      counter.inc(); 
    25.     } 
    26.    }).start(); 
    27.   } 
    28.   System.out.println(counter); 
    29.  } 
    30. } 

    上面的代码执行完后输出的结果确定为1000吗?
    答案是不一定,或者不等于1000。这是为什么吗?
    在 java 的内存模型中每一个线程运行时都有一个线程栈,线程栈保存了线程运行时候变量值信息。当线程访问某一个对象时候值的时候,首先通过对象的引用找到对应在堆内存的变量的值,然后把堆内存变量的具体值load到线程本地内存中,建立一个变量副本,之后线程就不再和对象在堆内存变量值有任何关系,而是直接修改副本变量的值,在修改完之后的某一个时刻(线程退出之前),自动把线程变量副本的值回写到对象在堆中变量。这样在堆中的对象的值就产生变化了。
    也就是说上面主函数中开启了 1000 个子线程,每个线程都有一个变量副本,每个线程修改变量只是临时修改了自己的副本,当线程结束时再将修改的值写入在主内存中,这样就出现了线程安全问题。因此结果就不可能等于1000了,一般都会小于1000。

     

    5. 什么是线程池,如何使用?

     

    线程池就是事先将多个线程对象放到一个容器中,当使用的时候就不用new线程而是直接去池中拿线程即可,节省了开辟子线程的时间,提高的代码执行效率。
    在JDK的java.util.concurrent.Executors中提供了生成多种线程池的静态方法。

     

    1. ExecutorService newCachedThreadPool = Executors.newCachedThreadPool(); 
    2. ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(4); 3. ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(4); 4. ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();

    然后调用他们的 execute 方法即可。 

    6. 常用的线程池有哪些?

    newSingleThreadExecutor:创建一个单线程的线程池,此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
    newFixedThreadPool:创建固定大小的线程池,每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。
    newCachedThreadPool:创建一个可缓存的线程池,此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
    newScheduledThreadPool:创建一个大小无限的线程池,此线程池支持定时以及周期性执行任务的需求。
    newSingleThreadExecutor:创建一个单线程的线程池。此线程池支持定时以及周期性执行任务的需求。

    7. 请叙述一下您对线程池的理解?

    第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
    第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
    第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

     

    8. 线程池的启动策略?

    官方对线程池的执行过程描述如下: 

    26.  /* 
    27.          * Proceed in 3 steps: 
    28.          * 
    29.          * 1. If fewer than corePoolSize threads are running, try to 
    30.          * start a new thread with the given command as its first 
    31.          * task.  The call to addWorker atomically checks runState and 
    32.          * workerCount, and so prevents false alarms that would add 
    33.          * threads when it shouldn‘t, by returning false. 
    34.          * 
    35.          * 2. If a task can be successfully queued, then we still need 
    36.          * to double-check whether we should have added a thread 
    37.          * (because existing ones died since last checking) or that 
    38.          * the pool shut down since entry into this method. So we 
    39.          * recheck state and if necessary roll back the enqueuing if 
    40.          * stopped, or start a new thread if there are none. 
    41.          * 
    42.          * 3. If we cannot queue task, then we try to add a new 
    43.          * thread.  If it fails, we know we are shut down or saturated 
    44.          * and so reject the task. 
    45.          */ 

    1、线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
    2、当调用execute() 方法添加一个任务时,线程池会做如下判断:
    a. 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
    b. 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列。
    c. 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建线程运行这个任务;
    d. 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常,告诉调用者“我不能再接受任务了”。
    3、当一个线程完成任务时,它会从队列中取下一个任务来执行。
    4、当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。

    9. 如何控制某个方法允许并发访问线程的个数

    可以使用Semaphore控制,第16行的构造函数创建了一个Semaphore对象,并且初始化了5个信号。这样的效果是控件 test 方法最多只能有 5 个线程并发访问,对于 5 个线程时就排队等待,走一个来一下。第 33行,请求一个信号(消费一个信号),如果信号被用完了则等待,第45行释放一个信号,释放的信号新的线程就可以使用了。

    10. 三个线程 a、b、c 并发运行,b,c 需要 a 线程的数据怎么实现

    根据问题的描述,我将问题用以下代码演示,ThreadA、ThreadB、ThreadC,ThreadA 用于初始化数据 num,只有当num初始化完成之后再让ThreadB和ThreadC获取到初始化后的变量num。
    分析过程如下:
    考虑到多线程的不确定性,因此我们不能确保ThreadA就一定先于ThreadB和ThreadC前执行,就算ThreadA先执行了,我们也无法保证ThreadA什么时候才能将变量num给初始化完成。因此我们必须让ThreadB和ThreadC去等待ThreadA完成任何后发出的消息。
    现在需要解决两个难题,一是让 ThreadB 和 ThreadC 等待 ThreadA 先执行完,二是 ThreadA 执行完之后给ThreadB和ThreadC发送消息。
    解决上面的难题我能想到的两种方案,一是使用纯Java API的Semaphore类来控制线程的等待和释放,二是使用Android提供的Handler消息机制。

    1. package com.example; 
    2. /** 
    3.  * 三个线程 a、b、c 并发运行,b,c 需要 a 线程的数据怎么实现(上海 3 期学员提供) 4.  * 
    5.  */ 
    6. public class ThreadCommunication { 
    7.  private static int num;//定义一个变量作为数据 
    8.   
    9.  public static void main(String[] args) { 
    10.    
    11.   Thread threadA = new Thread(new Runnable() { 
    12.     
    13.    @Override 
    14.    public void run() { 
    15.     try { 
    16.      //模拟耗时操作之后初始化变量 num 
    17.      Thread.sleep(1000); 
    18.      num = 1; 
    19.       
    20.     } catch (InterruptedException e) { 
    21.      e.printStackTrace(); 
    22.     } 
    23.    } 
    24.   }); 
    25.   Thread threadB = new Thread(new Runnable() { 
    26.     
    27.    @Override 
    28.    public void run() { 
    29.     System.out.println(Thread.currentThread().getName()+"获取到 num 的值为:"+num); 
    30.    } 
    31.   }); 
    32.   Thread threadC = new Thread(new Runnable() { 
    33.     
    34.    @Override 
    35.    public void run() { 
    36.     System.out.println(Thread.currentThread().getName()+"获取到 num 的值为:"+num); 
    37.    } 
    38.   }); 
    39.   //同时开启 3 个线程 
    40.   threadA.start(); 
    41.   threadB.start(); 
    42.   threadC.start(); 
    43.    
    44.  } 
    45. } 
    46.

    解决方案:

    1. public class ThreadCommunication { 
    2.  private static int num; 
    3.  /** 
    4.   * 定义一个信号量,该类内部维持了多个线程锁,可以阻塞多个线程,释放多个线程, 
    5. 线程的阻塞和释放是通过 permit 概念来实现的 
    6.   * 线程通过 semaphore.acquire()方法获取 permit,如果当前 semaphore 有 permit 则分配给该线程, 
    7. 如果没有则阻塞该线程直到 semaphore 
    8.   * 调用 release()方法释放 permit。 
    9.   * 构造函数中参数:permit(允许) 个数, 
    10.   */ 
    11.  private static Semaphore semaphore = new Semaphore(0); 
    12.  public static void main(String[] args) { 
    13.    
    14.   Thread threadA = new Thread(new Runnable() { 
    15.     
    16.    @Override 
    17.    public void run() { 
    18.     try { 
    19.      //模拟耗时操作之后初始化变量 num 
    20.      Thread.sleep(1000); 
    21.      num = 1; 
    22.      //初始化完参数后释放两个 permit 
    23.      semaphore.release(2); 
    24.       
    25.     } catch (InterruptedException e) { 
    26.      e.printStackTrace(); 
    27.     } 
    28.    } 
    29.   }); 
    30.   Thread threadB = new Thread(new Runnable() { 
    31.     
    32.    @Override 
    33.    public void run() { 
    34.     try { 
    35.      //获取 permit,如果 semaphore 没有可用的 permit 则等待,如果有则消耗一个 
    36.      semaphore.acquire(); 
    37.     } catch (InterruptedException e) { 
    38.      e.printStackTrace(); 
    39.     } 
    40.     System.out.println(Thread.currentThread().getName()+"获取到 num 的值为:"+num); 
    41.    } 
    42.   }); 
    43.   Thread threadC = new Thread(new Runnable() { 
    44.     
    45.    @Override 
    46.    public void run() { 
    47.     try { 
    48.      //获取 permit,如果 semaphore 没有可用的 permit 则等待,如果有则消耗一个 
    49.      semaphore.acquire(); 
    50.     } catch (InterruptedException e) { 
    51.      e.printStackTrace(); 
    52.     } 
    53.     System.out.println(Thread.currentThread().getName()+"获取到 num 的值为:"+num); 
    54.    } 
    55.   }); 
    56.   //同时开启 3 个线程 
    57.   threadA.start(); 
    58.   threadB.start(); 
    59.   threadC.start(); 
    60.    
    61.  } 
    62. }

    11. 同一个类中的 2 个方法都加了同步锁,多个线程能同时访问同一个类中的这两个方法吗?

    这个问题需要考虑到Lock与synchronized 两种实现锁的不同情形。因为这种情况下使用Lock 和synchronized 会有截然不同的结果。Lock可以让等待锁的线程响应中断,Lock获取锁,之后需要释放锁。如下代码,多个线程不可访问同一个类中的2个加了Lock锁的方法。

    而synchronized却不行,使用synchronized时,当我们访问同一个类对象的时候,是同一把锁,所以可以访问该对象的其他synchronized方法。

    12. 什么情况下导致线程死锁,遇到线程死锁该怎么解决?

    11.1 死锁的定义:所谓死锁是指多个线程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进。
    11.2 死锁产生的必要条件:
    互斥条件:线程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某 资源仅为一个线程所占有。此时若有其他线程请求该资源,则请求线程只能等待。
    不剥夺条件:线程所获得的资源在未使用完毕之前,不能被其他线程强行夺走,即只能由获得该资源的线程自己来释放(只能是主动释放)。
    请求和保持条件:线程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他线程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
    循环等待条件:存在一种线程资源的循环等待链,链中每一个线程已获得的资源同时被链中下一个线程所请求。即存在一个处于等待状态的线程集合{Pl, P2, ..., pn},其中Pi等待的资源被P(i+1)占有(i=0, 1, ..., n-1),Pn等待的资源被P0占有

    产生死锁的一个例子

    1.package .com; 
    2./** 
    3.* 一个简单的死锁类 
    4.* 当 DeadLock 类的对象 flag==1 时(td1),先锁定 o1,睡眠 500 毫秒 
    5.* 而 td1 在睡眠的时候另一个 flag==0 的对象(td2)线程启动,先锁定 o2,睡眠 500 毫秒   
    6.* td1 睡眠结束后需要锁定 o2 才能继续执行,而此时 o2 已被 td2 锁定;   
    7.* td2 睡眠结束后需要锁定 o1 才能继续执行,而此时 o1 已被 td1 锁定;   
    8.* td1、td2 相互等待,都需要得到对方锁定的资源才能继续执行,从而死锁。   
    9.*/     
    10.public class DeadLock implements Runnable {     
    11.    public int flag = 1;     
    12.    //静态对象是类的所有对象共享的     
    13.    private static Object o1 = new Object(), o2 = new Object();     
    14.    public void run() {     
    15.        System.out.println("flag=" + flag);     
    16.        if (flag == 1) {     
    17.            synchronized (o1) {     
    18.                try {     
    19.                    Thread.sleep(500);     
    20.                } catch (Exception e) {     
    21.                    e.printStackTrace();     
    22.                }     
    23.                synchronized (o2) {     
    24.                    System.out.println("1");     
    25.                }     
    26.            }     
    27.        }     
    28.        if (flag == 0) {     
    29.            synchronized (o2) {     
    30.                try {     
    31.                    Thread.sleep(500);     
    32.                } catch (Exception e) {     
    33.                    e.printStackTrace();     
    34.                }     
    35.                synchronized (o1) {     
    36.                    System.out.println("0");     
    37.                }     
    38.            }     
    39.        }     
    40.    }       
    41.    public static void main(String[] args) {        
    42.        DeadLock td1 = new DeadLock();     
    43.        DeadLock td2 = new DeadLock();     
    44.        td1.flag = 1;     
    45.        td2.flag = 0;     
    46.        //td1,td2 都处于可执行状态,但 JVM 线程调度先执行哪个线程是不确定的。     47.        //td2 的 run()可能在 td1 的 run()之前运行     
    48.        new Thread(td1).start();     
    49.        new Thread(td2).start();     
    50.    }     
    51.} 

    13. Java 中多线程间的通信怎么实现?

    线程通信的方式:
    1.共享变量
    线程间通信可以通过发送信号,发送信号的一个简单方式是在共享对象的变量里设置信号值。线程 A 在一个同步块里设置 boolean 型成员变量 hasDataToProcess 为 true,线程 B 也在同步块里读取 hasDataToProcess这个成员变量。这个简单的例子使用了一个持有信号的对象,并提供了 set 和 get 方法

    1.package igeekhome.com; 
    2.public class MySignal{ 
    3. //共享的变量 
    4. private boolean hasDataToProcess=false;  
    5.  //取值 
    6. public boolean getHasDataToProcess() { 
    7.  return hasDataToProcess; 
    8. } 
    9.  //存值 
    10. public void setHasDataToProcess(boolean hasDataToProcess) { 
    11.  this.hasDataToProcess = hasDataToProcess; 
    12. } 
    13. public static void main(String[] args){ 
    14.     //同一个对象 
    15.  final MySignal my=new MySignal();  
    16.     //线程 1 设置 hasDataToProcess 值为 true 
    17.  final Thread t1=new Thread(new Runnable(){ 
    18.   public void run() { 
    19.    my.setHasDataToProcess(true); 
    20.   }    
    21.  });   
    22.  t1.start();  
    23.    //线程 2 取这个值 hasDataToProcess 
    24.  Thread t2=new Thread(new Runnable(){ 
    25.   public void run() { 
    26.    try { 
    27.                  //等待线程 1 完成然后取值 
    28.     t1.join(); 
    29.    } catch (InterruptedException e) { 
    30.     e.printStackTrace(); 
    31.    } 
    32.    my.getHasDataToProcess(); 
    33.    System.out.println("t1 改变以后的值:" + my.isHasDataToProcess()); 34.   }   
    35.  });  
    36.  t2.start(); 
    37.} 
    38.} 
    结果: 
     
     t1 改变以后的值:true

    2.wait/notify 机制
    以资源为例,生产者生产一个资源,通知消费者就消费掉一个资源,生产者继续生产资源,消费者消费资源,以此循环。代码如下:

    1.package igeekhome.com; 
    2.//资源类 
    3. class Resource{ 
    4.    private String name;   
    5.    private int count=1;   
    6.    private boolean flag=false;   
    7.    public synchronized void set(String name){   
    8.     //生产资源 
    9.        while(flag) { 
    10.            try{ 
    11.             //线程等待。消费者消费资源 
    12.             wait(); 
    13.             }catch(Exception e){}   
    14.        } 
    15.        this.name=name+"---"+count++;   
    16.        System.out.println(Thread.currentThread().getName()+"...生产者..."+this.name);   
    17.        flag=true;   
    18.        //唤醒等待中的消费者 
    19.        this.notifyAll(); 
    20.        }   
    21.    public synchronized void out(){  
    22.     //消费资源 
    23.        while(!flag) { 
    24.         //线程等待,生产者生产资源 
    25.            try{wait();}catch(Exception e){}   
    26.        } 
    27.        System.out.println(Thread.currentThread().getName()+"...消费者..."+this.name);   
    28.        flag=false;   
    29.        //唤醒生产者,生产资源 
    30.        this.notifyAll();  
    31.        }   
    32.}   
    33. //生产者 
    34. class Producer implements Runnable{   
    35.     private Resource res;   
    36.     Producer(Resource res){   
    37.         this.res=res;   
    38.     }   
    39.     //生产者生产资源 
    40.     public void run(){   
    41.         while(true){   
    42.             res.set("商品");  
    43.         }   
    44.     }   
    45. }   
    46. //消费者消费资源 
    47. class Consumer implements Runnable{   
    48.     private Resource res;   
    49.     Consumer(Resource res){   
    50.         this.res=res;   
    51.     }   
    52.     public void run(){   
    53.         while(true){   
    54.             res.out();   
    55.         }   
    56.     }   
    57. }   
    58.public class ProducerConsumerDemo{   
    59.    public static void main(String[] args){   
    60.        Resource r=new Resource();   
    61.        Producer pro=new Producer(r);   
    62.        Consumer con=new Consumer(r);   
    63.        Thread t1=new Thread(pro);   
    64.        Thread t2=new Thread(con);   
    65.        t1.start();   
    66.        t2.start();   
    67.    }   
    68.} 

    14. 线程和进程的区别

     

    进程:具有一定独立功能的程序关于某个数据集合上的一次运行活动,是操作系统进行资源分配和调度的一个独立单位。
    线程:是进程的一个实体,是cpu调度和分派的基本单位,是比进程更小的可以独立运行的基本单位。
    特点:线程的划分尺度小于进程,这使多线程程序拥有高并发性,进程在运行时各自内存单元相互独立,线程之间内存共享,这使多线程编程可以拥有更好的性能和用户体验
    注意:多线程编程对于其它程序是不友好的,占据大量cpu资源。

    15. 请说出同步线程及线程调度相关的方法?

    wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁; 
    sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理InterruptedException异常; 
    notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,
    而是由JVM确定唤醒哪个线程,而且与优先级无关; 
    notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁
    的线程才能进入就绪状态; 
    注意:java 5 通过Lock接口提供了显示的锁机制,Lock接口中定义了加锁(lock()方法)和解锁(unLock()
    方法),增强了多线程编程的灵活性及对线程的协调 

    16. 启动一个线程是调用 run()方法还是 start()方法?

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

     

    以上是关于多线程四大经典案例及java多线程的实现的主要内容,如果未能解决你的问题,请参考以下文章

    多线程四大经典案例

    Java并发指南17:Java常见多线程面试题及答案

    Java 多线程与并发(案例 + 应用)

    Java 多线程与并发(案例 + 应用)

    Java多线程分析案例

    Java 多线程系列2——多线程的生命周期及生产消费者模型