java 面试题1

1. final关键字

1.修饰类

当用final修饰一个类时,表明这个类不能被继承。也就是说,如果一个类你永远不会让他被继承,就可以用final进行修饰。final类中的成员变量可以根据需要设为final,但是要注意final类中的所有成员方法都会被隐式地指定为final方法。
  在使用final修饰类的时候,要注意谨慎选择,除非这个类真的在以后不会用来继承或者出于安全的考虑,尽量不要将类设计为final类。

2. 修饰方法

“使用final方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。在早期的Java实现版本中,会将final方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升。在最近的Java版本中,不需要使用final方法进行这些优化了。“
因此,如果只有在想明确禁止 该方法在子类中被覆盖的情况下才将方法设置为final的。
  注:类的private方法会隐式地被指定为final方法。
 
2. java八个基本数据类型

  • byte:8位,最大存储数据量是255,存放的数据范围是-128~127之间。
  • short:16位,最大数据存储量是65536,数据范围是-32768~32767之间。
  • int:32位,最大数据存储容量是2的32次方减1,数据范围是负的2的31次方到正的2的31次方减1。
  • long:64位,最大数据存储容量是2的64次方减1,数据范围为负的2的63次方到正的2的63次方减1。
  • float:32位,数据范围在3.4e-45~1.4e38,直接赋值时必须在数字后加上f或F。
  • double:64位,数据范围在4.9e-324~1.8e308,赋值时可以加d或D也可以不加。
  • boolean:只有true和false两个取值。
  • char:16位,存储Unicode码,用单引号赋值。

在这里插入图片描述

3. java 中操作字符串都有哪些类?它们之间有什么区别?

  • String 类不可变,内部维护的char[] 数组长度不可变,为final修饰,String类也是final修饰,不存在扩容。字符串拼接,截取,都会生成一个新的对象。频繁操作字符串效率低下,因为每次都会生成新的对象
  • StringBuilder 类内部维护可变长度char[] , 初始化数组容量为16,存在扩容, 其append拼接字符串方法内部调用System的native方法,进行数组的拷贝,不会重新生成新的StringBuilder对象。
    它是非线程安全的字符串操作类, 其每次调用 toString方法而重新生成的String对象,不会共享StringBuilder对象内部的char[],会进行一次char[]的copy操作。
  • StringBuffer 类内部维护可变长度char[], 基本上与StringBuilder一致,但其为线程安全的字符串操作类,大部分方法都采用了Synchronized关键字修改,以此来实现在多线程下的操作字符串的安全性。
    其toString方法而重新生成的String对象,会共享StringBuffer对象中的toStringCache属性(char[]),但是每次的StringBuffer对象修改,都会置null该属性值。

4. 如何将字符串反转?

使用 StringBuilder 或者 stringBuffer 的 reverse() 方法。
示例代码:

// StringBuffer reverse
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append("abcdefg");
System.out.println(stringBuffer.reverse()); // gfedcba
// StringBuilder reverse
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("abcdefg");
System.out.println(stringBuilder.reverse()); // gfedcba

5. String 类的常用方法都有那些?

  • indexOf():返回指定字符的索引。
  • charAt():返回指定索引处的字符。
  • replace():字符串替换。
  • trim():去除字符串两端空白。
  • split():分割字符串,返回一个分割后的字符串数组。
  • getBytes():返回字符串的 byte 类型数组。
  • length():返回字符串长度。
  • toLowerCase():将字符串转成小写字母。
  • toUpperCase():将字符串转成大写字符。
  • substring():截取字符串。
  • equals():字符串比较。
public class Test {
	public static void main(String[] args) {
		String s1 = " acsnTdHsd";
		int a = s1.indexOf('a');
		char b = s1.charAt(2);
		byte[] bytes = s1.getBytes();
		System.out.println(a);
		System.out.println(b);
//		System.out.println(bytes.toString());
		String s2 = s1.replace('s', 'z');
		System.out.println("s2"+s2);
		String s3 = s1.trim();
		System.out.println("s3 "+s3);
		String[] strings = s1.split("H");
//		System.out.println(strings.toString());
	}
}

6. 接口和抽象类有什么区别?

  • 实现:抽象类的子类使用 extends 来继承;接口必须使用 implements 来实现接口。
  • 构造函数:抽象类可以有构造函数;接口不能有。
  • main 方法:抽象类可以有 main 方法,并且我们能运行它;接口不能有 main 方法。
  • 实现数量:类可以实现很多个接口;但是只能继承一个抽象类。
  • 访问修饰符:接口中的方法默认使用 public 修饰;抽象类中的方法可以是任意访问修饰符。

7.BIO、NIO、AIO 有什么区别?

  • BIO:Block IO 同步阻塞式 IO,就是我们平常使用的传统 IO,它的特点是模式简单使用方便,并发处理能力低。

  • NIO:New IO 同步非阻塞 IO,是传统 IO 的升级,客户端和服务器端通过 Channel(通道)通讯,实现了多路复用。

  • AIO:Asynchronous IO 是 NIO 的升级,也叫 NIO2,实现了异步非堵塞 IO ,异步 IO 的操作基于事件和回调机制。

    优秀的知乎解答:https://zhuanlan.zhihu.com/p/34408883

8. Collection 接口中的方法

在这里插入图片描述

9. Collections工具类常用方法

排序(正向和逆向)

  • Collections 提供了如下方法用于对 List 集合元素进行排序。
  • void reverse(List list):对指定 List 集合元素进行逆向排序。
  • void shuffle(List list):对 List 集合元素进行随机排序(shuffle 方法模拟了“洗牌”动作)。
  • void sort(List list):根据元素的自然顺序对指定 List 集合的元素按升序进行排序。
  • void sort(List list, Comparator c):根据指定 Comparator 产生的顺序对 List 集合元素进行排序。
  • void swap(List list, int i, int j):将指定 List 集合中的 i 处元素和 j 处元素进行交换。
  • void rotate(List list, int distance):当 distance 为正数时,将 list 集合的后 distance 个元素“整体”移到前面;当 distance 为负数时,将 list 集合的前 distance 个元素“整体”移到后面。该方法不会改变集合的长度。

查找、替换操作
Collections 还提供了如下常用的用于查找、替换集合元素的方法。

  • int binarySearch(List list, Object key):使用二分搜索法搜索指定的 List 集合,以获得指定对象在 List 集合中的索引。如果要使该方法可以正常工作,则必须保证 List 中的元素已经处于有序状态。
  • Object max(Collection coll):根据元素的自然顺序,返回给定集合中的最大元素。
  • Object max(Collection coll, Comparator comp):根据 Comparator 指定的顺序,返回给定集合中的最大元素。
  • Object min(Collection coll):根据元素的自然顺序,返回给定集合中的最小元素。
  • Object min(Collection coll, Comparator comp):根据 Comparator 指定的顺序,返回给定集合中的最小元素。
  • void fill(List list, Object obj):使用指定元素 obj 替换指定 List 集合中的所有元素。
  • int frequency(Collection c, Object o):返回指定集合中指定元素的出现次数。
  • int indexOfSubList(List source, List target):返回子 List 对象在父 List 对象中第一次出现的位置索引;如果父 List 中没有出现这样的子 List,则返回 -1。
  • int lastIndexOfSubList(List source, List target):返回子 List 对象在父 List 对象中最后一次出现的位置索引;如果父 List 中没有岀现这样的子 List,则返回 -1。
  • boolean replaceAll(List list, Object oldVal, Object newVal):使用一个新值 newVal 替换 List 对象的所有旧值 oldVal。

复制
Collections 类的 copy() 静态方法用于将指定集合中的所有元素复制到另一个集合中。执行 copy() 方法后,目标集合中每个已复制元素的索引将等同于源集合中该元素的索引。

  • copy() 方法的语法格式如下:
    void copy(List <? super T> dest,List<? extends T> src)
    其中,dest 表示目标集合对象,src 表示源集合对象。

注意:目标集合的长度至少和源集合的长度相同,如果目标集合的长度更长,则不影响目标集合中的其余元素。如果目标集合长度不够而无法包含整个源集合元素,程序将抛出 IndexOutOfBoundsException 异常。

10. HashMap和HashTable区别

  • 从数据结构上看 :
    HashMap和HashTable都使用哈希表来存储键值对。在数据结构上是基本相同的,都创建了一个继承自Map.Entry的私有的内部类Entry,每一个Entry对象表示存储在哈希表中的一个键值对(一个KEY-VALUE)。可以说,有多少个键值对,就有多少个Entry对象.
    得出结论,HashMap/HashTable内部用Entry数组实现哈希表,而对于映射到同一个哈希桶(数组的同一个位置)的键值对,使用Entry链表来存储(解决hash冲突)。
    从下面这个例子能看entry是HashMap,HashTable的一个基础单元。
package xuexi.heima.map;

import java.util.HashMap;
import java.util.Map;
import java.util.Set;

public class demo03entry {
    public static void main(String[] args) {
        Map<String, Integer> map1 = new HashMap<>();
        map1.put("赵丽颖",160);
        map1.put("赵zhao",162);
        map1.put("赵zuo",163);
        Set<Map.Entry<String, Integer>> set1 = map1.entrySet();
        for (Map.Entry<String, Integer> s : set1) {
            String key = s.getKey();
            Integer v = s.getValue();
            System.out.println(key+':'+v);
        }


    }
}
  • 从公共方法上看 :
    HashMap是支持null键和null值的,而HashTable在遇到null时,会抛出NullPointerException异常。这并不是因为HashTable有什么特殊的实现层面的原因导致不能支持null键和null值,这仅仅是因为HashMap在实现时对null做了特殊处理,将null的hashCode值定为了0,从而将其存放在哈希表的第0个bucket中。

  • 从算法上看 :
    HashTable默认的初始大小为11,之后每次扩充为原来的2n+1。HashMap默认的初始化大小为16,之后每次扩充为原来的2倍。如果在创建时给定了初始化大小,那么HashTable会直接使用你给定的大小,而HashMap会将其扩充为2的幂次方大小。
    因为哈希表的大小为素数时,简单的取模哈希的结果会更加均匀.这样设计可以减少哈希值冲突.
    但是在取模计算时,如果模数是2的幂,那么我们可以直接使用位运算来得到结果,效率要大大高于做除法。**hash计算的效率HashMap更胜一筹.**HashMap由于使用了2的幂次方,所以在取模运算时不需要做除法,只需要位的与运算就可以了。但是由于引入的hash冲突加剧问题,HashMap在调用了对象的hashCode方法之后,又做了一些位运算在打散数据。

  • 从线程安全上看 :
    Hashtable是线程安全的,它的每个方法中都加入了Synchronize方法。在多线程并发的环境下,可以直接使用Hashtable,不需要自己为它的方法实现同步.
    HashMap不是线程安全的,在多线程并发的环境下,可能会产生死锁等问题。使用HashMap时就必须要自己增加同步处理.HashMap进行同步: Map m = Collections.synchronizeMap(hashMap);
    虽然HashMap不是线程安全的,但是它的效率会比Hashtable要好很多。这样设计是合理的。在我们的日常使用当中,大部分时间是单线程操作的。HashMap把这部分操作解放出来了。当需要多线程操作的时候可以使用线程安全的ConcurrentHashMap。ConcurrentHashMap虽然也是线程安全的,但是它的效率比Hashtable要高好多倍。因为ConcurrentHashMap使用了分段锁,并不对整个数据进行锁定。

  • 从使用角度上看 :
    如果不需要线程安全,那么使用HashMap,如果需要线程安全,那么使用ConcurrentHashMap。HashTable已经被淘汰了,不要在新的代码中再使用它。
    每一版本的JDK,都会对HashMap和HashTable的内部实现做优化,比如JDK 1.8的红黑树优化。所以尽可能的使用新版本的JDK,会有性能上有提升。
    为什么HashTable已经淘汰了,还要优化它?因为有老的代码还在使用它,所以优化了它之后,这些老的代码也能获得性能提升。

11. HashSet原理

  • HashSet底层由HashMap实现

  • HashSet的值存放于HashMap的key上

  • HashMap的value统一为PRESENT

12. Iterator 和 ListIterator 有什么区别?

Iterator可用来遍历Set和List集合,但是ListIterator只能用来遍历List。

Iterator对集合只能是前向遍历,ListIterator既可以前向也可以后向。

ListIterator实现了Iterator接口,并包含其他的功能,比如:增加元素,替换元素,获取前一个和后一个元素的索引,等等。

13. 进程和线程的区别

简而言之,进程是程序运行和资源分配的基本单位,一个程序至少有一个进程,一个进程至少有一个线程。进程在执行过程中拥有独立的内存单元,而多个线程共享内存资源,减少切换次数,从而效率更高。线程是进程的一个实体,是cpu调度和分派的基本单位,是比程序更小的能独立运行的基本单位。同一进程中的多个线程之间可以并发执行。

14.守护线程

在Java中有两类线程:User Thread(用户线程)、Daemon Thread(守护线程) ;Daemon的作用是为其他线程的运行提供服务,比如说GC线程。其实User Thread线程和Daemon Thread守护线程本质上来说去没啥区别的,唯一的区别之处就在虚拟机的离开:如果User Thread全部撤离,那么Daemon Thread也就没啥线程好服务的了,所以虚拟机也就退出了。

  • 守护线程注意地方
    守护线程并非虚拟机内部可以提供,用户也可以自行的设定守护线程,方法:public final void setDaemon(boolean on)
    thread.setDaemon(true)必须在thread.start()之前设置,否则会跑出一个IllegalThreadStateException异常。你不能把正在运行的常规线程设置为守护线程**。在Daemon线程中产生的新线程也是Daemon的。**
    不是所有的应用都可以分配给Daemon线程来进行服务,比如读写操作或者计算逻辑,访问文件或数据库,这些动作比较慢。因为在Daemon Thread还没来的及进行操作时,虚拟机可能已经退出。
class TestRunnable implements Runnable{  

    public void run(){  
        try{  
            Thread.sleep(1000);//守护线程阻塞1秒后运行  
            File f=new File("daemon.txt");  
            FileOutputStream os=new FileOutputStream(f,true);  
            os.write("daemon".getBytes());  
        } catch(IOException e1){  
            e1.printStackTrace();  
        } catch(InterruptedException e2){  
            e2.printStackTrace();  
        }  
    }  
}  

public class TestDemo2{  

    public static void main(String[] args) throws InterruptedException {  

        Runnable tr=new TestRunnable();  
        Thread thread=new Thread(tr);  
        thread.setDaemon(true); //设置守护线程  
        thread.start(); //开始执行分进程  
    }  
} 

运行结果:
1. 文件daemon.txt中没有"daemon"字符串。
2. 但是如果把thread.setDaemon(true); //设置守护线程注释掉,文件daemon.txt是可以被写入daemon字符串的
这说明守护线程还没执行完虚拟机就关闭了,这样是危险的,不适合读写操作
守护线程举例:
1. qq,飞讯等等聊天软件,主程序是非守护线程,而所有的聊天窗口是守护线程
,当在聊天的过程中,直接关闭聊天应用程序时,聊天窗口也会随之关闭,但是不是
立即关闭,而是需要缓冲,等待接收到关闭命令后才会执行窗口关闭操作.
2. jvm的gc垃圾回收线程。

15 创建线程的方法

①. 继承Thread类创建线程类

  • 定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。
  • 创建Thread子类的实例,即创建了线程对象。
  • 调用线程对象的start()方法来启动该线程。

②. 通过Runnable接口创建线程类

  • 定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
  • 创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
  • 调用线程对象的start()方法来启动该线程。

③. 通过Callable和Future创建线程

  • 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
  • 创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
  • 使用FutureTask对象作为Thread对象的target创建并启动新线程。
  • 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
    在这里插入图片描述
    FutureTask是Future的具体实现。FutureTask实现了RunnableFuture接口。RunnableFuture接口又同时继承了Future 和 Runnable 接口。所以FutureTask既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值。
    这里演示一下FutureTask基本用法
package com;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.Callable;
public class Test{ 
	 public static void main(String[] args) throws InterruptedException, ExecutionException {
	        
	        long starttime = System.currentTimeMillis();
	        
	        //input2生成, 需要耗费3秒
	        FutureTask<Integer> input2_futuretask = new FutureTask<Integer>(new Callable<Integer>() {

	            @Override
	            public Integer call() throws Exception {
	                Thread.sleep(3000);
	                return 5;
	            }
	        });
	        
	        new Thread(input2_futuretask).start();
	        
	        //input1生成,需要耗费2秒
	        FutureTask<Integer> input1_futuretask = new FutureTask<>(new Callable<Integer>() {

	            @Override
	            public Integer call() throws Exception {
	                Thread.sleep(2000);
	                return 3;
	            }
	        });
	        new Thread(input1_futuretask).start();

	        Integer integer2 = input2_futuretask.get();
	        Integer integer1 = input1_futuretask.get();
	        System.out.println(algorithm(integer1, integer2));
	        long endtime = System.currentTimeMillis();
	        System.out.println("用时:" + String.valueOf(endtime - starttime));
	    }
	    
	    //这是我们要执行的算法
	    public static int algorithm(int input, int input2) {
	        return input + input2;
	    }
} 

/*

8
用时:3002
这说明了并行化的执行,节省时间。
*/

16线程有哪些状态?

线程通常都有五种状态,创建、就绪、运行、阻塞和死亡。

  • 创建状态。在生成线程对象,并没有调用该对象的start方法,这是线程处于创建状态。

  • 就绪状态。当调用了线程对象的start方法之后,该线程就进入了就绪状态,但是此时线程调度程序还没有把该线程设置为当前线程,此时处于就绪状态。在线程运行之后,从等待或者睡眠中回来之后,也会处于就绪状态。

  • 运行状态。线程调度程序将处于就绪状态的线程设置为当前线程,此时线程就进入了运行状态,开始运行run函数当中的代码。

  • 阻塞状态。线程正在运行的时候,被暂停,通常是为了等待某个时间的发生(比如说某项资源就绪)之后再继续运行。sleep,suspend,wait等方法都可以导致线程阻塞。

  • 死亡状态。如果一个线程的run方法执行结束或者调用stop方法后,该线程就会死亡。对于已经死亡的线程,无法再使用start方法令其进入就绪

17. sleep 和 wait的区别

sleep() 和 wait() 的区别就是 调用sleep方法的线程不会释放对象锁,而调用wait() 方法会释放对象锁

package com;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.Callable;
public class Test{ 
	static public class Service {

	    public void mSleep(){
	        synchronized(this){

	            try{
	                Thread.sleep(3*1000);
	                this.notifyAll();
	                System.out.println(" 唤醒等待 。 结束时间:"+System.currentTimeMillis());
	            }
	            catch(Exception e){
	                System.out.println(e);
	            }

	        }

	    }

	    public void mWait(){

	        synchronized(this){
	            try{
	                System.out.println(" 等待开始 。 当前时间:"+System.currentTimeMillis());
	                this.wait();
	            }catch(Exception e){
	                System.out.println(e);
	            }
	        }

	    }

	}
	static public class SleepThread implements Runnable{
		private Service myService;
		public SleepThread(Service myService) {
			// TODO 自动生成的构造函数存根
			this.myService = myService;
		}

		@Override
		public void run() {
			// TODO 自动生成的方法存根
			myService.mSleep();
			
		}
		
	}
	static public class WaitThread implements Runnable{
		private Service myService;
		public WaitThread(Service myService) {
			// TODO 自动生成的构造函数存根
			this.myService = myService;
		}

		@Override
		public void run() {
			// TODO 自动生成的方法存根
			myService.mWait();
			
		}
		
	}
	public static void main(String[] args){

        Service mService = new Service();

        Thread sleepThread = new Thread(new SleepThread(mService));
        Thread waitThread = new Thread(new WaitThread(mService));
        waitThread.start();
        sleepThread.start();

    }

} 

在这里插入图片描述
这个程序说明问题了,我们先开启的wait方法进程,然后系统释放对象锁,被sleep线程得到,在这期间不释放锁,一直等到sleep进程结束,然后会唤醒wait线程,继续执行。

18. 线程池的创建

在这里插入图片描述
其中Executor是线程池的顶级接口,接口中只定义了一个方法 void execute(Runnable command);线程池的操作方法都是定义子在ExecutorService子接口中的,所以说ExecutorService是线程池真正的接口。
Executors的创建线程池的方法,创建出来的线程池都实现了ExecutorService接口。常用方法有以下几个:
①. Executors.newFixedThreadPool(int nThreads)
创建一个固定长度的线程池,每当提交一个任务就创建一个线程,直到达到线程池的最大数量,这时线程规模将不再变化,当线程发生未预期的错误而结束时,线程池会补充一个新的线程。

package com.xzj;

import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Main {
    public static void main(String[] args) {
        ExecutorService cachedThreadPool = Executors.newFixedThreadPool(3);
        for (int i = 0; i < 10; i++) {
            cachedThreadPool.execute(new MyRunnable());
        }
        cachedThreadPool.shutdown();
    }
}

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println(new Date().toString());
        try {
            Thread.sleep(2000);
        } catch (InterruptedException ie) {
            ie.printStackTrace();
        }
    }
}

②Executors. newCachedThreadPool()
创建一个可缓存的线程池,如果线程池的规模超过了处理需求,将自动回收空闲线程,而当需求增加时,则可以自动添加新线程,线程池的规模不存在任何限制。

package com.xzj;

import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

//  
public class Main {
    public static void main(String[] args) {
        ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) {
            cachedThreadPool.execute(new MyRunnable());
        }
        cachedThreadPool.shutdown();
    }
}

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println(new Date().toString());
        try {
            Thread.sleep(2000);
        } catch (InterruptedException ie) {
            ie.printStackTrace();
        }
    }
}

③Executors. newSingleThreadExecutor()

package com.xzj;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Main {
    public static void main(String[] args) {
        ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 10; i++) {
            final int index = i;
            singleThreadExecutor.execute(new MyRunnable(index));
        }
    }
}

class MyRunnable implements Runnable {
    int i = 0;

    public MyRunnable(int i) {
        this.i = i;
    }

    @Override
    public void run() {
        try {
            System.out.print(i+" ");
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

这是一个单线程的Executor,它创建单个工作线程来执行任务,如果这个线程异常结束,会创建一个新的来替代它;它的特点是能确保依照任务在队列中的顺序来串行执行。

④Executors. newScheduledThreadPool(int corePoolSize)
创建了一个固定长度的线程池,而且以延迟或定时的方式来执行任务,类似于Timer。

package com.xzj;

import java.util.Date;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class Main {
    public static void main(String[] args) {
        ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
        System.out.println(new Date().toString());
        scheduledThreadPool.schedule(new MyRunnable(), 3, TimeUnit.SECONDS);
    }
}

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println(new Date().toString());
    }
}

Executors存在什么问题
先看看线程池的执行过程
在这里插入图片描述
Java中的BlockingQueue主要有两种实现,分别是ArrayBlockingQueue 和 LinkedBlockingQueue。
ArrayBlockingQueue是一个用数组实现的有界阻塞队列,必须设置容量。
LinkedBlockingQueue是一个用链表实现的有界阻塞队列,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列**,最大长度为Integer.MAX_VALUE。**
这里的问题就出在:**不设置的话,将是一个无边界的阻塞队列,最大长度为Integer.MAX_VALUE。**也就是说,如果我们不设置LinkedBlockingQueue的容量的话,其默认容量将会是Integer.MAX_VALUE。
而newFixedThreadPool中创建LinkedBlockingQueue时,并未指定容量。此时,LinkedBlockingQueue就是一个无边界队列,对于一个无边界队列来说,是可以不断的向队列中加入任务的,这种情况下就有可能因为任务过多而导致内存溢出问题。

上面提到的问题主要体现在newFixedThreadPool和newSingleThreadExecutor两个工厂方法上,并不是说newCachedThreadPool和newScheduledThreadPool这两个方法就安全了,这两种方式创建的最大线程数可能是Integer.MAX_VALUE,而创建这么多线程,必然就有可能导致OOM。

避免使用Executors创建线程池,主要是避免使用其中的默认实现,那么我们可以自己直接调用ThreadPoolExecutor的构造函数来自己创建线程池。在创建的同时,给BlockQueue指定容量就可以了。

public ThreadPoolExecutor(
  int corePoolSize, // 线程池长期维持的线程数,即使线程处于Idle状态,也不会回收。
  int maximumPoolSize, // 线程数的上限
  long keepAliveTime, TimeUnit unit, // 超过corePoolSize的线程的idle时长,
                                     // 超过这个时间,多余的线程会被回收。
  BlockingQueue<Runnable> workQueue, // 任务的排队队列
  ThreadFactory threadFactory, // 新线程的产生方式
  RejectedExecutionHandler handler) // 拒绝策略

竟然有7个参数,很无奈,构造一个线程池确实需要这么多参数。这些参数中,比较容易引起问题的有corePoolSize, maximumPoolSize, workQueue以及handler:

  • corePoolSize和maximumPoolSize设置不当会影响效率,甚至耗尽线程;
  • workQueue设置不当容易导致OOM;
  • handler设置不当会导致提交任务时抛出异常。

拒绝策略:
在这里插入图片描述
举个例子:

ExecutorService executorService = new ThreadPoolExecutor(2, 2, 
                0, TimeUnit.SECONDS, 
                new ArrayBlockingQueue<>(512), 
                new ThreadPoolExecutor.DiscardPolicy());// 指定拒绝策略

参考链接:https://www.cnblogs.com/CarpenterLee/p/9558026.html
https://www.cnblogs.com/captainad/p/10943091.html
https://blog.csdn.net/hollis_chuang/article/details/83743723

19.线程池的五种状态

在这里插入图片描述

  • RUNNING
    (1) 状态说明:线程池处在RUNNING状态时,能够接收新任务,以及对已添加的任务进行处理。
    (2) 状态切换:线程池的初始化状态是RUNNING。换句话说,线程池被一旦被创建,就处于RUNNING状态,并且线程池中的任务数为0!
    private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

  • SHUTDOWN
    (1) 状态说明:线程池处在SHUTDOWN状态时,不接收新任务,但能处理已添加的任务。
    (2) 状态切换:调用线程池的shutdown()接口时,线程池由RUNNING -> SHUTDOWN。

  • STOP
    (1) 状态说明:线程池处在STOP状态时,不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。
    (2) 状态切换:调用线程池的shutdownNow()接口时,线程池由(RUNNING or SHUTDOWN ) -> STOP。

  • TIDYING
    (1) 状态说明:当所有的任务已终止,ctl记录的”任务数量”为0,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,会执行钩子函数terminated()。terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理;可以通过重载terminated()函数来实现。
    (2) 状态切换:当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由 SHUTDOWN -> TIDYING。
    当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP -> TIDYING。

  • TERMINATED
    (1) 状态说明:线程池彻底终止,就变成TERMINATED状态。
    (2) 状态切换:线程池处在TIDYING状态时,执行完terminated()之后,就会由 TIDYING -> TERMINATED。

20 线程池中 submit()和 execute()方法有什么区别

  • 接收的参数不一样

  • submit有返回值,而execute没有

  • submit方便Exception处理

21 线程安全的三个方面体现

  • 原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作,(atomic,synchronized);

  • 可见性:一个线程对主内存的修改可以及时地被其他线程看到,(synchronized,volatile);

  • 有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序,(happens-before原则)。

22 Synchronized关键字

Synchronized关键字,我们一般称之为”同步锁“,用它来修饰需要同步的方法和需要同步代码块,默认是当前对象作为锁的对象。要注意每个实例对象只有一把锁,也就是说多个线程对同一实例的不同的同步方法会阻塞

  • 1 Synchronized对象锁:
    a. 修饰在方法上,多个线程调用同一个对象的同步方法会阻塞,调用不同对象的同步方法也会阻塞。(每个对象只有一把锁)
package com;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.Callable;
public class Test{ 
   static class T1{
   	public synchronized void t1() throws InterruptedException {
   		System.out.println("t1 start time" + System.currentTimeMillis());
   		Thread.sleep(1000);
   		System.out.println("t1 end time" + System.currentTimeMillis());
   	}
   	public synchronized void t2() {
   		System.out.println("t2 end time" + System.currentTimeMillis());
   	}
   }
   static class myThread1 implements Runnable{
   	private T1 t1;
   	public myThread1(T1 t1) {
   		// TODO 自动生成的构造函数存根
   	this.t1 = t1;
   	}

   	@Override
   	public void run() {
   		// TODO 自动生成的方法存根
   		try {
   			t1.t1();
   		} catch (InterruptedException e) {
   			// TODO 自动生成的 catch 块
   			e.printStackTrace();
   		}
   	}
   	
   }
   static class myThread2 implements Runnable{
   	private T1 t1;
   	public myThread2(T1 t1) {
   		// TODO 自动生成的构造函数存根
   	this.t1 = t1;
   	}

   	@Override
   	public void run() {
   		t1.t2();
   	}
   	
   }
   public static void main(String[] args) {
   	T1 test1 = new T1();
   	Thread thread1 = new Thread(new myThread1(test1));
   	Thread thread2 = new Thread(new myThread2(test1));
   	thread1.start();
   	thread2.start();
   	
   }
      
} 
输出结果:
/*
t1 start time1582419316076
t1 end time1582419317076
t2 end time1582419317076
*/

这个例子说明同一个实例两个Synchronized方法也会发生冲突,需要等待。因为一个实例在头部只有一个字段代表锁,当两个线程要同时调用这两个不同的方法,会 发生锁的争夺,阻塞。

  • 2 修饰代码块,这个this就是指当前对象(类的实例),多个线程调用同一个对象的同步方法会阻塞,调用不同对象的同步方法不会阻塞。(java对象的内存地址是否相同)
public void obj2() {
       synchronized (this) {
           int i = 5;
           while (i-- > 0) {
               System.out.println(Thread.currentThread().getName() + " : " + i);
               try {
                   Thread.sleep(500);
               } catch (InterruptedException ie) {
               }
           }
       }
   }
  • 3.修饰代码块,这个str就是指String对象,多个线程调用同一个对象的同步方法会阻塞,调用不同对象的同步方法不会阻塞。(java对象的内存地址是否相同)
 public void obj2() {
       String str=new String("lock");//在方法体内,调用一次就实例化一次,多线程访问不会阻塞,因为不是同一个对象,锁是不同的
       synchronized (str) {
           int i = 5;
           while (i-- > 0) {
               System.out.println(Thread.currentThread().getName() + " : " + i);
               try {
                   Thread.sleep(500);
               } catch (InterruptedException ie) {
               }
           }
       }
   }
public static void main(String[] args) throws InterruptedException {
        test test=new test();
        new Thread(new Runnable() {
            @Override
            public void run() {
                test.obj2();
            }
        }).start();
    
        new Thread(new Runnable() {
            @Override
            public void run() {
                test.obj2();
            }
        }).start();
    }
//两个方法之间交替执行 没有阻塞
Thread-0 : 4
Thread-1 : 4
Thread-1 : 3
Thread-0 : 3
Thread-1 : 2
Thread-0 : 2
Thread-1 : 1
Thread-0 : 1
Thread-0 : 0
Thread-1 : 0

共用一个对象,多线程调用obj2同步方法,因为使用的是一个对象锁,会阻塞。

String str=new String("lock"); //对象放在方法外,调用方法的时候不会新创建一个对象。
   public void obj2() {
       synchronized (str) {
           int i = 5;
           while (i-- > 0) {
               System.out.println(Thread.currentThread().getName() + " : " + i);
               try {
                   Thread.sleep(500);
               } catch (InterruptedException ie) {
               }
           }
       }
   }

      Thread-0 : 4
      Thread-0 : 3
      Thread-0 : 2
      Thread-0 : 1
      Thread-0 : 0
      Thread-1 : 4
      Thread-1 : 3
      Thread-1 : 2
      Thread-1 : 1
      Thread-1 : 0
  • 二.Synchronized类锁
  • 修饰类和修饰静态方法的本质都是一样的是对Class文件加锁,也就是说即使两个实例,调用两个Synchronized修饰的方法还是会互斥的
    1.Synchronized修饰静态的方法
public class Test{ 
	
	public static synchronized void run()  {
		   System.out.println(1);
		   try {
		       Thread.sleep(1000);
		   } catch (InterruptedException e) {
		       e.printStackTrace();
		   }
		   System.out.println(2);
		}
	public static synchronized void run2()  {
		   System.out.println(1);
		   try {
		       Thread.sleep(1000);
		   } catch (InterruptedException e) {
		       e.printStackTrace();
		   }
		   System.out.println(2);
		}

	public static void main(String[] args) {
		Test demo = new Test();
		Test demo2 = new Test();
		new Thread(() -> demo.run()).start();
		new Thread(() -> demo2.run2()).start();

	}
} 

/**
1
2
1
2
**/

2.synchronized (test.class) ,锁的对象是test.class,即test类的锁。

public void obj1() {
        synchronized (test.class) {
           int i = 5;
           while (i-- > 0) {
               System.out.println(Thread.currentThread().getName() + " : " + i);
               try {
                   Thread.sleep(500);
               } catch (InterruptedException ie) {
               }
           }
       }
   }

看完上面的例子,应该可以分清楚什么是对象锁和类锁了。
那么问题来了:在一个类中有两方法,分别用synchronized 修饰的静态方法(类锁)和非静态方法(对象锁)。多线程访问两个方法的时候,线程会不会阻塞?

public static synchronized void obj3() {
           int i = 5;
           while (i-- > 0) {
               System.out.println(Thread.currentThread().getName() + " : " + i);
               try {
                   Thread.sleep(500);
               } catch (InterruptedException ie) {
               }
           }
   }

 public synchronized void obj4() {
       int i = 5;
       while (i-- > 0) {
           System.out.println(Thread.currentThread().getName() + " : " + i);
           try {
               Thread.sleep(500);
           } catch (InterruptedException ie) {
           }
       }
   }

答案是???

Thread-0 : 4
Thread-1 : 4
Thread-0 : 3
Thread-1 : 3
Thread-1 : 2
Thread-0 : 2
Thread-1 : 1
Thread-0 : 1
Thread-1 : 0
Thread-0 : 0

不会阻塞,原因很简单,静态方法对应的是Class对象,而非静态方法对应的是实例,二者是不同的对象,因此不会冲突
总结
到这么我们应该知道了:
1,要满足方法同步(或者代码块同步)就必须保证多线程访问的是同一个对象(在java内存中的地址是否相同)。
2,类锁和对象锁同时存在时,多线程访问时不会阻塞,因为他们不是一个锁。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值