读书笔记之《Java多线程编程核心技术》

一、前言

    读书笔记系列主要记录自己看的书籍中的知识点,算是一个归纳整理吧。《Java多线程编程核心技术》这本书主要讲解了Java多线程相关的知识。全书分为7章,下面将记录个人认为每章中重要的知识点。

二、Java多线程的基础

1、进程和线程

进程是资源分配的最小单位,线程是CPU调度的最小单位。直观点理解:对于操作系统来说,一个任务就是一个进程,比如打开一个浏览器就是启动一个浏览器进程,打开两个记事本就启动了两个记事本进程。有些进程不止同时干一件事,比如Word,它可以同时进行打字、拼写检查、打印等事情。在一个进程内部,要同时干多件事,就需要同时运行多个“子任务”,把进程内的这些“子任务”称为线程(Thread)。每个进程至少要做一件事,所以进程里至少要有一个线程。线程是最小的执行单元,而进程由至少一个线程组成。如何调度进程和线程,完全由操作系统决定。

特别注意:多进程和多线程的程序涉及到同步、数据共享的问题。进程之间共享信息可通过TCP/IP协议,线程间共享信息可通过共用内存。线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。进程有独立的地址空间,相互不影响,线程没有自己独立的地址空间(地址空间都是按进程分配的,但在地址空间里有专属于线程的线程栈,地址空间是系统给进程分配的虚拟内存,线程栈是线程自己独有的)。进程的切换比线程的切换开销大。每个进程对应一个JVM实例,多个线程共享JVM里的堆。单核CPU执行多任务,是操作系统轮流让各个任务轮流执行,由于CPU的执行速度实在是太快了,感觉就像所有任务都在同时
执行一样。真正的并行执行多任务只能在多核CPU上实现。

注意:Java采用单线程编程模型,JVM创建主线程,主线程可以创建子线程。

2、Java多线程的几种实现方式

(1)继承Thread类,重写run()方法;
(2)实现Runnable接口,重写run()方法;
(3)通过Callable和FutureTask创建线程;
(4)通过线程池创建线程。

3、sleep()方法

使当前执行的线程休眠(暂时停止执行)指定的毫秒数,线程不会失去对监视器的所有权。休眠时间结束后,进入就绪状态,和其他线程一起竞争CPU的执行时间。注意:wait()是Object里的方法,wait是进入线程等待池等待,出让系统资源,其他线程可以占用CPU。调用wait方法的线程,不会自己唤醒,需要线程调用notify / notifyAll方法唤醒等待池中的所有线程,才会进入就绪队列中等待系统分配资源。sleep方法会自动唤醒,如果时间不到,想要唤醒,可以使用interrupt方法强行打断。

4、停止线程

(1)使用退出标志,使线程正常退出,当run()执行完后,线程终止;
(2)stop()方法强制执行,废弃了,不要用这个方式;
(3)interrupt()方法,该方法是在当前线程打了个停止标记,并不会真的停止线程。

注意:sleep()状态下停止某一个线程,会进入catch语句,并且清除停止状态值,使之变成false。

/**
 * Causes the currently executing thread to sleep (temporarily cease
 * execution) for the specified number of milliseconds, subject to
 * the precision and accuracy of system timers and schedulers. The thread
 * does not lose ownership of any monitors.
 *
 * @param  millis
 *         the length of time to sleep in milliseconds
 *
 * @throws  IllegalArgumentException
 *          if the value of {@code millis} is negative
 *
 * @throws  InterruptedException
 *          if any thread has interrupted the current thread. The
 *          interrupted status of the current thread is
 *          cleared when this exception is thrown.
 */
public static native void sleep(long millis) throws InterruptedException;

5、interrupted()、isInterrupted()

(1)interrupted()方法,测试当前线程是否已经是中断状态,执行后具有将状态标志清除为false的功能;
(2)isInterrupted()方法,测试线程Thread对象是否已经是中断状态,但不清除状态标志。

6、yield()

yield()方法的作用是放弃当前的CPU资源,将它让给其他的任务去占用CPU执行时间。但是放弃的时间不确定,有可能刚刚放弃,马上又获得了CPU时间片。yield让当前线程由“运行状态”进入到“就绪状态”。

7、优先级

操作系统中,线程可以划分优先级,优先级较高的线程得到的CPU资源较多。线程的优先级分为1~10 10个等级,1最低,10最高。注意:优先级和执行顺序具有不确定性和随机性。

8、守护线程

Java线程分两种:用户线程、守护线程。守护线程是一种特殊的线程,当进程中不存在非守护线程了,那么守护线程自动销毁。典型的守护线程就是垃圾回收线程。

三、多线程中对并发访问的控制

1、synchronized关键字

(1)synchronized关键字取得的锁是对象锁(对象锁锁住的是,同样由synchronized修饰的方法或代码段),而不是把一段代码或者方法、函数当作锁。哪个线程先执行带synchronized关键字的方法,哪个线程就持有该方法所属对象的锁Lock,那么其他线程就只能呈现等待状态,前提是多个线程访问的是同一个对象。如果多个线程访问多个对象,那么JVM会创建多个锁。

(2)synchronized关键字拥有锁重入的功能,在使用synchronized时,当一个线程得到一个对象锁后,再次请求此对象锁时是可以再次获得该对象的锁的。在一个synchronized方法/块的内部调用本类的其他synchronized方法/块时,是永远可以得到锁的。当存在父子类继承关系时,子类是完全可以通过“可重入锁”调用父类的同步方法的(注意:同步不可以继承,子类方法中也需要加上synchronized)。当一个线程执行的代码出现异常时,其所持有的锁会自动释放。

注意:只有共享资源的读写访问才需要同步化,如果不是共享资源,那么就没有同步的必要。

注意:A线程先持有object对象的Lock锁,那么B线程可以以异步方式调用object对象中的非synchronized类型的方法;A线程先持有object对象的Lock锁,B线程如果这时也调用object对象的synchronized类型的方法则需等待,也就是同步。

(3)synchronized关键字声明方法的话在某些情况下是有弊端的,比如A线程调用同步方法执行一个长时间的任务,那么B线程就需要等待很长时间。这个时候可以使用同步代码块。当两个并发线程访问同一个对象中的synchronized(this)同步代码块时,一段时间内只能有一个线程被执行,另一个线程需要等待当前线程执行完这个代码块后才可以执行该代码块,但是另一个线程可以访问该对象中的非synchronized(this)同步代码块。

注意:当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对同一个object中所有其他synchronized(this)同步代码块的访问将被阻塞。即synchronized使用的对象监视器是一个。synchronized、synchronized(this)都是锁定当前对象的。

(4)Java还支持将“任意对象”作为“对象监视器”来实现同步,这个“任意对象”大多数是实例变量及方法的参数,使用格式为synchronized(非this对象),锁非this对象具有一定的优点:如果在一个类中有很多个synchronized方法,这时虽然能实现同步,但是会受到阻塞,影响效率;如果使用同步代码块锁非this对象,则synchronized(非this)代码块中的程序与同步方法是异步的,不与其他锁this同步方法争抢this锁,则可以大大提高运行效率。

注意:当多个线程同时执行synchronized(x){}同步代码块时呈同步效果;当其他线程执行x对象中synchronized同步方法时呈同步效果;当其他线程执行x对象方法里面的synchronized(this)代码块时也呈现同步效果。

(5)synchronized关键字加到static静态方法上是给Class类上锁,而synchronized关键字加到非static静态方法上是给对象上锁。(一个是Class锁,一个是对象锁,是会产生异步的)。

Class锁可以对类的所有对象实例起作用,也就是说如果作用在两个实例,那么静态的同步方法还是同步运行。synchronized(类.class)同步代码块的作用和synchronized static方法的作用是一样的。

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

注意:只要对象不变,即使对象的属性改变,结果还是同步的。

2、volatile关键字

volatile关键字的主要作用是使实例变量在多个线程间可见。volatile关键字,强制从公共堆栈中取得变量的值,而不是从线程私有数据栈中取得变量的值。但是volatile关键字不支持原子性。

(1)synchronized和volatile比较

volatile是线程同步的轻量级实现,synchronized是重量级;volatile只能修饰变量,synchronized可以修饰方法以及代码块。
多线程访问volatile不会发生阻塞,而synchronized会发生阻塞。
volatile能保证数据的可见性,但不能保证原子性,而synchronized可以保证原子性,也可以间接保证可见性,它会将私有内存和公有内存中的数据做同步。
volatile解决的是变量在多个线程之间的可见性,而synchronized解决的是多个线程之间访问资源的同步性。

四、线程间的通信、交互

1、等待、通知机制(wait/notify机制)

在调用wait()前,线程必须获得该对象的对象级别锁,即只能在同步方法或同步块中调用wait方法。执行wait()后,当前线程释放锁。方法notify()也要在同步方法或同步块中调用,即在调用前,线程必须获得该对象的对象级别锁。在执行notify()方法后,当前线程不会马上释放该对象锁,呈wait状态的线程也不能马上获得该对象锁,要等到执行notify()方法的线程将程序执行完,即退出synchronized代码块后,当前线程才会释放锁。
总结:wait使线程停止运行,notify使停止的线程继续运行。

wait()方法可以使调用该方法的线程释放共享资源的锁,然后从运行状态退出,进入等待队列,直到被再次唤醒。notify()方法可以随机唤醒等待队列中等待同一共享资源的“一个”线程,并使该线程退出等待队列,进入可运行状态。notifyAll()使所有等待队列中等待同一共享资源的“全部”线程从等待状态退出,进入可运行状态。

wait(long)方法的功能是等待一段时间内是否有线程对锁进行唤醒,如果超过这个时间就自动唤醒。

2、生产者、消费者模式

原理基于wait/notify。特别注意:一些大厂面试会问这个,手写。

3、join()方法

join()的作用是等待线程对象销毁。使用场景举例:主线程创建并启动子线程,子线程运行时间长的话,主线程线运行完,如果主线程想要获取子线程的运行结果,也就是主线程想要等子线程运行完再结束,那么就可以用join()。

方法join的作用是使所属的线程对象x正常执行run()方法中的任务,而使当前线程z进行无限期的阻塞,等待线程x销毁后再继续执行线程z后面的代码。

join()在内部使用wait()方法进行等待,而synchronized关键字使用的是“对象监视器”原理做同步。

4、ThreadLocal

ThreadLocal解决的是变量在不同线程间的隔离性,也就是不同线程拥有自己的值。(可参考:一文带你搞定ThreadLocal原理与使用)。

五、Lock的使用

1、ReentrantLock类

调用ReentrantLock对象的lock()方法获得锁,调用unlock()方法释放锁。

一个Lock对象里面可以创建多个Condition(即监视器对象)实例,线程对象可以注册在指定的Condition中,从而可以选择性的进行线程通知,
调度上更加灵活。在使用notify()/notifyAll()方法进行通知时,被通知的线程由JVM随机选择,但使用ReentrantLock结合Condition可以实现“选择性通知”。

Object中的wait()相当于Condition里的await()。Object里的notify()相当于Condition里的signal()。

2、公平锁、非公平锁

锁Lock分为公平锁、非公平锁。公平锁表示线程获得锁的顺序是按照线程加锁的顺序来分配的,先来先得。非公平锁是一种获得锁的抢占机制,随机获得锁。

3、ReentrantReadWriteLock

类ReentrantLock具有完全互斥排他的效果,即同一时刻只有一个线程在执行ReentrantLock.lock()方法后面的业务。这样保证了实例变量的线程安全性,但是效率低。可以使用读写锁。

读写锁中,读相关操作的锁,称为共享锁;写操作相关的锁,称为排它锁。即读锁之间不互斥,写锁与读锁互斥,写锁与写锁互斥。

五、定时器

这一章都是讲Timer类的。对定时任务感兴趣的可以去研究研究分布式定时任务,实际项目中,一般还是用分布式定时任务多一些。

六、单例模式与多线程

单例设计模式,在实际应用中比较常见。但是结合多线程使用时候,还是需要有很多需要注意的地方。

1、饿汉模式/立即加载

立即加载(饿汉模式)就是使用类的时候已经将对象创建完毕。

public class MyObject {

    private static MyObject myObject = new MyObject();

    private MyObject() {

    }
    
    public static MyObject getInstance() {
        // 缺点:不能有其他实例变量,因为该方法没做同步,可能出现非线程安全问题
        return myObject;
    }
    
}

2、懒汉模式/延迟加载

延迟加载就是在调用get()方法时实例才被创建。

public class MyObject {

    private static MyObject myObject;

    private MyObject() {

    }

    public static MyObject getInstance() {
        // 没做同步,不安全
        if (myObject == null) {
            myObject = new MyObject();
        }
        return myObject;
    }

}

3、双锁检查机制(存在反射攻击问题、序列化问题)

public class MyObject {

    private volatile static MyObject myObject;

    private MyObject() {

    }

    public static MyObject getInstance() {
        if (myObject == null) {
            synchronized (MyObject.class) {
                if (myObject == null) {
                    myObject = new MyObject();
                }
            }
        }
        return myObject;
    }

}

4、使用静态内置类(存在序列化问题)

public class MyObject {

    private static class MyObjectHelper {
        private static MyObject myObject = new MyObject();
    }

    private MyObject() {

    }

    public static MyObject getInstance() {
        return MyObjectHelper.myObject;
    }

}

5、使用static代码块

静态代码块中的代码在使用类的时候就已经执行了,可以利用该特性来实现单例设计模式。

public class MyObject {

    private static MyObject instance = null;
    
    private MyObject() {
        
    }
    
    static {
        instance = new MyObject();
    }
    
    public static MyObject getInstance() {
        return instance;
    }
    
}

6、使用枚举enum (最佳,推荐这种)

枚举enum和静态代码块的特性相似,在使用枚举类时,构造方法会被自动调用,也可以利用该特性实现单例设计模式。

public enum Singleton {

    INSTANCE;

    public Singleton getInstance() {
       return INSTANCE;
    }

}

七、拾漏增补

1、线程的状态

public enum State {
    // 至今尚未启动的线程
    New,
    // 正在JVM中执行的线程
    RUNNABLE,
    // 受阻塞并等待某个监视器锁的线程
    BLOCKED,
    // 无限期的等待另一个线程来执行某一特定操作的线程
    WAITING,
    // 等待另一个线程来执行,取决于指定等待时间的操作的线程
    TIMED_WAITING,
    // 已退出的线程
    TERMINATED;
}

New状态是线程实例化后还未执行start()方法时的状态,Runnable包含Ready和Running,yield就是将线程从Running置为Ready。
wait、join—>WAITING,sleep(time)、wait(time)、join(time)—>TIMED_WAITING

2、线程组 (ThreadGroup)

可以把线程归属到线程组中,线程组中可以有线程对象,也可以有线程组,组里还可以有线程。线程组的作用是批量管理线程或者线程组对象。

线程组有自动归属特性,如果实例化一个ThreadGroup线程组x时,没有指定x所属的线程组,那么x线程组自动归属到当前线程对象所属的线程组里。

JVM的根线程组是system,system没有父线程组。(我们常用的main开始方法,它所在的线程组是main,线程组main的父线程组是system)。

ThreadGroup.interrupt()方法可以将该组中所有正在运行的线程批量停止。

你可能感兴趣的:(读书笔记)