赞
踩
String是不可变的,后两者可变,StringBuffer线程安全,StringBuilder线程不安全,但是StringBuilder效率高;
另外,StringBuffer和Stringbuilder都没有重写equals方法,在进行equals比较时,还是比较的是地址,而不是值
public static void main(String[] args) {
String a="abc";
String b="abc";
System.out.println(a==b);
System.out.println(a.equals(b));
}
结果都是:true true ,因为都在常量池中,比较的是值,地址也相同;
public static void test1(){
String a="abc";
String b=new String("abc");
System.out.println(a==b);
System.out.println(a.equals(b));
}
此时结果是:false true,==比较的是地址,equals比较的是值;
String t0 = new String("hello") + new String("world");
t0.intern();
String t1 = "helloworld";
System.out.println(t0 == t1);
这个运行结果,在JDK1.8中是true,1.7中是false,JDK1.7之前的版本中,intern方法会优先在方法区的运行时常量池中查找是否已经存在相同的字符串,倘若已经存在,则返回已存在的字符串,否则则在常量池中添加一个字符串常量,并返回字符串。从JDK1.7开始,HotSpot虚拟机将字符串常量移至Java Heap,intern方法的实现也发生了变化,首先还是会先去查询常量池中是否已经存在,如果存在,则返回常量池中的字符串,否则不再将字符串拷贝到常量池,而只是在常量池中保存字符串对象的引用。
equals(Object obj): 比较字符串的内容是否相同,区分大小写;
equalsIgnoreCase(String str):比较字符串的内容是否相同,忽略大小写
contains(String str):判断大字符串中是否包含小字符串
startsWith(String str):判断字符串是否以某个指定的字符串开头
endsWidth(String str):判断字符串是否以某个指定的字符串结尾
isEmpty():判断字符串是否为空
为了安全,多个线程对String进行读取时,不会发生线程安全问题,因为final是不可变性;
还能保证Arrays数组的安全;
实现字符串常量池,可提高效率,实现对内存的优化;
标记为final。不可被继承,保证String类型的对象只能是String类型,不会是其他子类型;
String 对象是缓存在String池中,由于缓存的字符串是在多个客户端之间共享,因此存在风险,因此通过String类不可变来规避这种风险
char charAt(int index);//返回指定索引处的char值 int compareTo(Object o);//把这个字符串跟另一个字符串比较 String concat(String str);//连接两个字符串 boolean contentEquals(StringBuffer sb);//当且仅当字符串与指定的StringBuffer有相同顺序的字符时候返回真 String substring(int beginIndex); String substring(int beginIndex,int endIndex); //截取字符串 int indexOf(String str, int fromIndex): 返回从 fromIndex 位置开始查找指定字符在字符串中第一次出现处的索引,如果此字符串中没有这样的字符,则返回 -1。 例:获取指定字符串中某个子串出现的次数 public static int strNum(String obj,String str){ int fromIndex = 0; int count =0; while (true){ int index = str.indexOf(obj,fromIndex); if(-1 != index){ fromIndex = index +1; count ++; }else { break; } } return count; }
临时变量一般是存在java堆中的,String类的长度理论上取决于传入的byte数组长度,byte[]数据数组最大长度理论上应该是Integer.MAX_VALUE,但是实际从ArrayList源码可以看出,应该是Integer.MAX_VALUE-8,但是还是会受到java堆可分配内存大小的限制
如果String变量是一个全局变量,其变量是存在java方法区的,此时他的长度取决于.class描述全局String类型变量的数据结构,如果是u2(描述是2个字节的数据类型),这意味着最大是65535。
如果是字符数,一个UTF-8编码的字符对应3个字节,所以此时可容纳的字符数应该是65535/3;
HashCode、equals、==区别?
equals() 定义在JDK的Object中,通过判断两个对象的地址是否相等(即,是否是同一个对象)来区分它们是否相等,默认的equals方法等价于“==”,所以一般要重新equals方法,若两个对象的内容相等,则返回true,否则返回false
在String中,“==”用来比较两个变量的内存地址是否相同,而equals用来比较内容是否相同(因为String重写了equals方法)
String a="abc";
String b="abc";
String c =new String("abc");
System.out.println(a==b); // true
System.out.println(a.equals(b));//true
System.out.println(a==c);//false
System.out.println(a.equals(c)); //true
HashCodes()是获取哈希码,确定该对象在哈希表中的位置,仅当创建了某个类的散列表,该类的hashCode()才有用,
在散列表中,若两个元素相等,那么他们的散列码一定相同,但是反过来,若两个对象相等,他们的hashCode()并不一定相等;
主要有5种方式,继承Thread类,实现Runnable接口,实现Callable接口通过FutureTask包装器来创建Thread线程,使用线程池 接口ExecutorService结合Callable、future实现有返回结果的多线程,前两种没有返回值,后两种有返回值,还可以使用Spring的@Async注解非常方便的实现多线程。
继承Thread类重写run方法创建线程
Thread 类本质上是实现了Runnable接口的一个实例,启动线程是通过Thread类的start()方法,start方法是一个native方法,将 会启动一个新线程,并执行run()方法;
public class Mythread extends Thread { @Override public void run() { System.out.println(Mythread.currentThread().getName()); } public static void main(String[] args) { Mythread mythread = new Mythread(); mythread.start(); Mythread mythread1 = new Mythread(); mythread1.start(); } } 结果: Thread-0 Thread-1 Process finished with exit code 0
public class RunnableTest extends ParentDemo implements Runnable{ @Override public void testDemo() { System.out.println("123"); } @Override public void run() { System.out.println("这是Runnable执行结果"); } public static void main(String[] args) { RunnableTest runnableTest=new RunnableTest(); Thread thread=new Thread(runnableTest); thread.start(); } }
当传入一个Runnable target参数给Thread后,Thread的run()方法就会调用target.run();
继承Thread类存在局限性,因为java是单继承多实现,所以如果继承Thread类就不能在继承其它类, 其次,Thread类更适合开启多个线程完成多个任务的场景,而实现Runnable接口更适合一个任务多个线程去完成的场景。
public class CallAbleTest implements Callable<Integer> { @Override public Integer call() throws Exception { int sum = 0; for (int i = 0; i < 10000; i++) { sum += i; } return sum; } public static void main(String[] args) { CallAbleTest callAbleTest = new CallAbleTest(); FutureTask<Integer> result = new FutureTask<>(callAbleTest); new Thread(result).start(); new Thread(result).start(); try { Integer sum = result.get(); System.out.println(sum); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } } }
Callable 对象实际属于Executor框架的功能类;
Callable 可以在任务结束的时候提供一个返回值,Runnable没有;
Callable 中的call方法可以抛出异常,Runnable不能;
运行Callable可以拿到一个future对象,future对象表示异步计算的结果,可提供检查运算是否结束的方法,由于线程属于异步计算模型,所以无法从其他线程中得到方法的返回值,在这种情况之下,就可以使用future来监视目标线程调用call方法的情况,当调用future的get方法以获取结果时,当前线程就会被阻塞,直到call方法结束返回结果
public class ExecutorTest { static class MyCallable implements Callable<Object> { private String taskNum; MyCallable(String taskNum) { this.taskNum = taskNum; } @Override public Object call() throws Exception { System.out.println(">>>>" + taskNum + "开启任务"); Thread.sleep(1000); System.out.println(">>>>" + taskNum + "任务结束"); return taskNum; } } public static void main(String[] args) throws ExecutionException, InterruptedException { System.out.println("开始执行-------"); //创建一个线程池 ExecutorService pool = Executors.newFixedThreadPool(5); //创建多个有返回值的任务 List<Future> list = new ArrayList<Future>(); for (int i = 0; i < 5; i++) { Callable c = new MyCallable(i + " "); //执行任务并获取Future对象 Future f = pool.submit(c); list.add(f); } //关闭线程池 pool.shutdown(); for (Future f : list) { System.out.println(">>>" + f.get().toString()); } System.out.println("程序运行结束--------"); } }
Executors类提供了一系列工厂方法用于创建线程池,返回的线程池都实现了ExecutorService接口,ExecutorService中提供submit()方法,传递一个Callable,或者Runnable,返回Future,如果Executor没有完成Callable的计算,调用返回Future对象的get()方法将阻塞,直到计算完成。
首选需要配置线程参数
#核心线程数,当线程数小于核心线程数时,即使有线程空闲,线程池也会优先创建新线程,设置allowCoreThreadTimeout=true(默认false)时,核心线程会超时退出
spring.task.pool.corePoolSize=20
#最大线程数,当线程数大于等于corePoolSize,且任务队列已满时,线程池会创建新线程来处理任务
spring.task.pool.maxPoolSize=60
#线程空闲时间,当线程空闲时间达到keepAliveSeconds(秒)时,线程会退出,直到线程数量等于corePoolSize,如果allowCoreThreadTimeout=true,则会直到线程数量等于0
spring.task.pool.keepAliveSeconds=1
#任务队列容量,当核心线程数达到最大时,新任务会放在队列中排队等待执行
spring.task.pool.queueCapacity=400
创建线程池配置类
@Configuration @EnableAsync public class AsyncTaskConfig { @Autowired private ThreadPoolCOnfig threadPoolCOnfig; public ThreadPoolTaskExecutor myThreadPool(){ ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); //设置参数 executor.setCorePoolSize(threadPoolCOnfig.getCorePoolSize()); executor.setMaxPoolSize(threadPoolCOnfig.getMaxPoolSize()); executor.setQueueCapacity(threadPoolCOnfig.getQueueCapacity()); executor.setKeepAliveSeconds(threadPoolCOnfig.getKeepAliveSeconds()); executor.setAllowCoreThreadTimeOut(true); executor.setThreadNamePrefix("MyThreadPool"); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.initialize(); return executor; } }
目前String已经实现5种线程池
1. SimpleAsyncTaskExecutor :不是真的线程池,这个类不重用线程,默 认每次调用都会创建一个新的线程。 2. SyncTaskExecutor: 这个类没有实现异步调用,只是一个同步操作。只适用于不需要多线程的地方。 3. ConcurrentTaskExecutor: Executor的适配类,不推荐使用。如果ThreadPoolTaskExecutor不满足要求时,才用考虑使用这个 类。 4. SimpleThreadPoolTaskExecutor: 是Quartz的SimpleThreadPool的类。线程池同时被quartz和非quartz使用,才需要使用此 类。 5. ThreadPoolTaskExecutor : 最常使用,推荐。 其实质是对java.util.concurrent.ThreadPoolExecutor的包装。 @Async正常使用默认线程池,为SimpleAsyncTaskExecutor,一般有两种调用,一种是无返回值的调用,一种是有返回值的Future调用。 基于@Async无返回值调用,直接作用在类,使用方法上,加上@Async注解; 有返回值的Future调用,返回类型设置为Future<Object>,需要我们在方法中捕获异常并处理,或者在调用方调用Future.get时捕获异常进行处理 有返回值CompletableFuture调用,在jdk1.8中提供了强大的Future拓展功能,可以简化异步编程的复杂性,并且提供函数式编程能力 private static final ThreadPoolExecutor SELECT_POOL_EXECUTOR = new ThreadPoolExecutor(10, 20, 5000, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(1024), new ThreadFactoryBuilder().setNameFormat("selectThreadPoolExecutor-%d").build()); // tradeMapper.countTradeLog(tradeSearchBean)方法表示,获取数量,返回值为int // 获取总条数 CompletableFuture<Integer> countFuture = CompletableFuture.supplyAsync(() -> tradeMapper.countTradeLog(tradeSearchBean), SELECT_POOL_EXECUTOR); // 同步阻塞 CompletableFuture.allOf(countFuture).join(); // 获取结果 int count = countFuture.get();
默认线程池存在比较多的弊端,一般不用Executors去创建,推荐使用ThreadPoolExecutor方式,Executors方法各个弊端如下
newFixedThreadPool和newSingleThreadExecutor:主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM。
newCachedThreadPool和newScheduledThreadPool:要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM。
@Async 默认是使用SimpleAsyncTaskExecutor,该线程池默认是来一个任务创建一个线程,如果系统不断创建线程,最终会导致占用内存过高,引发OutOfMemoryError错误,但是它本身也存在一个限流机制,ConcurrencyLimit属性,其大于等于0,开启限流机制,等于-1,关闭限流
@Async 存在自定义线程池,方法如下
实现接口AsyncConfigurer
继承AsyncConfigurerSupport
配置自定义的TaskExecutor配置参数代替内置的任务执行器
不管是继承还是重新实现接口,都需要指定一个线程池,并且重新实现getAsyncExecutor()方法
初始状态(new):new一个线程实例,线程进入初始状态;
就绪状态(ready):
运行中状态(Running):线程调用run方法,进入运行态;
阻塞状态(Blocked): 线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态;
等待(waiting): 此状态的线程不会被CPU分配执行时间,要等待被显示的唤醒,否则将无限等待;
超时等待(TIMED_WAITING): 和等待状态类似,但是此状态不需要无限期等待,当达到一定时间,会被自动唤醒;
终止状态(TERMINATED): 当线程run()方法完成,或者主线程main()方法完成,我们就可以认为是终止的,一旦终止,不能复生,此时调用start()方法会抛出异常;
start(): 用于开启多线程,使线程处于就绪态,当CPU分配时间片后可 执行run()方法,并且start()方法是一个synchronized修饰的方法,而run只是一个普通方法,start()可实现并发
run(): 是一个普通方法,当线程调用了start()方法,一旦获取CPU调度,处于运行态,线程就会调用这个run();
wait(): 线程等待,让线程进入阻塞状态,2种方式,一种无限等待,需要使用notify()方法唤醒,另外一种,设置等待时间,时间一到自动唤醒wait(long time), wait()方法属于Object方法,释放CPU执行权,同时释放锁,同时wait()方法只能在同步代码方法中或者同步代码块中使用,使用时需要捕获InterruptedException异常
sleep(): 强迫一个线程睡眠N毫秒,释放CPU资源,不释放锁,使用地点也不需要限制,但是需要捕获异常,而且sleep()是Thread类的方法
isAlive(): 判断一个线程是否存活
currentThread(): 获取当前线程
yield(): 线程礼让,暂停当前正在执行的线程对象,虽然让出CPU时间,但是不会让线程阻塞,线程任然处于可执行状态,随时可再分的CPU执行时间
getPriority()与setPriority(): 设置获取线程优先级
getId():
join(): 线程自闭,等待其他线程终止,在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪。
package com.example.day1.one; public class Mythread extends Thread { @Override public void run() { for (int i = 0; i < 50; i++) { System.out.println(Mythread.currentThread().getName()); } } public static void main(String[] args) throws InterruptedException { Mythread mythread = new Mythread(); Mythread mythread1 = new Mythread(); Mythread mythread2 = new Mythread(); mythread.start(); //mythread.join() mythread1.start(); //mythread1.join() mythread2.start(); //mythread2.join() } } 结果:如果不加join(),在第一个线程还没结束的时候,第二个或者第三个就可能开始执行,如果加上join(),一定是第一个执行完,第二个执行,然后第三个执行
interrupted(): 判断是否被中断,并清除当前中断状态;
interrupt(): 中断线程,设置中断标志位;
isInterrupted(): 判断是否被中断;
isDaemon()与setDaemon(boolean on): 设置一个线程为守护线程(用户线程和守护线程的区别在于,是否等待主线程依赖于主线程结束而结束)
stop():
notify(): 唤醒一个线程继续运行
notifyAll(): 唤醒所有线程进入争抢锁状态
线程调度是指系统为线程分配处理器使用权的过程,主要调度方式有:协同式线程调度和抢占式线程调度;
协同式线程调度:线程的执行时间由线程本身控制,线程把自己工作执行完成之后,要主动通知系统切换到另外一个线程上
抢占式线程调度: 每个线程都将由系统来分配时间,线程的切换不由线程本身决定
线程池作用:
降低系统资源消耗,通过重用已存在的线程,降低线程创建与销毁造成的消耗;
提高系统响应速度,有任务到达,通过复用已存在的线程,无需等待新线程的创建便能立即执行;
方便线程并发数管理;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hHAh7hpE-1618197750701)(C:\Users\lenovo\AppData\Roaming\Typora\typora-user-images\image-20210301204725267.png)]
submit(task): 可用来提交Callable或Runnable任务,并返回代表此任务的Future对象
shutdown(): 在完成已提交的任务后封闭办事,不在接管新任务
shutdownNow(): 停止所有正在履行的任务并封闭办事;
isTerminated(): 测试是否所有任务都履行完毕了;
ThreadPoolExecutor参数解析
Ctl: 对线程池的运行状态和线程池中有效的数量进行控制的一个字段,包含2部分信息:线程池的运行状态(runState)和线程池内有效线程的数量(workerCount) 五种状态:Runing:可接受新任务,并且处理已经添加的任务,线程池的初始化状态就是running shutdown: 不接受新任务,但是可以处理已经添加的新任务,调用shutdown()方法,由running变成shutdown stop: 不接受新任务,不处理已添加的任务,并且还会中断正在处理的任务; tidying: 当所有任务终止,ctl记录的任务数为0,线程池就会变成tidying状态,此时会执行钩子函数terminated(); terminated: 当线程池处于tidying状态并且执行了terminated()方法,就会变成此状态; 参数解析 corePoolSize:线程池核心线程数,如果当线程池中的线程数为corePoolSize,继续提交的任务将被保存到"阻塞队列中",等待被执行,如果执行了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有核心线程 maximumPoolSize:线程池中允许的最大线程数,如果当前阻塞队列满了,且继续提交任务,则创建新的线程任务,前提是当前线程数少于maximumPoolSize; keepAliveTime: 线程池维护线程所允许的空闲时间,当线程池中的线程数大于corePoolSize的时候,如果此时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了keepAliveTime; unit: keepAliveTime的单位 workQueue: 用来保存等待被执行的任务的阻塞队列,任务必须实现Runnable接口,有如下几种阻塞队列 1. ArrayBlockingQueue:基于数组结构的有界阻塞队列,按 FIFO排序任务; 2. LinkedBlockingQuene:基于链表结构的阻塞队列,按FIFO排序任务 3. SynchronousQuene:一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态; 4. priorityBlockingQuene:具有优先级的无界阻塞队列; 5. DelayQueue(延迟队列)是一个任务定时周期的延迟执行的队列 6. LinkedTransferQueue:由链表构成的无界阻塞队列; 7. LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列,队列头部和尾部都可以添加和移除元素,多线程并发时,可将锁的竞争降到一半; threadFactory :ThreadFactory,用来创建新线程。默认使用 Executors.defaultThreadFactory() 来创建线程。使用默认的ThreadFactory来创建线程时,会使新创建的线程具有相同的NORM_PRIORITY优先级并且是非守护线程,同时也设置了线程的名称。 Handler:线程池的拒绝策略,当阻塞队列满了,且没有空闲的工作线程,如果继续提交任务,必须采取一种策略处理该任务,线程池提供了4种策略: 1、AbortPolicy:直接抛出异常,默认策略; 2、CallerRunsPolicy:用调用者所在的线程来执行任务; 3、DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务; 4、DiscardPolicy:直接丢弃任务; 上面的4种策略都是ThreadPoolExecutor的内部类。 当然也可以根据应用场景实现RejectedExecutionHandler接口,自定义策略。
**注意:**通过execute或者submit向线程池提交任务。任务提交的时候先提交给核心线程(corePoolSize);
如果核心线程满了,就将任务放到workQueue里面去排队等待;如果队列也满了(取决用的什么队列,以及
设置的大小),就会将新进来的任务提交给非核心线程,非核心线程数量等于maximumPoolSize -
corePoolSize,非核心线程使用之后会被回收。如果非核心线程也满了,那么就执行相应的拒绝策略
RejectedExecutionHandler。
原理解析
1. execute方法解析:先获取当前工作线程数,-->添加任务到核心线程上去执行-->核心线程数满了,线程池状态是running,则添加到任务队列中去-->添加任务之后,保证线程池状态还是running,否则移除刚才添加的任务,并知心拒绝策略;
2. 关键在于AddWork方法,先判断状态是否是running,shutdown时队列是否有数据,判断工作线程是否大于容量;
3. worker内部类,继承AbstractQueuedSynchronizer并且实现了Runnable接口
newFixedThreadPool (固定数目线程的线程池)
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}
核心线程数和最大线程数大小一样;
没有所谓的非空闲时间,keepAliveTime为0;
阻塞队列为无界阻塞队列LinkedBlockingQueue;
特点
如果线程数少于核心线程数,创建核心线程执行任务;
等于核心线程数,把任务添加到阻塞队列;
任务执行完毕去阻塞队列获取任务继续执行;
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < Integer.MAX_VALUE; i++) {
executor.execute(()->{
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
//do nothing
}
});
因为是无界阻塞队列,最大可容纳INTEGER.MAX个,可能会导致OOM,适用于处理CPU密集型的任务,适用
于执行长期的任务
newCachedThreadPool(可缓存线程的线程池)
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
threadFactory);
}
特点
核心线程数为0;
最大线程数为Integer.MAX_VAULE,阻塞队列是SynchronousQuene;
非核心线程空闲存活时间为60秒;
ExecutorService executor = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++) {
executor.execute(() -> {
System.out.println(Thread.currentThread().getName()+"正在执行");
});
}
用于执行大量短期的小任务;
newSingleThreadExecutor(单线程的线程池)
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory));
}
特点:
核心线程数和最大线程数都是1;
阻塞队列是LinkedBlockingQueue;
keepAliveTime 为0
适用于串行执行任务的场景,一个任务一个任务的执行
newScheduledThreadPool(定时及周期执行的线程池)
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
特点:
最大线程数为Integer.MAX_VALUE
阻塞队列是DelayedWorkQueue
keepAliveTime为0
scheduleAtFixedRate() :按某种速率周期执行
scheduleWithFixedDelay():在某个延迟后执行
技能提升
为什么不用Executors创建线程池?
因为LinkedBlockingQueue.offer方法,如果使用LinkedBlockingQueue队列而不限制大小的话,就是一个无
边界的队列,会导致内存溢出问题,一般采用构造函数ThreadPoolExecutor来创建,并指定容量,另外还可
以使用开源库,apache的guava,可以提供ThreadFactoryBuilder来创建线程池
public class ExecutorsDemo { private static ThreadFactory namedThreadFactory = new ThreadFactoryBuilder() .setNameFormat("demo-pool-%d").build(); private static ExecutorService pool = new ThreadPoolExecutor(5, 200, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(1024), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy()); public static void main(String[] args) { for (int i = 0; i < Integer.MAX_VALUE; i++) { pool.execute(new SubThread()); } } }
首先需要理解一些概念:
CPU密集型,也叫计算密集型,指的是系统的硬盘,内存性能相对CPU要好很多,CPU读/写I/O(硬盘/内存),
IO在很短的时间就可以完成,CPU占用率很高。
IO密集型,指系统的CPU性能相对硬盘,内存要好很多,此时,系统大部分状况是CPU在等IO(硬盘/内存)读写
操作,CPU利用并不高
一般情况下,一个服务器只部署一个应用并且只有一个线程池,
针对CPU密集型:线程数=n+1,n为CPU核数
IO密集型:线程数=2n+1
通常应该考虑线程的执行时间,可以这么设置
最佳线程数=((线程等待时间+线程CPU时间)/线程CPU时间)*CPU核数;
另外,也可根据情况动态调整;
技术文章:美团线程池实践:https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html
实现线程同步,除了用synchronized,另外也可以用ThreadLocal,前者是用时间换空间,保证每次只有
一个线程去访问,后者采用空间换时间,ThreadLocal在每个线程中都是独立的,一个变量在每一个线程中都
存在其实例的引用,各个线程之间没有关系。
**原理:**其内部有一个ThreadLocalMap内部类,ThreadLocalMap内部有个Entry内部类。
在每个线程Thread内部有一个ThreadLocalMap 类型的成员变量ThreadLocals,其就是用来存储实际的变
量副本,键值为当前ThreadLocal变量,value 为变量副本;
最开始,在Thread里面,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会
对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副
本变量为value,存到threadLocals
使用场景
在同一个线程的不同开发层次中共享数据
最常见的场景是用来解决数据库连接、Session管理等
private` `static` `ThreadLocal<Connection> connectionHolder
= ` `new` `ThreadLocal<Connection>() {
public` `Connection initialValue() {
` `return` `DriverManager.getConnection(DB_URL);
}
};
public` `static` `Connection getConnection() {
return` `connectionHolder.get();
}
private static final ThreadLocal threadSession = new ThreadLocal();
public static Session getSession() throws InfrastructureException {
Session s = (Session) threadSession.get();
try {
if (s == null ) {
s = getSessionFactory().openSession();
threadSession.set(s);
}
} catch (HibernateException ex) {
throw new InfrastructureException(ex);
}
return s;
}
ThreadLocal内存泄露问题
内存泄露:程序在申请内存后,无法释放已申请的内存空间,特别是内存泄露堆积,危害很大,不会被使用的对象或者变量占用的内存不能被回收,就是内存泄露
强引用:一个对象具有强引用,不会被垃圾回收器回收,当内存空间不足,java虚拟机宁愿抛出异常,使程序异常终止,也不回收这种对象,
如果想取消强引用和某个对象的关联,可以显示的将引用赋值为null;
弱引用:JVM进行垃圾回收的时候,无论内存是否充足,都会回收被弱引用关联的对象,在java中,用java.lang.ref.WeakReference类来表示。可以在缓存中使用弱引用
每一个Thread维护一个ThreadLocalMap,key为使用弱引用的ThreadLocal实例,value为线程变量的副本,hreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal不存在外部强引用时,Key(ThreadLocal)势必会被GC回收,这样就会导致ThreadLocalMap中key为null, 而value还存在着强引用,只有thead线程退出以后,value的强引用链条才会断掉,但如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链,永远无法回收,造成内存泄漏
由于Thread中包含变量ThreadLocalMap,因此ThreadLocalMap与Thread的生命周期是一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set(),get(),remove()的时候会被清除。
因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用;
所以,每次使用完ThreadLocal都要调用他的remove()方法清除数据,将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉;
如何在子线程中获取主线程threadLocal中set()方法设置的值
可以使用InheritableThreadLocal,ThreadLocal threadLocal = new InheritableThreadLocal(),这样在子线程
中就可以通过get方法获取到主线程set方法设置的值了,
InheritableThreadLocal继承了ThreadLocal,并且重写了childValue、getMap和createMap方法,当在主线程
中创建InheritableThreadLocal实例对象后,当前线程Thread对象中维护了一个inheritableThreadLocals变
量,它也是ThreadLocalMap类型,在创建子线程的过程中,将主线程维护的inheritableThreadLocals变量的
值复制到子线程维护的inheritableThreadLocals变量中,这样子线程就可以获取到主线程设置的值了
volatile是java提供的一种轻量级的同步机制,java中包含两种在内的同步机制,同步块(或方法)和volatitle
变量,相比于synchronized(重量级锁),volatile更轻量级,因为它不会引起线程上下文的切换和调度。但是
volatile 变量的同步性较差(有时它更简单并且开销更低),而且其使用也更容易出错。
并发编程3个基本概念
原子性
可见性:多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看到修改的值
在多线程环境下,一个线程对共享变量的操作对其他线程是不可见的,java提供了volatitle来保证可见性,当一个变量被volatitle修饰后,表示着线程本地内存无效,当一个线程修改共享变量后他会立即被更新到主内存中,其他线程读取共享变量时,会直接从主内存中读取。当然,synchronize和Lock都可以保证可见性。synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。
有序性:如果在本线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有操作都是无序的。前半句是指“线程内表现为串行语义”,后半句是指“指令重排序”现象和“工作内存主主内存同步延迟”现象,Java提供volatile来保证一定的有序性。最著名的例子就是单例模式里面的DCL(双重检查锁)。另外,可以通过synchronized和Lock来保证有序性,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。
锁的互斥和可见性
volatile 变量提供理想的线程安全,必须同时满足下面两个条件
JMM定义了线程和主内存之间的抽象关系:共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存保存了被该线程使用到的主内存的副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。
解决这种共享变量在多线程模型中的不可见性问题,较粗暴的方式自然就是加锁,但是此处使用synchronized或者Lock这些方式太重量级了,比较合理的方式其实就是volatile;
volatitle变量特性
使用volatile关键字修饰共享变量便可以禁止这种重排序。若用volatile修饰共享变量,在编译时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序,volatile禁止指令重排序也有一些规则:
volatitle不适用的场景
volatitle原理
volatile可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。在JVM底层volatile是采用“内存屏障”来实现的。观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
单列模式的双重锁为啥要加volatitle
如果发生指令重排,会导致程序初始化错误
第一重锁,判断是否初始化,第二重锁,判断是否被多次实例化
class Singleton{
// 确保产生的对象完整性
private volatile static Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if(instance==null) { // 第一次检查
synchronized (Singleton.class) {
if(instance==null) // 第二次检查
instance = new Singleton();
}
}
return instance;
}
}
Memory Barrier(内存屏障)
volatile语义中的内存屏障
final语义中的内存屏障
优秀博文:https://blog.csdn.net/cy973071263/article/details/104383636
资源共享解决方案
synchronized原理
synchronized用来保证原子性和可见性,是利用锁的机制来实现线程同步,synchronized能保证原子性,可见性,有序性,和可重入性(避免死锁)
一个变量在同一个时刻只允许一个线程对其进行lock,持有一个锁的两个同步块只能串行执行,这就保证了原子性和有序性;
在对一个变量进行unlock操作前,必须也先把此变量同步回主内存中,这就实现了可见性
synchronized用法
synchronized可以修饰静态方法,成员函数(非静态方法),还可以直接修饰代码块,但是上锁的资源只有两类,对象和类
同步方法:是通过头部标志位用ACC_synchronized标志,告诉JVM这是一个同步方法;
同步代码块:通过Moninorenter和Monitorexit指令,会让线程在执行时,使其持有的锁计数器加1或者减1。每一个对象在同一时间只与一个monitor(锁)相关联,而一个monitor在同一时间只能被一个线程获得,一个线程在尝试获得与对象相关联的Monitor锁的所有权的时候;
java对象头
在java中,每个对象都会有一个monitor对象(监视器),JVM中,对象分三部分组成(对象头、实例数据、填充数据)
monitor对象是实现锁机制的基础,线程获取锁本质是线程获取Java对象对应的monitor对象。*重量级锁就是通过**ObjectMonitor**实现的,也就是说重量级锁是基于对象的**monitor**来实现的*
ObjectMonitor中有2个队列,—WaitSet和—EntryList
JVM对synchronized的优化
在 Java 早期版本中,synchronized 属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock(互斥锁) 来实现的。Java 的线程是映射到操作系统的原生线程之上的(详见Java线程和操作系统线程的关系)。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态
jdk1.6作出了大量的优化,主要有自旋锁、适应性自旋锁、锁消除、锁粗化等优化方法,又增加了两个锁的状态:偏向锁,轻量级锁,所以现在synchronized一共有四种锁状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态
synchronized锁优化方法
锁膨胀:根据实际情况进行膨胀升级,依次是:无锁——>偏向锁——>轻量级锁——>重量级锁,并且膨胀方向不可逆。也就是说对象对应的锁是会根据当前线程申请,抢占锁的情况自行改变锁的类型。
**偏向锁:**减少同一线程获取锁的代价。在大多数情况下,锁不存在多线程竞争,总是由同一线程多次获得,那么此时就是偏向锁。偏向锁的“偏”就是偏心的偏,它的意思是会偏向于第一个获得它的线程,如果在接下来的执行中,该锁没有被其他线程获取,那么持有偏向锁的线程就不需要进行同步!线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作,使用偏向锁也就去掉了这一部分的负担,也取消掉了加锁和解锁的过程消耗。
*轻量级锁在无竞争的情况下使用* *CAS 操作**去代替使用互斥量。而偏向锁在无竞争的情况下会把整个同步都消除掉*。
轻量级锁:偏向锁只允许一个线程获得锁,轻量级锁是允许多个线程获得锁,但是只允许他们顺序拿,不允许存在竞争,轻量级锁的加锁和解锁都是通过CAS操作实现
自旋锁:轮询申请锁,在轻量级锁转重量级锁之间,可能会存在自旋;
重量级锁:当同一时间有多个线程竞争锁时,锁就会被升级成重量级锁,此时其申请锁带来的开销也就变大,这种依赖于操作系统Mutex Lock所实现的锁我们称之为 “重量级锁”。它通过操作系统的互斥量和线程的阻塞和唤醒来实现锁机制;
各自优缺点
锁消除
消除锁是虚拟机另外一种锁的优化,这种优化更彻底,在****JIT****编译时,对运行上下文进行扫描,去除不可能存在竞争的锁。锁消除可以节省毫无意义的请求锁的时间。比如下面代码的method1和method2的执行效率是一样的,因为object锁是私有局部变量,不存在所得竞争关系。
主要通过"逃逸分析"技术来实现,判断一个同步块所使用的锁对象是否只能被一个线程访问而没有被发布到其他线程;
锁粗化
锁粗化是虚拟机对另一种极端情况的优化处理,通过扩大锁的范围,避免反复加锁和释放锁
博文连接:https://blog.csdn.net/cy973071263/article/details/104388899
加锁可以使用 synchronized 关键字,也可以使用 ReentrantLock 对象。synchronized 加锁后,会在同步代码块内添加 monitorenter他monitorexit这两个字节码指令。而 ReentrantLock 加锁,则是api层面实现的,它主要依赖于 Unsafe类的线程挂起和恢复功能。
ReentrantLock 重入锁,实现lock接口的一个类,支持可重入性,公平锁和非公平锁,
可重入获取锁,加锁核心方法是nonfairTryAcquire;
释放锁:tryRelease方法;
ReentrantLock 与synchronized区别
wait/notify/notifyAll
只能随机唤醒一个线程或者全部唤醒;ReentrantLock指定公平锁和非公平锁
公平锁和非公平锁描述的是多个线程竞争锁的时候要不要排队,直接排队的就是公平锁;先尝试插队,插队失败再排队的就是非公平锁;
//ReentrantLock两个构造方法,ReentrantLock默认是非公平锁,第二个传入参数能够声明是公平还是非公平
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
ReentrantLock结合Condition类实现有条件的分组唤醒
如果有三个线程Thread1,Thread2,Thread3按顺序打印A、B、C,可以使用ReentrantLock结合Condition
public class ReentrantLockTest { private static final Lock lock = new ReentrantLock(); private static Condition conditionA = lock.newCondition(); private static Condition conditionB = lock.newCondition(); private static Condition conditionC = lock.newCondition(); public static volatile int permit = 0; public static void main(String[] args) { Thread thread1 =new Thread(() -> { try { Thread.sleep(200); lock.lock(); for (int i = 0; i < 10; i++) { while ((permit % 3) !=0) { conditionA.await(); } System.out.println("A"); permit++; conditionB.signal(); } conditionB.signal(); } catch (InterruptedException e){ e.printStackTrace(); }finally { lock.unlock(); } }); Thread thread2 = new Thread(() ->{ try { Thread.sleep(200); lock.lock(); for (int i = 0; i < 10; i++) { while ((permit % 3) !=1) { conditionB.await(); } System.out.println("B"); permit++; conditionC.signal(); } conditionC.signal(); } catch (InterruptedException e){ e.printStackTrace(); }finally { lock.unlock(); } }); Thread thread3 = new Thread(() ->{ try { Thread.sleep(200); lock.lock(); for (int i = 0; i < 10; i++) { while ((permit % 3) !=2) { conditionC.await(); } System.out.println("C"); permit++; conditionA.signal(); } conditionA.signal(); } catch (InterruptedException e){ e.printStackTrace(); }finally { lock.unlock(); } }); thread1.start(); thread2.start(); thread3.start(); } } 上面的方法还可以优化一下,去掉取余运算 public class AbcTest implements Runnable { //打印次数 private static final int PRINT_COUNT = 10; //锁 private final ReentrantLock reentrantLock; //本次要打印所需要的condition private final Condition thisCondition; //下次要打印所需要的condition private final Condition nextCondition; //要打印的字符 private final char printChar; public AbcTest(ReentrantLock reentrantLock, Condition thisCondition, Condition nextCondition, char printChar) { this.reentrantLock = reentrantLock; this.thisCondition = thisCondition; this.nextCondition = nextCondition; this.printChar = printChar; } @Override public void run() { //进入临界区 reentrantLock.lock(); try { for (int i = 0; i < PRINT_COUNT; i++) { System.out.println(printChar); //使用nextCondition唤醒下一个线程 //因为只有一个线程,所以signal或者signalAll都可以 nextCondition.signal(); //不是最后一次,则通过thisCondition等待被唤醒 //此处要加判断,不然10次后会死锁 if (i < PRINT_COUNT - 1) { try { thisCondition.await(); } catch (InterruptedException e) { e.printStackTrace(); } } } } finally { reentrantLock.unlock(); } } public static void main(String[] args) throws InterruptedException { ReentrantLock lock = new ReentrantLock(); Condition conditionA = lock.newCondition(); Condition conditionB = lock.newCondition(); Condition conditionC = lock.newCondition(); Thread thread = new Thread(new AbcTest(lock, conditionA, conditionB, 'A')); Thread thread1 = new Thread(new AbcTest(lock, conditionB, conditionC, 'B')); Thread thread2 = new Thread(new AbcTest(lock, conditionC, conditionA, 'C')); thread.start(); Thread.sleep(100); thread1.start(); Thread.sleep(100); thread2.start(); Thread.sleep(100); } } 方法二:通过一个锁和一个状态变量来实现(推荐) public class AbcTest1 { //状态变量 private volatile int state = 0; //打印线程 private class Printer implements Runnable { private static final int PRINT_COUNT = 10; //打印锁 private final Object printLock; //打印标志位和state变量相关 private final int printFlag; //后续线程的线程打印标志位 private final int nextPrintFlag; //打印字符 private final char printChar; public Printer(Object printLock, int printFlag, int nextPrintFlag, char printChar) { this.printLock = printLock; this.printFlag = printFlag; this.nextPrintFlag = nextPrintFlag; this.printChar = printChar; } @Override public void run() { //获取打印锁,进入临界区 synchronized (printLock) { //联系打印printCount次 for (int i = 0; i < PRINT_COUNT; i++) { //循环检验标志位,每次都阻塞然后等待唤醒 while (state != printFlag) { try { printLock.wait(); } catch (InterruptedException e) { return; } } System.out.print(printChar); //设置状态变量为下一个线程标志位 state = nextPrintFlag; //一个线程在操作,另外两个在等待,所以需要notifyAll(); printLock.notifyAll(); } } } } public void test() throws InterruptedException { Object lock = new Object(); Thread threadA = new Thread(new Printer(lock, 0, 1, 'A')); Thread threadB = new Thread(new Printer(lock, 1, 2, 'B')); Thread threadC = new Thread(new Printer(lock, 2, 0, 'C')); threadA.start(); Thread.sleep(100); threadB.start(); Thread.sleep(100); threadC.start(); Thread.sleep(100); } public static void main(String[] args) throws InterruptedException { AbcTest1 abcTest1 = new AbcTest1(); abcTest1.test(); } }
三个Java多线程循环打印递增的数字,每个线程打印5个数值,打印周期1-75
public class AddNumberTest { //计数器 private final AtomicInteger atomicInteger = new AtomicInteger(0); private class Printer implements Runnable { //总打印次数 private static final int TOTAL_PRINT_COUNT = 5; //每次打印5次 private static final int PER_PRINT_COUNT = 5; private final ReentrantLock reentrantLock; //本线程的Condition private final Condition thisCondition; //前一个线程的Condition private final Condition afterCondition; public Printer(ReentrantLock reentrantLock, Condition thisCondition, Condition afterCondition) { this.reentrantLock = reentrantLock; this.thisCondition = thisCondition; this.afterCondition = afterCondition; } @Override public void run() { //进入临界区 reentrantLock.lock(); try { //循环打印5次 for (int i = 0; i < TOTAL_PRINT_COUNT; i++) { //打印操作,每次打印5个数 for (int j = 0; j < PER_PRINT_COUNT; j++) { //以原子方式将当前值加1 //incrementAndGet返回的是新值(加1后的值) System.out.print(atomicInteger.incrementAndGet()); } //操作完成通知后面的线程 afterCondition.signalAll(); if (i < TOTAL_PRINT_COUNT - 1) { try { //本线程释放锁并等待唤醒 thisCondition.await(); } catch (InterruptedException e) { e.printStackTrace(); } } } } finally { reentrantLock.unlock(); } } } public void test() throws InterruptedException { ReentrantLock reentrantLock = new ReentrantLock(); Condition conditionA = reentrantLock.newCondition(); Condition conditionB = reentrantLock.newCondition(); Condition conditionC = reentrantLock.newCondition(); Thread threadA = new Thread(new Printer(reentrantLock, conditionA, conditionB)); Thread threadB = new Thread(new Printer(reentrantLock, conditionB, conditionC)); Thread threadC = new Thread(new Printer(reentrantLock, conditionC, conditionA)); threadA.start(); Thread.sleep(100); threadB.start(); Thread.sleep(100); threadC.start(); } public static void main(String[] args) throws InterruptedException { AddNumberTest addNumberTest=new AddNumberTest(); addNumberTest.test(); } }
ReentrantLock提供了能够中断等待锁的线程的机制
ReentrantLock主要是靠ReentrantLock#lockInterruptibly()
方法来提供一种可中断的机制
public class LockDemo { private static final Lock lock = new ReentrantLock(); public static void main(String[] args) throws InterruptedException { Thread threadA = new Thread(() -> { try { lock.lockInterruptibly(); System.out.println(System.currentTimeMillis() + " -> thread-1 获取了锁"); } catch (InterruptedException e) { System.out.println(System.currentTimeMillis() + "-> thread-1 被中断"); } finally { //lock.unlock(); } }); Thread threadB = new Thread(() -> { try { lock.lockInterruptibly(); System.out.println(System.currentTimeMillis() + "-> thread-2 获取到了锁"); Thread.sleep(3000); System.out.println(System.currentTimeMillis() + "-> thread-2 中断thread-1"); threadA.interrupt(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } }); threadB.start(); Thread.sleep(200); threadA.start(); } } 输出 1614852259509-> thread-2 获取到了锁 1614852262510-> thread-2 中断thread-1 1614852262510-> thread-1 被中断
ReentrantLock内部有一个成员变量,sync,它继承自AbstractQueuedSynchronizer类,同时 sync 有两个实现类,NonfairSync 和 FairSync类,分别代表着非公平锁模式和公平锁模式。ReentrantLock内部的 sync 成员变量默认是非公平锁模式。
其实 ReentrantLock 的逻辑有一大半依靠 AbstractQueuedSynchronizer 类实现,它就是大名顶顶的 AQS,AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如常用的ReentrantLock/Semaphore/CountDownLatch
先简单介绍AQS
AbstractQueuedSynchronizer(简称AQS),提供原子式管理状态、阻塞和唤醒线程功能以及队列模型的简单框架
static final class NonfairSync extends Sync { ... final void lock() { if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); } ... } 这段代码含义为: a. 若通过CAS设置变量State(同步状态)成功,也就是获取锁成功,则将当前 线程设置为独占线程。 b. 若通过CAS设置变量State(同步状态)失败,也就是获取锁失败,则进入 Acquire方法进行后续处理。 在看公平锁 tatic final class FairSync extends Sync { ... final void lock() { acquire(1); } ... } Lock函数通过Acquire方法进行加锁,但是具体是如何加锁的呢? 结合公平锁和非公平锁的加锁流程,虽然流程上有一定的不同,但是都调用了Acquire方法,而Acquire方法是FairSync和UnfairSync的父类AQS中的核心方法
AQS整体架构
AQS框架共分为5层:API层、锁获取方法层、对列方法层、排队方法层、数据提供层;
**AQS核心思想:**如果被请求的共享资源空闲,那么就将当前请求资源的线程设置为有效的工作线程,将共享资源设置为锁定状态,如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配,这个机制主要是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中;
CLH:Craig、Landin and Hagersten队列,是单向链表,AQS中的队列是CLH变体的虚拟双向队列(FIFO),AQS是通过将每条请求共享资源的线程封装成一个节点来实现锁的分配
AQS使用一个volatitle的int成员变量来表示同步状态(state),通过内置的FIFO队列来完成资源获取的排队工作,通过CAS完成对state值修改,下图(AQS数据结构)
waitStatus: 当前节点在队列中的状态
thread: 表示处于该节点的线程
prev: 前驱指针
predecessor:返回前驱节点,没有抛出npe
nextWaiter: 指向下一个处于CONDITION状态节点
next: 后续指针
线程锁的2种模式
shared: 表示线程以共享的模式等待锁;
exclusive: 表示线程正在以独占的方式等待锁;
JUC中AQS应用场景
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-svZVQcnv-1618197750707)(C:\Users\lenovo\AppData\Roaming\Typora\typora-user-images\image-20210304190836375.png)]
AQS主要是加锁acquire()方法,解锁release()方法以及state标识
AQS解析:https://tech.meituan.com/2019/12/05/aqs-theory-and-apply.html
美团技术团队:https://tech.meituan.com/
功能:
普通读写:读写一个Object对象的field,直接从内存中的一个地址读写
volatitle读写:可以保证可见性和有序性;
有序写:保证有序性不保证可见性;
直接内存操作:申请内存,重新申请内存,内存初始化,释放内存,内存复制
CAS相关:提供int,long,和Object的CAS操作;
偏移量相关:staticFieldOffset方法用于获取静态属性Field在对象中的偏移量,读写静态属性时必须获取其偏移量。objectFieldOffset方法用于获取非静态属性Field在对象实例中的偏移量,读写对象的非静态属性时会用到这个偏移量。staticFieldBase方法用于返回Field所在的对象。arrayBaseOffset方法用于返回数组中第一个元素实际地址相对整个数组对象的地址的偏移量。arrayIndexScale方法用于计算数组中第一个元素所占用的内存空间。
线程调度:LockSupport中的park和unpark方法正是通过Unsafe来实现的,park挂起线程,unpark唤醒线程;
类加载:defineClass方法定义一个类,用于动态地创建类。
defineAnonymousClass用于动态的创建一个匿名内部类。
allocateInstance方法用于创建一个类的实例,但是不会调用这个实例的构造方法,如果这个类还未被初始化,则初始化这个类。
shouldBeInitialized方法用于判断是否需要初始化一个类。
ensureClassInitialized方法用于保证已经初始化过一个类。
内存屏障:loadFence:保证在这个屏障之前的所有读操作都已经完成。
storeFence:保证在这个屏障之前的所有写操作都已经完成。
fullFence:保证在这个屏障之前的所有读写操作都已经完成。
unsafe应用
内存操作:内存分配、释放、拷贝等,java中对堆外内存的操作,依赖于Unsafe提供的操作堆外内存的native方法;
为什么使用堆外内存:对垃圾回收停顿的改善,堆外内存直接受操作系统管理而不是JVM,所以当我们使用堆外内存时,即可保持较小的堆内内存规模。从而在GC时减少回收停顿对于应用的影响;
提升程序I/O操作的性能。通常在I/O通信过程中,会存在堆内内存到堆外内存的数据拷贝操作,对于需要频繁进行内存间数据拷贝且生命周期较短的暂存数据,都建议存储到堆外内存;
DirectByteBuffer是java用于实现堆外内存的一个重要类,通常用在通信过程中做缓冲池,如在Netty、MINA等NIO框架中应用广泛。DirectByteBuffer对于堆外内存的创建、使用、销毁等逻辑均由Unsafe提供的堆外内存API来实现;
创建DirectByteBuffer的时候,通过Unsafe.allocateMemory分配内存、Unsafe.setMemory进行内存初始化,而后构建Cleaner对象用于跟踪DirectByteBuffer对象的垃圾回收,以实现当DirectByteBuffer被垃圾回收时,分配的堆外内存一起被释放
unsafe应用解析:https://tech.meituan.com/2019/02/14/talk-about-java-magic-class-unsafe.html
1.2.13 并发工具三巨头CountDownLatch、CyclicBarrier、Semaphore使用
execute() 方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;
2)submit()方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完;
JUC包中的原子类是哪4类?
基本类型:
数组类型:
引用类型:
对象的属性修改类型:
AtomicInteger的使用
public final int get() //获取当前的值
public final int getAndSet(int newValue)//获取当前的值,并设置新的值
public final int getAndIncrement()//获取当前的值,并自增
public final int getAndDecrement() //获取当前的值,并自减
public final int getAndAdd(int delta) //获取当前的值,并加上预期的值
boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update)
public final void lazySet(int newValue)//最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
inCrementAndGet();//实现++操作,原子性,不需要加锁也是线程安全的;
AtomicInteger主要使用CAS+volatitle和native方法保证原子性,从而避免synchronized的高开销,设计到Unsafe方法;
AQS组件总结
CountDownLatch 和CycliBarrier有什么不同
CountDownLatch类位于java.util.concurrent包下,利用它可以实现类似计数器的功能,一个线程(或者多个), 等待另外N个线程完成某个事情之后才能执行
CyclicBarrier通过它可以实现让一组线程等待至某个状态之后再全部同时执行,CyclicBarrier可以被重用
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Kv71Wceh-1618197750707)(C:\Users\lenovo\AppData\Roaming\Typora\typora-user-images\image-20210305102855558.png)]
volatitle关键字场景
状态标记量
双重检查
public class Singleton { private volatile static Singleton instance = null; private Singleton() {} public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) {// 1 if (instance == null) {// 2 instance = new Singleton();// 3 } } } return instance; } }
独立观察
开销较低的读-写锁策略
如何终止一个线程
一般是当使用run()或者call()方法时,执行完毕线程会自动结束,如果要手动结束一个线程,可以使用volatitle布尔变量来退出run()方法的循环或者是取消任务来中断线程;
生产者消费者模型
public class ProductAndConsumer { final Lock lock = new ReentrantLock(); final Condition notFull = lock.newCondition(); final Condition notEmpty = lock.newCondition(); final Object[] items = new Object[100]; int putptr, takeptr, count; public void put(Object x) throws InterruptedException { lock.lock(); try { while (count == items.length) { notFull.await(); } items[putptr] = x; if (++putptr == items.length) { putptr = 0; } ++count; notEmpty.signal(); } finally { lock.unlock(); } } public Object take() throws InterruptedException { lock.lock(); try { while (count == 0) notEmpty.await(); Object x = items[takeptr]; if (++takeptr == items.length) takeptr = 0; --count; notFull.signal(); return x; } finally { lock.unlock(); } } }
什么是FutureTask
表示一个可以取消的异步运算,它有启动和取消运算、查询运算是否完成和取回运算结果等方法,此类提供了对 Future 的基本实现。只有当运算完成的时候结果才能取回,如果运算尚未完成get方法将会阻塞。可使用 FutureTask 包装 Callable 或 Runnable 对象。因为 FutureTask 实现了 Runnable,所以可将 FutureTask 提交给 Executor 执行
Java中interrupt 、interrupted 和 isInterruptedd方法的区别?
interrupt 中断线程,如果没有中断,则该线程的checkAccess方法会被调 用,可能抛出SecurityException异常;
如果线程在调用 Object 类的 wait()、wait(long) 或 wait(long, int) 方法,或者该类的 join()、join(long)、join(long, int)、sleep(long) 或 sleep(long, int) 方法过程中受阻,则其中断状态将被清除,它还将收到一个 InterruptedException;
如果该线程在可中断的通道上的 I/O 操作中受阻,则该通道将被关闭,该线程的中断状态将被设置并且该线程将收到一个 ClosedByInterruptException。
如果该线程在一个 Selector 中受阻,则该线程的中断状态将被设置,它将立即从选择操作返回,并可能带有一个非零值,就好像调用了选择器的 wakeup 方法一样。
interrupted() 和 isInterrupted()的主要区别是前者会将中断状态清除而后者不会。
interrupted 是作用于当前线程,isInterrupted 是作用于调用该方法的线程对象所对应的线程
java中的同步集合与并发集合有什么区别?
都支持线程安全,主要区别体现在性能和可扩展性,还有如何实现线程安全的;
同步:HashMap,Hashtable,HashSet,Vector,ArrayList相对比他们并发实现(如ConcurrentHashMap,CopyOnWriteArrayList,CopyOnWriteHashSet)会慢很多,造成此种原因是因为锁,同步集合会加锁,而并发集合不会加锁,并发集合实现线程安全是通过先进和成熟的技术像锁剥离,比如ConcurrentHashMap分段;
fail-fast和fail-safe机制
fail-fast即快速失败机制,是java集合(Collection)中的一种错误检测机制。当在迭代集合的过程中该集合在结构上发生改变的时候,就有可能会发生fail-fast,即抛出ConcurrentModificationException异常
fail-safe 采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历
优秀博文:https://blog.csdn.net/hzau_itdog/article/details/91359145
优秀博文:https://blog.csdn.net/qq_36860032/article/details/89928062
java多线程实现消费者生产者模型:https://www.jb51.net/article/183079.htm
Collection接口是List、set、Queue的父级接口;
Set接口有三个常用实现类,HashSet、TreeSet;EnumSet;
List下有ArrayList和Vector;
Collection框架中实现比较要怎么做?
实体类实现Compareable接口,并实现compareTo(T t)方法,此法为内部比较器;
创建一个外部比较器,外部比较器要实现Comparator接口的compare(T t1,T t2)方法
1. 定义外部类Student,无需实现任何接口
2. 定义外部比较器实现Comparator接口
public class StudentComparator implements Comparator<Student>{
@Override
public int compare(Student o1, Student o2) {
if (o1.getAge() > o2.getAge()) {
return 1;
}else if (o1.getAge() == o2.getAge()) {
return 0;
}else{
return -1;
}
}
ArrayList和Vector的区别(是否有序,是否重复、数据结构、底层实现)
ArrayList和LinkedList区别?
ArrayList和Set的区别?
ArrayList、Vector、LinkedList、Set遍历方式
For循环遍历ArrayList
增强for循环foreach 遍历ArrayList
迭代器Iterator遍历
Iterator iterator = list.iterator();
while(iterator.hasNext()) {
System.out.print(iterator.next() + " ");
}
双向迭代器listIterator
ListIterator listIterator = list.listIterator();
while (listIterator.hasNext()) {
System.out.print(listIterator.next() + " ");
}
HashMap与HashTable区别?
HashMap线程不安全、HashTable 线程安全,HashMap允许null键值,hashTable 不允许;
HashMap线程不安全是因为主要考虑到了多线程环境下进行扩容可能出现HashMap死循环,hashtable 单个方法操作是线程安全的,但是符合操作时不一定能保证,容易导致越界异常
继承的父类不同,hashMap继承自AbstractMap类,而hashtable继承自Dictionary类,不过都同时实现了map,Cloneable(可复制)、Serializable(可序列化)三个接口
当需要线程安全的时候也可以实现ConcurrentHashMap,效率也比hashTable高,因为其使用了分段锁;
扩容不同,HashTable默认初始化大小是11,之后每次扩容,容量是原来2n+1倍,HashMap默认是16,每次扩容为原来的2倍,
hashtable会尽量使用素数,基数,hashMap总是使用2的幂作为哈希表大小;
计算hash值方法不同,HashTable直接使用对象的hashCode,**hashCode是JDK根据对象的地址或者字符串或者数字算出来的int类型的数值。然后再使用除留余数法来获得最终的位置。**所以比较耗时,hashMap是将hash表的大小固定在2的幂,这样在取模运算时,不需要做除法运算,只需要做位运算,效率比除法高,但是相应的hash冲突也多了,hashMap也采取了分散法来打散运算结果;
或者可以使用Collections的synchronizedMap方法使hashMap具有同步的能力,LinkedHashMap记录了插入顺序(有序)
synchronizedMap使用互斥锁+synchronized关键字实现线程安全
HashMap遍历?
1.通过Map.keySet来遍历key和value
for (String key : map.keySet()) {
System.out.println("key= "+key+" and value= "+map.get(key));
}
2. 通过Map.entrySet使用iterator遍历key和value
Iterator<Map.Entry<String, String>> it =
map.entrySet().iterator();
while(it.hasNext()){
Map.Entry<String, String> entry = it.next();
System.out.println("key= "+entry.getKey()+" and value= "+entry.getValue());
}
3. 通过Map.values()遍历所有的value,但是不能遍历key;
Map集合java8新特性?
HashMap扩容机制?
hashSet和TreeSet的区别
解决hash冲突方法?
数组和集合List之间的转换?
主要通过Arrays.asList以及List.toArray方法
hashMap的put方法的具体流程?
1. 如果table为空或者长度为0,即没有元素,使用resize方法扩容;
2. 计算插入存储的数组索引i,如果数组为空,即不存在hash冲突,则直接插入数组;
3. 插入时如果发生hash冲突,则进行判断:
a. 判断table[i]的元素key是否与需要插入的key相同,若相同则直接使用新的vlue覆盖掉旧的value,使用equals方法;
b. 继续判断,需要插入的数据结构是红黑树还是链表,如果是红黑树,则直接在树中插入or更新键值对,如果是链表,则在链表中插入或者更新键值对;链表长度>8,则转为红黑树;
4. 插入成功,检查存在的键值对数量size>最大容量,大于则进行扩容;
jdk1.8,Map为什么要引入红黑树
主要是为了解决二叉树的缺陷,二叉树在特殊情况下会变成一条线性结构(会跟链表一样,造成很深的问题),遍历查询会非常慢,而红黑树在插入新数据时可能通过左旋、右旋、变色操作保持平衡,可以解决查找数据慢,链表深度太深的问题,当然链表很短就不需要引入红黑树;
为什么hashMap在链表长度大于8的时候转为红黑树?
主要是考虑到树需要存储节点占用空间是普通节点的2倍,节点足够多的时候,可以一空间换时间,保证效率,通常情况下链表长度很难达到8,在链表长度为8时性能已经很差;
介绍
BIO:同步阻塞IO,在读写操作完成之前,线程一直阻塞;
NIO:多路复用同步非阻塞IO,提供了Chanel、selector、buffer等新的概念
AIO: 异步非阻塞IO
NIO主要有三大核心:channel(通道)、Buffer(缓冲区)、Selector(选择区)
channel(通道):双向的,可进行读操作,也可进行写操作,主要实现有FileChannel,DatagramChannel,SocketChannel,ServerSocketChannel;
Buffer:缓冲区
Selector(选择区):能够检测多个注册的通道上是否有事件发生,如果有事件发生,便获取事件然后针对每个事件进行相应的响应处理;
多路复用三种实现?
**select:**它仅仅知道,有I/O事件发生,并不知道是那个流,只能无差别轮询所有流,找出能读或者写入的流,操作流程
使用copy_from_user 从用户空间拷贝fd_set到内核空间;
注册回调函数_pollwait
遍历所有fd,调用其对应的poll方法;
以tcp_poll为例,其核心实现就是_pollwait
__pollwait的主要工作就是把current(当前进程)挂到设备的等待队列中,不同的设备有不同的等待队列;
poll方法返回时会返回一个描述读写操作是否就绪的mask掩码,根据这个mask掩码给fd_set赋值;
如果遍历完所有的fd,还没有返回一个可读写的mask掩码,则会调用schedule_timeout是调用select的进程(也就是current)进入睡眠;
把fd_set从内核空间拷贝到用户空间;
**缺点:**单个进程所打印的fd是有限制的,通过FD_SETSIZE设置,默认1024;
都会把fd从用户态拷贝到内核态,这个开销在fd很多时很大;
poll:将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,没有最大连接数的限制,基于链表存储;
链接:https://blog.csdn.net/weixin_39662142/article/details/110396979
字节流和字符流那个好,怎么选择?
大多数情况使用字节流,大多数IO都是直接操作磁盘文件,所以一般都是以字节的形式进行;
如果操作IO需要通过内存中频繁处理字符串的情况,就用字符流,因为字符流具备缓冲区,提高了性能;
什么是java序列化,如何实现java序列化?
处理对象流的一种机制,将对象进行流花,可以对流化后的对象进行读写操作,一般实现Serialize接口;
BufferedReader属于那种流,主要用来做什么?
用于处理流中的缓冲流,可以将读取的数据存在内存里面,有readLine方法,用来读取一行;
Get请求和Post请求的区别?
DNS使用的协议
TCP和UDP都有使用,UDP报文最大长度是512字节,而TCP则超过512字节,DNS查询超过512字节,尽量使用TCP;
区域传送使用TCP:可以保证数据准确性,还有传输数据量大的问题;
域名解析使用UDP协议:不会经过TCP三次握手,字节数少,此时DNS负载低,响应快;
幂等
一个幂等操作的特点是其任意多次执行所产生的影响均与第一次执行的影响相同,幂等函数或者幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数,这些函数不会影响系统状态;
http get请求可以认为是幂等;
http post请求不是幂等,put幂等,patch非幂等,delete是幂等;
http、UDP、TCP区别?
TCP三次握手?四次挥手?
cookies和session区别?
TCP粘包和拆包区别?解决策略?
正向代理、反向代理区别联系?应用?
一次完整http请求过程?
域名解析–>发起TCP的3次握手–>建立TCP连接后发起httpq请求–>服务器响应http请求,浏览器得到html代码–>浏览器解析html代码,并请求html代码中的资源–>浏览器对页面进行渲染呈现给用户;
TCP如何保证可靠传输?
tomcat使用哪种IO,为什么?
tomcat默认使用的是BIO,客户系统使用BIO的时候往往为每一个web请求引入多线程,每个web请求一个单独的线程,如果并发量上去,线程数就上去了,CPU忙着线程切换,所以BIO不适合高吞吐量,高可伸缩性的web服务器;
NIO是使用单线程(单个CPU)或者少量的多线程来接受socket,可以大大提高web服务器的可伸缩性;
有哪些服务器?区别和用途?
Tomcat组成?
定义:指在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法,对于任意一个对象,都能够调用他的任意一个方法,动态获取信息,以及动态调用对象的方法叫做java的反射机制;
应用: 生成动态代理,面向切面编程
常见实现方法
三种实现方式之间的区别:
获取所有公共构造函数:getConstructors()
获取所有的构造函数:getDeclaredConstructors()
操作方法:
使用Class获取对应方法:getMethods(),获取所有的公共方法,包括父类的公共方法;
getDeclaredMethods(),获取所有本类的方法,包括本类的私有方法;
getDeclareMethod(String name,Class<?>…parameterTypes)
获取指定方法名称的方法,和访问权限无关;
创建对象的方法:
Method执行方法
Class中获取字段:
JDK动态代理反射机制?
动态代理:使用Proxy类的newProxyInstance方法创建代理对象,使用InvocationHandler来实现增强的逻辑(通常是创建一个InvocationHandler接口的实现类,在其invock方法中实现增强的逻辑),利用拦截器加上反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理;
CGLib代理:使用MethodInterceptor接口实现增强逻辑,使用Enhancer生成代理对象,利用ASM开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理;
如果目标对象实现了接口,默认情况下会采用jdk的动态代理实现AOP;
如果目标对象实现了接口,可以强制使用CGLIB实现AOP
如果目标对象没有实现接口,必须使用CGLIB库;并在Spring配置文件中加入<aop:aspectj-autoproxy proxy-target-class = “true”/>
java序列化的作用和方法?
将对象状态转化为字节流,以后可以通过这些值再生出相同状态的对象,用于存储和传输,可以实现数据的持久化,可以把数据永久保存到硬盘上,序列化可以实现远程通信,转成在网络上传输的字节序列;
**方法:**java.io.ObjectOutputStream对象输出流,他的writeObject(Object obj)可以对参数指定的obj对象进行序列化;
ObjectInputStream对象输入流,他的readObject()方法可以从输入流读取字节序列,再把它们反序列化成为一个对象;
实现Serializable或者Externalizable接口,可以实现序列化;
serialVersionUID :它决定着是否能够反序列化成功,相当于一个版本号,在反序列化的时候会进行序列号对比;
谷歌的一些工具也可以实现序列化;
继承中方法执行顺序:
父类静态方法>子类静态方法>父类普通方法>父类构造方法>子类普通方法>子类构造方法
1.20 基本数据类型
1.21 进制转换
1.22 计算机原理
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-07gnCuuc-1618197750710)(C:\Users\lenovo\AppData\Roaming\Typora\typora-user-images\image-20210308094500874.png)]
判断对象是活着还是死亡算法?
垃圾收集算法
垃圾收集器?
内存分配与回收策略?
虚拟机类加载机制?
虚拟机类加载器?
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。