程序是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,就是说程序是静态的代码。
进程就是一个程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。
线程是比进程更小的执行单位。一个进程在执行过程中可以产生多个线程,但是同一个进程的线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程或者在各线程之间进行切换,代价比进程切换要小。
关系:
系统运行一个程序即是一个进程从创建,运行到消亡的过程,所以说进程就是一个执行中的程序,在程序在执行时,将会被操作系统载入内存中,线程是进程划分成的更小的执行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。从另一角度来说,进程属于操作系统的范畴,主要是同一段时间内,可以同时执行一个以上的程序,而线程则是在同一程序内几乎同时执行一个以上的程序段。
线程的基本状态新建态,就绪态,运行态,阻塞态,终止态。
线程池
优点:
使用线程池可以减少创建和销毁线程的次数,每个工作线程都可以重复使用
可以根据系统的承受能力,调整线程池中工作线程的数量,防止线程过多导致系统崩溃。
提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
风险:
死锁
资源不足
并发错误
线程泄漏
请求过载
创建线程池
public ThreadPoolExecutor(int corePoolSize,//核心线程数 int maximumPoolSize,//允许最大线程数 long keepAliveTime,//当活跃线程数大于核心线程数时,空闲的多余线程最大存活时间 TimeUnit unit,//时间单位 BlockingQueueworkQueue,//任务执行队列 ThreadFactory threadFactory,//线程工厂,用于存放新建出来的线程 RejectedExecutionHandler handler//拒绝策略 ) { }
实现原理
线程池处理流程:
判断线程池中的核心线程是否都在执行任务,如果不是,则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则进入下一步。
线程池判断工作队列是否已满,如果工作队列没满,则将新提交的任务存储在这个任务队列中。如果队列满了,则进入下一步。
判断线程池中的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务。如果已满,则交给饱和策略来处理任务
Callable、Future、FutureTask介绍与关系
Callable是Runnable封装的异步运算任务
Future用来保存Callable异步运算结果
FutureTask封装Future的实体类
Callable与Runnable的区别
定义的方法不同,call()和run()
call()有返回值,run()没有返回值
call()可以抛出异常,run()不能
ThreadPoolExecutor、ThreadScheduledExecutor、ForkJoinPool
线程的生命周期,什么是僵死进程
僵死进程是指子进程退出时,父进程并未对其发出sigchld信号进行适当处理,导致子进程停留在僵死状态等待父进程收尸。该状态的子进程就是僵死进程。
如何实现进程安全互斥同步
临界区:Synchorized、ReentrantLock
信号量:semaphore
互斥量:mutex
非阻塞同步
CAS(Compare And Swap)
无同步方案
可重入代码
使用ThreadLocal类包装共享变量
线程本地存储
Synchorized,ReentrantLock
以互斥手段达到顺序访问的目的。操作系统提供了很多互斥机制比如信号量,互斥量,临界区资源等来控制在某一时刻只能有一个或者一组线程访问同一资源。
Synchorized是由语言级别实现的互斥同步锁,简单但机制笨拙,是由JVM实现的。jdk6之后性能提升,与ReentrantLock性能相差无几。
ReentrantLock是API层面的互斥同步锁,需要程序自己打开并在finally中关闭锁,和Synchorized相比更加灵活。等待可中断,公平锁以及绑定多个条件。
互斥同步锁都是可重入锁,可以保证不会死锁,但是因为涉及到核心态和用户态的切换,性能消耗大。之后有了一些优化:自旋锁和适应性自旋锁,轻量级锁,偏向锁,锁粗化和锁消除。
非阻塞同步锁(乐观锁)原子类(CAS)
非同步锁也叫乐观锁,对资源用版本号进行控制,可以先进性资源的修改,然后根据版本号是否与拿到数据时相同,若相同则修改,若不同,则重新取值进行相关计算,会一直重试直到成功。实现方式依赖处理器的机器指令:CAS(Compare And Swap)。CMPXCHG
JUC中提供了几个Automic类以及每个类上的原子操作就是乐观锁机制。不激烈情况下,性能比synchronized略逊,而激烈的时候,也能维持常态。激烈的时候,Atomic的性能会优于ReentrantLock一倍左右。但是其有一个缺点,就是只能同步一个值,一段代码中只能出现一个Atomic的变量,多于一个同步无效。因为他不能在多个Atomic之间同步。非阻塞锁是不可重入的,否则会造成死锁。
无同步方案可重入代码
在执行的任何时刻都可以中断-重入执行而不会产生冲突。特点就是不会依赖堆上的共享资源
ThreadLocal、Volaitile
线程本地的变量,每个线程获取一份共享变量的拷贝,单独进行处理
线程本地存储
如果一个共享资源一定要被多线程共享,可以尽量让一个线程完成所有的处理操作,比如生产者消费者模式中,一般会让一个消费者完成对队列上资源的消费。典型的应用是基于请求-应答模式的web服务器的设计
任务一般分为:CPU密集型、IO密集型、混合型,对于不同类型的任务需要分配不同大小的线程池
1、CPU密集型
尽量使用较小的线程池,一般Cpu核心数+1
因为CPU密集型任务CPU的使用率很高,若开过多的线程,只能增加线程上下文的切换次数,带来额外的开销
2、IO密集型
方法一:可以使用较大的线程池,一般CPU核心数 * 2IO密集型CPU使用率不高,可以让CPU等待IO的时候处理别的任务,充分利用cpu时间
方法二:线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程。
下面举个例子:
比如平均每个线程CPU运行时间为0.5s,而线程等待时间(非CPU运行时间,比如IO)为1.5s,CPU核心数为8,那么根据上面这个公式估算得到:((0.5+1.5)/0.5)*8=32。这个公式进一步转化为:
最佳线程数目 = (线程等待时间与线程CPU时间之比+ 1) CPU数目
3、混合型
可以将任务分为CPU密集型和IO密集型,然后分别使用不同的线程池去处理,按情况而定
Java的并发和并行并发:是指两个或多个事件在同一时间间隔发生在一台处理器上同时处理多个任务。
并行:是指两个或多个事件在同一时刻发生,在多台处理器上同时处理多个任务
如何提高并发量HTML静态化
图片服务器分离
数据库集群,分表分库
缓存
镜像
负载均衡
CDN加速技术
分布式部署,负载均衡就是将负载(工作任务、访问请求等)进行平衡、分摊到多个操作单元(服务器、组件等)上进行执行,是解决高性能,单点故障(高可用,如果你是单机版网络,一旦服务器挂掉了,那么用户就无法请求了,但对于集群来说,一台服务器挂掉了,负载均衡器会把用户的请求发送给其他的服务器进行处理),扩展性(这里主要是指水平伸缩)的终极解决方案。
nginx的负载均衡配置中默认是采用轮询的方式,这种方式中,每个请求按时间顺序逐一分配到不同的后端服务器,如果后端服务器down掉,能自动剔除,但存在各个服务器的session共享问题。
另外一种方式是ip_hash:每个请求按访问的ip的hash结果分配,如果访问的IP是固定的,那么在正常情况下,该用户的请求都会分配到后台的同一台服务器去处理,但是如果用户每次请求的IP都不同呢?所以这种方式也同1的方式一样都存在这么一个问题:session在各个服务器上的共享问题。
如果集群中的服务器的性能不一,可以通过配置各个服务器的权值来实现资源利用率的最大化,即性能好的优先选择。
解决服务器共享session问题:使用redis来共享各个服务器的session,并同时通过redis来缓存一些常用的资源,加快用户获得请求资源的速度(个人比较喜欢redis,当然你们也可以使用memcache来实现,不过,memcache不能做到持久化,这样这台服务器一挂掉,那么所有的资源也都没有了......)。
原理:使用一个可重入锁和这个锁生成的两个条件对象进行并发控制。
是一个带有长度的阻塞队列,初始化的时候必须要指定队列长度,且长度不能修改。
增加
add方法内部调用offer方法,如果队列满了,抛出IllegalStateException异常,否则返回trueoffer方法如果队列满了,返回false,否则返回true
add方法和offer方法不会阻塞线程,put方法如果队列满了会阻塞线程,直到有线程消费了队列里的数据才有可能被唤醒。
这3个方法内部都会使用可重入锁保证原子性。
删除
poll方法对于队列为空的情况,返回null,否则返回队列头部元素。
remove方法取的元素是基于对象的下标值,删除成功返回true,否则返回false。
poll方法和remove方法不会阻塞线程。
take方法对于队列为空的情况,会阻塞并挂起当前线程,直到有数据加入到队列中。
这3个方法内部都会调用notFull.signal方法通知正在等待队列满情况下的阻塞线程。
volatile关键字一旦一个共享变量被volatile修饰之后,那么就具备了两层语义:
保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这个新值会立刻加载到共享内存中。对其他的线程立即可见。
例如,有线程1和线程2,二者共享变量stop,线程1将主存中的stop加载到自己工作内存的缓存后,线程2对stop进行修改,由于有volatile修饰,所以线程1中的stop会立即失效,并从内存中获得更改后的stop
禁止进行指令重排序
voilatile无法保证对变量的操作原子性。
volatile能保证原子性和有序性
原理和机制:
加入volatile和没加入volatile所生成的汇编代码相差一个lock前缀指令。
lock前缀指令实际上相当于一个内存屏障。有三个功能:
确保指令重排序的时候不会将lock之前的放在后面或者之后的放在前面
会强制对缓存的修改操作立即写入主存
如果是写操作,会导致其他cpu的工作内存中的缓存失效
synchronized关键字是防止多个线程同时执行一段代码,影响效率,而volatile关键字在某些情况下优于synchronized,但是并不能替代他,因为volatile无法保证操作的原子性。
使用volatile必须具备两个条件:
对变量的写操作不依赖于当前值
该变量没有包含在具有其他变量的不变式中
适用场景:
状态标记量
double check
notify():唤醒在此对象监视器上等待的单个线程
notifyAll():唤醒在此对象监视器上等待的所有线程
wait():导致当前的线程等待,直到其他线程调用此对象的notify()或notifyAll()方法
都不属于Thread类,而是属于Object类
调用这些方法时一定是对晶竞争资源进行加锁,不加锁会报IllegalMonitorStateException 异常
当想要调用wait( )进行线程等待时,必须要取得这个锁对象的控制权(对象监视器),一般是放到synchronized(obj)代码中。
sleep方法是属于Thread类,而wait方法是属于Object类。
sleep方法导致线程暂停执行指定的的时间,让出cpu给其他线程,但是他的监控状态仍然保持者,当指定时间到了之后会立即恢复运行状态。
在调用sleep方法的过程中,线程不会释放对象锁
调用wati方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify或notifyAll方法后本线程才进入线程锁定池准备获取对象所进入运行状态。
start方法来启动线程真正实现了多线程的运行,这是无需等待run方法体代码执行完毕,可以直接继续执行下面的代码
通过调用Thread类的start方法来启动一个线程,这时此线程是处于就绪状态,并没运行。
方法run相当于线程的方法体,包含了执行的线程的内容,线程就进入运行态。
减少锁的时间
减少锁的粒度
ReentrantLock与synchronizedReentrantLock 通过方法 lock()与 unlock()来进行加锁与解锁操作,与 synchronized 会被 JVM 自动解锁机制不同,ReentrantLock 加锁后需要手动进行解锁。为了避免程序出现异常而无法正常解锁的情况,使用 ReentrantLock 必须在 finally 控制块中进行解锁操作。
ReentrantLock 相比 synchronized 的优势是可中断、公平锁、多个锁。这种情况下需要使用ReentrantLock。
两者的共同点:
都是用来协调多线程对共享对象、变量的访问
都是可重入锁,同一线程可以多次获得同一个锁
都保证了可见性和互斥性
两者的不同点:
ReentrantLock 显示的获得、释放锁,synchronized 隐式获得释放锁
ReentrantLock 可响应中断、可轮回,synchronized 是不可以响应中断的,为处理锁的不可用性提供了更高的灵活性
ReentrantLock 是 API 级别的,synchronized 是 JVM 级别的
ReentrantLock 可以实现公平锁
ReentrantLock 通过 Condition 可以绑定多个条件
底层实现不一样, synchronized 是同步阻塞,使用的是悲观并发策略,lock 是同步非阻塞,采用的是乐观并发策略
Lock 是一个接口,而 synchronized 是 Java 中的关键字,synchronized 是内置的语言实现。
synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而 Lock 在发生异常时,如果没有主动通过 unLock()去释放锁,则很可能造成死锁现象,因此使用 Lock 时需要在 finally 块中释放锁。
Lock 可以让等待锁的线程响应中断,而 synchronized 却不行,使用 synchronized 时,等待的线程会一直等待下去,不能够响应中断。
通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
Lock 可以提高多个线程进行读操作的效率,既就是实现读写锁等。
tryLock 能获得锁就返回 true,不能就立即返回 false,tryLock(long timeout,TimeUnit unit),可以增加时间限制,如果超过该时间段还没获得锁,返回 false
lock 能获得锁就返回 true,不能的话一直等待获得锁
lock 和 lockInterruptibly,如果两个线程分别执行这两个方法,但此时中断这两个线程, lock 不会抛出异常,而 lockInterruptibly 会抛出异常
将数据抽象成一个类,并将数据的操作作为这个类的方法
将数据抽象成一个类,并将对这个数据的操作作为这个类的方法,这么设计可以和容易做到同步,只要在方法上加”synchronized
将 Runnable 对象作为一个类的内部类,共享数据作为这个类的成员变量,每个线程对共享数据的操作方法也封装在外部类,以便实现对数据的各个操作的同步和互斥,作为内部类的各个Runnable 对象调用外部类的这些方法。
CAS 操作是抱着乐观的态度进行的(乐观锁),它总是认为自己可以成功完成操作。当多个线程同时使用CAS 操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。基于这样的原理,
CAS 操作即使没有锁,也可以发现其他线程对当前线程的干扰,并进行恰当的处理。
ABA问题比如说一个线程 one 从内存位置 V 中取出 A,这时候另一个线程 two 也从内存中取出 A,并且 two 进行了一些操作变成了 B,然后 two 又将 V 位置的数据变成 A,这时候线程 one 进行 CAS 操作发现内存中仍然是 A,然后 one 操作成功。尽管线程 one 的 CAS 操作成功,但是不代表这个过程就是没有问题的。
部分乐观锁的实现是通过版本号(version)的方式来解决 ABA 问题,乐观锁每次在执行数据的修改操作时,都会带上一个版本号,一旦版本号和数据的版本号一致就可以执行修改操作并对版本号执行+1 操作,否则就执行失败。因为每次操作的版本号都会随之增加,所以不会出现 ABA 问题,因为版本号只会增加不会减少。
什么是AQS(抽象的队列同步器)AbstractQueuedSynchronizer 类如其名,抽象的队列式的同步器,AQS 定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如常用的ReentrantLock/Semaphore/CountDownLatch。



