2 并发编程 面试

第1章 并发编程的三大挑战

  1. 线程的上下切换
  2. 死锁
  3. 资源限制

解决方法:

  1. 解决上下文切换
  1. 无锁并发编程
  2. Cas
  3. 使用最少的线程
  4. 协程
  1. 避免死锁
  1. 避免一个线程同时获取多把锁
  2. 避免一个线程在锁内同时占用多个资源
  3. 尝试使用定时锁

死锁的例子:

public void deadLock() {

       new Thread(new Runnable() {

          

           @Override

           public void run() {

              // TODO Auto-generated method stub

              synchronized(A) {

                  try {

                     Thread.sleep(1000);

                  } catch (InterruptedException e) {

                     // TODO Auto-generated catch block

                     e.printStackTrace();

                  }

                  synchronized (B) {

                    

                  }

                 

              }

           }

       }).start();

        new Thread(new Runnable() {

          

           @Override

           public void run() {

              // TODO Auto-generated method stub

              synchronized(B) {

                  try {

                     Thread.sleep(1000);

                  } catch (InterruptedException e) {

                     // TODO Auto-generated catch block

                     e.printStackTrace();

                  }

                  synchronized (A) {

                    

                  }

                 

              }

           }

       }).start();

    }

第2章 Java并发机制的底层实现原理

1 volatile关键字

Volatile保证了可见性和有序性

可见性:一个线程对共享变量的修改,其他的线程是可见的

Lock的前缀命令会引发两件事:

  1. 将当前处理器缓存的数据写回到内存
  2. 这个写操作会使得其他CPU里缓存了该内存地址的数据无效(缓存一致性协议)

有序性:编译重排序和处理器重排序,对不存在依赖关系的指令进行重排。

屏障类型

指令示例

说明

LoadLoad Barriers

Load1;LoadLoad;Load2

该屏障确保Load1数据的装载先于Load2及其后所有装载指令的的操作

StoreStore Barriers

Store1;StoreStore;Store2

该屏障确保Store1立刻刷新数据到内存(使其对其他处理器可见)的操作先于Store2及其后所有存储指令的操作

LoadStore Barriers

Load1;LoadStore;Store2

确保Load1的数据装载先于Store2及其后所有的存储指令刷新数据到内存的操作

StoreLoad Barriers

Store1;StoreLoad;Load2

该屏障确保Store1立刻刷新数据到内存的操作先于Load2及其后所有装载装载指令的操作。它会使该屏障之前的所有内存访问指令(存储指令和访问指令)完成之后,才执行该屏障之后的内存访问指令

  1. volatile读之后,所有变量读写操作都不会重排序到其前面。
  2. volatile读之前,所有volatile读写操作都已完成。
  3. volatile写之后,volatile变量读写操作都不会重排序到其前面。
  4. volatile写之前,所有变量的读写操作都已完成。
  1. 根据JMM规则,结合内存屏障的相关分析:
  2. 在每一个volatile写操作前面插入一个StoreStore屏障。这确保了在进行volatile写之前前面的所有普通的写操作都已经刷新到了内存。
  3. 在每一个volatile写操作后面插入一个StoreLoad屏障。这样可以避免volatile写操作与后面可能存在的volatile读写操作发生重排序。
  4. 在每一个volatile读操作后面插入一个LoadLoad屏障。这样可以避免volatile读操作和后面普通的读操作进行重排序。
  5. 在每一个volatile读操作后面插入一个LoadStore屏障。这样可以避免volatile读操作和后面普通的写操作进行重排序。

可见性: 没法保证

volatile static int count;

     public static void addCount(){

           count++;

     }

     public static void main(String[] args) {

       ExecutorService fixedPool = Executors.newFixedThreadPool(10);

       for (int i = 0; i < 3; i++) {

           fixedPool.execute(new Runnable() {

             

              @Override

              public void run() {

                  while(count<100) {

                     addCount();

                      System.out.println(Thread.currentThread().getName()+":"+count);

                      try {

                         Thread.sleep(10);

                     } catch (InterruptedException e) {

                         // TODO Auto-generated catch block

                         e.printStackTrace();

                     }

                  }

                 

              }

           });

       }

       fixedPool.shutdown();

    }

2 sychronized关键字

java中每一个对象都可以作为锁,这是synchronized实现同步的基础:

普通同步方法(实例方法),锁是当前实例对象 ,进入同步代码前要获得当前实例的锁

静态同步方法,锁是当前类的class对象 ,进入同步代码前要获得当前类对象的锁

同步方法块,锁是括号里面的对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

3 偏向所锁,轻量级锁及重量级锁

偏向所锁,轻量级锁都是乐观锁,重量级锁是悲观锁。
一个对象刚开始实例化的时候,没有任何线程来访问它的时候。它是可偏向的,意味着,它现在认为只可能有一个线程来访问它,所以当第一个
线程来访问它的时候,它会偏向这个线程,此时,对象持有偏向锁。偏向第一个线程,这个线程在修改对象头成为偏向锁的时候使用CAS操作,并将
对象头中的ThreadID改成自己的ID,之后再次访问这个对象时,只需要对比ID,不需要再使用CAS在进行操作。
一旦有第二个线程访问这个对象,因为偏向锁不会主动释放,所以第二个线程可以看到对象时偏向状态,这时表明在这个对象上已经存在竞争了,检查原来持有该对象锁的线程是否依然存活,如果挂了,则可以将对象变为无锁状态,然后重新偏向新的线程,如果原来的线程依然存活,则马上执行那个线程的操作栈,检查该对象的使用情况,如果仍然需要持有偏向锁,则偏向锁升级为轻量级锁,(偏向锁就是这个时候升级为轻量级锁的)。如果不存在使用了,则可以将对象回复成无锁状态,然后重新偏向。
轻量级锁认为竞争存在,但是竞争的程度很轻,一般两个线程对于同一个锁的操作都会错开,或者说稍微等待一下(自旋),另一个线程就会释放锁。 但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁,重量级锁使除了拥有锁的线程以外的线程都阻塞,防止CPU空转。

4 原子操作实现原理

CAS简介:

CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

1.  ABA问题。因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。

2. 循环时间长开销大。自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,

3. 只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。

第3章 Java内存模型

1 Java线程内存模型

每个线程都有自己的工作内存,里面存放共享变量副本。

2 volatile保证可见性和有序性的底层原理

内存屏障。

3 happen-before原则

1. 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
2. 两个操作之间存在happens-before关系,并不意味着一定要按照happens-before原则制定的顺序来执行。如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法。

下面是happens-before原则规则:

  1. 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;
  2. 锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作;
  3. volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;
  4. 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;
  5. 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作;
  6. 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
  7. 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;
  8. 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始;

4 顺序一致性

它是一个被计算机科学家理想化了的理论参考模型(划重点)。如果程序是正确同步的,那么程序的执行顺序将一致性,就是程序的执行结果和程序在一致性内存模型中执行的结果是相同的。

JMM 和数据一致性模型的差别:

1)顺序一致性模型会保证单线程内的操作都是按程序顺序来执行的。但是 JMM 不保证单线程内的操作都是按程序顺序来执行的。但是保证在单线程中的结果是正确的。为了性能嘛,可以理解。
2)顺序一致性模型会保证所有的线程看到的执行顺序是一致的。但是 JMM 不保证。(保证不了啊)
3)JMM 不保证对64位的long和double变量的写操作的原子性操作。JSR-133后读是原子性的。但是顺序一致性模型会保证。

  1. 锁的内存语义
  1. 程序次序规则:单个线程,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;
  2. 监视器规则: 一个锁的unlock()的操作,先于另一个锁的lock()操作
  3. 传递性规则:   A happen before B  B happen before C A happen before C

6 双重检查实现单例模式

public static Instance getInstance () {

       //第一次检查

       if(instance==null) {

           synchronized (doubleCheckLock.class) {

              //第二次检查

              if(instance==null) {

                  instance=new Instance();

              }

             

           }

       }

       return instance;

    }

第4章 Java并发编程基础

1 线程的状态切换

2 终止线程

中断结合bool变量来终止线程

public static class runner implements Runnable{

        private AtomicInteger ai=new AtomicInteger(0);

       private volatile boolean on=true;

        @Override

       public void run() {

           // TODO Auto-generated method stub

           while(on && !Thread.currentThread().isInterrupted()) {

              System.out.println(ai.incrementAndGet());

           }

          

          

       }

      

    }

3 线程间的通信方式

  1. Sychronized和volatile
  2. wait()和notify()
  3. 管道
  4. ThreadLocal()

Wait和notify经典的生产者和消费者问题:

public class Mall {

   

    private int count=0;

    private int MAX_COUNT=10;

    public synchronized void put() throws InterruptedException {

       if(count>=MAX_COUNT) {

           wait();

       }

        count++;

       System.out.println(Thread.currentThread().getName()+" "+count);

       notifyAll();

       Thread.sleep(100);

    }

    public synchronized void take() throws InterruptedException {

       if(count<=0) {

           wait();

       }

       count--;

       System.out.println(Thread.currentThread().getName()+" "+count);

       notifyAll();

       Thread.sleep(100);

      

    }

}

第5章 Java中的锁

1 AQS

关键是CAS修改volatile变量修饰的state: 修改成功的线程获取锁,如果修改不成功或者发现state状态时已经加锁的状态,则通过waiter对象封装线程,添加到等待队列中,挂起等待唤醒。

2 可重入锁

State初始为0,A线程Lock()会调用tryAcquire()使得state+1。此后,其他线程tyrAcquire()就会失败。直到 A线程unLock()把state置为0,其他线程才有机会获取锁。A可以多次重复的获取锁,state累加。

第6章 Java并发容器和框架

1 ConcurrentHashMap

保证线程安全:

  1. CAS初始化
  2. Put和get加锁
  3. 多线程同时参与扩容

Put的过程:

如果,没有初始化,先调用resize()进行初始化。

如果,已经初始化了,则hash(key)%size获取其位置,

如果,数组元素为空,则直接put.

如果,数组元素不为空,则插入到链表中

如果,链表的长度超过8,则把链表转为红黑树

Get的过程:

第7章 Java中的并发工具类

1 CountDownLatch

一个线程等待一个或者多个线程执行完成

2 CycleBarrier

一个线程等待一个或者多个线程执行完成

两者的区别

  1. CountDownLatch的下一步的动作实施者是主线程,具有不可重复性;而CyclicBarrier的下一步动作实施者还是“其他线程”
  2. CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。这个屏障之所以用循环修饰,是因为在所有的线程释放彼此之后,这个屏障是可以重新使用的(reset()方法重置屏障点)。

3 Semaphore

控制并发线程数量

4 Exchanger

线程间的通信

发布了146 篇原创文章 · 获赞 91 · 访问量 13万+

猜你喜欢

转载自blog.csdn.net/A1342772/article/details/101945905