多线程知识点总结(1)

1. 多线程编程的好处

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

2. 用户线程和守护线程

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

3. 守护线程简介

  • 守护线程是指在程序运行的时候在后台提供一种通用服务的线程,比如垃圾回收线程;如果用户线程已经全部退出运行了,只剩下守护线程存在,虚拟机也就退出了
  • 在线程启动之前,通过setDaemon(true)方法来将线程设置为守护线程:
daemonThread.setDaemon(true);
  • 守护线程的优先级很低,不能安排重要的任务
  • 当全部用户线程结束时,虚拟机推出,守护线程也自动结束

4. 简述线程生命周期

  • java线程生命周期包括新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5种状态
  • 5个生命周期描述:
  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()必须在同步方法或者同步块中被调用?并简单描述这三个方法

  • java线程并没有适用于所有对象的锁和同步器,因此需要Object类自己拥有这样的方法来保证线程间通信
  • 当前线程调用对象的wait()方法,释放该对象的同步锁,进入该对象的wait queue;该方法必须在对象的同步方法和同步块中被调用;当其他线程调用notify()或notifyAll()时,wait queue中的某个线程或者全部线程被唤醒,进入对象的锁池;唤醒后的线程何时能够获得对象锁,完全凭运气

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

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

7. yield()方法简介

  • 当前线程调用此方法,放弃获取的CPU时间片,由Running状态变回Runnable状态,让系统再次选择线程
  • 实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中
  • yield()方法不会导致线程Blocked
  • 该方法是Thread的静态方法,当前线程调用Thread.yield()使其让出CPU时间片

8. sleep()方法简介

  • 当前线程调用Thread.sleep(long millis)方法强制该线程休眠,进入Blocked状态
  • 线程进入休眠后,不会释放之前已经获得的同步锁
  • 休眠一定时间后线程回到Runnable状态

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

  • 使用场景:如果需要让子线程subThread执行完再结束主线程,那么可以在main方法中直接调用子线程subThread.join()方法,让主线程进入wait状态,让subThread执行完,再重新执行主线程
  • 原理:API中叙述如下:

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. 如何保证线程安全

  • 同步锁
  • 使用原子类(atomic concurrent class):该类位于java.util.concurrent.atomic包中,这些类保证在多线程环境下,当某个线程执行atomic的方法时不会被其他线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完成,才由JVM从等待队列中选择一个线程执行。Atomic类在软件层面上是非阻塞的,它的原子性其实是在硬件层面上借助相关的指令来保证的。使用Atomic类时不再需要人为添加synchronized关键字来保证同步性
  • 实现并发锁
  • 使用volatile关键字
  • 使用不变类
  • 使用线程安全类

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

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

  • 同步方法:通过将某个类的方法声明为同步方法,来保证同一时间只有一个线程能够获得该类实例对象的同步锁,来执行这个同步方法,与此同时,其他线程都无法访问这个同步方法以及同步块,但可以访问这个对象中的非同步部分;同步方法的锁是当前对象的锁

  • 同步块:当一个线程访问object的一个synchronized(this)同步代码块时,它就获得了这个object的对象锁,其他原理跟同步方法一致

  • 两者的差异:

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

12. 什么是ThreadLocal?

  • 为了保证对象的全局变量的线程安全,当不想使用同步时,可以选择使用ThreadLocal类
  • 单个线程使用ThreadLocal对象的get()set()方法去获取他们的默认值或者在线程内部改变他们的值
  • 实现方式:当多个线程需要访问同一个共享变量时,将该变量保存到ThreadLocal对象的ThreadLocalMap映射表中保存为副本,key为保存该变量的线程实例本身,这样就能实现每个线程在使用该共享变量时不受其他线程影响

13. 什么是Thread Group?

ThreadGroup API提供了两个功能:

  • 获取线程组中处于活跃状态线程的列表
  • 为线程设置未捕获异常处理器Uncaught exception handler(此功能在JDK1.5后已经被Thread类的setUncaughtExceptionHandler(UncaughtExceptionHandler eh)方法取代)

14. 说说UncaughtExceptionHandler接口

  • 当单线程的程序发生一个未捕获的异常时我们可以采用try....catch进行异常的捕获,但是在多线程环境中,线程抛出的异常是不能用try....catch捕获的,这样就有可能导致一些问题的出现,比如异常的时候无法回收一些系统资源,或者没有关闭的链接
  • 使用方法:实现UncaughtExceptionHandler接口,重写uncaughtException方法,定制异常捕获后需要进行的操作
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),如何得到它?

  • 线程转储是一个JVM活动线程的列表,它对于分析系统瓶颈和死锁非常有用。有很多方法可以获取线程转储——使用Profiler,Kill -3命令,jstack工具等等

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

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

  • x对象和y对象是线程t1和t2的共享资源,t1需要获取y对象的同步锁执行完y的同步方法后才能释放x对象的同步锁,而t2需要获取x对象的同步锁执行完x的同步方法才能释放y对象的同步锁,因此造成了死锁:
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);
        }
    
    }
}
  • 如何分析死锁:分析死锁需要查看Java应用程序的线程转储,找出那些状态为Blocked的线程和他们等待的资源。每个资源都有一个唯一的id,用这个id我们可以找出哪些线程已经拥有了它的对象锁:
  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. 提高线程的可管理性
  • 使用使用JDK自带的ExecutorService接口及其实现来进行线程池操作:
  1. 通过ThreadPoolExecutor来创建线程池
new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, milliseconds,runnableTaskQueue, threadFactory,handler);
  1. 通过execute()方法或者submit()方法来执行任务,均传入Runnable对象,不同之处在于submit()可返回任务执行的结果

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

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

  • 相比于synchronized,volatile是一个轻量级锁,使用volatile不会引起上下文切换和调度,提高了程序执行效率
  • 通过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实现公平锁/非公平锁

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,907评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,987评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,298评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,586评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,633评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,488评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,275评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,176评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,619评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,819评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,932评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,655评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,265评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,871评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,994评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,095评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,884评论 2 354

推荐阅读更多精彩内容

  • Java8张图 11、字符串不变性 12、equals()方法、hashCode()方法的区别 13、...
    Miley_MOJIE阅读 3,704评论 0 11
  • 进程和线程 进程 所有运行中的任务通常对应一个进程,当一个程序进入内存运行时,即变成一个进程.进程是处于运行过程中...
    小徐andorid阅读 2,808评论 3 53
  • 该文章转自:http://blog.csdn.net/evankaka/article/details/44153...
    加来依蓝阅读 7,353评论 3 87
  • 用潜意识来控制我们的事物,养成习惯。 时间管理的背后是拥有一种好的习惯,
    灵灵薰衣草阅读 198评论 1 0
  • 第一百三十六首 积攒 秋之瞳 我积攒了, 太多太多的大意。 以至于, 晶莹的玻璃...
    山丘qiu阅读 323评论 0 1