一言以蔽之:Java 并发编程的目的就是充分利用计算机的资源,把计算机的性能发挥到最大
什么是高并发
并发 concurrency 和并行 parallelism 的区别
并发是指多个线程操作同一个资源,不是同时操作,而是交替操作,单核 CPU,只不过因为速度太快,看起来是同时执行(张三、李四,共用一口锅炒菜,交替执行),通过时间片轮转机制RR调度实现并发。
速度太快,看起来像是并行。
并行才是真正的同时执行,多核 CPU,每个线程使用一个独立的 CPU 的资源来运行。(张三、李四,一人一口锅,一起炒菜)
并发编程是指使系统允许多个任务在重叠的时间段内执行的设计结构。
高并发我们设计的程序,可以支持海量任务的同时执行(任务的执行在时间段上有重叠的情况)
QPS:每秒响应的请求数,QPS 并不是并发数。
吞吐量:单位时间内处理的请求数,QPS 和并发数决定。
平均响应时间:系统对一个请求作出响应的平均时间,QPS = 并发数/平均响应时间
并发用户数:系统可以承载的最大用户数量。
高并发编程利弊
利:充分利用cpu的资源、加快用户响应的时间,程序模块化,异步化
弊:线程共享资源,存在冲突。容易导致死锁。 启用太多的线程,就有搞垮机器的可能。
互联网项目架构中,如何提高系统的并发能力?
垂直扩展
水平扩展
垂直扩展
提升单机的处理能力
1、增强单机的硬件性能:增加 CPU 的核数,硬盘扩容,内存升级。
2、提升系统的架构性能:使用 Cache 来提高效率,异步请求增加单个服务的吞吐量,使用 NoSQL 来提升数据的访问性能。
水平扩展
集群、分布式都是水平扩展的方案
集群:多个人做同一件事情(3 个厨师同时炒菜)
分布式:把一件复杂的事情,拆分成几个简单的步骤,分别找不同的人去完成 (1 洗菜、2 切菜、3 炒菜)
分布式是指多个系统协同合作完成一个特定任务的系统。分布式解决问题,把所有解决中心化管理的任务叠加到一个节点处理,太慢了。把一个业务拆分为多个子业务(当某个子业务访问量突增时,增加该子任务的节点数即可),部署在多个服务器上 ,分布式的主要工作是分解任务,将职能拆解。
集群主要的使用场景是为了分担请求的压力,同一个业务,部署在多个服务器上 ,也就是在几个服务器上部署来==相同的应用程序,==分担客户端请求。当压力进一步增大的时候,可能在需要存储的部分,mysql 无法面对很多的写压力。因为在 mysql 做成集群之后,主要的写压力还是在 master 的机器上面,其他 slave 机器无法分担写压力,从而这个时候,也就引出来分布式。分布式的主要应用场景是单台机器已经无法满足这种性能的要求,必须要融合多个节点,并且节点之间是相关之间有交互的。相当于在写 mysql 的时候,每个节点存储部分数据,也就是分布式存储的由来。存储一些非结构化数据:静态文件、图片、pdf、小视频 … 这些也就是分布式文件系统的由来。
集群主要是。分布式中的某个子任简单加机器解决问题,对于问题本身不做任何分解;分布式处理里必然包含任务分解与答案归并务节点,可能由一个集群来代替;集群中任一节点,都是做一个完整的任务。集群和分布式都是由多个节点组成,。但是集群之间的通信协调基本不需要;而分布式各个节点的通信协调必不可少RPC
将一套系统拆分成不同子系统部署在不同服务器上(这叫分布式),
然后部署多个相同的子系统在不同的服务器上(这叫集群),部署在不同服务器上的同一个子系统应做负载均衡。
负载均衡集群:一个集群节点来接受任务,然后根据其余节点的工作状态(CPU,内存等信息)派发任务量, 最终实现完成任务。
SOA:业务系统分解为多个组件,让每个组件都独立提供离散,自治,可复用的服务能力,通过服务的组合和编排来实现上层的业务流程
作用:简化维护,降低整体风险,伸缩灵活
微服务:架构设计概念,各服务间隔离(分布式也是隔离),自治(分布式依赖整体组合)其它特性(单一职责,边界,异步通信,独立部署)是分布式概念的跟严格执行SOA到微服务架构的演进过程 作用:各服务可独立应用,组合服务也可系统应用。SpringClond就是很经典的微服务框架。
具体的实行方案
1.站点层扩展:Nginx 方向代理,高并发系统,一个 Tomcat 带不起来,就找十个 Tomcat 去带
2.服务层扩展:通过 RPC 框架实现远程调用,Dubbo、Spring Boot/Spring Cloud,将业务逻辑拆分到不同的 RPC Client,各自完成不同的业务,如果某些业务的并发量很大,就增加新的 RPC Client,理论上实现无限高并发。
3.数据层扩展:一台数据库拆成多台,主从复制、读写分离、分表分库。
** 进程和线程**
进程就是计算机正在运行的一个独立的应用程序,进程是一个动态的概念,必须是运行状态,如果一个应用程序没有启动,那就不是进程。
线程就是组成进程的基本单位,可以完成特定的功能,一个进程是由一个或多个线程组成。
进程和线程的区别在于运行时是否拥有独立的内存空间,每个进程所拥有的空间都是独立的,互不干扰,多个线程是共享内存空间的,但是每个线程的执行是相互独立。
线程必须依赖于进程才能执行,单独线程是无法执行的,由进程来控制多个线程的执行。
Java 默认有几个线程?
public class OnlyMain {
public static void main(String[] args) {
//虚拟机线程管理的接口
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
ThreadInfo[] threadInfos =
threadMXBean.dumpAllThreads(false, false);
for(ThreadInfo threadInfo:threadInfos) {
System.out.println("["+threadInfo.getThreadId()+"]"+" "
+threadInfo.getThreadName());
}
}
}
Java 默认有两个线程:Main 和 GC
多线程实现方式
继承 Thread
实现 Runnable
实现 Callable
重点:Thread 是线程对象,Runnable 是任务,一线程启动的时候一定是对象。
```java
package com.southwind.demo;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class Test {
public static void main(String[] args) {
MyCallable myCallable = new MyCallable();
FutureTask futureTask = new FutureTask(myCallable);
Thread thread = new Thread(futureTask);
thread.start();
//获取Callable的返回值
try {
System.out.println(futureTask.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
class MyCallable implements Callable<String>{
@Override
public String call() throws Exception {
System.out.println("callable");
return "hello";
}
}
Runnable对象是Thread构造函数的参数,而Callable对象是FutureTask构造函数的参数,利用FutureTask这个对象,构建出最后的线程Thread。
Runable和Thread继承之后的时候,都需要重写run方法。而Callable对象,重写的是call方法。
Callable 与 Runnable 的区别:
Callable 的 call 方法有返回值,Runnable 的 run 方法没有返回值。
Callable 的 call 方法可以抛出异常,Runnable 的 run 方法不能抛出异常。
在外部通过 FutureTask 的 get 方法异步获取执行结果,FutureTask 是一个可以控制的异步任务,是对 Runnable 实现的一种继承和扩展。
get 方法可能会产生阻塞,一般放在代码的最后。Callable 有缓存。
线程状态
线程只有6种状态。整个生命周期就是这几种状态的切换。
run()和start() :run方法就是普通对象的普通方法,只有调用了start()后,Java才会将线程对象和操作系统中实际的线程进行映射,再来执行run方法。
yield() :Thread.yield(),让出cpu的执行权,将线程从运行转到可运行状态,但是下个时间片,该线程依然有可能被再次选中运行 。
线程的优先级 取值为1~10,缺省为5,最高10最低1,但线程的优先级不可靠,只是一个大概率执行而已,不建议作为线程开发时候
守护线程:和主线程共死,finally不能保证一定执行
synchronized 用法
修饰实例方法,对当前实例对象this加锁
public class SynchronizedDemo {
public synchronized void methodOne() {
.....
}
}
修饰静态方法,对当前类的Class对象加锁
public class SynchronizedDemo {
public static synchronized void methodTwo() {
......
}
}
修饰代码块,指定加锁对象,对给定对象加锁
public class SynchronizedDemo {
public void methodThree() {
// 对当前实例对象this加锁
synchronized (this) {
}
}
public void methodFour() {
// 对class对象加锁
synchronized (SynchronizedDemo.class) {
....
}
}
}
volatile
volatile适合于只有一个线程写,多个线程读的场景,因为它只能确保可见性。
java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保通过排他锁单独获得这个变量。Java语言提供了volatile,在某些情况下比锁更加方便。如果一个字段被声明成volatile,java线程内存模型确保所有线程看到这个变量的值是一致的。
这句话的含义有两层.
volatile 的写操作, 需要将线程本地内存值,立马刷新到 主内存的共享变量中.
volatile 的读操作, 需要从主内存的共享变量中读取,更新本地内存变量的值.
由此引出 volatile 的内存语义.
当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存.
当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量,并更新本地内存的值.
PS :本来在JVM中有共享变量区以及方法的栈区,栈区里有共享区变量的高速缓存的,一般方法区直接用高速缓存区数据来运算。
volatile 的特性
可见性 : 对一个volatile的变量的读,总是能看到任意线程对这个变量最后的写入
互斥性 : 同一时刻只允许一个线程对变量进行操作.(互斥锁的特点)
不具有原子性:复合操作不具有(如inc++ 等价inc= inc+ 1)
敲黑板,划重点,volatitle不具有原子性的重点
假如某个时刻变量inc的值为10,线程1对变量进行自增操作,线程1先读取了变量inc的原始值,然后线程1被阻塞了;
然后线程2对变量进行自增操作,线程2也去读取变量inc的原始值,由于线程1只是对变量inc进行读取操作,而没有对变量进行修改操作,所以不会导致线程2的工作内存中缓存变量inc的缓存行无效,也不会导致主存中的值刷新,所以线程2会直接去主存读取inc的值,发现inc的值时10,然后进行加1操作,并把11写入工作内存,最后写入主存。
然后线程1接着进行加1操作,由于已经读取了inc的值(inc++,包括3个操作,1.读取inc的值,2.进行加1操作,3.写入新的值),注意此时在线程1的工作内存中inc的值仍然为10,所以线程1对inc进行加1操作后inc的值为11,然后将11写入工作内存,最后写入主存。
那么两个线程分别进行了一次自增操作后,inc只增加了1。根源就在这里,自增操作不是原子性操作,而且volatile也无法保证对变量的任何操作都是原子性的。
解决方案:可以通过synchronized或lock,进行加锁,来保证操作的原子性。也可以通过使用AtomicInteger
什么是JMM
JMM(java memory model)决定一个线程对共享变量的写入何时对另一个线程可见,JMM定义了线程和主内存之间的抽象关系:共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存保存了被该线程使用到的主内存的副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。
声明变量为volatile后 方法区中的数据本地缓存就失效了,每次都要在主内存中拿。
ThreadLocal
线程局部变量。进来类型简单点哦,可以理解为是个map,比如类型 Map<Thread,Integer>,跟Python中的线程局部变量类似。
public class UseThreadLocal {
//可以理解为 一个map,类型 Map<Thread,Integer>
static ThreadLocal<Integer> threadLaocl = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return 1;
}
};
/**
* 运行3个线程
*/
public void StartThreadArray() {
Thread[] runs = new Thread[3];
for (int i = 0; i < runs.length; i++) {
runs[i] = new Thread(new TestThread(i));
}
for (int i = 0; i < runs.length; i++) {
runs[i].start();
}
}
/**
* 类说明:测试线程,线程的工作是将ThreadLocal变量的值变化,并写回,看看线程之间是否会互相影响
*/
public static class TestThread implements Runnable {
int id;
public TestThread(int id) {
this.id = id;
}
public void run() {
System.out.println(Thread.currentThread().getName() + ":start");
Integer s = threadLaocl.get();//获得变量的值
s = s + id;
threadLaocl.set(s);
System.out.println(Thread.currentThread().getName() + ":" + threadLaocl.get());
//threadLaocl.remove();
}
}
public static void main(String[] args) {
UseThreadLocal test = new UseThreadLocal();
test.StartThreadArray();
}
}
wait notify
wait是指在一个已经进入了同步锁的线程内,让自己暂时让出同步锁,以便其他正在等待此锁的线程可以得到同步锁并运行,只有其他线程调用了notify方法(notify并不释放锁,只是告诉调用过wait方法的线程可以去参与获得锁的竞争了,但不是马上得到锁,因为锁还在别人手里,别人还没释放),调用wait方法的一个或多个线程就会解除wait状态,重新参与竞争对象锁,程序如果可以再次得到锁,就可以继续向下运行。
wait()、notify()和notifyAll()方法是本地方法,并且为final方法,无法被重写。
当前线程必须拥有此对象的monitor(即锁),才能调用某个对象的wait()方法能让当前线程阻塞。(这种阻塞是通过提前释放synchronized锁,重新去请求锁导致的阻塞,这种请求必须有其他线程通过notify()或者notifyAll()唤醒重新竞争获得锁)
调用某个对象的notify()方法能够唤醒一个正在等待这个对象的monitor的线程,如果有多个线程都在等待这个对象的monitor,则只能唤醒其中一个线程; (notify()或者notifyAll()方法并不是真正释放锁,必须等到synchronized方法或者语法块执行完才真正释放锁)
调用notifyAll()方法能够唤醒所有正在等待这个对象的monitor的线程,唤醒的线程获得锁的概率是随机的,取决于cpu调度
调用yield() 、sleep()、wait()、notify()等方法对锁有何影响?
线程在执行yield()以后,持有的锁是不释放的,操作系统仍然可能选择到该进程。不释放锁
sleep()方法被调用以后,持有的锁是不释放的,休眠期间操作系统不会再执行该进程。不释放锁
调动方法之前,必须要持有锁。调用了wait()方法以后,锁就会被释放,当wait方法返回的时候,线程会重新持有锁 .
wait方法是面对对象的,表示当前的对象会进入阻塞,在notify/all调用之后,会重新进入就绪状态。
调动方法之前,必须要持有锁,调用notify()方法本身不会释放锁的,只是告诉调用过wait方法的线程可以去参与获得锁的竞争了.