多线程案例

案例一:单例模式

单例模式(一种设计模式)单个实例/对象 => 某个类,在一个进程中,只创建出一个实例。

使用单例模式可以对代码进行更严格的校验和检查。

static =>静态的“类属性”,instance就是Singleton类对象里持有的属性。

        其他代码想使用这个类的实例,就需要通过getInstance()方法来获取,不能在代码中重新new该对象。

Singleton.class(从.class文件加载到内存中,表示这个类的一个数据结构)

每个类的类对象只存在一个。类对象中的static属性,自然也只有一个。所以这个instance指向的对象是唯一的一个对象。

1)“饿汉模式”

单例模式中一种简单的写法。

“饿” => “非常迫切”        实例在类加载时就已经创建好了,创建时机非常早。

线程安全 => 对于饿汉来说,getInstance直接返回Instance实例,本质是“读操作”

2) “懒汉模式”

代码一:

创建实例的时机更晚,只有第一次使用的时候才会创建实例。

        首次调用getInstance,则instance为null,就会进去if,从而创建实例;再次调用,就不进入if,直接返回首次创建好的。这样设定,仍可以保证,该类的实例是唯一的一个,并且创建实例的时机不是程序驱动时,而是第一次调用getInstance时。

导致线程被new了两次,

代码二:将synchronized套在if外

多线程的代码是非常复杂的,代码稍微变换一点,结论就截然不同。

具体代码具体分析!!!

但是上述代码仍然存在问题:

        如果Instance已经创建过了,此时后续在调用getInstance都是直接返回Instance实例。但针对这个已没有线程安全问题的代码,仍然每次调用都先加锁,再解锁 =>效率低

代码三:双重if 

 

第一个if判定的是是否要加锁,第二个if判定的是是否要创建对象

3)指令重排序引起的线程问题 

编译器优化的一种方式

调整原有的代码的执行顺序,保证逻辑不变的前提下,提高程序效率。 

instance = new SingletonLazy();=>可以拆分成三个大的步骤(不是三个指令)

1.申请一段内存空间

2.在这个内存上调用构造方法,创建出这个实例

3.把这个内存地址赋值给instance引用变量

        t2没有进入if触发加锁,直接return。

        t2在return后,若使用instance里面的属性/方法就可能会出现错误,所以此时instance的属性是未初始化的“0”。

解决方法:核心思想volatile

1.保证内存可见性,每次访问变量必须重新读取内存,而不会优化到寄存器/缓存中。

2.禁止指令重排序,被volatile修饰的变量的读写操作相关指令,不能被重排序。

优化代码:

案例二:阻塞队列

阻塞队列 =>基于普通队列做出的扩展 

1.线程是安全的

2.具有阻塞特征

        a)如果针对一个已经满了的队列进行入队列,此时入队列操作会阻塞,直到队列不满之后。(其他线程出队列

        b)如果针对一个已经空了的队列进行出队列,此时出队列操作会阻塞,直到队列不空之后。(其他线程入队列

生产者消费者模型

1.引入生产者消费者模型,可以更好地做到“解耦合”(把代码的耦合程度,从高降低)

实际开发中,经常会涉及到“分布式系统” => 服务器整个功能不是由一个服务器完成的,而是每个服务器负责一部分功能,通过服务器之间的网络通信,最终完成整个功能。

代价:需要加机器引入更多的硬件资源

1)阻塞队列,并非是简单的数据结构,而是基于这个数据结构实现的服务器程序,又被部署到单独的主机上了

2)整个系统的结构更复杂了,要维护的机器更多了

3)效率,引入中间商,有差价,请求从A发出到B收到,这个过程经历队列的转发,有一定的开销。

优点:

1.解耦合        2.削峰填谷(三峡大坝)所谓的峰,谷不是长时间持续的,是短时间出现的

        服务器处理每个请求,都是需要消耗硬件资源的(cpu,内存,硬盘,网络宽带...)

即使一个请求消耗的资源比较少,但是有很多请求加在一起,总的消耗的资源就多了!!!

上述任何一种硬件资源达到瓶颈,服务器就挂了(发送请求,不会返回响应)

入口服务器A:做的工作一般比较简单,每个请求消耗的资源少,

用户服务器B,C:完成的工作更复杂,每个请求消耗的资源多。(从数据库中找到对应的用户信息)

一旦外界的请求出现突发峰值,B和C就挂了。

阻塞队列:数据结合(队列没啥业务逻辑,只是存储数据,抗压能力比较强)

消息队列:基于阻塞队列实现服务器程序。     

  

引入后,即使外界的请求出先峰值,也由队列承担请求峰值,B,C仍按原速度请求。

Java标准库中提供了现成的阻塞队列数据结构。

                                              ArrayBlockingQueue

BlockingQueue        =>         LinkedBlockingQueue

(interface接口)                      PriorityBlockingQueue

queue.put("aaa"):使用put和offer一样都是入队列

        put带有“阻塞功能”,offer没有阻塞功能(队列满了会返回结果)

queue.take(),也带有阻塞功能出队列

阻塞队列中没有提供带有阻塞功能的获取队首元素的方法。

基于数组实现阻塞队列(环形队列)

1)先实现普通队列

2)再加上线程安全

3)再加上阻塞功能

判断队列空满:1)浪费一个格子,tail最多走到head前一个位置。        2)引入size变量

可能出现交叉唤醒,put1为线程1执行到wait,put2为线程2执行到wait,take1为线程3执行到notify唤醒put1,put1执行到notify唤醒put2

        if只判定一次。一旦程序阻塞后在被唤醒,这中间间隔的时间很长,这个过程会有很多变数。难以保证条件,条件是否仍然满足,入队列的条件是否具备。

        改成while后,意味着wait被唤醒后再判定一次条件 =>多了一步确认操作。如果再次确认,发现队列还是满的,就继续等待。

生产者消费者模型

        实际开发中,生产者消费者模型,往往是有多个生产者,消费者。

生产者,消费者不仅仅是一个线程,也可能是一个独立的服务器程序,甚至一组服务器相连。

生产者消费者模型 => 最核心的部分是阻塞队列,使用synchronized和wait/notify达到线程安全,线程阻塞。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值