Java多线程问题总结

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

一: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-使用带有协调机制的独占锁,例如,读写锁。

 

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

线程的三种转储方式:

1-unix:kill -3,输出到/proc//fd/1

2-windows: ctrl+break

3-jstack: jstack >> 输出文件

转储的内容包含:

1-线程的名字;  2-damon,守护线程;  3-prio线程的优先级;   4-tid,java的线程id;

5-nid,线程本地标识;  6-线程的运行状态;  7-当前运行的线程在堆中的地址范围。

可以参考:https://blog.csdn.net/agzhchren/article/details/53727082

 

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

当你调用 start() 方法时,它会新建一个线程然后执行 run() 方法中的代码,真正实现了多线程运行。如果直接调用 run() 方法,并不会创建新线程,方法中的代码会在当前调用者的线程中执行。

 

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

这是有关线程的一个很狡猾的问题。有很多原因会导致阻塞,如果是 IO 阻塞,我认为没有方式可以中断线程(如果有的话请告诉我)。

另一方面,如果线程阻塞是由于调用了 wait()sleep() 或 join() 方法,你可以中断线程,通过抛出 InterruptedException 异常来唤醒该线程。

notify()和notifyAll()方法实现唤醒阻塞线程。

 

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

参考:https://blog.csdn.net/liangyihuai/article/details/83106584

CountDownLatch计数为0的时候就可以打开门闩了。

Cyclic Barrier表示循环的障碍物。

两个类都含有这一个意思:对应的线程都完成工作之后再进行下一步动作,也就是大家都准备好之后再进行下一步。

然而两者最大的区别是,进行下一步动作的动作实施者是不一样的。这里的“动作实施者”有两种,一种是主线程(即执行main函数),对于CountDownLatch,当计数为0的时候,下一步的动作实施者是main函数;

另一种是执行任务的其他线程,后面叫这种线程为“其他线程”,区分于主线程。对于CyclicBarrier,下一步动作实施者是“其他线程”。

import java.util.Random;
import java.util.concurrent.CountDownLatch;

public class CountDownLatchTest {

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(4);
        for(int i = 0; i < latch.getCount(); i++){
            new Thread(new MyThread(latch), "player"+i).start();
        }
        System.out.println("正在等待所有玩家准备好");
        latch.await();
        System.out.println("开始游戏");
    }

    private static class MyThread implements Runnable{
        private CountDownLatch latch ;

        public MyThread(CountDownLatch latch){
            this.latch = latch;
        }

        @Override
        public void run() {
            try {
                Random rand = new Random();
                int randomNum = rand.nextInt((3000 - 1000) + 1) + 1000;//产生1000到3000之间的随机整数
                Thread.sleep(randomNum);
                System.out.println(Thread.currentThread().getName()+" 已经准备好了, 所使用的时间为 "+((double)randomNum/1000)+"s");
                latch.countDown();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
    }
}
运行结果:
正在等待所有玩家准备好
player0 已经准备好了, 所使用的时间为 1.235s
player2 已经准备好了, 所使用的时间为 1.279s
player3 已经准备好了, 所使用的时间为 1.358s
player1 已经准备好了, 所使用的时间为 2.583s
开始游戏
package com.huai.thread;

import java.util.Random;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierTest {
    public static void main(String[] args) {
        CyclicBarrier barrier = new CyclicBarrier(3);
        for(int i = 0; i < barrier.getParties(); i++){
            new Thread(new MyRunnable(barrier), "队友"+i).start();
        }
        System.out.println("main function is finished.");
    }


    private static class MyRunnable implements Runnable{
        private CyclicBarrier barrier;

        public MyRunnable(CyclicBarrier barrier){
            this.barrier = barrier;
        }

        @Override
        public void run() {
            for(int i = 0; i < 3; i++) {
                try {
                    Random rand = new Random();
                    int randomNum = rand.nextInt((3000 - 1000) + 1) + 1000;//产生1000到3000之间的随机整数
                    Thread.sleep(randomNum);
                    System.out.println(Thread.currentThread().getName() + ", 通过了第"+i+"个障碍物, 使用了 "+((double)randomNum/1000)+"s");
                    this.barrier.await();   //Waits until all parties have invoked await on this barrier.等待所有的成员都完成这个动作
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
运行结果:
main function is finished.
队友1, 通过了第0个障碍物, 使用了 1.432s
队友0, 通过了第0个障碍物, 使用了 1.465s
队友2, 通过了第0个障碍物, 使用了 2.26s
队友1, 通过了第1个障碍物, 使用了 1.542s
队友0, 通过了第1个障碍物, 使用了 2.154s
队友2, 通过了第1个障碍物, 使用了 2.556s
队友1, 通过了第2个障碍物, 使用了 1.426s
队友2, 通过了第2个障碍物, 使用了 2.603s
队友0, 通过了第2个障碍物, 使用了 2.784s

 

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

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

 

十六.java守护线程和用户线程的区别

守护线程:指在程序运行的时候在后台提供一种通用服务的线程,比如垃圾回收线程就是一个很称职的守护者,并且这种线程并不属于程序中不可或缺的部分。因 此,当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程。反过来说,只要任何非守护线程还在运行,程序就不会终止。
用户线程:自己创建的线程。比如:new Thread。这就是自己创建了一个线程。
守护线程和用户线程的没啥本质的区别:唯一的不同之处就在于虚拟机的离开:如果用户线程已经全部退出运行了,只剩下守护线程存在了,虚拟机也就退出了。 因为没有了被守护者,守护线程也就没有工作可做了,也就没有继续运行程序的必要了。

十七 线程和进程之间的区别和联系

进程:是系统进行资源分配和调度管理的一个可并发执行的基本单位。

线程:是进程中实施调度和分派的基本单位。

之间的关系:

        1、一个进程可以有多个线程,但至少有一个线程;而一个线程只能在一个进程的地址空间内活动。
        2、资源分配给进程,同一个进程的所有线程共享该进程所有资源。
        3、CPU分配给线程,即真正在处理器运行的是线程。
        4、线程在执行过程中需要协作同步,不同进程的线程间要利用消息通信的办法实现同步。
进程间的通信方式:1-消息传递;2-共享存储;3-管道通讯。
 

十八 多线程的上下文切换

CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再次加载这个任务的状态,从任务保存到再加载的过程就是一次上下文切换

 

十九 一些基本概念

死锁:多线程中最差的一种情况,多个线程相互占用对方的资源的锁,而又相互等对方释放锁,此时若无外力干预。会一直处于阻塞状态。
活锁:和死锁相反,活锁是占用锁但是释放。同样也是没有执行任务。多线程中出现了相互谦让,都主动将资源释放给别的线程使用。
饥饿:线程有优先级别之分,优先级别高的会占用级别低的部分。这样会造成优先级别低的线程无法得到执行。
无锁:对共享资源没有进行加锁。
线程组不是线程安全的。
 

二十 java线程调度算法:

1-抢占式调度:容易引起线程饥饿。
2-协同式调度:容易出现阻塞。

 

二十一 为什么使用Excutor框架比直接创建线程要好

1.new Thread()的缺点

(1) 每次new Thread()耗费性能 
(2) 调用new Thread()创建的线程缺乏管理,被称为野线程,而且可以无限制创建,之间相互竞争,会导致过多占用系统资源,使系统瘫痪
(3) 不利于扩展,比如定时执行、定期执行

2.采用线程池的优点

(1) 重用存在的线程,减少对象创建、销毁的开销,性能佳 
(2) 可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞 
(3) 提供定时执行、定期执行、单线程、并发数控制等功能

 

二十二 Exector、ExectorService、Exectors的区别和联系

Exector是一个大的接口

public interface Executor {
    void execute(Runnable command);
}

ExectorService是继承Exector的接口

public interface ExecutorService extends Executor {
    void shutdown();
    <T> Future<T> submit(Callable<T> task);
    <T> Future<T> submit(Runnable task, T result);
    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException;
}

Executors是一个工具类,类似于Collections

public class Executors {
    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());
        }
         
     public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
        }
}

 

 

posted @ 2019-03-20 19:30  上海小墨子  阅读(253)  评论(0编辑  收藏  举报