String, StringBuffer, StringBuilder区别
第一点是可变性。String不可变,String Buffer和StringBuider可变。这是因为String被final修饰,每次操作都生成新的对象。StringBuffer和StringBuilder的父类AbstractStringBuilder没有被final修饰。
第二点是线程安全。String和StringBuffer是线程安全的,因为String被final修饰,StringBuffer所有的方法都用了synchronized。StringBuilder是线程不安全的。因此单线程时使用StringBuilder,性能较好,多线程时使用StringBuffer,线程安全。
为什么静态方法不能调用非静态方法和变量?
静态方法是属于类,在类加载时就会分配内存,可以通过类名去直接访问。
非静态成员属于类的对象,只有该对象实例化之后才存在,通过类的对象去访问。
异常类型
Throwable下面有两个直接子类,Error和Exception。
Error是严重的系统错误,无法被应用程序捕获和处理,例如内存溢出或者堆栈溢出。
Exception的子类有RuntimeException和其他Exception。
RuntimeException通常不需要在方法的声明中显式地捕获或声明抛出,例如空指针异常,算数异常等等。
其他Exception也称Checked Exception,这些异常通常需要在方法的声明中显式地捕获或声明抛出,例如IOException,SQLException等等。
捕获: try...catch,一般用在调用处,能让代码继续往下运行。
抛出: throw throws,在方法中,出现异常了,方法就没有继续运行下去的意义了,采取抛出处理让该方法结束运行并告诉调用者出现了问题。
字节流与字符流区别?
适用场景:字符流适合文本文件,字节流适合图片视频音频。因为字符流能自动处理文件编码,确保正确解析文件中的字符,并且字符流读取非文本文件时,可能会将某些特定的字节序列视为文件的结尾,导致数据丢失。
缓冲:字节流不使用缓冲,字符流将频繁访问的资源放入内存。
字节流是InputStream、OutputStream。字符流是Reader、Writer。
ArrayList扩容机制
ArrayList每次扩容是原来的1.5倍。因为扩容时,会将老数组中的元素重新拷贝一份到新的数组中,因此扩容代价比较高,我们要尽量避免数组扩容,尽可能地在创建ArrayList对象时指定其容量。
是否线程安全?如何线程安全地操作ArrayList?
ArrayList线程安全的操作方法
-
Vector。List list = new ArrayList(),替换为List arrayList = new Vector<>()。使用了synchronized关键字,效率较低。
-
JUC中的CopyOnWriteArrayList。CopyOnWriteArrayList<String> list =new CopyOnWriteArrayList<String>() 。写数据时将原来array复制到新的array,修改后,将引用指向新数组,任何可变的操作(add、set、remove等)都通过ReentrantLock 控制并发,读数据时不用加锁,适用于读多写少的并发场景。
- Collections.synchronizedList(list)。方法都加了synchronized修饰。加锁的对象是当前SynchronizedCollection实例,适用于将现有的非线程安全的 List 转换为线程安全的情况。
LinkedList线程安全的操作方法
JUC中的ConcurrentLinkedQueue和Collections.synchronizedList(List)。
HashMap、TreeMap、LinkedHashMap的区别?
相同点
- 都属于Map;
- Map 主要用于存储键(key)值(value)对,根据键得到值,因此键不允许键重复,但允许值重复。
- 都是线程不安全的
不同点
SingleDateFormat--线程安全的操作方法(有实例)
SingleDateFormat线程不安全的原因
SingleDateFormat类内部有个Calendar对象引用,Calendar用于存储日期信息,如果SimpleDateFormat是多个线程之间共享的, 那么多个线程可以同时访问和修改Calendar对象的状态。就会出现线程安全问题。
解决方案
1、每调用一次方法就会创建一个SimpleDateFormat对象,但每调用一次方法就会创建一个SimpleDateFormat对象。
2、方法加synchronized,不过性能差。
3、使用ThreadLocal,这种方式推荐。
4、如果是JDK8的应用,可以使用Instant代替Date,LocalDateTime代替Calendar,DateTimeFormatter(所有字段都是final类型)代替Simpledateformatter。
线程池有哪些参数?
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
1、corePoolSize
线程池的核心线程数
即便是线程池里没有任何任务,也会有corePoolSize个线程在候着等任务。
2、maximumPoolSize
最大线程数。
超过此数量,会触发拒绝策略。
3、keepAliveTime
线程的存活时间。
当线程池里的线程数大于corePoolSize时,如果等了keepAliveTime时长还没有任务可执行,则线程退出。
4、unit
指定keepAliveTime的单位
比如:秒:TimeUnit.SECONDS。
5、workQueue
一个阻塞队列,提交的任务将会被放到这个队列里。
6、threadFactory
线程工厂,用来创建线程
主要是为了给线程起名字,默认工厂的线程名字:pool-1-thread-3。
7、handler
拒绝策略
当线程池里线程被耗尽,且队列也满了的时候会调用。
默认拒绝策略为AbortPolicy。即:不执行此任务,而且抛出一个运行时异常
线程池使用步骤?
1:创建一个线程池对象,控制要创建几个线程对象。
2:实现线程,新建一个类实现Runnable或者Callable接口。
3:使用submit或者execute提交线程调用。submit有返回值,返回值是future对象,可以获取执行结果。execute无返回值。
4:使用shutdown方法关闭线程池。
线程池的工作流程
CPU密集与IO密集的场景如何设置线程池参数?
如果任务被阻塞的时间少于执行时间,即这些任务是计算密集型的,则程序所需线程数将随之减少,但最少也不应低于处理器的核心数。核心线程数 = CPU核数 + 1
如果任务被阻塞的时间大于执行时间,即该任务是IO密集型的,我们就需要创建比处理器核心数大几倍数量的线程。例如,如果任务有50%的时间处于阻塞状态,则程序所需线程数为处理器可用核心数的两倍。核心线程数 = CPU核数 * 2 + 1
线程池的阻塞队列(BlockingQueue)
ArrayBlockingQueue
基于数组的FIFO队列;有界;创建时必须指定大小;入队和出队共用一个可重入锁。默认使用非公平锁。
LinkedBlockingQueue
默认大小的LinkedBlockingQueue将导致所有 corePoolSize 线程都忙时新任务在队列中等待。这样,创建的线程不会超过 corePoolSize。(因此,maximumPoolSize 的值也就无效了)。使用无界队列的好处是可以避免任务因为队列满而被拒绝或阻塞,因此当每个任务相互独立且任务数量较大时,适合使用无界队列。
常见线程池种类
FixedThreadPool(固定线程池)
固定线程池是一种固定大小的线程池,其中线程数量是预先指定的。当有新任务提交时,如果线程池中有空闲线程,则立即执行;如果线程池中没有空闲线程,则任务进入等待队列,直到有线程可用。固定线程池适用于需要限制并发线程数量的场景,如服务器请求处理、并发任务数量可预知的情况。
SingleThreadExecutor(单线程池)
单线程池是只有一个工作线程的线程池,所有任务按照顺序执行,每个任务都在前一个任务执行完成后开始执行。适用于需要按照顺序串行执行任务的场景,如消息队列的消费者、数据库事务处理等。
ScheduledThreadPool(定时线程池)
定时线程池用于执行延迟任务或定时任务,可以指定任务的延迟时间或固定的执行间隔。定时线程池会根据任务的执行时间自动调度线程,保证任务按照预定的时间顺序执行。适用于需要定时执行任务的场景,如定时任务调度、定时数据备份等。
CachedThreadPool(缓存线程池)
缓存线程池根据需要创建线程,如果有空闲线程,则复用空闲线程执行任务;如果没有空闲线程,则创建新线程。当线程空闲时间超过指定的时间(例如60秒),则被终止并从线程池中移除。缓存线程池适用于任务数量不确定、任务执行时间短暂、需要快速响应的场景。
进程的三种创建方式
1、继承Thread类的方式进行实现:
定义一个MyThread继承Thread类
在MyThread类中重写run()方法
创建MyThread类对象
启动线程
public class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("MyThread线程方法执行" + i);
}
}
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
}
}
问题.run()方法和start()的区别
run():封装线程执行的代码,直接调用,相当于普通方法的调用,并没有开启线程
start():启动线程;然后由JVM调用此线程的run()方法
2、方式2:实现Runnable接口
定义一个类MyRunnable实现Runnable接口
在MyRunnable类中重写run()方法
创建MyRunnable类的对象
创建Thread类的对象,把MyRunnable对象作为构造方法的参数
启动线程
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i=0;i<10;i++){
System.out.println("线程方法执行"+i);
}
}
public static void main(String[] args) {
//创建了一个参数对象
MyRunnable myRunnable = new MyRunnable();
//创建了一个线程对象,并把参数传递给这个线程
//在线程启动后,执行的就是参数里面的run方法
Thread thread = new Thread(myRunnable);
//run方法
thread.start();
}
}
3、方式3:Callble和Future(可以获取返回结果)
定义一个类MyCallable实现Callable接口
在MyCallable类中重写call()方法
创建MyCallable类的对象
创建Future的实现类FutureTask对象,把MyCallable对象作为构造方法的参数
创建Thread类的对象,把FutureTask对象作为构造方法的参数
启动线程
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
for (int i=0;i<100;i++){
System.out.println("MyCallable运行次数"+i);
}
//返回值就是表示线程运行之后的结果
return "你好";
}
public static void main(String[] args) {
//线程开启之后需要执行里面的call方法
MyCallable myCallable = new MyCallable();
//可以获取线程执行完毕之后的结果,也可以作为参数传递给Thread对象
FutureTask<String> futureTask = new FutureTask<String>(myCallable);
//创建线程对象
Thread thread = new Thread(futureTask);
//开启线程
thread.start();
try {
//获取返回结果
String s = futureTask.get();
System.out.println(s);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
调用get方法,就可以获取线程结束之后的结果,必须在线程结束之后才能获取否则只能死等线程
ReentrantLock和synchronized锁
ReentrantLock
对象实例、类信息、常量、静态变量分别在运行时数据区的哪个位置?
各区域的数据
以JDK8为例:
- 堆:对象实例、String常量池、基本类型常量池
- 方法区:类信息、静态变量
- 虚拟机栈:临时变量(方法内的变量)
- 元空间:类常量池、运行时常量池