说之前谈两个之前一直有疑问的地方:
反向代理和负载均衡有何区别?
反向代理
,是把一些静态资源存储在服务器上,当用户有请求的时候,就直接返回反向代理服务器上的资源给用户,而如果反向代理服务器上没有的资源,就转发给后面的负载均衡服务器
,负载均衡服务器再将请求分发给后端的web服务器。
区别就是:
反向代理服务器是需要存储资源的,让用户更快速的接收到资源。
负载均衡就是,为了保证后端web服务器的高可用,高并发,是不需要要存储资源,只需要转发用户的请求。
使用本地缓存
的场景
在程序中,有些表数据,数据很少,但是程序加载的时候要马上访问,并且访问的很频繁,比如(例如系统配置参数,区域信息),
针对这种情况,将数据放到程序的本地缓存中即内存中,从而提升系统的访问效率,减少数据库访问,数据库访问要占用数据库连接,同时网络消耗比较大,但同时要注意,缓存的占用空间、缓存的失效策略。
跨平台的Java运行环境—JVM
不同平台有不同的Java虚拟机,但是不同Java虚拟机所识别的是统一格式的中间代码,也就是我们常说的Java Byte Code
(Java字节码),如下图所示:
从上图可以看到从源码到Java字节码再到具体不同平台JVM执行的过程。
垃圾回收与内存堆布局
Hotspot JVM 划分
虚拟机中的共划分为三个代:年轻代
(Young Generation)、老年代
(Old Generation)和持久代
(Permanent Generation)。
其中持久代主要存放的是Java类的类信息,与垃圾收集要收集的Java对象关系不大。年轻代和年老代的划分是对垃圾收集影响比较大的。
内存泄漏 vs 内存溢出
内存溢出
out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出。
内存泄露
memory leak,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。
memory leak会最终会导致out of memory!
Java并发编程的类,接口和方法
线程池
在Java中,我们主要使用的线程池就是ThreadPoolExecutor
,此外还有定时的线程池ScheduledThreadPoolExecutor
。
需要注意的是对于Executors.newCachedThreadPool()方法返回的线程池的使用,该方法返回的线程池是没有线程上限的,在使用时一定要当心,因为没有办法控制总体的线程数量,而每个线程都是消耗内存的,这可能会导致过多的内存被占用。
建议尽量不要用这个方法返回的线程池,而要使用有固定线程上限的线程池。
ExecutorService——shutdown方法和awaitTermination方法
ExecutorService的关闭
shutdown和awaitTermination为接口ExecutorService定义的两个方法,一般情况配合使用来关闭线程池。
方法简介
shutdown方法
:平滑的关闭ExecutorService,当此方法被调用时,ExecutorService停止接收新的任务并且等待已经提交的任务(包含提交正在执行和提交未执行)执行完成。当所有提交任务执行完毕,线程池即被关闭。
awaitTermination方法
:接收人timeout和TimeUnit两个参数,用于设定超时时间及单位。当等待超过设定时间时,会监测ExecutorService是否已经关闭,若关闭则返回true,否则返回false。一般情况下会和shutdown方法组合使用。
具体实例
service.shutdown();
while (!service.awaitTermination(1, TimeUnit.SECONDS)) {
System.out.println("线程池没有关闭");
}
System.out.println("线程池已经关闭");
volatile 和 synchronized
(1)Synchronized
:保证可见性和原子性
Synchronized能够实现原子性和可见性;
在Java内存模型中,synchronized规定,线程在加锁时,**先清空工作内存→在主内存中拷贝最新变量的副本到工作内存→执行完代码→将更改后的共享变量的值刷新到主内存中→释放互斥锁**。
(2)Volatile
:保证可见性,但不保证操作的原子性
Volatile实现内存可见性是通过`store和load`指令完成的;
也就是对volatile变量执行写操作时,会在写操作后加入一条store指令,即强迫线程将最新的值刷新到主内存中;
而在读操作时,会加入一条load指令,即强迫从主内存中读入变量的值。**但volatile不保证volatile变量的原子性**,例如:
Private int Num=0;
Num++;//Num不是原子操作
Num不是原子操作,因为其可以分为:读取Num的值,将Num的值+1,写入最新的Num的值。
对于Num++;操作,线程1和线程2都执行一次,最后输出Num的值可能是:1或者2
3)Synchronized和Volatile的比较
1)Synchronized保证内存可见性和操作的原子性
2)Volatile只能保证内存可见性
3)Volatile不需要加锁,比Synchronized更`轻量级`,并不会阻塞线程(volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。)
4)volatile标记的变量不会被编译器优化,而synchronized标记的变量可以`被编译器优化`(如编译器重排序的优化).
5)volatile是变量修饰符,仅能用于`变量`,而synchronized是一个方法或块的`修饰符`。
大型网站系统书上内容:
int i1; public int geti1() {return i1;}
volatile int i2; public int geti2() {return i2;}
int i3; public synchronized int geti3() {return i3;}
代码中对于geti1的调用获取的是当前线程中的副本,这个值不一定是最新的值。
对于geti2,因为i2被修饰为volatile,因此对于JVM来说这个变量不会有线程的本地副本,只会放在主存中,所以得到的值一定是最新的。
而对于geti3,因为有synchronized关键字修饰,保证了线程的本地副本与主存的同步,所以也会得到最新的值。
上面的对比是在读的层面。
下面来看看写的情况。
int i1; public void seti1(int v) {i1 = v;}
volatile int i2; public void seti2(int v) {i2 = v;}
int i3; public synchronized void seti3(int v) {i3 = v;}
同样的,对于seti1,当前线程在调用了seti1后会得到最新的i1值,而另外的线程获取不一定可以立刻看到最新的值。
对于seti2,则可以立刻在其他线程看到新的值,因为volatile保证了只有一份主存中的数据。
对于seti3,调用后必须在synchronized修饰的方法或者代码块中读取i3的值才可以看到最新值,因为sunchronized不仅会把当前线程修改的变量的本地副本同步给主存,还会从主存读取数据更新本地副本。
wait notify
java的wait/notify的通知机制
可以用来实现线程间通信。
wait表示线程的等待,调用该方法会导致线程阻塞,直至另一线程调用notify或notifyAll方法才可另其继续执行。
经典的生产者、消费者模式即是使用wait/notify机制得以完成。
需要注意,对wait, notify和notifyAll的调用都必须是在对象的synchronized块
中
在实践中,对wait的使用一般是嵌在一个循环中,并且会判断相关的数据状态是否达到预期,吐过没有则会继续等待,这么做主要是为了防止虚假唤醒
。
下面看一下while中wait的使用以及虚假唤醒的情况说明
//消费者线程
static class Consume implements Runnable {
@Override
public void run() {
synchronized (obj) {
System.out.println("进入消费者线程");
System.out.println("wait flag 1:" + flag);
while (!flag) { //判断条件是否满足,若不满足则等待
try {
System.out.println("还没生产,进入等待");
obj.wait();
System.out.println("结束等待");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("wait flag 2:" + flag);
System.out.println("消费");
System.out.println("退出消费者线程");
}
}
}
虚假唤醒
一般而言线程调用wait()方法后,需要其他线程调用notify,notifyAll方法后,线程才会从wait方法中返回, 而虚假唤醒(spurious wakeup)是指线程通过其他方式,从wait方法中返回。
下面是一个买票退票的操作例子,买票时,线程A买票,如果发现没有余票,则会调用wait方法,线程进入等待队列中,线程B进行退票操作,余票数量加一,然后调用notify 方法通知等待线程,此时线程A被唤醒执行购票操作。
从程序的顺序性来看 if (remainTicketNum<=0)没有问题,但是为什么会出现虚假唤醒呢?
因为wait方法可以分为三个操作:
(1)释放锁并阻塞
(2)等待条件cond发生
(3)获取通知后,竞争获取锁
假设此时有线程A,C买票,线程A调用wait方法进入等待队列,线程C买票时发现线程B在退票,获取锁失败,线程C阻塞,进入阻塞队列,线程B退票时,余票数量+1(满足条件2 等待条件发生),线程B调用notify方法后,线程C马上竞争获取到锁,购票成功后余票为0,而线程A此时正处于wait方法醒来过程中的第三步(竞争获取锁获取锁),当线程C释放锁,线程A获取锁后,会执行购买的操作,而此时是没有余票的。
解决的办法是条件判断通过while
(remainTicketNum<=0)来解决,但是有个问题是如果一直没有退票操作线程Notify,while语句会一直循环执行下去,CPU消耗巨大
Exchanger
常见的有CyclicBarrier、CountDownLatch、Semaphore,现在介绍并发工具类中的最后一个Exchange
。
Exchanger,它允许在并发任务之间交换数据。具体来说,Exchanger类允许在两个线程之间定义同步点
。当两个线程都到达同步点时,他们交换数据结构
,因此第一个线程的数据结构进入到第二个线程中,第二个线程的数据结构进入到第一个线程中。
Future和FutureTask
Future
是一个接口,FutureTask
是一个具体实现类。
例如,现在通过调用一个方法从远程获取一些计算结果,假设有这样一个方法:
HashMap getDataFromRemote();
如果是最传统的同步方式使用,代码大概是这样的:
HashMap data = getDataFromRemote();
我们一直等待方法的返回,然后才能继续后面的工作。 但是如果时间很长,阻塞
在这里很浪费时间。那么我们如何改进呢?
能够想到的办法就是调用函数后马上返回,然后继续向下执行,等需要勇敢数据时再来用,或者说再来等待这个数据。
这时候就轮到Future出场了!!
代码如下:
Future<HashMap> future = getDataFromRemote2();
// do something
HashMap data = (HashMap) future.get();
可以看到,返回的是一个future对象,之后通过调用get()
方法来获取真正的返回值。
也就是说,在调用了getDataFromRemote2()后,就已经启动了对远程计算结果的获取,同时自己的线程还在继续处理,直到需要时再获取数据。
下面来看一下getDataFromRemote2()的实现:
private Future<HashMap> getDataFromRemote2() {
return threadPool.submit(new Callable<HashMap>) {
public HashMap call() throws Exception {
return getDataFromRemote();
}
}
}
用到了线程池:把任务加入线程池中,把Future对象返回出去。
我们调用了getDataFromRemote2()的线程,然后返回来继续下面的执行,而背后是另外的线程在进行远程调用以及等待的工作。
参考资料
《大型网站系统与Java中间件实践》