死锁:多个线程运行时,彼此通信或者竞争资源,一直请求对方占有的资源而又不会放弃自己正在占有的资源,造成的无限等待的过程
① 相同点:
② 不同点:
Callable可以产生线程执行的结果,future可以获取这一结果
① 线程的状态:
② 基本操作
③ sleep()与wait()
sleep方法执行后进入阻塞状态,但是它不释放锁,执行完成后,线程进入就绪状态,由于还持有锁,所以在获取到时间片后就可以进入running状态
wait方法执行后,线程释放锁并进入等待池,当它被唤醒以后,会进入就绪状态,但是它需要获取到锁才能进入running状态
④yield()
使当前线程从运行状态变为就绪状态,不释放锁,只是放弃时间片,让其他线程有机会执行
⑤sleep()与yield()
⑥退出当前线程
⑦interrupt、interrupted、isinterrupted
⑧阻塞式方法
指程序会一直等待该方法完成,期间不做任何事
⑨如何唤醒一个阻塞的线程
wait()释放锁,notify()唤醒线程
⑩notify与notifyall
①垃圾回收机制
synchronized的作用:用来控制线程同步,多线程环境下,一个synchronized代码块不能被多个线程同时执行
使用synchronized的三个地方:
①修饰实例方法:
作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
②修饰静态方法:
作用于当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份)。所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。
③修饰代码块:
指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
synchronized实现原理
synchronized是通过对象的监视器完成线程同步(线程互斥)的,每个对象的对象头中都有一个监视器(修饰静态代码块时,锁对象是类,类在jvm中有自己的Class对象),每一个线程进入同步代码块之前都会尝试获取锁对象的监视器,如果获取成功,则可以进入执行,否则就会被阻塞,直到获取锁的线程释放锁,才可以去竞争锁,竞争成功才可以进入同步代码块或同步方法。
锁升级
锁升级是为了减低了锁带来的性能消耗。在 Java 6 之后优化 synchronized 的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减低了锁带来的性能消耗,具体实现如下:
synchronized、volatile、CAS区别
①synchronized是悲观锁,属于抢占式,会引起其他线程阻塞
②volatile提供了多线程共享变量可见性和禁止指令重排序优化
③CAS 是基于冲突检测的乐观锁(非阻塞)
synchronized与lock的区别
①synchronized是Java内置关键字,Lock是个Java类;
②synchronized 可以给类、方法、代码块加锁;而 lock 只能给代码块加锁。
③synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁;而 lock 需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁。
④通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
synchronized与ReentrantLock的区别
①synchronized是关键字,ReentrantLock是类
②他们都是可重入锁
③ReentrantLock只能修饰代码块
当一个线程访问volatile修饰的变量时,不能从自己的工作内存中读取,只能去主内存读取,而当一个线程修改volatile修饰的变量时,需要将修改结果理解同步回主内存,以此来完成线程对共享变量修改时,对其他线程的可见性,但是由于这一系列操作不是原子性的,比如在同步回主内存的过程中,有其他线程读取了主内存变量未修改的值,那么就会造成数据不一致,所以volatile不能保证原子性
AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器
AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
ReentrantLock重入锁,是实现Lock接口的一个类,表示能够对共享资源能够重复加锁,即当前线程获取该锁再次获取不会被阻塞。
在java关键字synchronized隐式支持重入性,synchronized通过获取自增,释放自减的方式实现重入。与此同时,ReentrantLock还支持公平锁和非公平锁两种方式。
要想支持重入性,就要解决两个问题:
ReentrantLock支持两种锁:公平锁和非公平锁。何谓公平性,是针对获取锁而言的,如果一个锁是公平的,那么锁的获取顺序就应该符合请求上的绝对时间顺序,满足FIFO
ConcurrentHashMap是Java中的一个线程安全且高效的HashMap实现。平时涉及高并发如果要用map结构,那第一时间想到的就是它。相对于hashmap来说,ConcurrentHashMap就是线程安全的map。
可以简单地理解为通过 synchronized 来实现同步的容器,如果有多个线程调用同步容器的方法,它们将会串行执行。
并发容器使用了与同步容器完全不同的加锁策略来提供更高的并发性和伸缩性,例如在 ConcurrentHashMap 中采用了一种粒度更细的加锁机制,可以称为分段锁,在这种锁机制下,允许任意数量的读线程并发地访问 map,并且执行读操作的线程和写操作的线程也可以并发的访问 map,同时允许一定数量的写操作线程并发地修改 map,所以它可以在并发环境下实现更高的吞吐量
是当多个迭代器同时遍历和修改这个列表时,不会抛出 ConcurrentModificationException。在CopyOnWriteArrayList 中,写入将导致创建整个底层数组的副本,而源数组将保留在原地,使得复制的数组在被修改时,读取操作可以安全地执行
ThreadLocal 是一个本地线程副本变量工具类,当一个线程调用ThreadLocal对象的方法时,就会在当前线程内部创建了一个 ThreadLocalMap 对象,这个ThreadLocalMap对象使用当前的ThreadLocal作为key,而要存储的值为value,存储到这个ThreadLocalMap中,取的时候,也是通过ThreadLocal取值,简单说 ThreadLocal 就是一种以空间换时间的做法,每个线程可以访问自己内部 ThreadLocalMap 对象内的 value。通过这种方式,避免资源在多线程间共享
线程局部变量是局限于线程内部的变量,属于线程自身所有,不在多个线程间共享。当一个线程调用ThreadLocal对象的方法时,就会在当前线程内部创建了一个 ThreadLocalMap 对象,这个ThreadLocalMap对象使用当前的ThreadLocal作为key,而要存储的值为value,存储到这个ThreadLocalMap中,取的时候,也是通过ThreadLocal取值。
public class ThreadLocalExample {
private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
threadLocal.set("Hello, ThreadLocal!");
// 此时会调用主线程的ThreadLocalMap,但是这个对象为空,所以第一次的时候会创建一个ThreadLocalMap对象
// 然后将这个ThreadLocal作为key,"Hello, ThreadLocal!"作为值,存储到这个ThreadLocalMap中
Thread thread1 = new Thread(() -> {
System.out.println(threadLocal.get()); // 在子线程中获取ThreadLocal的值
});
thread1.start();
System.out.println(threadLocal.get()); // 在主线程中获取ThreadLocal的值
// 使用ThreadLocal的hash值取主线程的ThreadLocalMap中查找对应值
}
}
// 由于ThreadLocal是线程独立的,所以子线程并不能从主线程的ThreadLocal中获取值
// 最终的输出结果为:
// null
// Hello, ThreadLocal!
使用ThreadLocal会有内存泄漏的风险,原因在于,线程中的ThreadLocalMap会持有ThreadLocal的强引用,如果在ThreadLocal使用完后,没有及时通过remove方法,清楚ThreadLocalMap中ThreadLocal对应的entry,那么会导致ThreadLocal无法被垃圾回收器回收
阻塞队列(BlockingQueue)是一个支持两个附加操作的队列
这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用
ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。
LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列。
PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。
DelayQueue:一个使用优先级队列实现的无界阻塞队列。
SynchronousQueue:一个不存储元素的阻塞队列。
LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。
线程池顾名思义就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程不用自行创建,使用完毕不需要销毁线程而是放回池中,从而减少创建和销毁线程对象的开销
以最常用的fixedThreadPool为例,简单展示如何创建并使用线程池
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class CustomThreadPoolExample {
public static void main(String[] args) {
// 创建一个有界队列,容量为10
ArrayBlockingQueue<Runnable> taskQueue = new ArrayBlockingQueue<>(10);
// 创建一个ThreadPoolExecutor,设置核心线程数为2,最大线程数为4,线程存活时间为60秒,使用有界队列和自定义拒绝策略
ThreadPoolExecutor customThreadPool = new ThreadPoolExecutor(
2, // 核心线程数
4, // 最大线程数
60, // 非核心线程的存活时间,如果非核心线程空闲时间超过这个值,就会被回收
TimeUnit.SECONDS, // 时间单位
taskQueue, // 任务队列
ThreadPoolExecutor.AbortPolicy // 拒绝策略
);
// 提交任务给线程池执行
for (int i = 1; i <= 10; i++) {
final int taskId = i;
customThreadPool.execute(() -> {
System.out.println("Task " + taskId + " is running on thread: " + Thread.currentThread().getName());
});
}
// 关闭线程池
customThreadPool.shutdown();
}
}
这里使用Executors也是可以创建固定线程数线程池的(Executors.newFixedThreadPool(…))
但是这里默认使用的是无界队列,在任务提交的速度大于线程处理速度时,有可能会无限往队列中添加任务,造成内存泄漏,而且这个方法内部其实也是通过new ThreadPoolExecutor来创建线程池的,所以推荐直接使用ThreadPoolExecutor来创建线程池,自己定义详细参数
ThreadPoolExecutor的拒绝策略
如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时,ThreadPoolTaskExecutor 定义一些策略来处理新任务:
(1)ThreadPoolExecutor.AbortPolicy:抛出 RejectedExecutionException来拒绝新任务的处理
(2)ThreadPoolExecutor.DiscardPolicy:不处理新任务,直接丢弃掉
(3)ThreadPoolExecutor.DiscardOldestPolicy: 此策略将丢弃最早的未处理的任务请求。
与Executors的区别:
《阿里巴巴Java开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,规避资源耗尽的风险
Executors 各个方法的弊端:
(1)newFixedThreadPool 和 newSingleThreadExecutor:
主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至 OOM
(2)newCachedThreadPool 和 newScheduledThreadPool:
主要问题是线程数最大数是 Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至 OOM
而ThreaPoolExecutor创建线程池方式只有一种,就是走它的构造函数,参数自己指定
ThreadPoolExecutor 3 个最重要的参数:
(1)corePoolSize :核心线程数,线程数定义了最小可以同时运行的线程数量。
(2)maximumPoolSize :线程池中允许存在的工作线程的最大数量
(3)workQueue:当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,任务就会被存放在队列中
因篇幅问题不能全部显示,请点此查看更多更全内容