由浅到深认识Java语言(34):多线程

该文章Github地址:https://github.com/AntonyCheng/java-notes【有条件的情况下推荐直接访问GitHub以获取最新的代码更新】

在此介绍一下作者开源的SpringBoot项目初始化模板(Github仓库地址:https://github.com/AntonyCheng/spring-boot-init-template【有条件的情况下推荐直接访问GitHub以获取最新的代码更新】& CSDN文章地址:https://blog.csdn.net/AntonyCheng/article/details/136555245),该模板集成了最常见的开发组件,同时基于修改配置文件实现组件的装载,除了这些,模板中还有非常丰富的整合示例,同时单体架构也非常适合SpringBoot框架入门,如果觉得有意义或者有帮助,欢迎Star & Issues & PR!

上一章:由浅到深认识Java语言(33):多线程

42.多线程

单例设计模式

定义

设计模式不是技术,是以前的开发人员为了解决某些问题实现的写代码的经验,所有的设计模式的核心技术就是面向对象;

设计模式定义:共有 23 种设计模式,人们经过反复的推敲,得到一些代码和思路,解决问题的方式上的一些有经验的,科学的,高效的代码组织方式,单例模式是其中的一种;

单例模式定义:

单例模式,是一种常用的软件设计模式,在它的核心结构中只包含一个被称为单例的特殊类,通过单例模式可以保证系统中应用该模式的类只有一个实例,即一个类只有一个对象实例;

创建方法

  • 私有修饰构造方法;

  • 自己创建自己;

    • 单例中创建自己的成员变量,不 new 对象(懒汉式)

    • 单例中创建自己的 new 对象(饿汉式)

  • 方法静态 get() ,返回本类的方法;

    • get() 方法中判断是否为空,为空则 new 一个对象,不为空则返回成员变量名(懒汉式)
    • 直接返回成员变量名(饿汉式)
懒汉式单例模式

这里需要判断该实例变量是否有值,有的话就返回原本有的那个值,这样就不会被重新创建;

Method 类:

package top.sharehome.Bag;

public class Method {
    //构造器私有化
	private Method() {
		
	}
    //静态实例变量
	private static Method a;
    //公共的静态的方法去获得一个本就存在于实例中的静态实例变量
	public static Method getInstance() {
        //判断该实例变量是否有值
		if(a==null) {
			a = new Method();
		}
		return a;
	}
    //测试单例模式是否成功的方法
	public static void testMethod() {
		System.out.println("ok");
	}
}

Demo 类:

package top.sharehome.Bag;

public class Demo {
	public static void main(String[] args) {
		Method a = Method.getInstance();
		a.testMethod();
	}
}

打印效果如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

懒汉式单例模式缺点:若该程序有多条线程,判断时若出现多个用户同时运行程序就有可能会起冲突,造成线程不安全;

懒汉式的安全问题

一个线程判断变量 s=null ,还没有执行 new 对象语句,就被另一个线程抢到 CPU 资源,同时有 2 个线程都进行判断变量,对象被多次创建;

解决方案

创建单例时使用同步代码块对多线程进行限制:

public class Single {
    private Single() {

    }

    private static Single single;

    public static Single getSingle() {
        synchronized (Single.class) {
            if (single == null) {
                single = new Single();
            }
            System.out.println(Math.random());
            return single;
        }
    }
}

但是添加同步会产生性能问题:第一个线程获取锁,创建对象,返回对象;第二个线程调用方法的时候,对象已经有值了,根本就不需要再一次进入同步代码块,可以直接 return ;

所以可以使用**双重判断(DCL)**来提高程序的效率,示例如下:

public class Single {
    private Single() {

    }

    private static volatile Single single; //volatile关键字下节介绍

    public static Single getSingle() {
        if (single == null) {  //判空是为了让滴一个之后的线程没办法进入同步代码块
            synchronized (Single.class) {
                if (single == null) {
                    single = new Single();
                }
                System.out.println(Math.random());
                return single;
            }
        }
    }
}
饿汉式单例模式

直接强制初始化内部的实例中的实例变量,每个用户得到的都只能是初始化的实例结果;

Method 类:

package top.sharehome.Bag;

public class Method {
    //构造器私有化
	private Method() {
		
	}
    //强制初始化实例变量
	private static Method a = new Method();
    //公共的静态的方法去获得一个本就存在于实例中的静态实例变量
	public static Method getInstance() {
		return a;
	}
    //测试单例模式是否成功的方法
	public static void testMethod() {
		System.out.println("ok");
	}
}

Demo 类:

package top.sharehome.Bag;

public class Demo {
	public static void main(String[] args) {
		Method a = Method.getInstance();
		a.testMethod();
	}
}

打印效果如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

饿汉式单例模式缺点:更容易浪费空间;

饿汉式单例模式优点:保证了线程的安全;

关键字volatile

成员变量修饰符,不能修饰其他内容;

  • 关键字作用:
    • 保证被修饰的变量在线程中的可见性;
    • 防止指令重排序;
      • 单例模式中,使用了该关键字,不使用关键字可能就会让线程拿到一个尚未初始化完成的对象(半初始化问题);

可见性示例:

package top.sharehome.Demo;

public class Demo {
    public static void main(String[] args) throws InterruptedException {
        MyRunnable myRunnable = new MyRunnable();

        new Thread(myRunnable).start();

        Thread.sleep(2000);

        //main线程修改变量
        myRunnable.setFlag(false);
    }
}

class MyRunnable implements Runnable {
    private volatile boolean flag = true;  //关键字在这里

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

    private void m() {
        System.out.println("开始执行");
        while (flag) {

        }
        System.out.println("结束执行");
    }


    public void setFlag(boolean flag) {
        this.flag = flag;
    }
}

理论上,如果不加 volatile 关键字,这个程序应该打印 “开始程序” 后两秒再打印 “结束程序”,事实上并不会停止,这是因为计算机内存数据和 CPU 缓存数据的差异造成的,flag 变量创建后被放入了内存当中,但是运行程序是在 CPU 三级缓存上跑,而 flag 只会被引入缓存一次,所以造成了 flag 值在内存和缓存中不一致的问题,即不可见问题,如果加上 volatile 关键字,就能够时刻同步内存与缓存中的变量数据;

指令重排序示例:

package top.sharehome.Demo;

import java.util.HashSet;
import java.util.Scanner;
import java.util.Set;

public class Demo {
    static int a, b, x, y;

    public static void main(String[] args) throws InterruptedException {
        Set<String> stringSet = new HashSet<>();
        while (true) {
            a = 0;
            b = 0;
            x = 0;
            y = 0;
            Thread t1 = new Thread() {
                @Override
                public void run() {
                    x = 1;  //x=1
                    a = y;  //a=0  or  b=1
                }
            };
            Thread t2 = new Thread() {
                @Override
                public void run() {
                    y = 1;  //y=1
                    b = x;  //b=1  or  b=0
                }
            };
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            stringSet.add("a:" + a + ",b=" + b);
            System.out.println(stringSet);
            if (a == 0 && b == 0) {
                break;
            }
        }
    }
}

读程序可知,a 和 b 不可能同时等于零,也就是说这个程序将会是一个死循环,但事实上这个程序非常有可能执行 break,这是因为 CPU 对指令的重排序问题,理论上 CPU 会对一个程序逐行执行,但是那是只有一个线程的情况下,但是一旦出现多个线程,CPU 就会对指令进行重排序,这种排序是一种未知可能,举例来说,该程序两个线程内部类其中一个将代码的执行顺序调换,就会极大可能造成 a 和 b 同时为零的情况;

打印效果如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

线程池ThreadPool

这是线程的缓冲池,目的就是提高效率,线程是内存中的一个独立的方法栈区,但 JVM 没有能力开辟内存,而 start() 方法是一个本地,回合 OS 交互,所以会限制其提高效率的效果;JDK 1.5 开始内置了线程池;

Executors类

  • Executors的静态方法 newFixedThreadPool(int 线程的个数);
    • 方法的返回值 ExecutorService 接口的实现类,该类负责管理线程池中的线程;
  • ExecutorService 接口的方法:
    • submit(Runnable r) 提交线程执行的任务;
    • shutdown() 销毁线程池,慎用!

线程池的使用

示例如下:

package top.sharehome.Demo;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Demo {
    public static void main(String[] args) throws InterruptedException {
        //创建线程池,线程的个数是两个
        ExecutorService es = Executors.newFixedThreadPool(2);
        //调用方法开启线程池里的线程
        es.submit(new MyRunnable());
        es.submit(new MyRunnable());
    }
}

class MyRunnable implements Runnable {

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}

打印效果如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

可以看出该程序不会停止,是因为线程池一直存在,没有被销毁;

Callable接口

作用类似于 Runnable 接口,但是有区别,因为 Callable 接口有返回值,还可以抛出异常,但是 Runnable 接口没有,Callable 接口的抽象方法只有一个 call();

启动线程方式:

  1. 让线程调用重写方法 call();
  2. 利用线程池中的 submit(Callable<T> c) 方法进行获取返回值 Future 对象;
  3. 使用 Future 对象中的 get() 方法获得最后的结果;
  4. 然后进行输出打印;

示例如下:

package top.sharehome.Demo;

import java.util.concurrent.*;

public class Demo {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        ExecutorService es = Executors.newFixedThreadPool(2);
        Future<String> submit = es.submit(new MyCallable());
        String s = submit.get();
        System.out.println("s = " + s);
    }
}

class MyCallable implements Callable<String> {

    @Override
    public String call() throws Exception {
        return "启动成功!";
    }
}

打印效果如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

ConcurrentHashMap

ConcurrentHashMap 类本质上是 Map 集合,即键值对的集合,使用方式和 HashMap 没有区别,但凡对此 Map 集合的操作,不去修改集合内部元素,就不会被加锁(synchronized);

线程的状态图-生命周期

线程的生命周期只有 6 种:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在某一时刻,线程只能处于其中的一种状态,这种线程的状态反映的是 JVM 中的线程状态和 OS 无关;

  • NEW 状态——新建线程状态——new Thread();
  • RUNNABLE 状态——正在运行状态——start();
  • BLOCKED 状态——受阻塞状态——synchronized(){},即无锁受阻或者运行时的 wait() 状态释放锁;
  • TERMINTED 状态——结束线程状态——run() 方法结束;
  • WAITING 状态——无线等待状态——wait();
  • TIMED_WAITING 状态——时间等待状态——sleep(),join(),wait(miles);

图示如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

下一章:由浅到深认识Java语言(35):File类

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值