高并发
面试的时候都喜欢问这个问题,解决高并发的问题根源在于解决高并发下共享资源的控制问题。也就牵扯到多线程下共享资源的同步问题。
- 同步:单线程场景下,不需要考虑资源的同步,在执行一个方法或者操作后,一直等待结果,当然这时候可以有个超时的处理机制,但是本质上还是一直阻塞的。
- 异步:多线程场景下,就需要考虑异步操作,也就是做一件事,不影响别的操作。在执行一个操作后,不去理会结果,而是去处理另外的操作,直到接收到已经处理完成的通知或消息,才去处理对应的结果。
解决并发和同步问题,主要是通过锁机制,不管是数据库级别,还是缓存级别,都是通过锁来完成。
- 悲观锁,一般是数据库级别,也就是锁定对应的行级数据,优点是可以控制数据不出问题,缺点是性能方面
- 乐观锁,一般通过采用version字段的方式来控制,不会锁定对应的数据,性能也比较高,但是会导致脏数据,比如发券场景下的超发现象,以及商品库存的超卖现象。
- 对于单系统而言,在代码级别使用syncrinized来控制代码的访问是可行的,但是在分布式环境下,只能通过数据库锁或者缓存锁来控制。
- 缓存锁的优势:速度相对DB是比较快,可以有效降低DB的压力。比如redis的watch,对应的jedis就需要使用setnx自己实现lock方法;比如tair在ldb的incr方法及put方法,incr方法指定lowBound和upBound(也就是一个取值范围)可以解决这个问题,而put方法则传入version版本来做,也是乐观锁。(参见:https://yq.aliyun.com/articles/58928)
- 当然数据库可以通过分库分表的方式来缓解压力,对于老数据可以考虑增加历史库,以保证数据量不会影响对应的数据库操作;而缓存也可以通过设置多个Key来分流单key的锁竞争压力。
设计模式 -- 后续每天补充
设计模式是代码设计经验的总结,在代码复用,稳定性以及易理解方面都有一定的保证。具体参见:
http://www.cnblogs.com/maowang1991/archive/2013/04/15/3023236.html
- 设计模式的六大原则
1、开闭原则(Open Close Principle)
开闭原则就是说对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。所以一句话概括就是:为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类,后面的具体设计中我们会提到这点。
2、里氏代换原则(Liskov Substitution Principle)
里氏代换原则(Liskov Substitution Principle LSP)面向对象设计的基本原则之一。 里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。 LSP是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。里氏代换原则是对“开-闭”原则的补充。实现“开-闭”原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。
3、依赖倒转原则(Dependence Inversion Principle)
这个是开闭原则的基础,针对接口编程,依赖于抽象而不依赖于具体。
4、接口隔离原则(Interface Segregation Principle)
使用多个隔离的接口,比使用单个接口要好。降低类之间的耦合度的意思,从这儿我们看出,其实设计模式就是一个软件的设计思想,从大型软件架构出发,为了升级和维护方便。所以上文中多次出现:降低依赖,降低耦合。
5、迪米特法则(最少知道原则)(Demeter Principle)
为什么叫最少知道原则,就是说:一个实体应当尽量少的与其他实体之间发生相互作用,使得系统功能模块相对独立。
6、合成复用原则(Composite Reuse Principle)
原则是尽量使用合成/聚合的方式,而不是使用继承。
工厂模式
实现同一个接口的对象,如果需要大量创建,则使用工厂模式。工厂模式又可以细分为普通工厂模式和静态工厂模式。以及为了解决上述工厂模式需要修改原有代码的抽象工厂模式
- 接口及实现
// interface
public interface Sender { public void send();}
//mailSender
public class MailSender implements Sender { public void send() { System.out.println("Mail Sender"); }}
// smsSender
public class SmsSender implements Sender { public void send() { System.out.println("Sms Sender"); }}
普通工厂模式:
public class NormalSenderFactory {
public Sender mailSender(){ return new MailSender(); }
public Sender smsSender(){ return new SmsSender(); }
}静态工厂模式
public class StaticSenderFactory {
public static Sender mailSender(){ return new MailSender(); }
public static Sender smsSender(){ return new SmsSender(); }
}调用方式
public class SenderFactoryTest {
public static void main(String[] args){
NormalSenderFactory senderFactory = new NormalSenderFactory();
senderFactory.mailSender().send();
senderFactory.smsSender().send();
// 静态工厂模式调用
StaticSenderFactory.smsSender().send();
StaticSenderFactory.mailSender().send(); }
}抽象工厂模式
工厂方法模式有一个问题就是,对象的创建依赖工厂类,也就是说,如果想要拓展程序,必须对工厂类进行修改,这违背了闭包原则。所以也就有了抽象工厂模式。
抽象工厂模式增加一个新的接口,用于封装原有的工厂,也就是说可以创建不同的工厂来处理不同的业务。
- 接口及实现
//provider
public interface Provider { public Sender produce();}
// smsFactory
public class SmsFactoryProvider implements Provider {
public Sender produce() { return new SmsSender(); }
}
// mailFactory
public class MailFactoryProvider implements Provider {
public Sender produce() { return new MailSender(); }
}
- 调用方式
public class AbstractFactoryTest {
public static void main(String[] args){
Provider provider = new SmsFactoryProvider(); provider.produce().send();
}
}
单例模式
保证只有一个对象存在,注意在多线程场景下,需要使用synchronized来锁住对应的instance,也就是需要用synchronized来锁定对应的对象
注意的是,采用类的静态方法,实现单例模式的效果,也是可行的,此处二者有什么不同?
- 首先,静态类不能实现接口。(从类的角度说是可以的,但是那样就破坏了静态了。因为接口中不允许有static修饰的方法,所以即使实现了也是非静态的)
- 其次,单例可以被延迟初始化,静态类一般在第一次加载是初始化。之所以延迟加载,是因为有些类比较庞大,所以延迟加载有助于提升性能。
- 再次,单例类可以被继承,他的方法可以被覆写。但是静态类内部方法都是static,无法被覆写。
- 最后一点,单例类比较灵活,毕竟从实现上只是一个普通的Java类,只要满足单例的基本需求,你可以在里面随心所欲的实现一些其它功能,但是静态类不行。从上面这些概括中,基本可以看出二者的区别,但是,从另一方面讲,我们上面最后实现的那个单例模式,内部就是用一个静态类来实现的,所以,二者有很大的关联,只是我们考虑问题的层面不同罢了。两种思想的结合,才能造就出完美的解决方案,就像HashMap采用数组+链表来实现一样,其实生活中很多事情都是这样,单用不同的方法来处理问题,总是有优点也有缺点,最完美的方法是,结合各个方法的优点,才能最好的解决问题!
建造者模式
相当于将多个工厂的调用放到一起,创建批量的不同子类型的复合体复杂对象,如下:
public class SenderBuilder {
private List<Sender> senderList = Lists.newArrayList();
public List<Sender> buildMail(int count){
for(int i=0;i<count;i++){ senderList.add(new MailSender()); }
return senderList;
}
public List<Sender> buildSms(int count){
for(int i=0;i<count;i++){ senderList.add(new SmsSender()); }
return senderList; }
}
// test类
public class BuilderTest {
public static void main(String[] args){
SenderBuilder senderBuilder = new SenderBuilder();
senderBuilder.buildMail(10);
}}
原型模式
适配器模式
将一种接口转换成另外一种接口,以消除接口不匹配造成的兼容性问题。
装饰模式
需要(动态)扩展一个类的功能,实现同一个接口,并且调用原有类,只不过在原有类对应方法的前后加入自己的业务处理逻辑。
代理模式
跟装饰模式其实很相似,都是去扩展一些原有类的功能,不过代理模式会隐藏掉原有类的对象及入口,而装饰模式不会。
外观模式
讲互相有依赖关系的对象,统一放入一个新的外观类中,已去除彼此间的依赖,降低耦合。类似于OrderTO中的bizOrder,payOrder以及logicOrder。
桥接模式
通过对Bridge类的调用,实现了对接口Sourceable的实现类SourceSub1和SourceSub2的调用
组合模式
将多个对象组合在一起进行操作,常用于表示树形结构中,例如二叉树
享元模式
享元模式的主要目的是实现对象的共享,即共享池,当系统中对象多的时候可以减少内存的开销,通常与工厂模式一起使用。
策略模式
策略模式定义了一系列算法,并将每个算法封装起来,使他们可以相互替换,且算法的变化不会影响到使用算法的客户。需要设计一个接口,为一系列实现类提供统一的方法,多个实现类实现该接口,设计一个抽象类(可有可无,属于辅助类),提供辅助函数。
模板模式
一个抽象类中,有一个主方法,再定义1...n个方法,可以是抽象的,也可以是实际的方法,定义一个类,继承该抽象类,重写抽象方法,通过调用抽象类,实现对子类的调用
观察者模式
当一个对象变化时,其它依赖该对象的对象都会收到通知,并且随着变化!对象之间是一种一对多的关系
责任链模式
有多个对象,每个对象持有对下一个对象的引用,这样就会形成一条链,请求在这条链上传递,直到某一对象决定处理该请求。但是发出者并不清楚到底最终那个对象会处理该请求,所以,责任链模式可以实现,在隐瞒客户端的情况下,对系统进行动态的调整
状态模式
根据状态的不同来实现不同的业务处理
HashMap的实现
HashMap基于数组+链表实现,其中数组是key值对应的hashcode,而链表则是对应的entry,entry是key-value的组合,根据key的hashcode决定存储在哪个位置对应的链表中。Entry还包含next属性,指向下一个entry对象。
在遍历hashMap时,如果需要remove某个位置的参数,要用iterator来remove,直接调用hashmap的remove方法会导致modCount变化,modCount不匹配时会报出ConcurrentModifitionException,导致remove失败。
key值的hashcode对应的链表需要去重排元素。
ConcurrentHashMap主要引入了segment,把整个hashmap分段加锁,来达到控制并发同时解决单一锁瓶颈的目的。segment继承了ReentrantLock,另一种锁机制。
Volatile
每个线程都有自己的线程栈空间,不存在共享资源的问题。
基础类型的变量存储在栈中,复杂对象的引用保存在线程栈中,而对应的对象则存储在堆中。
volatile只能修饰变量,而synchronized则可以修饰类、方法、代码块、变量,作用域是不同的
-
针对基础类型
- 对于没有使用volatile修饰的基础类型,每个线程在用到这个对象时,都会从主线程的栈空间中拷贝一份副本到本线程栈中,后面的每次操作都是操作这个副本。只有当线程结束时,会同步给主线程空间。
- 而对于使用volatile修饰的基础类型,则每个线程操作的都是主线程栈中的变量,但是jvm虚拟机只是保证从主内存加载到线程工作内存的值是最新的,还是会存在并发问题。
对于引用变量
不管是否使用volatile,线程栈中保存的都是一个引用指针,保存的是堆中的内存地址,指向的是堆中的对象。所以这种情况下的读写操作,没办法保证是原子性的。
IO
阻塞IO
非阻塞IO
SELECTOR