多线程并发

1,同步和异步
同步和异步通常用来形容一次方法调用。

1)同步方法
①概念
调用一旦开始,调用者必须等到方法调用返回后,才能继续后续的行为。
目的:都是为了解决多线程中的对同一变量的访问冲突

②场景
比如银行的转账系统,对数据库的保存操作等等,都会使用同步交互操作。

③实现方式
i>ThreadLocal
隔离多个线程的数据共享,从根本上就不在多个线程之间共享资源,这样当然不需要多个线程进行同步了。

ii>synchronized( )
iii>wait()
iv>notify()
v>volatile
2)异步方法
①概念
调用更像一个消息传递,一旦开始,方法调用就会立即返回,调用者就可以继续后续的操作。而,异步方法通常会在另外一个线程中,“真实”地执行着。整个过程,不会阻碍调用者的工作。

②场景
2,缓存一致性(缓存连贯性、缓存同调)
1)概念
在多核情况下,每条线程可能运行于不同的CPU 中,因此每个线程
运行时有自己的高速缓存(对单核CPU 来说,其实也会出现这种问题,只不过
是以线程调度的形式来分别执行的)。为了保留在这些高速缓存中的共享资源,保持数据一致性的机制,叫做缓存一致性。

2)方案
为了解决缓存不一致性问题,通常来说有以下2 种解决方法:

①通过在总线加LOCK#锁的方式
CPU 和其他部件进行通信都是通过总线来进行的,如果对总线加LOCK#锁的话,就阻塞了其他CPU 对其他部件访问(如内存),从而使得只能有一个CPU 能使用这个变量的内存。这样做的缺点是:在锁住总线期间,其他CPU 无法访问内存,导致效率低下。

②通过缓存一致性协议
该协议保证了每个缓存中使用的共享变量的副本是一致的。
核心思想:当CPU 向内存写入数据时,如果发现操作的变量是共享变量,即在其他CPU 中也存在该变量的副本,会发出信号通知其他CPU 将该变量的缓存行置为无效状态,因此当其他CPU 需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。

3,synchronized(同步块)
1)概念
用synchronized 关键字来实现同步块,所有加上synchronized 的块语句,在多线程访问的时候,同一时刻只能有一个线程能够访问,另一个线程必须等到当前线程访问完毕才能访问此代码块,解决了多线程的安全问题。即:加锁(java.util.concurrent.locks.Lock接口的实现类 也是显示锁的一种实现)。

2)缺陷
多个线程判断锁,耗费资源,降低代码执行效率。

3)作用
①实现操作的原子性
什么是原子性?
原子操作即:不可分割的单一的操作。
如 i++可以分为3步原子操作:① 读取变量 i 当前值 ②拿 i 的当前值,和1做加法运算 ③将加法结果赋值给 i 变量。
非原子操作可能会受到其他线程的干扰。
synchronized关键字实现操作的原子性,本质是通过该关键字所包括的临界区的排他性保证在任何一个时刻只有一个线程能够执行临界区中的代码。

②保障内存的可见性
什么是内存的可见性?
CPU在执行代码时,为减少变量访问时间将其缓存到CPU的缓存区。如此再次访问变量,是从cache中读取而不是从内存读取。同样也未写入内存。每个CPU有自己的缓存区,其内容对其他CPU不可见。

③保证多线程代码的正确性
保证一个线程执行临界区中的代码时,所修改的变量值对于稍后执行该临界区的线程来说是可见的。

4)使用
同步常用的有三种:

①同步块
用的锁是obj。

demo

package Thread;

import javax.swing.plaf.SliderUI;

public class MyThread implements Runnable {

    private int i = 4;
    Object obj = new Object();

    @Override
    public void run() {
        while (true) {
            /*
             * 对象如同锁,持有锁才可以在同步中执行。
             * 没有锁的即使获取cpu的执行权,也无法进入同步块。
             * 要等待当前进程释放锁,其他进程才能抢占对象锁。
             */
            synchronized (obj) {
                
                if (i > 0) {

                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {

                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "--this is MyThread:" + i--);
                }
            }
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
②同步方法
用的锁是this。

/**
*线程安全的计数器
*/
public class ThreadSafeCounter{
    private int counter = 0;
    
    public void increment(){
        synchronized(this){
            counter++;
        }
    }

    public int get(){
        synchronized(this){
            return counter;
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
③静态同步方法
用的锁是该类。
因为静态方法中不能定义this。静态进内存时,内存中没有本类对象,但是一定有该类对应的字节码文件对象。静态同步方法,使用的锁是该方法所在类的字节码文件对象:通过【类名.class】,获取。

demo
2-(4)-③通过标识退出线程

4,volatile
1)概念
轻量级的synchronized,在多处理器编程中保证共享变量的统一性。

2)场景
双重校验锁DCL(double checked locking)–使用volatile 的场景之一。
由于有些时候对 volatile的操作,不会被保存,说明不会造成阻塞。不可用与多线程环境下的计数器。

3)内存语义
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile 修饰之后,那么就具备了两层语义:

①保证内存的可见性
不像synchronized能保证操作的原子性。
一个线程对一个采用volatile关键字修饰的变量的值的更改对于其他访问该变量的线程而言总是可见的。

【可见性】指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。(volatile 解决了线程间共享变量的可见性问题)
1
第一:使用volatile 关键字会强制将修改的值立即写入主存;
第二:使用volatile 关键字的话,当线程2 进行修改时,会导致线程1 的工作内存中缓存变量stop 的缓存行无效(反映到硬件层的话,就是CPU 的L1或者L2 缓存中对应的缓存行效);
第三:由于线程1 的工作内存中缓存变量stop 的缓存行无效,所以线程1再次读取变量stop 的值时会去主存读取。
那么,在线程2 修改stop 值时(当然这里包括2 个操作,修改线程2 工作内存中的值,然后将修改后的值写入内存),会使得线程1 的工作内存中缓存变量stop 的缓存行无效,然后线程1 读取时,发现自己的缓存行无效,它会等待缓存行对应的主存地址被更新之后,然后去对应的主存读取最新的值。那么线程1 读取到的就是最新的正确的值。

②禁止指令重排序(Re-order),阻止编译器对代码的优化
编译器和CPU为了提高指令的执行效率可能会进行指令重排序,这使得代码实际执行方式和我们认为的不同。这样编译器就不会对volatile 做多余的优化,让代码执行时更符合我们的期望。
volatile 关键字禁止指令重排序有两层意思:
i> 当程序执行到volatile 变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
ii> 在进行指令优化时,不能把volatile 变量前面的语句放在其后面执行,也不能把volatile 变量后面的语句放到其前面执行。

3)CAS (compare and swap,比较并交换)
指一个旧的预期值A,主内存的值是B,要修改的值C,当且仅当A==B的时候,A的值才会被修改成C,而且这个操作是原子性的,是一个非阻塞性的 乐观锁。

扩展一:同步锁的分类?
Synchronized 和Lock 都是悲观锁。
乐观锁,CAS 同步原语,如原子类,非阻塞同步方式。
扩展二:锁的分类?
一种是代码层次上的,如java 中的同步锁,可重入锁,公平锁,读写锁。
另外一种是数据库层次上的,比较典型的就是悲观锁和乐观锁,表锁,行锁,页锁。
扩展三:java 中的悲观锁和乐观锁?
悲观锁:悲观锁是认为肯定有其他线程来争夺资源,因此不管到底会不会发生争夺,
悲观锁总是会先去锁住资源,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放
锁。Synchronized 和Lock 都是悲观锁。
乐观锁:每次不加锁,假设没有冲突去完成某项操作,如果因为冲突失败就重试,直
到成功为止。就是当去做某个修改或其他操作的时候它认为不会有其他线程来做同样的操
作(竞争),这是一种乐观的态度,通常是基于CAS 原子指令来实现的。CAS 通常不会将
线程挂起,因此有时性能会好一些。乐观锁的一种实现方式——CAS。

4)实现机制
①概念
加入volatile 关键字时,编译器在生成字节码时,会在指令序列中插入内存屏障,会多出一个lock 前缀指令。
编译器和CPU 可以在保证输出结果一样的情况下对指令重排序,使性能得到优化。处理器在进行重排序时是会考虑指令之间的数据依赖性。

②内存屏障
概念
是一组处理器指令,解决禁止指令重排序和内存可见性的问题。

作用
1.先于这个内存屏障的指令必须先执行,后于这个内存屏障的指令必须后执行。
2.使得内存可见性。
所以,如果你的字段是volatile,在读指令前插入读屏障,可以让高速缓存中的数据失效,重新从主内存加载数据。在写指令之后插入写屏障,能让写入缓存的最新数据写回到主内存。

类型
i> LoadLoad 屏障
对于这样的语句Load1; LoadLoad; Load2,在Load2 及后续读取操作要读取的数据被访问前,保证Load1 要读取的数据被读取完毕。
ii> StoreStore 屏障
对于这样的语句Store1; StoreStore; Store2,在Store2 及后续写入操作执行前,保证Store1 的写入操作对其它处理器可见。
iii> LoadStore 屏障
对于这样的语句Load1; LoadStore; Store2,在Store2 及后续写入操作被刷出前,保证Load1 要读取的数据被读取完毕。
iv> StoreLoad 屏障
对于这样的语句Store1; StoreLoad; Load2,在Load2 及后续所有读取操作执行前,保证Store1 的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。

③lock 前缀指令在多核处理器下会引发了两件事情
1.将当前处理器中这个变量所在缓存行的数据会写回到系统内存。
这个写回内存的操作会引起在其他CPU 里缓存了该内存地址的数据无效。但是就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题,所以在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内
存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进
行修改操作的时候,会强制重新从系统内存里把数据读到处理器缓存里。
2.它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面
的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全
部完成。

5)volatile 和synchronized 区别
1.volatile 是变量修饰符,而synchronized 则作用于代码块或方法。
2.volatile 不会对变量加锁,不会造成线程的阻塞;synchronized 会对变量加锁,可能会造成线程的阻塞。
3.volatile 仅能实现变量的修改可见性,并不能保证原子性;而synchronized 则可以保证变量的修改可见性和原子性。
(synchronized 有两个重要含义:它确保了一次只有一个线程可以执行代码的受保护部分(互斥),而且它确保了一个线程更改的数据对于其它线程是可见的(更改的可见性),在释放锁之前会将对变量的修改刷新到主存中)。
4.volatile 标记的变量不会被编译器优化,禁止指令重排序;synchronized 标记的变量可以被编译器优化。

6)什么场景下可以使用volatile 替换synchronized?
只需要保证共享资源的可见性的时候可以使用volatile 替代,
synchronized 保证可操作的原子性、一致性和可见性。

5,java.util.concurrent.locks.Lock
1)概念
2)用法
使用Lock 必须在try-catch-finally 块中进行,并且将释放锁的操作放在finally 块中进行,以保证锁一定被释放,防止死锁的发生。通常使用Lock 来进行同步的话,是以下面这种形式去使用的:

Lock lock = ...;
lock.lock();
try{
    //处理任务
}catch(Exception ex){

}finally{
    lock.unlock(); //释放锁
}
1
2
3
4
5
6
7
8
9
3)Lock 和synchronized 的区别
1)Lock 是一个接口,而synchronized 是Java 中的关键字,synchronized 是内置的语言实现;
2)synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock 在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock 时需要在finally 块中释放锁;
3)Lock 可以让等待锁的线程响应中断(可中断锁),而synchronized却不行,使用synchronized 时,等待的线程会一直等待下去,不能够响应中断(不可中断锁);
4)通过Lock 可以知道有没有成功获取锁(tryLock()方法:如果获取了锁,则返回true;否则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待),而synchronized 却无法办到。
5)Lock 可以提高多个线程进行读操作的效率(读写锁)。
6)Lock 可以实现公平锁,synchronized 不保证公平性。
在性能上来说,如果线程竞争资源不激烈时,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock 的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。

5,JAVA 的内存模型。
1)概念
区别于“JVM 的内存模型”。
Java 内存模型规定所有的变量都是存在主存当中(类似于前面说的物理内存),每个线程都有自己的工作内存(类似于前面的高速缓存)。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作,并且每个线程不能访问其他线程的工作内存。

2)原子性、可见性以及有序性
要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。

①原子性
即一个操作或者多个操作要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
可以通过Synchronized 和Lock 实现“原子性”。
在java 中,只有对除long 和double 外的基本类型进行简单的赋值(如int a=1)或读取操作,才是原子的。只要给long 或double加上volatile,操作就是原子的了。

例题:请分析以下哪些操作是原子性操作。

x = 10; //语句1 是原子性操作,其他三个语句都不是原子性操作。
y = x; //语句2 实际上包含2 个操作,它先要去读取x 的值,再将x 的值写入工作
x++; //语句3 包括3 个操作:读取x 的值,进行加1 操作,写入新的值。
x = x + 1; //语句4 包括3 个操作:读取x 的值,进行加1 操作,写入新的值。
1
2
3
4
②可见性
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线
程能够立即看得到修改的值。
通过Synchronized 和Lock 和volatile 实现“可见性”。

③有序性
有序性:即程序执行的顺序按照代码的先后顺序执行。

虚拟机可能会对程序代码进行指令重排序,最终执行的结果是与程序顺序执行的结果一致的,它只会对不存在数据依赖性的指令进行重排序。因此,在单个线程中,程序执行看起来是有序执行的。事实上,这个规则是用来保证程序在单线程中执行结果的正确性,但无
法保证程序在多线程中执行的正确性。

3)happens-before 原则(先行发生原则)
①程序次序规则
一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作。

②锁定规则
一个unLock 操作先行发生于后面对同一个锁的lock 操作

③volatile 变量规则
对一个变量的写操作先行发生于后面对这个变量的读操作

④传递规则
如果操作A 先行发生于操作B,而操作B 又先行发生于操作C,则可以得出操作A 先行发生于操作C

⑤线程启动规则
Thread 对象的start()方法先行发生于此线程的每个一个动作

⑥线程中断规则
对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生

⑦程终结规则
线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行

⑧对象终结规则
一个对象的初始化完成先行发生于他的finalize()方法的开始。

6,网站的高并发,大流量访问怎么解决?
1)HTML 页面静态化
访问频率较高但内容变动较小,使用网站HTML 静态化方案来优化访问速度。将社区内的帖子、文章进行实时的静态化,有更新的时候再重新静态化也是大量使用的策略。
优势:
一、减轻服务器负担。
二、加快页面打开速度,静态页面无需访问数据库,打开速度较动态页面有明显提高;
三、很多搜索引擎都会优先收录静态页面,不仅被收录的快,还收录的全,容易被搜索引擎找到;
四、HTML 静态页面不会受程序相关漏洞的影响,减少攻击,提高安全性。

2)图片服务器和应用服务器相分离
现在很多的网站上都会用到大量的图片,而图片是网页传输中占主要的数据量,也是影响网站性能的主要因素。因此很多网站都会将图片存储从网站中分离出来,另外架构一个或多个服务器来存储图片,将图片放到一个虚拟目录中,而网页上的图片都用一个URL 地址来指向这些服务器上的图片的地址,这样的话网站的性能就明显提高了。
优势:
一、分担Web 服务器的I/O 负载-将耗费资源的图片服务分离出来,提高服务器的性能和稳定性。
二、能够专门对图片服务器进行优化-为图片服务设置有针对性的缓存方案,减少带宽成本,提高访问速度。
三、提高网站的可扩展性-通过增加图片服务器,提高图片吞吐能力。

3)数据库
见“数据库部分的—如果有一个特别大的访问量到数据库上,怎么做优化?”。

4)缓存
尽量使用缓存,包括用户缓存,信息缓存等,多花点内存来做缓存,可以大量减少与数据库的交互,提高性能。假如我们能减少数据库频繁的访问,那对系统肯定大大有利的。比如一个电子商务系统的商品搜索,如果某个关键字的商品经常被搜,那就可以考虑这部分商品列表存放到缓存(内存中去),这样不用每次访问数据库,性能大大增加。

5)镜像
镜像是冗余的一种类型,一个磁盘上的数据在另一个磁盘上存在一个完全相同的副本即为镜像。

6)负载均衡
在网站高并发访问的场景下,使用负载均衡技术(负载均衡服务器)为一个应用构建一个由多台服务器组成的服务器集群,将并发访问请求分发到多台服务器上处理,避免单一服务器因负载压力过大而响应缓慢,使用户请求具有更好的响应延迟特性。

7)并发控制
加锁,如乐观锁和悲观锁。

如:订票系统,某车次只有一张火车票,假定有1w 个人同时打开12306 网站来订票,如何解决并发问题?(可扩展
到任何高并发网站要考虑的并发读写问题)。
不但要保证1w 个人能同时看到有票(数据的可读性),还要保证最终只能由一个人买到票(数据的排他性)。使用数据库层面的并发访问控制机制。采用乐观锁即可解决此问题。乐观锁意思是不锁定表的情况下,利用业务的控制来解决并发问题,这样既保证数据的并发可读性,又保证保存数据的排他性,保证性能的同时解决了并发带来的脏数据问题。hibernate 中实现乐观锁。

8)消息队列
通过mq 一个一个排队方式,跟12306 一样。

7,一台客户端有三百个客户与三百个客户端有三百个客户对服务器施压,有什么区别?
300个用户在一个客户端上,会占用客户机更多的资源,而影响测试的结果。
线程之间可能发生干扰,而产生一些异常。
300个用户在一个客户端上,需要更大的带宽。
IP地址的问题,可能需要使用IP Spoof来绕过服务器对于单一IP地址最大连接数的限制。
所有用户在一个客户端上,不必考虑分布式管理的问题;而用户分布在不同的客户端上,需要考虑使用控制器来整体调配不同客户机上的用户。同时,还需要给予相应的权限配置和防火墙设置。
 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值