- 1. ReentrantLock的tryLock方法和lock方法的区别
- 2. ReentrantLock中公平锁和非公平锁的底层实现
- 3. sleep(), wait(), join(), yield()的区别
- 4. 锁升级过程
- 5. Synchronized和ReentrantLock的区别
- 6. ThreadLocal的底层原理
- 7. ThreadLocal的内存泄漏,如何避免
- 8. 如何查看线程死锁
- 9. Volatile和Synchronized
- 10. 有A、B、C三个线程,如何保证三个线程同时执行?如何在并发情况下保证三个线程依次执行?交错进行?
- 11.JAVA死锁如何避免
- 12. Volatile如何保证可见性和有序性
1. ReentrantLock的tryLock方法和lock方法的区别
先说结论:
tryLock会去尝试加锁,如果成功就成功了,然后返回true,失败了什么都不做,然后返回false;
lock也会去尝试加锁,如果成功就成功了,失败了就会将当前线程入队,等待别的线程释放锁以后,被唤醒。lock方法没有返回值。
分析源码:
先看tryLock:
- 调用了sync.nonfairTryAcquire(1);方法 ```java public boolean tryLock() { return sync.nonfairTryAcquire(1); }
final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); // 0表示没有被其他线程锁上,尝试CAS加锁并且返回True if (c == 0) { if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } // 当前线程等于上锁线程,那么就state++ else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) // overflow throw new Error(“Maximum lock count exceeded”); setState(nextc); return true; }
// 加锁失败,返回false
return false;
}
而lock方法除了调用了tryLock的方法以外,如果加锁失败还会去入队等待。
```java
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
public final void acquire(int arg) {
// 先尝试加锁,成功了的话,&&后面的不再运行
// 没成功,就入队。
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
2. ReentrantLock中公平锁和非公平锁的底层实现
唯一的区别就是在某个尝试获取锁的这个时间结点:
- 公平锁会去检查是否有线程在排队;
- 而非公平锁不会检查排队,也就是说它极有可能会插队,如果在锁释放的一瞬间,它尝试获取锁了,那它就会立刻拿到这把锁。
源码:
先看公平锁:
protected final boolean tryAcquire(int acquires) {
xxx;
// 检查是否有排队
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
xxx;
}
非公平锁
final boolean nonfairTryAcquire(int acquires) {
xxx;
// 没有检查是否有排队的过程
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
xxx;
}
3. sleep(), wait(), join(), yield()的区别
首先明确两个概念: 锁池和等待池。
竞争锁的线程会进入锁池等待,一旦锁被释放,锁池中的线程就会去竞争锁,得到锁后进入就绪状态,等待CPU的分配。
而调用wait的线程不会进入锁池,而是进入等待池。等待池中的线程不会去竞争锁,而是要等到其他线程将它唤醒以后进入锁池。
sleep:
- 抱着锁睡觉,不会释放锁,
- 是Thread的静态本地方法;
- 不依赖synchronized关键字;
- 自动退出阻塞;
- 从运行 -> 阻塞 -> 就绪 -> 运行
- 用于当前线程休眠,或者轮询暂停操作
wait:
- 释放锁,
- 是Object类的本地方法,
- 依赖synchronized关键字
- 需要别的线程唤醒:
- 从运行 -> 阻塞 (等待别人唤醒)-> 就绪 -> 运行
- 用于线程间的通信
yield:
- 不会释放锁,只是让出CPU的使用权;
- 即使让出了CPU的使用权,也有可能马上拿回来接着执行。
- 从运行 -> 就绪
join:
- A线程中调用了B.join(), 那么A必须等待B执行完成以后,A才能继续执行。
4. 锁升级过程
指的是synchronized的锁升级过程,偏向锁 -> 轻量级锁 -> 重量级锁。
偏向锁:
虽然用到了synchronized关键字,但是有可能从头到尾都只有一个线程在获得这把锁,因此只需要在锁的对象头里将该线程ID保存下来,那么每次这个线程进来的时候,只要和对象头里存的ID比对一下,匹配则继续执行。它偏向于某个线程,所以叫偏向锁。
轻量级锁:
当有两个线程来竞争锁的时候,偏向锁必须要升级成轻量级锁,此时也不是直接阻塞,而是采用自旋的方式来获取锁(while循环)。
重量级锁:
在轻量级锁阶段,如果自旋很多次依然取不到锁,就升级成重量级锁 。会直接调用操作系统的API阻塞线程,重量级锁依赖操作系统,所以效率不高。
5. Synchronized和ReentrantLock的区别
- 使用方式不同,synchronized是关键字,隐式加锁解锁;ReentrantLock是一个类,程序员显示的加锁解锁。
- Synchronized是JVM层面的锁,例如对象头之类的,ReentrantLock是API层面的锁;
- Synchronized是非公平锁,ReentrantLock可以选择是公平还是非公平。
- Synchronized锁的是对象,ReentrantLock是根据实例对象state来标识锁的状态。
- Synchronize底层有锁升级的过程
6. ThreadLocal的底层原理
每个Thread对象里有个ThreadLocals的map,可以想象成key就是ThreadLocal的变量名,值就是设置的值:
每个线程都有自己独有的threadLocals这个map,互不干扰。因此:
public class TestThreadLocal {
private static ThreadLocal<String> s = new ThreadLocal<>();
public static void main(String[] args) {
s.set("Main线程");
System.out.println(s.get());
System.out.println(Thread.currentThread());
new Thread(() -> {
s.set("不知道啥线程");
System.out.println(s.get());
}).start();
}
}
以上代码中,各管各的。
注意:
如果使用线程池,因为结束后不会销毁线程,所以ThreadLocal里的数据也不会被回收,久而久之,数据越来越多,导致内存泄漏问题。
解决方案是,使用结束后手动销毁。
7. ThreadLocal的内存泄漏,如何避免
弱引用:GC一次,回收一次,无论内存是否充足;
软引用:内存不充足的时候,GC回收;
ThreadLocal对象使用的是弱引用,因此一次GC后,它就变成null了,但是value仍然存在,那么如果value引用了其他对象,就迟迟不能被回收,导致内存泄漏。
那么为什么不让ThreadLocal直接使用强引用呢?
因为ThreadLocal自身引用一个对象,ThreadLocals里的map的key也引用这个对象,那么即便你让ThreadLocal = null,ThreadLocal对象也无法被回收。
解决办法:
使用结束后调用remove手动删除。
8. 如何查看线程死锁
通过jstack命令:
jstack -l 15963 // 15963是端口号
9. Volatile和Synchronized
- Volatile只是保证变量的可见性,通常适用于一个线程写,多个线程读的场景,因为它不能加锁,无法解决多个线程写的问题。
- Volatile还能防止指令重排。提到单例模式,doubleCheck,即synchronized锁前和锁后都要判断,但是如果只是加了锁,还不够,还要通过volatile防止指令重排。
- Synchronized用来加锁,jdk1.6以后进行了优化,有锁升级的过程。
- 可以画个图给面试官讲解下Volatile的原理,重新让其他线程去主内存里拿值。
10. 有A、B、C三个线程,如何保证三个线程同时执行?如何在并发情况下保证三个线程依次执行?交错进行?
考查三个并发工具:
CountdownLatch、CyclicBarrier、Semaphore
同时执行:采用CountdownLatch:
public class TestThreadSameTime {
public static void main(String[] args) {
CountDownLatch countDownLatch = new CountDownLatch(1);
for (int i = 0; i < 3; i++) {
new Thread(() -> {
try {
countDownLatch.await();
System.out.println(System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
countDownLatch.countDown();
}
}
依次执行:
用一个Volatile静态变量即可实现,
- 例如ticket == 1的时候,线程1才执行,执行完将ticket改成2;
- ticket == 2的时候,线程2执行,执行完将ticket改成3;
- ticket == 3的时候,线程3执行。
交错执行:
采用Semaphore,例如三个线程,就初始化3个信号量,首先占用第二个和第三信号量,然后让第一个获得信号量,执行完毕释放第二个的信号量,第二个线程获得信号,执行完毕释放第三个信号量:
import java.util.concurrent.Semaphore;
/**
* 三个线程依次执行
*/
public class TestThreadOneByOne {
public static Semaphore s1 = new Semaphore(1);
public static Semaphore s2 = new Semaphore(1);
public static Semaphore s3 = new Semaphore(1);
public static void main(String[] args) throws InterruptedException {
s2.acquire();
s3.acquire();
new Thread(() -> {
while (true) {
try {
s1.acquire();
System.out.println("A");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
s2.release();
}
}).start();
new Thread(() -> {
while (true) {
try {
s2.acquire();
System.out.println("B");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
s3.release();
}
}).start();
new Thread(() -> {
while (true) {
try {
s3.acquire();
System.out.println("C");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
s1.release();
}
}).start();
}
}
11.JAVA死锁如何避免
造成死锁有四个条件:
- 一个资源一次只能被一个线程使用;
- 一个线程在阻塞等待某个资源时,不释放自己已占有的资源;
- 一个线程已经获得的资源,未使用完前,不能强行剥夺;
- 若干线程形成头尾相接的循环等待资源关系。
其中123都是锁必须要满足的条件,无法打破,只能打破4.
因此:
- 注意加锁顺序,保证每个线程按同样的方式加锁
- 注意加锁时限,设置有效期
- 注意死锁检查
12. Volatile如何保证可见性和有序性
可见性:加了Volatile关键字的变量,对其进行修改时,直接将CPU高级缓存中的数据写回主内存,对它的读取也变成从主内存读取。
有序性:通过内存屏障防止指令重排。