最系统的Java并发编程指南

多线程知识点总结(1)

2018-08-14  本文已影响7人  天渊hyominnLover

1. 多线程编程的好处

程序中启用多个线程并发执行以提高程序的效率,多个线程共享heap memory,创建多个线程充分利用CPU资源,创建多个线程执行任务比创建多个进程要好

2. 用户线程和守护线程

用户线程是用户在java程序中创建的线程,称为用户线程;
守护线程是程序在后台执行且并不会阻止JVM终止的线程,当没有用户线程运行的时候,JVM关闭程序并且推出,但守护线程仍然继续执行;守护线程创建的子线程依然是守护线程

3. 守护线程简介

daemonThread.setDaemon(true);

4. 简述线程生命周期

  1. 新建(New):当程序使用new关键字创建了一个线程之后,该线程就处于新建状态,JVM为其分配内存并初始化成员变量
  2. 就绪(Runnable):调用线程的start()方法后,线程就处于就绪状态,JVM会为其创建方法调用栈和程序计数器,等待调度运行
  3. 运行(Running):就绪中的线程获得了CPU时间片,开始执行run()方法的线程体
  4. 阻塞(Blocked):当发生如下情况时,线程将会进入阻塞状态;从阻塞状态只能进入Runnable状态,无法直接进入Running状态
1 线程调用sleep()方法主动放弃所占用的处理器资源

2 线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞

3 线程试图获得一个同步监视器(即对象的同步锁),但该同步监视器正被其他线程所持有

4 线程中运行的对象调用了wait()方法,线程进入了等待队列,在等待某个通知(notify or notifyALl())

5 程序调用了线程的suspend()方法将该线程挂起。但这个方法容易导致死锁,所以应该尽量避免使用该方法
  1. 死亡(Dead):线程会以如下3种方式结束,结束后就处于死亡状态:
1 run()或call()方法执行完成,线程正常结束

2 线程抛出一个未捕获的Exception或Error

3 直接调用该线程stop()方法来结束该线程——该方法容易导致死锁,通常不推荐使用

线程生命周期示意图:

5. 为什么wait(), notify()和notifyAll()必须在同步方法或者同步块中被调用?并简单描述这三个方法

6. 什么是上下文切换(context-switching)

存储和恢复CPU状态的过程,使得线程执行能够从中断点恢复执行。上下文切换是多任务操作系统和多线程环境的基本特征。

7. yield()方法简介

8. sleep()方法简介

9. 简单介绍Thread对象的join()方法

public final void join() throws InterruptedException Waits for this thread to die. Throws: InterruptedException - if any thread has interrupted the current thread. The interrupted status of the current thread is cleared when this exception is thrown.

意思就是说,join()可以让调用这个方法的线程进入wait状态直到子线程结束

从join()的JDK源码角度来理解:


//这个方法是个同步方法,就是说调用子线程的父线程必须拿到子线程对象的锁才能使join()起作用
public final synchronized void join(long millis) throws InterruptedException {  
    long base = System.currentTimeMillis();  //调用开始时间
    long now = 0;  
  
    if (millis < 0) {  
    //join方法中传入的毫秒值不能为负,否则报错
        throw new IllegalArgumentException("timeout value is negative");  
    }  
    //我们可以看到这里使用了while循环做判断的,然后调用wait方法的,所以说join方法的执行是完全通过wait方法实现的  
    //等待时间为0的时候,就是无限等待,直到线程死亡了(即线程执行完了)  
    if (millis == 0) {  
    //join()传入的毫秒值默认为0
        while (isAlive()) {  
            //只有当子线程为就绪、运行或阻塞状态时返回ture,新建但未start或者死亡状态返回false
            //调用该线程的join方法的父线程拿到锁之后,进入等待队列(wait queue),直到子线程执行结束(即子线程的isAlive()方法返回false)
            wait(0);  
        }  
    } else {  
        //如果父线程调用join()方法时传入了特定的毫秒值
        while (isAlive()) {  
    //同样是子线程状态为就绪、运行或阻塞状态时返回ture
            long delay = millis - now;
            if (delay <= 0) {  
                break;  
            }  
            wait(delay);  
            now = System.currentTimeMillis() - base;
        //父线程在等待队列中先等待delay时间,等delay时间过了就恢复(前提是子线程还没结束)
        }  
    }  
}  

综上所述,父线程调用子线程的join方法目的就是让父线程暂停执行,待子线程结束后再恢复;或者制定join某个时间,当到时间后,不管子线程有没有执行网,父线程都会恢复

10. 如何保证线程安全

11. 同步方法和同步块,哪个是更好的选择?简述两种同步方式的区别

同步块是更好的选择,它可以指定线程需要获取哪个对象的同步锁才能执行对应的方法,不局限于某个具体的同步方法,灵活性较高

  1. 灵活性:同步块不但可以指定获取当前对象的锁才能访问同步代码(synchronized(this)),还可以指定需要获取其他对象的锁才能访问(synchronized(otherObject));
  2. 效率:同步的范围越多,越影响程序执行效率,因此用同步代码块可以尽量缩小影响范围,有时候只需要将一个方法中该同步的地方同步了就行了,比如运算

12. 什么是ThreadLocal?

13. 什么是Thread Group?

ThreadGroup API提供了两个功能:

14. 说说UncaughtExceptionHandler接口

class ExceptionHandler implements UncaughtExceptionHandler  
{  
    @Override  
    public void uncaughtException(Thread t, Throwable e)  
    {  
        System.out.println("==Exception: "+e.getMessage());  
    }  
} 
Thread.setDefaultUncaughtExceptionHandler(new ExceptionHandler());  
Thread thread = new Thread(new Task());  
thread.start();  

15. 什么是Java线程转储(Thread Dump),如何得到它?

16. 什么是死锁(DeadLock)?如何分析和避免死锁?写一个简单的demo描述什么是死锁

两个线程A和B,如果线程A持有锁L并且想获得锁M,线程B持有锁M并且想获得锁L,那么这两个线程将永远等待下去,这种情况就是最简单的死锁形式。JVM中,当一组JAVA线程发生死锁时,这两个线程就永远无法使用了。
例子:

class X{
    public synchronized void doFirst(Y y){
        System.out.println("当前运行:"+Thread.currentThread.getName()+"的doFirst()方法");
        Thread.sleep(1000);
        y.doSecond();
    }

    public synchronized void doSecond(){
        System.out.println("当前运行:"+Thread.currentThread.getName()+"的doSecond()方法");
    }
}

class Y{
    public synchronized void doFirst(X x){
        System.out.println("当前运行:"+Thread.currentThread.getName()+"的doFirst()方法");
        Thread.sleep(1000);
        x.doSecond();
    }

    public synchronized void doSecond(){
        System.out.println("当前运行:"+Thread.currentThread.getName()+"的doSecond()方法");
    }
}

public class Run implements Runnable{
    public int flag;
    static X x = new X(), Y y = new Y();
    
    Run run1 = new Run();
    Run run2 = new Run();
    
    run1.flag = 1;
    run2.flag = 0;

    Thread t1 = new Thread(run1);
    Thread t2 = new Thread(run2);
    
    t1.start();
    t2.start();

    public void run(){
        if(flag == 1){
            x.doFirst(y);
        }
        if(flag == 0){
            y.doFirst(x);
        }
    
    }
}
  1. 控制台输入jps获得当前JVM进程的pid
  2. 输入jstack以及进程pid,打印当前进程堆栈,就可以发现哪些线程处于死锁状态及其等待的同步锁对象id
  1. 尽量让线程每次至多获得一个锁
  2. 设计程序时尽量减小嵌套加锁的情况
  3. 利用Lock功能代替synchronized来获取锁:object.lock.tryLock(),当获取到object对象的锁后才返回true,以此执行同步操作,最后使用object.lock.unLock()方法来手动释放同步锁

17. 什么是线程池?如何创建一个Java线程池?

  1. 降低资源消耗:重复利用已经创建的线程降低线程创建和销毁造成的消耗
  2. 提高相应速度:任务到达时不需要等到线程创建就能立即执行
  3. 提高线程的可管理性
  1. 通过ThreadPoolExecutor来创建线程池
new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, milliseconds,runnableTaskQueue, threadFactory,handler);
  1. 通过execute()方法或者submit()方法来执行任务,均传入Runnable对象,不同之处在于submit()可返回任务执行的结果

  2. 通过shutdown()和shutdownNow()来关闭线程池

18. 简述volatile关键字的作用及其原理

  1. 原子性:类似于数据库事务操作的原子性,某项操作执行的过程中要么成功要么失败,中途不能被其他因素(其他线程)打扰,volatile无法保证程序执行的原子性,仅能通过synchronized等同步方式来实现
  2. 可见性:多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值,volatile就能保证可见性,对应到操作系统内存模型中,当一个线程修改共享变量后他会立即被更新到主内存,其他线程读取该共享变量时会直接读取主内存中的最新数据
  3. 有序性:程序执行的顺序按照代码的先后顺序进行执行;JVM内存模型中,为了提高程序执行效率会对程序进行重排序,涉及volatile修饰变量的操作将不会进行重排序,以此保证了程序执行的有序性,让每个线程获取的变量都是正确的值

JVM底层的volatile机制是采用内存屏障来实现的

19. 什么是Java线程转储(Thread Dump),如何得到它?

线程转储是一个JVM活动线程的列表,它对于分析系统瓶颈和死锁非常有用。有很多方法可以获取线程转储——使用Profiler,Kill -3命令,jstack工具等等。我更喜欢jstack工具,因为它容易使用并且是JDK自带的。由于它是一个基于终端的工具,所以我们可以编写一些脚本去定时的产生线程转储以待分析。

20. lock的AQS机制:

java并发包的lock是基于AQS机制实现并发控制的:

1)AQS基本概念:

java实现的一种锁机制,互斥锁、读写锁、条件产量、信号量、栅栏的都是它的衍生物,主要工作基于CHL队列和voliate关键字修饰的状态符stat,线程通过CAS方式修改lock对象的stat,成功了就是获取锁成功,失败了就进队列等待

2)AQS实现自旋锁:

线程通过while(!cas())的方式不断尝试获取同一把lock锁

3)AQS实现共享锁/互斥锁

基于AQS实现的ReadWritelock能够保证读读共享,读写互斥,写写互斥,共享与独占的区别就在于CHL队列中的线程节点的模式是EXCLUSIVE还是SHARED,当一个线程成功修改了stat状态,表示获取了锁,如果线程所在的节点为SHARED将开始一个读锁传递的过程,从头结点向队列后续节点传递唤醒,直到队列结束或者遇到了EXCLUSIVE的节点,等待所有激活的读操作完成,然后进入到独享模式;如果线程节点本身就是EXCLUSIVE,则没有这个传递唤醒过程

4)AQS实现公平锁/非公平锁

公平锁和非公平锁区别在于,公平锁实现中,当某个线程获取锁时,会查看队列中是否有其他线程,如果有,则加入到队尾阻塞,如果没有则直接尝试获取锁;非公平锁实现中,某线程直接尝试获取锁,不会查看队列情况,因此可以进行插队,如果获取失败再进入队列,非公平锁由于允许插队所以上下文切换少的多,性能比较好,能保证大吞吐量,但是容易出现饥饿问题

上一篇 下一篇

猜你喜欢

热点阅读