一道从单例模式展开的面试题
涉及考点:
- synchronized 上锁
- volatile 关键字
- final、static关键字
单例模式,我只记懒汉式(饿汉式直接线程安全,还说个啥)
参考
懒汉式
public final class Singleton{
private static Singleton instance = null;
private Singleton(){
}
public static Singleton getInstance(){
if (instance == null){
instance = new Singleton();
}
return instance;
}
}
问题来了,A、B线程都开始创建,结果都进入if null 判断,就会导致创建两个instance,咋办?
- 上锁
代码
public final class Singleton{
private static Singleton instance = null;
private Singleton(){
}
public static synchronized Singleton getInstance(){
if (instance == null){
instance = new Singleton();
}
return instance;
}
}
静态方法加锁,相当于对类加锁 参考
参考摘要:
1、类锁 == 全局锁 区别于实例锁:是某一个实例的锁(互不影响)
2、对象中只有一把锁。对象中所有加synchronized的方法,分为static、non-static,那么static用的都是全局锁,无论创建多少实例,调用该方法都会受到制约;non-static用的实例锁,只有同一个实例调用方法才会收到限制(相同non-static方法、不同non-static方法都一样)
3、于是,就会有将方法进行scope限制,认为圈定需要同步范围。通过synchronized(非this对象) 将具体代码块加锁,这样不同线程调用同一个实例对象的不同方法就能实现并行。
- 等价写法
代码
public final class Singleton{
private static Singleton instance = null;
private Singleton(){
}
public static Singleton getInstance(){
synchronized(Singleton.class){
if (instance == null){
instance = new Singleton();
}
}
return instance;
}
}
then,每个线程访问这个方法都请求锁,overhead不大嘛?
先加个if null判断,有了直接返回,里面再加这个加锁判断
- 双重校验 + 锁
代码
public final class Singleton{
private static Singleton instance = null;
private Singleton(){
}
public static Singleton getInstance(){
if (instance == null){
synchronized(Singleton.class){
if (instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
这下没问题了吧?getInstance 方法是没问题了?
instance 这是reference 存在问题。。。
咋办? — volatile关键字
- 最终完美代码
public final class Singleton{
// 多了一个 volatile 关键词
private static volatile Singleton instance = null;
private Singleton(){
}
public static Singleton getInstance(){
if (instance == null){
synchronized(Singleton.class){
if (instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
扯了这么多,那就说说synchronized、volatile关键字?
- synchronized 与 volatile 参考链接
线程安全存在两个方面:1、执行控制 2、内存可见
Synchronized:可以通过锁,实现圈定代码块无法并发执行。加锁时,清空工作内存中共享变量的值,重新从主存拿,释放锁之前,将共享变量值刷进主内存。(原子性 — 中间不会被插入,可见性:上锁解锁前后的操作,保证代码块中的共享变量都是主内存的)
volatile:只能修饰变量,告诉JVM当前这个变量不稳定,一切以主内存为准(工作内存 vs 主内存),没办法保证原子性(i++ 无法连贯完成)
总结:volatile告诉JVM这个变量不稳定,访问需要去主存拿
synchronized 则通过加锁完成原子性、可见性,但是锁的代价很大
- final 与 static:
代码中的 final问题:final可以加在 类、方法、变量
final MyClass: 这个类不可以被继承(String类)
final myMethod(): 这个方法不可以被overwrite
final myVariable:这个变量不可以在被更改(常用的 public static final double PI)而static 则往往表示 全局只有一份,所有的实例都要基于这个进行取值(但可能被其他变量修改,所有可以作为共享变量来用)
我们天天说锁,锁到底是什么?参考
涉及到知识点:
- volatile 可见性
- CAS 机制保证原子性操作
- 线程通信
锁保证竞争条件下,只能有一个线程去处理业务逻辑。
1、怎么表示锁被占用?被谁占用?
volatile修饰变量Thread owner,变量不为null,表示占用
2、如何保证锁的争夺是原子性的?
CAS机制 — 用一个预期的值和内存值进行比较,如果两个值相等,就用预期的值替换内存值,并返回 true。否则,返回 false。
private volatile AtomicReference owner = new AtomicReference();
// CAS机制拿当前线程跟主内存中的值对比,owner是否为null,如果是就将其值设置为当前线程
owner.compareAndSet(null, Thread.currentThread());
3、抢不到锁的线程如何阻塞?阻塞后改如何唤醒呢?
线程通信,park/unpark
4、抢不到锁的线程该怎么保存?
private volatile LinkedBlockingQueue<thread> waiters = new LinkedBlockingQueue<>();</thread>
释放锁:
public void unlock() {
// CAS机制拿当前线程跟主内存中的值对比,是否是同一个线程,如果是就将其值设置为null
if (owner.compareAndSet(Thread.currentThread(), null)) {
// 将所有阻塞等待队列的线程都唤醒,让他们去抢锁。(非公平)
if (CollectionUtils.isNotEmpty(waiters)) {
waiters.stream().forEach(waiter -> {
LockSupport.unpark(waiter);
});
}
}
}
【线程AB交替打印AB】
参考
通过synchronized上锁,配合boolean变量完成交替打印
点击查看代码
public class ThreadWhileDemo {
private static boolean startA = true;
private static boolean startB = false;
public static void main(String[] args) {
final Object singal = new Object();
new Thread(() -> {
// 上锁
synchronized(singal){
while (true) {
if (startA) {
System.out.print("A");
startA = false;
startB = true;
singal.notify();
}else{
try {
singal.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}).start();
new Thread(() -> {
synchronized(singal){
while (true) {
if (startB) {
System.out.print("B");
startA = true;
startB = false;
singal.notify();
}else{
try {
singal.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}).start();
}
}
ReentrantLock 可以实现手动加锁,同时配合Condition可以随心所欲地控制多个线程的执行顺序。
点击查看代码
public class ReenterLockDemo {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
Condition conditionA = lock.newCondition();
Condition conditionB = lock.newCondition();
new Thread(() -> {
lock.lock();
for (int i = 0; i < 10; i++) {
System.out.print("A");
conditionB.signal();
try {
conditionA.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 这里有个坑,要记得在循环之后调用signal(),否则线程可能会一直处于await状态
conditionB.signal();
lock.unlock();
}).start();
new Thread(() -> {
lock.lock();
for (int i = 0; i < 10; i++) {
System.out.print("B");
conditionA.signal();
try {
conditionB.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
conditionA.signal();
lock.unlock();
}).start();
}
}
【实现一个死锁】
主要思路:有两个锁,线程AB各把持一个
点击查看代码
public class DeadLockDemo {
public static void main(String[] args) {
// 上两把锁
final Object o1 = new Object();
final Object o2 = new Object();
// 通过lambda表达式,完成Runable implement
new Thread(() -> {
synchronized(o1){
System.out.println("我上锁o1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized(o2){
System.out.println("我应该到不了这,谁知道呢");
}
}
}).start();
// 继承方式
new Thread(){
public void run() {
synchronized(o2){
System.out.println("I lock o2");
synchronized(o1){
System.out.println("Unreachable site...");
}
}
};
}.start();
}
}
- 怎么避免死锁? 一般都是想办法避免【循环等待】这条件,可以通过银行家算法来预分配资源,看看满足这个线程的话,其结束运行释放的资源能不能够其他线程用
【并发3种实现方式】
参考
1、extends Thread() 覆写run
2、implement Runnable() 实现run() -> new Thread(Runnable)
3、implement Callable() 实现call() -> new FutureTask(new Callable) -> new Thread(new Future)
点击查看代码
public class ThreeThread {
public static void main(String[] args) throws InterruptedException, ExecutionException {
// 1
new exThread().start();
// 2
new Thread(
new imThread()
).start();
new Thread(() -> {
System.out.println("匿名函数实现Runnable");
}).start();
// 3
FutureTask f = new FutureTask(
new caThread()
);
new Thread( f ).start();
System.out.println(f.get());
}
}
// 继承
class exThread extends Thread{
@Override
public void run() {
System.out.println("继承实现线程");
}
}
// 实现接口
class imThread implements Runnable{
@Override
public void run() {
System.out.println("implement Runable实现线程");
}
}
// 实现 Callable 接口
class caThread implements Callable{
@Override
public String call() throws Exception {
String ret = "Callable 可以完成返回值";
return ret;
}
}
- 比较
1、继承 和 Runnable
优先使用Runnable,Java只能单继承;
另外Runnable还可以轻松实现资源共享,创建两个Runnable对象,丢到两个Thread,运行的是同一套代码。
2、Callable 和 Runnable
参考
1、最大的区别:Callable有返回值
2、具体实现:C实现的call()、R实现run()
call()可以抛出异常,run()不可以
3、运行Call可以拿到Future对象,通过它观察任务执行情况。
4、加入线程池,Runnable使用ExecutorService的execute方法,Callable使用submit方法。
Original: https://www.cnblogs.com/spongie/p/16472705.html
Author: spongie
Title: 面试相关 — Java锁【单例、volatile关键字、锁实现、交替打印、死锁、3种实现方式
原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/579114/
转载文章受原作者版权保护。转载请注明原作者出处!