Java多线程问题总结

时间:2019-03-20
本文章向大家介绍Java多线程问题总结,主要包括Java多线程问题总结使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

本文大部分为整合内容,会参考不少其他的技术博客。如有问题,请联系我。

目录:

一:java线程状态和转化过程

二:现在有T1、T2、T3三个线程,你怎样保证T2在T1执行完后执行,T3在T2执行完后执行

三. Java 中新的 Lock 接口相对于同步代码块(synchronized block)有什么优势?如果让你实现一个高性能缓存,支持并发读取和单一写入,你如何保证数据完整性。

一:java线程状态和转化过程

参考:https://www.cnblogs.com/happy-coder/p/6587092.html

线程转化图:

说明
线程共包括以下5种状态。
1. 新建状态(New)         : 线程对象被创建后,就进入了新建状态。例如,Thread thread = new Thread()。
2. 就绪状态(Runnable): 也被称为“可执行状态”。线程对象被创建后,其它线程调用了该对象的start()方法,从而来启动该线程。例如,thread.start()。处于就绪状态的线程,随时可能被CPU调度执行。
3. 运行状态(Running) : 线程获取CPU权限进行执行。需要注意的是,线程只能从就绪状态进入到运行状态。
4. 阻塞状态(Blocked)  : 阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
    (01) 等待阻塞 -- 通过调用线程的wait()方法,让线程等待某工作的完成。
    (02) 同步阻塞 -- 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。
    (03) 其他阻塞 -- 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
5. 死亡状态(Dead)    : 线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

二:现在有T1、T2、T3三个线程,你怎样保证T2在T1执行完后执行,T3在T2执行完后执行?

主要考察,Thread的join()方法,看下api解释: 等待该线程终止。

代码如下:

public class ThreadTest {
 
    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        Thread thread1 = new Thread(){
            @Override
            public void run() {
                // TODO Auto-generated method stub
                
                try {
                    System.out.println("thread 1 running....");
                    sleep(1000);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }finally{
                    System.out.println("thread 1 stoped....");
                }
                super.run();
            }
        };
        Thread thread2 = new Thread(){
            @Override
            public void run() {...}
        };
        Thread thread3 = new Thread(){
            @Override
            public void run() {...}
        };        
            try {
                thread1.start();
                thread1.join();
                thread2.start();
                thread2.join();
                thread3.start();
                thread3.join();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
    
    }
 
}

三. Java 中新的 Lock 接口相对于同步代码块(synchronized block)有什么优势?如果让你实现一个高性能缓存,支持并发读取和单一写入,你如何保证数据完整性。

 常见的锁:

Synchronized,它就是一个:非公平,悲观,独享,互斥,可重入的重量级锁,又叫内置加锁。
ReentrantLock,它是一个:默认非公平但可实现公平的,悲观,独享,互斥,可重入,重量级锁。
ReentrantReadWriteLocK,它是一个,默认非公平但可实现公平的,悲观,写独享,读共享,读写,可重入,重量级锁。

Synchronized的优缺点:

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

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

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

问题:1-那么如果这个获取锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,其他线程便只能干巴巴地等待,试想一下,这多么影响程序执行效率。

2-不是太灵活,实现读写锁用synchronized就实现不了。而lock加锁更灵活,对锁的粒度有更好的控制效果。

Lock就可以解决上面的两个问题,缺点是,lock必须手动释放锁,而synchronized是不需要手动释放。所以更加危险。

lock的使用方法:

Lock lock = ...;
lock.lock();
try{
    //处理任务
}catch(Exception ex){     
}finally{
    lock.unlock();   //释放锁
}

如果让你实现一个高性能缓存,支持并发读取和单一写入,你如何保证数据完整性。

这个实质上就是读写锁的应用。可以直接使用ReadWriteLock接口实现;

代码如下:

    public class ReadWriteMap<K,V> {
        private final Map<K,V> map;
        private final ReadWriteLock lock = new ReentrantReadWriteLock();
        private final Lock readLock = lock.readLock();
        private final Lock writeLock = lock.writeLock();

        public ReadWriteMap(Map<K,V> map){
            this.map = map;
        }
        //remove,putAll等修改内容的方法类似
        public void put(K key,V value){
            writeLock.lock();
            try{
                map.put(key,value);
            }finally {
                writeLock.unlock();
            }
        }
        //只读操作
        public V get(K key){
            readLock.lock();
            try{
                return map.get(key);
            }finally {
                readLock.unlock();
            }
        }
    }

事实上,Java自带的ConcurrentHashMap已经能很好的实现上述的功能。

四:Java 中 wait 和 sleep 方法有什么区别

对于sleep()方法,我们首先要知道该方法是属于Thread类中的。而wait()方法,则是属于Object类中的。

sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。

在调用sleep()方法的过程中,线程不会释放对象锁。

而当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备

获取对象锁进入运行状态。

五:如何在 Java 中实现一个阻塞队列?

阻塞队列与普通队列的区别在于,当队列是空的时,从队列中获取元素的操作将会被阻塞,或者当队列是满时,往队列里添加元素的操作会被阻塞。试图从空的阻塞队列中获取元素的线程将会被阻塞,直到其他的线程往空的队列插入新的元素。同样,试图往已满的阻塞队列中添加新元素的线程同样也会被阻塞,直到其他的线程使队列重新变得空闲起来,如从队列中移除一个或者多个元素,或者完全清空队列。

代码:

public class MyBlockingQueue {
 
  private List queue = new LinkedList();
  private int  limit = 10;
 
  public MyBlockingQueue(int limit){
    this.limit = limit;
  }
 
 
  public synchronized void enqueue(Object item)
  throws InterruptedException  {
    while(this.queue.size() == this.limit) {
      wait();
    }
    if(this.queue.size() == 0) {
      notifyAll();
    }
    this.queue.add(item);
  }
 
 
  public synchronized Object dequeue()
  throws InterruptedException{
    while(this.queue.size() == 0){
      wait();
    }
    if(this.queue.size() == this.limit){
      notifyAll();
    }
 
    return this.queue.remove(0);
  }
 
}

如果用 Java 5 的并发类实现,基本上就是用java已经实现的类,例如BlockingQueue接口实现。

 六 如何在 Java 中编写代码解决生产者消费者问题?

阻塞队列的使用。

七 写一段死锁代码。你在 Java 中如何解决死锁?

死锁发生需要必备的四个条件:

1-互斥条件,资源中必须有一个不能被共享;

2-至少有一个任务它必须持有一个资源且正在等待获取另外一个当前被别的任务持有的资源;

3-资源不能被任务抢占;

4-必须有循环等待。

死锁代码:

    public static void main(String[] args) throws InterruptedException {
        final DeadLock dd1 = new DeadLock();
        final DeadLock dd2 = new DeadLock();
        Thread t1 = new Thread(new Runnable() {
            public void run() { //首先获得dd1的锁 
        synchronized (dd1)
{ //休眠 try { Thread.sleep(50); synchronized (dd2) { System.out.println(Thread.currentThread().getName() + "线程。。"); } } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }, "t1"); Thread t2 = new Thread(new Runnable() { public void run() { synchronized (dd2) { try { synchronized (dd1) { System.out.println(Thread.currentThread().getName() + "线程。。"); } } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }, "t2"); t1.start(); t2.start(); }

如何解决死锁(只需破坏到死锁的四个条件之一就可):

1-加锁顺序,所有的线程都按照顺序获取锁。破坏掉第二条。

2-加锁时限,在获取锁的时候加一个超时时间,如果超时,则释放已占用的锁。破坏掉第四条。

3-死锁检测。破坏掉第二条。

八:什么是原子操作?Java 中有哪些原子操作?

原子操作:不可中断的操作。

常见的原子类:AtomicInteger、AtomicLong

九:Java 中 volatile 关键字是什么?你如何使用它?它和 Java 中的同步方法有什么区别?

使用volatile修饰的变量会强制将修改的值立即写入主存,主存中值的更新会使缓存中的值失效。

volatile具有可见性、有序性,不具备原子性。

有序性:即程序执行时按照代码书写的先后顺序执行。

volatile适用场景

1-适用于对变量的写操作不依赖于当前值,对变量的读取操作不依赖于非volatile变量

2-适用于读多写少的场景。

3-可用作状态标志。

4-JDK中volatie应用:JDK中ConcurrentHashMap的Entry的value和next被声明为volatile,AtomicLong中的value被声明为volatile。AtomicLong通过CAS原理(也可以理解为乐观锁)保证了原子性。

volatile VS synchronized

volatile synchronized修饰对象修饰变量修饰方法或代码段可见性11有序性11原子性01线程阻塞01对比这个表格,你会不会觉得synchronized完胜volatile,答案是否定的,volatile不会让线程阻塞,响应速度比synchronized高,这是它的优点。

十. 什么是竞态条件?你如何发现并解决竞态条件?

竞争状态:可能发生在临界状态内的特殊状态。临界状态是被多个线程执行的一段代码,在这个代码段中,线程的执行顺序影响临界状态的并发执行结果。

如何解决竞争状态,基本原则有三种方式:

1-减少锁的持有时间;

2-降低锁的请求频率;

3-使用带有协调机制的独占锁。例如读写锁

具体方式:

1-缩小锁的范围;

2-锁分解(减少锁的粒度),如果一个锁需要保护多个相互独立的状态变量,那么可以将这个锁分解多个锁。并且每个所只保护一个变量。从而提高可伸缩性,并最终降低每个锁被请求的频率;

3-锁分段,例如,ConcurrentHashMap的实现中使用了一个包含16个锁的数组,每个锁保护所有散列桶的1/16。

4-避免热点域,将一些反复计算的结果缓存起来,类似于批处理。

5-使用带有协调机制的独占锁,例如,读写锁。

10. 在 Java 中你如何转储线程(thread dump)?如何分析它?

在 UNIX 中,你可以使用 kill -3 然后线程转储日志会打印在屏幕上,可以使用 CTRL+Break 查看。这只是一个较简单的线程面试题,狡猾一点的话他们会问你如何分析转储日志。线程转储日志对于分析死锁情况非常有用。

11. 既然 start() 方法会调用 run() 方法,为什么我们调用 start() 方法,而不直接调用 run() 方法?

这是一个基本的 Java 多线程面试题。最初,我刚开始多线程编程时对此还有些困惑。如今我一般在 Java 中级面试的电话面试或一轮面试中遇到。
这道问题的答案是这样的。当你调用 start() 方法时,它会新建一个线程然后执行 run() 方法中的代码。如果直接调用 run() 方法,并不会创建新线程,方法中的代码会在当前调用者的线程中执行。可以看这篇文章了解更多线程中 Start 和 Run 方法的区别

12. Java 中你如何唤醒阻塞线程?

这是有关线程的一个很狡猾的问题。有很多原因会导致阻塞,如果是 IO 阻塞,我认为没有方式可以中断线程(如果有的话请告诉我)。另一方面,如果线程阻塞是由于调用了 wait()sleep() 或 join() 方法,你可以中断线程,通过抛出 InterruptedException 异常来唤醒该线程。可以看这篇文章了解有关处理阻塞线程的知识Java 中如何处理阻塞方法

13. Java 中 CyclicBarriar 和 CountdownLatch 有什么区别?

最近的 Java 线程面试题多数在测试你对 JDK 5 并发包的掌握程度。两者区别之一就是 CyclicBarrier 在屏障打开之后(所有线程到达屏障点),可以重复使用。而 CountDownLatch 不行。想了解更多可以参与课程Java 中的多线程和并行计算

14. 什么是不可变类?它对于编写并发应用有何帮助?

尽管这道面试题和线程没有直接关系,但间接影响也很大。如果面试官随后让你写一个不可变类,或问你为什么 Java 中的 String 是不可变的,会让面试题变得更加复杂。

15. 你在多线程环境中遇到的最多的问题是什么?你如何解决的?

内存干扰、竞态条件、死锁、活锁、线程饥饿是多线程和并发编程中比较有代表性的问题。这类问题无休无止,而且难于定位和调试。
这是基于经验给出的 Java 面试题。你可以看看Java 并发实战课程来了解现实生活中高性能多线程应用所面临的问题。

上面所说的是我喜欢的,也是投行最常问的 Java 线程面试题。这个清单并不完整,所以可以在下方评论出你在面试中遇到的有意思的 Java 线程题目。这篇文章收集并分享与多线程概念有关的面试题,不仅仅有助于面试,还为大家打开多线程概念的大门。

有位读者提供了一些 Java 线程面试题。补充在下面:

    1. Java 中绿色线程和本地线程的区别?
    2. 线程和进程的区别?答案
    3. 多线程的上下文切换是什么?
    4. 死锁和活锁的区别?死锁和饥饿的区别?
    5. Java 中使用什么线程调度算法?
    6. Java 中线程调度是什么?
    7. 线程中如何处理某个未处理异常?
    8. 什么是线程组?为什么 Java 中不建议使用线程组?
    9. 为什么使用 Executor 框架比直接创建线程要好?
    10. Java 中 Executor 和 Executors 的区别?答案
    11. 在 windows 和 linux 系统上分别如何找到占用 CPU 最多的线程?