Java基础知识(多线程)

说明:其中内容摘自:《Java程序员面试宝典》,这里仅供学习使用。(P143)

 1. 什么是线程?他和进程有什么区别?为什么使用多线程?

进程是程序运行资源分配的基本单位,线程CPU调度和分配的基本单位,它是程序执行的最小单元。一个程序至少有一个进程,一个进程至少有一个线程。进程在执行过程中拥有独立的内存单元(代码段,数据段和堆空间),而在一个进程中运行的多个线程并发运行共享这些内存资源,但每个线程拥有自己的栈空间

2. 同步和异步有什么区别?

(1)同步方式是一种对共享资源的访问方式。当多个资源需要访问同一个互斥资源时,他们需要以某种顺序来确保该资源在某个时刻只能有一个线程对其使用。

要实现同步操作,必须要获取线程对象的锁,保证在每个时刻只有一个线程能够进出临界区访问互斥资源的代码块),并且在这个锁被释放之前,其他线程都不能在进入这个临界区。如果还有其他线程想要获得该对象的锁,只能进入等待队列等待。只有该对象的锁被释放之后,等待队列中优先级最高的线程才能获得该锁,进入临界区。

Java使用synchronized关键字:同步代码块同步方法。的方式支持同步操作,但系统开销代价很大,并且有可能发生死锁

(2)异步方式是一种非阻塞的方式,它一个线程对对象的操作时,不必关心其他线程的状态或行为,也不必等到方法处理完成返回后才进行之后的操作。

3. 如何实现Java多线程?

(1)extends Thread类,Thread类其实也是一个实现了Runnable接口的一个实例。这种方式下,启动线程的唯一方式就是通过调用Thread类的start()方法,他是一个native方法,将启动一个新的线程,并执行重新实现的run方法。但调用start方法并不是立即执行多个线程的代码,而是使该线程变成Runable的状态,什么时候运行则是由操作系统决定。

(2)implements Runnable接口,并实现run方法;创建Thread对象,用Runable接口的对象作为参数实例化该Thread对象;调用Thread的start方法。

(3)implements Callable 接口,重写call方法。它比Runnable功能强大,具有如下特点:

  • Callable可以在任务运行结束后提供一个返回值
  • Callable中的call方法可以抛出异常,而Runnable中的run方法无法抛出异常。
  • 运行Callable可以得到一个Future对象,Future对象表示异步计算的结果,它提供了检查计算是否完成的方法。可以使用Future来监视目标线程的call方法是否完成,当调用Future的get方法时,当前线程就会阻塞,知道call方法结束返回结果为止。

以上三种方式推荐使用Runnable的方式:

  • 使用它为实现一个接口,不想Thread类是继承的方式,这样不会影响当前类继承其他类。
  • 使用runnable可以实现资源共享。

4. run()方法和start()方法有什么区别?

系统调用Thread类的start方法启动一个线程时,该线程处于就绪状态而非运行状态,这就意味着这个线程可以被JVM来调度执行。在调度过程中,JVM通过调用线程类的run方法来完成实际的操作,当run方法结束后,线程终止。

但如果直接调用run方法,则被当做一个正常的函数调用,此时程序中仍然只有主线程这一个线程。也就是说,只有使用start方法才能实现真正的多线程的目的。

5. Java多线程同步的实现方式有哪些?

(1)synchronized关键字。java中每个对象都有一个锁,当一个线程调用一段synchronized代码时,需要先获取这个锁,然后去执行相应的代码,执行结束后释放锁。

synchronized有两种使用方式,同步方法同步代码块

  • synchronized方法。但这种方式会很大的影响程序的执行效率。
public synchronized void mutiThreadAccess();
  • synchronized代码块。可以被任意的代码声明为synchronized,也可以指定上锁的对象。
synchronized(syncObject) {
    // 访问syncObject的代码
}

 (2)wait和notify方法。

这两个方法与synchronized配合使用。当使用synchronized来修饰某个共享资源时,如果线程A1在执行该synchronized代码,另外一个线程A2也要同时执行同一对象的同一synchronized代码时,线程A2将要等到A1执行完成后,才能继续执行。这种情况下,线程A2可以使用wait和notify方法。

synchronized代码被执行期间,可以使用wait方法释放该对象的锁,进入等待状态,并可以调用notify方法或者notifyAll方法通知正在等待的其他线程。notify方法为通知等待队列中的第一个等待线程,而notifyAll方法则是唤醒所有等待线程,并允许他们去竞争获取锁

(3)Lock方法。JDK 5新增了Lock接口和他的一个实现类ReentrantLock(重入锁)。可以实现多线程的同步,提供如下的方法:

  • lock():阻塞的方式获取锁
  • tryLock():非阻塞的方式获取锁
  • tryLock(long timeout, TimeUnit unit):时间限制的方式获取锁
  • lockInterruptibly()

6. sleep方法和wait方法有什么区别?

  • sleep来自Thead类,wait来自Object类。调用sleep方法的过程中,线程不会释放对象锁,而调用wait方法,线程会释放对象锁。
  • sleep睡眠后不让出系统资源,wait让其他线程可以占用CPU
  • sleep方法需要指定一个睡眠时间,时间一到自动唤醒。而wait方法需要配合notify或notifyAll方法使用。
  • sleep可以在任何地方使用,而wait方法需要与synchronized(方法或代码块)中配合使用。

sleep方法和yield方法有什么区别?

一个调用yield()方法的线程告诉虚拟机它乐意让其他线程占用自己的位置。这表明该线程没有在做一些紧急的事情。注意,这仅是一个暗示,并不能保证不会产生任何影响。

  •  sleep方法给其他线程运行计划时不考虑线程的优先级,因此会给低优先级的线程以运行机会,而yield方法只会给相同优先级或者更高优先级以运行的机会。
  • 线程执行sleep方法后进入阻塞状态,所以执行sleep方法的线程在指定的时间内肯定不会被执行,而yield方法只是使当前线程重新回到可执行状态,所以执行yield方法的线程可能进入可执行状态后马上又被执行
  • sleep需要抛出InterrupatedException,而yield方法无需抛出异常

 7. 终止线程的方法有哪些?

  • stop方法。他会释放已经锁定的所有监视资源,但容易是程序进入一个不一致的状态
  • suspend方法。容易发生死锁,它不会释放锁。导致一个问题:如果使用suspend方法挂起一个有锁的线程,那么在锁恢复之前将不会被释放。
  • 自行结束进入DEAD状态。即执行完run方法,通过设置一个flag变量,可以让线程离开run方法从而终止线程。
  • 使用interrupt方法打断阻塞抛出异常,安全退出。

8. synchronized和ReentrantLock的区别?

synchronized使用Object对象本身notify,wait,notifyAll调度机制,Lock使用condition进行线程之间的调度,完成synchronized的所有功能。

  • 用法不一样,synchronized为同步方法或同步代码块。Lock需要显示的指定起始位置和终止位置。synchronized是托管给JVM执行,而Lock的锁定则是通过代码实现,可以更精确的定义语义。
  • 性能不一样。synchronized在资源竞争激烈时性能下降很快,而ReentrantLock的性能基本保持不变。
  • 锁的机制不一样。synchronized自动锁定,不会因为出了异常而引发死锁。但Lock是手动去释放,并且必须在finally方法中释放,否则容易引发死锁问题。
  • Lock提供了更强大的功能。例如tryLock以异步的方式获取锁。

9. 什么是守护线程?

Java中提供了两种线程类型:用户线程守护线程。两者的不同之处在于,如果用于线程已经全部退出运行,只剩下守护线程时,jvm就会退出了。但只要有任何非守护线程还在运行,程序就不会终止

守护线程一般具有较低的优先级,用户在编写程序时可以通过在start方法之前,调用对象的setDaemon(true)方法即可,如果参数设置为false,则表示用户线程。

当一个守护线程种产生了其他线程,那么这些新产生的线程默认还是守护线程。

10. join方法的作用是什么?

join方法的作用是让调用这个方法的线程执行完自己的run方法以后,再去调用join方法之后的代码。简单来看,是将两个线程合并,用于实现同步功能。  


20190310 补充面试资料,摘自:

1. 什么是多线程的上下文切换?

多线程的上下文切换是指CPU控制权由一个正在运行的线程切换到另外一个就绪并等待获取CPU执行权的线程的过程。

2. 什么导致线程阻塞?

 

3. 什么是死锁?死锁产生的条件是什么?

死锁是多个线程在执行过程中,因争夺资源而导致的一种相互等待的情况,如果无外力作用,他们都无法推进。

产生死锁的4个条件:

  • 互斥条件:一个资源每次只能别一个进程所使用。
  • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  • 不剥夺条件:进程已获得的资源,在未使用完成之前,不能强行剥夺。
  • 循环等待条件:若干线程之间形成一种头尾相接的循环等待的资源获取关系。

4. 如何唤醒一个阻塞的线程?

如果线程是因为调用了waitsleepjoin方法而导致的阻塞,可以中断线程,并通过抛出InterruptedExceptioin异常来唤醒它。如果线程遇到了IO阻塞,则无能为力。因为IO阻塞时操作系统实现的,Java代码无法处理。

5. Java线程的状态有哪些?状态转换是什么?

图片来自:https://www.cnblogs.com/wxd0108/p/5479442.html

(1)新建状态(New):新创建了一个线程对象。

(2)就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。

(3)运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。

(4)阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:

  • 等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)
  • 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
  • 其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(注意,sleep是不会释放持有的锁)

(5)死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

6. wait和notify/notifyAll方法在放弃对象监视器时有什么不同?

wait方法会立即释放对象监视器,notify/notifyAll方法则会等待线程剩余代码执行完毕后才会放弃对象监视器。

7. FutureTask是什么?

FutureTask表示一个异步运算的任务。FutureTask里面可以传入一个Callable的具体实现类,可以对这个异步运算的任务的结果进行等待获取,判断是否已经完成、取消任务等操作。由于FutrueTask也是Runnable接口实现的类,所以FutureTask也可以放入到线程池中。

8. 如果在两个线程间实现共享数据?

通过在线程之间共享对象就可以了,然后通过wait/notify/notifyAll方法进行唤起和等待,比如阻塞队列BlockingQueue就是为线程之间共享数据而设计的。

9. 如何正确的使用wait方法?使用if还是while?

wait方法应该在while循环中调用,因为当线程获取到CPU开始执行时,其他条件可能还没有结束,所以在处理前,循环检测条件是否满足会更好。下面是一段标准的使用wait和notify的代码

synchronized (obj) {
    while (condition does not hold) {
        obj.wait() // releases lock, and reacquires on wakeup
        ; // perform action appropriate to condition
    }
}

10. 什么是ThreadLocal,它的作用是什么?

ThreadLoad是线程局部变量,局限于线程内部,不再多个线程之间共享,从而实现一种线程安全的模式。它在每个线程种维护一个ThreadLocal.ThreadLocalMap把数据进行隔离。

11. 生产者和消费者模型的作用是什么?

  • 通过平衡生产者的生产能力消费者的消费能力提升整个系统的运行效率,这是生产者消费者模型最重要的作用
  • 解耦,这是这个模型的附带的作用。

12. 实现一个生产者和消费者模型?

生产者代码如下:

// producer
public class Producer implements Runnable {
    private final BlockingQueue<Integer> queue;
    public Producer(BlockingQueue queue) {
        this.queue = queue;
    }
    public void run() {
        try {
            while(true) {
                Thread.sleep(1000);
                queue.put(produce());
            }
        } catch (InterruptedException e) {
        }    
    }

    private int produce() {
        int n = new Random().nextInt(10000);
        System.out.println("Thread: " + Thread.currentThread().getId() + "produce: " + n);
        return n;
    }
}

消费者如下:

// consumer
public class Consumer implements Runnable {
    private final BlockingQueue<Integer> queue;
    public Consumer(BlockingQueue queue) {
        this.queue = queue;
    }
    public void run() {
        try {
            while(true) {
                Thread.sleep(2000);
                consume(queue.take());
            }
        } catch (InterruptedException e) {
        }    
    }

    private void consume(Integer n) {
        System.out.println("Thread: " + Thread.currentThread().getId() + "consume: " + n);
    }
}

主方法:

public static void main(String[] args) {
    BlockingQueue queue = new ArrayBlockingQueue<Integer>(100);
    Producer p1 = new Producer(queue);
    Consumer c1 = new Consumer(queue);
    Consumer c2 = new Consumer(queue);
    new Thread(p).start();
    new Thread(c1).start();
    new Thread(c2).start();
}

 13. 如果提交任务时,线程队列已经满了,这时会发生什么?

如果使用的是无界队列LinkedBlockingQueue的话,继续添加任务到阻塞的队列中等待执行就可以了。如果使用的是有界队列ArrayBlockingQueue的话,任务首先会被添加到ArrayBlockingQueue中,ArrayBlockingQueue如果已经满了,则会使用拒绝策略RejectedExceptionHandler处理满了的任务,默认为AbortPolicy(终止策略)。

14. Java中用到的线程调度算法是什么?

抢占式。一个线程的CPU时间使用完成后,OS会根据线程优先级,线程饥饿程度等数据算出一个调度优先级并分配下一个时间片给某个线程执行。

15. Thread.sleep(0)的作用是什么?

这样会手动触发一次操作系统分配时间片的操作,以平衡CPU控制权,使优先级较低的线程也有可能获得CPU的控制权。

16. 什么是CAS?

CAS全称为Compare and swap,即比较和交换。假设又有三个操作数:内存值V,旧的预期值A,要修改的值B。当且仅当预期值A = 内存值V,才会将内存值修改为B并返回true,否则什么都不做返回false。

CAS一定要和volatile关键字配合使用,这样才能保证每次拿到的变量内存中的最新的那个值,否则旧的预期值A对某个线程来说,永远是一个不会变的值A,只要某次CAS操作失败,永远都不可能成功。

17. 什么是乐观锁和悲观锁?

  • 乐观锁认为竞争不总是会发生,因此他不需要持有锁,将CAS这两个动作作为一个原子操作尝试求修改内存中的变量,如果失败则表示发生冲突,那么就有相应的重试逻辑。
  • 悲观锁认为竞争总是会发生,因此每次对某个资源进行操作时,都会持有一个独占的锁,就想synchronized,直接上锁后才操作资源。

18. ConcurrentHashMap的并发度是什么?

ConcurrentHashMap的并行度就是segment的大小,默认为16,即同时可以有16个线程操作ConcurrentHashMap。

19. ConcurrentHashMap的工作原理是什么?

ConcurrentHashMap在JDK1.6和JDK1.8中的实现原理不同:

(1)JDK1.6中,ConcurrentHashMap是采用分离锁的方式,它并没有对整个Hash表进行锁定,而是局部锁定,也就是当一个线程占用整个局部锁时,不影响其他线程对hash表的其他部分进行访问。

HashTable时通过对Hash表结构进行锁定,是阻塞式的,当一个线程占有这个锁时,其他线程必须阻塞等待他释放锁。

(2)JDK1.8中,ConcurrentHashMap不再使用Segment分离锁,而是采用一种乐观锁CAS算法来实现同步问题,但其底层还是:数组+链表->红黑树的实现方式。

20. CyclicBarrier和CountDownLatch的区别?

这两个类都在java.util.concurrent包中,表示代码运行到某个点上,区别如下:

  • CyclicBarrier的某个线程运行到某个点上后,该线程即停止运行,直到所有的线程都到达这个点上,所有线程才重新运行。CountDownLatch则不是,某个线程运行到某个点上之后,只是给某个数值-1而已,该线程继续运行。
  • CyclicBarrier只能唤起一个任务,CountDownLatch可以唤起多个任务。
  • CucylicBarrier可重用,CountDownLatch不可重用,计数值为0时,该CountDownLatch就不再可用了。

20190311 资料补充

1. Thread和Runnable区别总结?

实现Runnable接口比继承Thread类所具有的优势:

  • 适合实现资源共享
  • 可以避免Java单继承的影响
  • 线程池只能放入实现Runnable和Callable接口的线程,而不能放入直接继承Thread的类

此外:main方法其实本身是一个线程,java中,每次程序运行至少启动两个线程,一个是main线程,一个是GC线程。

2. 线程常用函数:

(1)线程睡眠:Thread.sleep(long millis)方法,使线程转到阻塞状态。millis参数设定睡眠的时间,以毫秒为单位。当睡眠结束后,就转为就绪(Runnable)状态。sleep()平台移植性好。

(2)线程等待:Object类中的wait()方法,导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 唤醒方法。这个两个唤醒方法也是Object类中的方法,行为等价于调用 wait(0) 一样。

(3)线程让步:Thread.yield() 方法,暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程。

  • 使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中

(4)线程加入:join()方法,等待其他线程终止。在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪状态。

  • 例如在主线程main中创建了两个子线程(T1,T2),在main中分别调用了T1.join(), T2.join(),则main方法会等待T1,T2运行完毕后继续运行。

(5)线程唤醒:Object类中的notify()方法,唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。选择是任意性的,并在对实现做出决定时发生。线程通过调用其中一个 wait 方法,在对象的监视器上等待。 直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程。被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争;例如,唤醒的线程在作为锁定此对象的下一个线程方面没有可靠的特权或劣势。类似的方法还有一个notifyAll(),唤醒在此对象监视器上等待的所有线程。

 注意:Thread中suspend()和resume()两个方法在JDK1.5中已经废除,不再介绍。因为有死锁倾向。

 (6)线程中断:Thread.interrupt()。不要以为它是中断某个线程!它只是线线程发送一个中断信号,让线程在无限等待时(如死锁时)能抛出异常,从而结束线程

3. wait和sleep比较:

共同点

(1)它们都工作在多线程的环境下,都可以在程序的调用处阻塞指定的毫秒数,并返回。 

(2)wait()和sleep()都可以通过interrupt()方法 打断线程的暂停状态 ,从而使线程立刻抛出InterruptedException。 

  • 如果线程A希望立即结束线程B,则可以对线程B对应的Thread实例调用interrupt方法。如果此刻线程B正在wait/sleep /join,则线程B会立刻抛出InterruptedException,在catch() {} 中直接return即可安全地结束线程。 
  • 需要注意的是,InterruptedException是线程自己从内部抛出的,并不是interrupt()方法抛出的。对某一线程调用 interrupt()时,如果该线程正在执行普通的代码,那么该线程根本就不会抛出InterruptedException。但是,一旦该线程进入到 wait()/sleep()/join()后,就会立刻抛出InterruptedException 。 

不同点: 
(1)Thread类的方法:sleep(),yield()等 。Object的方法:wait()和notify()等 

(2)每个对象都有一个锁来控制同步访问。Synchronized关键字可以和对象的锁交互,来实现线程的同步。 sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。 

(3)wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用 

(4)sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常

所以sleep()和wait()方法的最大区别是:

  1. sleep()睡眠时,保持对象锁,仍然占有该锁;
  2. 而wait()睡眠时,释放对象锁。

但是wait()和sleep()都可以通过interrupt()方法打断线程的暂停状态,从而使线程立刻抛出InterruptedException(但不建议使用该方法)。

4. synchronized使用解释:

public synchronized void method() {
    // do something
}

这个时候,synchronized获取的是哪个对象的锁呢?是调用这个同步方法的对象的锁。也就是说当一个对象O1在不同的线程种调用了这个同步方法,那么就会因为加锁达到同步的效果。但这个对象所属类的另外一个对象O2则可以仍可以调用这个方法。上述的代码等同于:

public void method() {
    synchronized (this) {
        // do something
    }
}

 5. 多线程的数据传递:

(1)通过构造方法传递数据。这种方法是在创建线程对象的同时传递数据,因此在线程运行之前这些数据就就已经到位了,这样就不会造成数据在线程运行后才传入的现象。如果要传递更复杂的数据,可以使用集合数据结构。使用构造方法来传递数据虽然比较安全,但如果要传递的数据比较多时,就会造成很多不便。

(2)通过变量和方法传递数据。向对象中传入数据一般有两次机会,第一次机会是在建立对象时通过构造方法将数据传入,另外一次机会就是在类中定义一系列的public的方法或变量(也可称之为字段)。然后在建立完对象后,通过对象实例逐个赋值

(3)通过回调函数传递数据。

interface Process {
    public int process(int[] numbers);
}

public class Data implements Process {
    public int process(int[] numbers) {
        int sum = 0;
        foreach (int i : numbers) {
            sum += i;
        }
        return sum;
    }
}

public class MyThread implements Runnable {
    private Data data;
    public MyThread(Data data) {
        this.data = data;
    }
    public void run() {
        Random random = new Random();
        int n1 = random.nextInt(1000);
        int n2 = random.nextInt(1000);
        int n3 = random.nextInt(1000);

        int[] numbers = {n1, n2, n3};
        int sum = data.process(numbers); // 调用回调函数

        System.out.println(sum);
    }
}

以上部分内容参考自:https://www.cnblogs.com/yjd_hycf_space/p/7526608.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值