文章目录
- 1. 说一说 synchronized 关键字
- 2. 怎么使用 synchronized 关键字
- 3. 说一下 synchronized 关键字的底层原理
- 4. JDK1.6 之后的 synchronized 关键字底层做了哪些优化
- 5. Synchronized 与 Lock 锁的区别
- 6. Synchronized 与 ReentrantLock 的区别
- 7. ThreadLocal 是什么
- 8. 说一下 ThreadLocal 原理
- 9. 为什么 ThreadLocalMap 底层需要数组呢?没有了链表怎么解决Hash冲突呢?
- 10. 线程中的对象存是存放在堆中还是栈中?
- 11. ThreadLocal 的实例以及其值是存放在栈上的吗?
- 12. ThreadLocal 的内存泄露问题
- 13. 想要共享线程的 ThreadLocal 数据怎么办?
- 14. 类实现 Runnable 接口和实现 Callable 接口的区别
- 15. 线程池执行 execute() 方法和 submit() 方法的区别是什么呢?
- 16. 如何创建线程池
- 17. 当线程池饱和时的四种拒绝策略是什么
- 18. 线程池执行任务的原理分析
- 19. 介绍一下 Atomic 原子类
- 20. JUC 包中的原子类是哪 4 类?
- 21. 讲讲 AtomicInteger 的使用
- 22. 介绍一下 AtomicInteger 类的原理
1. 说一说 synchronized 关键字
synchronized 解决在多线程环境下访问资源的同步性,被 synchronized 修饰的方法或代码块在某个时刻只能有一个线程去执行。在 jdk6 之前,synchronized 是一个重量级锁,效率低下;在 jdk6 之后,Java 官方在 JVM 层面对 synchronized 进行较大优化,也对锁的实现进行大量优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
2. 怎么使用 synchronized 关键字
- 修饰实例方法,就是给当前对象的实例加锁。
- 修饰静态方法,就是给当前类 class 对象加锁,锁的不再是实例成员,而是类成员,所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。
- 修饰代码块,给指定的对象加锁。
3. 说一下 synchronized 关键字的底层原理
-
修饰代码块时:
使用 javac 编译以下类:
public class SynchronizedDemo { public void method() { synchronized (this) { System.out.println("synchronized"); } } }
编译生成字节码文件后,通过
javap -c -s -v -l SynchronizedDemo.class
进行反编译,查看反编译后的方法:
可以发现第 3 行和第 13 行分别是 monitorenter 与 monitorexit ,得出结论:其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。
每个 Java 对象都含有一个 monitor 对象,同时也有一个计数器,当执行 monitorenter 指令时,获取当前对象,然后将锁计数器 +1。相应的在执行 monitorexit 指令后,将锁计数器置为 0,表明当前对象被释放。
-
修饰方法时:
将以下代码进行编译:
public class SynchronizedDemo { public synchronized void method() { System.out.println("synchronized"); } }
进行反编译后:
发现之前例子的 monitorenter 与 monitorexit 指令不在了,而是 ACC_SYNCHRONIZED 标识;该标识指明了该方法是一个同步方法,JVM 通过该标识来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
4. JDK1.6 之后的 synchronized 关键字底层做了哪些优化
JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。
锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。
5. Synchronized 与 Lock 锁的区别
- Synchronized 是一个关键字;Lock 是一个类
- Synchronized 无法获取锁状态;Lock 可以获知当前锁的状态与锁的拥有者;
- Synchronized 会自动释放锁;Lock 需要手动加锁与解锁
- Synchronized,某个线程获得锁,其余线程必须等待;使用 Lock,其余线程不一定会等待
- Synchronized 是可重入锁,不可以中断,非公平;Lock 也是可重入锁,可以获取锁状态、可以设置公平与非公平锁
- Synchronized 适合锁少量的同步语句块,Lock 适合锁大量的同步语句块
6. Synchronized 与 ReentrantLock 的区别
- 两者都是可重入锁,可重入锁例如:
//可重入锁指的是在一个线程中可以多次获取同一把锁
//主线程在调用method1时拿到test对象锁,在method1中调用method2,主线程不需重新获取test锁
public class SynchronizedDemo {
public static void main(String[] args) {
Test test = new Test();
test.method1();
}
}
class Test {
synchronized void method1() {
System.out.println("method1");
this.method2();
}
synchronized void method2() {
System.out.println("method2");
}
}
- synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API
|
synchronized 是依赖于 JVM 实现的,并没有直接暴露给我们。ReentrantLock 是一个类,是在 JDK 层面实现的,需要我们手动去 lock() 和 unlock() ,我们可以通过查看源代码,来看它是如何实现的。
- ReentrantLock 比 synchronized 功能更加强大
|
- ReentrantLock 可实现公平与非公平锁,通过构造器
ReentrantLock(boolean fair)
实现- 获知当前锁的状态与锁的拥有者
- 实现等待可中断,通过
lock.lockInterruptibly()
来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情- 实现指定唤醒某一个等待线程,通过
lock.newCondition()
获取一个 condition 对象后,可以实现线程的等待condition.await();
、唤醒指定线程condition1.signal();
、唤醒所有线程condition.signalAll();
而 synchronized 关键字就相当于整个 Lock 对象中只有一个 Condition 实例,所有的线程都注册在它一个身上。如果执行 notifyAll() 方法的话就会通知所有处于等待状态的线程,这样会造成很大的效率问题,而 Condition 实例的 signalAll() 方法 只会唤醒注册在该 Condition 实例中的所有等待线程。
7. ThreadLocal 是什么
通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。如果想实现每一个线程都有自己的专属本地变量该如何解决呢? JDK中提供的
ThreadLocal
类正是为了解决这样的问题。ThreadLocal
类主要解决的就是让每个线程绑定自己的值,可以将ThreadLocal
类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。
如果你创建了一个
ThreadLocal
变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是ThreadLocal
变量名的由来。他们可以使用get()
和set()
方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。
8. 说一下 ThreadLocal 原理
看 ThreadLocal 类的源码:
static class ThreadLocalMap{
...
}
其定义了一个静态内部类 ThreadLocalMap,我们可以把 ThreadLocalMap 理解为线程存储数据的容器。存取数据时调用的是 ThreadLocalMap 类的 get、set 方法。
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
而 ThreadLocalMap 中定义了以 ThreadLocal 作为 key,任意类型作为 value 的数据类型,且还定义了一个 Entry 数组:
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
...
//该数组初始大小为16
//`private static final int INITIAL_CAPACITY = 16;`
private Entry[] table;
}
现在数据结构就比较清晰了,一个 Thread 类可以有多个 ThreadLocal 类,一个 Thread 类拥有一个 ThreadLocalMap,键值对
Entry(ThreadLocal<?> k, Object v)
(多个 ThreadLocal)都存储在 ThreadLocalMap 的数组中,就像Map[{id:1},{name:"张三"},{age:20}]
。
ThreadLocal 为 map 结构是为了让每个线程可以关联多个 ThreadLocal 变量。这也就解释了 ThreadLocal 声明的变量为什么在每一个线程都有自己的专属本地变量。
9. 为什么 ThreadLocalMap 底层需要数组呢?没有了链表怎么解决Hash冲突呢?
我们先来看一下 set 方法的源码:
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
// 此处将ThreadLocal进行hash并与(len-1),得到的i就是数组的下标
int i = key.threadLocalHashCode & (len-1);
// 循环遍历Entry[]数组
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// 如果当前遍历到的ThreadLocal与传入的ThreadLocal一致,则覆盖value值
if (k == key) {
e.value = value;
return;
}
// 如果遍历到该位置上的key为null,也就是这个位置为空,那直接new一个Entry放在i位置上
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
当然在 get 方法时,也会计算 ThreadLocal 的哈希值算出下标为 i 的位置,然后判断该位置 Entry 对象中的 ThreadLocal 是否和该位置上的 key 一致,如果不一致,就判断下一个位置,源码:
private Entry getEntry(ThreadLocal<?> key) {
// 算出位置i
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
// 匹配到相同的ThreadLocal
return e;
else
// 没匹配到相同的ThreadLocal
return getEntryAfterMiss(key, i, e);
}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
// 遍历对比每一个ThreadLocal
while (e != null) {
ThreadLocal<?> k = e.get();
// 相等就直接返回,不相等就继续查找,找不到就返回null
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
10. 线程中的对象存是存放在堆中还是栈中?
在 Java 中,栈内存归属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可以理解成线程的私有内存,而堆内存中的对象对所有线程可见,堆内存中的对象可以被所有线程访问。
11. ThreadLocal 的实例以及其值是存放在栈上的吗?
不是,是存放在堆上的,因为 ThreadLocal 实例实际上也是被其创建的类持有,只是在存取数据时,会根据当前线程拿到属于该线程的 ThreadLocal 数据,其他线程不能访问,相当于做了隔离操作。
12. ThreadLocal 的内存泄露问题
之前我们看到 ThreadLocalMap 中的 Entry 类继承了弱引用,表示 ThreadLocalMap 中使用的 key 为弱引用,而 value 是强引用。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远无法被GC 回收,这个时候就可能会产生内存泄露。
ThreadLocalMap 实现中已经考虑了这种情况,在调用
set()
、get()
、remove()
方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal 方法后 最好手动调用remove()
方法
13. 想要共享线程的 ThreadLocal 数据怎么办?
使用 InheritableThreadLocal 类可以实现多个线程访问 ThreadLocal 的值,我们在主线程中创建一个 InheritableThreadLocal 的实例,然后在子线程中得到这个 InheritableThreadLocal 实例设置的值。
14. 类实现 Runnable 接口和实现 Callable 接口的区别
Runnable 自 Java 1.0 以来一直存在,Callable 在 Java 1.5 中引入,目的就是为了来处理 Runnable 不支持的用例。Runnable 的 run 方法无返回值不能抛出异常,但是 Callable 的 call 方法可以。所以,如果任务不需要返回结果或抛出异常推荐实现 Runnable 接口,这样代码看起来会更加简洁。
工具类 Executors 可以实现 Runnable 对象和 Callable 对象之间的相互转换。(
Executors.callable(Runnable task
)或Executors.callable(Runnable task,Object resule)
)
15. 线程池执行 execute() 方法和 submit() 方法的区别是什么呢?
- execute() 方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否。
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
else if (!addWorker(command, false))
reject(command);
}
- submit() 方法用于提交需要返回值的任务。线程池会返回一个 Future 类型的对象,通过这个 Future 对象可以判断任务是否执行成功,并且可以通过 Future 的
get()
方法来获取返回值,get()
方法会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit)
方法则会阻塞当前线程 timeout 时间后返回,这时候有可能任务没有执行完。
public <T> Future<T> submit(Callable<T> task) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task);
execute(ftask);
return ftask;
}
16. 如何创建线程池
使用 Executors 创建四种类型线程池
Executors.newSingleThreadExecutor(); //创建单个线程的线程池
Executors.newFixedThreadPool(4); //创建固定线程数的线程池
Executors.newCachedThreadPool(); //创建线程数可变的线程池
Executors.newScheduledThreadPool(4); //创建可定时处理任务的线程池
然而以上方式创建线程池并不安全,可能发生 OOM(内存溢出)的事故。
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
//在创建单个线程的池子中,new 了一个链表结构的阻塞队列
//队列可以理解为存任务的容器,当池子中线程在运行时,后续任务会进入该队列等待执行
new LinkedBlockingQueue<Runnable>()));
}
//而创建该阻塞队列时,队列长度为integer最大值,
//如果堆积的任务超过该阈值,则发生OOM(内存溢出)
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
FixedThreadPool 和 SingleThreadExecutor :允许请求的队列长度为 Integer.MAX_VALUE ,如果堆积的任务超过该阙值,会导致OOM。
CachedThreadPool 和 ScheduledThreadPool :允许创建的线程数量为 Integer.MAX_VALUE ,如果创建的线程数超过该阙值,会导致OOM。
所以在阿里编程规约中,不允许使用 Executors 创建线程池。
可使用 new ThreadPoolExecutor
方式创建,且更加灵活:
//这是安全创建池子的构造方法,七个参数都可自定义
public ThreadPoolExecutor(int corePoolSize, //池子中的核心线程数
int maximumPoolSize, //池子中最大线程数
long keepAliveTime, //线程在 keepAliveTime 时间内没有任务,自动销毁
TimeUnit unit, // keepAliveTime 的时间单位
BlockingQueue<Runnable> workQueue, //阻塞队列
ThreadFactory threadFactory, //线程工厂
RejectedExecutionHandler handler //拒绝策略,当池中线程都在工作,队列也满了,还有任务进来如何拒绝的策略
) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
17. 当线程池饱和时的四种拒绝策略是什么
new ThreadPoolExecutor.AbortPolicy() // 池子满了,队列满了,还有任务进来,不处理该任务,并抛异常
new ThreadPoolExecutor.CallerRunsPolicy() // 该任务是哪个线程开启的,就由哪个线程执行
new ThreadPoolExecutor.DiscardPolicy() // 池子满了,队列满了,丢掉任务,不抛异常
new ThreadPoolExecutor.DiscardOldestPolicy() //池子满了,队列满了,看看最早开始执行的线程有没有空闲,尝试竞争,不抛异常
18. 线程池执行任务的原理分析
来看以下代码与执行结果:
public class ThreadPoolExecutorDemo {
public static void main(String[] args) throws InterruptedException, ExecutionException {
ThreadPoolExecutor pool = new ThreadPoolExecutor(
5, 10, 2, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(10), Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
// 开启10个Task1线程
for (int i = 0; i < 10; i++) {
pool.execute(new Task1(i));
}
while (pool.getActiveCount() != 0) {
}
pool.shutdown();
}
}
class Task1 implements Runnable {
int i;
Task1(int i) {
this.i = i;
}
@Override
public void run() {
try {
System.out.println("start time: " + new SimpleDateFormat("hh : ss").format(new Date()) + " Task" + i);
TimeUnit.SECONDS.sleep(5);
System.out.println("end time: " + new SimpleDateFormat("hh : ss").format(new Date()) + " Task" + i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
打印结果:
start time: 10 : 19 Task1
start time: 10 : 19 Task2
start time: 10 : 19 Task0
start time: 10 : 19 Task4
start time: 10 : 19 Task3
end time: 10 : 24 Task2
end time: 10 : 24 Task0
end time: 10 : 24 Task1
end time: 10 : 24 Task4
start time: 10 : 24 Task5
start time: 10 : 24 Task6
start time: 10 : 24 Task7
end time: 10 : 24 Task3
start time: 10 : 24 Task8
start time: 10 : 24 Task9
end time: 10 : 29 Task6
end time: 10 : 29 Task5
end time: 10 : 29 Task7
end time: 10 : 29 Task8
end time: 10 : 29 Task9
我们发现线程池是先执行五个任务,当其中有任务执行完后,再去执行剩下五个任务。为了探究其为什么会这样,就要分析 execute 方法:
public void execute(Runnable command) {
// 当任务为null时,抛出空指针异常
if (command == null)
throw new NullPointerException();
int c = ctl.get();
// 判断当前线程池中正在工作的线程数量是否小于核心线程数
if (workerCountOf(c) < corePoolSize) {
// 如果小于,则开启一个线程执行任务
if (addWorker(command, true))
return;
c = ctl.get();
}
// 如果工作线程数大于或等于核心线程数,判断线程池是否是Running状态,尝试将任务放入队列中
// 线程池在Running状态时,能够接收新任务,以及对在堵塞队列中的任务进行处理
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
// 如果线程池状态不是Running状态,就从任务队列中移除该任务,并尝试判断线程是否全部执行完毕。同时执行拒绝策略
if (! isRunning(recheck) && remove(command))
reject(command);
// 如果当前线程池为空就创建一个线程并执行
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
// 通过addWorker新建一个线程,并执行该任务,如果执行失败则拒绝该任务
else if (!addWorker(command, false))
reject(command);
}
直观的执行流程:
结论:在我们开启了 10 个任务之后,5 个核心线程先执行 5 个任务,剩下 5 个放入队列中,当有任务完成后,才去执行队列中的任务。
19. 介绍一下 Atomic 原子类
Atomic 类例如 AtomicBoolean、AtomicInteger、AtomicLong,这些类具有数据操作的原子性,就像事务也具有原子性一样,操作时不可分割;使用场景在多线程且不使用锁的环境下,例如 20 个线程对 AtomicInteger 对象进行 +1 操作,一个线程加 100 次,这 20000 次操作一定都能成功执行,也不会出现数据不一致问题。
20. JUC 包中的原子类是哪 4 类?
基本类型
- AtomicInteger:整形原子类
- AtomicLong:长整型原子类
- AtomicBoolean:布尔型原子类
数组类型
- AtomicIntegerArray:整形数组原子类
- AtomicLongArray:长整形数组原子类
- AtomicReferenceArray:引用类型数组原子类
引用类型
- AtomicReference:引用类型原子类
- AtomicStampedReference:原子更新带有版本号的引用类型里的字段原子类
- AtomicMarkableReference :原子更新带有标记位的引用类型
对象的属性修改类型
- AtomicIntegerFieldUpdater:原子更新整形字段的更新器
- AtomicLongFieldUpdater:原子更新长整形字段的更新器
- AtomicStampedReference:原子更新带有版本号的引用类型(乐观锁)。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。
21. 讲讲 AtomicInteger 的使用
AtomicInteger 使用方法:
public final int get() //获取当前的值
public final int getAndSet(int newValue) //获取当前的值,并设置为 newValue
public final int getAndIncrement() //获取当前的值,并自增
public final int getAndDecrement() //获取当前的值,并自减
public final int getAndAdd(int delta) //获取当前的值,并将 delta 加到当前值上
boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则把update设置为实际值
public final void lazySet(int newValue) //最终设置为 newValue,可能导致其他线程在设置之后的一小段时间内还是可以读到旧的值。为实际值的一小段时间内还是可以读到旧的值。
22. 介绍一下 AtomicInteger 类的原理
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
...
}
AtomicInteger 类主要利用 CAS (比较和交换) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。
CAS 是(compare and swap)的缩写,也就是比较和交换。CAS 的原理是拿期望值和原本值作比较,如果相同则更新成新的值。UnSafe 类的 objectFieldOffset() 方法是一个 native 本地方法,这个方法是用来拿到“原来的值”的内存地址,返回值是 valueOffset。CAS 是一种基于锁的操作,而且是乐观锁,其通过不加锁的方式来处理共享变量。另外 value 是一个 volatile 变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值。