构建Java中间件必备的基础知识

说之前谈两个之前一直有疑问的地方:

反向代理和负载均衡有何区别?

反向代理,是把一些静态资源存储在服务器上,当用户有请求的时候,就直接返回反向代理服务器上的资源给用户,而如果反向代理服务器上没有的资源,就转发给后面的负载均衡服务器负载均衡服务器再将请求分发给后端的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中间件实践》

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

mangoBUPT

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值