面试相关 — Java锁【单例、volatile关键字、锁实现、交替打印、死锁、3种实现方式

一道从单例模式展开的面试题

涉及考点:

  1. synchronized 上锁
  2. volatile 关键字
  3. 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 存在问题。。。

面试相关 -- Java锁【单例、volatile关键字、锁实现、交替打印、死锁、3种实现方式

咋办? — 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关键字?

线程安全存在两个方面: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 则往往表示 全局只有一份,所有的实例都要基于这个进行取值(但可能被其他变量修改,所有可以作为共享变量来用)

我们天天说锁,锁到底是什么?参考
涉及到知识点:

  1. volatile 可见性
  2. CAS 机制保证原子性操作
  3. 线程通信

锁保证竞争条件下,只能有一个线程去处理业务逻辑。
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&#x673A;&#x5236;&#x62FF;&#x5F53;&#x524D;&#x7EBF;&#x7A0B;&#x8DDF;&#x4E3B;&#x5185;&#x5B58;&#x4E2D;&#x7684;&#x503C;&#x5BF9;&#x6BD4;&#xFF0C;&#x662F;&#x5426;&#x662F;&#x540C;&#x4E00;&#x4E2A;&#x7EBF;&#x7A0B;&#xFF0C;&#x5982;&#x679C;&#x662F;&#x5C31;&#x5C06;&#x5176;&#x503C;&#x8BBE;&#x7F6E;&#x4E3A;null
    if (owner.compareAndSet(Thread.currentThread(), null)) {
        // &#x5C06;&#x6240;&#x6709;&#x963B;&#x585E;&#x7B49;&#x5F85;&#x961F;&#x5217;&#x7684;&#x7EBF;&#x7A0B;&#x90FD;&#x5524;&#x9192;&#xFF0C;&#x8BA9;&#x4ED6;&#x4EEC;&#x53BB;&#x62A2;&#x9501;&#x3002;&#xFF08;&#x975E;&#x516C;&#x5E73;&#xFF09;
        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/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球