秋招结束,回馈牛客4(这次是真的要结束了)

这次是真的要结束秋招了。祝大家新年快乐!!!

本人非科班渣硕,最近陆续收到了携程、京东的offer,就在21春招已经悄然开始的时候,我终于结束了自己的秋招。在20年的最后,还是将之前总结的部分java多线程面试题总结分享一下(如有错误,请各位大佬指正),回馈下广大牛友,同时也感谢牛客网过去一年提供的帮助。在上几篇帖子发出以后,有些22届的学弟私信我如何准备面试中的算法题部分,这个其实很多大佬都总结了很多方法,可以多参考参考他们的想法,其实大家大部分都是刷力扣或者牛客的《剑指offer》,力扣的Hot100等模块的算法题,这些刷的熟练后,再刷其他的题,在刷题之前,还是应该看看数据结构或者算法视频或者课程,不然上来就刷题目的话,可能会比较懵,效率不高。

这里打个广告,最近牛客也联系到了我,因为当时我自己也是看的牛客网左程云老师(左神巨佬)的算法基础的入门和提升课程,感觉收获还是很大,看完再去刷题感觉很多地方更容易理解一些,印象也会更加深刻,所以推荐给大家,
通过我的专属链接可以获得课程的半价优惠(原价399优惠到199;原价599优惠到299),我觉得对于学生党来说,还是挺划算的,毕竟能省点谁愿意多花钱呢。下面是课程的详细介绍,感兴趣的同学可以看一下,不感兴趣的可以直接跳过

入门班 ,这部分课程是讲解基础算法(如排序)和基础数据结构(如链表、二叉树等),非常适合在刷题前学习这个视频,对于入门来说很有帮助,看完这个视频可以刷刷简单题和部分中等难度题;
优惠码:AA13V76
提升班 ,这部分课程包括刷题中高频算法(如动态规划)和高频技巧性数据结构(如哈希表等),再刷完入门班课程和相应题目后,可以通过提升班作为中高等题目的衔接阶段,当时看完感觉收获很大,尤其是从暴力递归转化为动态规划,掌握了思想,再刷此类题目感觉比较得心应手。
优惠码:ATaoRKL
课程链接:https://www.nowcoder.com/courses/cover/live/512?coupon=ATaoRKL
中级班,这部分课程是从经典/最新的真题出发,来讲解高频的算法与数据结构知识点,针对性比较强,感觉比较适合短时间内想快速提高的同学,价格优惠完要比上面的贵100块,性价比较高。
优惠码:AhoGBDV
课程链接: https://www.nowcoder.com/courses/cover/live/501?coupon=AhoGBDV
高级班,这部分课程跟中级班类似,也是从真题角度出发,但是比中级班课程讲解深入,从基础到深挖都有涵盖,对于想冲击大厂及SP的同学会是比较不错的选择;
优惠码:A1nWmQt

java多线程相关面试题总结

资料结构介绍:
前两部分线程基础和高并发总结是关于尚硅谷周阳老师及马士兵教育马士兵老师部分多线程总结,知识点有些以代码形式进一步说明;
后面部分是自己根据大佬面经和自身面试总结的一些知识点和面试题目。

一、线程基础

1.并行与并发

并行:多个任务同时进行,A任务在执行的同时B任务也在执行,需要多核Cpu;

并发:对个任务同时进行申请,但是处理器只能处理一个任务,交替执行这些任务,因为时间间隔很短,看起来像是同时执行,但是在某一个时间点,只能有一个任务在执行。

2.线程实现方式

①实现方式1:继承Thread类,重写run()方法

a.定义一个类来实现Thread类;

b.重写Thread类的run()方法;

c.将需要执行的代码写在run()方法内部

d.创建Thread子类对象交替;

e.启动线程(用对象.start())
public class Test{
	//4创建子类对象
    public static void main(String[] args){
    	     MyThread mt=new MyThread();
        //5启动线程
        mt.start();
        //主线程
        for(int i=0;i<1000;i++){
            System.out.println(5678);
        }
    }
}
//1自定义类继承Thread类
public class MyThread extends Thread{
    //2重写run()方法
	public void run(){
      //3实现方法
    	    for(int i=0;i<1000;i++){  System.out.println(1234);
      }    
    }    
}

f.方式一的匿名内部类形式

new 类名(){    }表示类的子类对象
public class Test{
    public static void main(String[] args){
        //1继承Thread类并创建对象
    	    new Thread(){
          //重写run()方法
           public void run(){
             //实现run()方法
              for(){
               System.out.println(1234);
             }    
          }
        }.start();
        //主线程
        for(int i=0;i<1000;i++){
           System.out.println(5678);
        }
    }
}

②实现方式2:实现Runnable接口,重写run()方法;

a.定义一个类实现Runnable接口;

b.重写run()方法;

c.将要执行的代码写在run()方法内部

d.创建Runnable子类对象

e.将Runnable子类对象当做参数传入到Thread构造器中创建Thread对象

f.开启线程
public class Test{
    public static void main(String[] args){
        //4创建Runnabkle子类对象
        Mythread my=new Mythread();
        //5将Runnable子类对象作为参数传入Thread构造方法中,创建Thread对象
    	Thread mt=new Thread(my);
        //6启动线程
        mt.start();
        //主线程
        for(int i=0;i<1000;i++){
            System.out.println(5678);
        }
    }
}
//1自定义类实现Runnable接口
public class MyThread implements Runnable{
    //2重写run()方法
	public void run(){
        //3实现方法
    	for(int i=0;i<1000;i++){
            System.out.println(1234);
        }    
    }    
}
g.方式2的匿名内部类实现
public class Test{
    public static void main(String[] args){
        Thread th=new Thread(new Runnable(){
        	public void run(){
            	for(int i=0;i<1000;i++){
                    System.out.println(1234);
                }     
            }
        });
        /*
        new Thread(new Runnable(){
                public void run(){
                    for(int i=0;i<1000;i++){
                        System.out.println(1234);
                    }     
                }
            }
        ).start();
        /*
    	th.start();
        //主线程
        for(int i=0;i<1000;i++){
            System.out.println(5678);
        }
    }
}

③实现方式三:实现Callable接口

④线程类的相关方法(补充):

a.设置线程名称

1.通过Thread类的构造方法

public Thread(String name)

2.通过Thread的成员方法

public final void setName(String name)

b.获取线程名称

public final String getName():返回该线程的名称

c.获取当前正在执行的线程

public static Thread currentThread():返回当前正在执行的线程的引用;

3.线程方法

①线程休眠:

public static void sleep(long millis)

在指定的毫秒数以内让当前正在执行的线程休眠(暂停执行);到时间或者notify()/notifyAll()会被唤醒

②守护线程:

public final void setDaemon(boolean on)

将该线程标记为守护线程,当正在运行的线程都是守护线程时,JVM退出;

注:该方法必须在启动线程前调用。

应用:例如QQ聊天,聊天界面为守护线程,QQ程序为非守护线程,当非守护线程的QQ程序退出后,守护线程的聊天界面也退出。

③加入线程(相当于插队):

a.public final void join()

当前线程暂停,等待指定的线程执行结束后,当前线程再继续执行;

b.public final void join(long millis)

当前线程暂停,等待指定的线程执行固定时长后,再继续执行剩余线程。

dafe952b41cb8d834372cd39d0794be.png
public class Test{
	public static void main(String[] arge){
        //线程1
        //这里加final修饰:因为匿名内类在调用方法的局部变量时,局部变量必须加final修饰。
       final Thread t1=new Thread(){
        	public void run(){
            	for(int i=0;i<100;i++){
                	System.out.println("aaa");
                }
            } 
        };
        //线程2
        Thread t1=new Thread(){
        	public void run(){
            	for(int i=0;i<100;i++){
                    //线程2执行后,线程1加入
                    t1.join();
                	System.out.println("bbb");
                }
            } 
        };
        t2.start();
        t1.start();
    }
}

④礼让线程:yield()

public static void yield()

暂停当前正在执行的线程对象,并执行其他线程。(当前线程让出一下CPU

⑤设置线程的优先级

a.public final void setPriority(int newPriority):更改线程的优先级

b.public static final int MAX_PRIORITY:最高优先级为10;

MIN_PRIORITY:最低优先级为1;

NORM_PRIORITY:默认优先级为5;

:设置为最高优先级,也不是先执行完,而是尽可能先执行;

该属性需要在执行前进行设置;跟设置守护线程一样;

⑥isAlive():

测试线程是否处于活动状态,如果线程已经开启且尚未终止,则为活动状态

⑦activeCount():

返回当前线程的线程组中活动线程的数目。

4.线程状态

①新建(初始状态):线程被创建,但是还没有调用start()方法

②运行状态:
a.就绪状态:线程已经启动,但是没有获得CPU的执行权;
b.运行状态:抢到cpu的执行权,正在运行;

③阻塞状态:表示线程阻塞于锁,在synchronized的等待对列中;

④等待状态:表示线程进入等待状态,该状态表示当前线程需要等待其他线程做出一些特定动作(通知或中断)

⑤超时等待状态:不同于等待状态,可以在指定的时间自行返回

⑥终止状态:当前线程执行完毕

0aa5c9d01e3da4ee50319eeac7b86a6.png

5.线程同步

①同步代码块

a.什么时候同步?

并发时,有多段代码需要同时执行,我们希望某一段代码在执行时,不希望其他线程进入到该代码块中,只让某一个线程运行该段代码,这里理解的并不是说,只有这一个线程在执行,CPU不去执行其他的线程。

b.同步代码块

使用Synchronized关键字加上一个锁对象来定义一段代码,称为同步代码块。多个同步代码块如果使用同一个锁对象,那么他们就是同步的。

注:锁对象不能是匿名类对象,锁的是对象,锁的不是代码块。

②同步方法

a.使用synchronized关键字修饰一个方法,该方法中的所有代码都是同步的;

b.实现方式:在方法的返回值前面加上synchronized进行修饰

c.分类:

1.非静态方法:锁对象为当前类对象的实例,实例是存放在堆内存的;

public synchronized void show(){}

2.静态方法:锁对象为当前类的Class对象

public static synchronized show(){}

6.线程安全

①线程安全问题

多个线程并发操作同一个数据,就有可能造成线程安全问题。

②线程死锁

多线程同步的时候,如果同步代码块嵌套,使用相同锁,则有可能造成死锁。

③线程死锁代码举例?
public class DepthLock{
	private static String s1="左";
    private static String s2="右";
    public static void main(String[] args){
        //线程1
    	new Thread(){
        	public void run(){
            	while(true){
                	synchronized(s1){
                        //解释一下:当前第一个线程拿到了s1对象,然后往下执行
                    	System.out.println(this.getName()+s1);
                        //当执行完打印的时候,这个时候如果线程2还没有拿到s2的话,则会继续向下进行,直到打印完
                        //然后释放掉s1;但是如果此时恰好线程2抢到执行权,拿到了S2,则会发生死锁,因为线程2还没有释放
                        //s2,所以线程1拿不到s2,然后,线程1执行不下去,也就没法释放s1,线程2如果想释放s2,就得先拿到s1,
                        //则陷入死锁状态
                        synchronized(s2){
                        	System.out.println(this.getName()+s2);
                        }
                    }
                }
            }
        }.start();
        //线程2
        new Thread(){
        	public void run(){
            	while(true){
                	synchronized(s2){
                    	System.out.println(this.getName()+s2);
                        synchronized(s1){
                        	System.out.println(this.getName()+s1);
                        }
                    }
                }
            }
        }.start();
    }
}

7.线程之间通信

①为什么需要通信

多个线程并发时,默认情况下cpu是随机切换线程的,如果我们希望他们有规律的执行,则需要使用通信。

②如何实现线程通信

a.如果希望线程等待,就调用wait();

b.如果希望唤醒等待的线程,采用notify();

注:这另个方法必须在同步代码块中执行,并且使用同步锁对象来调用

③方法补充(☆):

a.public final void wait():

在其他线程调用此对象的notify()方法或者notifyAll()方法前,导致当前线程等待。

b.public final void notify():

随机唤醒在此对象的监视器上等待的单个线程。

c.public final void notifyAll():

唤醒在此对象的监视器上等待的所有线程。

④三个及以上线程通信

notifyAll()是唤醒所有线程;

如果多个线程之间通信,需要使用notifyAll()通知所有线程,用while来反复判断条件

⑤两个线程轮流打印例子(☆)

public class Demo_notify {
	/**
	 * 两线程之间通讯
	 */
	public static void main(String[] args) {
		final Demo d=new Demo();
		//线程1
		new Thread(){
			public void run(){
                //一定要在循环里执行
				while(true){
					try {
						d.show1();
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			}
		}.start();
		//线程二:
		new Thread(){
			public void run(){
				while(true){
					try {
						d.show2();
					} catch (InterruptedException e) {
						
						e.printStackTrace();
					}
				}
			}
		}.start();
	}

}
//两线程交替进行
class Demo{
	private  int flag=1;
	public void show1() throws InterruptedException{
		synchronized (this) {
            //过程解释:两个线程都开启,然后假设线程1,抢到了执行权,则拿到了锁对象,此时线程2,得不到锁,无法打印
            //然后打印“赵钱孙李”,然后flag为2
            //执行唤醒,此时两个线程都是醒着的,然后,循环进行,然后线程1变为等待状态,然后释放了锁对象。
            //线程2就拿到了锁对象,然后打印“周吴郑王”,然后flag变为1,然后执行了对象的notify(),此时线程1被唤醒
            //这个时候也就是都醒着,然后循环继续,flag不为2,线程2变为等待状态,然后释放锁对象,线程1再继续。
			if (flag!=1) {
				//线程等待
				this.wait();
			}
			System.out.print("赵");
			System.out.print("钱");
			System.out.print("孙");
			System.out.print("李");
			System.out.print("\r\n");
			flag=2;
			this.notify();
		}
	}
	public void show2() throws InterruptedException{
		synchronized (this) {
			if (flag!=2) {
				this.wait();
			}
			System.out.print("周");
			System.out.print("吴");
			System.out.print("郑");
			System.out.print("王");
			System.out.print("\r\n");
			flag=1;
			this.notify();
		}
	}
}

⑥sleep()方法和wait()方法的区别()?

a.sleep()方法必须传入参数,参数为时间,时间到了自动醒来;wait()方法可传可不传,不传即为直接等待,传入之后为参数的时间结束后等待。

b.sleep()方法在同步方法或者同步代码块中不会释放锁;wait()方法在同步方法或者同步代码块中,会释放锁。

c.sleep不需要唤醒;wait()方法需要notify()或者notifyAll()方法唤醒。

8.线程池

①线程池作用?

程序启动一个新的线程成本比较高,使用线程池可以很好地提高性能,尤其是程序中存在大量声明周期较短的线程时,更应该考虑使用线程池,线程池中的每一个线程结束后,并不会死亡,而是再次回到线程池中成为空闲状态。

二、高并发总结

1.CAS相关

①什么是CAS(CAS过程)?

e802133825b7e10ee1d6119297c9cbb.png

a.CAS(compare and swap)过程:

首先当前线程从主物理内存中获取当前值E,然后计算获得结果V,然后比较现在的主内存中的值是否还是E,如果不变,则更新当前值为V,如果当前值不是E,则说明其他线程操作过主内存中的值,则重新获取当前值。重复该过程.

b.补充:compareAndSet()方法(CAS的具体实现)

1.如类AtomicInteger下的:public final boolean compareAndSet(int expect,int update)

如果当前值=预期值,则以原子方式将值设置为给定的更新值。

c.CAS存在问题

1.循环时间长,开销很大,因为底层是do while循环,如果主内存中的值一直被修改,则会一直循环下去

2.只能保证一个共享变量的原子操作

3.ABA问题

d.原子引用(AtomicReference)

如果想要实现自定义类对象的原子性,可以使用原子引用类

023d0b1506adb9f1c226540ca205676.png

②CAS的问题(解释ABA问题)?如何解决?

a.ABA问题,ABA问题:因此CAS需要在操作值的时候,检查值有没有发生改变,如果没有则更新,但是如果值原来是A,变成了B,然后又变成了A,则CAS认为并没有改变。实际上已经改变了。

b.解决方法:加版本号,在读取值的同时,也读取值对象的版本号。如果都没有改变,则进行交换。

c.具体实现:AtomicStampedReference

d.影响:

基本数据类型:在Integer值自增这种操作时,存在ABA问题没有影响。

引用数据类型:而引用数据类型会产生变化。

③CAS底层实现

a.马士兵老师讲解:

1.底层是基于c++指令:lock cmpxchg(compare and exchange)

2.仅仅通过compare and exchange不行的原因(为什么需要加lock):

因为某一个线程将贡献变量(值为0)拷贝回本地内存,然后修改完后,然后取到当前的值为0,然后就将1写入主内存中,此时在比较之后,写入之前被别的线程修改为1,就造成了值覆盖,因此要加lock。

b.周阳老师讲解:

e785b2e6c83ada0baa0fce08df631e5.png

1.CAS的应用

既然AtomicInteger类对象的getAntIncrement能够解决i++在多线程条件下的安全问题(保证原子性),底层依赖的就是CAS原理,如图可以看出底层依赖的是unsafe类中的compareAndSwapInt()方法;

过程为:var1为当前对象,var2为主内存中的值,然后var5为拷贝回本地内存的值(这里的getIntVolatile保证可见性),如果当前var5与var2相等,则将原var5的值+var4(值传的是1),然后成功为ture,while(!)循环结束,返回自增后的结果,如果不同,则重复循环。

1467c1f27043f7700aeb3a64ed3eb2b.png

2.再底层的原理(CAS能够保证原子性的底层原理)

底层依赖的是CPU原语,能够保证一个线程执行的时候,不被其他线程中断。

aa64c4300c4844d675a933d28cac632.png

2.JMM(内存模型)

概述:JMM是一个抽象概念,是一组规则或规范,来定义程序中各变量的访问方式。

①内存模型基础

a.内存模型的介绍

线程之间的共享变量存储在主内存中,每个线程都有一个本地内存,本地内存中存着了该线程读/写共享变量的副本,JMM有三大特性:可见性;原子性;顺序性。

008d50d55a5546058508445913aea19.png

b.可见性:当一个线程将共享变量写入主内存时,JMM保证能够其他线程可见。

c.原子性:一个线程在执行一系列语句时,不能被其他线程所干扰,整个线程语句要么全部执行,要么全部不执行。

d.顺序性: 程序的运行顺序与定义的顺序一致。

3.Volatile相关

①概述

volatile是虚拟机提供的轻量级的同步机制,特点:保证可见性不保证原子性禁止指令重排序

②.保证可见性证明:

将volitile加在变量的前面

5ad2f441d41e4519e4ede4943aea280.png结果:bbd917719e90c53d5b69c187440df85.png
变量未加volatile修饰,主线程感知不到变量的变化,然后一直无限循环中。
faf3ff1cfc4fb4c62d02d3f64b15a8c.png

在变量前面加上volatile进行修饰

c0d8f01703c9a7322eeee2c4bc16bed.png结果:6f3e7eec2459ab75a04f2a80714a852.png

③.不保证原子性

a.什么是原子性

原子性指的是某个线程在操作时,不能够被中断或者分割。

b.volatile不保证原子性的原因?

多个线程操作同一个变量时,正常情况下,应该为每个线程轮流执行,但是中间有可能出现线程被挂起,而唤醒时,没有来得及被通知到主内存中的值改变,造成写覆盖的现象。

67f215a12b6475847c4f26667fad3f5.png

c.不保证原子性的例子:

4559dccf1d6f52ddca14cd31fe047fe.png

执行结果为:18552;如果改为synchronized,则执行结果为20000

a2bf302ecfec01dab118775b4193b33.png结果:image.png

d.如何解决volatile的这种不保证原子性

1采用synchronized对方法进行加锁(“大材小用”)

2采用juc下的atomic类相关子类(如atomicInteger类),其采用的是CAS原理,保证原子性

例子:如下图

3采用LongAdder也可以解决,LongAdder内部采用的是分段锁,分段锁内部采用的是CAS操作,最后获取总和,能够提高整体效率。

7796b01e8498cc1cb8b796f2094ae34.png

补充atomicInteger相关方法

public final void compareAndSwap(int expect,int update)

如果当前值=预期值,则以原子方式更新当前值

public final int getAndIncrement()

以原子方式将当前值+1,相当于i++;

public final int incrementAndGet()

以原子方式将当前值+1,相当于++i;

public final int getAndDecrement()/decrementAndGet()

以原子方式将当前值-1;相当于i--/--i;

构造方法 public AtomicInteger():默认的初始为0;

8b79d532153ceacf803232ec5a65237.png结果:aa3cfdee78078cd4ad12e4c7de9b288.png

e.保证有序性(禁止指令重排序)

1.指令重排序

程序执行时,为了提高性能,编译器和处理器会进行指令的重新排序,一般为以下三种

96c82b64f46ff0a83c94b895719102f.png

①.单线程中,不会发生指令重排序;

②.处理器在执行指令重排时,需要保证指令之间的数据依赖性
public void mySort(){
	int x=11;//语句1
 	int y=12;//语句2
    x=x+5;//语句3
    y=x*x;//语句4
    //即使存在指令重排,这里的语句4不会先执行,因为y和x还没有被声明,即指令重排需保证数据依赖性
}

③.在多个线程交替执行时,由于存在编译器的指令重排优化,两个线程所使用的变量的一致性无法保证的

2.指令重排序代码
//分析:
//假设现在有两个线程,第一个线程进入了方法1,然后由于存在指令重排,flag=true被先执行,然后会被其他线程可见
//此时,线程2抢到了执行权,线程2调用了方法2,然后将a=5输出,我们的意思是让结果等于6,但是结果最终等于5。
public class Demo{
	int a=0;
    boolean flag=false;
    public void fun1(){
        a=1;
        flag=true;
    }    
    public void fun2(){
    	if(flag==true){
        	a=a+5;
            System.out.println("a="+a);
        }
    }
}

3.volatile能够保证禁止指令重排序的原因:因为存在内存屏障cpu指令,内存屏障作用:

①.保证指令按照特定顺序执行;

②.保证某些变量的内存可见性;

f.DCL模式中需要加volatile吗?为什么?

1.单例设计模式

我们有一个类,要求通过new操作符创建对象,在内存中只有这一个对象,从头到尾保证内存中只有这么一个对象,称为单例模式。
//版本1:最简单的单例模式(饿汉式)
public class SingletonDemo1 {
	//创建类中的唯一对象
	private static SingletonDemo1 instence=new SingletonDemo1();
	//构造器私有
	private SingletonDemo1(){
	}
	//其他类获取该对象
	public static SingletonDemo1 getInstence(){
		return instence;
	}
	public static void main(String[] args) {
		SingletonDemo1 s1=SingletonDemo1.getInstence();
		SingletonDemo1 s2=SingletonDemo1.getInstence();
		System.out.println(s1==s2);
	}
}
//版本2:当用到这个对象时,再new出来(懒汉式)
public class SingletonDemo2 {
    //
	private static SingletonDemo2 instence=null;
	//构造器私有
	private SingletonDemo2(){
	}
	//其他类获取该对象
	public static SingletonDemo2 getInstence(){
		//用到的时候再创建对象
		if(instence==null){
			instence=new SingletonDemo2();
		}
		return instence;
	}  
	public static void main(String[] args) {
		//多个线程访问
		for (int i = 0; i < 10; i++) {
			new Thread(String.valueOf(i)){
				public void run(){
					System.out.println(SingletonDemo2.getInstence().hashCode());
				}
			}.start();
		}
	}
}
版本2存在问题(线程不安全):多个线程访问的时候,有可能new出多个对象来

如:第一个线程调用getInstence()方法时,发现为instence为空,new对象的时候,第二个线程进来了,发现instence依然为空,也会new出来对象。

解决方法1:①将getInstence()方法加synchronized()加锁,使只有一个线程同一时间进入这个方法,“大材小用”

解决方法2:DCL(double check lock)双重检锁机制

2.什么是DCL模式(Double Check Lock 双端检锁机制)?
//版本3:DCL模式
public class SingletonDemo3 {
	private static volatile SingletonDemo3 instence=null;
	//构造方法私有,不让其他类再new出来对象
	private SingletonDemo3(){}
	public static  SingletonDemo3 getInstence(){
		//DCL模式(在锁的前后进行双重判断)
		if(instence==null){ 
        //当第一个线程进来了,发现为空,此时第二个线程也进来了,然后线程1抢到了锁,然后获取了new对象,然后释放锁
        //问题:线程2是什么时候知道已经new出来了呢?(因为volatile保证了可见性)
        //此时第二个线程抢到了锁,然后进入了,此时线程2发现instence已经不为空,则直接退出,释放锁。
			synchronized(SingletonDemo3.class){
				if(instence==null){
					instence=new SingletonDemo3();
				}
			}
		}
		return instence;
	}
	public static void main(String[] args) {
		for (int i = 0; i < 10; i++) {
			new Thread(String.valueOf(i)){
				public void run(){
					System.out.println(SingletonDemo3.getInstence().hashCode());
				}
			}.start();
		}
	}
}

3.需要加volatile的理由?

7b1926934d9bba4e3653b0785c8a091.png

需要加volatile,因为有可能存在指令重排序,采用volatile的话,能够禁止指令重排。

假设线程1一开始判断当前为空,然后堆空间就new出来了对象,进行了半初始化,此时如果发生了指令重排,则会先执行建立引用指令,即将单一实例指向该地址。然后此时线程2进来,进入最外层的if()判断,发现引用不为空,则直接使用半初始化状态的值。

4.Synchronized相关

①.同步和非同步方法是否可以同时调用?

public class 可重入锁 {
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		Phone p=new Phone();
		new Thread("t1"){
			public void run(){
				p.sendMess();
			}
		}.start();
		System.out.println("======================");
		new Thread("t2"){
			public void run(){
				p.sendMess();
			}
		}.start();
	}	
}
//线程操纵资源类
class Phone{
	public synchronized void sendMess(){
		System.out.println(Thread.currentThread().getName()+"\t sendMess()");
		sendEail();
	}
	public void sendEail(){
		System.out.println(Thread.currentThread().getName()+"\t sendEmail()");
	}
}

②.可重入(一个加锁的方法可以调用另外一个加锁的方法(锁的是同一个对象))

public class 可重入锁 {
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		Phone p=new Phone();
		new Thread("t1"){
			public void run(){
				p.sendMess();
			}
		}.start();
		System.out.println("======================");
		new Thread("t2"){
			public void run(){
				p.sendMess();
			}
		}.start();
	}	
}
//线程操纵资源类
class Phone{
	public synchronized void sendMess(){
		System.out.println(Thread.currentThread().getName()+"\t sendMess()");
		sendEail();
	}
	public synchronized void sendEail(){
		System.out.println(Thread.currentThread().getName()+"\t sendEmail()");
	}
}

③.Synchronized与异常的关系

执行过程中,如果出现异常,锁会被释放。(1/0异常)
import java.util.concurrent.TimeUnit;
public class 异常释放锁 {
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		Test t=new Test();
		new Thread("t1"){
			public void run(){
				t.fun();
			}
		}.start();
		System.out.println("===================");
		new Thread("t2"){
			public void run(){
				t.fun();
			}
		}.start();
	}
}
//线程操作资源类
class Test{
	int count=0;
	boolean flag=true;
	public synchronized void fun(){
		System.out.println(Thread.currentThread().getName()+"start");
		while(flag){
			count++;
			System.out.println(Thread.currentThread().getName()+"count="+count);
			//线程休眠1秒钟
			try {
				TimeUnit.SECONDS.sleep(1);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			if(count==5){
				//添加异常
				int i=1/0;
				System.out.println(i);
			}
			if(count==10){
				//为了停止循环
				flag=false;
			}
			
		}
	}
}

运行结果:159d540b9c5989ad54c30cbf6dd1b5f.png

④.Synchronized锁对象相关

a.synchronized修饰代码块:锁的是()内的对象;

b.synchronized修饰方法:锁的是当前类的实例对象;

c.synchronized修饰静态方法:锁的是当前类的Class对象;

d.synchronized()括号中对象不能是String常量,Integer对象及Long类型等基本数据类型。

⑤.什么时候用自旋锁,什么时候用重量级锁?

获取锁的线程执行时间短,且总的线程数较少时,采用自旋锁;

如果锁的线程执行时间长,且总的线程数较多时,采用重量级锁。

⑥synchronized的优化

a.锁的细化(细化锁)

当一个方法中,存在前后两段业务逻辑代码,中间是需要加锁的代码,不应该将整个方法加锁,而是应该在该段代码加锁。

b.锁的粗化(粗化锁

当某一个方法中存在很多的细化锁,可以考虑将其转为粗化锁,即将锁加在整个方法上。

⑦synchronized注意事项

a.锁的如果是当前对象o,o的属性值改变,不影响锁的使用,如果对象o变成另外一个对象,则锁对象发生改变。

解决方法:在对象变量o之前加final修饰
import java.util.concurrent.TimeUnit;
public class 对象O改变 {
	/*final*/ Object o=new Object();
	 void m(){
		 synchronized(o){
			 while(true){
				try {
					TimeUnit.SECONDS.sleep(1);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				System.out.println(Thread.currentThread().getName());
			} 
		}
	}
	 
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		对象O改变 t=new 对象O改变();
		Thread t1=new Thread("t1"){
			public void run(){
				t.m();
			}
		};
		t1.start();
		//这个的意思是让当前执行的线程暂停下来????
		try {
			TimeUnit.SECONDS.sleep(3);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		Thread t2=new Thread("t2"){
			public void run(){
				t.m();
			}
		};
		//此时将o对象变成别的对象,如果没有这句话,则线程2永远也没有机会执行。
		t.o=new Object();
		//然后t2就会拿到这把锁
		t2.start();
	}
}

5.锁相关

①.锁的升级过程(简述一下锁的升级过程)

a.无锁状态:刚刚new出来对象的时候,并没有任何锁相关的事情。

b.偏向锁状态:当一个线程访问同步块并获取锁时,上偏向锁指的是将markword中的线程ID改为自己的线程ID的过程。

c.轻量级锁(自旋锁):当另外一个线程来进行竞争时,就会由升级为轻量级锁,同时撤销偏向锁。

线程在自己的虚拟机栈的栈帧生成LockRecord ,用CAS操作将markword设置为指向自己这个线程的LockRecord的指针,设置成功者得到锁。

d.重量级锁:当有较多的线程同时运行时,会造成部分线程自旋时间较长,严重消耗CPU资源,此时JVM会升级为重量级锁。其他线程进入到该锁的等待队列中,按照顺序依次获取锁。

对象头(8字节64位)信息(补充)

markword.png

第一步:无锁状态时,倒数第三位表示是否为偏向锁,再向前4位是GC分代年龄,最高位15,再向前的31位是对象的哈希值。

第二步:偏向锁状态,将Markword中的线程ID改为自己的线程id

第三步:轻量级锁状态:每个线程在自己的虚拟机栈的栈帧中,生成自己的LockRecord,然后采用CAS的方式将markword中的线程ID改为指向自己LR的指针。

②公平锁与非公平锁

a.公平锁:多个线程按照申请锁的顺序来获取锁,遵循先来后到的原则。

b.非公平锁:多个线程获取锁的顺序并不是按照申请的先后顺序执行的,后申请的比先申请的优先获得锁,在高并发的情况下,会造成优先级反转和饥饿现象。好处:增强线程的性能。

c.举例子

1.synchronized是非公平锁

2.ReentrantLock通过构造器可以设置是否公平,默认是非公平锁。

可重入锁(递归锁)

a.定义:线程可以进入任何一个它已经拥有的锁的同步代码块中。

b.作用:避免产生死锁。
public class 可重入锁 {
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		Phone p=new Phone();
		new Thread("t1"){
			public void run(){
				p.sendMess();
			}
		}.start();
		System.out.println("======================");
		new Thread("t2"){
			public void run(){
				p.sendMess();
			}
		}.start();
	}	
}
//线程操纵资源类
class Phone{
	public synchronized void sendMess(){
		System.out.println(Thread.currentThread().getName()+"\t sendMess()");
		sendEail();
	}
	public synchronized void sendEail(){
		System.out.println(Thread.currentThread().getName()+"\t sendEmail()");
	}
}
输出结果:7cd13c726ba8ce4d2f1702bea4e3db7.png
先执行t1线程,然后此时并没有执行到sendEmail()方法,然后被主线程抢到执行,主线程执行完后,然后线程t1又抢到了执行权,执行完sendEmail()方法,然后线程t2执行。

④自旋锁(乐观锁/CAS)

a.定义:尝试获取锁的线程不会立即阻塞,而是采用自旋的方式来获取锁。好处就是减少线程上下文的切换,弊端是自旋的过程会消耗CPU资源。

⑤读写锁

a.独占锁定义:指该锁同一时间只能由一个线程所拥有

b.共享锁:指的是该锁可以被多个线程所持有

c.举例:ReentrantReadWriteLock

读锁是共享锁,允许多个线程同时来读取数据。

写锁是独占锁,当写入数据时,其他读线程不能获取锁,(等我写完之后,大家可以一块读),不然会造成读取数据的不完整。

读-写,写-读,写-写都是互斥的。
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReentrantReadWriteLockStudy {
	
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		TestRRW t=new TestRRW();
		//定义一个读写锁对
		ReentrantReadWriteLock readWriteLock=new ReentrantReadWriteLock();
		Lock readLock=readWriteLock.readLock();
		Lock writeLock=readWriteLock.writeLock();
		
		//创建18个读锁
		for (int i = 0; i < 18; i++) {
			new Thread(){
				public void run(){
					t.read(readLock);
				}
			}.start();;
		}
		
		for (int i = 0; i < 2; i++) {
			new Thread(){
				public void run(){
					t.write(writeLock, 2);;
				}
			}.start();;
		}
	}

}
//线程操作资源类
class TestRRW{
	public int val=0;
	//读方法(传入锁)
	public void read(Lock lock){
		lock.lock();
		try {
			//模拟读的过程
			TimeUnit.SECONDS.sleep(1);
			System.out.println("读完了");
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}finally{
			lock.unlock();
		}
	}
	
	public void write(Lock lock,int num){
		lock.lock();
		try {
			//模拟写的过程
			TimeUnit.SECONDS.sleep(1);
			this.val=num;
			System.out.println("写完了");
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}finally{
			lock.unlock();
		}
	}
	
}

结果:因为读锁是共享锁所以18个线程仅有1秒就读完了,然后,两个写锁用了两秒写完了;18个读线程比加普通lock的效率高很多的。

⑥ReentrantLock

a.能够保证可重入性

b.采用tryLock表示该线程在指定时间内是否能够拿到该锁

c.通过传入构造器参数来实现公平锁/非公平锁(默认为非公平锁)。

6.阻塞队列

①定义(什么是阻塞队列)

首先阻塞队列是一个队列,在队列一段线程1相对列中插入元素,线程2在队列另一端获取元素,当队列中元素已满时,线程1阻塞,当队列中元素为空时,线程2阻塞。

85907e24d7ec23ca6efa31195843b0e.png

②作用

在某些情况下,我们需要挂起线程,然后再需要的时候将其唤醒,而阻塞队列的作用为不需要人为的挂起和唤醒线程,能够实现自我管理线程。

③阻塞队列架构(BlockingQueue接口

继承自queue接口,间接继承自collection

④常见阻塞队列接口实现类

a.ArrayBlcokingQueue

底层以数组实现的阻塞队列

b.linkedBlockingQueue

底层以链表实现的阻塞队列

c.同步synchronousBlockingQueue

特点:SynchronousQueue是一个不存储元素的BlcokingQueue,每一次put都需要take()的完成,否则不能添加元素;每一个take()都需要一个put()才行,否则取不到元素(进一个,我立马取一个)。
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.TimeUnit;
public class SynchronousBlcokingQueueDemo {
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		BlockingQueue<String> bq=new SynchronousQueue<>();
		//线程1负责添加
		new Thread("t1"){
			public void run(){
				try {
					bq.put("A");
					System.out.println(Thread.currentThread().getName()+"\t put A");
					
					bq.put("B");
					System.out.println(Thread.currentThread().getName()+"\t put B");
					
					bq.put("C");
					System.out.println(Thread.currentThread().getName()+"\t put C");
				
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}

			}
		}.start();
		//线层2负责取,但是需要每隔3秒钟取一次
		new Thread("t2"){
			public void run(){
				try {
					//每隔3秒钟我取一次(为了防止刚加进去该没来的及打印,就被取出)
					TimeUnit.SECONDS.sleep(3);
					System.out.println(Thread.currentThread().getName()+"\t "+ bq.take());
					
					TimeUnit.SECONDS.sleep(3);
					System.out.println(Thread.currentThread().getName()+"\t "+ bq.take());
					
					TimeUnit.SECONDS.sleep(3);
					System.out.println(Thread.currentThread().getName()+"\t "+ bq.take());
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
		}.start();
	}
}

输出:image.png

⑤阻塞队列接口的最常用的方法

16954f67a4c66544f26e6515a1173d0.png

a.抛出异常类

1.boolean add(e):如果超过了阻塞队列的长度,则会抛出异常

2.E e remove():如果阻塞队列为空,则会抛出异常

3.element():获取阻塞队列的第一个元素,如果没有,抛出异常
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class 抛出异常类 {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		BlockingQueue<String> bq=new ArrayBlockingQueue<>(3);
		System.out.println(bq.add("a"));
		System.out.println(bq.add("b"));
		System.out.println(bq.add("c"));
		System.out.println("=========");
		System.out.println(bq.remove());
		System.out.println(bq.remove());
		System.out.println(bq.remove());
		System.out.println(bq.remove());
	}
}

b.特殊值类

1.boolean offer(e):添加元素,满了的话不会报异常,而是false;

2.poll():移除元素,对列空了就会弹出null

3.peek():获取但不移除第一个元素。
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class 抛出异常类 {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		BlockingQueue<String> bq=new ArrayBlockingQueue<>(3);
		System.out.println(bq.offer("a"));
		System.out.println(bq.offer("b"));
		System.out.println(bq.offer("c"));
		System.out.println(bq.offer("x"));
		System.out.println("=========");
		System.out.println(bq.poll());
		System.out.println(bq.poll());
		System.out.println(bq.poll());
		System.out.println(bq.poll());
	
	}
}

c.阻塞类

1.void put():向队列中添加元素,如果满了,则会一直等待

2.void take():从对列中取出元素,如果为空, 则会一直等待
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class 常用方法介绍 {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		BlockingQueue<String> bq=new ArrayBlockingQueue<>(3);
		try {
			bq.put("a");
			bq.put("b");
			bq.put("c");
			
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		try {
			bq.take();
			bq.take();
			bq.take();
			bq.take();
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
}

d.超时阻塞

1.boolean offer(e,time,timeUnit):向对列中添加元素,如果超过时间time,还没有成功,则不会再添加

2.E poll(time,timeUnit):取出队列中的元素,time时间内没有取出,则不会再取
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
public class 常用方法介绍 {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		BlockingQueue<String> bq=new ArrayBlockingQueue<>(3);
		try {
			System.out.println( bq.offer("a",3, TimeUnit.SECONDS));
			System.out.println( bq.offer("b",3, TimeUnit.SECONDS));
			System.out.println( bq.offer("c",3, TimeUnit.SECONDS));
			System.out.println( bq.offer("d",3, TimeUnit.SECONDS));
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
}

⑥应用

a.线程通信之生产者-消费者模式

题目:给定一个变量num=0;两个线程交替执行+1,-1操作,共执行5次

1.传统版v1.0(synchronized和wait()及notify()唤醒,记住:多线程的循环判断用while而不是if

代码:交替打印AB CD
import java.util.concurrent.TimeUnit;

public class 交替打印 {
	//实现两个线程交替打印ABCD
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		Print p=new Print();
		new Thread("t1"){
			public void run(){
				while(true){
					p.print1();		
				}
			}
		}.start();
		
		new Thread("t2"){
			public void run(){
				while(true){
					p.print2();
				}
			}
		}.start();
	}
}
class Print{
	//方法1
	boolean isPrint=false;
	public synchronized void print1(){
		while(isPrint==true){
			try {
				this.wait();
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		try {
			TimeUnit.SECONDS.sleep(1);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		System.out.println("AB");
		isPrint=true;
		this.notify();
	}
	
	public synchronized void print2(){
		//判断
		while(isPrint==false){
			try {
				this.wait();
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		try {
			TimeUnit.SECONDS.sleep(1);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		//干活
		System.out.println("CD");
		//通知
		isPrint=false;
		this.notify();
	}
}

2.传统版v2.0(lock和await()及signal()方法

补充一:Lock接口

定义:Lock 接口的实现允许锁在不同的作用范围内获取和释放,并允许以任何顺序获取和释放多个锁。

方法:

void lock():获取锁

void unlock():释放锁

Condition newCondition():返回绑定在此lock实例的新的Condition对象

补充二:ReentrantLock类

定义:一个可重入的Lock实现类,可以通过传参表示是否是公平锁,公平锁不能保证线程调度的公平性(底层依赖的是CAS)

方法:

void lock():获取锁

void unlock():释放锁

补充三:Condition接口

定义:Lock替代了Synchronized,而condition接口替代了Object对象监视器;Condition实例实质上被绑定到一个锁上

方法:

void await():造成当前线程在接到信号或者被中断之前一直处于等待状态;

void signal():随机唤醒一个等待的线程。

void signalAll():唤醒所有等待的线程。

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class 传统版2 {
	//给定一个变量num=0;两线程交替+1;减1操作,共执行5次
	//步骤
	//一:线程		操作(定义方法)		资源类(高内聚,低耦合)
	//二:判断		干活				通知
	//三:
	public static void main(String[] args) {
		shareData sd=new shareData();
		
		new Thread("t1"){
			public void run(){
				//执行5次
				for (int i = 0; i <5; i++) {
					sd.increment();
				}
			}
		}.start();
		
		new Thread("t2"){
			public void run(){
				//执行5次
				for (int i = 0; i <5; i++) {
					sd.decrement();
				}
			}
		}.start();
		
	}

}
//线程操作资源类
class shareData{
	public  int num=0;
	private Lock lock=new ReentrantLock();
	private Condition condition=lock.newCondition();
	public void increment(){
		//加锁
		lock.lock();
			try {
					//先判断
					while(num!=0){
						condition.await();
					}
					//干活
					num++;
					System.out.println(Thread.currentThread().getName()+"\t"+num);
					//通知
					condition.signal();
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}finally{
				lock.unlock();
			}
	}
	
	public void decrement(){
		//加锁
		lock.lock();
			try {
					//先判断
					while(num==0){
						condition.await();
					}
					//干活
					num--;
					System.out.println(Thread.currentThread().getName()+"\t"+num);
					//通知
					condition.signal();
				
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}finally{
				lock.unlock();
			}
	}
}

3.阻塞队列版本

//后期再补充

b.线程池

c.消息中间件

⑦synchronized和lock的区别(使用lock有什么好处)

a.原始构成:

Synchronized:是关键字,属于JVM层面;wait/notify()只有在同步方法或者同步代码块中使用才可以

ReentrantLock是Lock接口的具体实现类,属于API层面。
//lock代替sunchronized
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ReplaceSynchronized {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		TestR t=new TestR();
		for (int i = 0; i < 200; i++) {
			new Thread(){
				public void run(){
					t.fun();
				}
			}.start();
		}
		while(Thread.activeCount()>1){
			Thread.yield();
		}
		System.out.println(t.num);
	}	
	
}
class TestR{
	//加volatile保证可见性
	public volatile int num=0;
	//这里采用ReentrantLock替代synchronized(这种写法成功了)
	Lock lock=new ReentrantLock();
	public void fun(){
		try {
			lock.lock();
			TimeUnit.NANOSECONDS.sleep(1);
			for (int i = 0; i < 1000; i++) {
				num++;
			}
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}finally{
			lock.unlock();
		}
	
	}
}

b.释放锁

Synchronized:不需要手动释放锁,当执行完代码时,会自动释放锁;

ReentrantLock:需要手动释放锁,采用lock()和unlock()并配合try catch使用(try用睡眠方法调试出)。

c.是否可以中断

Synchronized:不可以中断,除非遇到异常或者正常结束

ReentrantLock:可以中断。

d.加锁是否公平

Synchronized:非公平锁

ReentrantLock:默认为非公平锁,传入参数能够实现公平锁;但是锁公平不能保证线程调度的公平性。

e.锁绑定多个Condition

Synchronized:没有;

ReentrantLock用于实现分组唤醒需要唤醒的线程,实现精准唤醒,而Synchronized每次只能全部唤醒或者随机你唤醒一个。

f.代码演示:多个线程之间通讯(ReentrantLock比synchronized方便之处)

题目:A线程打印5次,B线程打印10次,C线程打印15次,交替打印。
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class 多线程通信 {
	//题目:A线程打印5次,B线程打印10次,C线程打印15次,交替打印,
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		shareResourse sr=new shareResourse();
		new Thread("A"){
			public void run(){
				while(true){
					sr.print5();		
				}
			}
		}.start();
		new Thread("B"){
			public void run(){
				while(true){
					sr.print10();		
				}
			}
		}.start();
		new Thread("C"){
			public void run(){
				while(true){
					sr.print15();		
				}
			}
		}.start();
	}

}
class shareResourse{
	//标志位
	private int num=1;
	Lock lock=new ReentrantLock();
	Condition a=lock.newCondition();
	Condition b=lock.newCondition();
	Condition c=lock.newCondition();
	
	public void print5(){
		//加锁
		lock.lock();
		try {
			//判断
			while(num!=1){
				a.await();
			}
			//干活
			for (int i = 0; i <5; i++) {
				System.out.println(Thread.currentThread().getName()+"\t"+i);
			}
			//让A歇会
			TimeUnit.SECONDS.sleep(1);
			//更换标志位
			num=2;
			//唤醒B,让B干活
			b.signal();
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}finally{
			lock.unlock();
		}
	}
	public void print10(){
		//加锁
		lock.lock();
		try {
			//判断
			while(num!=2){
				b.await();
			}
			//干活
			for (int i = 0; i <10; i++) {
				System.out.println(Thread.currentThread().getName()+"\t"+i);
			}
			//让B歇会
			TimeUnit.SECONDS.sleep(1);
			//更换标志位
			num=3;
			//唤醒C,让C干活
			c.signal();
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}finally{
			lock.unlock();
		}
	}
	public void print15(){
		//加锁
		lock.lock();
		try {
			//判断
			while(num!=3){
				c.await();
			}
			//干活
			for (int i = 0; i <15; i++) {
				System.out.println(Thread.currentThread().getName()+"\t"+i);
			}
			//让c歇会
			TimeUnit.SECONDS.sleep(1);
			//更换标志位
			num=1;
			//唤醒a,让a干活
			a.signal();
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}finally{
			lock.unlock();
		}
	}
}

⑧callable接口

a.特点:
采用callable接口创建线程,可以获取返回值,便于对线程异常进行跟踪

b.用法:

1.定义一个类实现callable接口,然后重写里面的call方法;

2.将callable具体实现类作为参数传入FutureTask构造方法中,间接实现Runnable接口。

3.将FutureTask对象传入Thread的构造方法中,然后完成线程创建,然后启动

c.补充:

1.FutureTask<T>

可取消的异步计算,继承自Future类,可使用FutureTask包装Callable或者Runnable接口。FutureTask类实现了Runnable接口。则可以作为创建线程的传入参数。

2.FutureTask与callable关系

FutureTask构造方法可以传入callable接口的具体实现类

Public FutureTask(Callable<T> c):创建一个FutureTask对象,一旦运行,则执行指定的call方法。

3.FutureTask的方法补充

public V get():如有必要,等待计算完成,并返回最终结果。因此需要将其放到最后

public boolean isDone():如果任务已完成返回true,如果被中断或者取消,则也返回true.

常用的API方法

1.T call()方法:计算结果,如果无法计算,抛出异常

d.应用

b8efb4efb5e008e21ff0b03f0d363b6.png

实现不同线程计算不同值,最后再合并计算结果。

代码如下:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;

public class CallableDemo {
	//callable实现创建线程
	public static void main(String[] args) throws InterruptedException, ExecutionException {
		Test t=new Test();
		FutureTask<Integer> ft=new FutureTask<>(t);
		Thread t1=new Thread(ft, "t1");
		t1.start();
		Integer i=100;
		Integer i2=ft.get();
		
		System.out.println(i+i2);
	}
}
//第一步:实现callable接口,重写call()方法
class Test implements Callable<Integer>{
	//重写call()方法
	public Integer  call(){
		//模拟相乘运行时长
		try {
			TimeUnit.SECONDS.sleep(3);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return 1024;
	}
}

7.并发工具类

①countDownLatch(倒计时发射)

a.定义:同步辅助类,在完成一组正在其他线程中的操作之前,允许一个或多个线程一致等待

b.使用步骤:

1.首先用给定值初始化countDownLatch;

2.调用countDown()方法,然后每当一个线程执行完后,就-1;

3.采用await()方***一直阻塞,直到countDownlatch的值减到0;

c.代码演示
public class CountDLDemo {
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		CountDownLatch cd=new CountDownLatch(6);
		for (int i = 0; i < 6; i++) {
			new Thread(String.valueOf(i)){
				public void run(){
					System.out.println(Thread.currentThread().getName()+"\t 同学上完自习,走了");
					cd.countDown();
				}
			}.start();
		}
		try {
            //后面的主线程需要等待前面的六个线程执行完才能执行。
			cd.await();
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		System.out.println(Thread.currentThread().getName()+"\t 班长锁门了");
	}

}

②cyclicBarrier(人满了把栅栏推到,开始跑)

a.定义:可循环使用的屏障,主要是让一组线程达到一个屏障前被阻塞,直到最后一个线程达到屏障时,屏障才会被打开,被拦截的线程才会继续进行,如果想让达到一定数量后执行某种操作,可以在构造器的第二个参数传入执行的Runnable子类对象。

b.使用:构造函数CyclicBarrier(int nums,Runnable barrier-Action)

当阻塞线程达到nums时,会优先执行barrierAction

阻塞采用的是 cb.await();

c.代码(集齐七颗龙珠,召唤神龙)
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBDemo1 {
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		CyclicBarrier cb=new CyclicBarrier(7,new Runnable(){
			public void run(){
				System.out.println("召唤神龙");
			}
		});
		for (int i = 0; i < 7; i++) {
			new Thread(String.valueOf(i)){
				public void run(){
					System.out.println(Thread.currentThread().getName());
					try {
						cb.await();
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					} catch (BrokenBarrierException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
				}
			}.start();
		}
	}
}

//例子2:人满发车
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
//人满发车
public class CyclicBarrierDemo2 {
	
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		CyclicBarrier cb=new CyclicBarrier(20, new Runnable(){
			public void run(){
				System.out.println("20人满,发车");
			}
		});
		
		for (int i = 0; i < 100; i++) {
			new Thread(){
				public void run(){
					try {
						cb.await();
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					} catch (BrokenBarrierException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
				}
			}.start();
		}
	}
}

d.应用场景

可以用于多线程计算数据,最后合并计算结果的场景。

③控制线程数的Semaphore(6辆车只有3个车位)

a.定义:用来控制同时访问特定资源的线程数量,通过协调各个线程,保证合理利用资源(限流)。

b.使用方法:

1.public void auquire():从此信号量获取一个许可,在提供一个许可之前将一直阻塞。

2.public void relese():释放一个许可,将许可返回给信号量。

3.构造方法:public Semaphore(int nThreads,boolean fair):创建具有给定许可数和给定公平设置的Semaphore。公平的意思是:允许线程按照先来后到的顺序获取许可证。

c.代码:
public class SemaphoreDemo {
	//模拟总共有6辆车,然后只有三个停车位置
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		//这里模拟只有三个车位
		Semaphore s=new Semaphore(3, true);
		//模拟存在六辆车
		for (int i = 0; i < 6; i++) {
			new Thread(String.valueOf(i)){
				public void run(){
					//申请车位
					try {
						//申请到车位
						s.acquire();
						System.out.println(Thread.currentThread().getName()+"\t 号车已经进来");
						TimeUnit.SECONDS.sleep(3);
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}finally{
                        //释放许可
						s.release();
						System.out.println(Thread.currentThread().getName()+"\t 号车已经离开");
					}
				}
			}.start();
		}
		
	}
}

运行结果:image.png

④线程间交换数据的Exchanger()

用于两个线程之间交换数据。如:两游戏玩家交换装备。

8.线程池

①定义:

a.线程池是什么?

程序启动一个新的线程比较耗费资源,使用线程池可以提高性能,尤其是存在大量声明周期比较短的线程时,线程池中的线程执行完后,并不会死亡,而是会再次返回到线程池中成为空闲状态,以备复用。

b.线程池的特点?

1.线程复用

2.控制最大并发数

3.管理线程

②作用:

a.降低资源消耗,通过重复利用已创建的线程降低线程创建和销毁造成的消耗

b.提高响应速度,任务到达时,任务不需要等到线程创建就能立即执行

c.可以有效管理线程,线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,利用线程池可以做到统一管理,分配。

③架构

2b9dfb59c84113db4779e41308e40ff.png

a.整体架构

整体继承自Executor接口,java中提供了Executors服务类。ThreadPoolExecutor类继承了接口ExecutorService接口,后面的接口类似于list,可以作为线程池创建左侧的变量使用。

通过Executors提供了8种创建线程池的方法,常用的有如下三种

b.通过Executor常用的创建线程池的方式

1.Executors.newFixedThreadPoll(int nThreads):创建固定数量的线程池,控制最大并发数,超出的线程等待

f49be55482c61e4520594fd748891dc.png
底层实现是依赖于ThreadPollExecutor类的构造方法,参数中传的是LinkedBlockingQueue阻塞队列。

2.Executors.newSingleThreadPoll():创建一个单线程化的线程池,保证任务按序进行。适用于一个任务一个线程执行的场景。

6701b4516f516312ecf6dce82470608.png

底层依赖的还是ThreadPollExecutor类的构造方法,参数中传的是LinkedBlockingQueue阻塞队列。

3.Executors.newCacheThreadPoll():创建一个没有固定数量的线程池。如果线程数超过了预期,就回收线程,如果没有可回收的,就创建新的线程。

1c2b9cd04e37d1a691a34c5d64953c2.png

底层依赖的是SynchronousQueue,即来一个任务,我创建一个队列,然后如果线程空闲超过60s,则回收。适用于执行较多线程生命周期短的任务

④线程池的七大参数介绍(ThreadPollExecutor构造器的七大参数)

a.概述

f0bfff8c082ca86be84449ff573f7c5.png

b.各参数介绍

31303dae0eb2fc18be08e74b0ef274a.png

1.corePollSize:核心线程数量

2.maximunPollSize:最大的线程数量

3.keepAliveTime:多余的空闲线程存活时长

4.unit:keepAliveTime时间的单位

5.workQueue:任务队列,存储被提交但是尚未被执行的任务

6.threadFactor:生成线程池中工作线程的线程工厂。

7.handler:拒绝策略:当线程数达到最大线程数量,并且任务队列已经满了,拒绝更多线程进入线程池中。

⑤底层实现原理

1.概述:

e04096b19ed3d48893a92d7dd30d206.png

5bacfe1158a183555ba364e3ddc1746.png

2.执行过程

a.当创建了线程池后,会等待提交的任务申请

b.当调用execute()方法添加一个任务时,线程池进行如下判断:

①:如果正在运行的线程数小于核心线程数,将创建新的线程运行这个任务;

②:如果正在运行的线程数大于或者等于核心线程数,则将任务放入阻塞队列中;

③:如果阻塞队列已满且正在运行的线程数小于最大线程数量,则创建非核心线程执行任务;

④:如果阻塞队列已满并且线程运行数等于最大线程数,则执行饱和拒绝测策略。

c.当一个线程完成任务时,会从阻塞队列中取出下一个任务来执行。

d.当一个空闲线程超过一定的时间,线程池进行判断,如果当前线程数大于核心线程数,则会进行线程回收。

3.自定义线程(手写线程池)

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ThreadPollByhands {
	
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		ExecutorService threadpoll=new ThreadPoolExecutor(
				2, 
				5, 
				1L,
				TimeUnit.SECONDS,
				new LinkedBlockingQueue<>(3),
				Executors.defaultThreadFactory(),
				new ThreadPoolExecutor.AbortPolicy());
		
		//模拟十个办理业务人员
		for (int i = 0; i < 10; i++) {
			
			threadpoll.execute(new Runnable(){
				public void run(){
					System.out.println(Thread.currentThread().getName()+"\t 办理业务");
				}
			});
		}
		threadpoll.shutdown();
	}
}

4.如何合理配置线程池

a.cpu密集型:该任务需要大量的运算,没有阻塞,CPU一直全速运行

通过Runtime.getRuntime().availableProcessors()查看CPU核心数。

核心线程数=CPU核数+1;尽量减少cpu的切换

b.IO密集型:IO密集型任务线程并不是一直在执行,因此可以设置为CPU核心数*2

9.多线程与集合类

①并发修改异常

a.ArrayList举例子
import java.util.ArrayList;
//ArrayList为线程不安全类,因为其add方法没有加锁
public class ArrayListDemo {
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		ArrayList<String> al=new ArrayList<>();
		for (int i = 0; i < 30; i++) {
			new Thread(){
				public void run(){
					al.add(String.valueOf(Math.random()));
					System.out.println(al);
				}
			}.start();
		}
	}
}

b.解决措施:

1.因为vector方法都是Synchronized修饰的,因此可以采用vector来代替ArrayList();

2.可以利用Collections(集合的辅助工具类)来将ArrayList变成相线程安全的。即 Collections.synchronizedList<List<T> list>替代ArrayList.
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Vector;

//ArrayList为线程不安全类,因为其add方法没有加锁
public class ArrayListDemo {
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		//ArrayList<String> al=new ArrayList<>();
		//List<String> al=new Vector<>();
		List<String> al=Collections.synchronizedList(new ArrayList<>());
		for (int i = 0; i < 30; i++) {
			new Thread(){
				public void run(){
					al.add(String.valueOf(Math.random()));
					System.out.println(al);
				}
			}.start();
		}
	}
}

3.利用JUC下的copyOnWriteArrayList<E>(写时复用集合类)

底层原理:copyOnWrite是写时复制的容器,向容器中添加元素的时候,不是直接添加,而是将当前容器object[]拷贝复制到一个新的容器object[] newElement内,然后向新的容器内添加元素,添加完之后,再将原容器的引用指向新的容器,重读过程完成添加。

好处:采用读写分类的思想,保证并发安全性

底层源码:

86bf42dfece29b1e88e8f35e0bd4213.png

②集合不安全之Set

a.HashSet不安全(并发修改异常)

b.HashSet不安全的解决措施

1.采用collections工具类将HashSet转换为线程安全的集合(Collections.synchronizedSet(new HashSet<>())

2.采用JUC并发包下的写时复用集合类copyOnWrite集合类

Set<String> set=new copyOnWriteArraySet<>();
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;

public class HashSetDemo {
	public static void main(String[] args) {
		//情况一:并发修改异常
		//HashSet<String> set=new HashSet<>();
		//解决一:采用collections工具类将其转换为线程安全的
		//Set<String> set=Collections.synchronizedSet(new HashSet<>());
		//解决二:采用JUC并发包下的copyOnWriteArraySet类。
		Set<String> set=new CopyOnWriteArraySet<>();
		for (int i = 0; i < 30; i++) {
			new Thread(){
				public void run(){
					set.add(String.valueOf(Math.random()));
					System.out.println(set);
				}
			}.start();
		}
	}

}

c.HashSet底层的数据结构

数据结构:底层依赖的是HashMap,初始容量为10,扩容因子为0.75。HashMap中的键相当于HashSet中的数据,值为固定的Object常量。

2da4f4605829da7b734b28d8dc5bfe4.png

③集合不安全之Map

a.HashMap线程不安全
import java.util.HashMap;
import java.util.Map;
public class HashMapDemo {
	public static void main(String[] args) {
		
		Map<Long, String> map=new HashMap<>();
		for (int i = 0; i < 30; i++) {
			new Thread(String.valueOf(i)){
				public void run(){
					map.put(Thread.currentThread().getId(),String.valueOf(Math.random()));
					System.out.println(map);
				}
			}.start();
		}
	}
}

b.解决措施

采用线程安全的ConcurrentHashMap
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class HashMapDemo {
	public static void main(String[] args) {
		Map<Long, String> map=new ConcurrentHashMap<>();
		for (int i = 0; i < 30; i++) {
			new Thread(String.valueOf(i)){
				public void run(){
					map.put(Thread.currentThread().getId(),String.valueOf(Math.random()));
					System.out.println(map);
				}
			}.start();
		}
	}
}

c.HashMap底层数据结构

详见java集合类总结。

三、JMM相关面试题

1.CAS是什么?

CAS是一种更新的原子操作机制,主要过程是:当某个线程获取到主内存***享变量B,并将共享变量拷贝到自己的工作内存,然后将共享变量改为A,在写回主内存时,需要检查当前主内存中的变量是否还是B,如果是B,再将其改为A,如果不是,则需要读取当前最新值,重复上述过程。

2.线程与进程的区别(高频)

①定义

进程是正在执行的程序,是系统分配资源的基本单元。

线程是进程内不同的执行路径,是操作系统独立调度的基本单位。

一个进程中可以包含多个线程,多个线程并发执行。

②拥有资源

进程是资源分配的基本单元,但是线程不拥有资源,线程可以访问属于进程的资源。

③调度

线程是独立调度的基本单元,在一个进程中,从一个线程切换到另一个线程不需要进程切换,而从一个进程中的线程切换到另一个进程中的线程,需要进程的切换。

④性能消耗

创建和撤销进程时,系统要为之分配或者回收资源,因此产生的开销要大于创建和销毁线程。

⑤通信方面

线程可以通过直接读写同一个进程中的数据进行通信,而进程通信需要借助IPC。

3.CAS的问题解决策略

①ABA问题:

采用版本号机制解决。

②只能够保证一个共享变量的原子操作:

采用加锁的方式解决

③多线程状态下,有可能存在长时间的自旋:

这个时候升级为重量级锁。

4.JMM模型

概述:JMM定义了线程和主内存之间的关系:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存中存储了该线程读/写共享变量的副本,本地内存是JMM的一个抽象概念。

5.JMM存在问题

存在写覆盖的现象。

6.JMM特点/性质

原子性:是指一个操作是不可中断的,要么全部执行成功要么全部执行失败,即使在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程所干扰。

可见性:当一个线程修改了共享变量后,其他线程能够立即获取到这个修改。

顺序性:程序的执行顺序与编写顺序是一致的。

指令重排序:我们编写的程序转化为计算机执行的程序过程中,为了能让程序更加高效的执行,存在编译器优化重排序指令级并行重排序内存系统重排序

四、线程通信相关面试题

1.Synchronized关键字

①Synchronized作用

保证多个线程之间访问共享资源的同步性;synchronized可以保证被它修饰的方法或者代码块同一时间只能被一个线程所访问。在jdk1.6之后,对锁的实现进行了大量的优化,如采用偏向锁,自旋锁等减少开销。

②synchronized关键字三种常用方式

a.修饰同步代码块

指定加锁的对象,进入同步代码块前,需要获取对象的锁

b.修饰静态方法

锁的是当前类的Class对象,

c.修饰成员方法

锁的是当前类的实例,进入同步代码之前,要获取当前类的实例对象的锁

③synchronized和volatile的区别?

a.作用不同

synchronized关键字的作用是保证多线程访问数据的同步性

volatile关键字的作用是保证共享变量的可见性。

b.作用域

synchronized可以修饰方法和代码块

volatile只能够修饰变量

c.是否能够保证原子性(作用效果)

synchronized能够保证原子性

volatile不能够保证原子性

d.阻塞情况

多线程访问volatile不会发生阻塞;

多线程访问synchronized会发生阻塞;

④Synchronized底层实现

每个对象都有一个monitor监视器,锁的实现通过monitorenter和monitorexit实现,方法解释如下:

1.monitorenter方法:线程执行monitorenter指令时,会尝试获取monitor的所有权:

①如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者;

②如果线程已经占用了该monitor,只是重新进入,则进入数+1;

③如果其他线程已经占用了该monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权;

2.monitorexit方法:

执行该方法的线程必须是objecter所对应的monitor的所有者。执行指令时,monitor的进入数减1,如果减1后进入数为0,线程直接退出monitor,不再是该monitor的所有者,其他线程会尝试获取该monitor的所有权。

⑤Synchronized特点

a.同步

b.可重入

c.非公平

d.悲观锁

2.volitile关键字

①volitile是什么?

是java虚拟机提供的轻量级的同步机制。

②特点

a.内存可见性

一个线程修改了共享变量的值,其他线程是立即可见的。

b.禁止指令重排序

cpu和编译器在程序运行期间,会对其进行优化,为了保证效率,可能会对指令进行重新排序,但是在多线程环境下,容易产生线程安全问题,因此采用volatile修饰能够保证进行指令重排序。

c.不保证原子性

③什么是内存可见性?

内存可见性指的是:当一个线程将共享变量拷贝回工作内存并修改后,将修改后的变量写回主物理内存,其他线程能够第一时间内被通知到该变量已经被修改。这种性质被称为内存可见性。

④volitile是如何保证内存可见性的?

volatile保证可见性的原因在于其与内存屏障有关;

内存屏障的作用:确保特定操作执行的顺序,影响数据的可见性,强制更新一次不同的cpu缓存。

当线程A修改完主物理内存***享变量后,会强制令其他线程中的共享变量的值无效,然后迫使其他线程读取最新的修改后的值。

写一个volatile时,JMM会首先会修改线程工作内存的值,然后刷新主物理内存的值;


image

读一个volatile变量时,JMM会强制令其他线程中的本地变量无效,然后从主内存中读取新的变量值。

⑤volatile的具体应用场景有哪些?

a:volatile修饰主物理内存共享变量,为防止多线程并发安全问题,元素递增,采用AutomicInteger等原子类;

b:单例模式中,实例变量需要用volitile修饰;

⑥volatile为什么不能保证原子性?结合场景描述

原子性指的是某个线程在操作时,不能被中断或者是分割;

多个线程操作同一个变量时,正常情况下,应该为每个线程轮流执行,但是中间有可能出现线程被挂起,而当线程被唤醒时,没有来得及被通知到内存中的变量的值改变,然后就写回主物理内存,造成了写覆盖的现象。

如当前有两个线程A,B;两个线程操作共享变量+1操作,共享变量采用volatile进行修饰,初始为0,此时线程A执行加1操作,此时线程B可能被挂起,当线程B被唤醒时,还没来得及获取到共享变量的值时,仍然认为当前的值为0,于是进行了加1操作,此时,造成了写覆盖,本来是为2,但是现在为1,这种现象是因为volatile不保证原子性。

⑦volatile是如何保证禁止指令重排序的?

a.内存屏障定义及作用:

内存屏障又称为内存栅栏,是一条CPU指令,主要作用有两个:

①保证特定操作执行的顺序;

②保证某系某些变量的内存可见性;

b.如何保证内存可见性

由于编译器和处理器都能执行指令重排序优化,如果在指令间插入一条内存屏障,则不管什么指令都不能和这条内存屏障指令重排,也就是通过插入内存屏障,就能禁止在内存屏障前后的指令进行重排优化。

具体实现:

1在每个volatile的写操作前后加内存屏障;

前面加内存屏障,作用是防止上面的普通写和下面的的volatile写重排序;

下面的内存屏障,作用是防止volatile写与下面可能有的volatile写/读重排序;

2在每个volatile的读操作之后加两道内存屏障

第一个内存屏障的作用,禁止下面所有的普通读操作与volatile读操作重排序;

第二个内存屏障的作用,禁止下面的所有的普通写操作与volatile读操作重排序;

image

3.java线程的状态和怎么转化的?

a.新建状态:通过new状态创建一个新的线程,但是还没有启动;

b.运行状态:调用线程的start()方法启动线程,线程进入到运行状态,此运行又分为两个子状态,一个是等待获取CPU的执行权,另外一个是目前获取到CPU的执行权正在运行;

c.阻塞状态:表示线程阻塞于锁,在synchronized的等待队列中,当获取到锁之后,转为运行状态。

d.等待状态:调用o.wait(),t.join(),LockSupport.park()后线程进入到等待状态,当出现o.notify(),o.notifyAll()或者是LockSupport.unPark()后,线程进入到运行状态。

e.超时-等待状态:调用sleep(time),wait(time),join(time)及LockSupport.parkUnit()后进入到超时等待状态,不同于等待状态,不需要其他线程操作,时间到了之后会自动进入到运行状态。

f.终止状态:线程执行完成或者因为异常而被终止,则进入到终止状态。

4.一个等待获取synchronize锁情况下,该线程处于什么转态?

处于线程阻塞状态,当获取到锁后,转换为运行状态。

5.同步和异步

同步:发送一个请求,需要等待返回,然后才能发送下一个请求,存在等待过程;

如:多个线程访问一个数据,如果需要以某种顺序确保该数据在某一时刻只能被一个线程读写,如果不控制,则会造成安全问题,比如加synchronized同步锁机制,在线程A访问数据的时候,线程B无法访问到该数据,只能等线程A访问完成后,线程B访问。

异步:发送一个请求,不需要等待返回,随时可以发送下一个请求,不存在等待过程。

6.java的线程模型

1.内核线程模型

2.用户线程模型

3.混合线程模型

7.协程

在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。

8.JMM为什么线程有工作内存?

因为如果线程不采用自己工作内存,所有线程都直接读写主内存中的变量,会造成前后数据不一致问题,如线程A修改变量的同时,线程B也来读这个变量,但是读到了修改前的值,再次读取时,造成数据不一致问题。

9.线程实现四种方式及区别?

a.继承Thread类,重写run()方法;

b.实现Runnable接口,重写run()方法;

c.实现callable接口,重写call()方法;

d.通过创建线程池的方法创建线程;

e.四种实现方式的不同:

继承T和实现R相比,实现R更加灵活,因为可以继承其他类;

实现Callable接口比前两种方式的主要区别在于call()方法存在返回值,而run()方法不存在返回值;

线程池的方法主要的区别在于创建线程后,当线程执行完成后,不会立即被销毁,而是再次回到线程池中成为空闲状态,避免频繁创建销毁线程造成内存开销。

10.线程之间的通信方式?

a.采用共享变量的方式

即采用volitile加AtomicInteger方式

b.采用synchronized方式

采用synchronized修饰同步代码块,并且使用wait()方法促使对象阻塞,然后使用notify()方法唤醒;

c.采用lock加锁的方式

采用lock加锁,然后对锁绑定多个condition对象,然后采用await()方法阻塞线程,采用signal唤醒线程

最后采用unlock()方法释放锁;

d.采用join()方法

当前线程A在执行时,如果调用线程B的join()方法,则会跳到线程B执行,线程B执行完后,线程A再继续执行。

11.sleep()和wait()及join()的区别

a.传入参数

sleep()必须传入参数,传入的参数为线程休眠时间,时间到了之后,线程会恢复为运行状态;而wait()方法可传入参数也可以不用传入参数,wait()如果不传入参数,则会一直被处于等待状态,直到通过notify()/notifyAll()方法唤醒后进入到运行状态;而传入参数的话,时间到了会自动转为转为运行状态;join()方法也是可传参可不传,与wait()方法类似,可通过notify()或者notifyAll()方法唤醒,时间到了会自动转为运行状态。

b.释放锁

在同步方法或者同步代码块中,调用sleep()方法不会释放锁;

调用了wait()方***释放锁资源;join()方法也会释放锁;

c.唤醒问题

sleep()方法不需要被唤醒;而wait()和join方法需要被唤醒;

五、锁相关

1.锁机制

同下面11锁分类部分。

2.悲观锁和乐观锁的区别?分别适合什么场景?

悲观锁其实是一种悲观思想,线程每次去获取数据的时候都认为别人会修改数据,因此在整个执行过程中都需要上锁,这样其他线程进行读写操作时都会被阻塞直到获取锁,java中的悲观锁其实就是synchronized。

乐观锁: 乐观锁其实是一种乐观思想,线程每次去获取数据都认为别人没有修改,因此不会上锁,但是在更新时,会先判断当前值与之前获取数据的时候值是否相等,如果相等,则认为其他线程没有动过,则将值更新。否则会再次进行读取。乐观锁一般都是基于CAS实现的,CAS是一种更新的原子操作,比较当前值与传入的值是否一样,一样的话则更新。

3.悲观锁和乐观锁的适用场景?

java中的乐观锁:在java中java.util.concurrent.atomic包下的原子类就是使用了乐观锁的一种实现方式。(CAS机制和版本号机制)。

java中的悲观锁:synchronized/ReentrantLock;

4.java有哪些悲观锁?

a.synchronized

b.ReentrantLock

ReentrantLock使用了setExclusiveOwnerThread方法,这个方法将某一个线程设置为独占线程,也就是说的互斥锁;该线程占用了该方法之后,其他线程无法占用,不符合乐观锁的定义。

5.Sychronized和Lock(ReentrantLock)的区别

①.结构层面

synchronize是关键字,属于JVM层面

Lock是一个类,属于API层面

②.释放锁

synchronized不需要手动释放锁,当线程执行完任务后,会自动释放锁。

Lock类需要手动释放锁,当通过lock方法添加锁后,需要配合try catch机制通过unlock()释放锁。

③.是否可以被中断

synchronized不能够被中断,除非是异常或者错误导致被迫中止

reentrantLock可以被中断;

④.是否公平

synchronized是非公平锁;

reentrantLock默认是非公平锁,但是可以通过构造器中传入true,实现公平锁,但是锁的公平性不能够保证线程调度的公平性。

⑤.绑定多个condition

synchronized只能够随机唤醒一个线程或者是唤醒所有等待的线程;

ReentrantLock通过绑定多个condition对象,来实现精准唤醒某个等待的线程。

7.java中的公平锁和非公平锁,java中有公平锁吗?

公平锁:公平锁指的是在获取锁之前,需要检查是否有排队等待的线程,遵循“先到先得”原则;

非公平锁:非公平锁指的是在获取锁之前,不需要检查是否有排队等待的线程,谁抢到算谁的。

非公平锁比公平锁的效率高;

java中的synchronized是非公平锁,ReentrantLock可以通过传入参数,实现公平锁。锁的公平性不能保证线程调度的公平性。

8.Java锁 优化方法

①.减少锁持有的时间

只有在需要考虑线程安全程序上加锁。

②.降低锁的力度

将大对象拆成小对象,增加并行度,降低锁竞争。这样偏向锁、轻量级锁几率会增大,典型:concurrentHashMap分段锁

③.锁分离

最常见的锁分离即为ReadWriteLock,即将读操作和写操作进行分离,读读操作不需要加锁,而读写和写写操作需要加锁。

④.锁细化

如果一个方法内部,方法前后均为业务代码,只有小部分代码需要加锁,则可以将锁只加在这一小部分上,而不用将锁加在整个方法上。

9.什么是死锁?

线程死锁描述的是这样的一种情况:多个线程同时被阻塞,他们中的一个或者多个都在等待着某个资源被释放,然而每个线程都被阻塞,因此资源无法释放,导致程序无法正常退出。

举个例子:线程A持有资源1,线程B持有资源2,他们都想申请对方的资源,从而导致死锁,如线程A想身申请资源2,但是资源2释放需要线程2拿到资源1,而线程A拿不到资源2就没法释放资源1,从而导致死锁。一般出现在嵌套使用synchronized情况中。

10.死锁产生的条件,如何避免死锁?

①.死锁产生的条件

a.互斥条件:该资源任意时刻只能由一个线程占用

b.请求与保持条件:一个线程因请求资源被阻塞时,对已获得的资源保持不放(自己不放)

c.不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程所剥夺;(其他人不能夺)

d.循环等待条件:若干线程进入一种首尾相接的循环等待资源关系;

②.死锁如何避免

a.互斥条件:这个我们没有办法破坏,因为用锁本来就是让线程互斥

b.破坏请求与保持条件:一次性申请所有的资源。

c.破坏不剥夺条件:占有部分资源的线程在进一步申请资源时,如果长时间申请不到,可以释放已获取的资源

d.破坏循环等待:按照某种顺序获取资源,释放资源则按照反序释放。

11.锁分类

分类标准

分类

线程要不要锁住同步资源

锁住:悲观锁

不锁住:乐观锁

锁住同步资源失败,线程要不要阻塞

阻塞

非阻塞:自旋锁

多个线程竞争同步资源的流程细节有没有区别

只有一个线程,不采用加锁的方式

无锁

当有两个线程时,谁获取到了资源,谁执行

偏向锁

当线程超过2个时,多个线程竞争同步资源,没有获取到资源的线程自旋等待锁释放

轻量级锁

自旋次数超过10次,多个线程竞争同步资源,没有获取资源的线程处于阻塞状态

重量级锁

多个线程竞争锁时要不要排队

排队:公平锁

不排队:非公平锁

一个线程中的多个线程能不能获取同一把锁

能:可重入锁

不能:不可重入锁

多个线程能不能共享同一把锁

能:共享锁

不能:排他锁

六、并发容器和框架

1.ConcurrentHashMap

①concurrentHashMap的底层里面对节点进行加锁的具体实现方式有过了解吗?

ConcurrentHashMap成员变量使用volatile修饰,保证了禁止指令重排和内存可见性,另外使用CAS和Synchronized结合实现赋值操作,多线程操作只会锁住当前的节点

②concurrentHashMap为什么能够实现线程安全?

jdk1.7之前,将数组分解成一段一段的segment,然后对每段segment加锁,这样多个线程访问不同的segment,不会被阻塞,同时能够保证线程的安全性。

jdk1.8及以后,使用CAS和Synchronized结合实现,多线程操作只会锁住当前的节点;

2.阻塞机制(队列)

①.定义

阻塞队列是一个支持两个附加操作的队列,这两个附加操作支持阻塞的插入和移除方法。

支持阻塞的插入方法:意思是当队列满时,队列会阻塞插入元素的线程,直到队列不满;

支持阻塞的移除方法:意思是当队列为空时,获取元素的线程会等待队列变为非空。

②.适用场景

常用于生产者和消费者模式

③.java中常用的阻塞队列

a.ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列;此队列按照先进先出的原则对元素进行排序。

b.LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列;此队列的默认最大长度是;Integer.MAX_VALUE;此队列按照先进先出的原则对元素进行排序;

c.SynchronizedQueue:一个不存储元素的阻塞队列,每一个put操作必须等待一个take操作,否则不能继续添加元素。

七、并发工具

1.CountDownLatch

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

当我们调用CountDownLatch的countDown()方法时,计数器-1,调用await()方法时,会阻塞当前线程,直到计数器为零,会释放所有等待的线程。

2.CyclicBarrier

让一组线程到达一个屏障时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有的线程同时释放。

3.CountDownLatch与CyclicBarrier区别

CountDownLatch

CyclicBarrier

减数计数方式

加数计数方式

计数为0时释放所有等待的线程

计数达到指定值时释放所有的线程

计数为0时,无法重置

计数到达指定值后,reset()方法实现重置

调用countDown()方法计数减1,调用await()方法只是进行阻塞

调用await()方法计数+1,+1后的值不等于指定的值,则线程阻塞

不可重复利用

可重复利用

4.控制并发线程数的Semaphore

信号量是用来控制同时访问特定资源的线程数量,他通过协调各个线程,以保证合理使用公共资源。

5.场景题目

①.如何实现让十个线程同时开始?

方法一:使用CountDownLatch,传递参数为10,来一个线程,调用一下await(),阻塞,并调用下countDown()方法,计数器减少1,直到为0,同时开始。

方法二:采用CyclicBarrier也可以实现。思路类似。

七、并发工具

1.CountDownLatch

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

当我们调用CountDownLatch的countDown()方法时,计数器-1,调用await()方法时,会阻塞当前线程,直到计数器为零,会释放所有等待的线程。

适用场景:

2.CyclicBarrier

让一组线程到达一个屏障时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有的线程同时释放。

适用场景:

3.CountDownLatch与CyclicBarrier区别

CountDownLatch

CyclicBarrier

减数计数方式

加数计数方式

计数为0时释放所有等待的线程

计数达到指定值时释放所有的线程

计数为0时,无法重置

计数到达指定值后,reset()方法实现重置

调用countDown()方法计数减1,调用await()方法只是进行阻塞

调用await()方法计数+1,+1后的值不等于指定的值,则线程阻塞

不可重复利用

可重复利用

4.控制并发线程数的Semaphore

信号量是用来控制同时访问特定资源的线程数量,他通过协调各个线程,以保证合理使用公共资源。

5.场景题目

1.如何实现让十个线程同时开始?

方法一:使用CountDownLatch,传递参数为10,来一个线程,调用一下await(),阻塞,并调用下countDown()方法,计数器减少1,直到为0,同时开始。

方法二:采用CyclicBarrier也可以实现。思路类似。

八、线程池相关面试题

1.你知道哪些线程池?

①.创建固定数量的线程池

通过Executors.newFixedThreadPoll(),线程的数量固定,当前线程数量超过该值,则会进行等待

适用场景:控制最大并发线程数

②.创建单个线程的线程池

通过Executors.newSingleThreadPoll(),线程池中仅有一个线程,保证按序执行。

适用场景:适用于单一线程执行一个任务。

③.创建非固定数量的线程池

通过Executors.newCachedThreadPoll(),创建非固定数量线程的线程池。如果线程数超过了预期,则进行回收,如果无可回收线程,则创建新的线程。

适用于多个生命周期较短的线程。

2.线程池参数

线程池的七大参数:

参数一:核心线程数(corePollSize

参数二:最大线程数(maximumPollSize

参数三:空闲线程存活时间(keepAliveTime

参数四:空闲线程存活时间单位(unit

参数五:任务队列(workQueue):存储被提交但是尚未被执行的任务

参数六:线程工厂(threadFactory):生成线程池中工作线程的工厂。

参数七:拒绝策略(handler):拒绝策略:当线程数达到最大线程数量,并且任务队列已经满了时,拒绝更多任务进入线程池中。

3.线程池的执行过程:

a.首先,当创建了线程池后,等待提交的任务申请;

b.然后,线程池调用execute()方法添加一个任务时,线程池会进行如下的判断:

1.如果正在运行的线程数小于核心线程数,此时将创建新的线程执行这个任务。

2.如果正在运行的线程数大于或者等于核心线程数,则会将任务添加到队列中,

3.如果任务队列已经满了且正在运行的线程数小于最大的线程数,则创建非核心线程来执行任务;

4.如果阻塞队列已满,而且正在运行的线程数等于最大线程数,则执行拒绝饱和策略;

c.当一个线程任务执行完成后,会从阻塞队列中取出下一个任务执行;

d.当一个空闲线程超过一定的时间,线程池会进行判断,如果当前线程数超过核心线程数,则会进行线程回收。

4.线程池的抛弃策略

a.终止策略(AbortPolicy):直接抛出异常,从而中止系统正常运行。

b.调用者运行策略(CallerRunsPolicy):既不会抛弃任务,也不会抛出异常,而是将任务交给调用者

c.抛弃等待最久策略(DiscardOldestPolicy):丢弃队里中等待最久的任务,尝试执行新的任务。

d.抛弃策略(DiscardPolicy):直接不处理,丢弃掉。

5.停止线程池,停止时会接新的任务吗?

终止线程池有两种方法:即shutdown()方法和shutdownNow()方法

a.shutdown()方法执行后,不会接收新的任务,但是会继续执行完当前任务和阻塞队列中等待的任务。

b.shutdownNow()方法执行后,不会接收新的任务,阻塞对列中的任务不会继续执行,也会尝试终止当前正在执行的任务。

6.线程池的好处

①减少资源消耗:线程的创建非常消耗CPU资源,执行完任务的线程不会立即被销毁,而是会再次回到线程池中转为空闲状态。

②提高响应速度:当需要执行任务时,不需要等待线程创建完成就能立即执行任务

③管理线程:通过线程池的方式,能够对线程进行统一管理。

7.线程池的核心线程数如何配置?

a.CPU密集型:指的是需要大量的计算,没有阻塞,cpu一直运行的任务;

核心线程数=cpu核数+1;尽量减少CPU切换。

b.IO密集型:IO密集型任务并不需要一直在执行,根据经验可设置为核心线程数=cpu核数*2.

8.线程池的创建步骤

1.创建:通过ThreadPollExecutor(线程池执行单元)类的构造函数中传入7大参数,通过new关键字创建该类的对象;

2.执行:通过调用线程池的execute()方法,内部传入Runnable的子类对象,等待任务申请执行;

3.关闭:我们可以通过线程池的shutdown()方法或者是shutdownNow()方法关闭线程池

9.什么是线程池

线程池是存储线程的容器,创建一个线程非常消耗资源,尤其是存在大量声明周期较短的线程时,线程池的作用是可以对这些线程线程进行管理,运行结束的线程不会立即被销毁,而是再次回到线程池。

10.如果任务队列是空的,线程池中的核心线程是什么状态?

如果任务队列是空的,线程池中的核心线程一直处于运行状态

实现:线程池通过队列的take()方法阻塞核心线程(Worker)的run()方法运行,保证核心线程不会被销毁。

线程池调用execute()方***在内部调用addWorker创建一个worker类对象。

11.线程池中的空闲的线程处于什么状态?

如果线程池中等待队列未满,则此时空闲线程处于未被创建的过程;

如果线程池中等待队列已满,而且运行的线程数小于最大线程数,则空闲的线程会执行当前的任务;

如果任务执行完成后,空闲线程超过一定的时间,线程池会进行判断,如果当前线程数>核心线程数,则会回收空闲线程。

12.线程池什么情况下采用FixedThread?什么情况下采用cachedThread?

①如果是线程数量比较固定的情况下,可以使用FixedThreadPool

但是:FixedThreadPooL允许的队列长度是Integer.MAX_VALUE,可能会堆积大量的请求,导致OOM;

②如果是线程数量不固定的情况下,使用cachedThreadPool;

但是:cachedThreadPool允许创建的线程数量最大为Integer.MAX_VALUE,可能会创建大量的线程,导致OOM的发生;

九、其他问题

1.调用线程的start方法后再调用一次会怎么样

java线程是不允许启动两次的,第二次调用会报出IllegalThreadStateException,在第二次调用start()时,线程可能处于终止或者其他状态,但是无论什么状态,都不能再次启动。

2.高并发下如何保证数据的一致性

①.通过volatile+Automic原子类

volatile能够保证可见性和禁行指令重排序,但是无法保证原子性;当两个线程对共享变量进行加1操作时,会产生写覆盖的现象,因为当第一个线程对共享变量加1操作后,第二个线程可能被中断,等到它再次执行的时候,没有来得及接收到这个+1的通知,就把值进行了+1,这就造成了本来该为2的值现在却为1。而选择采用原子类,其底层实现是基于CAS操作实现的,也就是说当线程B执行时,会重新读取最新的值,发现已经不是0了,然后就会重新读取当前的值,然后执行+1操作,这样就实现了为2的结果。

②.通过ReentrantLock同步锁实现

ReentrantLock是java.util.concurrent包下提供的一套互斥锁,简单来说,ReentrantLock底层实现是通过循环调用CAS操作来实现加锁;

具体实现:首先加锁,然后再try catch中编写要执行的语句,执行完成后在finally块中将锁资源释放掉;

③.通过ThreadLocal方式实现

ThreadLocal提供了线程内存储变量的能力,这些变量不同之处在于每一个线程读取的变量是对应的互相独立的。也就是说我们各自线程自己做自己的操作,互相不影响,最后把得到的结果都加起来就得到了最终总的结果。从而避免了线程同步的问题。

④.通过synchronized实现

synchronized相当于加锁,然后当前线程获取到该锁资源后,其他线程会进入到阻塞状态,等待该线程执行完成释放锁资源。

3.threadLocal如何保证线程安全?

threadlocal底层是一个HashMap,键存储的是线程的id,值存储的是线程对应的共享变量的副本,多线程环境下,可以保证各个线程的共享变量互相隔离,相互独立,也就是说我们各自线程自己做自己的操作,互相不影响。在线程中可以通过set()/get()方法来访问共享变量。

4.threadLocal是否存在内存泄漏的问题?为什么?

threadlocal底层依赖的threadlocalMap中的key使用的是ThreadLocal对象的弱引用,在没有其他地方对Threadlocal依赖,threadlocalMap中的threadlocal对象就会被回收掉,但是对应的value不会被回收掉,这个时候Map中就可能存在key为null,但是value不为null的项,造成内存泄漏。

解决方法:使用完毕后及时调用remove方法,手动删除不再使用的threadlocal。

5.列举常见的内存泄漏的例子

单例模式中:一个对象已经不再需要了,但是单例对象还持有该对象的引用;我们知道单例对象是静态的,随着类的消亡消亡;

资源未及时关闭:对于使用了数据库等资源,使用完成后,应及时关闭数据库连接,如果不关闭,GC是无法进行回收的,造成内存泄漏;

集合类中:把一些对象的引用加入到集合容器中,当我们不需要该对象时,并没有将他的引用从集合中删除,从而造成内存泄漏;

编程的不规范:本来应该定义在方法中的局部变量定义成全部变量甚至是静态变量,本来该随着方法结束弹栈,从而造成内存泄漏。

6.说一下JUC?

①.结构分类:

java.util.concurrent(并发包)

线程安全集合

ConcurrentHashMap

ConcurrentLinkedQueue

CopyOnWriteArrayList

CopyOnWriteArraySet

并发工具

CountDownLatch

CycliBarrier

Semphore

线程池

ThreadPoolExecutors

java.util.concurrent.atomic

原子操作整型类

AutomicInteger

原子操作数组类

AutomicIntegerArray

java.util.concurrent.locks

ReentrantLock


LockSupport

7.对原子类了解吗?

①原子更新基本类型类

AtomicInteger:原子更新整型:底层实现:CAS。

②原子更新数组

AtomicIntegerArray:原子更新整形数组里的元素。底层实现:CAS;

③原子更新引用类型

AtomicReference:原子更新引用类型。底层实现:CAS;

④原子更新字段类

AtomicIntegerFieldUpdater:原子更新整形的字段的更新器。

感谢大家的理解与支持,衷心希望每位牛友都能斩获心仪的offer


#校招##学习路径##面经#
全部评论
学到了!大佬
1 回复
分享
发布于 2021-01-01 09:17
tql
点赞 回复
分享
发布于 2020-12-31 21:21
阿里巴巴
校招火热招聘中
官网直投
大佬带我
点赞 回复
分享
发布于 2020-12-31 21:23
大佬有心了
点赞 回复
分享
发布于 2020-12-31 21:29
恭喜发大财
点赞 回复
分享
发布于 2021-01-01 11:38
大佬
点赞 回复
分享
发布于 2021-01-02 04:18
这也太多了吧
点赞 回复
分享
发布于 2021-01-02 16:21
**你的,这也太详细了
点赞 回复
分享
发布于 2021-01-15 20:53

相关推荐

21 86 评论
分享
牛客网
牛客企业服务