多线程并发编程面试常考

对象在内存中的内存布局

sychronized锁住对象后该对象的锁状态升级过程:new - 无锁态 - 偏向锁 - 轻量级锁/自旋锁/无锁 (CAS)- 重量级锁 - GC标记信息


线程的几个状态
  • NEW(新建状态)
  • Runnable
    • Ready(就绪状态,线程被放在等待队列中,等着被CPU执行)
    • Running(运行状态,被扔到CPU中执行)
    • Blocked
    • Waiting
    • TimedWaiting
  • Terminated(终止态)
三种新建线程的方法
  • 实现Thread
  • 实现Runnable接口
  • 线程池
线程的常用方法:
  • sleep(),沉睡一段时间(当前线程回到就绪状态),这段时间CPU执行其它线程
  • yield(),和sleep()类似,让出CPU,当前线程回到就绪状态。使用很少见
  • join(),通知其它线程获得CPU执行,比如在t1线程内运行t2.join(),意思就是t1线程通知t2线程执行,自己回到就绪状态。
Synchronized讲解

synchronized实现过程:(不能禁止指令重排)
  • Java代码:synchronized
  • monitorentermoniterexit
  • 执行过程中自动升级(偏向锁、自旋锁、重量级锁)
  • 更底层的实现lock comxchg
volatile讲解:
  • 保证变量的各线程可见性/数据一致性 (多个线程要用到变量时,重新去内存拿)
  • 禁止CPU指令重排(在单线程没问题,多线程就会出现问题。为什么要指令重排,其实就是因为CPU太快了,而访问内存比访问缓存又慢了太多)
    • 举个例子:对象的初始化三个步骤Person p = new Person("zeng", 24);
      • 申请对象Person的内存,这个时候给实例变量设置了默认值,比如name = null; age = 0;
      • 调用该对象的构造函数进行真正的初始化实例变量name = "zeng"; age = 24;
      • 返回对象Personp
  • volatile不能实现synchronized的原子性操作
    • 比如定义一个变量volatile int count = 0;10个线程分别count++加1000次,最终的count不一定会是10000,因为这里的count++并不是一个原子性操作,它包含好几个指令,所以为了要实现整个的count++原子性操作,也就是必须要使用sychronizedcount++加锁。
再注意一些问题:
  • 在用synchronized锁住一个对象时,这个时候不能将这个引用去指向另一个对象
  • 不要用synchronized去锁一个String、Integer等基本数据类型的封装类的对象
CAS(无锁优化/自旋):
  • CompareAndSwap
  • Java里面java.util.concurrnet.atomic.AtomicXXX开头的类都是使用CAS自旋锁实现的。内部都是使用UnSafe这个类的compareAndSet等操作实现线程安全地修改值
    • 举个例子:AtomicInteger count = new AtomicInteger(0);在上面的volatile的讨论中,count++如果不加sychronized锁会导致非原子性操作,但这里直接使用AtomicInteger即可实现线程可见、原子性操作,将count++到10000。并且不需要volatile、synchronized
  • ABA问题(1变为2又被变为1),加版本号version
  • 所有的JavaCAS的操作基本上都是用的UnSafe这个类,这个UnSafe使Java语言有了像C++的直接操作JVM内存的能力。
ReentrantLock(可重入锁,公平锁(默认是非公平锁))本身底层也是CAS
  • 可以替代synchronized,替换方法:lock.lock();
  • 可以通过lock.interupt的方法将该锁设置为可以通过interup方法唤醒正在wait的线程
  • 相比上个特点,synchronized的线程,wait之后必须通过其它线程的notify()才能唤醒
  • 如果设置为公平锁,那么线程在抢一个资源时,会进入优先队列排队按先后顺序等待
  • synchronized是非公平锁
  • synchronized自动加锁解锁,ReentrantLock手动加锁解锁lock.lock()
  • 底层实现:ReentrantLock是CAS的实现,synchronized底层是有锁的升级过程(4种)
CountDownLatch锁(倒计时完了继续执行(门栓))
CyclicBarrier锁(当线程数目到达某个数目(栅栏值)时,继续执行后面的事物)
Phase锁(阶段锁,CyclicBarrier的升级版本,有多个阶段,比如结婚现场有7个人,先7人到达现场,再7人吃完饭,再xxxxx)
ReadWriteLock(共享锁、排他锁、多个线程可以一起执行)
Semaphore(信号量,用于限流(仅允许几个线程同时工作))
Exchanger(两个线程运行时交换值)
LockSupport(可以通过park()方法随时将线程停止,并通过unpark()方法随时让某线程就绪)
面试题1:定义两个线程,A线程往容器里放数据,B线程监测容器容量为5时,停止运行
  • 有3种方法
  • 使用wait()notify()方法的组合。这个很重要
  • 使用门栓锁CountDownLatch
  • 使用LockSupport直接park()unpark()
面试题2:顺序打印A1B2C3……
面试题3:生产者消费者问题
版本1 通过synchronized、wait()、notify()实现
package zr.thread;

import java.util.LinkedList;
import java.util.concurrent.TimeUnit;

/*
    生产者与消费者实现1

    写一个固定容量同步容器,拥有put和get方法, 以及getCount方法
    能够支持2个生产者线程以及10个消费者线程的阻塞调用

    使用wait()和notifyAll()来实现

    这个方法是有瑕疵的,因为使用notifyAll()会唤醒所有的其它等待队列的线程,包括生产者、消费者
    有没有办法只唤醒生产者,或者只唤醒消费者?

 */

/**
 * @author ZR
 * @Classname MyContainer1
 * @Description 生产者消费者最简单写法
 * @Date 2020/9/12 21:02
 */
public class MyContainer1<T> {
    final private LinkedList<T> lists = new LinkedList<>();
    // 最多10个元素
    final private int MAX = 10;
    private int count = 0;

    // 因为++count所以要加synchronized
    public synchronized void put(T t){
        // 想想为什么用while而不是if
        while(lists.size() == MAX){
            try{
                this.wait();
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }

        lists.add(t);
        ++count;
        // 通知所有消费者线程消费
        // 这个方法其实是有点小瑕疵的,因为notifyAll()会叫醒所有的其它wait()线程,也包括了另一个生产者
        this.notifyAll();
    }

    // 因为--count所以要加synchronized
    public synchronized T get(){
        T t = null;
        while(lists.size() == 0){
            try{
                this.wait();
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
        t = lists.removeFirst();
        --count;
        // 通知生产者进行生产
        // 这个方法其实是有点小瑕疵的,因为notifyAll()会叫醒所有的其它wait()线程,也包括了其它消费者
        this.notifyAll();
        return t;
    }

    public static void main(String[] args){
        MyContainer1<String> c = new MyContainer1<>();
        // 启动消费者线程
        for(int i = 0; i < 10; i++){
            new Thread(()->{
                for(int j = 0; j < 5; j++)
                    System.out.println(c.get());
            }, "customer" + i).start();
        }

        try {
            TimeUnit.SECONDS.sleep(2);

        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 启动生产者线程
        for(int i = 0; i < 2; i++){
            new Thread(()->{
                for(int j = 0; j < 25; j++)
                    c.put(Thread.currentThread().getName() + " " + j);
            }, "producer" + i).start();
        }
    }
}
版本2 通过ReentrantLock实现
package zr.thread;

import com.sun.org.glassfish.external.statistics.CountStatistic;

import java.util.LinkedList;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author ZR
 * @Classname MyContainer2
 * @Description TODO
 * @Date 2020/9/12 21:27
 */
public class MyContainer2<T> {

    final private LinkedList<T> lists = new LinkedList<>();
    // 最多10个元素
    final private int MAX = 10;
    private int count = 0;

    private Lock lock = new ReentrantLock();
    // Condition的本质就是等待队列,在这里生产者在生产者的队列,消费者在消费者的队列
    // 在Container1例中,等待队列只有一个,生产者和消费者都在里边儿
    private Condition producer = lock.newCondition();
    private Condition customer = lock.newCondition();

    public void put(T t){
        try {
            // 需要手动加锁
            lock.lock();
            while(lists.size() == MAX)
                producer.await();

            lists.add(t);
            ++count;
            // 通知消费者线程进行消费
            customer.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 手动解锁
            lock.unlock();
        }
    }

    public T get(){
        T t = null;
        try {
            lock.lock();
            while(lists.size() == 0)
                customer.await();

            t = lists.removeFirst();
            --count;
            // 通知生产者线程生产
            producer.signalAll();
        } catch (InterruptedException e){
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
        return t;
    }


    public static void main(String[] args){
        MyContainer2<String> c = new MyContainer2<>();
        // 启动消费者线程
        for(int i = 0; i < 10; i++){
            new Thread(()->{
                for(int j = 0; j < 5; j++)
                    System.out.println(c.get());
            }, "customer" + i).start();
        }

        try {
            TimeUnit.SECONDS.sleep(2);

        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 启动生产者线程
        for(int i = 0; i < 2; i++){
            new Thread(()->{
                for(int j = 0; j < 25; j++)
                    c.put(Thread.currentThread().getName() + " " + j);
            }, "producer" + i).start();
        }
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章