Java多线程面试大全

目录

多线程的几种实现方式?

什么是线程安全?

volatile的原理?作用?能代替锁吗?

画一个线程的生命周期状态图?

sleep和wait的区别?

sleep和sleep(0)的区别?

Lock与Synchronized的区别?

synchronized的原理是什么?一般用在什么地方(比如加在静态方法和非静态方法的区别,静态方法和非静态方法同时执行的时候会有影响吗?)

解释以下名词:重排序,自旋锁,偏向锁,轻量级锁,可重入锁,公平锁,非公平锁,乐观锁,悲观锁?

用过哪些原子类?他们的原理是什么?

用过线程池吗?如果用过,请说明原理?并说说newCache和newFixed有什么区别?构造函数的各个参数的含义是什么?比如coreSize,maxsize等?

线程池的关闭方式有几种?各自的区别是什么?

假如有一个第三方接口,有很多个线程去调用获取数据,现在规定每秒钟最多有10个线程同时调用它,如何做到?

spring的controller是单例还是多例?怎么保证并发的安全?

用三个线程按顺序循环打印abc三个字母,比如abcabcabc?

ThreadLocal用途是什么?原理是什么?用的时候要注意什么?

如果让你实现一个并发安全的链表,你会怎么做?

有哪些无锁数据结构?他们实现的原理是什么?

讲讲java同步机制的wait和notify?


多线程的几种实现方式?

Java多线程实现方式主要有四种:继承Thread类实现Runnable接口实现Callable接口通过FutureTask包装器来创建Thread线程使用ExecutorService、Callable、Future实现有返回结果的多线程。其中前两种方式线程执行完后都没有返回值,后两种是带返回值的。

1、继承Thread类创建线程

Thread类本质上是实现了Runnable接口的一个实例,代表一个线程的实例。启动线程的唯一方法就是通过Thread类的start()实例方法。start()方法是一个native方法,它将启动一个新线程,并执行run()方法。这种方式实现多线程很简单,通过自己的类直接extend Thread,并复写run()方法,就可以启动新线程并执行自己定义的run()方法。例如:

public class MyThread extends Thread {  
  public void run() {  
   System.out.println("MyThread.run()");  
  }  
}  
 
MyThread myThread1 = new MyThread();  
MyThread myThread2 = new MyThread();  
myThread1.start();  
myThread2.start(); 

2、实现Runnable接口创建线程
如果自己的类已经extends另一个类,就无法直接extends Thread,此时,可以实现一个Runnable接口,如下:

public class MyThread extends OtherClass implements Runnable {  
  public void run() {  
   System.out.println("MyThread.run()");  
  }  
}  

为了启动MyThread,需要首先实例化一个Thread,并传入自己的MyThread实例:

MyThread myThread = new MyThread();  
Thread thread = new Thread(myThread);  
thread.start();  

事实上,当传入一个Runnable target参数给Thread后,Thread的run()方法就会调用target.run(),参考JDK源代码:

public void run() {  
  if (target != null) {  
   target.run();  
  }  
}  

3、实现Callable接口通过FutureTask包装器来创建Thread线程

Callable接口(也只有一个方法)定义如下:   

public interface Callable<V> { 
  V call() throws Exception;
} 

public class SomeCallable<V> extends OtherClass implements Callable<V> {
    @Override
    public V call() throws Exception {
        // TODO Auto-generated method stub
        return null;
    }

}

Callable<V> oneCallable = new SomeCallable<V>();   
//由Callable<Integer>创建一个FutureTask<Integer>对象:   
FutureTask<V> oneTask = new FutureTask<V>(oneCallable);   
//注释:FutureTask<Integer>是一个包装器,它通过接受Callable<Integer>来创建,它同时实现了Future和Runnable接口。 
  //由FutureTask<Integer>创建一个Thread对象:   
Thread oneThread = new Thread(oneTask);   
oneThread.start();   
//至此,一个线程就创建完成了。

4、使用ExecutorService、Callable、Future实现有返回结果的线程

ExecutorService、Callable、Future三个接口实际上都是属于Executor框架。返回结果的线程是在JDK1.5中引入的新特征,有了这种特征就不需要再为了得到返回值而大费周折了。而且自己实现了也可能漏洞百出。

1.可返回值的任务必须实现Callable接口。类似的,无返回值的任务必须实现Runnable接口。

2.执行Callable任务后,可以获取一个Future的对象,在该对象上调用get就可以获取到Callable任务返回的Object了。

注意:get方法是阻塞的,即:线程无返回结果,get方法会一直等待。

再结合线程池接口ExecutorService就可以实现传说中有返回结果的多线程了。

下面提供了一个完整的有返回结果的多线程测试例子,在JDK1.5下验证过没问题可以直接使用。代码如下:

import java.util.concurrent.*;  
import java.util.Date;  
import java.util.List;  
import java.util.ArrayList;  
  
/** 
* 有返回值的线程 
*/  
@SuppressWarnings("unchecked")  
public class Test {  
public static void main(String[] args) throws ExecutionException,  
    InterruptedException {  
   System.out.println("----程序开始运行----");  
   Date date1 = new Date();  
  
   int taskSize = 5;  
   // 创建一个线程池  
   ExecutorService pool = Executors.newFixedThreadPool(taskSize);  
   // 创建多个有返回值的任务  
   List<Future> list = new ArrayList<Future>();  
   for (int i = 0; i < taskSize; i++) {  
    Callable c = new MyCallable(i + " ");  
    // 执行任务并获取Future对象  
    Future f = pool.submit(c);  
    // System.out.println(">>>" + f.get().toString());  
    list.add(f);  
   }  
   // 关闭线程池  
   pool.shutdown();  
  
   // 获取所有并发任务的运行结果  
   for (Future f : list) {  
    // 从Future对象上获取任务的返回值,并输出到控制台  
    System.out.println(">>>" + f.get().toString());  
   }  
  
   Date date2 = new Date();  
   System.out.println("----程序结束运行----,程序运行时间【"  
     + (date2.getTime() - date1.getTime()) + "毫秒】");  
}  
}  
  
class MyCallable implements Callable<Object> {  
private String taskNum;  
  
MyCallable(String taskNum) {  
   this.taskNum = taskNum;  
}  
  
public Object call() throws Exception {  
   System.out.println(">>>" + taskNum + "任务启动");  
   Date dateTmp1 = new Date();  
   Thread.sleep(1000);  
   Date dateTmp2 = new Date();  
   long time = dateTmp2.getTime() - dateTmp1.getTime();  
   System.out.println(">>>" + taskNum + "任务终止");  
   return taskNum + "任务返回运行结果,当前任务时间【" + time + "毫秒】";  
}  
}

代码说明:
上述代码中Executors类,提供了一系列工厂方法用于创建线程池,返回的线程池都实现了ExecutorService接口。
public static ExecutorService newFixedThreadPool(int nThreads) 
创建固定数目线程的线程池。
public static ExecutorService newCachedThreadPool() 
创建一个可缓存的线程池,调用execute 将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。
public static ExecutorService newSingleThreadExecutor() 
创建一个单线程化的Executor。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) 
创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。
ExecutoreService提供了submit()方法,传递一个Callable,或Runnable,返回Future。如果Executor后台线程池还没有完成Callable的计算,这调用返回Future对象的get()方法,会阻塞直到计算完成。

什么是线程安全?

既然是线程安全问题,那么毫无疑问所有的隐患都是出现在多个线程访问的情况下产生的,也就是我们要确保在多条线程访问的时候,我们的程序还能按照我们预期的行为去执行,我们看一下下面的代码。

很简单的一段代码,我们就来统计一下这个方法的访问次数,多个线程同时访问会不会出现什么问题,我开启的3条线程每个线程循环10次,得到以下结果:

我们可以看到,这里出现了两个26,为什么会出现这种情况,出现这种情况显然表明我们这个方法根本就不是线程安全的,出现这种问题的原因有很多,我们说最常见的一种,就是我们A线程在进入方法后,拿到了count的值,刚把这个值读取出来还没有改变count的值的时候,结果线程B也进来的,那么导致线程A和线程B拿到的count值是一样的。

那么由此我们可以了解这确实不是一个线程安全的类,因为他们都需要操作这个共享的变量,其实要对线程安全问题给出一个明确的定义还是蛮复杂的,我们根据我们这个程序来总结下什么是线程安全。

当多个线程访问某个方法时,不管你通过怎样的调用方式或者说这些线程如何交替的执行,我们在主程序中不需要去做任何的同步,这个类的结果行为都是我们设想的正确行为,那么我们就可以说这个类时线程安全的。

volatile的原理?作用?能代替锁吗?

java编程语言允许线程之间共享变量,为了保证共享变量能够被准确和一致的更新,那么线程应该确保通过排他锁单独获得这个变量。

原理:

处理器为了提高处理速度,不直接和内存进行通讯,而是先将系统内存的数据读到内部缓存(L1,L2或其他)后再进行操作,但操作完之后不知道何时会写到内存,如果对声明了Volatile变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题,所以在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作的时候,会强制重新从系统内存里把数据读到处理器缓存里。

总而言之:

1.Lock前缀指令会引起处理器缓存回写到内存

指令会锁定这块内存区域的缓存并回写到内存,并使用缓存一致性机制来确保修改的原子性,此操作被称为“缓存锁定”,缓存一致性机制会阻止同时修改被两个以上处理器缓存的内存区域数据。

2.一个处理器的缓存回写到内存会导致其他处理器的缓存无效

处理器使用嗅探技术保证它的内部缓存,系统内存和其他处理器的缓存的数据在总线上保持一致。如果通过嗅探一个处理器来检测其他处理器打算写内存地址,而这个地址当前处理共享状态,那么正在嗅探的处理器将无效它的缓存行,在下次访问相同内存地址时,强制执行缓存行填充。

作用:

1.保证共享变量的可见性。

2.防止重排序。

volatile和锁的区别:

当多个线程同时请求锁的时候,一些线程将被挂起并且等待其他线程执行完它们的时间片后才能被调度执行。频繁的线程间上下文切换及线程调度是十分耗资源的。另外锁还存在着死锁的风险。与锁相比,volatile是一种更加轻量级的同步机制,因为在使用这些变量的时候不会发生上下文切换和线程调度等操作。但是volatile同样也存在局限性:当变量依赖于其他变量或旧值时(自增)就不能使用volatile变量,因为他们不是原子操作。

画一个线程的生命周期状态图?

sleep和wait的区别?

我们都知道sleep是让线程休眠,到时间后会继续执行,wait是等待,需要唤醒再继续执行,那么这两种方法在多线程中的表现形态,它们各有什么区别呢?

可以总结为以下几点:

使用上

从使用角度看,sleep是Thread线程类的方法,而wait是Object顶级类的方法。

sleep可以在任何地方使用,而wait只能在同步方法或者同步块中使用。

CPU及资源锁释放

sleep,wait调用后都会暂停当前线程并让出cpu的执行时间,但不同的是sleep不会释放当前持有的对象的锁资源,到时间后会继续执行,而wait会放弃所有锁并需要notify/notifyAll后重新获取到对象锁资源后才能继续执行。

异常捕获

sleep需要捕获或者抛出异常,而wait/notify/notifyAll不需要。

sleep和sleep(0)的区别?

当 timeout = 0, 即 Sleep(0),如果线程调度器的可运行队列中有大于或等于当前线程优先级的就绪线程存在,操作系统会将当前线程从处理器上移除,调度其他优先级高的就绪线程运行;如果可运行队列中的没有就绪线程或所有就绪线程的优先级均低于当前线程优先级,那么当前线程会继续执行,就像没有调用 Sleep(0)一样。

当 timeout > 0 时,如:Sleep(1),会引发线程上下文切换:调用线程会从线程调度器的可运行队列中被移除一段时间,这个时间段约等于 timeout 所指定的时间长度。为什么说约等于呢?是因为睡眠时间单位为毫秒,这与系统的时间精度有关。通常情况下,系统的时间精度为 10 ms,那么指定任意少于 10 ms但大于 0 ms 的睡眠时间,均会向上求值为 10 ms。

Lock与Synchronized的区别?

synchronized是java中的一个关键字,也就是说是Java语言内置的特性。那么为什么会出现Lock呢?

我们了解到如果一个代码块被synchronized修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁只会有两种情况:

1)获取锁的线程执行完了该代码块,然后线程释放对锁的占有;

2)线程执行发生异常,此时JVM会让线程自动释放锁。

那么如果这个获取锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,其他线程便只能干巴巴地等待,试想一下,这多么影响程序执行效率。因此就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断),通过Lock就可以办到。

再举个例子:当有多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作会发生冲突现象,但是读操作和读操作不会发生冲突现象。

但是采用synchronized关键字来实现同步的话,就会导致一个问题:如果多个线程都只是进行读操作,所以当一个线程在进行读操作时,其他线程只能等待无法进行读操作。

因此就需要一种机制来使得多个线程都只是进行读操作时,线程之间不会发生冲突,通过Lock就可以办到。

另外,通过Lock可以知道线程有没有成功获取到锁。这个是synchronized无法办到的。

总结一下,也就是说Lock提供了比synchronized更多的功能。但是要注意以下几点:

1)Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。Lock是一个类,通过这个类可以实现同步访问;

2)Lock和synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。

以下是Lock接口的源码:

public interface Lock {

    /**
     * Acquires the lock.
     */
    void lock();

    /**
     * Acquires the lock unless the current thread is
     * {@linkplain Thread#interrupt interrupted}.
     */
    void lockInterruptibly() throws InterruptedException;

    /**
     * Acquires the lock only if it is free at the time of invocation.
     */
    boolean tryLock();

    /**
     * Acquires the lock if it is free within the given waiting time and the
     * current thread has not been {@linkplain Thread#interrupt interrupted}.
     */
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

    /**
     * Releases the lock.
     */
    void unlock();

}

从Lock接口中我们可以看到主要有这几个方法,这些方法的功能从注释中可以看出:

lock():获取锁,如果锁被占用则一直等待。

unlock():释放锁。

tryLock():注意返回类型是boolean,如果获取锁的时候锁被占用就返回false,否则返回true。

tryLock(long time, TimeUnit unit):比起tryLock()就是给了一个时间期限,保证等待参数时间。

lockInterruptibly():用该锁的获得方式,如果线程在获取锁的阶段进入了等待,那么可以中断此线程,先去做别的事。

通过以上的解释,大致可以解释在上个部分中“锁类型(lockInterruptibly())”,“锁状态(tryLock())”等问题,还有就是前面锁获取的过程我所写的“大致就是可以尝试获得锁,线程可以不会一直等待”用了“可以”的原因。

下面是Lock一般使用的例子,注意ReentrantLock是Lock接口的实现。

lock():

package com.brickworkers;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LockTest {
    private Lock lock = new ReentrantLock();

    //需要参与同步的方法
    private void method(Thread thread){
        lock.lock();
        try {
            System.out.println("线程名"+thread.getName() + "获得了锁");
        }catch(Exception e){
            e.printStackTrace();
        } finally {
            System.out.println("线程名"+thread.getName() + "释放了锁");
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        LockTest lockTest = new LockTest();

        //线程1
        Thread t1 = new Thread(new Runnable() {

            @Override
            public void run() {
                lockTest.method(Thread.currentThread());
            }
        }, "t1");

        Thread t2 = new Thread(new Runnable() {

            @Override
            public void run() {
                lockTest.method(Thread.currentThread());
            }
        }, "t2");

        t1.start();
        t2.start();
    }
}
//执行情况:线程名t1获得了锁
//         线程名t1释放了锁
//         线程名t2获得了锁
//         线程名t2释放了锁

tryLock():

package com.brickworkers;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LockTest {
    private Lock lock = new ReentrantLock();

    //需要参与同步的方法
    private void method(Thread thread){
/*      lock.lock();
        try {
            System.out.println("线程名"+thread.getName() + "获得了锁");
        }catch(Exception e){
            e.printStackTrace();
        } finally {
            System.out.println("线程名"+thread.getName() + "释放了锁");
            lock.unlock();
        }*/


        if(lock.tryLock()){
            try {
                System.out.println("线程名"+thread.getName() + "获得了锁");
            }catch(Exception e){
                e.printStackTrace();
            } finally {
                System.out.println("线程名"+thread.getName() + "释放了锁");
                lock.unlock();
            }
        }else{
            System.out.println("我是"+Thread.currentThread().getName()+"有人占着锁,我就不要啦");
        }
    }

    public static void main(String[] args) {
        LockTest lockTest = new LockTest();

        //线程1
        Thread t1 = new Thread(new Runnable() {

            @Override
            public void run() {
                lockTest.method(Thread.currentThread());
            }
        }, "t1");

        Thread t2 = new Thread(new Runnable() {

            @Override
            public void run() {
                lockTest.method(Thread.currentThread());
            }
        }, "t2");

        t1.start();
        t2.start();
    }
}

//执行结果: 线程名t2获得了锁
//我是t1有人占着锁,我就不要啦
//线程名t2释放了锁

看到这里相信大家也都会使用如何使用Lock了吧,关于tryLock(long time, TimeUnit unit)和lockInterruptibly()不再赘述。前者主要存在一个等待时间,在测试代码中写入一个等待时间,后者主要是等待中断,会抛出一个中断异常,常用度不高,喜欢探究可以自己深入研究。

synchronized的原理是什么?一般用在什么地方(比如加在静态方法和非静态方法的区别,静态方法和非静态方法同时执行的时候会有影响吗?)

Java 虚拟机中的同步(Synchronization)基于进入和退出Monitor对象实现, 无论是显式同步(有明确的 monitorenter 和 monitorexit 指令,即同步代码块)还是隐式同步都是如此。在 Java 语言中,同步用的最多的地方可能是被 synchronized 修饰的同步方法。同步方法 并不是由 monitorenter 和 monitorexit 指令来实现同步的,而是由方法调用指令读取运行时常量池中方法表结构的 ACC_SYNCHRONIZED 标志来隐式实现的。

同步代码块:monitorenter指令插入到同步代码块的开始位置,monitorexit指令插入到同步代码块的结束位置,JVM需要保证每一个monitorenter都有一个monitorexit与之相对应。任何对象都有一个monitor与之相关联,当且一个monitor被持有之后,他将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor所有权,即尝试获取对象的锁;

在JVM中,对象在内存中的布局分为三块区域:对象头、实例变量和填充数据。如下:

实例变量:存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。

填充数据:由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐,这点了解即可。

对象头:Hotspot虚拟机的对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)。其中Klass Point是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,Mark Word用于存储对象自身的运行时数据,它是实现轻量级锁和偏向锁的关键。

Mark Word:用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等。Java对象头一般占有两个机器码(在32位虚拟机中,1个机器码等于4字节,也就是32bit),但是如果对象是数组类型,则需要三个机器码,因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小,但是无法从数组的元数据来确认数组的大小,所以用一块来记录数组长度。

Monior:我们可以把它理解为一个同步工具,也可以描述为一种同步机制,它通常被描述为一个对象。与一切皆对象一样,所有的Java对象是天生的Monitor,每一个Java对象都有成为Monitor的潜质,因为在Java的设计中 ,每一个Java对象自打娘胎里出来就带了一把看不见的锁,它叫做内部锁或者Monitor锁。Monitor是线程私有的数据结构,每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个monitor关联(对象头的MarkWord中的LockWord指向monitor的起始地址),同时monitor中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。其结构如下:

Owner:初始时为NULL表示当前没有任何线程拥有该monitor record,当线程成功拥有该锁后保存线程唯一标识,当锁被释放时又设置为NULL。
EntryQ:关联一个系统互斥锁(semaphore),阻塞所有试图锁住monitor record失败的线程。
RcThis:表示blocked或waiting在该monitor record上的所有线程的个数。
Nest:用来实现重入锁的计数。
HashCode:保存从对象头拷贝过来的HashCode值(可能还包含GC age)。
Candidate:用来避免不必要的阻塞或等待线程唤醒,因为每一次只有一个线程能够成功拥有锁,如果每次前一个释放锁的线程唤醒所有正在阻塞或等待的线程,会引起不必要的上下文切换(从阻塞到就绪然后因为竞争锁失败又被阻塞)从而导致性能严重下降。Candidate只有两种可能的值0表示没有需要唤醒的线程1表示要唤醒一个继任线程来竞争锁。

1、普通同步方法,锁是当前实例对象:

public class SynchronizedTest {
      public synchronized void method1(){
          System.out.println("Method 1 start");
          try {
              System.out.println("Method 1 execute");
              Thread.sleep(3000);
          } catch (InterruptedException e) {
             e.printStackTrace();
         }
         System.out.println("Method 1 end");
     }

     public synchronized void method2(){
         System.out.println("Method 2 start");
         try {
             System.out.println("Method 2 execute");
             Thread.sleep(1000);
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
         System.out.println("Method 2 end");
     }

     public static void main(String[] args) {
         final SynchronizedTest test = new SynchronizedTest();

         new Thread(new Runnable() {
             @Override
             public void run() {
                 test.method1();
             }
         }).start();

         new Thread(new Runnable() {
             @Override
             public void run() {
                 test.method2();
             }
         }).start();
     }
 }

2、静态同步方法,锁是当前类的class对象:

public class SynchronizedTest {
      public static synchronized void method1(){
          System.out.println("Method 1 start");
          try {
              System.out.println("Method 1 execute");
              Thread.sleep(3000);
          } catch (InterruptedException e) {
              e.printStackTrace();
          }
          System.out.println("Method 1 end");
      }
 
      public static synchronized void method2(){
          System.out.println("Method 2 start");
          try {
              System.out.println("Method 2 execute");
              Thread.sleep(1000);
          } catch (InterruptedException e) {
              e.printStackTrace();
          }
          System.out.println("Method 2 end");
      }
 
      public static void main(String[] args) {
          final SynchronizedTest test = new SynchronizedTest();
          final SynchronizedTest test2 = new SynchronizedTest();
 
          new Thread(new Runnable() {
              @Override
              public void run() {
                  test.method1();
              }
          }).start();
 
          new Thread(new Runnable() {
              @Override
              public void run() {
                  test2.method2();
              }
          }).start();
      }
  }

3、同步方法块,锁是括号里面的对象:

public class SynchronizedTest {
     public void method1(){
         System.out.println("Method 1 start");
         try {
             synchronized (this) {
                 System.out.println("Method 1 execute");
                 Thread.sleep(3000);
             }
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
         System.out.println("Method 1 end");
     }

     public void method2(){
         System.out.println("Method 2 start");
         try {
             synchronized (this) {
                 System.out.println("Method 2 execute");
                 Thread.sleep(1000);
             }
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
         System.out.println("Method 2 end");
     }

     public static void main(String[] args) {
         final SynchronizedTest test = new SynchronizedTest();

         new Thread(new Runnable() {
             @Override
             public void run() {
                 test.method1();
             }
         }).start();

         new Thread(new Runnable() {
             @Override
             public void run() {
                 test.method2();
             }
         }).start();
     }
 }

解释以下名词:重排序,自旋锁,偏向锁,轻量级锁,可重入锁,公平锁,非公平锁,乐观锁,悲观锁?

重排序:

重排序通常是编译器或运行时环境为了优化程序性能而采取的对指令进行重新排序执行的一种手段。重排序分为两类:编译器重排序和运行期重排序。

自旋锁:

是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。

偏向锁:

在实际应用运行过程中发现,“锁总是同一个线程持有,很少发生竞争”,也就是说锁总是被第一个占用他的线程拥有,这个线程就是锁的偏向线程。

那么只需要在锁第一次被拥有的时候,记录下偏向线程ID。这样偏向线程就一直持有着锁,直到竞争发生才释放锁。以后每次同步,检查锁的偏向线程ID与当前线程ID是否一致,如果一致直接进入同步,退出同步也一样,无需每次加锁解锁都去CAS更新对象头,如果不一致意味着发生了竞争,锁已经不是总是偏向于同一个线程了,这时候需要锁膨胀为轻量级锁,才能保证线程间公平竞争锁。

轻量级锁:

轻量锁与偏向锁不同的是:

1.轻量级锁每次退出同步块都需要释放锁,而偏向锁是在竞争发生时才释放锁。

2.每次进入退出同步块都需要CAS更新对象头。

3.争夺轻量级锁失败时,自旋尝试抢占锁。

可以看到轻量锁适合在竞争情况下使用,其自旋锁可以保证响应速度快,但自旋操作会占用CPU,所以一些计算时间长的操作不适合使用轻量级锁。

可重入锁:

广义上的可重入锁指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁(前提得是同一个对象或者class),这样的锁就叫做可重入锁。ReentrantLock和synchronized都是可重入锁。

公平锁、非公平锁:

简单的来说,如果一个线程组里,能保证每个线程都能拿到锁,那么这个锁就是公平锁。相反,如果保证不了每个线程都能拿到锁,也就是存在有线程饿死,那么这个锁就是非公平锁。

那如何能保证每个线程都能拿到锁呢,队列FIFO是一个完美的解决方案,也就是先进先出,java的ReenTrantLock也就是用队列实现的公平锁和非公平锁。

在公平的锁中,如果有另一个线程持有锁或者有其他线程在等待队列中等待这个锁,那么新发出的请求的线程将被放入到队列中。而非公平锁上,只有当锁被某个线程持有时,新发出请求的线程才会被放入队列中(此时和公平锁是一样的)。所以,它们的差别在于非公平锁会有更多的机会去抢占锁。

非公平锁性能高于公平锁性能。首先,在恢复一个被挂起的线程与该线程真正运行之间存在着严重的延迟。而且,非公平锁能更充分的利用cpu的时间片,尽量的减少cpu空闲的状态时间。

乐观锁、悲观锁:

乐观锁总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

悲观锁总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronizedReentrantLock等独占锁就是悲观锁思想的实现。

用过哪些原子类?他们的原理是什么?

原子类AtomicInteger:

/**
     * Atomically increments by one the current value.
     *
     * @return the updated value
     */
    public final int incrementAndGet() {
        for (;;) {
            int current = get();
            int next = current + 1;
            if (compareAndSet(current, next))
                return next;
        }
    }

我们来分析下incrementAndGet的逻辑:

1.先获取当前的value值;

2.对value加一;

3.第三步是关键步骤,调用compareAndSet方法来来进行原子更新操作,这个方法的语义是:先检查当前value是否等于current,如果相等,则意味着value没被其他线程修改过,更新并返回true。如果不相等,compareAndSet则会返回false,然后循环继续尝试更新。

compareAndSet调用了Unsafe类的compareAndSwapInt方法:

/**
     * Atomically sets the value to the given updated value
     * if the current value {@code ==} the expected value.
     *
     * @param expect the expected value
     * @param update the new value
     * @return true if successful. False return indicates that
     * the actual value was not equal to the expected value.
     */
    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

Unsafe的compareAndSwapInt是个native方法,也就是平台相关的。它是基于CPU的CAS指令来完成的。

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

用过线程池吗?如果用过,请说明原理?并说说newCache和newFixed有什么区别?构造函数的各个参数的含义是什么?比如coreSize,maxsize等?

线程池的工作模型主要两部分组成,一部分是运行Runnable的Thread对象,另一部分就是阻塞队列。由线程池创建的Thread对象其内部的run方法会通过阻塞队列的take方法获取一个Runnable对象,然后执行这个Runnable对象的run方法(即,在Thread的run方法中调用Runnable对象的run方法)。当Runnable对象的run方法执行完毕以后,Thread中的run方法又循环的从阻塞队列中获取下一个Runnable对象继续执行。这样就实现了Thread对象的重复利用,也就减少了创建线程和销毁线程所消耗的资源。

Java通过Executors提供四种线程池,分别为:

newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

newFixedThreadPool创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

newScheduledThreadPool创建一个定长线程池,支持定时及周期性任务执行。

newSingleThreadExecutor创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

线程池的关闭方式有几种?各自的区别是什么?

Java提供的对ExecutorService的关闭方式有两种,一种是调用其shutdown()方法,另一种是调用shutdownNow()方法。

这两者是有区别的:

shutdown():

1.调用之后不允许往线程池内继续添加线程;

2.线程池的状态变为shutdown状态;

3.所有在调用shutdown()方法之前提交到ExecutorService的任务都会执行;

4.一旦所有线程结束执行当前任务,ExecutorService才会真正关闭。

shutdownNow():

1.该方法返回尚未执行的 task 的 List;

2.线程池的状态变为STOP状态;

3.阻止所有正在等待启动的任务, 并且停止当前正在执行的任务。

简单点来说,就是::shutdown()调用后,不可以再submit新的 task,已经submit的将继续执行shutdownNow()调用后,试图停止当前正在执行的 task,并返回尚未执行的 task 的 list。

假如有一个第三方接口,有很多个线程去调用获取数据,现在规定每秒钟最多有10个线程同时调用它,如何做到?

ScheduledThreadPoolExecutor设置定时,进行调度。 

public ScheduledThreadPoolExecutor(int corePoolSize, ThreadFactory threadFactory) {                     
      super(corePoolSize, Integer.MAX_VALUE, 0, TimeUnit.NANOSECONDS, 
      new DelayedWorkQueue(), threadFactory); 
}

spring的controller是单例还是多例?怎么保证并发的安全?

单例。

Spring多线程请求过来调用的Controller对象都是一个,而不是一个请求过来就创建一个Controller对象。

原因就在于Controller对象是单例的,那么如果不小心在类中定义了类变量,那么这个类变量是被所有请求共享的,这可能会造成多个请求修改该变量的值,出现与预期结果不符合的异常。

那有没有办法让Controller不以单例而以每次请求都重新创建的形式存在呢?
答案是当然可以,只需要在类上添加注解@Scope("prototype")即可,这样每次请求调用的类都是重新生成的(每次生成会影响效率)虽然这样可以解决问题,但增加了时间成本,总让人不爽,还有其他方法么?答案是肯定的!

使用ThreadLocal来保存类变量,将类变量保存在线程的变量域中,让不同的请求隔离开来。

用三个线程按顺序循环打印abc三个字母,比如abcabcabc?

这是一道经典的Java多线程面试题,直接上代码:

package JavaDay5_27;
 
import java.util.Collections;
 
/**
 * @author [email protected]
 * @date 18-5-27  下午8:00
 */
 
public class Demo1 {
    public static void main(String[] args) {
        Demo1 demo1 = new Demo1();
        PrintLetter printLetter = demo1.new PrintLetter();
        new Thread(demo1.new PrintThread(printLetter, 'B')).start();
        new Thread(demo1.new PrintThread(printLetter, 'A')).start();
        new Thread(demo1.new PrintThread(printLetter, 'C')).start();
//        Collections.synchronizedList()
    }
 
    private class PrintLetter {
        private char letter = 'A';
 
        public char getLetter() {
            return letter;
        }
 
        public void print() {
            System.out.print(letter);
            if('C' == letter) {
                System.out.println();
            }
        }
 
        public void nextLetter() {
            switch (letter) {
                case 'A': {
                    letter = 'B';
                    break;
                }
                case 'B': {
                    letter = 'C';
                    break;
                }
                case 'C': {
                    letter = 'A';
                    break;
                }
            }
        }
    }
 
    private class PrintThread implements Runnable {
        private PrintLetter printLetter;
        private char letter;
 
        public PrintThread(PrintLetter printLetter, char letter) {
            this.printLetter = printLetter;
            this.letter = letter;
        }
 
        @Override
        public void run() {
            for(int i = 0; i < 10; i++) {
                synchronized (printLetter) {
                    while(letter != printLetter.getLetter()) {
                        try {
                            printLetter.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    printLetter.print();
                    printLetter.nextLetter();
                    printLetter.notifyAll();
                } 
            }
        }
    }
}

运行结果为:

ThreadLocal用途是什么?原理是什么?用的时候要注意什么?

ThreadLocal是一个线程内部的数据存储类,通过它可以在指定的线程中存储数据,数据存储以后,只有在指定的线程中可以获取到存储的数据,对于其他线程来说则无法取到数据。

应用场景:

1.某些数据是以线程为作用域并且不同线程具有不同的数据的副本时,就可以考虑用ThreadLocal。

2.复杂逻辑下的对象传递,比如监听器的传递,有些时候一个线程中的任务过于复杂,我们又需要监听器能够贯穿整个线程的执行过程。采用ThreadLocal可以让监听器作为线程内的全局对象而存在,在线程内部只要通过get方法就可以获取到监听器。

ThreadLocal是一个泛型类,定义为public class ThreadLocal,只要弄清楚ThreadLocal的get方法和set方法,就可以明白它的实现原理:

ThreadLocal的set方法

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

从set源码可以看出,首先getMap方法来获取当前线程的ThreadLocalMap,这个Map是一个自定义的hashMap,key是TheadLocal,value是对应存储的值。

ThreadLocal的get方法

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

从get源码可以看出,首先也是一样用getMap方法来获取当前线程的ThreadLocalMap,然后根据key=当前ThreadLocal来获取对应的value值。

从ThreadLocal的set和get方法可以看出,它们所操作的都是当前线程的ThreadLocalMap对象。

因此在不同线程中,访问同一个ThreadLocal的set和get方法,它们对ThreadLocal的读、写操作仅限于各自线程的内部,从而使ThreadLocal可以在多个线程中互不干扰地存储和修改数据。

如果让你实现一个并发安全的链表,你会怎么做?

不会。

有哪些无锁数据结构?他们实现的原理是什么?

java 1.5提供了一种无锁队列(wait-free/lock-free)ConcurrentLinkedQueue,通过volatile关键字来保证数据唯一性,但是里面又用到了atomic。

讲讲java同步机制的wait和notify?

典型的Wait-Notify场景一般与以下两个内容相关:

1. 状态变量(State Variable)

当线程需要wait的时候,总是因为一些条件得不到满足导致的。例如往队列里填充数据,当队列元素已经满时,线程就需要wait停止运行。当队列元素有空缺时,再继续自己的执行。

2. 条件断言(Condition Predicate)

当线程确定是否进入wait或者是从notify醒来的时候是否继续往下执行,大部分都要测试状态条件是否满足。例如,往队列里添加元素,队列已满,于是阻塞当前线程,当有其他线程从队列里取走了元素,就通知在等待的线程“队列有剩余空间,可以往里添加元素了”。这时,等待添加元素的进程就会被唤醒,然后判断一下当前队列是否真的有剩余空间,如果真的有剩余空间,就将元素添加进去,如果没有,则继续阻塞等待下次唤醒。

3. 条件队列(Condition Queue)

每个对象都有一个内置的条件队列,当一个线程在该对象锁上调用wait函数的时候,就会将该线程加入到该对象的条件队列中。

wait与notify是Java同步机制中的重要组成部分。结合与synchronized关键字使用,可以建立很多优秀的同步模型,例如生产者-消费者模型。但是在使用wait()、notify()、notifyAll()函数的时候,需要特别注意以下几点:

1.wait()、notify()、notifyAll()方法不属于Thread类,而是属于Object基础类,也就是说每个对象都有wait()、notify()、notifyAll()的功能。因为每个对象都有锁,锁是每个对象的基础,因此操作锁的方法也是最基础的。

2.调用obj的wait(), notify()方法前,必须获得obj锁,也就是必须写在synchronized(obj){...} 代码段内。

3.调用obj.wait()后,线程A就释放了obj的锁,否则线程B无法获得obj锁,也就无法在synchronized(obj){...} 代码段内唤醒线程A。

4.当obj.wait()方法返回后,线程A需要再次获得obj锁,才能继续执行。

5.如果线程A1,A2,A3都在obj.wait(),则线程B调用obj.notify()只能唤醒线程A1,A2,A3中的一个(具体哪一个由JVM决定)。

6.如果线程B调用obj.notifyAll()则能全部唤醒等待的线程A1,A2,A3,但是等待的线程要继续执行obj.wait()的下一条语句,必须获得obj锁。因此,线程A1,A2,A3只有一个有机会获得锁继续执行,例如A1,其余的需要等待A1释放obj锁之后才能继续执行。

7.当线程B调用obj.notify()或者obj.notifyAll()的时候,线程B正持有obj锁,因此,线程A1,A2,A3虽被唤醒,但是仍无法获得obj锁。直到线程B退出synchronized代码块,释放obj锁后,线程A1,A2,A3中的一个才有机会获得对象锁并得以继续执行。

示例代码:

线程的wait操作的典型代码结构如下:

public void test() throws InterruptedException {
    synchronized(obj) {
        while (!contidition) {
            obj.wait();
        }
    }
}

为什么obj.wait()操作必须位于循环中呢?有以下几个主要原因:

1. 一个对象锁可能用于保护多个状态变量,当它们都需要wait-notify操作时,如果不将wait放到while循环中就会有问题。例如,某对象锁obj保护两种状态变量a和b,当a的条件断言不成立时发生了wait操作,当b的条件断言不成立时也发生了wait操作,两个线程被加入到obj对应的条件队列中。现在若改变状态变量a的某操作发生,在obj上调用了notifyAll操作,则obj对应的条件队列里的所有线程均被唤醒,之前等待a的一个或几个线程去判断a的条件断言可能成立了,但是b对于的条件断言肯定仍不成立,而此时等待b的线程也被唤醒了,所以需要循环判断b的条件断言是否满足,如果不满足,则继续wait。

2. 多个线程wait的同一状态的条件断言。例如,向队列添加元素的场景,当前队列是满的,多个线程想往里面添加元素,于是都wait了。此时,另一个线程从队列里取出了一个元素,调用了notifyAll操作,唤醒了所有线程,但是只有一个线程能够往队列里添加一个元素,其他的仍需要等待。

3. 虚假唤醒。在没有被通知、中断或超时的情况下,线程自动苏醒了。虽然这种情况在实践中很少发生,但是通过循环等待可以杜绝这一情况的发生。

猜你喜欢

转载自blog.csdn.net/Kevin_Gu6/article/details/88184520