[Java核心基础】Java多线程编程详细入门教程

文章目录

java多线程编程

一、java多线程技能

1.1、currentThread()方法

**currentThread()**方法可返回代码正在被那个线程调用。

执行Thread中的run()和start()是有区别的。

1)、thread.run();:立即执run()方法,不启动新的线程。

2)、thread.start();:执行run()方法实际不确定,启动新线程。

1.2、isAlive()方法

isAlive()方法的功能是判断当前线程是否存活状态。

存活状态:

​ 如果线程处于运行准备开始运行的状态,就认为是“存活”状态

1.3、sleep(long millis)方法

sleep()方法的作用是在是定的时间(毫秒)内让当前“正在执行的线程”休眠(暂停执行),这个“正在执行的线程”是指this.currentThread()返回的线程。

如果调用sleep()方法所在的类是Thread类,则可执行下列任意代码:

Thread.sleep(3000);
this.sleep(3000);

如果调用sleep()方法不是在Thread类,则使用:

Thread.sleep(3000);

1.4、sleep(long millis, int nanos)方法

sleep(long millis, int nanos)方法的作用是在指定的毫秒数加指定的纳秒数内让当前正在执行的线程休眠,此操作受到系统计时器和调度程序的精度和准确性的影响。

1.5、StackTraceElement[] getStackTrace()方法

StackTraceElement[] getStackTrace()方法的作用是返回一个表示该线程堆栈跟踪元素数组。如果该线程尚未启动或已经终止,则该方法将返回一个零长度数组。如果返回的数组不是零长度的,则其第一个元素代表堆栈顶,它是该数组中最新的方法调用。最后一个元素代表堆栈底,是数组中最旧的方法调用。

1.6、static void dumpStack()方法

static void dumpStack()方法的作用是将当前线程的堆栈跟踪信息输出至标准错误流。该方法仅用于调试。

运行结果如下:

在这里插入图片描述

1.7、static Map方法

static Map<Thread,StackTraceElement[]> getAllStackTraces()方法的作用是返回所有活动线程的堆栈跟踪映射。每个线程的堆栈跟踪仅代表一个快照,并且每个堆栈跟踪都可以在不同时间获得。

1.8、getId()方法

getId()方法用于取得线程的唯一标识。main线程id为1。

1.9、线程停止

在java中有3钟方法可以使线程正常退出。

  1. 使用退出标志使线程正常退出。
  2. 使用stop()方法强行终止线程,但是这个方法不推荐使用,因为stop()suspend()、~~resume()~~一样,都是作废过期的方法,使用他们可能发生不可预料的结果。
  3. 使用interrupt()方法终端线程。

1.9.1、this.interrupted()、this.isInterrupted()

this.interrupted():测试当前线程是否已经是终端状态,执行后具有清除状态标志值为false的功能。

this.isInterrupted():测试线程Thread对象是否已经是中断状态,不清除状态标志。

1.9.2、停止线程-异常法

使用thread.interrupt()方法中断线程,在线程run()方法内添加this.interrupted()方法判断是否中断状态,当为中断状态则抛出异常throw new InterruptedException();

public class MyThread extends Thread{
    
    
    public void run(){
    
    
        try{
    
    
            for(int i = 0; i < 5000000; i++){
    
    
                if(this.interrupted()){
    
    
                    throw new InterruptedException();
                }
            }
        }catch(InterruptedException e){
    
    
            ...
        }
    }
}

public class Run{
    
    
    public static void main(String[] args){
    
    
        try{
    
    
            MyThread thread = new MyThread();
            thread.start();
            Threrad.sleep(1000);
            thread.interrupt();
        }catch(InterruptedException e){
    
    
            ...
        }
    }
}

1.9.3 在sleep状态下停止线程

如果线程在sleep状态下使用interrupt()停止,则该线程会进入catch语句,并且清楚停止状态值,变成false。

不管调用顺序,只要interrupt()和sleep()方法碰到一起就会出现异常:

​ 1)、在sleep状态执行interrupt()方法会出现异常;

​ 2)、调用interrupt()方法给线程打了中断的标记,再执行sleep()方法也会出现异常。

1.10、yield()方法

yield()方法的作用是放弃当前的cpu资源,让其他任务去占用cpu执行时间,放弃的时间不确定,有可能刚刚放弃,马上又获得cpu时间片。

1.11、线程的优先级

在曹组偶系统中,线程可以划分优先级,优先级较高的线程得到cpu资源较多,也就是cpu有限执行优先级较高的线程对象中的任务,其实就是让高优先级的线程获得更多的cpu时间片。

设置线程优先级有助于“线程规划器”确定在下一次选择那一个线程来有限执行。

设置线程优先级使用setPriority()方法。

在java中,线程的优先级分为1~10共10个等级,如果优先级小于1或大于10,则JDK抛出异常 throw new IllegalArgumentException()。

jdk使用3个常量来预置定义优先级的值,代码如下:

public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;

1.11.1、线程优先级的继承特性

在java中,线程的优先级具有继承性,例如,A线程启动B线程,则B线程的优先级与A线程是一样的。

1.11.2、优先级的规律性

高优先级的线程总是大部分先执行完,但不代表高优先级的线程全部先执行完。当线程优先级的等级差距很大时,谁先执行完和代码的调用顺序无关。CPU尽量将执行资源让给优先级比较高的线程。

1.11.3、优先级的随机性

优先级较高的线程并不一定每一次都先执行run()方法中的任务,也就是线程优先级与输出顺序无关,这两者并没有依赖关系,他们具有不确定性、随机性。

1.11.4、优先级对线程运行速度的影响

高优先级的运行速度快(因为分配的cpu片段较多)

1.12、守护线程

java中有两种线程:一种是用户线程,也称非守护线程;另一种是守护线程。

守护线程是一种特殊线程,当进程中不存在非守护线程了,则守护线程自动销毁。典型的守护线程是垃圾回收线程。

犯事调用thread.setDaemon(true)代码并且传入true值的线程才是守护线程。

二、对象及变量的并发访问

本章重要知识点:

  • synchronized对象监视器为Object时的使用方法;

  • synchronized对象监视器为Class时的使用方法;

  • 非线程安全问题是如何出现的;

  • 关键字volatile的主要作用;

  • 关键字volatile与synchronized的区别及使用情况

2.1、synchronized同步方法

关键字synchronized可用来保障原子性、可见性和有序性。

2.1.1、方法内的变量为线程安全

非线程安全问题存在于实例变量中,对于方法内部的私有变量,则不存在非线程安全问题,结果是“线程安全”的。

2.1.2、实例变量非线程安全问题与解决方案

如果多个线程共同访问一个对象中的实例变量,则有可能出现线程安全问题。

用线程访问的对象中如果有多个实例变量,则运行结果有可能出现交叉的情况。(两个成员变量值不是一个线程的赋值)

要解决上述问题,只需要在操作实例变量的方法上添加synchronized关键字即可,例如:

synchronized public void add(String username){
    
    ...}

两个线程同时访问同一个对象中的同步方法时一定是线程安全的。不管哪个线程先运行,这个线程进入用synchronized声明的方法时就上锁,方法执行完成后自动解锁,之后下一个线程才会进入用synchronized声明的方法里,不解锁其他线程执行不了用synchronized声明的方法。

2.1.3、同步synchronized在字节码指令中的原理

在方法中使用synchronized关键字实现同步的原理是使用了flag标记ACC_SYNCHRONIZED,当调用方法时,调用质量会检查方法的ACC_SYNCHONIZED访问标志是否设置,如果设置了,执行线程现持有同步锁,然后执行方法,最后在方法完成时释放锁。

使用javap命令反编译加synchronized标志的方法如下:

javap -c -v Test.class

在这里插入图片描述

在反编译的字节码指令中,对public synchronized void myMethod()方法使用了flag标记ACC_SYNCHRONIZED,说明方法是同步的。

如果使用synchronized代码块,则使用monitorenter和monitorexit指令进行同步处理,反编译后的文件如下:

在这里插入图片描述

同步:按顺序执行A和B这两个业务,就是同步。

异步:执行A业务的时候,B业务也在同时执行,就是异步。

2.1.4、多个对象多个锁

当创建两个业务对象,在系统中产生两个锁,线程和业务对象属于一对一的关系,每个线程执行自己所属业务对象中的同步方法,不存在争抢关系,所以运行结果是异步的,另外,在这种情况下,synchronized可以不需要,因为不会出现非线程安全问题。

只有多个线程执行相同的业务对象中的同步方法时,线程和业务对象属于多对一的关系,为了避免出现非线程安全问题,所以使用了synchronized。

2.1.5、将synchronized方法与对象作为锁

结论:

  1. A线程先持有object对象的Lock锁,B线程可以以异步的方式调用object对象中的非synchronized类型的方法。
  2. A线程先持有object对象的Lock锁,B线程如果在这时调用object对象中的synchronized类型的方法,则需要等待,也就是同步。
  3. 在方法声明处添加synchronized并不是锁方法,而是锁当前类对象。
  4. 在java中只有“将对象作为锁”这种说法,并没有“锁方法”这种说法。
  5. 在java语言中,“锁”就是“对象”,“对象”可以映射成“锁”,哪个线程拿到这把锁,哪个线程就可以执行这个对象中的synchronized同步方法。
  6. 如果在X对象中使用了synchronized关键字生命非金泰方法,则X对象就被当成锁。

2.1.6、脏读

虽然在赋值时进行了同步,但在取值时有可能出现一些意想不到的情况,这种情况就是脏读(dirty read),发生脏读的原因是在读取实例变量时,此值已经被其他线程更改过了。

解决办法就是在读取变量时也使用synchronized同步,如:

public synchronized int getValue();

2.1.7、synchronized锁重入

关键字synchronized拥有重入锁的功能,即在使用synchronized时,当一个线程得到一个对象锁后,再次请求此对象锁时是可以得到该对象锁的,这也证明在一个synchronized方法/块的内部调用本类的其他synchronized方法/块时,是永远可以得到锁的。

2.1.8、锁重入支持继承的环境

锁重入也支持父子类继承的环境。

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

2.1.9、出现异常,锁自动释放

当一个线程执行的代码出现异常时,其所持有的锁会自动释放。

要注意,类Thread中的suspend()方法和sleep()方法被调用后并不释放锁。

2.1.10、重写方法不使用synchronized

重写方法如果不使用synchronized关键字,即是非同步方法,使用后变成同步方法。

2.1.11、public static boolean holdsLock(Object obj)方法的使用

public static native boolean holdsLock(Object obj)方法的作用是当currentThread在指定的对象上保持锁定时,才返回true。

在这里插入图片描述

2.2、synchronized同步语句块

用关键字synchronized声明方法在某些情况下有弊端,例如:A线程调用同步方法执行一个长时间的任务,那么B线程等待的时间就较长,这种情况可以使用synchronized同步语句块来解决,以提高运行效率。

synchronized方法是将当前对象作为锁,而synchronized代码块是将任意对象作为锁。可以将锁看成一个标识,哪个线程持有这个标识,就可以执行同步方法。

2.2.1、synchronized方法的弊端

因为整个方法都是同步的,所以其他线程想要调用共享数据时,需要等待当前线程完全执行方法后才可获得锁,在方法中非共享数据处理部分将浪费很多资源。

2.2.2、synchronized同步代码块的使用

当两个并发线程访问同一个对象object中的synchronized(this)同步代码块时,一段时间内智能有一个线程得到执行,另一个线程必须等待当前线程执行完成以后才能执行代码块。

2.2.3、同步代码块解决同步方法的弊端

当一个线程访问object的synchronized同步代码块时,另一个线程仍然可以访问该object对象中的非synchronized(this)同步代码块。

2.2.4、一半异步,一半同步

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

2.2.5、synchronized代码块间的同步性

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

2.2.6、println()方法也是同步的

在这里插入图片描述

以上两个方法都用到了synchronized(this)同步代码块,说明public void println(String x)方法和public void println(Object x)方法在synchronized(this)同步代码块中执行的方式是按顺序同步执行的。这样输出的数据是完整的,不会出现信息交叉的情况。

2.2.7、验证同步synchronized(this)代码块是锁定当前对象的

2.2.8、将任意对象作为锁

多个线程调用同一个对象中的不同名称的synchronized同步方法或synchronized(this)同步代码块时,调用的效果是按照顺序执行,即同步。

synchronized同步方法或synchronized(this)同步代码块分别有两种作用。

synchronized同步方法的作用:

1)、对其他synchronized同步方法或synchronized(this)同步代码块调用呈同步效果。

2)、同一时间只有一个线程可以执行synchronized同步方法中的代码。

synchronized(this)同步代码块的作用:

1)、对其他synchronized同步方法或synchronized(this)同步代码块调用呈同步效果。

2)、同一时间只有一个线程可以执行synchronized(this)同步代码块中的代码。

除了使用synchronized(this)格式来创建同步代码块,java还支持“任意对象”作为锁来实现同步的功能,这个“任意对象”大多数是实例变量及方法的参数。使用格式为:

synchronized(this对象)

synchronized(非this对象)同步代码块的作用:当多个线程争抢相同的“非this对象x”的锁时,同一时间只有一个线程可以执行synchronized(非this对象)同步代码块中的代码。


锁非this对象具有一定的有点:

如果一个类中有很多个synchronized方法。则这时虽然能实现同步,但影响运行效率,如果使用同步代码块锁非this对象,则synchronized(非this)代码块中的程序与同步方法是异步的,因为有两把锁,不与其他锁this同步方法争抢this锁,可大大提高运行效率。

2.2.9、多个锁就是异步执行

使用“synchronized(非this对象x)同步代码块”格式进行同步操作时,锁必须是同一个,如果不是同一个锁,则运行结果就是异步调用,交叉运行。

2.2.10、验证方法被调用是随机的

2.2.11、不同步导致的逻辑错误及其解决方法

2.2.12、细化验证3个结论

synchronized(非this对象x)格式的写法是将x对象本身作为“对象监视器”,这样就可以分析出三个结论:

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

需要注意,如果其他线程调用不加synchronized关键字的方法,则还是异步调用

// 数据存储类
public class MyOneList {
    
    
    private List<Object> list = new ArrayList<>();
    synchronized public void add(Object o){
    
    
        list.add(o);
    }
    synchronized public int getSize(){
    
    
        return list.size();
    }
}
// 业务处理类
public class MyService {
    
    
    public MyOneList addServiceMethod(MyOneList list, Object data) {
    
    
        // 虽然MyOneList中的方法都是同步方法,但是被传入的参数有可能在外部被修改
        // 所以在业务处理部分也需要对list对象进行同步。否则将还是异步的。
        synchronized (list) {
    
    
            if (list.getSize() < 1) {
    
    
                Thread.sleep(2000);
                list.add(data);
            }
        }
        return list;
    }
}

2.2.13、类Class的单例性

每一个*.java文件对应的Class类的实例都是一个,在内存中是单例的。(这里要分清类Class和类对象实例的区别

Class类用于描述类的基本信息,包括有多少个字段,有多少个构造方法,有多少个普通方法等,为了减少对内存的高占用率,在内存中只需要存在一份Class类对象就可以了,所以被设计成是单例的。

2.2.14、静态同步synchronized方法与synchronized(class)代码块

关键字synchronized还可以应用在static静态方法上,如果这样写,那是对当前的*.java文件对应的Class类对象进行持锁,Class类的对象是单例的,更具体地说,在静态static方法上使用synchronized关键字生命同步方法时,使用当前静态方法所在的类对应Class类的单例对象作为锁。

2.2.15、同步syn static方法可以对类的所有对象实例起作用

Class锁可以对类的所有对象实例起作用。

2.2.16、同步syn(class)代码块可以对类的所有对象实例起作用

同步synchronized(class)代码块的作用其实和synchronized static 方法的作用一样。

// 同步代码块使用Class锁的使用方式
synchronized(Object.class){
    
    
    ...
}

2.2.17、String常量池特性与同步相关的问题与解决方案

JVM具有String常量池的功能。

当将synchronized(string)同步块与String联合使用时,要注意常量池会带来一些意外。

例如:

定义Service类,将String类型的变量定义为锁,

在这里插入图片描述

A、B两个线程调用print()方法时都是使用的"AA"。

在这里插入图片描述

由于String常量池的缘故,"AA"为同一个对象,两个线程持有相同的锁,造成B线程不能执行。

所以大多数情况下,同步synchronized代码块不使用String作为锁对象,而改用其他,例如,new Object()实例化一个新的Object对象,它并不放入缓存池中,或者执行new String()创建不通的字符串对象,形成不同的锁。

2.2.18、同步synchronized方法无限等待问题与解决方案

使用同步方法会导致锁资源被长期占用,得不到运行机会。

这时候可以使用同步代码块来解决

// 产生的等待的同步方法
// 当有两个线程要分别访问methonA、methonB
// 由于对象被锁,导致methonB一直在等待methonA释放锁
synchronized public void methonA(){
    
    
    boolean isContinueRun = true;
    while(isContinueRun){
    
    }
}
synchronized public void methonB(){
    
    
    ...
}
// 解决方法无限等待,使用同步代码块
Object object1 = new Object();
public void methonA(){
    
    
    synchronized(object1){
    
    
        boolean isContinueRun = true;
    	while(isContinueRun){
    
    }
    }
}
Object object2 = new Object();
public void methonB(){
    
    
    synchronized(object2){
    
    
        boolean isContinueRun = true;
    	while(isContinueRun){
    
    }
    }
}

2.2.19、多线程的死锁

Java线程死锁是一个经典的多线程问题,因为不同的线程都在等待根本不可能释放的锁,从而导致所有的任务都无法继续完成。在多线程技术中,“死锁”是必须避免的,因为会造成线程“假死”。

死锁是程序设计的Bug,在设计程序时要避免双方互相持有对方的锁,只要互相等待对方释放锁,就有可能出现死锁。

2.2.20、内置类与静态内置类

2.2.21、内置类与同步:实验1

2.2.22、内置类与同步:实验2

2.2.23、锁对象改变导致异步执行

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

通常情况下,一旦持有锁后就不再对锁对象进行更改,因为一旦更改就有可能出现一些错误。

2.2.24、锁对象不改变已然同步执行

只要对象不变就是同步效果,因为A线程和B线程持有的锁对象永远为同一个,仅仅对象的属性改变了,但对象未发生改变。

2.2.25、同步写法案例比较

使用关键字synchronized的写法常用的有如下几种:

public class MyService{
    
    
    synchronized public static void testMethod1(){
    
    }
    
    public void testMethod2(){
    
    
        synchronized(MyService.class){
    
    }
    }
    
    synchronized public void testMethod3(){
    
    }
    
    public void testMethod4(){
    
    
        synchronized(this){
    
    }
    }
    
    public void testMethod5(){
    
    
        synchronized("abc"){
    
    }
    }
}

上面的代码中出现了3种类型的锁对象:

A)、testMethod1()和testMethod2()持有的锁 是同一个,即MyService.java对应的Class类对象。

B)、testMethod3()和testMethod4()持有的锁是同一个,即MyService.java类的对象。

C)、testMethod5()持有的锁是字符串"abc"。

说明testMethod1()和testMethod2()是同步关系,testMethod3()和testMethod4()是同步关系。A和C之间是异步关系,B和C之间是异步关系,A和B之间是异步关系。

2.3、volatile关键字

volatile在使用上具有一下特征:

1)、可见性:B线程能马上看到A线程更改的数据。

2)、原子性:在32位系统中,针对未使用volatile生命的long或double数据类型没有实现写原子性,如果想实现,则生命变量时添加volatile,而在64位系统中,原子性取决于具体的实现,在X86架构64位JDK版本中,写double或long是原子的。另外,针对volatile声明的int i变量进行i++操作时是非原子的。

3)、禁止代码重排序

2.3.1、可见性的测试

关键字volatile具有可见性,可见性是指A线程更改变量的值后,B线程马上就能看到更改后的变量值,提高了软件的灵敏度。

1、单线程出现死循环

public class PrintString {
    
    
    private boolean isContinuePrint = true;
    public boolean isContinuePrint() {
    
    
        return isContinuePrint;
    }
    public void setContinuePrint(boolean continuePrint) {
    
    
        isContinuePrint = continuePrint;
    }
    public void printStringMethod() {
    
    
        try {
    
    
            while (isContinuePrint) {
    
    
                System.out.println("run printStringMethod Thread-name: " + Thread.currentThread().getName());
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }
}
public class Run {
    
    
    public static void main(String[] args) {
    
    
        PrintString ps = new PrintString();
        ps.printStringMethod();
        System.out.println("我要停止它 stopThread=" + Thread.currentThread().getName());
        ps.setContinuePrint(false);
    }
}

程序停不下来的原因主要是main线程一直在处理while()循环,导致程序不能继续执行后面的代码。可以使用多线程技术解决这种问题。

2、使用多线程解决死循环

将上一个例子的代码改多线程方式运行,如下:

public class PrintString implements Runnable{
    
    
    private boolean isContinuePrint = true;
    public boolean isContinuePrint() {
    
    
        return isContinuePrint;
    }
    public void setContinuePrint(boolean continuePrint) {
    
    
        isContinuePrint = continuePrint;
    }
    public void printStringMethod() {
    
    
        try {
    
    
            while (isContinuePrint) {
    
    
                System.out.println("run printStringMethod Thread-name: " + Thread.currentThread().getName());
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
    
    
        printStringMethod();
    }
}

public class Run {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        PrintString ps = new PrintString();
        Thread thread = new Thread(ps);
        thread.start();
        Thread.sleep(100);
        System.out.println("我要停止它 stopThread=" + Thread.currentThread().getName());
        ps.setContinuePrint(false);
    }
}

可以发现循环可以停下来了。

3、使用多线程有可能出现死循环

public class RunThread extends Thread {
    
    
    private boolean isRuning = true;
    public boolean isRuning() {
    
    
        return isRuning;
    }
    public void setRuning(boolean runing) {
    
    
        isRuning = runing;
    }
    @Override
    public void run() {
    
    
        System.out.println("进入run了");
        while (isRuning) {
    
    
			// 要实现实例要表达的死循环效果,此处为空循环,不要加任何代码
        }
        System.out.println("线程被停止了");
    }
}
public class Run {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        RunThread runThread = new RunThread();
        runThread.start();
        Thread.sleep(1000);
        System.out.println("我要停止它 stopThread=" + Thread.currentThread().getName());
        runThread.setRuning(false);
    }
}

运行上面代码发现,RunThread线程并没有被停止,而是进入了死循环。

4、使用volatile关键字解决多线程出现的死循环

下面分析下是什么原因造成的死循环。

在启动RunThread线程时,变量“private boolean isRuning=true”存在于公共堆栈及线程的私有堆栈中,线程运行后一直在线程的私有堆栈中取得isRunning的值是true,而代码"thread.setRunning(false)"虽然被执行,更新的确是公共堆栈中的isRunning变量,改为了false,操作的是两块内存地址中的数据,所以线程一直处于死循环的状态,内存结构如图所示。

在这里插入图片描述

这个问题其实是私有堆栈中的值和公共堆栈中的值不同步造成的,解决这样的问题就要使用volatile关键字了,其主要的作用是当线程访问isRunning这个变量时,强制从公共堆栈中进行取值。

更改代码:

volatile private boolean isRuning = true;

通过使用volatile关键字,可以强制从公共内存中读取变量值

在这里插入图片描述

使用volatile关键字,可以增加实例变量在多个线程之间的可见性。

5、synchronized代码块具有增加可见性的作用

关键字synchronized可以使多个线程访问同一个资源具有同步性,而且具有使线程工作内存中的私有变量与公共内存中的变量同步的特性,即可见性。

2.3.2、原子性的测试

在32位操作系统,针对未使用volatile声明的long或double数据类型没有实现写原子性,如果想实现,则声明变量时添加volatile。在64位系统中,原子性取决于具体实现,在X86架构64位JDK版本中,写double或long是原子的。

另外,volatile关键字最致命的缺点是不支持原子性,也就是多个线程对用volatile修饰的变量i执行i–操作时,i–操作还会被分解成3步,造成非线程安全问题出现。

如果在方法前加入synchronized同步关键字,那么就没必要再使用volatile关键字来声明变量了。

关键字volatile使用的主要场合是在多个线程中可以感知实例变量被更改了,并且可以获得最新的值,也就是可以用于增加可见性/可视性。

关键字volatile提示线程每次从共享内存中去读取变量,而不是从私有内存中读取,这样就保证了同步数据的可见性。但这里需要注意的是,如果修改实例变量中的数据,如i++,即i=i+1,则这样的操作其实并不是一个原子的操作,也就是非线程安全的。表达式i++的操作步骤分解如下:

1)、从内存中取出i的值;

2)、计算i的值;

3)、将i的值写到内存中;

加入在第2步计算i的值时,另外一个线程也修改i的值,那么这个时候就会出现脏数据,解决的办法是使用synchronized关键字,这个知识点在前面介绍过了。所以,volatile本身并不处理int i++操作的原子性。

使用Atomic原子类进行i++操作实现原子性

除了在i++操作时使用synchronized关键字实现同步外,还可以使用AtomicInteger元子类实现原子性。

原子操作是不能分割的整体,没有其他线程能够终端或检查处于原子操作中的变量。一个原子(atomic)类型就是一个原子操作可用的类型,它可以在没有锁(lock)的情况下做到线程安全(thread-safe)。

2.3.3、禁止代码重排序的测试

使用关键字volatile可以禁止代码重排序。

在java程序运行时,JIT(Just-In-Time Compiler,即时编译器)可以动态地改变程序代码运行的顺序,例如,有如下代码:

A代码-重耗时

B代码-轻耗时

C代码-重耗时

D代码-轻耗时

在多线程的环境中,JIT有可能进行代码重排序,重排序后的代码顺序有可能如下:

B代码-轻耗时

D代码-轻耗时

A代码-重耗时

C代码-重耗时

这样做的主要原因是CPU流水线是同时执行这4个指令的,那么轻耗时的代码在很大程度上先执行完,以让出CPU流水线资源给其他指令,所以代码重排序是为了追求更高的程序运行效率。

重排序发生在没有依赖关系时,例如,对于上面的A、B、C、D代码,B、C、D代码不依赖A代码的结果,C、D代码不依赖A、B代码的结果,D代码不依赖A、B、C代码的结果,这种情况下就会发生重排序,如果代码之间有依赖关系,则代码不会重排序。

使用关键字volatile可以禁止代码重排序,例如有如下代码:

/*
	A变量的操作
	B变量的操作
	volatile Z变量的操作
	C变量的操作
	D变量的操作
*/

那么会有4种情况发生:

1)、A、B可以重排序

2)、C、D可以重排序

3)、A、B不可以重排序到Z的后面

4)、C、D不可以重排序到Z的前面

换而言之,变量Z是一个“屏障”,Z变量之前或之后的代码不可以跨越Z变量,这就是屏障的作用,关键字synchronized具有同样的特性。

2.4、总结

关键字synchronized的主要作用是保证同一时刻,只有一个线程可以执行某一个方法,或是某一个代码块,synchronized可以修饰方法及代码块,随着JDK的版本升级,synchronized关键字在执行效率上得到很大提升。它包含三个特征。

​ 1)、可见性:synchronized具有可见性。

​ 2)、原子性、使用synchronized实现了同步,同步实现了原子性,保证被同步的代码段在同一时间只有一个线程在执行。

​ 3)、禁止代码重排序:synchronized禁止代码重排序。

关键字volatile的主要作用是让其他线程可以蛋刀最新的值,volatile只能修饰变量。它包含三个特征:

​ 1)、可见性:B线程能马上看到A线程更改的数据。

​ 2)、原子性:在32位系统中,针对未使用volatile声明的long或double数据类型没有实现写原子性,如果想实现,则声明变量时添加volatile,而在64位系统中,原子性取决于具体实现,在X86架构64位JDK版本中,写double或long是原子的。另外,针对用volatile声明的int i变量进行i++操作时候是非原子的。

​ 3)、禁止代码重排序。

关键字volatile和synchronized的使用场景总结如下:

​ 1)、当想实现一个变量的值被更改时,让其他线程能取到最新的值时,就要对变量使用volatile。

​ 2)、当多个线程对同一个对象中的同一个实例变量进行操作时,为了避免出现非线程安全问题,就要使用synchronized。

三、线程间通信

本章围绕下列问题进行讲解:

  • 如何使用wait/notify实现线程间通信;
  • 生产者/消费者模式的实现;
  • join方法的使用;
  • ThreadLocal类的使用;

3.1、wait/notify机制

一般情况下我们会使用while(true)无限循环来轮询数据和检测条件进行消费,但这产生了一个问题。如果轮询的时间间隔很小,则更浪费CPU资源;如果轮询的时间间隔很大,则有可能取不到想要的数据;

所以需要引入一种机制——wait/notify(等待/通知)机制,以减少CPU资源浪费,还可以实现在多个线程间随时通信。

3.1.2、wait/notify机制

wait/notify机制在生活中比比皆是,例如在就餐时就会出现,如图:

在这里插入图片描述

厨师和服务员的交互发生在“菜品传递台”上,在这期间考虑以下几个问题。

​ 1)、厨师做完一个菜的时间未定,所以厨师将菜品放到“菜品传递台”上的时间也未定。

​ 2)、服务员取到菜的时间取决于厨师,所以服务员就有“等待”(wait)状态。

​ 3)、服务员如何取到菜呢,这取决于厨师,厨师将菜放在“菜品传递台”上,其实相当于一种通知(notify),这时服务员才可以拿到菜并交给就餐者。

在这个过程中出现了wait/notify机制。

3.1.3、wait/notify机制的原理

拥有相同锁的线程才可以实现wait/notify机制。

​ wait()方法是Object类的方法,它的作用是使当前执行wait()方法的线程等待,在wait()所在的代码行处暂停执行,并释放锁,直到接到通知或被中断为止。在调用wait()之前,线程必须获得该对象的对象级别锁,即只能在同步方法或同步块中调用wait()方法。通过通知机制使某个线程继续执行wait()方法后面的代码时,对线程的选择是按照执行 wait()方法的顺序确定的,并需要重新获得锁。如果调用wait()时没有持有适当的锁,则抛出IllegalMonitorStateException,它是RuntimeException的一个子类,因此不需要try-catch语句捕捉异常。

​ notify()方法要在同步方法或同步块中调用,即在调用前,线程必须获得锁,如果调用notify()时没有持有适当的锁,则会抛出IllegalMonitorStateException。该方法用来通知那些可能等待该锁的其他线程,如果有多个线程等待,则按照执行wait()方法的顺序对处于wait状态的线程发出一次通知(notify),并使该线程重新获取锁。需要说明的是,执行notify()方法后,当前线程不会马上释放该锁,呈wait状态的线程也并不能马上获得该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出synchronized同步区域后,当前线程才会释放锁,而呈wait状态的线程才可以获取该对象锁。当第一个获得了该对象锁的wait线程运行完毕后,它会释放该对象锁,此时如果没有再次使用notify语句,那么其它呈wait状态的线程因为没有得到通知,会继续处于wait状态。

​ 总结:wait()方法使线程暂停运行,而notify()方法通知暂停的线程继续运行。

3.1.4、wait()方法的基本使用

public class WaitTest{
    
    
    public static void main(String[] args){
    
    
        try{
    
    
            Object lock = new Object();
            synchronized(lock){
    
    
                ...
                lock.wait();
                ...
            }
        }catch(...){
    
    ...}
    }
}

在没有得到notify()通知时,线程将永远等待下去。

3.1.5、完整实现wait/notify机制

//---ThreadWait.java
public class ThreadWait extends Thread {
    
    
    private Object lock;
    public ThreadWait(Object lock) {
    
    
        this.lock = lock;
    }
    @Override
    public void run() {
    
    
        synchronized (lock) {
    
    
            try {
    
    
                System.out.println("开始 wait time: " + System.currentTimeMillis());
                lock.wait();
                System.out.println("结束 wait time: " + System.currentTimeMillis());
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }
    }
}
//---ThreadNotify.java
public class ThreadNotify extends Thread {
    
    
    private Object lock;
    public ThreadNotify(Object lock) {
    
    
        this.lock = lock;
    }
    @Override
    public void run() {
    
    
        synchronized (lock){
    
    
            System.out.println("开始 notify time: " + System.currentTimeMillis());
            lock.notify();
            System.out.println("结束 notify time: " + System.currentTimeMillis());
        }
    }
}
//---Run.java
public class Run {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        Object lock = new Object();
        ThreadWait threadWait = new ThreadWait(lock);
        ThreadNotify threadNotify = new ThreadNotify(lock);
        threadWait.start();
        Thread.sleep(3000);
        threadNotify.start();
    }
}

输出结果:

开始 wait time: 1598861302530
开始 notify time: 1598861305529
结束 notify time: 1598861305530
结束 wait time: 1598861305530

从运行结果看,3s后线程被通知(notify)唤醒。

3.1.6、使用wait/notify机制实现list.size()等于5时的线程销毁

​ 关键字synchronized可以将任何一个Object对象作为锁来看待,而Java为每个Object都实现了wait()和notify()方法,它们必须用在被synchronized同步的Object的临界区内。通过调用wait()方法可以使处于临界区内的线程进入等待状态,同时释放被同步对象的锁,而notify操作可以唤醒一个因调用了wait操作而处于wait状态中的线程,使其进入就绪状态,被重新唤醒的线程会视图重新获得临界区的控制权,也就是锁,并继续执行临界区内wait之后的代码。如果发出notify操作时没有处于wait状态中的线程,那么该命令会被忽略。

wait()方法可以使调用该方法的线程释放锁,然后从运行状态转换成wait状态,等待被唤醒。

notify()方法按照执行wait()方法的顺序唤醒等待同一锁的“一个”线程,使其进入可运行状态,即notify()方法仅通知“一个”线程。

notifyAll()方法执行后,会按照执行wait()方法相反的顺序一次唤醒全部的线程。

3.1.7、对业务代码进行封装

业务代码要尽量放在Service业务类中进行处理,这样代码更加标准。

3.1.8、线程状态的切换

在这里插入图片描述

​ 之前介绍了与Thread有关的大部分API,这些API可以改变线程对象的状态,可以总结出下面一张线程状态切换示意图:

​ 1)、创建一个新的线程对象后,调用它的start()方法,系统会为此线程分配CPU资源,此时线程处于runnable(可运行)状态,这是一个准备运行的阶段。如果线程抢占到CPU资源,则此线程就处于running(运行)状态。

​ 2)、runnable状态和running状态可互相切换,因为有可能线程运行一段时间后,其他高优先级的线程抢占了CPU资源,这时此线程就从running状态变成runnable状态。

​ 线程进入runnable状态答题分为如下4种情况。

  • 调用sleep()方法后经过的时间超过了指定的休眠时间;
  • 线程成功获得了试图同步的监视器;
  • 线程正在等待某个通知,其他线程发出了通知;
  • 处于挂起状态的线程调用了resume恢复方法。

​ 3)、blocked是阻塞的意思,例如如果遇到了一个I/O操作,此时当前线程由runnable运行状态转成blocked阻塞状态,等待I/O操作的结果。这时操作系统会把宝贵的CPU时间片分配给其他线程,当I/O操作结束后,线程由blocked状态结束,进入runnable状态,线程会继续运行后面的任务。

​ 出现阻塞的情况大体分为以下5种。

  • 线程调用sleep()方法,主动放弃占用的处理器资源。

  • 线程调用了阻塞式I/O方法,在该方法返回前,该线程被阻塞。

  • 线程视图获得一个同步监视器,但该同步监视器正被其他线程所持有。

  • 线程等待某个通知(notify)。

  • 程序调用了suspend()方法将该线程挂起。此方法容易导致死锁,应尽量避免使用该方法。

    4)、run()方法运行结束后进入销毁截断,整个线程执行完毕。

3.1.9、wait()方法:立即释放锁

​ 执行wait()方法后,锁被立即释放。

3.1.10、sleep()方法:不释放锁

​ 如果将wait()方法改成sleep()方法,就获得了同步的效果,因为sleep()方法不释放锁

3.1.11、notify()方法:不立即释放锁

​ 执行notify()方法后,不立即释放锁。必须执行完notify()方法所在的同步synchronized代码块后才释放锁。

3.1.12、interrupt()方法遇到wait()方法

​ 当线程调用wait()方法后,再对该线程对象执行interrupt()方法会出现InterruptedException异常。

(interrupt()中断线程)

​ 总结:

​ 1)、执行完notify()方法后,按照执行wait()方法的顺序唤醒其他线程。notify()所在的同步代码块执行完才会释放对象的锁,其他线程继续执行wait()之后的代码。

​ 2)、在执行同步代码块的过程中,遇到异常而导致线程终止,锁也会被释放。

​ 3)、在执行同步代码块的过程中,执行了锁所属对象的wait()方法,这个线程会释放对象锁,等待被唤醒。

3.1.13、notify()方法:只通知一个线程

​ 每调用一次notify()方法,只通知一个线程进行唤醒,唤醒的顺序与执行wait()方法的顺序一致。

3.1.14、notifyAll()方法:通知所有线程

注意:

​ notifyAll()方法会按照执行wait()方法的倒序一次对其它线程进行唤醒。

​ 唤醒的顺序是正序、倒序、随机,取决于具体的JVM实现,不是所有的JVM在执行notify()都是按照调用wait()方法的正序进行唤醒的,也不是所有的JVM在执行notifyAll()时都是按照调用wait()方法的倒序进行唤醒的,具体的唤醒顺序依赖于JVM的具体实现。

3.1.15、wait(long)方法的基本使用

​ 带一个参数的wait(long)方法的功能是等待某一时间内是否有线程对锁进行notify()通知唤醒。如果超过这个时间则线程自动唤醒,能继续向下运行的前提是再次持有锁。

3.1.16、wait(long)方法自动向下运行需要重新持有锁

​ wait(long)方法想要自动向下运行也要持有锁,如果没有锁则一直在等待,直到持有锁为止。

3.1.17、通知过早的问题与解决方法

​ 如果通知过早,则会打乱程序正常的运行逻辑。

​ 可以增加一些条件判断来确定执行顺序。

3.1.18、wait条件发生变化与使用while的必要性

​ 在使用wait/notify模式时,需要注意wait条件发生变化,容易造成逻辑混乱。

public void sub() {
    
    
        try {
    
    
            synchronized (lock) {
    
    
                if (ValueObject.list.size()  0) {
    
    
                    System.out.println("wait begin thread-name: " + Thread.currentThread().getName());
                    lock.wait();
                    System.out.println("wait end   thread-name: " + Thread.currentThread().getName());
                }
                ValueObject.list.remove(0);
                System.out.println("list size :" + ValueObject.list.size());
            }
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }

在方法中使用了if判断语句,当wait等待再次被唤醒时,当初的if条件可能已经不成立,再做操作会产生错误。

此时可以使用while循环体代替。一般用作多生产者和多消费者时候使用while

synchronized (lock) {
    
    
                while (ValueObject.list.size()  0) {
    
    
                    System.out.println("wait begin thread-name: " + Thread.currentThread().getName());
                    lock.wait();
                    System.out.println("wait end   thread-name: " + Thread.currentThread().getName());
                }
                ValueObject.list.remove(0);
                System.out.println("list size :" + ValueObject.list.size());
            }

3.1.19、生产者/消费者模式的实现

​ wait/notify模式最近点的案例就是生产者/消费者模式,但此模式在使用和是哪个有几种“变形”,还有一些注意事项,但原理都是基于wait/notify的。

1、一生产与一消费:操作值

Data.java

public class Data {
    
    
    public static String data = "";
}

Producer.java

public class Producer {
    
    
    private Object lock;

    public Producer(Object lock) {
    
    
        this.lock = lock;
    }

    public void setData() {
    
    
        try {
    
    
            synchronized (lock) {
    
    
                if (!Data.data.equals("")) {
    
    
                    lock.wait();
                }
                String data = System.currentTimeMillis() + "_" + System.nanoTime();
                System.out.println("set的值是:" + data);
                Data.data = data;
                lock.notify();
            }
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }
}

Consumer.java

public class Consumer {
    
    
    private Object lock;

    public Consumer(Object lock) {
    
    
        this.lock = lock;
    }

    public void getData() {
    
    
        try {
    
    
            synchronized (lock) {
    
    
                if (Data.data.equals("")) {
    
    
                    lock.wait();
                }
                System.out.println("get的值是:" + Data.data);
                Data.data = "";
                lock.notify();
            }
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }
}

ThreadProducer.java

public class ThreadProducer extends Thread {
    
    

    private Producer producer;

    public ThreadProducer(Producer producer) {
    
    
        this.producer = producer;
    }

    @Override
    public void run() {
    
    
        while (true) {
    
    
            try {
    
    
                Thread.sleep(100);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            producer.setData();
        }
    }
}

ThreadConsumer.java

public class ThreadConsumer extends Thread {
    
    
    private Consumer consumer;

    public ThreadConsumer(Consumer consumer) {
    
    
        this.consumer = consumer;
    }

    @Override
    public void run() {
    
    
        while (true) {
    
    
            try {
    
    
                Thread.sleep(100);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            consumer.getData();
        }
    }
}

Run.java

public class Run {
    
    
    public static void main(String[] args) {
    
    
        Object lock = new Object();
        Producer producer = new Producer(lock);
        Consumer consumer = new Consumer(lock);
        ThreadProducer threadProducer = new ThreadProducer(producer);
        ThreadConsumer threadConsumer = new ThreadConsumer(consumer);
        threadProducer.start();
        threadConsumer.start();
    }
}

在这里插入图片描述

2、多生产与多消费:操作值(假死)

​ “假死”的现象其实就是线程进入waiting状态,如果全部线程都进入waiting状态,则程序就不再执行任何业务功能了,整个项目呈停止状态,这在使用生产者/消费者模式时经常遇到。

修改上面实例代码如下:

Producer.java

public class Producer {
    
    
    private Object lock;

    public Producer(Object lock) {
    
    
        this.lock = lock;
    }

    public void setData() {
    
    
        try {
    
    
            synchronized (lock) {
    
    
                while (!Data.data.equals("")) {
    
    
                    System.out.println("生产者"+Thread.currentThread().getName()+"WAITING了");
                    lock.wait();
                }
                System.out.println("生产者"+Thread.currentThread().getName()+"RUNNING了");
                String data = System.currentTimeMillis() + "_" + System.nanoTime();
                Data.data = data;
                lock.notify();
            }
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }
}

Consumer.java

public class Consumer {
    
    
    private Object lock;

    public Consumer(Object lock) {
    
    
        this.lock = lock;
    }

    public void getData() {
    
    
        try {
    
    
            synchronized (lock) {
    
    
                while (Data.data.equals("")) {
    
    
                    System.out.println("消费者"+Thread.currentThread().getName()+"WAITING了");
                    lock.wait();
                }
                System.out.println("消费者"+Thread.currentThread().getName()+"RUNNING了");
                Data.data = "";
                lock.notify();
            }
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }
}

Run.java

public class Run {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        Object lock = new Object();
        Producer producer = new Producer(lock);
        Consumer consumer = new Consumer(lock);

        ThreadProducer[] thProArr = new ThreadProducer[2];
        ThreadConsumer[] thConArr = new ThreadConsumer[2];


        for (int i = 0; i < 2; i++) {
    
    
            thProArr[i] = new ThreadProducer(producer);
            thConArr[i] = new ThreadConsumer(consumer);
            thProArr[i].setName("生产者-" + i);
            thConArr[i].setName("消费者-" + i);
            thProArr[i].start();
            thConArr[i].start();
        }
        Thread.sleep(5000);
        Thread[] threadArr = new Thread[Thread.currentThread().getThreadGroup().activeCount()];
        Thread.currentThread().getThreadGroup().enumerate(threadArr);
        for (int i = 0; i < threadArr.length; i++) {
    
    
            System.out.println(threadArr[i].getName() + "_" + threadArr[i].getState());
        }

    }
}

​ 在代码中确实已经通过wait/notify进行通信了,但不保证notify唤醒的是异类,也许是同类,如“生产者”唤醒“生产者”,或“消费者”唤醒“消费者”这样的情况,如果这样的情况积少成多,就会导致所有的线程都不能继续运行辖区,大家都在等待,都呈waiting状态,程序最后就呈“假死”状态,不能继续运行。

输出日志:

在这里插入图片描述

对日志进行分析:

  1. 生产者1进行生产,生产完毕后发出通知(但此通知属于“通知过早”),并释放锁,准备进入下一次的while循环。
  2. 生产者1进入下一次while循环,迅速再次持有锁,发现产品并没有被消费,所以生产者1呈等待状态。
  3. 生产者2被start()启动,生产者2发现产品还没有被消费,所以生产者2也呈等待状态。
  4. 消费者2被start()启动,消费者2持有锁,将产品消费并发出通知(发出的通知唤醒了第7行生产者1),运行结束后释放锁,等待消费者2进入下次循环。
  5. 消费者进入下一次while循环,并快速持有锁,发现产品并未生产,所以释放锁并呈等待状态。
  6. 消费者1被start()启动,快速持有锁,发现产品并未生产,所以释放锁并呈等待状态。
  7. 由于消费者2在第4行已经将产品进行消费,唤醒了第7行的生产者1顺利生产后释放锁并发出通知(此通知唤醒了第9行的生产者2),生产者1准备进入下一次while循环。
  8. 这时生产者1进入下一次while循环,再次持有锁,发现产品还未消费,所以生产者1也呈等待状态了。
  9. 由于第7行的生产者1唤醒了生产者2,生产者2发现产品还未消费,所以生产者2呈等待状态。

​ 出现假死的极大原因是有可能连续唤醒同类,怎么解决这样的问题?不光唤醒同类,异类也一同唤醒就解决了。

3、多生产与多消费:操作值(解决假死)

​ 解决“假死”问题的方法很简单,将Producer和Consumer中的notify()方法改成notifyAll()方法即可,它的原理就是通知的线程不光包括同类,也包括异类,这样就不至于出现假死的状态了,程序会一直运行下去。

3.1.20、通过管道进行线程间通信——字节流

​ Java语言提供了各种各样的输入/输出流,使我们能很方便的对数据进行操作,其中管道流(pipe stream)是一种特殊的流,用于在不通线程间直接传送数据。一个线程发送数据到输出管道,另一个线程从输入管道中读数据。通过使用管道,实现不通线程间的通信,而无需借助于类似临时文件之类的东西。

​ Java JDK提供了4个类来使线程间可以进行通信,即:

  • PipedInputStream(字节流)
  • PipedOutputStream(字节流)
  • PipedReader(字符流)
  • PipedWriter(字符流)

3.2、join()方法的使用

​ 在很多情况下,主线程创建并启动了子线程,如果子线程要进行大量的耗时运算,主线程往往将早于子线程结束之前结束,这时如果主线程想等待子线程执行完之后再结束,例如,当子线程处理一个数据,主线程要取得这个数据中的值时,就要用到join()方法了。方法join()的作用是等待线程对象销毁。

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

​ join()方法具有使线程排队运行的效果,有些类似同步的运行效果,但是join()方法与synchronized的区别是join()方法在内部使用wait()方法进行等待,而synchronized关键字使用锁作为同步。

3.2.2、join()方法和interrupt()方法出现异常

​ 在使用join()方法的过程中,如果当前线程对象被中断,则当前线程出现异常。

​ join()方法与interrupt()方法如果在同一个线程彼此遇到,则出现异常,不管先后顺序。

3.2.3、join(long)方法的使用

​ x.join(long)方法中的参数用于设定等待的时间,不管x线程是否执行完毕,时间到了并且重新获得了锁,则当前线程会继续向后运行。如果没有重新获得锁,则一直在尝试,知道获得锁为止。

3.3、类ThreadLocal的使用

​ 变量值的共享可以使用public static变量的形式实现,所有的线程都使用同一个public static变量,那如何实现每一个线程都有自己的变量呢?JDK提供的ThreadLocal可用于解决这样的问题。

​ 类ThreadLocal的主要作用是将数据放入当前线程对象中的Map中,这个Map是Thread类的实例变量。类ThreadLocal自己不管理、不存储任何数据,它只是数据和Map之间的桥梁,用于将数据放入Map中,执行流程如下:数据->ThreadLocal->currentThread()->Map。

​ 执行后每个线程中的Map存有自己的数据,Map中的key存储的是ThreadLocal对象,value就是存储的值,每个Thread中的Map值只对当前线程可见,其他线程不可以访问当前线程对象中的Map的值。当前线程销毁,Map随之销毁,Map中的数据如果没有被引用、没有被使用,则随时GC收回。

​ 由于Map中的key不可以重复,所以一个ThreadLocal对象对应一个value,内存存储结构如图:
在这里插入图片描述

3.4、类InheritableThreadLocal的使用

​ 使用类InhertableThreadLocal可使子线程继承父线程的值。

​ 父线程存放值后启动子线程,再次更新InheritableThreadLocal的值,子线程继承的值将不会发生改变,还是旧的值。同样,子线程的InheritableThreadLocal值发生改变,父线程的值也不会改变,仍是旧值。

​ 那么如何让父子线程互相感知数据变化呢?

​ 答案是使用可变数据对象,例如Map,List,或者自定义数据对象。

在使用时需要定义一个中间类,如果在父线程和子线程都使用new InheritableThreadLocal()的话,将无法互相取到值。

public class Tool {
    
    
    public static InheritableThreadLocal<UserInfo> local = new InheritableThreadLocal<>();
}

四、Lock对象的使用

​ 本章使用Lock对象实现同步的效果,Lock对象在功能上比synchronized更加丰富,本章着重掌握如下两个知识点:

  • ReentrantLock类的使用;
  • ReentrantReadWriteLock类的使用;

4.1、使用ReentrantLock类

​ Java多线程可以使用synchronized关键字来实现线程间同步,不过JDK1.5新增加的ReentrantLock类也能达到同样的效果,并且在扩展功能上更加强大,如具有嗅探锁定、多路分支通知等功能。

4.1.1、使用ReentrantLock实现同步

​ 调用ReentrantLock对象的lock()方法获得锁,调用unlock()方法释放锁,这两个方法成对使用。想要实现同步某些代码,把这些代码放在lock()和unlock()之间即可。

public class MyService {
    
    
    private ReentrantLock lock = new ReentrantLock();
    public void testMethod() {
    
    
        lock.lock();
        for (int i = 0; i < 5; i++) {
    
    
            System.out.println("Thread Name :" + Thread.currentThread().getName() + (" " + (i + 1)));
        }
        lock.unlock();
    }
}

创建4个线程来调用testMethod,输出结果如下:

Thread Name :Thread-0 1
Thread Name :Thread-0 2
Thread Name :Thread-0 3
Thread Name :Thread-0 4
Thread Name :Thread-0 5
Thread Name :Thread-1 1
Thread Name :Thread-1 2
Thread Name :Thread-1 3
Thread Name :Thread-1 4
Thread Name :Thread-1 5
Thread Name :Thread-2 1
Thread Name :Thread-2 2
Thread Name :Thread-2 3
Thread Name :Thread-2 4
Thread Name :Thread-2 5
Thread Name :Thread-3 1
Thread Name :Thread-3 2
Thread Name :Thread-3 3
Thread Name :Thread-3 4
Thread Name :Thread-3 5

​ 从输出结果来看,只有当当前线程输出完毕之后将锁释放,其他线程才可以继续抢锁并输出,每个线程内输出的数据是有序的,从1到5,因为当前线程已经持有锁,具有互斥排他性,但线程之间输出的顺序是随机的,即谁抢到锁,谁输出。

4.1.2、多代码块间的同步性

​ 不管在一个方法还是多个方法环境中,那个线程持有锁,哪个线程就执行业务,其他线程只有等待锁被释放时再次争抢,抢到锁就开始执行业务,运行效果和使用synchronized关键字一样。

4.1.3、await()方法的使用与更正

​ 关键字synchronized与wait()、notify()、notifyAll()方法相结合可以实现wait/notify模式,ReentrantLock类也可以实现同样的功能,但需要借助于Condition对象。Condition类是JDK5的技术,具有更好的灵活性,例如,可以实现多路通知功能,也就是在一个Lock对象中可以创建多个Condition实例,线程对象注册在指定的Condition中,从而可以有选择性地进行线程通知,在调度线程上更加灵活。

​ 在使用notify/notifyAll()方法进行通知时,被通知的线程有JVM进行选择,而方法notifyAll()会通知所有的waiting线程,没有选择权,会出现相当大的效率问题,但使用ReentrantLock结合Condition类可以实现“选择性通知”,这个功能是Condition类默认提供的。

​ Condition对象的作用是控制并处理线程状态,它可以使线程呈wait状态,也可以让线程继续运行。

​ 使用ReentrantLock().newCondition()获取Condition。

​ Condition.await()方法:作用是使当前线程在接到通知或被终端之前一直处于等待wait状态。它和wait()方法的作用一样。

public class MyService {
    
    

    private ReentrantLock lock = new ReentrantLock();

    private Condition condition = lock.newCondition();

    public void awaitMethod(){
    
    
        try {
    
    
            lock.lock();
            System.out.println("A");
            condition.await();
            System.out.println("B");
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }finally {
    
    
            lock.unlock();
            System.out.println("释放锁了");
        }
    }
}
public class MyThreadA extends Thread {
    
    
    private MyService service;

    public MyThreadA(MyService service) {
    
    
        this.service = service;
    }

    @Override
    public void run() {
    
    
        service.awaitMethod();
    }
}
public class Run {
    
    
    public static void main(String[] args) {
    
    
        MyService service = new MyService();
        MyThreadA a1 = new MyThreadA(service);
        MyThreadA a2 = new MyThreadA(service);
        MyThreadA a3 = new MyThreadA(service);
        a1.start();
        a2.start();
        a3.start();
    }
}

打印结果:

A
A
A

​ 控制台输出3个字母A,说明调用了Condition对象的await()方法将当前执行任务的线程转换成了wait状态并释放锁。

4.1.4、使用await()和signal()实现wait/notify机制

​ Object类中的wait()方法相当于Condition类中的await()方法。

​ Object类中的wait(long timeout)方法相当于Condition类中的await(long time, TimeUnit unit)方法。

​ Object类中的notify()方法相当于Condition类中的singnal()方法。

​ Object类中的notifyAll()方法相当于Condition类中的signalAll()方法

4.1.5、await()方法暂停线程运行的原理

并发包源码内部执行了Unsafe类中的public native void park(boolean isAbsolute, long time)方法,让当前线程呈暂停状态,方法参数isAbsolute代表是否为绝对时间,方法参数time代表时间值。如果对参数isAbsolute传入true,则第2个参数time时间单位为毫秒;如果传入false,则第2个参数时间单位为纳秒。

4.1.7、通知部分线程——唤醒部分线程

​ 通过定义两个Condition,并在程序中应用不通的Condition可以实现唤醒部分线程,调用Condition_A的signalAll()方法只唤醒Condition_A的wait状态部分。

4.1.10、公平锁与非公平锁

​ 公平锁:采用先到先得的策略,每次获取锁之前都会检查队列里面有咩有排队等待的线程,没有才会尝试获取锁,如果有就将当前线程追加到队列中。

​ 非公平锁:采用“有机会插队”的策略,一个线程获取锁之前要先尝试获取锁而不是在队列中等待,如果获取锁成功,则说明线程虽然是后启动的,但先获得了锁,这就是“作弊插队”的效果。如果获取锁没有成功,那么才将自身追加到队列中进行等待。

4.1.11、public int getHoldCount()方法的作用

​ public int getHoldCount()方法的作用是查询“当前线程”保持此锁定的个数,即调用lock()方法的次数。

4.1.12、public final int getQueueLength()方法的作用

​ public final int getQueueLength()方法的作用是返回正等待获取此锁的线程估计数,例如,这里有5个线程,其中1个线程长时间占有锁,那么调用getQueueLength()方法后,其返回值是4,说明有4个线程同时在等待锁的释放。

4.1.13、public int getWaitQueueLength(Condition condition)方法的作用

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

4.1.14、public final boolean hasQueuedThread(Thread thread)方法的作用

​ public final boolean hasQueuedThread(Thread thread)方法的作用是查询指定的线程是否正在等待获取此锁,也就是判断参数中的线程是否在等待队列中。

4.1.15、public final boolean hasQueuedThreads()方法的作用

​ public final boolean hasQueuedThreads()方法的作用是查询是否有线程正在等待获取此锁,也就是等待队列中是否有等待的线程。

4.1.16、public boolean hasWaiters(Condition condition)方法的作用

​ public boolean hasWaiters(Condition condition)方法的作用是查询是否有线程正在等待与此锁有关的condition条件,也就是是否有线程执行了condition对象中的await()方法而呈等待状态。而public int getWaitQueueLength(Condition condition)方法的作用是返回有多少个线程执行了condition对象中的await()方法而呈等待状态。

4.1.17、public final boolean isFair()方法的作用

​ public final boolean isFair()方法的作用是判断是不是公平锁。

4.1.18、public boolean isHeldByCurrentThread()方法的作用

​ public boolean isHeldByCurrentThread()方法的作用是查询当前线程是否保持此锁。

4.1.19、public boolean isLocked()方法的作用

​ public boolean isLocked()方法的作用是查询此锁是否由任意线程保持,并没有释放。

4.1.20、public void lockInterruptibly()方法的作用

​ public void lockInterruptibly()方法的作用是当某个线程尝试获得锁并且阻塞在lockInterruptibly()方法时,该线程可以被中断。

4.1.21、public boolean tryLock()方法的作用

​ public boolean tryLock()方法的作用是嗅探拿锁,如果当前线程发现锁被其他线程持有了,则返回false,程序继续执行后面的代码,而不是呈阻塞等待锁的状态

public class MyService{
    
    
    public ReentrantLock lock = new ReentrantLock();
    public void waitMethond(){
    
    
        if(lock.tryLock()){
    
    
            System.out.println("获得锁");
        }else{
    
    
            System.out.println("没有获得锁");
        }
    }
}

4.1.22、public boolean tryLock(long timeout, TimeUnit unit)方法的作用

​ public boolean tryLock(long timeout, TimeUnit unit)方法的作用是嗅探拿锁,如果当前线程发现锁被其他线程持有了,则返回false,程序继续执行后面的代码,而不是呈阻塞状态。如果当前线程在指定的timeout内持有了锁,则返回true,超过时间则返回false。

4.1.23、public boolean await(long time, TimeUnit unit)方法的作用

​ public boolean await(long time, TimeUnit unit)方法的作用方法的作用和public final native wait(long timeout)方法一样,都具有自动唤醒线程的功能。

4.1.24、public long awaitNanos(long nanosTimeout)方法的作用

​ public long awaitNanos(long nanosTimeout)方法的作用和public final native wait(long timeout)方法一样,都具有自动唤醒线程的功能。时间单位是纳秒(ns)。

4.1.25、public boolean awaitUnitil(Date deadline)方法的作用

​ public boolean awaitUnitil(Date deadline)方法的作用是在指定的Date结束等待。

4.2、使用ReentrantReadWriteLock类

​ ReentrantLock类具有完全互斥排他的效果,同一时间只有一个线程在执行ReentrantLock.lock()方法后面的任务,这样做虽然保证了同时写实例变量的线程安全,但效率是非常低的,所以JDK提供了一种读写锁——ReentrantReadWriteLock类,使用它可以在进行读操作时不需要同步执行,提升运行速度和效率。

​ 读写锁有两个锁:一个是读操作相关的锁,也称共享锁;另一个是写操作的锁,也称排他锁。

​ 读锁之间不互斥,读锁和写锁互斥,写锁与写锁互斥,因此只要出现写锁,就会出现互斥同步的效果。

​ 读操作是指读取实例变量的值,写操作是指向实例变量写入值。

五、定时器Timer

​ 定时/计划功能在移动开发领域应用较多,如Android技术。定时/计划任务功能在Java中主要使用Timer对象实现,它在内部使用多线程的方式进行处理,所以它和线程技术有非常大的关联。本章重点:

  • 如何实现按指定时间执行任务;
  • 如何实现按指定周期执行任务;

5.1、定时器Timer的使用

​ 在JDK库中,Timer类的主要作用是设置计划任务,即在指定时间开始执行某一个任务。Timer类的方法列表如图所示:

在这里插入图片描述

​ TimerTask类的主要作用是封装任务,该类的结构如图:

在这里插入图片描述

​ 执行计划任务的代码要放入TimerTask的子类中,因为TimerTask是一个抽象类。

5.1.1、schedule(TimerTask task, Date time)方法的测试

​ 该方法作用是在指定日期执行一次某一任务。

1、执行任务的时间晚于当前时间——在未来执行的效果
public class MyTask extends TimerTask {
    
    
    @Override
    public void run() {
    
    
        System.out.println("执行了任务,时间是:" + System.currentTimeMillis());
    }
}
public class Test {
    
    
    public static void main(String[] args) {
    
    
        try {
    
    
            long nowTime = System.currentTimeMillis();
            System.out.println("当前时间为:" + nowTime);
            long scheduleTime = nowTime + 10000;
            System.out.println("计划执行时间为:" + scheduleTime);
            MyTask task = new MyTask();
            Timer timer = new Timer();
            Thread.sleep(1000);
            timer.schedule(task, new Date(scheduleTime));
            Thread.sleep(Integer.MAX_VALUE);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }
}

返回结果:

当前时间为:1599202917487
计划执行时间为:1599202927487
执行了任务,时间是:1599202927487

​ 10s之后任务成功执行,任务虽然执行完了,但进程还未销毁,说明内部还有非守护线程正在执行。

2、TimerThread线程不销毁的原因

​ 进程不销毁的原因是在创建Timer对象时启动了一个新的非守护线程,JDK源码如下:

public Timer() {
    
    
    this("Timer-" + serialNumber());
}

public Timer(String name) {
    
    
    thread.setName(name);
    thread.start();
}

​ 查看构造方法可以得知,创建一个Timer对象时内部就启动了一个新的线程,可以用新启动的这个线程去执行计划任务。TimerThread是线程类,代码如下:

class TimerThread extends Thread {
    
    ...}

​ 这个新启动的线程并不是守护线程,而且一直在运行,一直在运行的原因是新的线程内部有一个死循环。TimerThread.java类中的mainLoop()方法,代码如下:

private void mainLoop() {
    
    
    while (true) {
    
    
        try {
    
    
            TimerTask task;
            boolean taskFired;
            synchronized(queue) {
    
    
                // Wait for queue to become non-empty
                while (queue.isEmpty() && newTasksMayBeScheduled)
                    queue.wait();
                if (queue.isEmpty())
                    break; // Queue is empty and will forever remain; die

                // Queue nonempty; look at first evt and do the right thing
                long currentTime, executionTime;
                task = queue.getMin();
                synchronized(task.lock) {
    
    
                    if (task.state  TimerTask.CANCELLED) {
    
    
                        queue.removeMin();
                        continue;  // No action required, poll queue again
                    }
                    currentTime = System.currentTimeMillis();
                    executionTime = task.nextExecutionTime;
                    if (taskFired = (executionTime<=currentTime)) {
    
    
                        if (task.period  0) {
    
     // Non-repeating, remove
                            queue.removeMin();
                            task.state = TimerTask.EXECUTED;
                        } else {
    
     // Repeating task, reschedule
                            queue.rescheduleMin(
                              task.period<0 ? currentTime   - task.period
                                            : executionTime + task.period);
                        }
                    }
                }
                if (!taskFired) // Task hasn't yet fired; wait
                    queue.wait(executionTime - currentTime);
            }
            if (taskFired)  // Task fired; run it, holding no locks
                task.run();
        } catch(InterruptedException e) {
    
    
        }
    }
}

​ private void mainLoop()方法内部使用while(true)死循环一直执行计划任务,并不退出死循环,但是根据源码的执行流程,只有满足if(queue.isEmpty())条件,才执行break退出while死循环,退出逻辑的核心代码如下:

while (queue.isEmpty() && newTasksMayBeScheduled)
    queue.wait();
if (queue.isEmpty())
    break; // Queue is empty and will forever remain; die

​ 下面梳理下mainLoop()方法的代码逻辑:

  1. 使用while循环对queue.isEmpty() && newTaskMayBeScheduled条件进行判断。
  2. 当&&两端运算结果都为true时,执行wait()方法使当前线程暂停运行,等待被唤醒。
  3. 唤醒线程的时机是执行了public void schedule(TimerTask task, Date date)方法。
  4. 唤醒线程后while继续判断queue.isEmpty() && newTaskMayBeScheduled条件。如果queue.isEmpty() 为true,则说明队列中并没有任务,而且布尔变量newTaskMayBeScheduled的值由true变成false,继续执行下面if语句。
  5. if(queue.isEmpty())中的queue.isEmpty()结果为true,说明队列为空,那么就执行break语句退出while(true)死循环。
  6. 执行public void cancle()方法会使布尔变量newTaskMayBeScheduled的值由true变成false。
  7. 不执行public void cancle(),则变量newTaskMayBeScheduled的值就不会是false,进程一直呈死循环状态,进程不销毁就是这个原因。

public void cancle()的源码如下:

public void cancel() {
    
    
    synchronized(queue) {
    
    
        thread.newTasksMayBeScheduled = false;
        queue.clear();
        queue.notify();  // In case queue was already empty.
    }
}

​ 上面7步就是进程不销毁的原因,以及退出死循环while(true)的逻辑。

3、使用public void cancle()方法实现线程TimerThread销毁

​ Timer类中的public void cancle()方法的作用是终止此计时器,丢弃所有当前已安排的任务。这不会干扰当前正在执行地点任务(如果存在)。一旦终止了计时器,那么它的执行线程也会终止,并且无法根据它安排更多的任务。注意:在此计时器调用的计时器任务的run()方法内部调用此方法,可以确保正在执行的任务是此计时器锁执行的最后一个任务。可以重复调用此方法,但是第二次和后续调用无效。

​ 根据上面源码分析可知,当队列为空,并且newTasksMayBeScheduled的值是false时,退出while(true)死循环,导致TimerThread线程结束运行并销毁。

4、计划时间早于当前时间——立即运行

​ 如果执行任务的时间早于当前时间,则立即执行task任务。

5、在Timer中执行多个TimerTask任务

​ 在Timer中可以执行多个TimerTask任务

public class Test {
    
    
    public static void main(String[] args) {
    
    
        try {
    
    
            long nowTime = System.currentTimeMillis();
            System.out.println("当前时间为:" + nowTime);
            long scheduleTime = nowTime + 10000;
            System.out.println("计划执行时间为:" + scheduleTime);
            MyTask task1 = new MyTask();
            MyTask task2 = new MyTask();
            Timer timer = new Timer();
            Thread.sleep(1000);
            timer.schedule(task1, new Date(scheduleTime));
            timer.schedule(task2, new Date(scheduleTime+1000));
            Thread.sleep(15000);
            timer.cancel();

        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }
}

6、延时执行TimerTask

​ TimerTask以队列的方式逐一按顺序执行,所以执行的时间有可能和预期的时间不一直,因为前面任务有可能小号的时间较长,而后面的任务的运行时间也可能会被延后。

​ 在代码中,long scheduleTime2=nowTime+5000原计划设置任务1和任务2的运行间隔时间为5s,但是由于task1需要用时20s执行完任务,而task1的结束时间就是task2的开始时间,所以task2不再以5s作为参考,而是以20s作为参考,这是因为创建了一个Timer对象,即创建一个TimerThread线程,一个TimerThread线程管理一个队列,队列按顺序运行task任务。

5.1.2、schedule(TimerTask task, Data firstTime, long period)方法的作用

​ 该方法的作用是在指定日期之后按指定的间隔周期无限循环地执行某一任务。

1.计划时间晚于当前时间——在未来执行
2.计划时间早于当前时间——立即运行
3.延时执行TimerTask

​ 加入myTask任务执行时间是5秒

​ schedule(myTask, new Date(System.currentTimeMillis()), 3000)

​ 因为循环执行任务而任务本身时间是5秒,每3秒运行就不成立了,所以循环执行间隔时间将以最长时间的5秒决定。

4.TimerTask类中的cancle()方法

​ TimerTask类中的cancle()方法的作用是将自身从任务队列中清除。

​ TimerTask类中的cancle()方法是将自身从任务队列中消除,其他任务不受影响。

5.Timer类中的cancle()方法

​ 和TimerTask类中的cancle()方法清除自身不通,Timer类中的cancle()方法作用是将任务队列中的全部任务清空。

6.间隔执行Task任务的算法

​ 当队列中有3个任务ABC时,这3个任务执行顺序的算法是每次将最后一个任务放入队列头,再执行队列头中的Task任务的run()方法,算法效果如下:

​ 1)、ABC;

​ 2)、CAB(将C放入AB之前);

​ 3)、BCA(将B放入CA之前);

7.Timer类中的cancle()方法的使用注意事项

​ 调用Timer类中的cancle()方法有时并不一定停止计划任务,即计划任务正常执行。原因就是Timer类中的cancle()方法有时并没有争抢到queue锁,所以TimerTask类中的任务正常执行。

5.1.3、schedule(TimerTask task, long delay)方法的作用

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

5.1.4、schedule(TimerTask task, long delay, long period)方法的作用

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

5.1.5、scheduleAtFixedRat(TimerTask task, Date firstTime, long period)方法的作用

​ schedule()方法和scheduleAtFixedRat()方法的主要区别在于有没有追赶特性。

​ 追赶特性是指,将两个时间段内的时间所对应的Task任务被“弥补”地执行,也就是在指定时间段内的运行次数必须运行完整,这就是Task任务的追赶特性。

6.1、立即加载/饿汉模式

​ 立即加载是指使用类的时候已经将对象创建完毕,常见的实现办法是直接用new实例化。从中文的语境看,立即加载有“着急”“急迫”的意味,所以也称为“饿汉模式”。

​ 在立即加载/饿汉模式中,调用方法前,实例已经被工厂创建了。

public class MyObject {
    
    //饿汉模式
    private static MyObject myObject = new MyObject();
    private MyObject(){
    
    }
    public static MyObject getInstance(){
    
    
        return myObject;
    }
}

6.2、延迟加载/懒汉模式

​ 延迟加载是指调用get()方法时实例才被工厂创建,常见的实现办法是在get()方法中进行new实例化。从中文的语境来看,延迟加载有“缓慢”“不急迫”的意味,所以也称为“懒汉模式”。

6.2.1、延迟加载/懒汉模式解析

​ 在延迟加载/懒汉模式中,调用方法时实例才被工厂创建。

public class MyObject {
    
    //懒汉模式
    private static MyObject myObject;
    private MyObject() {
    
    
    }
    public static MyObject getInstance() {
    
    
        if (myObject  null) {
    
    
            myObject = new MyObject();
        }
        return myObject;
    }
}

6.2.2、延迟加载/懒汉模式的缺点

​ 前面两个实例虽然使用“立即模式”和“延迟模式”实现了单例模式,但在多线程环境中,“延迟加载”示例代码就是错误的,不能实现保持单例的状态。

6.2.3、延迟加载/懒汉模式的多线程解决方案

1、声明synchronized关键字

​ 虽然多个线程可以同时进入getInstance()方法,那么只需要对getInstance()方法声明synchronized关键字即可。

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

​ 方法加入同步synchronized关键字后可以得到相同的实例,但这种方法的运行效率非常低,是同步运行的。下一个线程想要取得对象,则上一个线程必须释放完锁之后,下一个线程才可以继续执行。

2、尝试同步代码块

​ 同步方法用于对方法的整体进行同步,这其运行效率是比较低的。

​ 那么尝试用同步代码块来实现:

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

​ 此方法加入同步synchronized语句块得到相同实例对象,但这种方法的运行效率也非常低,和synchronized同步方法一样是同步运行的。

3、针对某些重要代码进行单独同步

​ 同步代码块可以针对某些重要代码进行单独同步,而其他代码则不需要同步,运行效率完全可以得到大幅提升。

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

​ 虽然相较之前的代码效率有提升,但又无法保证多线程情况下产生的是同一个实例。

4、使用DCL机制

​ 可以使用**DCL(Double-Check Locking,双检查锁)**机制来实现多线程环境中的延迟加载单例模式。

public class MyObject {
    
    
    volatile private static MyObject myObject;
    private MyObject() {
    
    
    }
    public static MyObject getInstance() {
    
    
        if (myObject  null) {
    
    
            synchronized (MyObject.class) {
    
    
                if (myObject  null) {
    
    

                    myObject = new MyObject();
                }
            }
        }
        return myObject;
    }
}

使用volatile修改变量myObject使该变量在多个线程间达到可见性,另外也禁止了myObject=new MyObject()代码重排序,因为myObject=new MyObject()代码在内部分为3个步骤:

1)、memory = allocate();	// 分配对象的内存控件

2)、ctorInstance(memory);	//初始化对象

3)、myObject = memory;	// 设置instance指向刚分配的内存地址

​ JIT编译器有可能将这3个步骤重排序成:

1)、memory = allocate();	// 分配对象的内存控件

2)、myObject = memory;	// 设置instance指向刚分配的内存地址

3)、ctorInstance(memory);	//初始化对象

​ 这时就会出现以下情况:虽然构造方法还没有执行,但myObject对象具有了内存地址,值不是null,当访问myObject对象中的实量时还是数据类型的默认值。

​ 使用DCL机制成功解决了懒汉模式遇到多线程的问题,DCL也是大多数多线程结合单例模式使用的解决方案。

5、DCL使用volatile的必要性

​ 解决代码重排序。

6.3、使用静态内置类实现单例模式

​ DCL可以解决多线程单例模式的多线程安全问题,当然,还可以使用其他办法达到同样效果。

public class MyObject {
    
    
    // 内部类方式
    private static class MyObjectHandle {
    
    
        private static MyObject myObject = new MyObject();
    }
    private MyObject() {
    
    
    }
    public static MyObject getInstance() {
    
    
        return MyObjectHandle.myObject;
    }
}

6.4、序列化与反序列化的单例模式实现

​ 当将单例的对象进行序列化时,使用默认的反序列化行为取出的对象是多例的。

public class SingletonObject implements Serializable {
    
    
    private static final long serialVersionUID = 5234927117683002109L;
    private UserInfo userInfo = new UserInfo();
    private static SingletonObject object = new SingletonObject();

    private SingletonObject() {
    
    
    }

    public static SingletonObject getInstance() {
    
    
        return object;
    }

//    protected Object readResolve(){
    
    
//        System.out.println("调用了readResolve方法");
//        return SingletonObject.object;
//    }
}

​ protected Object readResolve()方法的作用是反序列化时不创建新的SingletonObject对象,而是复用原有的SingletonObject对象。

public class SaveAndReadRun {
    
    
    public static void main(String[] args) {
    
    
        try {
    
    
            SingletonObject singletonObject = SingletonObject.getInstance();
            System.out.println(singletonObject.hashCode());
            ObjectOutput out = new ObjectOutputStream(new FileOutputStream("SingletonObject.dat"));
            out.writeObject(singletonObject);
            out.close();
        } catch (IOException e) {
    
    
            e.printStackTrace();
        }
        try {
    
    
            SingletonObject singletonObject = null;
            ObjectInput input = new ObjectInputStream(new FileInputStream("SingletonObject.dat"));
            singletonObject = (SingletonObject) input.readObject();
            input.close();
            System.out.println(singletonObject.hashCode());
        } catch (IOException | ClassNotFoundException e) {
    
    
            e.printStackTrace();
        }

    }
}

​ 从程序运行结果可以分析,在反序列化时创建了新的SingletonObject对象,内存中产生了两个SingletonObject对象,SingletonObject并不是单例的,但UserInfo对象得到了复用,因为hashcode是相同的,为了实现SingletonObject在内存中一直呈单例的效果,解决变法就是在反序列化的时使用readResolve()方法,对原有的SingletonObject对象进行复用,去掉代码中的注释:

protected Object readResolve(){
    
    
    System.out.println("调用了readResolve方法");
    return SingletonObject.object;
}

​ 再运行可以发现达到了我们要的效果。

​ protected Object readResolve()方法的作用是在反序列化时不创建新的SingletonObject对象,而是复用JVM内存中原有的SingletonObject单例对象,Userinfo对象也被复用,也就实现了对SingletonObject序列化与反序列化时保持单例性的效果。

​ 注意

​ 如果将序列化和反序列化操作分别放入两个class中,则反序列化时会产生新的SingletonObject对象,放在两个class类中分别执行其实相当于创建了两个JVM虚拟机,每个虚拟机里面的确只有一个SingletonObject对象,我们想要实现的是在一个JVM虚拟机中进行序列化与反序列化时保持SingletonObject单例性的效果,而不是创建两个JVM虚拟机。

6.5、使用static代码块实现单例模式

​ 静态代码块中的代码在使用类的时候就已经执行,所以可以应用静态代码块的这个特性来实现单例模式。

public class MyObject {
    
    
    private static MyObject myObject;

    static {
    
    
        myObject = new MyObject();
    }

    private MyObject() {
    
    
    }

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

​ 饿汉模式的一种形式。

6.6、使用enum枚举数据类型实现单例模式

​ enum枚举数据类型的特性和静态代码块的特性相似,在使用枚举类时,构造方法会被自动调用,可以应用这个特性实现单例模式。

public enum EnumSingletonObject {
    
    
    connectionFactory;
    private Connection connection;

    private EnumSingletonObject() {
    
    
        try {
    
    
            System.out.println("调用了构造方法");
            String url = "";
            String username = "";
            String password = "";
            String driverName = "";
            Class.forName(driverName);
            connection = DriverManager.getConnection(url, username, password);
        } catch (ClassNotFoundException e) {
    
    
            e.printStackTrace();
        } catch (SQLException throwables) {
    
    
            throwables.printStackTrace();
        }
    }

    public Connection getConnection() {
    
    
        return connection;
    }
}

6.7、完善使用enum枚举数据类型实现单例模式

猜你喜欢

转载自blog.csdn.net/weixin_54707168/article/details/114153221
今日推荐