多线程_多线程并发 - CSDN
多线程 订阅
多线程(multithreading),是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。具有这种能力的系统包括对称多处理机、多核心处理器以及芯片级多处理或同时多线程处理器。在一个程序中,这些独立运行的程序片段叫作“线程”(Thread),利用它编程的概念就叫作“多线程处理” [1]  。 展开全文
多线程(multithreading),是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。具有这种能力的系统包括对称多处理机、多核心处理器以及芯片级多处理或同时多线程处理器。在一个程序中,这些独立运行的程序片段叫作“线程”(Thread),利用它编程的概念就叫作“多线程处理” [1]  。
信息
外文名
multithreading
对    象
计算机
作    用
提升整体处理性能
用    途
实现多个线程并发执行的技术
含    义
从软件或者硬件上实现多个线程并发执行的技术
中文名
多线程
多线程简介
在计算机编程中,一个基本的概念就是同时对多个任务加以控制。许多程序设计问题都要求程序能够停下手头的工作,改为处理其他一些问题,再返回主进程。可以通过多种途径达到这个目的。最开始的时候,那些掌握机器低级语言的程序员编写一些“中断服务例程”,主进程的暂停是通过硬件级的中断实现的。尽管这是一种有用的方法,但编出的程序很难移植,由此造成了另一类的代价高昂问题。中断对那些实时性很强的任务来说是很有必要的。但对于其他许多问题,只要求将问题划分进入独立运行的程序片断中,使整个程序能更迅速地响应用户的请求 [2]  。最开始,线程只是用于分配单个处理器的处理时间的一种工具。但假如操作系统本身支持多个处理器,那么每个线程都可分配给一个不同的处理器,真正进入“并行运算”状态。从程序设计语言的角度看,多线程操作最有价值的特性之一就是程序员不必关心到底使用了多少个处理器。程序在逻辑意义上被分割为数个线程;假如机器本身安装了多个处理器,那么程序会运行得更快,毋需作出任何特殊的调校。根据前面的论述,大家可能感觉线程处理非常简单。但必须注意一个问题:共享资源!如果有多个线程同时运行,而且它们试图访问相同的资源,就会遇到一个问题。举个例子来说,两个线程不能将信息同时发送给一台打印机。为解决这个问题,对那些可共享的资源来说(比如打印机),它们在使用期间必须进入锁定状态。所以一个线程可将资源锁定,在完成了它的任务后,再解开(释放)这个锁,使其他线程可以接着使用同样的资源 [2]  。多线程是为了同步完成多项任务,不是为了提高运行效率,而是为了提高资源使用效率来提高系统的效率。线程是在同一时间需要完成多项任务的时候实现的 [2]  。 多线程(2张)
收起全文
精华内容
参与话题
  • Java多线程超详解

    万次阅读 多人点赞 2019-06-11 01:00:30
    随着计算机的配置越来越高,我们需要将进程进一步优化,细分为线程,充分提高图形化界面的多线程的开发。这就要求对线程的掌握很彻底。 那么话不多说,今天本帅将记录自己线程的学习。 线程的相关API //获取当前...

    引言

    随着计算机的配置越来越高,我们需要将进程进一步优化,细分为线程,充分提高图形化界面的多线程的开发。这就要求对线程的掌握很彻底。
    那么话不多说,今天本帅将记录自己线程的学习。

    程序,进程,线程的基本概念+并行与并发:

    程序:是为完成特定任务,用某种语言编写的一组指令的集合,即指一段静态的代码,静态对象。
    进程:是程序的一次执行过程,或是正在运行的一个程序,是一个动态的过程,有它自身的产生,存在和消亡的过程。-------生命周期
    线程:进程可进一步细化为线程,是一个程序内部的一条执行路径

    即:线程《线程(一个程序可以有多个线程)
    程序:静态的代码 进程:动态执行的程序
    线程:进程中要同时干几件事时,每一件事的执行路径成为线程。

    并行:多个CPU同时执行多个任务,比如:多个人同时做不同的事
    并发:一个CPU(采用时间片)同时执行多个任务,比如秒杀平台,多个人做同件事

    线程的相关API

    //获取当前线程的名字
    Thread.currentThread().getName()

    1.start():1.启动当前线程2.调用线程中的run方法
    2.run():通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中
    3.currentThread():静态方法,返回执行当前代码的线程
    4.getName():获取当前线程的名字
    5.setName():设置当前线程的名字
    6.yield():主动释放当前线程的执行权
    7.join():在线程中插入执行另一个线程,该线程被阻塞,直到插入执行的线程完全执行完毕以后,该线程才继续执行下去
    8.stop():过时方法。当执行此方法时,强制结束当前线程。
    9.sleep(long millitime):线程休眠一段时间
    10.isAlive():判断当前线程是否存活

    判断是否是多线程

    一条线程即为一条执行路径,即当能用一条路径画出来时即为一个线程
    例:如下看似既执行了方法一,又执行了方法2,但是其实质就是主线程在执行方法2和方法1这一条路径,所以就是一个线程

    public class Sample{
    		public void method1(String str){
    			System.out.println(str);
    		}
    	
    	public void method2(String str){
    		method1(str);
    	}
    	
    	public static void main(String[] args){
    		Sample s = new Sample();
    		s.method2("hello");
    	}
    }
    

    在这里插入图片描述

    线程的调度

    调度策略:
    时间片:线程的调度采用时间片轮转的方式
    抢占式:高优先级的线程抢占CPU
    Java的调度方法:
    1.对于同优先级的线程组成先进先出队列(先到先服务),使用时间片策略
    2.对高优先级,使用优先调度的抢占式策略

    线程的优先级

    等级:
    MAX_PRIORITY:10
    MIN_PRIORITY:1
    NORM_PRIORITY:5

    方法:
    getPriority():返回线程优先级
    setPriority(int newPriority):改变线程的优先级

    注意!:高优先级的线程要抢占低优先级的线程的cpu的执行权。但是仅是从概率上来说的,高优先级的线程更有可能被执行。并不意味着只有高优先级的线程执行完以后,低优先级的线程才执行。

    多线程的创建方式

    1. 方式1:继承于Thread类

    1.创建一个集成于Thread类的子类 (通过ctrl+o(override)输入run查找run方法)
    2.重写Thread类的run()方法
    3.创建Thread子类的对象
    4.通过此对象调用start()方法

    start与run方法的区别:

    start方法的作用:1.启动当前线程 2.调用当前线程的重写的run方法(在主线程中生成子线程,有两条线程)
    调用start方法以后,一条路径代表一个线程,同时执行两线程时,因为时间片的轮换,所以执行过程随机分配,且一个线程对象只能调用一次start方法。
    run方法的作用:在主线程中调用以后,直接在主线程一条线程中执行了该线程中run的方法。(调用线程中的run方法,只调用run方法,并不新开线程)

    总结:我们不能通过run方法来新开一个线程,只能调用线程中重写的run方法(可以在线程中不断的调用run方法,但是不能开启子线程,即不能同时干几件事),start是开启线程,再调用方法(即默认开启一次线程,调用一次run方法,可以同时执行几件事)
    在这里插入图片描述

    多线程例子(火车站多窗口卖票问题)

    	package com.example.paoduantui.Thread;
    	
    	import android.view.Window;
    	
    	/**
    	 *
    	 * 创建三个窗口卖票,总票数为100张,使用继承自Thread方式
    	 * 用静态变量保证三个线程的数据独一份
    	 * 
    	 * 存在线程的安全问题,有待解决
    	 *
    	 * */
    	
    	public class ThreadDemo extends Thread{
    	
    	    public static void main(String[] args){
    	        window t1 = new window();
    	        window t2 = new window();
    	        window t3 = new window();
    	
    	        t1.setName("售票口1");
    	        t2.setName("售票口2");
    	        t3.setName("售票口3");
    	
    	        t1.start();
    	        t2.start();
    	        t3.start();
    	    }
    	
    	}
    	
    	class window extends Thread{
    	    private static int ticket = 100; //将其加载在类的静态区,所有线程共享该静态变量
    	
    	    @Override
    	    public void run() {
    	        while(true){
    	            if(ticket>0){
    	//                try {
    	//                    sleep(100);
    	//                } catch (InterruptedException e) {
    	//                    e.printStackTrace();
    	//                }
    	                System.out.println(getName()+"当前售出第"+ticket+"张票");
    	                ticket--;
    	            }else{
    	                break;
    	            }
    	        }
    	    }
    	}
    

    2. 方式2:实现Runable接口方式

    1.创建一个实现了Runable接口的类
    2.实现类去实现Runnable中的抽象方法:run()
    3.创建实现类的对象
    4.将此对象作为参数传递到Thread类中的构造器中,创建Thread类的对象
    5.通过Thread类的对象调用start()

    具体操作,将一个类实现Runable接口,(插上接口一端)。
    另外一端,通过实现类的对象与线程对象通过此Runable接口插上接口实现

    	package com.example.paoduantui.Thread;
    	
    	public class ThreadDemo01 {
    	    
    	    public static  void main(String[] args){
    	        window1 w = new window1();
    	        
    	        //虽然有三个线程,但是只有一个窗口类实现的Runnable方法,由于三个线程共用一个window对象,所以自动共用100张票
    	        
    	        Thread t1=new Thread(w);
    	        Thread t2=new Thread(w);
    	        Thread t3=new Thread(w);
    	
    	        t1.setName("窗口1");
    	        t2.setName("窗口2");
    	        t3.setName("窗口3");
    	        
    	        t1.start();
    	        t2.start();
    	        t3.start();
    	    }
    	}
    	
    	class window1 implements Runnable{
    	    
    	    private int ticket = 100;
    	
    	    @Override
    	    public void run() {
    	        while(true){
    	            if(ticket>0){
    	//                try {
    	//                    sleep(100);
    	//                } catch (InterruptedException e) {
    	//                    e.printStackTrace();
    	//                }
    	                System.out.println(Thread.currentThread().getName()+"当前售出第"+ticket+"张票");
    	                ticket--;
    	            }else{
    	                break;
    	            }
    	        }
    	    }
    	}
    

    比较创建线程的两种方式:
    开发中,优先选择实现Runable接口的方式
    原因1:实现的方式没有类的单继承性的局限性
    2:实现的方式更适合用来处理多个线程有共享数据的情况
    联系:Thread也是实现自Runable,两种方式都需要重写run()方法,将线程要执行的逻辑声明在run中

    3.新增的两种创建多线程方式

    1.实现callable接口方式:

    与使用runnable方式相比,callable功能更强大些:
    runnable重写的run方法不如callaalbe的call方法强大,call方法可以有返回值
    方法可以抛出异常
    支持泛型的返回值
    需要借助FutureTask类,比如获取返回结果

    package com.example.paoduantui.Thread;
    
    
    import java.util.concurrent.Callable;
    import java.util.concurrent.ExecutionException;
    import java.util.concurrent.FutureTask;
    
    /**
     * 创建线程的方式三:实现callable接口。---JDK 5.0新增
     *是否多线程?否,就一个线程
     *
     * 比runable多一个FutureTask类,用来接收call方法的返回值。
     * 适用于需要从线程中接收返回值的形式
     * 
     * //callable实现新建线程的步骤:
     * 1.创建一个实现callable的实现类
     * 2.实现call方法,将此线程需要执行的操作声明在call()中
     * 3.创建callable实现类的对象
     * 4.将callable接口实现类的对象作为传递到FutureTask的构造器中,创建FutureTask的对象
     * 5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start方法启动(通过FutureTask的对象调用方法get获取线程中的call的返回值)
     * 
     * */
    
    
    //实现callable接口的call方法
    class NumThread implements Callable{
    
        private int sum=0;//
    
        //可以抛出异常
        @Override
        public Object call() throws Exception {
            for(int i = 0;i<=100;i++){
                if(i % 2 == 0){
                    System.out.println(Thread.currentThread().getName()+":"+i);
                    sum += i;
                }
            }
            return sum;
        }
    }
    
    public class ThreadNew {
    
        public static void main(String[] args){
            //new一个实现callable接口的对象
            NumThread numThread = new NumThread();
    
            //通过futureTask对象的get方法来接收futureTask的值
            FutureTask futureTask = new FutureTask(numThread);
    
            Thread t1 = new Thread(futureTask);
            t1.setName("线程1");
            t1.start();
    
            try {
                //get返回值即为FutureTask构造器参数callable实现类重写的call的返回值
               Object sum = futureTask.get();
               System.out.println(Thread.currentThread().getName()+":"+sum);
            } catch (ExecutionException e) {
                e.printStackTrace();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    使用线程池的方式:

    背景:经常创建和销毁,使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
    思路:提前创建好多个线程,放入线程池之,使用时直接获取,使用完放回池中。可以避免频繁创建销毁,实现重复利用。类似生活中的公共交通工具。(数据库连接池)
    好处:提高响应速度(减少了创建新线程的时间)
    降低资源消耗(重复利用线程池中线程,不需要每次都创建)
    便于线程管理
    corePoolSize:核心池的大小
    maximumPoolSize:最大线程数
    keepAliveTime:线程没有任务时最多保持多长时间后会终止
    。。。。。。

    JDK 5.0 起提供了线程池相关API:ExecutorService 和 Executors
    ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor.
    void execute(Runnable coommand):执行任务/命令,没有返回值,一般用来执行Runnable
    Futuresubmit(Callable task):执行任务,有返回值,一般又来执行Callable
    void shutdown():关闭连接池。

    Executors 工具类,线程池的工厂类,用于创建并返回不同类型的线程池
    Executors.newCachedThreadPool() 创建一个可根据需要创建新线程的线程池
    Executors.newFixedThreadPool(n) 创建一个可重用固定线程数的线程池
    Executors.newSingleThreadExecutor() :创建一个只有一个线程的线程池
    Executors.newScheduledThreadPool(n) 创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。

    线程池构造批量线程代码如下:

    package com.example.paoduantui.Thread;
    
    
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    /**
     * 创建线程的方式四:使用线程池(批量使用线程)
     *1.需要创建实现runnable或者callable接口方式的对象
     * 2.创建executorservice线程池
     * 3.将创建好的实现了runnable接口类的对象放入executorService对象的execute方法中执行。
     * 4.关闭线程池
     *
     * */
    
    class NumberThread implements Runnable{
    
    
        @Override
        public void run() {
            for(int i = 0;i<=100;i++){
                if (i % 2 ==0 )
                System.out.println(Thread.currentThread().getName()+":"+i);
            }
        }
    }
    
    class NumberThread1 implements Runnable{
        @Override
        public void run() {
            for(int i = 0;i<100; i++){
                if(i%2==1){
                    System.out.println(Thread.currentThread().getName()+":"+i);
                }
            }
        }
    }
    
    public class ThreadPool {
    
        public static void main(String[] args){
    
            //创建固定线程个数为十个的线程池
            ExecutorService executorService = Executors.newFixedThreadPool(10);
    
            //new一个Runnable接口的对象
            NumberThread number = new NumberThread();
            NumberThread1 number1 = new NumberThread1();
    
            //执行线程,最多十个
            executorService.execute(number1);
            executorService.execute(number);//适合适用于Runnable
    
            //executorService.submit();//适合使用于Callable
            //关闭线程池
            executorService.shutdown();
        }
    
    }
    

    目前两种方式要想调用新线程,都需要用到Thread中的start方法。

    java virtual machine(JVM):java虚拟机内存结构

    程序(一段静态的代码)——————》加载到内存中——————》进程(加载到内存中的代码,动态的程序)
    进程可细分为多个线程,一个线程代表一个程序内部的一条执行路径
    每个线程有其独立的程序计数器(PC,指导着程序向下执行)与运行栈(本地变量等,本地方法等)
    在这里插入图片描述

    大佬传送门:https://blog.csdn.net/bluetjs/article/details/52874852

    线程通信方法:

    wait()/ notify()/ notifayAll():此三个方法定义在Object类中的,因为这三个方法需要用到锁,而锁是任意对象都能充当的,所以这三个方法定义在Object类中。

    由于wait,notify,以及notifyAll都涉及到与锁相关的操作
    wait(在进入锁住的区域以后阻塞等待,释放锁让别的线程先进来操作)---- Obj.wait 进入Obj这个锁住的区域的线程把锁交出来原地等待通知
    notify(由于有很多锁住的区域,所以需要将区域用锁来标识,也涉及到锁) ----- Obj.notify 新线程进入Obj这个区域进行操作并唤醒wait的线程

    有点类似于我要拉粑粑,我先进了厕所关了门,但是发现厕所有牌子写着不能用,于是我把厕所锁给了别人,别人进来拉粑粑还是修厕所不得而知,直到有人通知我厕所好了我再接着用。

    所以wait,notify需要使用在有锁的地方,也就是需要用synchronize关键字来标识的区域,即使用在同步代码块或者同步方法中,且为了保证wait和notify的区域是同一个锁住的区域,需要用锁来标识,也就是锁要相同的对象来充当

    线程的分类:

    java中的线程分为两类:1.守护线程(如垃圾回收线程,异常处理线程),2.用户线程(如主线程)

    若JVM中都是守护线程,当前JVM将退出。(形象理解,唇亡齿寒)

    线程的生命周期:

    JDK中用Thread.State类定义了线程的几种状态,如下:

    线程生命周期的阶段 描述
    新建 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
    就绪 处于新建状态的线程被start后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源
    运行 当就绪的线程被调度并获得CPU资源时,便进入运行状态,run方法定义了线程的操作和功能
    阻塞 在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时终止自己的执行,进入阻塞状态
    死亡 线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束

    在这里插入图片描述

    线程的同步:在同步代码块中,只能存在一个线程。

    线程的安全问题:

    什么是线程安全问题呢?
    线程安全问题是指,多个线程对同一个共享数据进行操作时,线程没来得及更新共享数据,从而导致另外线程没得到最新的数据,从而产生线程安全问题。

    上述例子中:创建三个窗口卖票,总票数为100张票
    1.卖票过程中,出现了重票(票被反复的卖出,ticket未被减少时就打印出了)错票。
    2.问题出现的原因:当某个线程操作车票的过程中,尚未完成操作时,其他线程参与进来,也来操作车票。(将此过程的代码看作一个区域,当有线程进去时,装锁,不让别的线程进去)
    生动理解的例子:有一个厕所,有人进去了,但是没有上锁,于是别人不知道你进去了,别人也进去了对厕所也使用造成错误。
    3.如何解决:当一个线程在操作ticket时,其他线程不能参与进来,直到此线程的生命周期结束
    4.在java中,我们通过同步机制,来解决线程的安全问题。

    方式一:同步代码块
    使用同步监视器(锁)
    Synchronized(同步监视器){
    //需要被同步的代码
    }
    说明:

    1. 操作共享数据的代码(所有线程共享的数据的操作的代码)(视作卫生间区域(所有人共享的厕所)),即为需要共享的代码(同步代码块,在同步代码块中,相当于是一个单线程,效率低)
    2. 共享数据:多个线程共同操作的数据,比如公共厕所就类比共享数据
    3. 同步监视器(俗称:锁):任何一个的对象都可以充当锁。(但是为了可读性一般设置英文成lock)当锁住以后只能有一个线程能进去(要求:多个线程必须要共用同一把锁,比如火车上的厕所,同一个标志表示有人)

    Runable天生共享锁,而Thread中需要用static对象或者this关键字或者当前类(window。class)来充当唯一锁

    方式二:同步方法
    使用同步方法,对方法进行synchronized关键字修饰
    将同步代码块提取出来成为一个方法,用synchronized关键字修饰此方法。
    对于runnable接口实现多线程,只需要将同步方法用synchronized修饰
    而对于继承自Thread方式,需要将同步方法用static和synchronized修饰,因为对象不唯一(锁不唯一)

    总结:1.同步方法仍然涉及到同步监视器,只是不需要我们显示的声明。
    2.非静态的同步方法,同步监视器是this
    静态的同步方法,同步监视器是当前类本身。继承自Thread。class

    方式三:JDK5.0新增的lock锁方法

    package com.example.paoduantui.Thread;
    
    
    import java.util.concurrent.locks.ReentrantLock;
    
    class Window implements Runnable{
        private int ticket = 100;//定义一百张票
        //1.实例化锁
        private ReentrantLock lock = new ReentrantLock();
    
        @Override
        public void run() {
            
                while (true) {
    
                    //2.调用锁定方法lock
                    lock.lock();
    
                    if (ticket > 0) {
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
    
                        System.out.println(Thread.currentThread().getName() + "售出第" + ticket + "张票");
                        ticket--;
                    } else {
                        break;
                    }
                }
    
    
            }
    }
    
    public class LockTest {
    
        public static void main(String[] args){
           Window w= new Window();
    
           Thread t1 = new Thread(w);
           Thread t2 = new Thread(w);
           Thread t3 = new Thread(w);
    
           t1.setName("窗口1");
           t2.setName("窗口1");
           t3.setName("窗口1");
    
           t1.start();
           t2.start();
           t3.start();
        }
    
    }
    

    总结:Synchronized与lock的异同?

    相同:二者都可以解决线程安全问题
    不同:synchronized机制在执行完相应的代码逻辑以后,自动的释放同步监视器
    lock需要手动的启动同步(lock()),同时结束同步也需要手动的实现(unlock())(同时以为着lock的方式更为灵活)

    优先使用顺序:
    LOCK-》同步代码块-》同步方法

    判断线程是否有安全问题,以及如何解决:

    1.先判断是否多线程
    2.再判断是否有共享数据
    3.是否并发的对共享数据进行操作
    4.选择上述三种方法解决线程安全问题

    例题:

    	package com.example.paoduantui.Thread;
    	
    	/***
    	 * 描述:甲乙同时往银行存钱,存够3000
    	 *
    	 *
    	 * */
    	
    	//账户
    	class Account{
    	    private double balance;//余额
    	    //构造器
    	    public Account(double balance) {
    	        this.balance = balance;
    	    }
    	    //存钱方法
    	    public synchronized void deposit(double amt){
    	        if(amt>0){
    	            balance +=amt;
    	            try {
    	                Thread.sleep(1000);
    	            } catch (InterruptedException e) {
    	                e.printStackTrace();
    	            }
    	            System.out.println(Thread.currentThread().getName()+"存钱成功,余额为:"+balance);
    	        }
    	    }
    	}
    	
    	//两个顾客线程
    	class Customer extends Thread{
    	     private Account acct;
    	
    	     public Customer(Account acct){
    	         this.acct = acct;
    	     }
    	
    	
    	
    	    @Override
    	    public void run() {
    	        for (int i = 0;i<3;i++){
    	            acct.deposit(1000);
    	        }
    	    }
    	}
    	
    	//主方法,之中new同一个账户,甲乙两个存钱线程。
    	public class AccountTest {
    	
    	    public static void main(String[] args){
    	        Account acct = new Account(0);
    	        Customer c1 = new Customer(acct);
    	        Customer c2 = new Customer(acct);
    	
    	        c1.setName("甲");
    	        c2.setName("乙");
    	
    	        c1.start();
    	        c2.start();
    	    }
    	
    	}
    

    解决单例模式的懒汉式的线程安全问题:

    单例:只能通过静态方法获取一个实例,不能通过构造器来构造实例
    1.构造器的私有化:
    private Bank(){}//可以在构造器中初始化东西
    private static Bank instance = null;//初始化静态实例

    public static Bank getInstance(){
    if(instance!=null){
    instance = new Bank();
    }
    return instance;
    }

    假设有多个线程调用此单例,而调用的获取单例的函数作为操作共享单例的代码块并没有解决线程的安全问题,会导致多个线程都判断实例是否为空,此时就会导致多个实例的产生,也就是单例模式的线程安全问题。

    解决线程安全问题的思路:

    1. 将获取单例的方法改写成同部方法,即加上synchronized关键字,此时同步监视器为当前类本身。(当有多个线程并发的获取实例时,同时只能有一个线程获取实例),解决了单例模式的线程安全问题。
    2. 用同步监视器包裹住同步代码块的方式。

    懒汉式单例模式的模型,例如:生活中的限量版的抢购:
    当一群人并发的抢一个限量版的东西的时候,可能同时抢到了几个人,他们同时进入了房间(同步代码块内)
    但是只有第一个拿到限量版东西的人才能到手,其余人都不能拿到,所以效率稍高的做法是,当东西被拿走时,我们在门外立一块牌子,售罄。
    这样就减少了线程的等待。即下面效率稍高的懒汉式写法:

    package com.example.paoduantui.Thread;
    
    public class Bank {
        //私有化构造器
        private Bank(){}
        //初始化静态实例化对象
        private static  Bank instance = null;
    
        //获取单例实例,此种懒汉式单例模式存在线程不安全问题(从并发考虑)
    
        public static  Bank getInstance(){
            if(instance==null){
                instance = new Bank();
            }
            return  instance;
        }
    
        //同步方法模式的线程安全
        public static synchronized Bank getInstance1(){
            if(instance==null){
                instance = new Bank();
            }
            return  instance;
        }
        //同步代码块模式的线程安全(上锁)
        public  static Bank getInstance2(){
            synchronized (Bank.class){
                if(instance==null){
                    instance = new Bank();
                }
                return  instance;
            }
        }
        
        //效率更高的线程安全的懒汉式单例模式
        /**
         * 由于当高并发调用单例模式的时候,类似于万人夺宝,只有第一个进入房间的人才能拿到宝物,
         * 当多个人进入这个房间时,第一个人拿走了宝物,也就另外几个人需要在同步代码块外等候,
         * 剩下的人只需要看到门口售罄的牌子即已知宝物已经被夺,可以不用进入同步代码块内,提高了效率。
         * 
         * 
         * */
        public static Bank getInstance3(){
            if (instance==null){
                synchronized (Bank.class){
                    if(instance==null){
                        instance = new Bank();
                    }
                }
            }
            return  instance;
        }
    }
    

    线程的死锁问题:

    线程死锁的理解:僵持,谁都不放手,一双筷子,我一只你一只,都等对方放手(死锁,两者都进入阻塞,谁都吃不了饭,进行不了下面吃饭的操作)
    出现死锁以后,不会出现提示,只是所有线程都处于阻塞状态,无法继续

    package com.example.paoduantui.Thread;
    
    
    /**
     * 演示线程的死锁问题
     *
     * */
    public class Demo {
    
        public static void main(String[] args){
    
            final StringBuffer s1 = new StringBuffer();
            final StringBuffer s2 = new StringBuffer();
    
    
            new Thread(){
                @Override
                public void run() {
                    //先拿锁一,再拿锁二
                    synchronized (s1){
                        s1.append("a");
                        s2.append("1");
    
                        synchronized (s2) {
                            s1.append("b");
                            s2.append("2");
    
                            System.out.println(s1);
                            System.out.println(s2);
                        }
                    }
                }
            }.start();
    
            //使用匿名内部类实现runnable接口的方式实现线程的创建
            new Thread(new Runnable() {
                @Override
                public void run() {
                    synchronized (s2){
                        s1.append("c");
                        s2.append("3");
    
                        synchronized (s1) {
                            s1.append("d");
                            s2.append("4");
    
                            System.out.println(s1);
                            System.out.println(s2);
                        }
                    }
                }
            }).start();
        }
    
    }
    

    运行结果:
    1.先调用上面的线程,再调用下面的线程:
    在这里插入图片描述
    2.出现死锁:
    在这里插入图片描述
    3.先调用下面的线程,再调用上面的线程。
    在这里插入图片描述

    死锁的解决办法:

    1.减少同步共享变量
    2.采用专门的算法,多个线程之间规定先后执行的顺序,规避死锁问题
    3.减少锁的嵌套。

    线程的通信

    通信常用方法:

    通信方法 描述
    wait() 一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器
    notify 一旦执行此方法,就会唤醒被wait的一个线程,如果有多个线程,就唤醒优先级高的线程
    notifyAll 一旦执行此方法,就会唤醒所有被wait()的线程

    使用前提:这三个方法均只能使用在同步代码块或者同步方法中。

    package com.example.paoduantui.Thread;
    
    
    /**
     * 线程通信的例子:使用两个线程打印1—100,线程1,线程2交替打印
     *
     * 当我们不采取线程之间的通信时,无法达到线程1,2交替打印(cpu的控制权,是自动分配的)
     * 若想达到线程1,2交替打印,需要:
     * 1.当线程1获取锁以后,进入代码块里将number++(数字打印并增加)操作完以后,为了保证下个锁为线程2所有,需要将线程1阻塞(线程1你等等wait())。(输出1,number为2)
     * 2.当线程2获取锁以后,此时线程1已经不能进入同步代码块中了,所以,为了让线程1继续抢占下一把锁,需要让线程1的阻塞状态取消(通知线程1不用等了notify()及notifyAll()),即应该在进入同步代码块时取消线程1的阻塞。
     *
     * */
    
    class Number implements Runnable{
    
        private int number = 1;//设置共享数据(线程之间对于共享数据的共享即为通信)
    
    
        //对共享数据进行操作的代码块,需要线程安全
        @Override
        public synchronized void run() {
    
            while(true){
                //使得线程交替等待以及通知交替解等待
                notify();//省略了this.notify()关键字
                if(number<100){
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+":"+number);
                    number++;
                    try {
                        wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }else{
                    break;
                }
            }
        }
    }
    
    public class CommunicationTest {
    
        public static void main(String[] args){
            //创建runnable对象
            Number number = new Number();
    
            //创建线程,并实现runnable接口
            Thread t1 = new Thread(number);
            Thread t2 = new Thread(number);
    
            //给线程设置名字
            t1.setName("线程1");
            t2.setName("线程2");
    
            //开启线程
            t1.start();
            t2.start();
    
        }
    
    }
    

    sleep和wait的异同:

    相同点:一旦执行方法以后,都会使得当前的进程进入阻塞状态
    不同点:
    1.两个方法声明的位置不同,Thread类中声明sleep,Object类中声明wait。
    2.调用的要求不同,sleep可以在任何需要的场景下调用,wait必须使用在同步代码块或者同步方法中
    3.关于是否释放同步监视器,如果两个方法都使用在同步代码块或同步方法中,sleep不会释放,wait会释放

    经典例题:生产者/消费者问题:

    生产者(Priductor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有固定数量的产品(比如20个),如果生产者视图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产:如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。

    这里可能出现两个问题:
    生产者比消费者快的时候,消费者会漏掉一些数据没有收到。
    消费者比生产者快时,消费者会去相同的数据。

    package com.example.paoduantui.Thread;
    
    
    /**
     * 线程通信的应用:生产者/消费者问题
     *
     * 1.是否是多线程问题?是的,有生产者线程和消费者线程(多线程的创建,四种方式)
     * 2.多线程问题是否存在共享数据? 存在共享数据----产品(同步方法,同步代码块,lock锁)
     * 3.多线程是否存在线程安全问题? 存在----都对共享数据产品进行了操作。(三种方法)
     * 4.是否存在线程间的通信,是,如果生产多了到20时,需要通知停止生产(wait)。(线程之间的通信问题,需要wait,notify等)
     *
     * */
    
    
    	class Clerk{
    	
    	    private int productCount = 0;
    	
    	
    	    //生产产品
    	    public synchronized void produceProduct() {
    	
    	        if(productCount<20) {
    	            productCount++;
    	
    	            System.out.println(Thread.currentThread().getName()+":开始生产第"+productCount+"个产品");
    	            notify();
    	        }else{
    	            //当有20个时,等待wait
    	            try {
    	                wait();
    	            } catch (InterruptedException e) {
    	                e.printStackTrace();
    	            }
    	        }
    	    }
    	
    	    //消费产品
    	    public synchronized void consumeProduct() {
    	        if (productCount>0){
    	            System.out.println(Thread.currentThread().getName()+":开始消费第"+productCount+"个产品");
    	            productCount--;
    	            notify();
    	        }else{
    	            //当0个时等待
    	            try {
    	                wait();
    	            } catch (InterruptedException e) {
    	                e.printStackTrace();
    	            }
    	        }
    	    }
    	}
    	
    	class Producer extends Thread{//生产者线程
    	
    	    private Clerk clerk;
    	
    	    public Producer(Clerk clerk) {
    	        this.clerk = clerk;
    	    }
    	
    	    @Override
    	    public void run() {
    	
    	        try {
    	            sleep(10);
    	        } catch (InterruptedException e) {
    	            e.printStackTrace();
    	        }
    	        System.out.println(Thread.currentThread().getName()+";开始生产产品......");
    	
    	        while(true){
    	            clerk.produceProduct();
    	        }
    	    }
    	}
    	
    	class Consumer implements Runnable{//消费者线程
    	
    	    private Clerk clerk;
    	
    	    public Consumer(Clerk clerk) {
    	        this.clerk = clerk;
    	    }
    	
    	    @Override
    	    public void run() {
    	
    	        System.out.println(Thread.currentThread().getName()+":开始消费产品");
    	
    	        while(true){
    	            try {
    	                Thread.sleep(1);
    	            } catch (InterruptedException e) {
    	                e.printStackTrace();
    	            }
    	
    	            clerk.consumeProduct();
    	        }
    	
    	    }
    	}
    	
    	public class ProductTest {
    	
    	    public static void main(String[] args){
    	        Clerk clerk = new Clerk();
    	
    	        Producer p1 = new Producer(clerk);
    	        p1.setName("生产者1");
    	
    	        Consumer c1 = new Consumer(clerk);
    	        Thread t1 = new Thread(c1);
    	        t1.setName("消费者1");
    	
    	        p1.start();
    	        t1.start();
    	
    	    }
    	
    	}
    
    展开全文
  • 50个多线程面试题,你会多少?

    万次阅读 多人点赞 2019-10-15 15:14:04
    下面是Java线程相关的热门面试题,你可以用它来好好准备面试。 什么是线程? 什么是线程安全和线程不安全? 什么是自旋锁? 什么是Java内存模型? 什么是CAS? 什么是乐观锁和悲观锁? 什么是AQS? 什么是...

    下面是Java线程相关的热门面试题,你可以用它来好好准备面试。

    1. 什么是线程?
    2. 什么是线程安全和线程不安全?
    3. 什么是自旋锁?
    4. 什么是Java内存模型?
    5. 什么是CAS?
    6. 什么是乐观锁和悲观锁?
    7. 什么是AQS?
    8. 什么是原子操作?在Java Concurrency API中有哪些原子类(atomic classes)?
    9. 什么是Executors框架?
    10. 什么是阻塞队列?如何使用阻塞队列来实现生产者-消费者模型?
    11. 什么是Callable和Future?
    12. 什么是FutureTask?
    13. 什么是同步容器和并发容器的实现?
    14. 什么是多线程?优缺点?
    15. 什么是多线程的上下文切换?
    16. ThreadLocal的设计理念与作用?
    17. ThreadPool(线程池)用法与优势?
    18. Concurrent包里的其他东西:ArrayBlockingQueue、CountDownLatch等等。
    19. synchronized和ReentrantLock的区别?
    20. Semaphore有什么作用?
    21. Java Concurrency API中的Lock接口(Lock interface)是什么?对比同步它有什么优势?
    22. Hashtable的size()方法中明明只有一条语句”return count”,为什么还要做同步?
    23. ConcurrentHashMap的并发度是什么?
    24. ReentrantReadWriteLock读写锁的使用?
    25. CyclicBarrier和CountDownLatch的用法及区别?
    26. LockSupport工具?
    27. Condition接口及其实现原理?
    28. Fork/Join框架的理解?
    29. wait()和sleep()的区别?
    30. 线程的五个状态(五种状态,创建、就绪、运行、阻塞和死亡)?
    31. start()方法和run()方法的区别?
    32. Runnable接口和Callable接口的区别?
    33. volatile关键字的作用?
    34. Java中如何获取到线程dump文件?
    35. 线程和进程有什么区别?
    36. 线程实现的方式有几种(四种)?
    37. 高并发、任务执行时间短的业务怎样使用线程池?并发不高、任务执行时间长的业务怎样使用线程池?并发高、业务执行时间长的业务怎样使用线程池?
    38. 如果你提交任务时,线程池队列已满,这时会发生什么?
    39. 锁的等级:方法锁、对象锁、类锁?
    40. 如果同步块内的线程抛出异常会发生什么?
    41. 并发编程(concurrency)并行编程(parallellism)有什么区别?
    42. 如何保证多线程下 i++ 结果正确?
    43. 一个线程如果出现了运行时异常会怎么样?
    44. 如何在两个线程之间共享数据?
    45. 生产者消费者模型的作用是什么?
    46. 怎么唤醒一个阻塞的线程?
    47. Java中用到的线程调度算法是什么
    48. 单例模式的线程安全性?
    49. 线程类的构造方法、静态块是被哪个线程调用的?
    50. 同步方法和同步块,哪个是更好的选择?
    51. 如何检测死锁?怎么预防死锁?

    什么是线程?

    线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位,可以使用多线程对进行运算提速。

    比如,如果一个线程完成一个任务要100毫秒,那么用十个线程完成改任务只需10毫秒

    什么是线程安全和线程不安全?

    通俗的说:加锁的就是是线程安全的,不加锁的就是是线程不安全的

    线程安全

    线程安全: 就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问,直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染

    一个线程安全的计数器类的同一个实例对象在被多个线程使用的情况下也不会出现计算失误。很显然你可以将集合类分成两组,线程安全和非线程安全的。Vector 是用同步方法来实现线程安全的, 而和它相似的ArrayList不是线程安全的。

    线程不安全

    线程不安全:就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据

    如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。

    线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。

    什么是自旋锁?

    基本概念

    自旋锁是SMP架构中的一种low-level的同步机制

    当线程A想要获取一把自旋锁而该锁又被其它线程锁持有时,线程A会在一个循环中自旋以检测锁是不是已经可用了。

    锁需要注意

    • 由于自旋时不释放CPU,因而持有自旋锁的线程应该尽快释放自旋锁,否则等待该自旋锁的线程会一直在那里自旋,这就会浪费CPU时间。
    • 持有自旋锁的线程在sleep之前应该释放自旋锁以便其它线程可以获得自旋锁

    实现自旋锁

    参考

    segmentfault.com/q/101000000…

    一个简单的while就可以满足你的要求。

    目前的JVM实现自旋会消耗CPU,如果长时间不调用doNotify方法,doWait方法会一直自旋,CPU会消耗太大。

    public class MyWaitNotify3{
    
      MonitorObject myMonitorObject = new MonitorObject();
      boolean wasSignalled = false;
    
      public void doWait(){
        synchronized(myMonitorObject){
          while(!wasSignalled){
            try{
              myMonitorObject.wait();
             } catch(InterruptedException e){...}
          }
          //clear signal and continue running.
          wasSignalled = false;
        }
      }
    
      public void doNotify(){
        synchronized(myMonitorObject){
          wasSignalled = true;
          myMonitorObject.notify();
        }
      }
    }
    

    什么是Java内存模型?

    Java内存模型描述了在多线程代码中哪些行为是合法的,以及线程如何通过内存进行交互。它描述了“程序中的变量“ 和 ”从内存或者寄存器获取或存储它们的底层细节”之间的关系。Java内存模型通过使用各种各样的硬件和编译器的优化来正确实现以上事情

    Java包含了几个语言级别的关键字,包括:volatile, final以及synchronized,目的是为了帮助程序员向编译器描述一个程序的并发需求。Java内存模型定义了volatile和synchronized的行为,更重要的是保证了同步的java程序在所有的处理器架构下面都能正确的运行。

    “一个线程的写操作对其他线程可见”这个问题是因为编译器对代码进行重排序导致的。例如,只要代码移动不会改变程序的语义,当编译器认为程序中移动一个写操作到后面会更有效的时候,编译器就会对代码进行移动。如果编译器推迟执行一个操作,其他线程可能在这个操作执行完之前都不会看到该操作的结果,这反映了缓存的影响。

    此外,写入内存的操作能够被移动到程序里更前的时候。在这种情况下,其他的线程在程序中可能看到一个比它实际发生更早的写操作。所有的这些灵活性的设计是为了通过给编译器,运行时或硬件灵活性使其能在最佳顺序的情况下来执行操作。在内存模型的限定之内,我们能够获取到更高的性能。

    看下面代码展示的一个简单例子:

    ClassReordering {
        
        int x = 0, y = 0;
       
        public void writer() {
            x = 1;
            y = 2;
        }
    
        public void reader() {
            int r1 = y;
            int r2 = x;
        }
    }
    

    让我们看在两个并发线程中执行这段代码,读取Y变量将会得到2这个值。因为这个写入比写到X变量更晚一些,程序员可能认为读取X变量将肯定会得到1。但是,写入操作可能被重排序过。如果重排序发生了,那么,就能发生对Y变量的写入操作,读取两个变量的操作紧随其后,而且写入到X这个操作能发生。程序的结果可能是r1变量的值是2,但是r2变量的值为0。

    但是面试官,有时候不这么认为,认为就是JVM内存结构

    JVM内存结构主要有三大块:堆内存、方法区和栈

    堆内存是JVM中最大的一块由年轻代和老年代组成,而年轻代内存又被分成三部分,Eden空间、From Survivor空间、To Survivor空间,默认情况下年轻代按照8:1:1的比例来分配;方法区存储类信息、常量、静态变量等数据,是线程共享的区域,为与Java堆区分,方法区还有一个别名Non-Heap(非堆);栈又分为java虚拟机栈和本地方法栈主要用于方法的执行。

    JAVA的JVM的内存可分为3个区:堆(heap)、栈(stack)和方法区(method)

    java堆(Java Heap)

    • 可通过参数 -Xms 和-Xmx设置
    1. Java堆是被所有线程共享,是Java虚拟机所管理的内存中最大的一块 Java堆在虚拟机启动时创建
    2. Java堆唯一的目的是存放对象实例,几乎所有的对象实例和数组都在这里
    3. Java堆为了便于更好的回收和分配内存,可以细分为:新生代和老年代;再细致一点的有Eden空间、From Survivor空间、To Survivor区
    • 新生代:包括Eden区、From Survivor区、To Survivor区,系统默认大小Eden:Survivor=8:1。
    • 老年代:在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
    1. Survivor空间等Java堆可以处在物理上不连续的内存空间中,只要逻辑上是连续的即可(就像我们的磁盘空间一样。在实现时,既可以实现成固定大小的,也可以是可扩展的)。

    据Java虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常

    java虚拟机栈(stack)

    可通过参数 栈帧是方法运行期的基础数据结构栈容量可由-Xss设置

    1.Java虚拟机栈是线程私有的,它的生命周期与线程相同

    1. 每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
    2. 虚拟机栈是执行Java方法的内存模型(也就是字节码)服务:每个方法在执行的同时都会创建一个栈帧用于存储 局部变量表操作数栈动态链接方法出口等信息。
    • 局部变量表:32位变量槽,存放了编译期可知的各种基本数据类型、对象引用、returnAddress类型
    • 操作数栈:基于栈的执行引擎,虚拟机把操作数栈作为它的工作区,大多数指令都要从这里弹出数据、执行运算,然后把结果压回操作数栈。
    • 动态连接每个栈帧都包含一个指向运行时常量池(方法区的一部分)中该栈帧所属方法的引用。持有这个引用是为了支持方法调用过程中的动态连接。Class文件的常量池中有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用一部分会在类加载阶段或第一次使用的时候转化为直接引用,这种转化称为静态解析。另一部分将在每一次的运行期间转化为直接应用,这部分称为动态连接
    • 方法出口:返回方法被调用的位置,恢复上层方法的局部变量和操作数栈,如果无返回值,则把它压入调用者的操作数栈。
    1. 局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的。

    2. 在方法运行期间不会改变局部变量表的大小。主要存放了编译期可知的各种基本数据类型、对象引用 (reference类型)、returnAddress类型)

    java虚拟机栈,规定了两种异常状况:

    1. 如果线程请求的深度大于虚拟机所允许的深度,将抛出StackOverflowError异常
    2. 如果虚拟机栈动态扩展,而扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常

    本地方法栈

    可通过参数 栈容量可由-Xss设置

    1. 虚拟机栈为虚拟机执行Java方法(也就是字节码)服务。
    2. 本地方法栈则是为虚拟机使用到的Native方法服务。有的虚拟机(譬如Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一

    方法区(Method Area)

    可通过参数-XX:MaxPermSize设置

    1. 线程共享内存区域,用于储存已被虚拟机加载的类信息、常量、静态变量,即编译器编译后的代码,方法区也称持久代(Permanent Generation)

    2. 虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。

    3. 如何实现方法区,属于虚拟机的实现细节,不受虚拟机规范约束。

    4. 方法区主要存放java类定义信息,与垃圾回收关系不大,方法区可以选择不实现垃圾回收,但不是没有垃圾回收。

    5. 方法区域的内存回收目标主要是针对常量池的回收和对类型的卸载

    6. 运行时常量池,也是方法区的一部分,虚拟机加载Class后把常量池中的数据放入运行时常量池

    运行时常量池

    JDK1.6之前字符串常量池位于方法区之中JDK1.7字符串常量池已经被挪到堆之中

    可通过参数-XX:PermSize和-XX:MaxPermSize设置

    • 常量池(Constant Pool):常量池数据编译期被确定,是Class文件中的一部分。存储了类、方法、接口等中的常量,当然也包括字符串常量
    • 字符串池/字符串常量池(String Pool/String Constant Pool):是常量池中的一部分,存储编译期类中产生的字符串类型数据。
    • 运行时常量池(Runtime Constant Pool):方法区的一部分,所有线程共享。虚拟机加载Class后把常量池中的数据放入到运行时常量池。常量池:可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目资源关联最多的数据类型。
    1. 常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic Reference)。

    2. 字面量:文本字符串、声明为final的常量值等。

    3. 符号引用:类和接口的完全限定名(Fully Qualified Name)、字段的名称和描述符(Descriptor)、方法的名称和描述符。

    直接内存

    可通过-XX:MaxDirectMemorySize指定,如果不指定,则默认与Java堆的最大值(-Xmx指定)一样

    • 直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现

    总结的简单一点

    java堆(Java Heap)

    可通过参数 -Xms 和-Xmx设置

    1. Java堆是被所有线程共享,是Java虚拟机所管理的内存中最大的一块 Java堆在虚拟机启动时创建
    2. Java堆唯一的目的是存放对象实例,几乎所有的对象实例和数组都在这里
    3. Java堆为了便于更好的回收和分配内存,可以细分为:新生代和老年代;再细致一点的有Eden空间、From Survivor空间、To Survivor区
    • 新生代:包括Eden区、From Survivor区、To Survivor区,系统默认大小Eden:Survivor=8:1。
    • 老年代:在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。

    java虚拟机栈(stack)

    可通过参数 栈帧是方法运行期的基础数据结构栈容量可由-Xss设置

    1. Java虚拟机栈是线程私有的,它的生命周期与线程相同
    2. 每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
    3. 虚拟机栈是执行Java方法的内存模型(也就是字节码)服务:每个方法在执行的同时都会创建一个栈帧,用于存储 局部变量表操作数栈动态链接方法出口等信息

    方法区(Method Area)

    可通过参数-XX:MaxPermSize设置

    1. 线程共享内存区域),用于储存已被虚拟机加载的类信息、常量、静态变量,即编译器编译后的代码方法区也称持久代(Permanent Generation)

    2. 方法区主要存放java类定义信息,与垃圾回收关系不大,方法区可以选择不实现垃圾回收,但不是没有垃圾回收。

    3. 方法区域的内存回收目标主要是针对常量池的回收和对类型的卸载。

    4. 运行时常量池,也是方法区的一部分,虚拟机加载Class后把常量池中的数据放入运行时常量池

    什么是CAS?

    CAS(compare and swap)的缩写,中文翻译成比较并交换

    CAS 不通过JVM,直接利用java本地方 JNI(Java Native Interface为JAVA本地调用),直接调用CPU 的cmpxchg(是汇编指令)指令。

    利用CPU的CAS指令,同时借助JNI来完成Java的非阻塞算法,实现原子操作。其它原子操作都是利用类似的特性完成的

    整个java.util.concurrent都是建立在CAS之上的,因此对于synchronized阻塞算法,J.U.C在性能上有了很大的提升。

    CAS是项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。

    CAS应用

    CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

    CAS优点

    确保对内存的读-改-写操作都是原子操作执行

    CAS缺点

    CAS虽然很高效的解决原子操作,但是CAS仍然存在三大问题。ABA问题,循环时间长开销大和只能保证一个共享变量的原子操作

    总结

    1. 使用CAS在线程冲突严重时,会大幅降低程序性能;CAS只适合于线程冲突较少的情况使用
    2. synchronized在jdk1.6之后,已经改进优化。synchronized的底层实现主要依靠Lock-Free的队列,基本思路是自旋后阻塞,竞争切换后继续竞争锁,稍微牺牲了公平性,但获得了高吞吐量。在线程冲突较少的情况下,可以获得和CAS类似的性能;而线程冲突严重的情况下,性能远高于CAS

    参考blog.52itstyle.com/archives/94…

    什么是乐观锁和悲观锁?

    悲观锁

    Java在JDK1.5之前都是靠synchronized关键字保证同步的,这种通过使用一致的锁定协议来协调对共享状态的访问,可以确保无论哪个线程持有共享变量的锁,都采用独占的方式来访问这些变量。独占锁其实就是一种悲观锁,所以可以说synchronized是悲观锁。

    乐观锁

    乐观锁( Optimistic Locking)其实是一种思想。相对悲观锁而言,乐观锁假设认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则让返回用户错误的信息,让用户决定如何去做。

    什么是AQS?

    AbstractQueuedSynchronizer简称AQS,是一个用于构建锁和同步容器的框架。事实上concurrent包内许多类都是基于AQS构建,例如ReentrantLock,Semaphore,CountDownLatch,ReentrantReadWriteLock,FutureTask等。AQS解决了在实现同步容器时设计的大量细节问题。

    AQS使用一个FIFO的队列表示排队等待锁的线程,队列头节点称作“哨兵节点”或者“哑节点”,它不与任何线程关联。其他的节点与等待线程关联,每个节点维护一个等待状态waitStatus。

    CAS 原子操作在concurrent包的实现

    参考blog.52itstyle.com/archives/94…

    由于java的CAS同时具有 volatile 读和volatile写的内存语义,因此Java线程之间的通信现在有了下面四种方式:

    • A线程写volatile变量,随后B线程读这个volatile变量。
    • A线程写volatile变量,随后B线程用CAS更新这个volatile变量。
    • A线程用CAS更新一个volatile变量,随后B线程用CAS更新这个volatile变量。
    • A线程用CAS更新一个volatile变量,随后B线程读这个volatile变量。

    Java的CAS会使用现代处理器上提供的高效机器级别原子指令,这些原子指令以原子方式对内存执行读-改-写操作,这是在多处理器中实现同步的关键(从本质上来说,能够支持原子性读-改-写指令的计算机器,是顺序计算图灵机的异步等价机器,因此任何现代的多处理器都会去支持某种能对内存执行原子性读-改-写操作的原子指令)。同时,volatile变量的读/写和CAS可以实现线程之间的通信。把这些特性整合在一起,就形成了整个concurrent包得以实现的基石。如果我们仔细分析concurrent包的源代码实现,会发现一个通用化的实现模式:

    首先,声明共享变量为volatile;然后,使用CAS的原子条件更新来实现线程之间的同步;

    同时,配合以volatile的读/写和CAS所具有的volatile读和写的内存语义来实现线程之间的通信。

    AQS,非阻塞数据结构和原子变量类(Java.util.concurrent.atomic包中的类),这些concurrent包中的基础类都是使用这种模式来实现的,而concurrent包中的高层类又是依赖于这些基础类来实现的。从整体来看,concurrent包的实现示意图如下:

     

    image

     

    AQS没有锁之类的概念,它有个state变量,是个int类型,在不同场合有着不同含义。

    AQS围绕state提供两种基本操作“获取”和“释放”,有条双向队列存放阻塞的等待线程,并提供一系列判断和处理方法,简单说几点:

    • state是独占的,还是共享的;
    • state被获取后,其他线程需要等待;
    • state被释放后,唤醒等待线程;
    • 线程等不及时,如何退出等待。

    至于线程是否可以获得state,如何释放state,就不是AQS关心的了,要由子类具体实现。

    AQS中还有一个表示状态的字段state,例如ReentrantLocky用它表示线程重入锁的次数,Semaphore用它表示剩余的许可数量,FutureTask用它表示任务的状态。对state变量值的更新都采用CAS操作保证更新操作的原子性

    AbstractQueuedSynchronizer继承了AbstractOwnableSynchronizer,这个类只有一个变量:exclusiveOwnerThread,表示当前占用该锁的线程,并且提供了相应的get,set方法。

    ReentrantLock实现原理

    www.cnblogs.com/maypattis/p…

    什么是原子操作?在Java Concurrency API中有哪些原子类(atomic classes)?

    原子操作是指一个不受其他操作影响的操作任务单元。原子操作是在多线程环境下避免数据不一致必须的手段。

    int++并不是一个原子操作,所以当一个线程读取它的值并加1时,另外一个线程有可能会读到之前的值,这就会引发错误。

    为了解决这个问题,必须保证增加操作是原子的,在JDK1.5之前我们可以使用同步技术来做到这一点。

    到JDK1.5,java.util.concurrent.atomic包提供了int和long类型的装类,它们可以自动的保证对于他们的操作是原子的并且不需要使用同步。  

    什么是Executors框架?

    Executor框架同java.util.concurrent.Executor 接口在Java 5中被引入。

    Executor框架是一个根据一组执行策略调用,调度,执行和控制的异步任务的框架。

    无限制的创建线程会引起应用程序内存溢出。所以创建一个线程池是个更好的的解决方案,因为可以限制线程的数量并且可以回收再利用这些线程。

    利用Executors框架可以非常方便的创建一个线程池,

    Java通过Executors提供四种线程池,分别为:

    newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

    newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

    newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。

    newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。  

    什么是阻塞队列?如何使用阻塞队列来实现生产者-消费者模型?

    JDK7提供了7个阻塞队列。(也属于并发容器)

    1. ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。
    2. LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列。
    3. PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。
    4. DelayQueue:一个使用优先级队列实现的无界阻塞队列。
    5. SynchronousQueue:一个不存储元素的阻塞队列。
    6. LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
    7. LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。

    什么是阻塞队列?

    阻塞队列是一个在队列基础上又支持了两个附加操作的队列。

    2个附加操作:

    支持阻塞的插入方法:队列满时,队列会阻塞插入元素的线程,直到队列不满。支持阻塞的移除方法:队列空时,获取元素的线程会等待队列变为非空。

    阻塞队列的应用场景

    阻塞队列常用于生产者和消费者的场景,生产者是向队列里添加元素的线程,消费者是从队列里取元素的线程。简而言之,阻塞队列是生产者用来存放元素、消费者获取元素的容器。

    几个方法

    在阻塞队列不可用的时候,上述2个附加操作提供了四种处理方法

    方法\处理方式 抛出异常 返回特殊值 一直阻塞 超时退出
    插入方法 add(e) offer(e) put(e) offer(e,time,unit)
    移除方法 remove() poll() take() poll(time,unit)
    检查方法 element() peek() 不可用 不可用

    JAVA里的阻塞队列

    JDK 7 提供了7个阻塞队列,如下

    1、ArrayBlockingQueue 数组结构组成的有界阻塞队列。

    此队列按照先进先出(FIFO)的原则对元素进行排序,但是默认情况下不保证线程公平的访问队列,即如果队列满了,那么被阻塞在外面的线程对队列访问的顺序是不能保证线程公平(即先阻塞,先插入)的。

    2、LinkedBlockingQueue一个由链表结构组成的有界阻塞队列

    此队列按照先出先进的原则对元素进行排序

    3、PriorityBlockingQueue支持优先级的无界阻塞队列

    4、DelayQueue支持延时获取元素的无界阻塞队列,即可以指定多久才能从队列中获取当前元素

    5、SynchronousQueue不存储元素的阻塞队列,每一个put必须等待一个take操作,否则不能继续添加元素。并且他支持公平访问队列。

    6、LinkedTransferQueue由链表结构组成的无界阻塞TransferQueue队列。相对于其他阻塞队列,多了tryTransfer和transfer方法

    transfer方法

    如果当前有消费者正在等待接收元素(take或者待时间限制的poll方法),transfer可以把生产者传入的元素立刻传给消费者。如果没有消费者等待接收元素,则将元素放在队列的tail节点,并等到该元素被消费者消费了才返回。

    tryTransfer方法

    用来试探生产者传入的元素能否直接传给消费者。,如果没有消费者在等待,则返回false。和上述方法的区别是该方法无论消费者是否接收,方法立即返回。而transfer方法是必须等到消费者消费了才返回。

    7、LinkedBlockingDeque链表结构的双向阻塞队列,优势在于多线程入队时,减少一半的竞争。

    如何使用阻塞队列来实现生产者-消费者模型?

    通知模式实现:所谓通知模式,就是当生产者往满的队列里添加元素时会阻塞住生产者,当消费者消费了一个队列中的元素后,会通知生产者当前队列可用。

    使用BlockingQueue解决生产者消费者问题

    为什么BlockingQueue适合解决生产者消费者问题

    任何有效的生产者-消费者问题解决方案都是通过控制生产者put()方法(生产资源)和消费者take()方法(消费资源)的调用来实现的,一旦你实现了对方法的阻塞控制,那么你将解决该问题。

    Java通过BlockingQueue提供了开箱即用的支持来控制这些方法的调用(一个线程创建资源,另一个消费资源)。java.util.concurrent包下的BlockingQueue接口是一个线程安全的可用于存取对象的队列。

    BlockingQueue是一种数据结构,支持一个线程往里存资源,另一个线程从里取资源。这正是解决生产者消费者问题所需要的,那么让我们开始解决该问题吧。

    生产者

    以下代码用于生产者线程

    package io.ymq.example.thread;
    
    import java.util.concurrent.BlockingQueue;
    
    /**
     * 描述:生产者
     *
     * @author yanpenglei
     * @create 2018-03-14 15:52
     **/
    class Producer implements Runnable {
    
        protected BlockingQueue<Object> queue;
    
        Producer(BlockingQueue<Object> theQueue) {
            this.queue = theQueue;
        }
    
        public void run() {
            try {
                while (true) {
                    Object justProduced = getResource();
                    queue.put(justProduced);
                    System.out.println("生产者资源队列大小= " + queue.size());
                }
            } catch (InterruptedException ex) {
                System.out.println("生产者 中断");
            }
        }
    
        Object getResource() {
            try {
                Thread.sleep(100);
            } catch (InterruptedException ex) {
                System.out.println("生产者 读 中断");
            }
            return new Object();
        }
    }
    

    消费者

    以下代码用于消费者线程

    package io.ymq.example.thread;
    
    import java.util.concurrent.BlockingQueue;
    
    /**
     * 描述: 消费者
     *
     * @author yanpenglei
     * @create 2018-03-14 15:54
     **/
    class Consumer implements Runnable {
        protected BlockingQueue<Object> queue;
    
        Consumer(BlockingQueue<Object> theQueue) {
            this.queue = theQueue;
        }
    
        public void run() {
            try {
                while (true) {
                    Object obj = queue.take();
                    System.out.println("消费者 资源 队列大小 " + queue.size());
                    take(obj);
                }
            } catch (InterruptedException ex) {
                System.out.println("消费者 中断");
            }
        }
    
        void take(Object obj) {
            try {
                Thread.sleep(100); // simulate time passing
            } catch (InterruptedException ex) {
                System.out.println("消费者 读 中断");
            }
            System.out.println("消费对象 " + obj);
        }
    }
    

    测试该解决方案是否运行正常

    package io.ymq.example.thread;
    import java.util.concurrent.BlockingQueue;
    import java.util.concurrent.LinkedBlockingQueue;
    
    /**
     * 描述: 测试
     *
     * @author yanpenglei
     * @create 2018-03-14 15:58
     **/
    public class ProducerConsumerExample {
    
        public static void main(String[] args) throws InterruptedException {
    
            int numProducers = 4;
            int numConsumers = 3;
    
            BlockingQueue<Object> myQueue = new LinkedBlockingQueue<Object>(5);
    
            for (int i = 0; i < numProducers; i++) {
                new Thread(new Producer(myQueue)).start();
            }
    
            for (int i = 0; i < numConsumers; i++) {
                new Thread(new Consumer(myQueue)).start();
            }
    
            Thread.sleep(1000);
    
            System.exit(0);
        }
    }
    

    运行结果

    生产者资源队列大小= 1
    生产者资源队列大小= 1
    消费者 资源 队列大小 1
    生产者资源队列大小= 1
    消费者 资源 队列大小 1
    消费者 资源 队列大小 1
    生产者资源队列大小= 1
    生产者资源队列大小= 3
    消费对象 java.lang.Object@1e1aa52b
    生产者资源队列大小= 2
    生产者资源队列大小= 5
    消费对象 java.lang.Object@6e740a76
    消费对象 java.lang.Object@697853f6
    
    ......
    
    消费对象 java.lang.Object@41a10cbc
    消费对象 java.lang.Object@4963c8d1
    消费者 资源 队列大小 5
    生产者资源队列大小= 5
    生产者资源队列大小= 5
    消费者 资源 队列大小 4
    消费对象 java.lang.Object@3e49c35d
    消费者 资源 队列大小 4
    生产者资源队列大小= 5
    

    从输出结果中,我们可以发现队列大小永远不会超过5,消费者线程消费了生产者生产的资源

    什么是Callable和Future?

    Callable 和 Future 是比较有趣的一对组合。当我们需要获取线程的执行结果时,就需要用到它们。Callable用于产生结果,Future用于获取结果

    Callable接口使用泛型去定义它的返回类型。Executors类提供了一些有用的方法去在线程池中执行Callable内的任务。由于Callable任务是并行的,必须等待它返回的结果。java.util.concurrent.Future对象解决了这个问题。

    在线程池提交Callable任务后返回了一个Future对象,使用它可以知道Callable任务的状态和得到Callable返回的执行结果。Future提供了get()方法,等待Callable结束并获取它的执行结果。

    代码示例

    Callable 是一个接口,它只包含一个call()方法。Callable是一个返回结果并且可能抛出异常的任务

    为了便于理解,我们可以将Callable比作一个Runnable接口,而Callable的call()方法则类似于Runnable的run()方法

    public class CallableFutureTest {
    
        public static void main(String[] args) throws InterruptedException, ExecutionException {
    
            System.out.println("start main thread ");
    
            ExecutorService exec = Executors.newFixedThreadPool(2);
    
            //新建一个Callable 任务,并将其提交到一个ExecutorService. 将返回一个描述任务情况的Future.
            Callable<String> call = new Callable<String>() {
    
                @Override
                public String call() throws Exception {
                    System.out.println("start new thread ");
                    Thread.sleep(5000);
                    System.out.println("end new thread ");
                    return "我是返回的内容";
                }
            };
    
            Future<String> task = exec.submit(call);
            Thread.sleep(1000);
            String retn = task.get();
            //关闭线程池
            exec.shutdown();
            System.out.println(retn + "--end main thread");
        }
    }
    

    控制台打印

    start main thread 
    start new thread 
    end new thread 
    我是返回的内容--end main thread
    

    什么是FutureTask?

    FutureTask可用于异步获取执行结果或取消执行任务的场景。通过传入Runnable或者Callable的任务给FutureTask,直接调用其run方法或者放入线程池执行,之后可以在外部通过FutureTask的get方法异步获取执行结果,因此,FutureTask非常适合用于耗时的计算,主线程可以在完成自己的任务后,再去获取结果。另外,FutureTask还可以确保即使调用了多次run方法,它都只会执行一次Runnable或者Callable任务,或者通过cancel取消FutureTask的执行等。

    1.执行多任务计算

    FutureTask执行多任务计算的使用场景

    利用FutureTask和ExecutorService,可以用多线程的方式提交计算任务,主线程继续执行其他任务,当主线程需要子线程的计算结果时,在异步获取子线程的执行结果。

    import java.util.ArrayList;
    import java.util.List;
    import java.util.concurrent.*;
    
    public class FutureTaskForMultiCompute {
    
        public static void main(String[] args) {
    
            FutureTaskForMultiCompute inst = new FutureTaskForMultiCompute();
            // 创建任务集合
            List<FutureTask<Integer>> taskList = new ArrayList<FutureTask<Integer>>();
            // 创建线程池
            ExecutorService exec = Executors.newFixedThreadPool(5);
            for (int i = 0; i < 10; i++) {
                // 传入Callable对象创建FutureTask对象
                FutureTask<Integer> ft = new FutureTask<Integer>(inst.new ComputeTask(i, "" + i));
                taskList.add(ft);
                // 提交给线程池执行任务,也可以通过exec.invokeAll(taskList)一次性提交所有任务;
                exec.submit(ft);
            }
    
            System.out.println("所有计算任务提交完毕, 主线程接着干其他事情!");
    
            // 开始统计各计算线程计算结果
            Integer totalResult = 0;
            for (FutureTask<Integer> ft : taskList) {
                try {
                    //FutureTask的get方法会自动阻塞,直到获取计算结果为止
                    totalResult = totalResult + ft.get();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (ExecutionException e) {
                    e.printStackTrace();
                }
            }
    
            // 关闭线程池
            exec.shutdown();
            System.out.println("多任务计算后的总结果是:" + totalResult);
    
        }
    
        private class ComputeTask implements Callable<Integer> {
    
            private Integer result = 0;
            private String taskName = "";
    
            public ComputeTask(Integer iniResult, String taskName) {
                result = iniResult;
                this.taskName = taskName;
                System.out.println("生成子线程计算任务: " + taskName);
            }
    
            public String getTaskName() {
                return this.taskName;
            }
    
            @Override
            public Integer call() throws Exception {
                // TODO Auto-generated method stub
    
                for (int i = 0; i < 100; i++) {
                    result = +i;
                }
                // 休眠5秒钟,观察主线程行为,预期的结果是主线程会继续执行,到要取得FutureTask的结果是等待直至完成。
                Thread.sleep(5000);
                System.out.println("子线程计算任务: " + taskName + " 执行完成!");
                return result;
            }
        }
    }
    
    生成子线程计算任务: 0
    生成子线程计算任务: 1
    生成子线程计算任务: 2
    生成子线程计算任务: 3
    生成子线程计算任务: 4
    生成子线程计算任务: 5
    生成子线程计算任务: 6
    生成子线程计算任务: 7
    生成子线程计算任务: 8
    生成子线程计算任务: 9
    所有计算任务提交完毕, 主线程接着干其他事情!
    子线程计算任务: 0 执行完成!
    子线程计算任务: 2 执行完成!
    子线程计算任务: 3 执行完成!
    子线程计算任务: 4 执行完成!
    子线程计算任务: 1 执行完成!
    子线程计算任务: 8 执行完成!
    子线程计算任务: 7 执行完成!
    子线程计算任务: 6 执行完成!
    子线程计算任务: 9 执行完成!
    子线程计算任务: 5 执行完成!
    多任务计算后的总结果是:990
    

    2.高并发环境下

    FutureTask在高并发环境下确保任务只执行一次

    在很多高并发的环境下,往往我们只需要某些任务只执行一次。这种使用情景FutureTask的特性恰能胜任。举一个例子,假设有一个带key的连接池,当key存在时,即直接返回key对应的对象;当key不存在时,则创建连接。对于这样的应用场景,通常采用的方法为使用一个Map对象来存储key和连接池对应的对应关系,典型的代码如下面所示:

      private Map<String, Connection> connectionPool = new HashMap<String, Connection>();
        private ReentrantLock lock = new ReentrantLock();
    
        public Connection getConnection(String key) {
            try {
                lock.lock();
                if (connectionPool.containsKey(key)) {
                    return connectionPool.get(key);
                } else {
                    //创建 Connection  
                    Connection conn = createConnection();
                    connectionPool.put(key, conn);
                    return conn;
                }
            } finally {
                lock.unlock();
            }
        }
    
        //创建Connection  
        private Connection createConnection() {
            return null;
        }
    
    

    在上面的例子中,我们通过加锁确保高并发环境下的线程安全,也确保了connection只创建一次,然而确牺牲了性能。改用ConcurrentHash的情况下,几乎可以避免加锁的操作,性能大大提高,但是在高并发的情况下有可能出现Connection被创建多次的现象。这时最需要解决的问题就是当key不存在时,创建Connection的动作能放在connectionPool之后执行,这正是FutureTask发挥作用的时机,基于ConcurrentHashMap和FutureTask的改造代码如下:

      private ConcurrentHashMap<String, FutureTask<Connection>> connectionPool = new ConcurrentHashMap<String, FutureTask<Connection>>();
    
        public Connection getConnection(String key) throws Exception {
            FutureTask<Connection> connectionTask = connectionPool.get(key);
            if (connectionTask != null) {
                return connectionTask.get();
            } else {
                Callable<Connection> callable = new Callable<Connection>() {
                    @Override
                    public Connection call() throws Exception {
                        // TODO Auto-generated method stub  
                        return createConnection();
                    }
                };
                FutureTask<Connection> newTask = new FutureTask<Connection>(callable);
                connectionTask = connectionPool.putIfAbsent(key, newTask);
                if (connectionTask == null) {
                    connectionTask = newTask;
                    connectionTask.run();
                }
                return connectionTask.get();
            }
        }
    
        //创建Connection  
        private Connection createConnection() {
            return null;
        }
    

    经过这样的改造,可以避免由于并发带来的多次创建连接及锁的出现。

    什么是同步容器和并发容器的实现?

    一、同步容器

    主要代表有Vector和Hashtable,以及Collections.synchronizedXxx等。锁的粒度为当前对象整体。迭代器是及时失败的,即在迭代的过程中发现被修改,就会抛出ConcurrentModificationException。

    二、并发容器

    主要代表有ConcurrentHashMap、CopyOnWriteArrayList、ConcurrentSkipListMap、ConcurrentSkipListSet。锁的粒度是分散的、细粒度的,即读和写是使用不同的锁。迭代器具有弱一致性,即可以容忍并发修改,不会抛出ConcurrentModificationException。

    JDK 7 ConcurrentHashMap

    采用分离锁技术,同步容器中,是一个容器一个锁,但在ConcurrentHashMap中,会将hash表的数组部分分成若干段,每段维护一个锁,以达到高效的并发访问;

    JDK 8 ConcurrentHashMap

    采用分离锁技术,同步容器中,是一个容器一个锁,但在ConcurrentHashMap中,会将hash表的数组部分分成若干段,每段维护一个锁,以达到高效的并发访问;

    三、阻塞队列

    主要代表有LinkedBlockingQueue、ArrayBlockingQueue、PriorityBlockingQueue(Comparable,Comparator)、SynchronousQueue。提供了可阻塞的put和take方法,以及支持定时的offer和poll方法。适用于生产者、消费者模式(线程池和工作队列-Executor),同时也是同步容器

    四、双端队列

    主要代表有ArrayDeque和LinkedBlockingDeque。意义:正如阻塞队列适用于生产者消费者模式,双端队列同样适用与另一种模式,即工作密取。在生产者-消费者设计中,所有消费者共享一个工作队列,而在工作密取中,每个消费者都有各自的双端队列。如果一个消费者完成了自己双端队列中的全部工作,那么他就可以从其他消费者的双端队列末尾秘密的获取工作。具有更好的可伸缩性,这是因为工作者线程不会在单个共享的任务队列上发生竞争。在大多数时候,他们都只是访问自己的双端队列,从而极大的减少了竞争。当工作者线程需要访问另一个队列时,它会从队列的尾部而不是头部获取工作,因此进一步降低了队列上的竞争。适用于:网页爬虫等任务中

    五、比较及适用场景

    如果不需要阻塞队列,优先选择ConcurrentLinkedQueue;如果需要阻塞队列,队列大小固定优先选择ArrayBlockingQueue,队列大小不固定优先选择LinkedBlockingQueue;如果需要对队列进行排序,选择PriorityBlockingQueue;如果需要一个快速交换的队列,选择SynchronousQueue;如果需要对队列中的元素进行延时操作,则选择DelayQueue。

    什么是多线程?优缺点?

    什么是多线程?

    多线程:是指从软件或者硬件上实现多个线程的并发技术。

    多线程的好处:

    1. 使用多线程可以把程序中占据时间长的任务放到后台去处理,如图片、视屏的下载
    2. 发挥多核处理器的优势,并发执行让系统运行的更快、更流畅,用户体验更好

    多线程的缺点:

    1. 大量的线程降低代码的可读性;
    2. 更多的线程需要更多的内存空间
    3. 当多个线程对同一个资源出现争夺时候要注意线程安全的问题。

    什么是多线程的上下文切换?

    即使是单核CPU也支持多线程执行代码,CPU通过给每个线程分配CPU时间片来实现这个机制。时间片是CPU分配给各个线程的时间,因为时间片非常短,所以CPU通过不停地切换线程执行,让我们感觉多个线程时同时执行的,时间片一般是几十毫秒(ms)

    上下文切换过程中,CPU会停止处理当前运行的程序,并保存当前程序运行的具体位置以便之后继续运行

    CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再次加载这个任务的状态

    • 从任务保存到再加载的过程就是一次上下文切换

    ThreadLocal的设计理念与作用?

    Java中的ThreadLocal类允许我们创建只能被同一个线程读写的变量。因此,如果一段代码含有一个ThreadLocal变量的引用,即使两个线程同时执行这段代码,它们也无法访问到对方的ThreadLocal变量

    ThreadLocal

    如何创建ThreadLocal变量

    以下代码展示了如何创建一个ThreadLocal变量:

    private ThreadLocal myThreadLocal = new ThreadLocal();
    

    通过这段代码实例化了一个ThreadLocal对象。我们只需要实例化对象一次,并且也不需要知道它是被哪个线程实例化。虽然所有的线程都能访问到这个ThreadLocal实例,但是每个线程却只能访问到自己通过调用ThreadLocal的set()方法设置的值。即使是两个不同的线程在同一个ThreadLocal对象上设置了不同的值,他们仍然无法访问到对方的值

    如何访问ThreadLocal变量

    一旦创建了一个ThreadLocal变量,你可以通过如下代码设置某个需要保存的值:

    myThreadLocal.set("A thread local value”);
    

    可以通过下面方法读取保存在ThreadLocal变量中的值:

    String threadLocalValue = (String) myThreadLocal.get();
    

    get()方法返回一个Object对象,set()对象需要传入一个Object类型的参数。

    为ThreadLocal指定泛型类型

    public static ThreadLocal<String> myThreadLocal = new ThreadLocal<String>();
    

    我们可以创建一个指定泛型类型的ThreadLocal对象,这样我们就不需要每次对使用get()方法返回的值作强制类型转换了。下面展示了指定泛型类型的ThreadLocal例子:

    ThreadLocal的设计理念与作用

    http://blog.csdn.net/u011860731/article/details/48733073http://blog.csdn.net/u011860731/article/details/48733073)

    InheritableThreadLocal

    public static ThreadLocal<Integer> threadLocal = new InheritableThreadLocal<Integer>();
    

    InheritableThreadLocal类是ThreadLocal类的子类。ThreadLocal中每个线程拥有它自己的值,与ThreadLocal不同的是,InheritableThreadLocal允许一个线程以及该线程创建的所有子线程都可以访问它保存的值

    InheritableThreadLocal 原理

    Java 多线程:InheritableThreadLocal 实现原理

    blog.csdn.net/ni357103403…

    ThreadPool(线程池)用法与优势?

    为什么要用线程池:

    1. 减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
    2. 可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
    3. Java里面线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是ExecutorService。

    new Thread 缺点

    1. 每次new Thread新建对象性能差。
    2. 线程缺乏统一管理,可能无限制新建线程,相互之间竞争,及可能占用过多系统资源导致死机或oom。
    3. 缺乏更多功能,如定时执行、定期执行、线程中断。

    ThreadPool 优点

    减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务

    可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)

    • 减少在创建和销毁线程上所花的时间以及系统资源的开销
    • 如不使用线程池,有可能造成系统创建大量线程而导致消耗完系统内存

    Java提供的四种线程池的好处在于

    1. 重用存在的线程,减少对象创建、销毁的开销,提高性能。
    2. 可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。
    3. 提供定时执行、定期执行、单线程、并发数控制等功能。

    比较重要的几个类:

    描述
    ExecutorService 真正的线程池接口。
    ScheduledExecutorService 能和Timer/TimerTask类似,解决那些需要任务重复执行的问题。
    ThreadPoolExecutor ExecutorService的默认实现。
    ScheduledThreadPoolExecutor 继承ThreadPoolExecutor的ScheduledExecutorService接口实现,周期性任务调度的类实现。

    要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在Executors类里面提供了一些静态工厂,生成一些常用的线程池。

    Executors提供四种线程池

    newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

    newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

    newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。

    newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

    一般都不用Executors提供的线程创建方式

    使用ThreadPoolExecutor创建线程池

    ThreadPoolExecutor的构造函数

    public ThreadPoolExecutor(int corePoolSize,
                                  int maximumPoolSize,
                                  long keepAliveTime,
                                  TimeUnit unit,
                                  BlockingQueue<Runnable> workQueue,
                                  ThreadFactory threadFactory,
                                  RejectedExecutionHandler handler) {
            if (corePoolSize < 0 ||
                maximumPoolSize <= 0 ||
                maximumPoolSize < corePoolSize ||
                keepAliveTime < 0)
                throw new IllegalArgumentException();
            if (workQueue == null || threadFactory == null || handler == null)
                throw new NullPointerException();
            this.corePoolSize = corePoolSize;
            this.maximumPoolSize = maximumPoolSize;
            this.workQueue = workQueue;
            this.keepAliveTime = unit.toNanos(keepAliveTime);
            this.threadFactory = threadFactory;
            this.handler = handler;
        }
    

    参数:

    1. corePoolSize核心线程数大小,当线程数<corePoolSize ,会创建线程执行runnable
    2. maximumPoolSize 最大线程数, 当线程数 >= corePoolSize的时候,会把runnable放入workQueue中
    3. keepAliveTime 保持存活时间,当线程数大于corePoolSize的空闲线程能保持的最大时间。
    4. unit 时间单位
    5. workQueue 保存任务的阻塞队列
    6. threadFactory 创建线程的工厂
    7. handler 拒绝策略

    任务执行顺序:

    1. 当线程数小于corePoolSize时,创建线程执行任务。
    2. 当线程数大于等于corePoolSize并且workQueue没有满时,放入workQueue中
    3. 线程数大于等于corePoolSize并且当workQueue满时,新任务新建线程运行,线程总数要小于maximumPoolSize
    4. 当线程总数等于maximumPoolSize并且workQueue满了的时候执行handler的rejectedExecution。也就是拒绝策略。

    ThreadPoolExecutor默认有四个拒绝策略:

    1. ThreadPoolExecutor.AbortPolicy() 直接抛出异常RejectedExecutionException
    2. ThreadPoolExecutor.CallerRunsPolicy() 直接调用run方法并且阻塞执行
    3. ThreadPoolExecutor.DiscardPolicy() 直接丢弃后来的任务
    4. ThreadPoolExecutor.DiscardOldestPolicy() 丢弃在队列中队首的任务

    当然可以自己继承 RejectedExecutionHandler 来写拒绝策略.

    java 四种线程池的使用

    juejin.im/post/59df0c…

    Concurrent包里的其他东西:ArrayBlockingQueue、CountDownLatch等等。

    阻塞队列

    1、ArrayBlockingQueue 数组结构组成的有界阻塞队列。

    此队列按照先进先出(FIFO)的原则对元素进行排序,但是默认情况下不保证线程公平的访问队列,即如果队列满了,那么被阻塞在外面的线程对队列访问的顺序是不能保证线程公平(即先阻塞,先插入)的。

    CountDownLatch

    CountDownLatch 允许一个或多个线程等待其他线程完成操作。

    应用场景

    假如有这样一个需求,当我们需要解析一个Excel里多个sheet的数据时,可以考虑使用多线程,每个线程解析一个sheet里的数据,等到所有的sheet都解析完之后,程序需要提示解析完成。

    在这个需求中,要实现主线程等待所有线程完成sheet的解析操作,最简单的做法是使用join。代码如下:

    public class JoinCountDownLatchTest {
    
    	public static void main(String[] args) throws InterruptedException {
    		Thread parser1 = new Thread(new Runnable() {
    			@Override
    			public void run() {
    			}
    		});
    
    		Thread parser2 = new Thread(new Runnable() {
    			@Override
    			public void run() {
    				System.out.println("parser2 finish");
    			}
    		});
    
    		parser1.start();
    		parser2.start();
    		parser1.join();
    		parser2.join();
    		System.out.println("all parser finish");
    	}
    
    }
    

    join用于让当前执行线程等待join线程执行结束。其实现原理是不停检查join线程是否存活,如果join线程存活则让当前线程永远wait,代码片段如下,wait(0)表示永远等待下去。

    while (isAlive()) {
     wait(0);
    }
    
    • 方法isAlive()功能是判断当前线程是否处于活动状态。
    • 活动状态就是线程启动且尚未终止,比如正在运行或准备开始运行。

    CountDownLatch用法

    public class Test {
         public static void main(String[] args) {   
    	 
             final CountDownLatch latch = new CountDownLatch(2);
     
             new Thread(){
                 public void run() {
                     try {
                         System.out.println("子线程"+Thread.currentThread().getName()+"正在执行");
                        Thread.sleep(3000);
                        System.out.println("子线程"+Thread.currentThread().getName()+"执行完毕");
                        latch.countDown();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                 };
             }.start();
     
             new Thread(){
                 public void run() {
                     try {
                         System.out.println("子线程"+Thread.currentThread().getName()+"正在执行");
                         Thread.sleep(3000);
                         System.out.println("子线程"+Thread.currentThread().getName()+"执行完毕");
                         latch.countDown();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                 };
             }.start();
     
             try {
                 System.out.println("等待2个子线程执行完毕...");
                latch.await();
                System.out.println("2个子线程已经执行完毕");
                System.out.println("继续执行主线程");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
         }
     }
    
    线程Thread-0正在执行
    线程Thread-1正在执行
    等待2个子线程执行完毕...
    线程Thread-0执行完毕
    线程Thread-1执行完毕
    2个子线程已经执行完毕
    继续执行主线程
    

    new CountDownLatch(2)的构造函数接收一个int类型的参数作为计数器,如果你想等待N个点完成,这里就传入N。

    当我们调用一次CountDownLatch的countDown()方法时,N就会减1,CountDownLatch的await()会阻塞当前线程,直到N变成零。由于countDown方法可以用在任何地方,所以这里说的N个点,可以是N个线程,也可以是1个线程里的N个执行步骤。用在多个线程时,你只需要把这个CountDownLatch的引用传递到线程里。

    Java并发编程:CountDownLatch、CyclicBarrier和 Semaphore

    www.importnew.com/21889.html

    synchronized和ReentrantLock的区别?

    java在编写多线程程序时,为了保证线程安全,需要对数据同步,经常用到两种同步方式就是Synchronized和重入锁ReentrantLock。

    基础知识

    • 可重入锁。可重入锁是指同一个线程可以多次获取同一把锁。ReentrantLock和synchronized都是可重入锁
    • 可中断锁。可中断锁是指线程尝试获取锁的过程中,是否可以响应中断。synchronized是不可中断锁,而ReentrantLock则提供了中断功能。
    • 公平锁与非公平锁。公平锁是指多个线程同时尝试获取同一把锁时,获取锁的顺序按照线程达到的顺序,而非公平锁则允许线程“插队”。synchronized是非公平锁,而ReentrantLock的默认实现是非公平锁,但是也可以设置为公平锁。
    • CAS操作(CompareAndSwap)。CAS操作简单的说就是比较并交换。CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。”

    Synchronized

    synchronized是java内置的关键字,它提供了一种独占的加锁方式。synchronized的获取和释放锁由JVM实现,用户不需要显示的释放锁,非常方便。然而synchronized也有一定的局限性

    例如:

    1. 当线程尝试获取锁的时候,如果获取不到锁会一直阻塞。
    2. 如果获取锁的线程进入休眠或者阻塞,除非当前线程异常,否则其他线程尝试获取锁必须一直等待。

    ReentrantLock

    ReentrantLock它是JDK 1.5之后提供的API层面的互斥锁,需要lock()和unlock()方法配合try/finally语句块来完成。

    代码示例

    private Lock lock = new ReentrantLock();
    public void test(){
     lock.lock();
     try{
     doSomeThing();
     }catch (Exception e){
     // ignored
     }finally {
     lock.unlock();
     }
    }
    
    • **lock()**, 如果获取了锁立即返回,如果别的线程持有锁,当前线程则一直处于休眠状态,直到获取锁
    • tryLock(), 如果获取了锁立即返回true,如果别的线程正持有锁,立即返回false;
    • tryLock(long timeout,TimeUnit unit)****,如果获取了锁定立即返回true,如果别的线程正持有锁,会等待参数给定的时间,在等待的过程中,如果获取了锁定,就返回true,如果等待超时,返回false;
    • lockInterruptibly:如果获取了锁定立即返回,如果没有获取锁定,当前线程处于休眠状态,直到或者锁定,或者当前线程被别的线程中断

    ReentrantLock 一些特性

    1. 等待可中断避免,出现死锁的情况(如果别的线程正持有锁,会等待参数给定的时间,在等待的过程中,如果获取了锁定,就返回true,如果等待超时,返回false)
    2. 公平锁与非公平锁多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁,Synchronized锁非公平锁,ReentrantLock默认的构造函数是创建的非公平锁,可以通过参数true设为公平锁,但公平锁表现的性能不是很好。

    公平锁:线程获取锁的顺序和调用lock的顺序一样,FIFO;

    非公平锁:线程获取锁的顺序和调用lock的顺序无关,全凭运气。

    Java并发包(java.util.concurrent)中大量使用了CAS操作,涉及到并发的地方都调用了sun.misc.Unsafe类方法进行CAS操作

    ReenTrantLock实现的原理:

    简单来说,ReenTrantLock的实现是一种自旋锁,通过循环调用CAS操作来实现加锁。它的性能比较好也是因为避免了使线程进入内核态的阻塞状态。想尽办法避免线程进入内核的阻塞状态是我们去分析和理解锁设计的关键钥匙。

    总结一下

    在Synchronized优化以前,synchronized的性能是比ReenTrantLock差很多的,但是自从Synchronized引入了偏向锁,轻量级锁(自旋锁)后,两者的性能就差不多了,在两种方法都可用的情况下,官方甚至建议使用synchronized,其实synchronized的优化我感觉就借鉴了ReenTrantLock中的CAS技术。都是试图在用户态就把加锁问题解决,避免进入内核态的线程阻塞。

    synchronized

    在资源竞争不是很激烈的情况下,偶尔会有同步的情形下,synchronized是很合适的。原因在于,编译程序通常会尽可能的进行优化synchronize,另外可读性非常好。

    ReentrantLock:

    ReentrantLock用起来会复杂一些。在基本的加锁和解锁上,两者是一样的,所以无特殊情况下,推荐使用synchronized。ReentrantLock的优势在于它更灵活、更强大,增加了轮训、超时、中断等高级功能。

    ReentrantLock默认使用非公平锁是基于性能考虑,公平锁为了保证线程规规矩矩地排队,需要增加阻塞和唤醒的时间开销。如果直接插队获取非公平锁,跳过了对队列的处理,速度会更快。

    ReentrantLock实现原理

    www.cnblogs.com/maypattis/p…

    分析ReentrantLock的实现原理(ReentrantLock和同步工具类的实现基础都是AQS)

    www.jianshu.com/p/fe027772e…

    Semaphore有什么作用?

    1. Semaphore就是一个信号量,它的作用是限制某段代码块的并发数
    2. Semaphore有一个构造函数,可以传入一个int型整数n,表示某段代码最多只有n个线程可以访问
    3. 如果超出了n,那么请等待,等到某个线程执行完毕这段代码块,下一个线程再进入
    4. 由此可以看出如果Semaphore构造函数中传入的int型整数n=1,相当于变成了一个synchronized了。

    Semaphore类位于java.util.concurrent包下,它提供了2个构造器:

    //参数permits表示许可数目,即同时可以允许多少线程进行访问  
    public Semaphore(int permits) {  
        sync = new NonfairSync(permits);  
    }  
    //这个多了一个参数fair表示是否是公平的,即等待时间越久的越先获取许可  
    public Semaphore(int permits, boolean fair) {  
        sync = (fair)? new FairSync(permits) : new NonfairSync(permits);  
    }  
    
    • Semaphore类中比较重要的几个方法,首先是acquire()、release()方法:
    • acquire()用来获取一个许可,若无许可能够获得,则会一直等待,直到获得许可。
    • release()用来释放许可。注意,在释放许可之前,必须先获获得许可。
    Semaphore类中比较重要的几个方法,首先是acquire()、release()方法:
    acquire()用来获取一个许可,若无许可能够获得,则会一直等待,直到获得许可。
    release()用来释放许可。注意,在释放许可之前,必须先获获得许可。
    

    这4个方法都会被阻塞,如果想立即得到执行结果,可以使用下面几个方法:

    //尝试获取一个许可,若获取成功,则立即返回true,若获取失败,则立即返回false  
    public boolean tryAcquire() { };  
    //尝试获取一个许可,若在指定的时间内获取成功,则立即返回true,否则则立即返回false  
    public boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException { };   
    //尝试获取permits个许可,若获取成功,则立即返回true,若获取失败,则立即返回false  
    public boolean tryAcquire(int permits) { };   
    //尝试获取permits个许可,若在指定的时间内获取成功,则立即返回true  
    public boolean tryAcquire(int permits, long timeout, TimeUnit unit) throws InterruptedException { };  
    //得到当前可用的许可数目  
    public int availablePermits(); 
    

    示例

    假若一个工厂有5台机器,但是有8个工人,一台机器同时只能被一个工人使用,只有使用完了,其他工人才能继续使用。那么我们就可以通过Semaphore来实现:

    public class Test {  
        public static void main(String[] args) {  
            int N = 8; //工人数  
            Semaphore semaphore = new Semaphore(5); //机器数目  
            for(int i=0;i<N;i++)  
                new Worker(i,semaphore).start();  
        }      
        static class Worker extends Thread{  
            private int num;  
            private Semaphore semaphore;  
            public Worker(int num,Semaphore semaphore){  
                this.num = num;  
                this.semaphore = semaphore;  
            }          
            @Override  
            public void run() {  
                try {  
                    semaphore.acquire();  
                    System.out.println("工人"+this.num+"占用一个机器在生产...");  
                    Thread.sleep(2000);  
                    System.out.println("工人"+this.num+"释放出机器");  
                    semaphore.release();              
                } catch (InterruptedException e) {  
                    e.printStackTrace();  
                }  
            }  
        }  
    } 
    

    运行结果:

    工人0占用一个机器在生产...  
    工人1占用一个机器在生产...  
    工人2占用一个机器在生产...  
    工人4占用一个机器在生产...  
    工人5占用一个机器在生产...  
    工人0释放出机器  
    工人2释放出机器  
    工人3占用一个机器在生产...  
    工人7占用一个机器在生产...  
    工人4释放出机器  
    工人5释放出机器  
    工人1释放出机器  
    工人6占用一个机器在生产...  
    工人3释放出机器  
    工人7释放出机器  
    工人6释放出机器
    

    Java Concurrency API中的Lock接口(Lock interface)是什么?对比同步它有什么优势?

    Lock接口比同步方法和同步块提供了更具扩展性的锁操作。他们允许更灵活的结构,可以具有完全不同的性质,并且可以支持多个相关类的条件对象。

    它的优势有:

    • 可以使锁更公平
    • 可以使线程在等待锁的时候响应中断
    • 可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间
    • 可以在不同的范围,以不同的顺序获取和释放锁

    Hashtable的size()方法中明明只有一条语句”return count”,为什么还要做同步?

    同一时间只能有一条线程执行固定类的同步方法,但是对于类的非同步方法,可以多条线程同时访问。所以,这样就有问题了,可能线程A在执行Hashtable的put方法添加数据,线程B则可以正常调用size()方法读取Hashtable中当前元素的个数,那读取到的值可能不是最新的,可能线程A添加了完了数据,但是没有对size++,线程B就已经读取size了,那么对于线程B来说读取到的size一定是不准确的。

    而给size()方法加了同步之后,意味着线程B调用size()方法只有在线程A调用put方法完毕之后才可以调用,这样就保证了线程安全性

    ConcurrentHashMap的并发度是什么?

    ConcurrentHashMap的并发度就是segment的大小,默认为16,这意味着最多同时可以有16条线程操作ConcurrentHashMap,这也是ConcurrentHashMap对Hashtable的最大优势

    ReentrantReadWriteLock读写锁的使用

    Lock比传统线程模型中的synchronized方式更加面向对象,与生活中的锁类似,锁本身也应该是一个对象。两个线程执行的代码片段要实现同步互斥的效果,它们必须用同一个Lock对象。

    读写锁分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,这是由jvm自己控制的,你只要上好相应的锁即可如果你的代码只读数据,可以很多人同时读,但不能同时写,那就上读锁

    如果你的代码修改数据,只能有一个人在写,且不能同时读取,那就上写锁。总之,读的时候上读锁,写的时候上写锁!

    ReentrantReadWriteLock会使用两把锁来解决问题,一个读锁,一个写锁

    线程进入读锁的前提条件

    • 没有其他线程的写锁
    • 没有写请求或者有写请求,但调用线程和持有锁的线程是同一个

    线程进入写锁的前提条件

    • 没有其他线程的读锁
    • 没有其他线程的写锁
    • 读锁的重入是允许多个申请读操作的线程的,而写锁同时只允许单个线程占有,该线程的写操作可以重入。
    • 如果一个线程占有了写锁,在不释放写锁的情况下,它还能占有读锁,即写锁降级为读锁。
    • 对于同时占有读锁和写锁的线程,如果完全释放了写锁,那么它就完全转换成了读锁,以后的写操作无法重入,在写锁未完全释放时写操作是可以重入的。
    • 公平模式下无论读锁还是写锁的申请都必须按照AQS锁等待队列先进先出的顺序。非公平模式下读操作插队的条件是锁等待队列head节点后的下一个节点是SHARED型节点,写锁则无条件插队。
    • 读锁不允许newConditon获取Condition接口,而写锁的newCondition接口实现方法同ReentrantLock。

     

    展开全文
  • 什么是多线程?如何实现多线程

    万次阅读 多人点赞 2019-04-09 09:53:36
    【转】什么是线程安全?怎么实现线程安全?什么是进程?什么是线程?什么是线程安全?添加一个状态呢?如何确保线程安全?synchronizedlock 转自:https://blog.csdn.net/csdnnews/article/details/82321777 什么是...


    转自:https://blog.csdn.net/csdnnews/article/details/82321777

    什么是进程?

    电脑中时会有很多单独运行的程序,每个程序有一个独立的进程,而进程之间是相互独立存在的。比如下图中的QQ、酷狗播放器、电脑管家等等。在这里插入图片描述

    什么是线程?

    进程想要执行任务就需要依赖线程。换句话说,就是进程中的最小执行单位就是线程,并且一个进程中至少有一个线程。

    那什么是多线程?提到多线程这里要说两个概念,就是串行和并行,搞清楚这个,我们才能更好地理解多线程。

    所谓串行,其实是相对于单条线程来执行多个任务来说的,我们就拿下载文件来举个例子:当我们下载多个文件时,在串行中它是按照一定的顺序去进行下载的,也就是说,必须等下载完A之后才能开始下载B,它们在时间上是不可能发生重叠的。
    在这里插入图片描述
    并行:下载多个文件,开启多条线程,多个文件同时进行下载,这里是严格意义上的,在同一时刻发生的,并行在时间上是重叠的。
    在这里插入图片描述
    了解了这两个概念之后,我们再来说说什么是多线程。举个例子,我们打开腾讯管家,腾讯管家本身就是一个程序,也就是说它就是一个进程,它里面有很多的功能,我们可以看下图,能查杀病毒、清理垃圾、电脑加速等众多功能。

    按照单线程来说,无论你想要清理垃圾、还是要病毒查杀,那么你必须先做完其中的一件事,才能做下一件事,这里面是有一个执行顺序的。

    如果是多线程的话,我们其实在清理垃圾的时候,还可以进行查杀病毒、电脑加速等等其他的操作,这个是严格意义上的同一时刻发生的,没有执行上的先后顺序。
    在这里插入图片描述
    以上就是,一个进程运行时产生了多个线程。

    在了解完这个问题后,我们又需要去了解一个使用多线程不得不考虑的问题——线程安全。

    今天我们不说如何保证一个线程的安全,我们聊聊什么是线程安全?因为我之前面试被问到了,说真的,我之前真的不是特别了解这个问题,我们好像只学了如何确保一个线程安全,却不知道所谓的安全到底是什么!

    什么是线程安全?

    当多个线程访问某个方法时,不管你通过怎样的调用方式、或者说这些线程如何交替地执行,我们在主程序中不需要去做任何的同步,这个类的结果行为都是我们设想的正确行为,那么我们就可以说这个类是线程安全的。
    既然是线程安全问题,那么毫无疑问,所有的隐患都是在多个线程访问的情况下产生的,也就是我们要确保在多条线程访问的时候,我们的程序还能按照我们预期的行为去执行,我们看一下下面的代码。

    Integer count = 0;
    public void getCount() {
           count ++;
           System.out.println(count);
     }
    

    很简单的一段代码,下面我们就来统计一下这个方法的访问次数,多个线程同时访问会不会出现什么问题,我开启的3条线程,每个线程循环10次,得到以下结果:
    在这里插入图片描述

    我们可以看到,这里出现了两个26,出现这种情况显然表明这个方法根本就不是线程安全的,出现这种问题的原因有很多。

    最常见的一种,就是我们A线程在进入方法后,拿到了count的值,刚把这个值读取出来,还没有改变count的值的时候,结果线程B也进来的,那么导致线程A和线程B拿到的count值是一样的。

    那么由此我们可以了解到,这确实不是一个线程安全的类,因为他们都需要操作这个共享的变量。其实要对线程安全问题给出一个明确的定义,还是蛮复杂的,我们根据我们这个程序来总结下什么是线程安全。

    当多个线程访问某个方法时,不管你通过怎样的调用方式、或者说这些线程如何交替地执行,我们在主程序中不需要去做任何的同步,这个类的结果行为都是我们设想的正确行为,那么我们就可以说这个类是线程安全的。

    搞清楚了什么是线程安全,接下来我们看看Java中确保线程安全最常用的两种方式。先来看段代码。

    public void threadMethod(int j) {
    
        int i = 1;
    
        j = j + i;
    }
    
    

    大家觉得这段代码是线程安全的吗?

    毫无疑问,它绝对是线程安全的,我们来分析一下,为什么它是线程安全的?

    我们可以看到这段代码是没有任何状态的,就是说我们这段代码,不包含任何的作用域,也没有去引用其他类中的域进行引用,它所执行的作用范围与执行结果只存在它这条线程的局部变量中,并且只能由正在执行的线程进行访问。当前线程的访问,不会对另一个访问同一个方法的线程造成任何的影响。

    两个线程同时访问这个方法,因为没有共享的数据,所以他们之间的行为,并不会影响其他线程的操作和结果,所以说无状态的对象,也是线程安全的。

    添加一个状态呢?

    如果我们给这段代码添加一个状态,添加一个count,来记录这个方法并命中的次数,每请求一次count+1,那么这个时候这个线程还是安全的吗?

    public class ThreadDemo {
    
       int count = 0; // 记录方法的命中次数
    
       public void threadMethod(int j) {
    
           count++ ;
    
           int i = 1;
    
           j = j + i;
       }
    }
    
    

    明显已经不是了,单线程运行起来确实是没有任何问题的,但是当出现多条线程并发访问这个方法的时候,问题就出现了,我们先来分析下count+1这个操作。

    进入这个方法之后首先要读取count的值,然后修改count的值,最后才把这把值赋值给count,总共包含了三步过程:“读取”一>“修改”一>“赋值”,既然这个过程是分步的,那么我们先来看下面这张图,看看你能不能看出问题:
    在这里插入图片描述
    可以发现,count的值并不是正确的结果,当线程A读取到count的值,但是还没有进行修改的时候,线程B已经进来了,然后线程B读取到的还是count为1的值,正因为如此所以我们的count值已经出现了偏差,那么这样的程序放在我们的代码中,是存在很多的隐患的。

    如何确保线程安全?

    既然存在线程安全的问题,那么肯定得想办法解决这个问题,怎么解决?我们说说常见的几种方式

    synchronized

    synchronized关键字,就是用来控制线程同步的,保证我们的线程在多线程环境下,不被多个线程同时执行,确保我们数据的完整性,使用方法一般是加在方法上。

    public class ThreadDemo {
    
       int count = 0; // 记录方法的命中次数
    
       public synchronized void threadMethod(int j) {
    
           count++ ;
    
           int i = 1;
    
           j = j + i;
       }
    }
    
    

    这样就可以确保我们的线程同步了,同时这里需要注意一个大家平时忽略的问题,首先synchronized锁的是括号里的对象,而不是代码,其次,对于非静态的synchronized方法,锁的是对象本身也就是this。

    当synchronized锁住一个对象之后,别的线程如果想要获取锁对象,那么就必须等这个线程执行完释放锁对象之后才可以,否则一直处于等待状态。

    注意点:虽然加synchronized关键字,可以让我们的线程变得安全,但是我们在用的时候,也要注意缩小synchronized的使用范围,如果随意使用时很影响程序的性能,别的对象想拿到锁,结果你没用锁还一直把锁占用,这样就有点浪费资源。

    lock

    先来说说它跟synchronized有什么区别吧,Lock是在Java1.6被引入进来的,Lock的引入让锁有了可操作性,什么意思?就是我们在需要的时候去手动的获取锁和释放锁,甚至我们还可以中断获取以及超时获取的同步特性,但是从使用上说Lock明显没有synchronized使用起来方便快捷。我们先来看下一般是如何使用的:

    private Lock lock = new ReentrantLock(); // ReentrantLock是Lock的子类
    
       private void method(Thread thread){
           lock.lock(); // 获取锁对象
           try {
               System.out.println("线程名:"+thread.getName() + "获得了锁");
               // Thread.sleep(2000);
           }catch(Exception e){
               e.printStackTrace();
           } finally {
               System.out.println("线程名:"+thread.getName() + "释放了锁");
               lock.unlock(); // 释放锁对象
           }
       }
    
    

    进入方法我们首先要获取到锁,然后去执行我们业务代码,这里跟synchronized不同的是,Lock获取的所对象需要我们亲自去进行释放,为了防止我们代码出现异常,所以我们的释放锁操作放在finally中,因为finally中的代码无论如何都是会执行的。

    写个主方法,开启两个线程测试一下我们的程序是否正常:

    public static void main(String[] args) {
           LockTest lockTest = new LockTest();
    
           // 线程1
           Thread t1 = new Thread(new Runnable() {
    
               @Override
               public void run() {
                   // Thread.currentThread()  返回当前线程的引用
                   lockTest.method(Thread.currentThread());
               }
           }, "t1");
    
           // 线程2
           Thread t2 = new Thread(new Runnable() {
    
               @Override
               public void run() {
                   lockTest.method(Thread.currentThread());
               }
           }, "t2");
    
           t1.start();
           t2.start();
       }
    
    

    结果
    在这里插入图片描述
    可以看出我们的执行,是没有任何问题的。

    其实在Lock还有几种获取锁的方式,我们这里再说一种,就是tryLock()这个方法跟Lock()是有区别的,Lock在获取锁的时候,如果拿不到锁,就一直处于等待状态,直到拿到锁,但是tryLock()却不是这样的,tryLock是有一个Boolean的返回值的,如果没有拿到锁,直接返回false,停止等待,它不会像Lock()那样去一直等待获取锁。

    我们来看下代码:

    private void method(Thread thread){
           // lock.lock(); // 获取锁对象
           if (lock.tryLock()) {
               try {
                   System.out.println("线程名:"+thread.getName() + "获得了锁");
                   // Thread.sleep(2000);
               }catch(Exception e){
                   e.printStackTrace();
               } finally {
                   System.out.println("线程名:"+thread.getName() + "释放了锁");
                   lock.unlock(); // 释放锁对象
               }
           }
       }
    
    

    结果:我们继续使用刚才的两个线程进行测试可以发现,在线程t1获取到锁之后,线程t2立马进来,然后发现锁已经被占用,那么这个时候它也不在继续等待。

    在这里插入图片描述
    似乎这种方法,感觉不是很完美,如果我第一个线程,拿到锁的时间,比第二个线程进来的时间还要长,是不是也拿不到锁对象?

    那我能不能,用一中方式来控制一下,让后面等待的线程,可以等待5秒,如果5秒之后,还获取不到锁,那么就停止等,其实tryLock()是可以进行设置等待的相应时间的。

    private void method(Thread thread) throws InterruptedException {
           // lock.lock(); // 获取锁对象
    
           // 如果2秒内获取不到锁对象,那就不再等待
           if (lock.tryLock(2,TimeUnit.SECONDS)) {
               try {
                   System.out.println("线程名:"+thread.getName() + "获得了锁");
    
                   // 这里睡眠3秒
                   Thread.sleep(3000);
               }catch(Exception e){
                   e.printStackTrace();
               } finally {
                   System.out.println("线程名:"+thread.getName() + "释放了锁");
                   lock.unlock(); // 释放锁对象
               }
           }
       }
    
    

    结果:看上面的代码,我们可以发现,虽然我们获取锁对象的时候,可以等待2秒,但是我们线程t1在获取锁对象之后,执行任务缺花费了3秒,那么这个时候线程t2是不在等待的。
    在这里插入图片描述
    我们再来改一下这个等待时间,改为5秒,再来看下结果:

    private void method(Thread thread) throws InterruptedException {
           // lock.lock(); // 获取锁对象
    
           // 如果5秒内获取不到锁对象,那就不再等待
           if (lock.tryLock(5,TimeUnit.SECONDS)) {
               try {
                   System.out.println("线程名:"+thread.getName() + "获得了锁");
               }catch(Exception e){
                   e.printStackTrace();
               } finally {
                   System.out.println("线程名:"+thread.getName() + "释放了锁");
                   lock.unlock(); // 释放锁对象
               }
           }
       }
    
    

    结果:这个时候我们可以看到,线程t2等到5秒获取到了锁对象,执行了任务代码。
    在这里插入图片描述
    以上就是使用Lock,来保证我们线程安全的方式。

    展开全文
  • 多线程(一):创建线程和线程的常用方法

    万次阅读 多人点赞 2019-08-13 11:33:57
    一:为什么要学多线程 应付面试 :多线程几乎是面试中必问的题,所以掌握一定的基础知识是必须的。 了解并发编程:实际工作中很少写多线程的代码,这部分代码一般都被人封装起来了,在业务中使用多线程的机会也...
    分享一个朋友的人工智能教程(请以“右键”->"在新标签页中打开连接”的方式访问)。比较通俗易懂,风趣幽默,感兴趣的朋友可以去看看。

    一:为什么要学多线程

    1. 应付面试 :多线程几乎是面试中必问的题,所以掌握一定的基础知识是必须的。
    2. 了解并发编程:实际工作中很少写多线程的代码,这部分代码一般都被人封装起来了,在业务中使用多线程的机会也不是很多(看具体项目),虽然代码中很少会自己去创建线程,但是实际环境中每行代码却都是并行执行的,同一时刻大量请求同一个接口,并发可能会产生一些问题,所以也需要掌握一定的并发知识

    二:进程与线程

    1. 进程

    进程是资源(CPU、内存等)分配的基本单位,它是程序执行时的一个实例。程序运行时系统就会创建一个进程,并为它分配资源,然后把该进程放入进程就绪队列,进程调度器选中它的时候就会为它分配CPU时间,程序开始真正运行。

    2. 线程

    线程是一条执行路径,是程序执行时的最小单位,它是进程的一个执行流,是CPU调度和分派的基本单位,一个进程可以由很多个线程组成,线程间共享进程的所有资源,每个线程有自己的堆栈和局部变量。线程由CPU独立调度执行,在多CPU环境下就允许多个线程同时运行。同样多线程也可以实现并发操作,每个请求分配一个线程来处理。

    一个正在运行的软件(如迅雷)就是一个进程,一个进程可以同时运行多个任务( 迅雷软件可以同时下载多个文件,每个下载任务就是一个线程), 可以简单的认为进程是线程的集合。

    线程是一条可以执行的路径。多线程就是同时有多条执行路径在同时(并行)执行。

    3. 进程与线程的关系

    一个程序就是一个进程,而一个程序中的多个任务则被称为线程。进程是表示资源分配的基本单位,又是调度运行的基本单位。,亦即执行处理机调度的基本单位。 进程和线程的关系:

    • 一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。线程是操作系统可识别的最小执行和调度单位。

    • 资源分配给进程,同一进程的所有线程共享该进程的所有资源。同一进程中的多个线程共享代码段(代码和常量),数据段(全局变量和静态变量),扩展段(堆存储)。但是每个线程拥有自己的栈段,栈段又叫运行时段,用来存放所有局部变量和临时变量,即每个线程都有自己的堆栈和局部变量。

    • 处理机分给线程,即真正在处理机上运行的是线程。

    • 线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。

    如果把上课的过程比作进程,把老师比作CPU,那么可以把每个学生比作每个线程,所有学生共享这个教室(也就是所有线程共享进程的资源),上课时学生A向老师提出问题,老师对A进行解答,此时可能会有学生B对老师的解答不懂会提出B的疑问(注意:此时可能老师还没有对A同学的问题解答完毕),此时老师又向学生B解惑,解释完之后又继续回答学生A的问题,同一时刻老师只能向一个学生回答问题(即:当多个线程在运行时,同一个CPU在某一个时刻只能服务于一个线程,可能一个线程分配一点时间,时间到了就轮到其它线程执行了,这样多个线程在来回的切换)

    4. 为什么要使用多线程

    多线程可以提高程序的效率。

    实际生活案例:村长要求喜洋洋在一个小时内打100桶水,可以喜洋洋一个小时只能打25桶水,如果这样就需要4个小时才能完成任务,为了在一个小时能够完成,喜洋洋就请美洋洋、懒洋洋、沸洋洋,来帮忙,这样4只羊同时干活,在一小时内完成了任务。原本用4个小时完成的任务现在只需要1个小时就完成了,如果把每只羊看做一个线程,多只羊即多线程可以提高程序的效率。

    5. 多线程应用场景

    • 一般线程之间比较独立,互不影响
    • 一个线程发生问题,一般不影响其它线程

    三:多线程的实现方式

    1. 顺序编程

    顺序编程:程序从上往下的同步执行,即如果第一行代码执行没有结束,第二行代码就只能等待第一行执行结束后才能结束。

    public class Main {
        // 顺序编程 吃喝示例:当吃饭吃不完的时候,是不能喝酒的,只能吃完晚才能喝酒
        public static void main(String[] args) throws Exception {
    		// 先吃饭再喝酒
            eat();
            drink();
        }
    
        private static void eat() throws Exception {
            System.out.println("开始吃饭?...\t" + new Date());
            Thread.sleep(5000);
            System.out.println("结束吃饭?...\t" + new Date());
        }
    
        private static void drink() throws Exception {
            System.out.println("开始喝酒?️...\t" + new Date());
            Thread.sleep(5000);
            System.out.println("结束喝酒?...\t" + new Date());
        }
    }
    

    这里写图片描述

    2. 并发编程

    并发编程:多个任务可以同时做,常用与任务之间比较独立,互不影响。
    线程上下文切换:

    同一个时刻一个CPU只能做一件事情,即同一时刻只能一个线程中的部分代码,假如有两个线程,Thread-0和Thread-1,刚开始CPU说Thread-0你先执行,给你3毫秒时间,Thread-0执行了3毫秒时间,但是没有执行完,此时CPU会暂停Thread-0执行并记录Thread-0执行到哪行代码了,当时的变量的值是多少,然后CPU说Thread-1你可以执行了,给你2毫秒的时间,Thread-1执行了2毫秒也没执行完,此时CPU会暂停Thread-1执行并记录Thread-1执行到哪行代码了,当时的变量的值是多少,此时CPU又说Thread-0又该你,这次我给你5毫秒时间,去执行吧,此时CPU就找出上次Thread-0线程执行到哪行代码了,当时的变量值是多少,然后接着上次继续执行,结果用了2毫秒就Thread-0就执行完了,就终止了,然后CPU说Thread-1又轮到你,这次给你4毫秒,同样CPU也会先找出上次Thread-1线程执行到哪行代码了,当时的变量值是多少,然后接着上次继续开始执行,结果Thread-1在4毫秒内也执行结束了,Thread-1也结束了终止了。CPU在来回改变线程的执行机会称之为线程上下文切换。

    public class Main {
        public static void main(String[] args) {
    	    // 一边吃饭一边喝酒
            new EatThread().start();
            new DrinkThread().start();
        }
    }
    
    class EatThread extends Thread{
        @Override
        public void run() {
            System.out.println("开始吃饭?...\t" + new Date());
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("结束吃饭?...\t" + new Date());
        }
    }
    
    class DrinkThread extends Thread {
        @Override
        public void run() {
            System.out.println("开始喝酒?️...\t" + new Date());
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("结束喝酒?...\t" + new Date());
        }
    }
    

    并发编程,一边吃饭一边喝酒总共用时5秒,比顺序编程更快,因为并发编程可以同时运行,而不必等前面的代码运行完之后才允许后面的代码

    这里写图片描述

    本示例主要启动3个线程,一个主线程main thread、一个吃饭线程(Thread-0)和一个喝酒线程(Thread-1),共三个线程, 三个线程并发切换着执行。main线程很快执行完,吃饭线程和喝酒线程会继续执行,直到所有线程(非守护线程)执行完毕,整个程序才会结束,main线程结束并不意味着整个程序结束。
    这里写图片描述

    • 顺序:代码从上而下按照固定的顺序执行,只有上一件事情执行完毕,才能执行下一件事。就像物理电路中的串行,假如有十件事情,一个人来完成,这个人必须先做第一件事情,然后再做第二件事情,最后做第十件事情,按照顺序做。

    • 并行:多个操作同时处理,他们之间是并行的。假如十件事情,两个人来完成,每个人在某个时间点各自做各自的事情,互不影响

    • 并发:将一个操作分割成多个部分执行并且允许无序处理,假如有十件事情,如果有一个人在做,这个人可能做一会这个不想做了,再去做别的,做着做着可能也不想做了,又去干其它事情了,看他心情想干哪个就干哪个,最终把十件事情都做完。如果有两个人在做,他们俩先分一下,比如张三做4件,李四做6件,他们各做自己的,在做自己的事情过程中可以随意的切换到别的事情,不一定要把某件事情干完再去干其它事情,有可能一件事做了N次才做完。

    通常一台电脑只有一个cpu,多个线程属于并发执行,如果有多个cpu,多线程并发执行有可能变成并行执行。
    这里写图片描述

    3. 多线程创建方式

    • 继承 Thread
    • 实现 Runable
    • 实现 Callable
    ①:继成java.lang.Thread, 重写run()方法
    public class Main {
        public static void main(String[] args) {
            new MyThread().start();
        }
    }
    
    class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId());
        }
    }
    

    Thread 类

    package java.lang;
    public class Thread implements Runnable {
    	// 构造方法
    	public Thread(Runnable target);
    	public Thread(Runnable target, String name);
    	
    	public synchronized void start();
    }
    

    Runnable 接口

    package java.lang;
    
    @FunctionalInterface
    public interface Runnable {
        pubic abstract void run();
    }
    

    ②:实现java.lang.Runnable接口,重写run()方法,然后使用Thread类来包装

    public class Main {
        public static void main(String[] args) {
        	 // 将Runnable实现类作为Thread的构造参数传递到Thread类中,然后启动Thread类
            MyRunnable runnable = new MyRunnable();
            new Thread(runnable).start();
        }
    }
    
    class MyRunnable implements Runnable {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId());
        }
    }
    

    可以看到两种方式都是围绕着Thread和Runnable,继承Thread类把run()写到类中,实现Runnable接口是把run()方法写到接口中然后再用Thread类来包装, 两种方式最终都是调用Thread类的start()方法来启动线程的。
    两种方式在本质上没有明显的区别,在外观上有很大的区别,第一种方式是继承Thread类,因Java是单继承,如果一个类继承了Thread类,那么就没办法继承其它的类了,在继承上有一点受制,有一点不灵活,第二种方式就是为了解决第一种方式的单继承不灵活的问题,所以平常使用就使用第二种方式

    其它变体写法:

    public class Main {
        public static void main(String[] args) {
            // 匿名内部类
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId());
                }
            }).start();
    
            // 尾部代码块, 是对匿名内部类形式的语法糖
            new Thread() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId());
                }
            }.start();
    
            // Runnable是函数式接口,所以可以使用Lamda表达式形式
            Runnable runnable = () -> {System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId());};
            new Thread(runnable).start();
        }
    }
    

    ③:实现Callable接口,重写call()方法,然后包装成java.util.concurrent.FutureTask, 再然后包装成Thread

    Callable:有返回值的线程,能取消线程,可以判断线程是否执行完毕

    public class Main {
        public static void main(String[] args) throws Exception {
        	 // 将Callable包装成FutureTask,FutureTask也是一种Runnable
            MyCallable callable = new MyCallable();
            FutureTask<Integer> futureTask = new FutureTask<>(callable);
            new Thread(futureTask).start();
    
            // get方法会阻塞调用的线程
            Integer sum = futureTask.get();
            System.out.println(Thread.currentThread().getName() + Thread.currentThread().getId() + "=" + sum);
        }
    }
    
    
    class MyCallable implements Callable<Integer> {
    
        @Override
        public Integer call() throws Exception {
            System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId() + "\t" + new Date() + " \tstarting...");
    
            int sum = 0;
            for (int i = 0; i <= 100000; i++) {
                sum += i;
            }
            Thread.sleep(5000);
    
            System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId() + "\t" + new Date() + " \tover...");
            return sum;
        }
    }
    

    Callable 也是一种函数式接口

    @FunctionalInterface
    public interface Callable<V> {
        V call() throws Exception;
    }
    

    FutureTask

    public class FutureTask<V> implements RunnableFuture<V> {
    	// 构造函数
    	public FutureTask(Callable<V> callable);
    	
    	// 取消线程
    	public boolean cancel(boolean mayInterruptIfRunning);
    	// 判断线程
    	public boolean isDone();
    	// 获取线程执行结果
    	public V get() throws InterruptedException, ExecutionException;
    }
    

    RunnableFuture

    public interface RunnableFuture<V> extends Runnable, Future<V> {
        void run();
    }
    

    三种方式比较:

    • Thread: 继承方式, 不建议使用, 因为Java是单继承的,继承了Thread就没办法继承其它类了,不够灵活
    • Runnable: 实现接口,比Thread类更加灵活,没有单继承的限制
    • Callable: Thread和Runnable都是重写的run()方法并且没有返回值,Callable是重写的call()方法并且有返回值并可以借助FutureTask类来判断线程是否已经执行完毕或者取消线程执行
    • 当线程不需要返回值时使用Runnable,需要返回值时就使用Callable,一般情况下不直接把线程体代码放到Thread类中,一般通过Thread类来启动线程
    • Thread类是实现Runnable,Callable封装成FutureTask,FutureTask实现RunnableFuture,RunnableFuture继承Runnable,所以Callable也算是一种Runnable,所以三种实现方式本质上都是Runnable实现

    四:线程的状态

    1. 创建(new)状态: 准备好了一个多线程的对象,即执行了new Thread(); 创建完成后就需要为线程分配内存
    2. 就绪(runnable)状态: 调用了start()方法, 等待CPU进行调度
    3. 运行(running)状态: 执行run()方法
    4. 阻塞(blocked)状态: 暂时停止执行线程,将线程挂起(sleep()、wait()、join()、没有获取到锁都会使线程阻塞), 可能将资源交给其它线程使用
    5. 死亡(terminated)状态: 线程销毁(正常执行完毕、发生异常或者被打断interrupt()都会导致线程终止)

    这里写图片描述

    这里写图片描述

    这里写图片描述

    五:Thread常用方法

    Thread

    public class Thread implements Runnable {
        // 线程名字
        private volatile String name;
        // 线程优先级(1~10)
        private int priority;
        // 守护线程
        private boolean daemon = false;
        // 线程id
        private long tid;
        // 线程组
        private ThreadGroup group;
        
        // 预定义3个优先级
        public final static int MIN_PRIORITY = 1;
        public final static int NORM_PRIORITY = 5;
        public final static int MAX_PRIORITY = 10;
        
        
        // 构造函数
        public Thread();
        public Thread(String name);
        public Thread(Runnable target);
        public Thread(Runnable target, String name);
        // 线程组
        public Thread(ThreadGroup group, Runnable target);
        
        
        // 返回当前正在执行线程对象的引用
        public static native Thread currentThread();
        
        // 启动一个新线程
        public synchronized void start();
        // 线程的方法体,和启动线程没毛关系
        public void run();
        
        // 让线程睡眠一会,由活跃状态改为挂起状态
        public static native void sleep(long millis) throws InterruptedException;
        public static void sleep(long millis, int nanos) throws InterruptedException;
        
        // 打断线程 中断线程 用于停止线程
        // 调用该方法时并不需要获取Thread实例的锁。无论何时,任何线程都可以调用其它线程的interruptf方法
        public void interrupt();
        public boolean isInterrupted()
        
        // 线程是否处于活动状态
        public final native boolean isAlive();
        
        // 交出CPU的使用权,从运行状态改为挂起状态
        public static native void yield();
        
        public final void join() throws InterruptedException
        public final synchronized void join(long millis)
        public final synchronized void join(long millis, int nanos) throws InterruptedException
        
        
        // 设置线程优先级
        public final void setPriority(int newPriority);
        // 设置是否守护线程
        public final void setDaemon(boolean on);
        // 线程id
        public long getId() { return this.tid; }
        
        
        // 线程状态
        public enum State {
            // new 创建
            NEW,
    
            // runnable 就绪
            RUNNABLE,
    
            // blocked 阻塞
            BLOCKED,
    
            // waiting 等待
            WAITING,
    
            // timed_waiting
            TIMED_WAITING,
    
            // terminated 结束
            TERMINATED;
        }
    }
    
    public static void main(String[] args) {
        // main方法就是一个主线程
    
        // 获取当前正在运行的线程
        Thread thread = Thread.currentThread();
        // 线程名字
        String name = thread.getName();
        // 线程id
        long id = thread.getId();
        // 线程优先级
        int priority = thread.getPriority();
        // 是否存活
        boolean alive = thread.isAlive();
        // 是否守护线程
        boolean daemon = thread.isDaemon();
    
        // Thread[name=main, id=1 ,priority=5 ,alive=true ,daemon=false]
        System.out.println("Thread[name=" + name + ", id=" + id + " ,priority=" + priority + " ,alive=" + alive + " ,daemon=" + daemon + "]");
    }
    
    0. Thread.currentThread()
    public static void main(String[] args) {
        Thread thread = Thread.currentThread();
        // 线程名称
        String name = thread.getName();
        // 线程id
        long id = thread.getId();
        // 线程已经启动且尚未终止
        // 线程处于正在运行或准备开始运行的状态,就认为线程是“存活”的
        boolean alive = thread.isAlive();
        // 线程优先级
        int priority = thread.getPriority();
        // 是否守护线程
        boolean daemon = thread.isDaemon();
        
        // Thread[name=main,id=1,alive=true,priority=5,daemon=false]
        System.out.println("Thread[name=" + name + ",id=" + id + ",alive=" + alive + ",priority=" + priority + ",daemon=" + daemon + "]");
    }
    
    1. start() 与 run()
    public static void main(String[] args) throws Exception {
       new Thread(()-> {
           for (int i = 0; i < 5; i++) {
               System.out.println(Thread.currentThread().getName() + " " + i);
               try { Thread.sleep(200); } catch (InterruptedException e) { }
           }
       }, "Thread-A").start();
    
       new Thread(()-> {
           for (int j = 0; j < 5; j++) {
               System.out.println(Thread.currentThread().getName() + " " + j);
               try { Thread.sleep(200); } catch (InterruptedException e) { }
           }
       }, "Thread-B").start();
    }
    

    start(): 启动一个线程,线程之间是没有顺序的,是按CPU分配的时间片来回切换的。
    这里写图片描述

    public static void main(String[] args) throws Exception {
        new Thread(()-> {
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + " " + i);
                try { Thread.sleep(200); } catch (InterruptedException e) { }
            }
        }, "Thread-A").run();
    
        new Thread(()-> {
            for (int j = 0; j < 5; j++) {
                System.out.println(Thread.currentThread().getName() + " " + j);
                try { Thread.sleep(200); } catch (InterruptedException e) { }
            }
        }, "Thread-B").run();
    }
    

    注意:执行结果都是main主线程
    这里写图片描述

    run(): 调用线程的run方法,就是普通的方法调用,虽然将代码封装到两个线程体中,可以看到线程中打印的线程名字都是main主线程,run()方法用于封装线程的代码,具体要启动一个线程来运行线程体中的代码(run()方法)还是通过start()方法来实现,调用run()方法就是一种顺序编程不是并发编程。

    有些面试官经常问一些启动一个线程是用start()方法还是run()方法,为了面试而面试。

    2. sleep() 与 interrupt()
    public static native void sleep(long millis) throws InterruptedException;
    public void interrupt();
    

    sleep(long millis): 睡眠指定时间,程序暂停运行,睡眠期间会让出CPU的执行权,去执行其它线程,同时CPU也会监视睡眠的时间,一旦睡眠时间到就会立刻执行(因为睡眠过程中仍然保留着锁,有锁只要睡眠时间到就能立刻执行)。

    • sleep(): 睡眠指定时间,即让程序暂停指定时间运行,时间到了会继续执行代码,如果时间未到就要醒需要使用interrupt()来随时唤醒
    • interrupt(): 唤醒正在睡眠的程序,调用interrupt()方法,会使得sleep()方法抛出InterruptedException异常,当sleep()方法抛出异常就中断了sleep的方法,从而让程序继续运行下去
    public static void main(String[] args) throws Exception {
        Thread thread0 = new Thread(()-> {
            try {
                System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t太困了,让我睡10秒,中间有事叫我,zZZ。。。");
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t被叫醒了,又要继续干活了");
            }
        });
        thread0.start();
    
        // 这里睡眠只是为了保证先让上面的那个线程先执行
        Thread.sleep(2000);
    
        new Thread(()-> {
            System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t醒醒,醒醒,别睡了,起来干活了!!!");
            // 无需获取锁就可以调用interrupt
            thread0.interrupt();
        }).start();
    }
    

    这里写图片描述

    3. wait() 与 notify()

    wait、notify和notifyAll方法是Object类的final native方法。所以这些方法不能被子类重写,Object类是所有类的超类,因此在程序中可以通过this或者super来调用this.wait(), super.wait()

    • wait(): 导致线程进入等待阻塞状态,会一直等待直到它被其他线程通过notify()或者notifyAll唤醒。该方法只能在同步方法中调用。如果当前线程不是锁的持有者,该方法抛出一个IllegalMonitorStateException异常。wait(long timeout): 时间到了自动执行,类似于sleep(long millis)
    • notify(): 该方法只能在同步方法或同步块内部调用, 随机选择一个(注意:只会通知一个)在该对象上调用wait方法的线程,解除其阻塞状态
    • notifyAll(): 唤醒所有的wait对象

    注意:

    • Object.wait()和Object.notify()和Object.notifyall()必须写在synchronized方法内部或者synchronized块内部
    • 让哪个对象等待wait就去通知notify哪个对象,不要让A对象等待,结果却去通知B对象,要操作同一个对象

    Object

    public class Object {
    	public final void wait() throws InterruptedException;
    	public final native void wait(long timeout) throws InterruptedException;
    	public final void wait(long timeout, int nanos) throws InterruptedException;
    	
    	
    	public final native void notify();
    	public final native void notifyAll();
    }
    

    WaitNotifyTest

    public class WaitNotifyTest {
        public static void main(String[] args) throws Exception {
            WaitNotifyTest waitNotifyTest = new WaitNotifyTest();
            new Thread(() -> {
                try {
                    waitNotifyTest.printFile();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
    
            }).start();
    
            new Thread(() -> {
                try {
                    waitNotifyTest.printFile();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
    
            }).start();
    
            new Thread(() -> {
                try {
                    System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t睡觉1秒中,目的是让上面的线程先执行,即先执行wait()");
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
    
                waitNotifyTest.notifyPrint();
            }).start();
        }
    
        private synchronized void printFile() throws InterruptedException {
            System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t等待打印文件...");
            this.wait();
            System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t打印结束。。。");
        }
    
        private synchronized void notifyPrint() {
            this.notify();
            System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t通知完成...");
        }
    }
    
    

    这里写图片描述

    wait():让程序暂停执行,相当于让当前,线程进入当前实例的等待队列,这个队列属于该实例对象,所以调用notify也必须使用该对象来调用,不能使用别的对象来调用。调用wait和notify必须使用同一个对象来调用。
    这里写图片描述

    this.notifyAll();
    这里写图片描述

    4. sleep() 与 wait()
    ① Thread.sleep(long millis): 睡眠时不会释放锁
    public static void main(String[] args) throws InterruptedException {
        Object lock = new Object();
    
        new Thread(() -> {
            synchronized (lock) {
                for (int i = 0; i < 5; i++) {
                    System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t" + i);
                    try { Thread.sleep(1000); } catch (InterruptedException e) { }
                }
            }
        }).start();
    
        Thread.sleep(1000);
    
        new Thread(() -> {
            synchronized (lock) {
                for (int i = 0; i < 5; i++) {
                    System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t" + i);
                }
            }
        }).start();
    }
    

    因main方法中Thread.sleep(1000)所以上面的线程Thread-0先被执行,当循环第一次时就会Thread.sleep(1000)睡眠,因为sleep并不会释放锁,所以Thread-1得不到执行的机会,所以直到Thread-0执行完毕释放锁对象lock,Thread-1才能拿到锁,然后执行Thread-1;
    这里写图片描述

    5. wait() 与 interrupt()

    wait(): 方法的作用是释放锁,加入到等待队列,当调用interrupt()方法后,线程必须先获取到锁后,然后才抛出异常InterruptedException 。注意: 在获取锁之前是不会抛出异常的,只有在获取锁之后才会抛异常

    所有能抛出InterruptedException的方法都可以通过interrupt()来取消的

    public static native void sleep(long millis) throws InterruptedException;
    public final void wait() throws InterruptedException;
    public final void join() throws InterruptedException;
    public void interrupt();
    

    notify()和interrupt()
    从让正在wait的线程重新运行这一点来说,notify方法和intterrupt方法的作用有些类似,但仍有以下不同之处:

    • notify/notifyAll是java.lang.Object类的方法,唤醒的是该实例的等待队列中的线程,而不能直接指定某个具体的线程。notify/notifyAll唤醒的线程会继续执行wait的下一条语句,另外执行notify/notifyAll时线程必须要获取实例的锁

    • interrupte方法是java.lang.Thread类的方法,可以直接指定线程并唤醒,当被interrupt的线程处于sleep或者wait中时会抛出InterruptedException异常。执行interrupt()并不需要获取取消线程的锁。

    • 总之notify/notifyAll和interrupt的区别在于是否能直接让某个指定的线程唤醒、执行唤醒是否需要锁、方法属于的类不同

    6. interrupt()

    有人也许认为“当调用interrupt方法时,调用对象的线程就会InterruptedException异常”, 其实这是一种误解,实际上interrupt方法只是改变了线程的“中断状态”而已,所谓中断状态是一个boolean值,表示线程是否被中断的状态。

    public class Thread implements Runnable {
    	public void interrupt() {
    		中断状态 = true;
    	}
    	
    	// 检查中断状态
    	public boolean isInterrupted();
    	
    	// 检查中断状态并清除当前线程的中断状态
    	public static boolean interrupted() {
    		// 伪代码
    		boolean isInterrupted = isInterrupted();
    		中断状态 = false;
    	}
    }	
    

    假设Thread-0执行了sleep、wait、join中的一个方法而停止运行,在Thread-1中调用了interrupt方法,此时线程Thread-0的确会抛出InterruptedException异常,但这其实是sleep、wait、join中的方法内部会对线程的“中断状态”进行检查,如果中断状态为true,就会抛出InterruptedException异常。假如某个线程的中断状态为true,但线程体中却没有调用或者没有判断线程中断状态的值,那么线程则不会抛出InterruptedException异常。

    isInterrupted() 检查中断状态
    若指定线程处于中断状态则返回true,若指定线程为非中断状态,则反回false, isInterrupted() 只是获取中断状态的值,并不会改变中断状态的值。

    interrupted()
    检查中断状态并清除当前线程的中断状态。如当前线程处于中断状态返回true,若当前线程处于非中断状态则返回false, 并清除中断状态(将中断状态设置为false), 只有这个方法才可以清除中断状态,Thread.interrupted的操作对象是当前线程,所以该方法并不能用于清除其它线程的中断状态。

    interrupt()与interrupted()

    • interrupt():打断线程,将中断状态修改为true
    • interrupted(): 不打断线程,获取线程的中断状态,并将中断状态设置为false

    这里写图片描述

    public class InterrupptTest {
        public static void main(String[] args) {
            Thread thread = new Thread(new MyRunnable());
            thread.start();
            boolean interrupted = thread.isInterrupted();
            // interrupted=false
            System.out.println("interrupted=" + interrupted);
    
            thread.interrupt();
    
            boolean interrupted2 = thread.isInterrupted();
            // interrupted2=true
            System.out.println("interrupted2=" + interrupted2);
    
            boolean interrupted3 = Thread.interrupted();
            // interrupted3=false
            System.out.println("interrupted3=" + interrupted3);
        }
    }
    
    class MyRunnable implements Runnable {
        @Override
        public void run() {
            synchronized (this) {
                try {
                    wait();
                } catch (InterruptedException e) {
                    // InterruptedException	false
                    System.out.println("InterruptedException\t" + Thread.currentThread().isInterrupted());
                }
            }
        }
    }
    
    

    这里写图片描述

    ② object.wait(long timeout): 会释放锁
    public class SleepWaitTest {
        public static void main(String[] args) throws InterruptedException {
            SleepWaitTest object = new SleepWaitTest();
    
            new Thread(() -> {
                synchronized (object) {
                    System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t等待打印文件...");
                    try {
                        object.wait(5000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t打印结束。。。");
                }
            }).start();
    
    		 // 先上面的线程先执行
            Thread.sleep(1000);
    
            new Thread(() -> {
                synchronized (object) {
                    for (int i = 0; i < 5; i++) {
                        System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t" + i);
                    }
                }
            }).start();
        }
    }
    

    因main方法中有Thread.sleep(1000)所以上面的线程Thread-0肯定会被先执行,当Thread-0被执行时就拿到了object对象锁,然后进入wait(5000)5秒钟等待,此时wait释放了锁,然后Thread-1就拿到了锁就执行线程体,Thread-1执行完后就释放了锁,当等待5秒后Thread-0就能再次获取object锁,这样就继续执行后面的代码。wait方法是释放锁的,如果wait方法不释放锁那么Thread-1是拿不到锁也就没有执行的机会的,事实是Thread-1得到了执行,所以说wait方法会释放锁

    这里写图片描述

    ③ sleep与wait的区别
    • sleep在Thread类中,wait在Object类中
    • sleep不会释放锁,wait会释放锁
    • sleep使用interrupt()来唤醒,wait需要notify或者notifyAll来通知
    5.join()

    让当前线程加入父线程,加入后父线程会一直wait,直到子线程执行完毕后父线程才能执行。当我们调用某个线程的这个方法时,这个方法会挂起调用线程,直到被调用线程结束执行,调用线程才会继续执行。

    将某个线程加入到当前线程中来,一般某个线程和当前线程依赖关系比较强,必须先等待某个线程执行完毕才能执行当前线程。一般在run()方法内使用

    join() 方法:

    public final void join() throws InterruptedException {
            join(0);
    }
    
    
    public final synchronized void join(long millis) throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;
    
        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }
    
        if (millis == 0) {
        	 // 循环检查线程的状态是否还活着,如果死了就结束了,如果活着继续等到死
            while (isAlive()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }
    
    
    public final synchronized void join(long millis, int nanos) throws InterruptedException {
    
        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }
    
        if (nanos < 0 || nanos > 999999) {
            throw new IllegalArgumentException("nanosecond timeout value out of range");
        }
    
        if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
            millis++;
        }
    
        join(millis);
    }
    
    

    JoinTest

    public class JoinTest {
        public static void main(String[] args) {
            new Thread(new ParentRunnable()).start();
        }
    }
    
    class ParentRunnable implements Runnable {
        @Override
        public void run() {
            // 线程处于new状态
            Thread childThread = new Thread(new ChildRunable());
            // 线程处于runnable就绪状态
            childThread.start();
            try {
                // 当调用join时,parent会等待child执行完毕后再继续运行
                // 将某个线程加入到当前线程
                childThread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            for (int i = 0; i < 5; i++) {
                System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "父线程 running");
            }
        }
    }
    
    class ChildRunable implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 5; i++) {
                try { Thread.sleep(1000); } catch (InterruptedException e) {}
                System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "子线程 running");
            }
        }
    }
    
    

    程序进入主线程,运行Parent对应的线程,Parent的线程代码分两段,一段是启动一个子线程,一段是Parent线程的线程体代码,首先会将Child线程加入到Parent线程,join()方法会调用join(0)方法(join()方法是普通方法并没有加锁,join(0)会加锁),join(0)会执行while(isAlive()) { wait(0);} 循环判断线程是否处于活动状态,如果是继续wait(0)知道isAlive=false结束掉join(0), 从而结束掉join(), 最后回到Parent线程体中继续执行其它代码。

    在Parent调用child.join()后,child子线程正常运行,Parent父线程会等待child子线程结束后再继续运行。
    这里写图片描述

    • join() 和 join(long millis, int nanos) 最后都调用了 join(long millis)。

    • join(long millis, int nanos)和join(long millis)方法 都是synchronized。

    • join() 调用了join(0),从源码可以看到join(0)不断检查当前线程是否处于Active状态。

    • join() 和 sleep() 一样,都可以被中断(被中断时,会抛出 InterrupptedException 异常);不同的是,join() 内部调用了wait(),会出让锁,而 sleep() 会一直保持锁。

    6. yield()

    交出CPU的执行时间,不会释放锁,让线程进入就绪状态,等待重新获取CPU执行时间,yield就像一个好人似的,当CPU轮到它了,它却说我先不急,先给其他线程执行吧, 此方法很少被使用到,

    /**
     * A hint to the scheduler that the current thread is willing to yield
     * its current use of a processor. The scheduler is free to ignore this
     * hint.
     *
     * <p> Yield is a heuristic attempt to improve relative progression
     * between threads that would otherwise over-utilise a CPU. Its use
     * should be combined with detailed profiling and benchmarking to
     * ensure that it actually has the desired effect.
     *
     * <p> It is rarely appropriate to use this method. It may be useful
     * for debugging or testing purposes, where it may help to reproduce
     * bugs due to race conditions. It may also be useful when designing
     * concurrency control constructs such as the ones in the
     * {@link java.util.concurrent.locks} package.
     */
    public static native void yield();
    

    这里写图片描述

    public static void main(String[] args) {
        new Thread(new Runnable() {
            int sum = 0;
            @Override
            public void run() {
                long beginTime=System.currentTimeMillis();
                for (int i = 0; i < 99999; i++) {
                    sum += 1;
                    // 去掉该行执行用2毫秒,加上271毫秒
                    Thread.yield();
                }
                long endTime=System.currentTimeMillis();
                System.out.println("用时:"+ (endTime - beginTime) + " 毫秒!");
            }
        }).start();
    }
    

    sleep(long millis) 与 yeid()

    • sleep(long millis): 需要指定具体睡眠的时间,不会释放锁,睡眠期间CPU会执行其它线程,睡眠时间到会立刻执行
    • yeid(): 交出CPU的执行权,不会释放锁,和sleep不同的时当再次获取到CPU的执行,不能确定是什么时候,而sleep是能确定什么时候再次执行。两者的区别就是sleep后再次执行的时间能确定,而yeid是不能确定的
    • yield会把CPU的执行权交出去,所以可以用yield来控制线程的执行速度,当一个线程执行的比较快,此时想让它执行的稍微慢一些可以使用该方法,想让线程变慢可以使用sleep和wait,但是这两个方法都需要指定具体时间,而yield不需要指定具体时间,让CPU决定什么时候能再次被执行,当放弃到下次再次被执行的中间时间就是间歇等待的时间
    7. setDaemon(boolean on)

    线程分两种:

    • 用户线程:如果主线程main停止掉,不会影响用户线程,用户线程可以继续运行。
    • 守护线程:如果主线程死亡,守护线程如果没有执行完毕也要跟着一块死(就像皇上死了,带刀侍卫也要一块死),GC垃圾回收线程就是守护线程
    public static void main(String[] args) {
        Thread thread = new Thread() {
            @Override
            public void run() {
                IntStream.range(0, 5).forEach(i -> {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "\ti=" + i);
                });
            }
        };
        thread.start();
    
    
        for (int i = 0; i < 2; i++) {
            System.out.println(Thread.currentThread().getName() + "\ti=" + i);
        }
        System.out.println("主线程执行结束,子线程仍然继续执行,主线程和用户线程的生命周期各自独立。");
    }
    

    这里写图片描述

    public static void main(String[] args) {
        Thread thread = new Thread() {
            @Override
            public void run() {
                IntStream.range(0, 5).forEach(i -> {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "\ti=" + i);
                });
            }
        };
        thread.setDaemon(true);
        thread.start();
    
    
        for (int i = 0; i < 2; i++) {
            System.out.println(Thread.currentThread().getName() + "\ti=" + i);
        }
        System.out.println("主线程死亡,子线程也要陪着一块死!");
    }
    

    这里写图片描述

    六 线程组

    可以对线程分组,分组后可以统一管理某个组下的所有线程,例如统一中断所有线程

    public class ThreadGroup implements Thread.UncaughtExceptionHandler {
        private final ThreadGroup parent;
        String name;
        int maxPriority;
        
        Thread threads[];
        
        private ThreadGroup() {
            this.name = "system";
            this.maxPriority = Thread.MAX_PRIORITY;
            this.parent = null;
        }
        
        public ThreadGroup(String name) {
            this(Thread.currentThread().getThreadGroup(), name);
        }
        
        public ThreadGroup(ThreadGroup parent, String name) {
            this(checkParentAccess(parent), parent, name);
        }
        
        // 返回此线程组中活动线程的估计数。 
        public int activeGroupCount();
        
        // 中断此线程组中的所有线程。
        public final void interrupt();
    }
    
    public static void main(String[] args) {
        String mainThreadGroupName = Thread.currentThread().getThreadGroup().getName();
        System.out.println(mainThreadGroupName);
        // 如果一个线程没有指定线程组,默认为当前线程所在的线程组
        new Thread(() -> { }, "my thread1").start();
    
        ThreadGroup myGroup = new ThreadGroup("MyGroup");
        myGroup.setMaxPriority(5);
    
        Runnable runnable = () -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            ThreadGroup threadGroup = Thread.currentThread().getThreadGroup();
            String groupName = threadGroup.getName();
            ThreadGroup parentGroup = threadGroup.getParent();
            String parentGroupName = parentGroup.getName();
            ThreadGroup grandpaThreadGroup = parentGroup.getParent();
            String grandpaThreadGroupName = grandpaThreadGroup.getName();
            int maxPriority = threadGroup.getMaxPriority();
            int activeCount = myGroup.activeCount();
    
            // system <- main <- MyGroup(1) <- my thread2
            System.out.println(MessageFormat.format("{0} <- {1} <- {2}({3}) <- {4}",
                    grandpaThreadGroupName,
                    parentGroupName,
                    groupName,
                    activeCount,
                    Thread.currentThread().getName()));
        };
    
        new Thread(myGroup, runnable, "my thread2").start();
    }
    

    线程组与线程组之间是有父子关系的,自定义线程组的父线程组是main线程组,main线程组的父线程组是system线程组。
    这里写图片描述

    分享一个朋友的人工智能教程(请以“右键”->"在新标签页中打开连接”的方式访问)。比较通俗易懂,风趣幽默,感兴趣的朋友可以去看看。
    展开全文
  • 多线程

    千次阅读 2019-09-08 00:26:12
    多线程 进程: 程序执行系统分配给我们的程序的一个内存空间,启动时,会创建一个主线程 简单来说一个程序的执行就是一个进程. 分时操作: 将处理机的运行时间分成很短的时间片,按时间片轮流把处理机分配给各联机作业...
  • 面试题:线程是什么?多线程

    万次阅读 多人点赞 2019-08-04 17:26:25
    为什么使用多线程?多线程的示例以及解决方案?线程池是什么? 一.线程是什么? 在Thread类中有这样的明确定义:线程是程序中执行的线程,Java虚拟机允许程序同时运行多个执行线程。 怎么创建一个线程呢? Thread中...
  • 多线程实现的四种方式

    万次阅读 2018-07-31 16:52:46
    实现多线程有以下四种方式: 1. 继承Thread类 2.实现Runnable接口 3.实现Callable接口 4.线程池:提供了一个线程队列,队列中保存着所有等待状态的线程。避免了创建与销毁额外开销,提高了响应的速度。 体系...
  • 多线程的几种实现方式

    万次阅读 2019-02-25 15:33:38
    上篇文章总结了下关于线程池很干,很干的干货,这次想着顺便连其他实现多线程的三种方式也汇总下吧! java多线程的几种实现方式: 1.继承Thread类,重写run方法 2.实现Runnable接口,重写run方法,实现Runnable...
  • 线程和进程都是现在电脑概念里比较时髦的用语,什么是多线程,什么是多进程?本文详细的给您介绍一下,希望能增进您对当代电脑技术的了解,有不到之处,还往高手予以更正。 一. 进程与线程进程(英语:Process,中国...
  • 多线程与高并发编程之基础知识(上)

    万次阅读 多人点赞 2018-10-04 20:46:33
    使用多线程,第一步就是需要知道如何实现自定义线程,因为实际开发中,需要线程完成的任务是不同的,所以我们需要根据线程任务来自定义线程,那么,JDK提供了三种自定义线程的方式,供我们实际开发中使用,来开发出...
  • 秒杀多线程第一篇 多线程笔试面试题汇总

    万次阅读 多人点赞 2013-03-13 10:46:56
    系列中不但会详细讲解多线程同步互斥的各种“招式”,而且会进一步的讲解多线程同步互斥的“内功心法”。有了“招式”和“内功心法”,相信你也能对多线程挥洒自如,在笔试面试中顺利的秒杀多线程试题。 ----------...
  • 3-04多线程相关

    万次阅读 2019-02-17 14:37:20
    多线程是指一个进程(执行中的程序)同时运行多个线程(进程中负责程序执行的执行单元),多线程可以协作完成进程工作,其目的是更好的利用 CPU 资源 Java线程具有五中基本状态 新建状态(New):当线程对象对创建...
  • 秒杀多线程第四篇 一个经典的多线程同步问题

    万次阅读 多人点赞 2012-05-03 09:32:59
    上一篇《秒杀多线程第三篇原子操作 Interlocked系列函数》中介绍了原子操作在多进程中的作用,现在来个复杂点的。这个问题涉及到线程的同步和互斥,是一道非常有代表性的多线程同步问题,如果能将这个问题搞清楚,...
  • 一个简单的C#多线程间同步的例子

    万次阅读 热门讨论 2017-04-25 09:00:01
    本博客有关多线程的文章:C#多线程编程(1):线程的启动多线程编程(2):线程的同步多线程编程(3):线程池ThreadPool多线程编程(4):多线程与UI操作在开发中经常会遇到线程的例子,如果某个后台操作比较费时间,我们...
  • 多线程面试题(值得收藏)

    万次阅读 多人点赞 2019-08-16 09:41:18
    史上最强多线程面试47题(含答案),建议收藏 金九银十快到了,即将进入找工作的高峰期,最新整理的最全多线程并发面试47题和答案总结,希望对想进BAT的同学有帮助,由于篇幅较长,建议收藏后细看~ 1、并发编程三要素?...
  • 多线程和多进程的区别(小结)

    万次阅读 多人点赞 2011-08-15 15:54:54
    很想写点关于多进程和多线程的东西,我确实很爱他们。但是每每想动手写点关于他们的东西,却总是求全心理作祟,始终动不了手。今天终于下了决心,写点东西,以后可以再修修补补也无妨。 一.为何需要多进程(或者...
  • Java多线程学习(吐血超详细总结)

    万次阅读 多人点赞 2017-03-01 22:47:36
    本文主要讲了java中多线程的使用方法、线程同步、线程数据传递、线程状态及相应的一些线程函数用法、概述等。
  • 秒杀多线程第六篇 经典线程同步 事件Event

    万次阅读 多人点赞 2012-06-10 18:42:44
    阅读本篇之前推荐阅读以下姊妹篇:《秒杀多线程第四篇 一个经典的多线程同步问题》《秒杀多线程第五篇 经典线程同步关键段CS》 上一篇中使用关键段来解决经典的多线程同步互斥问题,由于关键段的“线程所有权”特性...
  • Java多线程学习(一)Java多线程入门

    万次阅读 多人点赞 2019-11-08 11:04:20
    Java多线程学习(一)Java多线程入门 Java多线程学习(二)synchronized关键字(1) Java多线程学习(二)synchronized关键字(2) Java多线程学习(三)volatile关键字 Java多线程学习(...
  • java多线程编程实例

    万次阅读 多人点赞 2018-05-25 10:01:22
    这篇文章主要介绍了java多线程编程实例,分享了几则多线程的实例代码,具有一定参考价值,加深多线程编程的理解还是很有帮助的,需要的朋友可以参考下。1.相关知识:Java多线程程序设计到的知识:(1)对同一个数量...
1 2 3 4 5 ... 20
收藏数 1,863,180
精华内容 745,272
关键字:

多线程