Threading 3|学习笔记

简介: 快速学习 Threading 3

开发者学堂课程【高校精品课-上海交通大学-企业级应用体系架构: Threading 3】学习笔记,与课程紧密联系,让用户快速学习知识。

课程地址:https://developer.aliyun.com/learning/course/75/detail/15836


Threading 3

 

内容介绍:

一、Threading 2补充

二、怎么去处理内存里面的缓存

 

一、Threading 2补充

1. Immutable Objects

讲多线程中谈到,之前举个例子

import java.util.Random;

public class Consumer implements Runnable {

private Drop drop;

public Consumer(Drop drop){ this.drop = drop;}

public void run() {

Random random = new Random();

for (String message = drop.take();

!message.equals("DONE");message=drop.take()) {System.out.format("MESSAGE RECEIVED: %s%n", message);

try {Thread.sleep(random.nextInt(5000)); }

catch (InterruptedException e){}

}

}

}

就是颜色的这个例子,这个颜色它的每一个方法都被同步,但是在两个方法的中间如果插进一个线程来进行操作,最后还是得不到想要的结果。所以说最好的方法就是一个对象一旦被创建出来,就不要修改它的值。这样的话,无论谁来读这个对象,读到的都是一个正确的值,一致的值。那之所以之前不这样考虑,原因是因为创建新对象代价比较大,比如说在 java 里内存是分代的,那它在初始代里面,它会尽量的去使用这块内存分配很多的对象,当它对象已经没有剩余空间的时候,如果代码里出现了 new 一个 object,就某一个对象的时候,那它会做一次垃圾回收机制,会把所有对象继续用的往老一代里搬,不用的就把它删掉,然后把你空出来去分配新对象。所以有可能就会非常费时间,那么为什么需要去创建新对象,而不是说把所有的方法全部同步。

2. A Synchronized Class Example

上节课举个例子,

public class SynchronizedRGB (

// Values must be between 0 and 255.

private int red;

private int green;

private int blue;

private String name;

private void check(int red, int green, int blue){

if (red <0 || red >255 || green < 0 || green >255 |[ blue < 0 1Ⅱ blue > 255)

{ throw new lllegalArgumentException();

}

}

public SynchronizedRGB(int red, int green, int blue, String name){

check(red, green, blue);

this.red = red;

this.green = green;

this.blue = blue;

this.name = name;

}

这一个颜色它实际上所有的方法,重要的方法全部都同步。但是会看到当有一个线程进来,它在两条语句中间做一个睡眠的话,比如一开始创建一个颜色。

public class SynChronizedRGBDemo

SynchronizedRGB

Public SynChronizedRGBDemo (){

Color= new SynchronizedRGB(0, 0,0,"Pitch Black);

}

Public void demo (){

Thread t1 = new Thread(new Runnable(){

@Override

Public void run(){

int myColorint= color.getRGB();

System.out.println( color.getName())

try {

Thread sleep(1000);

} catch (InterruptedException e){

e.printStackTrace();

}

String myColorName= color.getName(); //Statemi

System.out.println(myColorName);

}

}};

t1.start();

然后你想获取它的颜色,获取它的名字,如果连着执行没什么问题但是如果在它俩之间有一个间隔,获取了一个颜色RGB 的值,然后让它睡一会儿,再去获取它上面的内容,那么在这个空当里面,如果有另外一个线程T2进来把颜色改掉,那就会得到一个颜色是一直是黑色,但是它反映出来是红色的问题。

image.png

那这个问题呢,是因为所有的方法看起来都被同步了,你在执行这些同步的操作的中间有一个睡眠,有其它的这个线程进来把它改掉,这个对象本身它的状态没有变乱,就是在这里看到的,当这一边要把它变成红色的时候,它的值确实是红色的值。

问题是前面这个线程,它在没有变成红色之前,获取 RGB 的值睡着一秒的过程,被别人改写成了红色,RGB的值已经不是这个值,然后睡一秒之后再去打印它的名字,所以出现了这种情况,这个对象本身它的颜色,RGB的值和名字是配套,只是在读取 RGB 的值和名字的中间有一段时间什么都没做,在那里睡眠有其它线程进来改写了值。那这才出现了这个情况。

3. A Strategy for Defining Immutable Objects

那所以说最主要的问题就是因为有一系列的 setter 方法,让其它的线程可以修改为止。于是我在一个绘画里边,在多个操作中间的这个状态,如果有停止或者是休眠或者挂起这样的动作,就会有其它方法尽量改写值,之所以压根儿就不提供 setter 方法去修改它的值。如果用这种方法的话,所有的对象在任何一个时刻去看到这个对象的时候,它们的状态都是一样的,所以不提供这个值,并且把所有的阈全部变成 final。 final 意思就是说它只会初始化一次,以后再也不会被修改。private 就是说你如果跳出我的这个类,只能用 get 方法来访问,没有办法直接去访问这个对象的属性。

然后就是不允许子类去覆盖父类提供的方法,所以所有方法也都在,因为子类如果一旦覆盖这个父类方法,它可以在get 外面去做类似的设置,那就比较麻烦。总的来说就是把一个类变成了不可修改的类,现在看到是这个不可修改的RGB。

final public class ImmutableRGB(

// Values must be between 0 and 255.

final private int red;

final private int green;

final private int blue;

final private String name;

private void check(int ked, int green,int blue){

if (red <0 || red >255 || green <0 |l green >255 || blue <0 || blue >255)

{throw new lllegalArgumentException();}

}

public ImmutableRGB(int red, int green,int blue, String name)(

check(red, green, blue);

this.red = red;

this.green = green;

this.blue = blue;

this.name = name;

}

它仍然是有红绿蓝三个颜色分量值,然后还有颜色的值,然后 check 也没有变,构造器也没有变,还是要去检查一下,然后赋值给它。但问题是它只有一个 get RGB 和  getname。它不存在所有的 setter 方法都删掉,不存在setter方法。你不能去修改设置 read 或者各样的值。

当你有真正的写操作,比如说我想获取一个。把当前的颜色反一下,比如黑变白,白变黑这样动作,这时候大家看到它不是在修改当前这个颜色的值,它是新生成的一个对象。这个新生成的对象呢,只是当前这个颜色的反。所以它每一次一但有新的操作,想要改变的值不是在改变当前的值,而是创建一个新的对象具有新的值,使用这种方法来保证所有的对象,它不会被多线程这个并行的操作得到一个比较荒谬的结果。当然如果这样做,那也满足像在数学上定义的函数的这个概念,也就是 Y 等于 FX 的时候,无论你做什么样的计算,X 本身的值不发生变化,发生变化应该是y,就是我生成一个新值给它。

4.High Level Concurrency Objects

实际上 java 这个并发包里还有一些高级对象。拿到一个项目,就是第一个是允许自己去创建锁,之前都是在对象上面去锁,也就是说任何一个对象,之前说它本身就有一个内在的锁。就是一个内在的锁,每一次都是在一个对象同步获取这个锁。那另外一个是之前都是在自己的主线程里开了一个新的线程,让这个线程跑,就有两个线程在跑,那么还有一个叫线程池的东西。

Executors 来管理线程,把所有线程管理工作从主线程里剥离出来,靠 ExecutorsL 让它进行处理,再往下就是经常会看到有一个 list,或者是一个 set 这种集合类型,集合类型很明显的就是有可能多线程同时往里写,大家说有没有一种线程安全呢?这种和支出并发的这种集合类型。比如说一个 list 里面,要添加元素的时候,多线程同时添加,它不会乱。再往下就是它自动提供给原子变量,也就是说上节课说的,当你把一个 int,它是一个四字节的一个变量进行操作的时候,它实际上汇编语言至少要有五六条,你要把它装载到寄存器,然后从底下的最低字节做加法,就加一操作,然后把进位去跟倒数第二字节加,然后再把进位跟倒数第三个字节加,以此类推。

所以最后再把它从寄存器里放回到内存,那这个地方就可以看到多线程操作的时候,就有可能操作到前面一半儿,就会有另外一线程进来。那它可能会把这个状态给改掉,让它前后不一致,那么原子类型的变量,就是说它可以保证当你对这个变量操作的时候,它一定是同步。可以防止内存的这种不一致性错误。那再往下就是它可以有一个生成,伪随机的,就是一个看起来就生成了随机数量的这种线程,但是它其实是伪随机的,随机数可能大多数都是伪随机的,不是真正随机。它就是来帮生成了这个随机数量的这种线程,那你可以把这些线程全部丢到这个线程池里。

5.Lock Objects

锁对象,之前都是在当前对象上去做。那实际上就表示你的程序里面实际上只有这一个锁。那么如果你想明确的设计一个锁,不依赖于这个对象去锁的话,那就是 lock 对象可以来做的事情。举个例子

import java.util.concurrent.locks.Lock;

importjava.util.concurrent.locks.ReentrantLock;

import java.util.Random;

public class Safelock{

static class Friend {

private final String name;

private final Lock lock = new ReentrantLock();

public Friend(String name) {this.name = name;}

public String getName(){return this.name;}

Public boolean impendingBow(Friend bower)

{Boolean myLock = false;

Boolean yourLock = false;

try {

myLock = lock.tryLock();

yourLock = bowerlock.tryLock();

}finally {

if (! (myLock && yourLock)) {

if (myLock) (lock.unlockO; }

if (yourLock) ( bowerlock.unlock(); }

}

}

return myLock && yourLock;

friend 里面就定义了一个可重复的锁,刚才之前说过,A 如果获得了这个锁。在执行的过程当中,比如说一直到执行到这里才结束,它才释放这个锁。但是当它执行到中间一个步骤,可能又要求获取这个锁。这时候这锁实际上已经被,如果你不做锁的拥有者的判断,你会认为这个锁已经被别人占用了。

但是这个锁实际上它自己占用了,对于自己曾经占用的锁。可以再次获得这样一个可重复的锁。那所以这里定义为可重复的锁,然后它有 impendingBow 这个方法,就是说的鞠躬这个方法,它设计了这个两个变量。两个变量是两个force 对象,一开始大家都还没有动作,然后它要想要锁。就是要想要鞠躬,首先我在我自己这个对象给它调这个lock的可重复的锁。要把这个锁看能不能获取到。获取到之后因为我要给对方去鞠躬,所以对方它不能向我鞠躬,还要等我先给它鞠,然后它再鞠,所以我把被我鞠躬的这个对象,它上面的锁也给它获取一下,只有两个锁都获取了,我才做鞠躬这个动作,那否则的话,那我就不去做这个动作,那不管成功不成功。比如说我确实会觉得两个锁,或者是两个锁当中有一个没获取到,不管怎样。鞠躬完之后,最后都要去把锁给它释放掉,也就是说如果我最后这两个锁有一个没拿到,那它的这个与就是一个 force,也就是说除非你成功获得两个锁,否则的话,比如说我获取了一个锁,另外一个没拿到,应该把你已经占用的锁释放,才有可能让对方拿到两个锁,如果说我拿到了一个锁,另外一个锁没拿到,我就等那个锁,只要你拿不到这个锁,两个锁不能同时拿到,你就把你已经拿到了其中一个锁释放,所以在这里面就看一下你是不是拿到两个锁,不管锁是正常获取到还是异常,只要在这个获取两个锁这个动作。

你没有获得两把锁,那你获得了哪个锁就把哪个锁释放掉,这样就给了对方一个机会,能够获取到两把锁。所以如果两把锁全都获取到,它就会返回处理。所以在鞠躬之前,先要来判断能不能获得两把锁,如果不能同时获得,就释放掉锁。

然后现在开始鞠躬,鞠躬的时候,于是就用那个方法,去判断一下我能不能拿到两把锁,如果能拿,那就去鞠躬。所谓鞠躬就是我在这边输出了什么,然后我就叫它的回鞠这个动作。它就给我回鞠一下。鞠完躬之后,把两把锁全部释放。这就是明确定义一个锁对象,它在编程做一个跟之前的差异,它就不是在当前的这个 friend 对象在进行加锁,而是它明确的写了一个锁对象,通过这个对象你可以去尝试能不能获取这个锁。如果能获取,那就执行这边鞠躬操作,如果不能获取就不获取,所以这两个是有一个明显的差异,在这个对象直接上锁有明显差异,可以通过定义一个锁。来进行一个更灵活的控制。那现在这个东西如果大家再去看

 image.png

就说定一个鞠躬的循环,让两个人去鞠躬。那就在底下创建两个人,分别启动两个线程,让它们互相去鞠,然后它俩在鞠的时候就不会死锁。这是看到的输出,其中有一个动作会快一点拿到两把锁,然后另外一个再想要去的时候,它鞠不成,因为它会看到对方把它锁住,它会输出这样一个信息,然后第一个人鞠完之后释放两把锁,第二个人才开始鞠。 

6. Executors

Executors 像一个线程池一样,要把这个线程的管理工作,创建线程的这些工作,把它和之前写的代码分离开,就像前面是写了这个现成的东西,然后写了两个朋友,然后人为的要去处理这两个线程,然后让它们启动了。把能管理的工作给 Executors。Executors 就像一个线程池一样,你只要把线程丢给它,它来进行管理,所以以后就不是这样写。而是用这个

image.png

就是当我创建好一个对象,然后直接丢给这个线程池,它可以去帮我做了其它的事情,然后创建执行完之后会不会被销毁,这些东西都一概不管,这是一个和多线程相关的问题。开发的项目在 Tomcat 跑去访问数据库,其实你是有一个连接池。一个 connection 的一个 pool,实际上你并没有去写一个什么 new,一个 connection,都是在一个data source 上去 get connection。

那么 get connection 实际上就是你的代码再从连接池里要一个连接。连接池是 Tomcat 在启动的时候你再配datasource。那配套的方式有很多种,比如说你用那个 spring 的话,就是在a pplication properties这个文件里面去定义的如果你什么都不用,那你就可以在一个 contacts 里定义。反正不管怎么样,你配一个连接池,配置这个数据源要求创建的数据库的连接。那么这里面在配置中就有一个连接池的尺寸。就是能放多少个连接

假设说你的这个程序有一千万用户。然后并发数可能最高能达到1万。就按1 : 1000的在线一万并发。你觉得这个连接要配多大?你说一万个并发用户,我配500个连接,这样的话足够1万个人用,还是说1万个连接,我配一连接。这是很明显的一个多线程问题。一个连接就是一个线程。那么一个线程它是怎么跑的?需要在一个CPU的里跑。

那么你开100个连接有100个线程,这是个八核的一个机器于是我在100个线程里面跑,那你是不是就会有线程上下文的来回的切换。你说我把这100个连接给1000个人,1万个人服务,那某一个连接上有处理的激活,只有八个,所以任意时刻只能有八个线程在跑,你要有100个连接,100个连接的跑,你需要在这八个核之间上下文的来回的切换。那所以其实这个问题跟多少个人没有什么关系,跟你CPU的核有关系,所以在这种情况下,认为八个连接是最好的。所以一定要记住,连接不越大越好,开越大可能性越慢。而且一定要记住,不是说没有影响开八个就够八个以上就没有提高了,不是这个意思,是说超过八性能反而会下降,如果真开500个,那你的系统性能会非常的差,因为实际上八个核500个线程当中频繁的去做切换。

那500个连接服务于1万个一个服务20个用户。但问题是你在这里做现成的来回切换会花费大量的精力。CPU处理效率非常低,那这八个一个要处理1200多个,比如说1280个,这个这个客户端不会很慢,每一个线程会单独占用一个CPU 的核,所以处理起来会非常的快你处理数据库去操作它的时间就是毫秒。甚至可能到微秒级,所以你付的多不怕,每一个连接转一个这个 CPU 的核独占它,省去上下文切换的时间,的效率不会。所以一定要记住,这是一个多线程的一个很重要的应用场景,线程执行的快不快,上下文切换会带来很大的空间,你能控制它的数量就比较好。那另外一个问题,你说我机器是八核的,但是我带超线程功能,可以出来16个虚拟。要记住开16个不如8个这是虚拟出来的,是靠超线程一个模拟两个,在这两个线程本身还需要切换,所以8是在8核机器上一个比较好的一个配置。

配16个就是两个共享一个核,上面还会有线程的切换,那么线程切换的时间一定比执行时间要长的多。所以这是一个多线程的一个很重要的例子连接处不是越开越大越好,其实连接处应该跟你的 CPU 的核数成一种相关性,最好的就是一对一的,那不行的话,至少也应该是的整数倍,比如说16个就够了,再开到32就已经很多了,在上线程切换的代价更大

就不如你用一个 CPU 为一个线程专门服务,让所有人在这个专用的这个连接上去共享那线程池怎么用的呢?就是Executors 有这种 Executors service 这种接口,这个接口里面它有一个 Executors 方法,可以接这种这个runnable 的类型的对象,然后也可以接收callable对象。那么它会返回future对象,那它的一个子类叫scheduleExecutorService 的,它和定时器一样。就说我指定了一个延迟之后你再去执行,这就所谓的我安排好了一个三秒钟之后开始执行,那总的来说就是个线程池的概念那个连接就属于一个线程池的概念,那么连接池里面它可以有固定数量的这个线程,那么它的调度会比较方便,就像刚才连接池实际上就是固定数量的一个线程池。实际上不是你的应用决定了有多少个连接Tomcat 在启动的时候,根据你的 data source 的配置,它就直接创建好了到后台的数据库之间的连接。这个数量已经放在这儿,哪怕没有一个人去使用。这些连接也是创建好放在连接池里,在那里等待客户端使用。 

7. Fork/Join

下面要讲的就是你可以用 Fork/Join 这种方式,自己来实现一个产生新的线程这样的一个功能。那比如说碰到一个场景,就说我来判断一下,你现在让我做这个工作是不是足够小,如果是足够小了,我就开始工作,如果这个工作比较大,我就把我的工作一分为二,然后我调用两个线程同时去处理,其中每一个线程处理一部分。就是它的伪代码大概就这个样子

image.png

就基本上就是你的想法到达一定程度之后,就把它切开,另外一个线程和当时的线程两个直接去执行。那我举个例子,假设要做一个动作是把一张图片给它模糊化,然后给你一张图片,就是在这张图片上面。从左到右,一个像素一个像素去处理,处理完一行再处理下一行,处理过程是把它周围的八个点取出来,算一下平均值,把这个点填进去,那这样的话新的值就处理完了,最后整个这张图就是个模糊化的效果。但是要让你处理这一张图,那么可能太大,于是比如说把它切成四部分。用四个线程处理,在这四个线程里再判断可能还是太大,在切成多个线程,一直到比如说切完之后。只对这一块儿做处理,小到一定规模可以直接做。刚才说的把它周围八个点去做平均这个动作,你切完一个之后,你会发现你需要用更多的线程来产生新的线程,多线程并行去做这件事情,具体的代码

public class ForkBlur extends RecursiveAction{

private int[] mSource;

private int mStart;

private int mLength;

private int[] mDestination;

// Processing window size; should be odd.

private int mBlurWidth = 15;

public ForkBlur(int[] src, int start, int length, int, dst) {

mSource =src;

mStart = start;

mLength = length;

mDestination = dst;

}

它首先要扩展这个叫做递归的动作,这个类已经做好了一些事。有这么一个 ForkBlur,就用它来做它这个ForkBlurk能提供了 RecursiveAction,要做的事情就是要去做这个处理,那一开始工作器里面接收到这个图片,从什么位置到什么位置,要把它处理完,放到另外一个图,那个图片进来的那些像素。然后在这里要做的就是直接处理,直接处理呢,它的逻辑比较简单,这个其实跟多线程没什么关系,就是说我要我要把这个图片把它周围的这一圈给它取出来,取出来之后不断的去把它们里面的值去做相应的处理,那包括去做平均反正就处理完这个 RGB 的值,给它填回到一个目的地。目标这个数组里面去,具体逻辑可以忽略掉,反正就是说当你图小到一定程度时,那么这个整个compute是怎么做的呢?

它的做法是这样。当你要让我去对一张图去做这个处理的时候,我就来判断当前这张图是不是小于一个给定的阈值,就是说这张图里的像素是不是小于10万,如果小于它,那我就直接做,就做刚才这套逻辑,这套逻辑本身的合理不合理,不用去操心,反正就是说它执行逻辑,如果这个阀值没有到达这个之下。比如说它有100万像素。那先把它一切为二。这就是这里就是传递进来的它的这个长度、要处理多少个像素的这个模糊化的处理。

一千零二之后,产生两个新的 forkblur 的线程,就是看到这个第一个 forkblur。然后它说我把前一半儿给第一个,把后一半儿给第二,后一半儿大家可以看到,我把它一切为二,所以长度变成了原来的1/2,它说我从开始取1/2的长度是第一个。从开始加上1/2的这个长度,到最后的长度减去之前切掉了一部分,就是第二个 forkblur。现在就有两个,做一次递归切割,就把它切成两部分工作,把这两个线程跑起来,这两个跑起来之后又会跑到 compute这里来计算。当时是不是少于这个,如果还是没有小于,比如100万像素,你做一次切割是50万,那肯定到这里再切割,那每一个又切成量就有四个线程,每个是二十五万。二十五万进来肯定还要再切割又变成了八个线程,最后是16线程。然后每一个线程到16个线程,每个线程都很小。然后整个一张图就出来了,所以这就是用 fork /join 的一个例子。真正的运行的时候,起一个 forkblur 对象,这是最初的图像,从零一直要处理到所有的长度,把它写入 destination里面去,然后创建一个 pool,就是线程池,然后就是 fork/ join,它的框架把定义好的这样一个 pool,然后说把这个刚刚创建的对象扔给线程池,让它去调用,于是它就会去调用这个上面的 computer 方法,然后它就出来了新的,出来新的之后,这些线程又给了线程池,它又在那跑,所以到最后这个线程池里会有16线程在那里去跑,然后每一个只处理这个图的1/16,最后得出来的结果,大家拼到一起,就出来了完整的这个图像,这就是 folk/ join 的意思。那就是说现在通过folk/ join 可以不断的去产生新的线程,新的线程和旧的线程都在同一个线程池里面,让它们一起去跑,就看到一个比较复杂的应用场景。

8. Atomic Variables

原子性的变量是什么意思?就是之前说。你的这个整数类型的数字去进行操作,实际上是有若干条。这个汇编语言的实际上你不能保证它的原子性,那原子类型的变量的作用就是它可以保证你对它的操作全部都是原子性的,所以它可以保证如果对这个变量作多个操作。让它们能够满足 happens 结构。也就是说当我在写入这个变量的时候,不会有一个 get 方法,在我没有完成写入之前,插入进来,去读到一个中间状态的值。这个变量法变量怎么用呢?其实上节课讲过一个计数器的例子

class Counter(

private int c = 0;

public void increment(){

C++;

}

public void decrement() {

C--;

}

public int value(){

return c;

}

}

这是不加任何控制计数器,那说你这个C加加或者C减减这个动作。它本身有可能。比如说你执行了C减减四个字节的操作,当你刚执行完两个字节的操作的时候,有其它的操作进来要求去获取它的值。然后你就会发现它获取的是一个乱值,因为前两个字节是改写过,后两个字节是没有改写,为了解决这个问题。可以用它递增、递减和Value个方法,这样的话,这三个方法,任何一个被调用,其它方法不能调用,于是可以保证所有的人不会读到一个状态不一致的乱掉的基础值。但是还有另外一种方法,那就是不用,这里面看到 int 类。

用 AtomicInteger 这样的一个叫原子型整数这样的类,这个类似就是在保证当你做这种减法或者是这种加法或者减法的时候,加一会儿减一个。它一定是原子性完成,也就是说在调用这个 incrementAndGet 这个方法的时候。那你如果有人去掉 value 是得不到那个值的,它要保证上面这个方法结束之后才能。那这种编码就不用你人为的去写着synchronized 。

你如果忘了写这个,那还是不安全呢,这个就不怕,它也就把这个类封装好,它的操作一定是原子性。所以你如果你就不要求其实在这个 value 或者是其它上写出来,它自动能保证这一点。那有这样的东西存在的话,AtomicInteger是一种原子性的变量,所以这只是一种,大家去看   atomic 这个包里还有其它的原子。

你拿它们来做就可以,但是反过来说,我要不要把我的变量全部替换成这种类型,这种类型显然好处是多线程访问的时候不会出错,但是缺点也会相当的明显。那就是这种东西它肯定比较占资源,一方面是它是个对象,它本来就比int类型占用资源多。也就说在内存里面它占用资源多,它包括要加载它的类,然后去除变量,会有这样的一些资源占用,第二就是说它做的这种控制。那么显然比你去做比较好,也就是说你可能会忘掉,但是对于时间要求非常紧张,或者是现场控制要求非常精细的这种例子。用它来说就是有可能会显得这个同步的力度特别粗,想做更细力度控制做不了,所以有利就有弊,简单情况用它比较好,但是不能把所有变量全部用这种东西。那这种东西的话,我刚才说的至少它在内存里的占用是非常大。不能一下子走向另外一个极端。

9. From

image.png

这些东西全部来自于官方文档上,可以再去看一看,真正的有关并发的东西,Java 内库里提供的类会非常的丰富,有很多东西都没有讲,比如说像门栓,像刚才讲到的,它会返程返回一个 future 对象,Future 是什么,这些都没有再提到,那么关于现代化进程的一个解释,上网找了一个帖子,这个帖子写的比较有趣,就是如果你有意,如果大家是学过操作系统的话,肯定还是很清楚,但是它帮你整理了一下,有一些细节的东西大家可以去看一下,数据库的连接池这个概念其实是非常重要的,这个东西你知道了之后你就清楚,连接池不是把连接配的越大越好,所以大家以后在实际开发当中,这一点一定要记住。就是它通常来说是有一个限制要跟你的。CPU 的核的数量相关,不是多少个并发的影响,而且一定记住就是如果这是 CPU 核的数量。那么你的性能应该是多了不行,少了也不行,少了你没有充分利用 CPU,多了线程上下文导致性能下降。

 

二、怎么去处理内存里面的缓存

来谈另外的问题,还是跟数据访问相关。就是怎么样去做内存里面的缓存呢?

那么做内存缓存一定要注意,就是无论之前讲的 Hibernate。还是 Spring 的 JPA 它都有缓存。但是它们两个缓存有一个限制。我刚才说了,所有的东西都在 Tomcat 里跑。也就是说你的缓存实际上是在 Tomcat 里。

Tomcat 是在 Java 虚拟机里跑。Java 虚拟机在启动的时候需要去可以指定一下它的最大内存是多少,最小内存多少。就是说你在 Java 虚拟机的限制的话,也就意味着一方面 Hibernate 或者是 SpringJPA。它能利用的缓存的数量是有一个上限,而这个上限也许不太大,就是不能超过 Java 虚 拟机最大这个内存最大上限。第二个问题是,这两个缓存被它们封装了,你没法自己控制。想利用更大范围的缓存该怎么办?如果访问的不是 Hibernate,不是通过Hibernate或者是SpringJPA 去访问数据库的,那我怎么去缓存?要解决的问题是自己建一个独立于应用程序之外的一个缓存,应用程序可以去利用这个缓存来实现它的目标。这是第一节课讲的,要建立一个分布式缓存的一个服务器集群。在这个集群里面怎么样去利用这个环节,先谈谈这个内存的缓存,然后它既然是个集群,看看集群的一般的一些需要注意的东西。

1. Contents

缓存主要有两类东西,两个工具,一个 MemCached,一个 Readis,两个工程,一个就是用的 MemCached,一个用的Readis,,它俩实现的功能一样,就是在数据库里,就是大二上课的时候那两张表,Person 和 Even。然后它俩互相之间有引用,代码全部写好了,你们下来可以看到,那现在要做的事情就是。当我再访问到一个 Person的时候,访问逻辑都是到这个 cache 里面去找,当我找到了,我就直接返回给客户端。

当我找不到的时候,数据库里去读一次,读完之后把它放到缓存里。这样的话就会看到大量的数据,我只在在数据库里读一次,然后以后的操作全部从缓存里拿到,那么这个速度就很快,因为毕竟所有这些东西全部都是在Tomcat里。

而 DB 里面是真正存着这两张表的数据的,我只访问一次,访完之后拿到 cache 里就把它抽象成了 Person对象,或者是 Even 对象。真正的原始数据,无论是 person 还是 even,都存在数据库这边,那它俩之间的通信是个进程。它来操作独立的数据是很费时的。好不容易读回去一次,为什么不放到这里缓存,下次读直接从这里拿,这就跟你在浏览器里面,浏览器为什么会有浏览缓存,它的道理一样,它每一次会把你要访问的那个页面的。比如有个URL,它会把URL 加上在本地里找缓存有没有这个页面,如果有,它会把那个页面的版本,也就是说它 modified,那个是最后last modify 这个时间发回到服务器,这个服务器正在检验这个 last modified 字段,发现两边一样的时候,它就说其实你上次读懂之后,到现在没发生过变化,就从本地把数据加载上来就行。

那么大家看这两个场景就何其相似,都是把数据拿回来,只拿一次,到下一次的时候就不要再到远端,到进程外去拿,在这个进程,这个内存里,缓存要把它放到 Tomcat 外面,和到数据库里面去拿有什么区别?

刚才提到 Readis 里面它是一个单独进程,它的缓存可以开的比较大,而你这个数据库是在硬盘上的,Readis是在内存里头,所以一方面从内存里读一定比读硬盘要快,再一个内存是不受 Tomcat 里面或者是 SpringJPA的控制。你可以做更犀利的控制,你可以开更大的缓存,缓存更多的数据。所以极端的做法会一开机就把数据全部录入到Readis,以后只跟 Readis 打交道,就不要再去数据库再拿数据了。当然我指的是,如果你大多数都是读操作,可以这样做。

相关文章
|
6月前
|
Python
Threading
Threading
38 3
|
缓存 安全 Java
Threading 1 |学习笔记
快速学习 Threading 1
Threading 1 |学习笔记
|
存储 安全 Java
Threading 2 |学习笔记
快速学习 Threading 2
111 0
Threading 2 |学习笔记
|
Python
python多线程(threading库)
我们在平常工作中会遇到需要同时做操作的功能,这里我举一些实例给大家看下多线程的处理
175 0
python多线程(threading库)
|
安全 调度 Python
Python 多线程之threading介绍
Python 多线程之threading介绍
640 0
Python 多线程之threading介绍
C#编程-145:Threading线程基础
C#编程-145:Threading线程基础
C#编程-145:Threading线程基础
|
Python
python多线程执行任务Threading
python多线程执行任务Threading
|
Python
Python编程:threading多线程
Python编程:threading多线程
116 0