读书笔记——Java多线程编程核心技术

本文详细介绍了Java多线程编程的核心概念和技术,包括进程与线程的区别、线程的创建与管理、线程同步与通信、异常处理等。通过分析线程的状态、生命周期、线程安全问题以及各种同步机制,如synchronized、volatile和Lock,深入探讨了Java多线程编程的实践与最佳实践。此外,还讨论了线程间的协作与通信,如wait、notify和条件变量,以及单例模式在多线程环境下的应用。

第一章 Java多线程技能

     1.1进程/线程/多线程

     进程:受操作系统管理的基本运行单位,即操作系统中正在运行的exe程序。

     线程:在进程中独立运行的子任务。

     1.2

     实现多线程方法:1.继承Thread 2.实现Runnable接口

     1.继承Thread

     Thread.start():通知”线程规划器“此线程已经准备就绪,等待调用线程对象的run()。即让系统安排一个时间来调用run()。使得其具有异步执行效果。

     Thread.run():同步执行,此线程对象不是由”线程规划器“处理,而是由main主线程来调用run(),即必须等run()执行完后才可以执行下面的代码。

     线程对象的构造方法是被main线程调用的,而run方法是被Thread-0的线程调用的,它是自动调用的方法。

     注意在 MyThread t = new MyThread();Thread t = new Thread(c)时,此时得到的t1是Thread.currentThread,是一个新的线程,而this则表示的仍然是MyThread的引用,二者是不一样的。

     1.4 isAlive()方法

     作用:判断当前线程是否处于活动状态。

     活动状态:指线程已经启动且尚未终止,即执行.start()方法后,到线程终止的这一段时间。

     1.5 sleep()方法

     作用:在指定的毫秒数内让当前的”正在执行的线程“休眠(暂停执行)

     1.6 getId()方法

     作用:取得线程的唯一标识

     1.7 停止线程

     1.停止方法:三种,1,使用退出标志,当run方法完成后线程终止;2,使用stop方法强行终止,不推荐,其与suspend,reesume为过期方法,不安全且已废弃;3,使用interrupt方法中断线程。

关于Thread.interrupt:https://www.zhihu.com/question/41048032

若在for循环内,检测到已经intterrupt,并break出循环,但循环外的语句仍会执行,避免此情况,可以在for内主动抛出一个exception来让线程终止。

     1.7.3 能停止的线程--异常法

     虽然线程已停止,但仍会执行下面的语句,此时只要在判断线程已终止后抛出异常,就不会再执行后面的语句了。

     1.7.4 在沉睡中停止

     如果在sleep状态下停止某一线程,会进入catch语句,并且清除停止状态值,使之变成false。

     1.7.5 暴力停止--使用stop()方法

注意:线程的停止应由线程主动完成,而不应由外界来造成。因为可能会导致一些请理性的工作无法完成,以及数据不一致。

关于此方法的危害:https://blog.youkuaiyun.com/jiangwei0910410003/article/details/19900007

     1.7.8 使用return停止线程

return和interrupt()配合使用,但仍建议抛异常+interrupted(),可使得线程停止的事件向上传播

(stop()抛出的ThreadDeath还不用显式捕获,悄无声息地就挂了)

     1.9 yield()方法

向调度程序发出当前线程愿意让出CPU的提示,调度程序可以忽略。  即,此种让CPU的行为时间是不确定的,且可以被忽略。

     1.10 线程优先级

1.setPriority()来设置;2.具有继承性(子类与父类优先级一致)3.高优先级的线程只是意味着有更大机率获得CPU,优先级与代码执行顺序无关。(即随机性,优先级高的线程不一定先执行完)

     1.11 守护线程

当所有非守护线程全部完成,守护线程会自动销毁。

     1.12 Thread.suspend()和Thread.resume()

已废除的方法,容易导致死锁。参考blog的问题:https://blog.youkuaiyun.com/jiangwei0910410003/article/details/19910517

     第二章

      2.1.1 局部变量线程安全

      方法内部的变量是私有的,不存在线程安全的问题。因为其存在栈中,为线程所私有,方法运行完即销毁。  只有实例变量才为线程共有,其存在堆中。

      2.1.6 synchronized锁重入

      1.自己可以再次获取自己的锁。即在一个synchronized方法/块的内部调用本类的其他synchronized方法/块时,是永远可以得到锁的。若不可重入锁,就会造成死锁。

      2.当存在父子类继承关系时,子类完全可以通过“可重入锁”调用父类的同步方法。

      2.1.7 出现异常,锁自动释放

      2.1.8 同步不具有继承性

     2.2 synchronized同步块

      应用场景:用关键字synchronized声明方法,若A线程调用同步方法执行一个长时间任务,则B线程必须较长时间。此时可以用synchronized同步语句块来解决。

      2.2.2 同步代码块的使用(只在必须同步的地方使用)

      当两个并发线程访问同一个对象object中的同步代码块时,一段时间内只能有一个线程被执行,另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。

      2.2.4 一半同步,一半异步

      不在块中的就是异步,在块中的就是同步执行。

     2.2.5 synchronized代码块间的同步性

      当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对同一个object中所有其他synchronized(this)同步代码块的访问将被阻塞,这说明synchronized使用的“对象监视器”是一个。

     2.2.6 synchronized(this)代码块是锁定当前对象的。

     2.2.7 将任意对象作为对象监视器

     多个线程调用同一个对象中的不同名称的同步方法还是同步代码块,调用的效果就是按顺序执行,也就是同步的,阻塞的。Java还支持对“任意对象”作为“对象监视器”来实现同步的功能。这个“任意对象”大多是实例变量及方法的参数,使用格式为synchronized(非this对象)。

     作用:1.在多个线程持有“对象监视器”为同一个对象的前提下,同一时间只有一个线程可以执行synchronized(非this对象x)同步代码块中的代码。

                2.当持有“对象监视器”为同一个对象的前提下,同一时间只有一个线程可以执行synchronized(非this对象x)同步代码块中的代码。

     优点:1.如果一个类中有许多个synchronized方法,若使用同步代码块锁非this对象,则synchronized(非this)代码块中的程序与同步方法是异步的,不与其他锁this同步方法争抢this锁,提高运行效率。

                2.解决脏读问题:同步代码块放在非同步synchronized方法中进行声明,并不能保证调用方法的线程的执行同步/顺序性,即线程调用方法是无序的,这样很容易出现”脏读“问题,而使用它可以解决此类问题。

     2.8.2 三个结论

     1.当多个线程同时执行synchronized(x){}同步代码块时呈同步效果。

     2.当其他线程执行x对象中synchronized同步方法时呈同步效果。

     3.当其他线程执行x对象方法里面的synchronized(this)代码块时也呈现同步效果。

     以上三个结论呈现的效果与将synchronized关键字加到非static方法上使用的效果是一样的。

     ps:synchronized关键字加到static方法上是给Class类上锁(作用与同步synchronized(class)一样),加到非static方法上是给对象上锁。

     2.2.10 数据类型String的常量池特性

     若使用synchronized(str),当字符串相同时,则两个线程持有相同的锁,则会造成后一个线程不能执行。因此大多数情况下,synchronized代码块都不使用String作为锁对象,而改用其他,比如new Object()实例化一个Object对象,但它并不放入缓存中。(可查看书本104例子)

     2.2.11 同步synchronized方法无限等待与循环

     可改用同步块来解决。
     2.2.12 多线程的死锁

     造成死锁的可能情形:synchronized嵌套使用

     2.2.13 内置类与静态内置类

     若PrivateClass为PublicClass的内置类,若想要实例化内置类,则要使用如下代码:

     PrivateClass privateClass = publicClass.new PrivateClass();

     2.2.14 内置类与同步

     1.若内置类中有两个同步方法,但使用的为不同的锁,则为异步操作。

     2.同步代码块synchronized(class2)对class2上锁后,其他线程只能以同步的方式调用class2中的静态同步方法。

     2.2.16 锁对象的改变

     在将任何数据类型作为同步锁时,需要注意的是,是否有多个线程同时持有锁对象,如果同时持有相同的锁对象,则这些线程之间就是同步的;如果是分别获得锁对象,这些线程之间就是异步的。

     以及,只要对象不变,即使对象的属性被改变,运行的结果还是同步的。

    2.3 volatile 关键字

    主要作用:使得变量在多个线程间可见,即强制从公共堆栈中取得变量的值,而不是从线程私有数据栈中取得变量的值。

    致命缺点:不支持原子性(从内存取数据——>修改数据(脏数据出现)——>把数据放回内存),即volatile可保证数据强制从公共堆中取出,但是若取出后堆中的数据发生了变化,线程私有栈的数据却不会发生变化,此时就会发生数据不同步的事情。

     2.3.2 解决同步死循环

     可用多线程解决。

     2.3.3 解决异步死循环

     volatile关键字。

     volatile缺点:不支持原子性。(即同步性)

     与synchronized对比:1.volatile是线程同步的轻量级实现,故volatile性能肯定比synchronized要好,并且volaile只能修饰变量,而synchronized可以修饰方法以及代码块。随着JDK版本提升,synchronized关键字在执行效率上有很大提升,在开发中使用synchronized的机率是比较大的。

     2.多线程访问volatile不会阻塞,synchronized会阻塞。

     3.volatile能保证数据的可见性,但不能保证原子性,而synchronized可以保证原子性,也可间接保证可见性,因为它会将私有内存和公共内存中的数据做同步。

     4.volatile解决的是变量在多个线程之间的可见性,而synchronized解决的是多个线程之间资源的同步性。

     2.3.5 使用原子类进行i++操作(AtomicInteger)(CAS实现

     注意:在具有逻辑性的操作下原子类也不完全安全,它的输出解决也具有随机性。因为AtomichXX类的操作是原子性的,但是方法和方法之间的调用不是原子的,因此要在方法上加上synchronized关键字来解决。(书本P128例子)

    synchronized的2个特点:保证互斥性和可见性

    第三章 线程间通信

     3.1.1 不使用等待/通知机制实现线程间通信

     弊端:浪费 CPU资源。若轮询时间间隔很小,更浪费CPU资源;若轮询时间间隔很大,可能会取不到想要得到的数据。

     3.1.2 等待/通知机制

    object.wait():让当前线程释放锁并自我阻塞,使用时当前线程必须拥有该对象的对象锁(即必须在同步块/方法内),否则抛出IllegalMonitorException(RuntimeException,不需捕获)。不被notify()的线程会一直处于阻塞状态,即使所需对象的对象锁已经空闲。

    object.notify():随机唤醒一个正在等待该对象锁的线程,使用时也当前线程也必须要拥有该对象的对象锁。注意,其不会马上释放锁,而是等待代码执行完,即退出synchronized代码块(方法)后才会释放锁。

    object.notifyAll():唤醒所有正在等待该对象锁的线程。至于谁来执行,可能是按照优先级,也可能是随机,看具体JVM实现。

     实现:使用wait和notify(只能在同步方法或代码块中使用),wait使线程停止运行,而notify使停止的线程继续运行。一般为一对出现,若获得了某对象锁的wait线程运行完毕,对象已被释放,但没有再次使用notify语句,即使对象已经空闲,还会继续阻塞在wait状态。

     线程状态切换示意图:

参考:https://www.zhihu.com/question/27654579 (曹旭东的回答)

 

     Runnable状态:准备运行的阶段,系统为此线程分配CPU资源

     一.线程进入Runnable状态的5种情况:1.调用sleep()方法后经过的时间超过了指定的休眠时间;

      2.线程调用的阻塞IO已经返回,阻塞方法执行完毕;

      3.线程成功地获得了试图同步的监视器;

      4.线程正在等待某个通知,其他线程发出了通知;

      5.处于挂起状态的线程调用了resume恢复方法。

     二.出现阻塞状态的5种情况:1.线程调用sleep()方法,主动放弃占用的处理器资源;

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

      3.线程试图获得一个同步监视器,但该同步监视器正在被其他线程所持有;

      4.线程等待某个通知;

      5.线程带哦用了suspend方法将该线程挂起。此方法容易导致死锁,尽量避免使用。

     3.1.4 方法wait()调用后锁自动释放;方法notify()调用后要直到同步代码块后锁才释放。

     3.1.5当interrupt方法遇到wait方法

      当线程呈现wait()状态时,调用线程对象的interrupt()方法会出现InterruptedException异常。

     3.1.11 生产者/消费者模式实现                                                                           i/

      模式一:一生产与一消费:在条件判断中使用if,容易出现异常。因为条件改变没有得到及时的响应,多个呈wait()状态的线程被唤醒,执行list.remove(0)(实际此前已经remove了)会出现异常。

                    解决方法:把if改成while        

      模式二:多生产与多消费:易造成假死状态(连续唤醒同类,例如生产者连续唤醒生产者,导致生产者都waiting)

                     解决方法:把notify()改成notifyAll()

      一般来说,使用消费者/生产者模式,容器size()的值不会大于1.(不是很懂?P167)

     3.1.12 通过管道进行线程间通信

      管道流:用于在不同线程间直接传送数据,无需借助于类似临时文件之类的东西。在JDK中提供了4个类来使线程间可以通信:1)PipedInputStream PipedOutputStream 2)PipedReader PipedWriter

      可传递:字节流和字符流

    3.2 方法join的使用

      join()方法的作用:使所属的线程对象x正常执行run()方法中的任务,而使当前线程z进行无限期的阻塞,等待线程x销毁后再继续执行线程z后面的代码,它具有使线程排排队的作用,有点类似sychronized的作用。(P181例子)

      与synchronized区别:join在内部使用的是wait()方法进行等待,而synchronized则使用的是”对象监视器“原理作为同步。

      join()方法与异常:当当前线程对象被中断,则当前线程抛出异常。(join()遇上interrupt()方法)

      join(long)与sleep(long)的区别:

      join(long)中的参数是设定等待的时间。join(long)的功能在内部是使用wait(long)方法实现的,故它具有释放锁的特点。sleep(long)方法不释放锁。

      join()后面的代码提前运行原因:可查看P189 (我看不懂)

     3.3 类ThreadLocal的使用

      1.作用:存储每个线程自己的私有变量,保证线程变量的隔离性。

      2.解决初始值为null:重写initialValue()方法,赋予其初始值。

     3.继承InheritableThreadLocal:

      3.1能够让子线程从父线程中取得值。注意,如果子线程在取得值的同时,主线程将InheritableThreadLocal中的值进行更改,子线程取得的还是旧值。

      3.2 重写childValue(Object parentValue)能够对子线程从父线程中获得值进行进一步的修改;

     

    第四章 Lock的使用

     4.1 使用ReentrantLock类

      1.作用:与Synchronized相同,并且扩展功能上更强大,比如具有嗅探锁定、多路分支通知等功能。

      2.方法:1)使用lock()和unlock()实现同步功能

                    2)使用condition.await()进入waiting状态:使用该方法前要先用lock()获得对象监视器,否则会报错。

                    3)使用多个Condition实现通知部分线程:例如分别创建conditionA,conditionB,A类线程调用包含的conditionA的await()方法,B类则调用包含conditionB的await()。

      3.公平锁和非公平锁(默认情况下,ReentrantLock使用的是非公平锁)

       1. 公平锁:lock = new ReentrantLock(true),线程获取锁的顺序是按照加锁的顺序来分配的。

       2.非公平锁:lock = new ReentrantLock(false),一种获取锁的抢占机制,是随机获得锁的。

       3.方法getHoldCount()、getQueueLength()、getWaitQueueLength()

         1)getHoldCount:获取调用lock()方法的次数

         2)返回正在等待获取锁的线程估计数

         3)getWaitQueueLength():返回等待与此锁顶相关的给定条件Condition的线程估计数。比如有5个线程,每个线程都执行了同一个condition对象的await()方法,则调用此方法时返回的int值是5.

      4.方法hasQueuedThread()、hasQueuedThreads、hasWaiters

       1)hasQueuedThread():查询指定的线程是否正在等待获取此锁定。

       2)hasQueuedThreads():查询是否有线程正在等待获取此锁定。

       3)hasWiters():查询是否有线程正在等待与此锁定有关的condition条件。

      5.方法isFair()、isHeldByCurrentThread()、isLocked()

       1)isFair():判断是否为公平锁

       2)isHeldByCurrentThread():查询当前线程是否保持此锁定。

       3)isLocked():查询此锁定是否由任意线程保持。

      6.方法lockInterruptibly()、tryLock()、tryLock(Long timeout, TimeUnit unit)

       1)lockInterruptibly():如果当前线程未被中断则获取锁定;若中断则出现异常。

       2)tryLock():仅在调用时锁定未被另一个线程保持的情况下,才获取该锁定。

       3)tryLock(Long timeout, TimeUnit unit):如果锁定在给定等待时间内没有被另一个线程保持,且当前线程未被中断,则获取该锁定。

      7.方法awaitUninterruptibly()的使用:直到发出信号前使线程一直保持等待状态,可以解决在await()时遇到interrupt()方法抛异常的情况。

      8.方法awaitUntil()的使用:使得线程在等待时间到达前,可以被其他线程提前唤醒。

     4.2 使用ReentrantReadWriteLock类(读写锁)

      1.作用:类ReentrantLock具有完全互斥排他的效果,统一时间只有一个线程在执行REentrantLock.lock()方法后面的任务,这样做虽然保证了实例变量的线程安全性,但是效率低下。故提供读写锁,在某些不需要操作实例变量的方法中,提高运行速度。

      2.内容:包括两个锁,一个是读操作相关的,称为共享锁;一个是写操作相关的,称为排他锁。多个Thread可以同时获取读锁,只有一个Thread可以获取写锁。只要出现写操作,就是互斥的。

   

   第五章 定时器Timer

     5.1 Timer的使用

      1.作用:设置计划任务,即在指定的时间开始执行某一个任务。封装任务的类为TimeTask类(抽象类),任务要放入其的子类。

      2.schedule(TimerTask task, Date time):

       1)作用:在指定的日期执行某一次任务。Timer默认并非守护线程,它执行完之后线程不会销毁,Timer(true)构造才为守护线程。

       2)计划时间早于当前时间:任务立即被执行

       3)多个TimerTask任务及延时:TimeTask是以队列的方式一个个被顺序执行的,若前面的任务消耗时间较长,则后面的任务会被延迟。

      3.schedule(TimerTask task, Date firstTime, long period):

       1)作用:在指定的日期之后,按指定的间隔周期性地无限循环地执行某一任务。

       二三点与上面的相同。

     4.TimerTask类的coancel()方法:将自身从任务队列中清除。

     5.Timer类的cancel()方法:

      1)作用:将任务队列中的全部任务清空。

      2)注意:有时任务会正常运行下去。因为Timer类中的cancel()方法有时并没有争抢到queue锁,故TimerTask类中的任务正常执行。

     6.schedule(TimerTask task, long delay):以执行该方法当前的时间为参考时间,在此时间基础上延迟指定的毫秒数后执行一次TimeTask任务。

     7.schedule(TimerTask task, long delay, long period):以执行该方法当前的时间为参考时间,在此时间基础上延迟指定的毫秒数,再以某一间隔时间无限次数地执行某一任务。

     8.scheduleAtFixedRate(TimerTask task, Date firstTime, long period):与schedule方法都会按顺序执行,故不用考虑非线程安全的情况。

      与schedule()的区别:

      1)在不延时的情况下,schedule()下一次任务的执行时间参考的是上一次任务的”开始“时的时间。而AtFixedRate()参考的是”结束“的时间。延时的情况没有区别,参考的都是”结束“的时间。

      2)schedule()任务不追赶,而FixRate任务追赶:若任务时间在当前时间之前,则任务时间到当前时间的任务被取消为不追赶。

 

第六章 单例模式与多线程

     6.1 立即加载/"饿汉模式"

      1.含义:”立即加载“即使用类的时候已经把对象创建完毕,常见的实现方法就是new实例化。立即加载/”饿汉模式“是在调用方法前,实例已经被创建了。

     6.2 延迟加载/”懒汉模式“

      1.含义:延迟加载即在调用get()方法时实力才被创建出来,常见的实现方法就是在get()方法中进行new实例化。

      2.缺点:在多线程的环境中,延迟加载代码根本不能实现保持单例的状态。

      3.解决方法:1)使用sychronized关键字;2)使用同步代码块  3)对关键代码进行同步 (效率1=2<3,但是1和2能够解决,3还是无法解决)

      4.真正的解决方法:使用DCL双检查锁机制。DCL也是大多数多线程结合单例模式使用的解决方案。

      5.其他解决方法:使用静态内置类实现单例模式(若遇到序列化对象,使用默认的方式运行得到的还是多例的,解决方法是使用readReslove()方法) P274、使用static代码块实现、使用enum枚举数据类型实现。

 

第七章 拾遗增补

      7.1 线程的状态

       1.查看:在Thread.State中可以查看。共有六种,分别是new、runnable、blocked、waiting、timed waiting、terminated。

       2.线程状态改变的主要原因:调用与线程有关的方法。

       7.1.1 new、runnable、terminated、timed waiting、blocked、waiting

        new:线程实例化后还从未执行start()方法时的状态

        runnable:线程进入运行的状态

        terminated:线程被销毁时的状态

        timed waiting:表示线程执行了Thread.sleep()方法,呈等待状态,等待时间到达,继续向下运行。

        blocked:此状态出现在某一个线程在等待锁的时候。

        waiting:线程执行了Object.wait()后的状态。 

    7.2 线程组

      1.作用:可以批量地管理线程或线程组对象,有效地对线程或线程组对象进行组织。

      2.线程对象关联线程组:

       1)一级关联:即父对象中有子对象,但并不创建子孙对象。(开发中常见)

       2)多级关联:即父对象中有子对象,子对象中再创建子对象。      (不常见)

      3.线程组自动归属特性:在实例化一个ThreadGroup线程组x时如果不指定所属的线程组,则x线程组自动归到当前线程对象所属的线程组中。

      4.获取根线程组:getParent(),JVM的根线程组就是System。

      5.组内的线程批量停止:调用线程组ThreadGroup的interrupt()方法。

      6.使线程具有有序性:书中的做法是给线程编号,若非该轮编号的线程则wait(),该轮编号线程拿到锁则执行run()方法。

    7.4 SimpleDateFormat 非线程安全

      1.解决方法:

       1)创建多个SimpleDateFormat类的实例,每个Thread对应一个,就不会出现线程安全问题了。

       2)使用ThreadLocal:把SimpleDateFormat放进ThreadLocal中,每个线程都放一个。

    7.5 线程中出现的异常

       1.使用t当前自定义Thread类对象.setUncaughtExceptionHandler()方法:可以捕捉出异常的线程信息

       2.使用当前自定义Thread类.setDefaultUncaughtExceptionHandler()方法:对所有线程对象设置异常处理器

    7.6 线程组内处理异常

       1.不处理情况下:一个线程出现异常,不会影响其他线程的运行。

       2.处理情况:自定义MyThreadGroup,在里面重写uncaughtException(Thread t, Throwable e)方法,用this.interrupted()使一个线程出现异常时,其他线程全部停止。注意重写了该方法后,线程组内的线程的run()方法都不要有catch()语句,否则重写的该方法不生效。

    7.7 线程异常处理的传递

  

 

 

     

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值