目录
多线程的代码案例
1.单例模式
单例模式(一种设计模式),单个实例(对象),强制要求,某个类在某个程序中,只有唯一一个实例(不允许创建多个实例,不允许new多次)。
JDBC代码的基本流程:
1.创建DataSource => 描述了数据库服务器在哪里(url,user,password)
2.建立连接 dataSource.getConnection();
3.拼装SQL语句 Statement或者PreparedStatement
4.执行SQL execute方法/executeQuery/executeUpdate
5.遍历结果集合 ResultSet,迭代器遍历
6.关闭资源 1)ResultSet 2)Statement 3)Connection
饿汉方式的单例模式:
package thread;
class Singleton{
//饿汉方式的单例模式
private static Singleton istance=new Singleton();
//静态成员的初始化,是在类加载的阶段触发的
//类加载往往就是在程序一启动就触发
public static Singleton getInstance(){
//后续通过此方法获取实例
return istance;
}
private Singleton(){
}
}
public class Demo27 {
public static void main(String[] args) {
Singleton t1=Singleton.getInstance();
Singleton t2=Singleton.getInstance();
System.out.println(t1==t2);//true
}
}
- 懒汉方式的单例模式:
懒和饿相对,饿是尽早创建实例,懒是尽晚创建实例(甚至不创建)
//通过懒汉模式构造单例模式
class SingletonLazy{
private static SingletonLazy instance=null;
public static SingletonLazy getInstance(){
if(instance==null){
//创建实例时机-第一次使用的时候,而不是程序启动的时候
instance=new SingletonLazy();
}
return instance;
}
private SingletonLazy(){
}
}
public class Demo28 {
public static void main(String[] args) {
SingletonLazy s1=SingletonLazy.getInstance();
SingletonLazy s2=SingletonLazy.getInstance();
System.out.println(s1==s2);//true
}
}
对于饿汉方式来说,是线程安全的。return instance是读操作,String不可变对象,天然线程安全。
对于懒汉方式,是线程不安全的。instance=new SingletonLazy();涉及到多线程的修改。随着第二个线程的覆盖操作,第一个线程new出来的对象会被GC释放掉。
为了解决懒汉模式线程不安全,引入了加锁。引入加锁之后,后执行的线程就会在加锁的位置阻塞,阻塞到前一个线程解锁。当后一个线程进入条件的时候,前一个线程已经修改完毕,Instance不再为null,就不会进行后续的new操作。
//通过懒汉模式构造单例模式
class SingletonLazy{
private static SingletonLazy instance=null;
private static Object lock=new Object();
public static SingletonLazy getInstance(){
synchronized (lock){
if(instance==null){
//创建实例时机-第一次使用的时候,而不是程序启动的时候
instance=new SingletonLazy();
}
}
return instance;
}
private SingletonLazy(){
}
}
public class Demo28 {
public static void main(String[] args) {
SingletonLazy s1=SingletonLazy.getInstance();
SingletonLazy s2=SingletonLazy.getInstance();
System.out.println(s1==s2);//true
}
}
当把实例创建好了之后,后续再调用getInstance,此时都是直接执行return .
如果只是进行if判定+return,就是纯粹的读操作了。读操作不涉及线程安全问题。
但是,每次调用上述方法,都会触发一次加锁操作,多线程情况下,加锁就会互相阻塞,影响程序的执行效率。
解决方法就是按需加锁,真正涉及到线程安全的时候再加锁。如果实例已经创建过了,就不涉及线程安全问题;如果没有创建,就涉及线程安全问题。
//通过懒汉模式构造单例模式
class SingletonLazy{
private static volatile SingletonLazy instance=null;
//可能存在“内存可见性”问题,编译器优化比较复杂。
//为了稳妥起见,给Instance直接加上volatile,从根本上杜绝内存可见性问题
private static Object lock=new Object();
public static SingletonLazy getInstance(){
if(instance==null){//判定是否加锁
//在多线程中,两次判定之间,可能存在其他线程把if中的Instance变量修改了
//导致两次if的结果不同
synchronized (lock){
if(instance==null){//判定是否需要new对象
//创建实例时机-第一次使用的时候,而不是程序启动的时候
instance=new SingletonLazy();
}
}
}
return instance;
}
private SingletonLazy(){
}
}
public class Demo28 {
public static void main(String[] args) {
SingletonLazy s1=SingletonLazy.getInstance();
SingletonLazy s2=SingletonLazy.getInstance();
System.out.println(s1==s2);//true
}
}
还有一个问题,是“指令重排序”的问题。也是编译器优化的一种体现形式,编译会在逻辑不变的前提下,调整代码执行的先后顺序,以达到提升性能的效果。
instance=new SingletonLazy();有三步:1.申请内存空间
2.在空间上构造对象(初始化)
3.内存空间的首地址,赋值给引用变量
volatile的功能:
- 确保每次读取操作都是读内存
- 关于该变量的读取和修改操作,不会触发重排序
2.生产者消费者模型
优点:
- 解耦合(不一定是两个线程之间,也可以是两个服务器之间)。A和B不再直接交互,而是通过中间的阻塞队列进行交互。降低耦合,是为了让后续修改的时候成本低。
- 削峰填谷。队列服务器针对单个请求,做的事情很少(存储,转发),队列服务器可以抗很高的请求量。
缺点:
- 引入队列之后,服务器整体的结构会更复杂。此时就需要更多的机器进行部署。生产环境的结构会更复杂,管理起来更麻烦。
- 效率(性能)降低
Java标准库中提供了现成的阻塞队列。
Java中Linkedlist这个类进行中间插入的时候,add(插入值的下标),需要从头找到指定下标才能插入。O(N)
把阻塞队列单独包装成服务器程序并且使用单独的机器(集群)来部署,这样的队列称为“消息队列”(MQ)。
阻塞队列:
1.线程安全(天然使用在多线程环境中)
2.阻塞特性(队列满,入队列;队列空,出队列)
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class Demo29 {
public static void main(String[] args) throws InterruptedException {
BlockingQueue<String> queue=new LinkedBlockingQueue<>(100);//最大容纳100个元素
queue.put("a");
String element=queue.take();
//put和take带有阻塞功能
System.out.println(element);
}
}
生产消费:
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class Demo30 {
public static void main(String[] args) {
BlockingQueue<Integer> queue=new LinkedBlockingQueue<>(1000);
Thread producer=new Thread(()->{
int n=0;
while(true){
try{
queue.put(n);
System.out.println("生产元素"+n);
n++;
Thread.sleep(1000);
} catch (InterruptedException e){
throw new RuntimeException(e);
}
}
},"producer");
Thread consumer=new Thread(()->{
//加入sleep队列会满
while(true){
try{
System.out.println("消费元素"+queue.take());
}catch (InterruptedException e){
throw new RuntimeException(e);
}
}
},"consumer");
producer.start();
consumer.start();
}
}
单位换算中,Thousand 千=>K;Million 百万=>M;Billion 十亿=>G.
模拟实现简单的阻塞队列,基于阻塞队列实现生产者消费者模型:
优势:1.解耦合(生产者只和队列交互,消费者也只和队列交互,生产者和消费者能够更好的解耦合),修改代码更方便
2.削峰填谷
class MyBlockingQueue{
private String[] data=null;
private int head=0;//队头
private int tail=0;//队尾
private int size=0;//元素个数
public MyBlockingQueue(int capacity){
data=new String[capacity];
}
public void put(String element) throws InterruptedException {//入队
synchronized (this){
while(size>=data.length){
this.wait();//队列满,需要阻塞等待
//此时即使wait被唤醒,还会进行再次判断,再次进行阻塞
}
data[tail++]=element;
if(tail>=data.length){
tail=0;//循环
}
//if判断也可写成tail=(tail+1)%data.length;
//但是运行效率较低(因为取余操作较加减操作慢)
//开发效率也低,不便于理解
size++;
this.notify();
}
}
public String take() throws InterruptedException {//出队
synchronized (this){
if(size==0){
this.wait();//队列空,需要阻塞
}
String ret=data[head];
head++;
if(head>=data.length){
head=0;
}
size--;
this.notify();//唤醒wait
return ret;
}
}
}
public class Demo31 {
public static void main(String[] args) {
MyBlockingQueue queue=new MyBlockingQueue(1000);
Thread producer =new Thread(()->{
int n=0;
while(true){
try{
queue.put(n+"");
System.out.println("生产元素"+n);
n++;
Thread.sleep(1000);
}catch(InterruptedException e){
throw new RuntimeException(e);
}
}
});
Thread consumer =new Thread(()->{
while(true){
String n=null;
try{
n=queue.take();
System.out.println("消费元素"+n);
}catch(InterruptedException e){
throw new RuntimeException(e);
}
}
});
producer.start();
consumer.start();
}
}
比如有若干个线程使用这个队列,要么所有的线程阻塞在put,要么所有的线程阻塞在take,不可能一部分线程阻塞在put一部分线程阻塞在take.
3.线程池
常量池
字符串常量,在Java程序最初构建的时候就已经准备好。等程序运行的时候,这样的常量也就加载到内存中了。剩下了构造/销毁的开销。
线程池:把线程提前创建好,放到一个地方(类似数组),需要用的时候去取,用完了还回池子中。
操作系统的用户态和内核态:
操作系统=内核+配套的应用程序
内核包含操作系统的各种核心功能:
1.管理硬件设备
2.给软件提供稳定的运行环境
从线程池取线程,纯应用程序代码就可完成【可控】
从操作系统创建新线程,就需要操作系统内核配合完成【不可控】
使用线程池,就可以省下应用程序切换到内核中运行这样的开销。
ThreadPoolExecutor线程池执行器
核心方法:submit,任务放到线程池中,此时线程池里的线程就会执行这样的任务。
- int corePoolSize核心线程数(至少有多少个线程,线程池一创建,这些线程也要随之创建,直到整个线程池销毁,这些线程才会销毁)
- int maximumPoolSize最大线程数(核心线程+非核心线程,非核心线程不繁忙就销毁,繁忙就再创建)
线程也不是越多越好
Java的线程池里面包含几个线程是可以动态调整的。任务多的时候自动扩容成更多的线程;任务少的时候把额外的线程干掉,节约资源。
- long keepAliveTime非核心线程允许空闲的最大时间
- TimeUnit unit枚举(TimeUnit.SECONDS)
- BlockingQueue<Runnable> workQueue工作队列
线程池本质上也是生产者消费者模型,调用submit就是在生产任务,线程池里的线程就是在消费任务。
- ThreadFactory threadFactory线程工厂
工厂模式(也是一种设计模式,和单例模式并列关系)
用来弥补构造方法的缺陷。构造方法的名字是固定的,要想提供不同的版本就需要通过重载,有时候不一定能构成重载。
工厂方法的核心,通过静态方法,把构造对象new的过程,各种属性初始化的过程封装起来。提供多组静态方法,实现不同情况的构造。
class Point{
public static Point makePointByXY(double x,double y){
Point ret=new Point();//通过x和y给p进行属性设置
return ret;
}
public static Point makePointByRA(double r,double a){
Point ret=new Point();//通过r和a给p进行属性设置
return ret;
}
}
public class Demo33 {
public static void main(String[] args) {
Point p=Point.makePointByXY(10,20);//工厂设计模式
}
}
- RejectedExecutionHandler handler拒绝策略
对于线程池来说,发现入队列操作时,队列满了,不会真的触发“入队列操作”,不会真阻塞,而是执行拒绝策略相关的代码。
4种策略:
- ThreadPoolExecutor.AbortPolicy线程池直接抛出异常(线程池可能无法继续工作)
- ThreadPoolExecutor.CallerRunsPolicy让调用submit的线程自行执行任务
- ThreadPoolExecutor.DiscardOldestPolicy丢弃队列中最老的任务
- ThreadPoolExecutor.DiscardPolicy丢弃最新的任务,当前submit的这个任务
Java标准库提供了另一种类,针对ThreadPoolExecutor进行进一步封装,简化线程池的使用,也是基于工厂设计模式。
import java.util.concurrent.Executors;
public class Demo34 {
public static void main(String[] args) {
Executors.newFixedThreadPool(5);//创建固定数量的线程池(核心线程数和最大线程数一样)
Executors.newCachedThreadPool();//最大线程数是一个很大的数字(线程可以无限增加)
}
}
线程池,最核心的就是submit这样的操作,往线程池中添加任务(任务就是runnable).
得有线程来执行队列中的任务。在构造方法中,把线程创建出来。
线程池里,提前准备好10个线程,有100个客户端把请求发过来,把这100个客户端的请求封装成任务(Runnable)添加到线程池里,线程池中由10个线程负责处理这100个任务,这个过程就不涉及线程的创建销毁了。
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
class MyThreadPool{
private BlockingQueue<Runnable> queue=null;
public MyThreadPool(int n){
//初始化线程池,创建固定个数的线程
//这里使用ArrayBlockingQueue作为任务队列,容量为1000
queue=new ArrayBlockingQueue<Runnable>(1000);
//创建N个线程
for(int i=0;i<n;i++){
Thread t=new Thread(()->{
try{
while(true){
Runnable task=queue.take();
task.run();
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
//t.setDaemon(true);设为后台线程
t.start();
}
}
public void submit(Runnable task) throws InterruptedException{
queue.put(task);
}
}
public class Demo35 {
public static void main(String[] args) throws InterruptedException {
// MyThreadPool pool=new MyThreadPool(10);
ExecutorService pool= Executors.newCachedThreadPool();
//向线程池提交任务
for(int i=0;i<100;i++){
int id=i;
pool.submit(()->{
System.out.println(Thread.currentThread().getName()+"id="+id);
});
//线程池里的线程还在take阻塞(等待)
//线程池中的线程是前台线程,阻止进程结束
pool.shutdown();
//shutdown能够把线程池里的线程全部关闭,但不能保证线程池里的任务一定能全部执行完毕
//如果要等待线程池内的任务全部执行完毕,需要调用awaitTermination方法
}
}
}
4.定时器
Java标准库中的Timer
import java.util.Timer;
import java.util.TimerTask;
public class Demo37 {
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
//核心就是重写run方法
public void run() {
System.out.println("hello 1000");
}
},1000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("hello 2000");
}
},2000);//2000ms之后执行
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("hello 3000");
}
},3000);
//和线程池一样,Timer中也包含前台线程,阻止进程结束
System.out.println("hello main");
}
}
匿名内部类的写法:
- 创建子类(父类TimerTask),匿名
- 重写run
- new了子类的实例
模拟实现定时器:
- 创建一个类表示一个任务
- 定时器中,能够管理多个任务的。必须使用一些集合类把这多个任务管理起来
- 实现schedule方法,把任务添加到队列中即可
- 创建一个线程负责执行队列中的任务
//这样的写法基于抽象类的方式定义MyTimerTask
//abstract class MyTimerTask implements Runnable{
// @Override
// public abstract void run();
//}
import java.util.PriorityQueue;
import java.util.TimerTask;
import java.util.concurrent.Executors;
//另一种方法
class MyTimerTask implements Comparable<MyTimerTask>{
private long time;
private Runnable task;//持有成员
public MyTimerTask(Runnable task,long time){
this.task=task;
this.time=time;
}
@Override
public int compareTo(MyTimerTask o){
return (int)(this.time-o.time);
}
public long getTime() {
return time;
}
public void setTime(long time) {
this.time = time;
}
public void run(){
task.run();
}
}
class MyTimer{
private PriorityQueue<MyTimerTask> queue=new PriorityQueue<>();
Object lock=new Object();
public void schedule(Runnable task,long delay){
synchronized (lock){
MyTimerTask timerTask=new MyTimerTask(task,System.currentTimeMillis()+delay);
queue.add(timerTask);
lock.notify();
}
}
public MyTimer(){
//创建一个线程负责执行队列中的任务
Thread t=new Thread(()->{
try{
while(true){
synchronized (lock){
//取出队首元素
while(queue.isEmpty()){
lock.wait();
}
MyTimerTask task=queue.peek();
if(System.currentTimeMillis()<task.getTime()){
//当前任务时间比系统时间大,说明任务执行时机未到
lock.wait(task.getTime()-System.currentTimeMillis());
}else{
task.run();
queue.poll();
}
}
}
}catch(InterruptedException e){
e.printStackTrace();
}
});
t.start();
}
}
public class Demo38 {
public static void main(String[] args) {
MyTimer timer = new MyTimer();
timer.schedule(new TimerTask() {
@Override
//核心就是重写run方法
public void run() {
System.out.println("hello 1000");
}
},1000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("hello 2000");
}
},2000);//2000ms之后执行
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("hello 3000");
}
},3000);
Executors.newScheduledThreadPool(4);
//创建了一个带有线程池的定时器
}
}
定时器除了基于堆(优先级队列)方式实现之外,还可以基于“时间轮”:
类似一个循环队列(数组),每个元素是一个“时间单位”,每个元素又是一个链表,每到一个时间单位,光标指向下一个元素,同时把这个元素上对应链表中的任务都执行一遍。优势是性能更高。劣势是时间精度不如优先级队列(优先级队列,堆的调整 logN,适合精度高的情况)。更适合任务多的情况。