1. ReentrantLock的tryLock方法和lock方法的区别

先说结论:
tryLock会去尝试加锁,如果成功就成功了,然后返回true,失败了什么都不做,然后返回false;

lock也会去尝试加锁,如果成功就成功了,失败了就会将当前线程入队,等待别的线程释放锁以后,被唤醒。lock方法没有返回值。

分析源码:
先看tryLock:

  1. 调用了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; }

  1. // 加锁失败,返回false
  2. 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的区别

  1. 使用方式不同,synchronized是关键字,隐式加锁解锁;ReentrantLock是一个类,程序员显示的加锁解锁。
  2. Synchronized是JVM层面的锁,例如对象头之类的,ReentrantLock是API层面的锁;
  3. Synchronized是非公平锁,ReentrantLock可以选择是公平还是非公平。
  4. Synchronized锁的是对象,ReentrantLock是根据实例对象state来标识锁的状态。
  5. Synchronize底层有锁升级的过程

6. ThreadLocal的底层原理

每个Thread对象里有个ThreadLocals的map,可以想象成key就是ThreadLocal的变量名,值就是设置的值:
image.png
每个线程都有自己独有的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是端口号

image.png

9. Volatile和Synchronized

  1. Volatile只是保证变量的可见性,通常适用于一个线程写,多个线程读的场景,因为它不能加锁,无法解决多个线程写的问题。
  2. Volatile还能防止指令重排。提到单例模式,doubleCheck,即synchronized锁前和锁后都要判断,但是如果只是加了锁,还不够,还要通过volatile防止指令重排。
  3. Synchronized用来加锁,jdk1.6以后进行了优化,有锁升级的过程。
  4. 可以画个图给面试官讲解下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死锁如何避免

造成死锁有四个条件:

  1. 一个资源一次只能被一个线程使用;
  2. 一个线程在阻塞等待某个资源时,不释放自己已占有的资源;
  3. 一个线程已经获得的资源,未使用完前,不能强行剥夺;
  4. 若干线程形成头尾相接的循环等待资源关系。

其中123都是锁必须要满足的条件,无法打破,只能打破4.

因此:

  1. 注意加锁顺序,保证每个线程按同样的方式加锁
  2. 注意加锁时限,设置有效期
  3. 注意死锁检查

12. Volatile如何保证可见性和有序性

可见性:加了Volatile关键字的变量,对其进行修改时,直接将CPU高级缓存中的数据写回主内存,对它的读取也变成从主内存读取。

有序性:通过内存屏障防止指令重排。