目录
1 进程和线程
进程是一个正在执行的程序,一个程序可以同时执行多个任务(线程)。
进程独占内从空间,保持各自的运行状态,相互之间不会干扰,进程是在并发执行程序过程中资源分配和管理的最小单位(资源分配的最小单位)。每个进程都有独立的地址空间,每启动一个进程,系统就会分配地址空间。
通常一个任务被称作一个线程,线程有时候会被称为轻量级的进程。它是程序执行的最小单位,一个进程可以有多个线程,多个线程之间共享进程的地址空间以及一些进程级别的其他资源,但是各个线程拥有独立的栈空间。
进程和线程的区别和联系?
- 进程是资源分配的最小单位,线程是程序执行(CPU调度)的最小单位。
- 进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段,这种操作非常昂贵。
线程是共享进程中的数据的,使用相同的地址空间,因此CPU切换一个线程的花费远比进程要小很多,同时创建一个线程的开销也比进程要小很多。 - 线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式(IPC)进行。不过如何处理好同步与互斥是编写多线程程序的难点。
- 多进程程序更健壮,多线程程序只要有一个线程死掉,那么对于其共享资源的其他线程也会产生影响,而一个进程死掉并不会对另外一个进程造成影响,因为进程有自己独立的地址空间。
列举进程和线程的使用场景?
- 在程序中,如果需要频繁创建和销毁,则使用线程。因为进程创建和销毁开销很大(需要不停的分配资源),但是线程频繁的调用只是改变CPU的执行,开销小。
- 如果需要程序更加的稳定安全时,可以选择进程。如果追求速度,就选择线程。
2 并发和并行
并发:多个线程操作一个资源,不是同时执行,而是交替执行,单核cpu,只不过因为cpu的时间片很短,速度太快,看起来像同时执行。
并行:真正的同时进行,多核cpu,每一个线程都可以使用一个单独的cpu资源运行。
QPS:每秒能够响应的请求数。
并发用户数:系统可以承载的最大用户量。
平均响应时间:并发数/平均响应时间=QPS。
吞吐量:单位时间内处理的请求数。
互联网系统架构中,如何提高系统的并发能力?
垂直扩展(硬件)
- 提升单机的处理能力
- 增强单机的硬件性能:增加CPU的核数、内存升级、磁盘扩容
- 提升系统的架构能力:使用Cache来提高效率
水平扩展(软件:集群、分布式)
- 集群:多个人做同一事(同时多顾几个厨师同时炒菜)
- 分布式:一个复杂的事情,拆分成几个简单的步骤,分别找不同的人去完成。
1、站点层扩容:通过Nginx反向代理,实现高并发的系统,将服务部署在多个服务器上
2、服务层扩容:通过RPC框架实现远程调用:Dubbo,Spring Clodud,将业务逻辑分拆成不同的RPC Client,Clident完成各自的不同的业务,如果并发量比较大,新增加RPC Client。 - 数据层扩容:一台数据库拆分成多态,分库分表,主从复制,读写分离。
3 创建线程
3.1 继承Thread类
重写run()
class MyThread extends Thread{
@Override
public void run(){
System.out.println("hello world!");
}
}
public class demo1 {
public static void main(String[] args) {
Thread thread = new MyThread();
thread.start();
}
}
3.2 继承Runnable接口
重写run()
class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("implements Runnable to get a thread!");
}
}
public class demo1 {
public static void main(String[] args) {
Thread thread1 = new Thread(new MyRunnable());
thread1.start();
}
}
3.3 匿名线程
new Thread("watchTV"){
@Override
public void run(){
System.out.println("watchtv");
}
}.start();
new Thread("eating"){
@Override
public void run(){
System.out.println("eating");
}
}.start();
3.4 继承Callable接口
重写call()方法
class MyCallable<T> implements Callable<T>{
@Override
public T call() throws Exception {
return (T)"123";
}
}
public class demo1 {
public static void main(String[] args) {
Callable<String> myCallable = new MyCallable<>();
FutureTask<String> task = new FutureTask<>(myCallable);
Thread thread = new Thread(task);
thread.start();
try {
System.out.println(task.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
注意:
start方法本身是用来启动一个线程的,并将其添加到一个线程组里,此时线程获取cpu资源后就会执行所定义的run方法。run方法本身是一个普通的方法,并不会启动新的线程。
Callable和Runnable的区别?、
- Callable接口实现的是call方法,Runnable接口实现的是run方法。
- Callable的任务执行后有返回值,Runnable没有返回值。
- call()方法会抛出异常,run方法不能抛出异常。
4 守护线程
为了让进程退出后线程也能自动停止,引入了守护线程的概念。
守护线程能结束生命周期,但非守护线程不可以。
非守护线程
public class demo2 {
public static void main(String[] args) {
Thread thread = new Thread(){
@Override
public void run(){
try {
while(true){
TimeUnit.SECONDS.sleep(1);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
thread.start();
try {
TimeUnit.MILLISECONDS.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("main thread finished");
}
}
结果:进程结束后子线程无法结束
设置子线程为守护线程
public class demo2 {
public static void main(String[] args) {
Thread thread = new Thread(){
@Override
public void run(){
try {
while(true){
TimeUnit.SECONDS.sleep(1);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
thread.setDaemon(true);// *********************
thread.start();
try {
TimeUnit.MILLISECONDS.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("main thread finished");
}
}
结果:进程结束,子线程也跟着结束了
5 线程的生命周期
NEW(新建)状态
new Thread()创建线程对象。
RUNNABLE(就绪)状态
线程对象此时调用start方法,此时在JVM进程中创建了一个线程,线程并不是一经创建就直接得到执行,需要等到操作系统的其他资源,比如:处理器。
BLOCKED(阻塞)状态
等到一个监视器锁进入到同步代码块或者同步方法中,代码块/方法某一个时刻只允许一个线程去执行,其他线程只能等待,这种情况下等待的线程会从RUNNABLE状态转换到BLOCKED状态Objcet.wait()。
WAITIMNG(等待)状态
调用Object.wait()/join()/LockSupport.park()等方法,此时线程从RUNNABLE转换到WAITING状态。
TIME_WAITING(睡眠)状态
调用带超时参数的THread.sleep(long millis)/Object.wait(long timeout)/join(long milles)/LockSupport.parkNanos()/LockSupport.parkUntil等方法都会使得当前线程进入到TIMED_WAITING状态。
TERMINATED(终止)状态
是线程的最终状态,有如下三种情况会进入到终止状态
- 线程正常运行结束
- 线程运行出错
- JVM crash
6 线程常用方法
start()
用来启动一个线程 将其添加一个线程组当中 此时线程就会处于Runnable就绪状态
Thread.sleep()
sleep方法使得当前按线程指定毫秒级的休眠,暂停执行,不会放弃monitor锁的使用权
Thread A:monitor lock sleep
Thread B:期望获取monitor lock
jdk1.5之后,引入枚举类型TimeUnit,对sleep方法对其进行了封装,省去了时间单位换算的步骤。
- TimeUnit.HOURS.sleep(3);
- TimeUnit.MINUTES.sleep(27);
- TimeUnit.SECONDS.sleep(8);
yield()
yield属于启发式的方法。
线程A.yield(),会提醒调度器线程A愿意放弃本次的cpu资源,如果cpu资源不紧张,处理器有可能会忽略这种提示。
yield()和sleep()的区别
sleep()不会占用cpu资源,而yield会被其他线程占用cpu资源。
join()
含义:thread B中调用threadA.join(),此时thread B进入到等待状态,
直到当前threadA结束自己的生命周期或者达到join方法的超时时间。
先输出线程1,再输出线程2,最后输出主线程
public class demo3 {
public static void main(String[] args) {
Thread thread1 = new Thread(){
@Override
public void run() {
System.out.print("线程1:");
for(int i = 0;i<5;i++){
System.out.print(i+" ");
}
System.out.println();
}
};
Thread thread2 = new Thread(){
@Override
public void run() {
System.out.print("线程2:");
for(int i = 5;i<10;i++){
System.out.print(i+" ");
}
System.out.println();
}
};
try {
thread1.start();
thread1.join();
thread2.start();
thread2.join();
System.out.print("主线程:");
for(int i = 10;i<15;i++){
System.out.print(i+" ");
}
System.out.println();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
结果
实现线程中断的方法
- interrupt() 将java线程当中的中断状态位置为true
thread A : sleep()/join()/wait throw InterruptedException 可中断方法
以上方法都会使得当前进入阻塞状态,另外一个线程调用被阻塞线程的interrupt方法会
打断当前的这种阻塞状态,抛出一个InterruptedException的异常,这样的方法称之为
可中断方法。 并不是结束当前被阻塞线程的生命周期,只是打断了当前线程的阻塞状态。
thread B : thread A对象.interrupt() - isInterrupted() 判断中断状态位是否位true
public class demo4 {
public static void main(String[] args) {
Thread thread = new Thread(){
@Override
public void run() {
while(true){
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
System.out.println("thread被打断了!");
}
}
}
};
thread.setDaemon(true);
thread.start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("打断前:"+thread.isInterrupted());
thread.interrupt();
System.out.println("打断后:"+thread.isInterrupted());
}
}
- interrupted() 判断中断状态位是否为true
区别在于interrupted方法调用之后会擦除掉线程的interrupt标识
public class demo5 {
public static void main(String[] args) {
Thread thread = new Thread(){
@Override
public void run() {
while(true){
System.out.println(Thread.interrupted());
}
}
};
thread.setDaemon(true);
thread.start();
try {
TimeUnit.MILLISECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread.interrupt();
}
}
wait/notify/notifyAll
wait调用该synchornized同步代码块或方法当中,使得当前线程进入阻塞
notify/notifyAll唤醒当前的阻塞
Object类中的方法
7 线程优先级
每个线程都有优先级,优先级默认为5,优先级为1~10。
Java线程优先级继承于创建它的线程。
setPriority(int priority)
注意:优先级只能增加抢到cpu执行权的概率。
getPriority()用于获取线程优先级。
ThreadGroup用于对于一批线程进行管理
8 线程同步
场景:index = 0,为共享资源,多个线程同时执行index = index+1操作并输出,到50停止。
该执行语句会有三个步骤:取到index;+1;打印。在每个步骤的间隙产生进程切换时会产生如下问题:
1)某个数字会有重复
线程A:先取到index=0,cpu将执行权利交给了线程B。
线程B:index=index+1=0+1=1,输出1,cpu将执行权利交给了线程A。
线程A:+1=1,输出1。
2)某个数字被略过
线程A:index=index+1=0+1=1,cpu将执行权利交给了线程B
线程B:index=index+1=1+1=2,输出2,线程A再也无法获得当前cpu的使用权。
3)数字有可能超过最大值
线程A:取index=49,cpu将执行权利交给了线程B。
线程B:index=index+1=49+1=50,输出50。
线程A:再次去执行 index=50+1=51,超过范围。
显然多个线程同时操作共享资源,出现了数据不一致的问题。因为我们不清楚每一个线程什么时候开始执行什么时候执行结束,也就无法控制当前线程的最终结果。
8.1 并发编程的三大特性
8.1.1 原子性
原子操作是不可分割的操作,一个原子操作不会被其他线程打断,所以不需要同步。
int i = 10属于原子操作。i++不是一个原子操作。
多个原子操作合起来则不是一个原子操作,这时就需要进行同步。
8.1.2 可见性
当一个线程对共享变量进行了修改,那么其他线程可以立即看到修改后的最新值。
8.1.3 有序性
Java编译器会在运行期会优化代码的执行顺序,导致了代码的执行顺序未必就是开发者编写代码时的顺序。
临界资源:同一时刻只允许一个线程访问的资源。
临界区:访问临界资源的代码段。
8.2 synchronized关键字
防止线程干扰和内存一致性错误。
- 同步方法
public synchornized void sync(){
//表示要访问这个成员方法必须获取当前方法所在类的this引用的锁
}
- 同步代码块
public final Object obj = new Object();
public void sync(){
synchornized(obj){
//需要保证独占性的资源
}
}
8.2.1 Demo
练习:实现两个线程,线程A输出5,4,3,2,1,之后线程B再次输出5,4,3,2,1。
public class SynchornizedTest implements Runnable{
@Override
public synchronized void run() {
for(int i = 5;i>=1;i--){
System.out.print(i+" ");
}
System.out.println();
}
public static void main(String[] args) {
Runnable runnable = new SynchornizedTest();
new Thread(runnable).start();
new Thread(runnable).start();
}
}
8.2.2 底层原理
Java对象在内存中的布局分为3个部分:对象头,实例数据,和对齐填充。
对象头
对象头的结构如下:
虚拟机位数 | 头对象结构 | 说明 |
---|---|---|
32/64bit | Mark Word | 默认存储对象的hashCode、分代年龄、锁类型、所标志位等信息 |
32/64bit | Class Metadata | 类型指针指向对象的类元数据,JVM通过这个指针确定对该对象是哪个类型的数据 |
32/64bit | Array Length | 数组长度(如果当前对象为数组) |
Mark Word 被设计为非固定的数据结构,以便在极小的空间内存储更多的信息,它会根据对象的状态复用自己的存储空间。其结构如下:
monitor:
每个Java对象天生就自带了一把看不见的锁,它可以视为是一种同步工具或者是一种同步机制。monitor是线程私有的数据结构,每一个线程都有一个可用monitor 列表,同时还有一个全局的可用列表,如上面所说每一个被锁住的对象都会持有一个monitor。monitor结构如下:
说明 | |
---|---|
Owner | 初始时为NULL表示当前没有任何线程拥有该monitor record,当线程成功拥有该锁后保存线程唯一标识,当锁被释放时又设置为NULL。 |
EntryQ | 关联一个系统互斥锁(semaphore),阻塞所有试图锁住monitor record而失败的线程。 |
RcThis | 表示blocked或waiting在该monitor record上的所有线程的个数。 |
Nest | 用来实现重入锁的计数。 |
HashCode | 保存从对象头拷贝过来的HashCode值(可能还包含GC age)。 |
Candidate | 用来避免不必要的阻塞或等待线程唤醒,因为每一次只有一个线程能够成功拥有锁,如果每次前一个释放锁的线程唤醒所有正在阻塞或等待的线程,会引起不必要的上下文切换(从阻塞到就绪然后因为竞争锁失败又被阻塞)从而导致性能严重下降。Candidate只有两种可能的值,0表示没有需要唤醒的线程,1表示要唤醒一个继任线程来竞争锁。 |
同步代码块
monitorenter
每一个对象都跟一个monitor相关联,一个monitor的lock在某一个时刻只能够被一个线程锁获得,在一个线程中想要获取monitor的所有权,接下里有如下几件事情发生:
- 如果montior的计数器为0,则意味者当前monitor的lock还没有被获得,某个线程获得之后对该计数器加一,该线程就是这个monitor的所有者了。
- 如果当前这个线程已经获取这把monitor lock,再次获取导致monitor计数器再次+1。
- 如果monitor已经被其他线程所拥有,则当前线程会被陷入阻塞状态直到monitor的计数器变为0,再次去尝试获取对monitor的所有权。
monitorexit
释放对monitor所有权,即对monitor的计数器-1,当计数器结果为0,那就意味着当前线程不再拥有对monitor的使用锁。
同步方法
class文件中静态常量池ACC_SYNCHRONIZED,表示当前的方法是同步方法。
synchronized可重入锁
可重入锁:同一个线程重复请求自己持有的锁对象,可以请求成功而不会发生死锁
8.3 锁升级
在jdk1.6之后引入了偏向锁和轻量级锁。简单来说,一个线程执行时,是偏向锁。两个线程争抢锁时,是轻量锁。更多线程争抢锁时,锁升级为重量级锁。
8.3.1 偏向锁
为什么要引入偏向锁?
在大多数情况下,锁之间不仅不存在竞争,而且总是会由同一线程多次获得,所以引入了偏向锁。
当一个线程获得了锁,锁就进入偏向模式。
CAS(Compare And Swap):改变mark word中的线程id,是一个原子操作。
其有三个操作数:
- 需要读写的内存位置(V)
- 进行比较的预期值(A)
- 将写入的新值(B)
步骤:判断A和V是否一致,如果一致,将B写到V的位置。
如果CAS操作成功,那么说明当前线程成功获取偏向锁。如果失败,那么说明有其他线程抢先获取了这把锁,即存在线程竞争,此时,锁升级为轻量锁。
8.3.2 轻量锁
有两种情况可以获取到轻量锁:关闭了偏向锁功能;多个线程竞争偏向锁。
轻量级锁什么时候升级为重量级锁?
- JVM会在当前线程的栈帧中建立一个lock record的空间,用于存储当前的mark word副本。
- 尝试用CAS将对象的mark word中的指针指向lock record。
- 如果成功,当前线程获得锁。如果失败,表示其他线程在竞争锁,当前线程使用自旋来获取锁。当自旋次数达到一定次数时,这时又有一个线程3过来竞争这个锁对象,那么这个时候轻量级锁就会膨胀为重量级锁。
注意:
为了避免无用的自旋,轻量级锁一旦膨胀为重量级锁就不会再降级为轻量级锁了;偏向锁升级为轻量级锁也不能再降级为偏向锁。一句话就是锁可以升级不可以降级,但是偏向锁状态可以被重置为无锁状态。
8.3.3 重量级锁
重量级锁把除了拥有锁的线程都阻塞,防止CPU空转。
注意:由操作系统负责线程的调度和状态切换。
8.4 Volatile关键字
保证可见性:
与Java内存模型相关
保证有序性:
volatile关键字修饰变量,修饰的变量会存在一个lock;的前缀,这个前缀相当于是一个内存屏障,这个内存屏障可以提供:
- 确保指令重排序时不会将后面的代码排到内存屏障之前
- 确保指令重排序时不会将前面的代码排到内存屏障之后
- 确保在执行到内存屏障修饰的指令时前面的代码已经全部执行完成
- 强制地将线程工作内存中的修改刷新到主内存当中
- 如果是写操作,则会导致其他线程工作内存当中的缓存数据失效
在Java中,每个线程都对应一个工作空间,会将volatile修饰的共享变量的副本存入工作空间中,
实例:
int x = 10;
int y = x;
volatile int z = 100; //内存屏障
int sum = y + z;
thread A : z++; z=101; -》刷新到主内存
thread B : z=100 -》从主内存中获取最新的数据
练习
创建10个线程对index++ 1000次,获取10个线程加加之后的结果,结果应为10000。
public class VolatileTest implements Runnable{
private volatile static int index = 0;
public void increIndex(){
index++;
}
@Override
public synchronized void run() {
for(int i = 0;i<1000;i++){
increIndex();
}
}
public static void main(String[] args) throws InterruptedException {
Runnable runnable = new VolatileTest();
for(int i = 0;i < 10;i++){
Thread thread = new Thread(runnable);
thread.start();
thread.join();
}
System.out.println(index);
}
}
注意:volatile关键字不具备保证原子性的语义,只能够禁止指令重排序,保证共享变量修改
立即刷新至主内存。
9 悲观锁与乐观锁
悲观锁
总是假设最坏的情况,假设每一次去拿数据都默认别人会修改,所以每次拿数据都会上锁,这样就导致了其他线程想要拿数据就会阻塞,直到获取锁。synchornized关键字的实现是悲观锁。
悲观锁机制存在的问题:
- 多线程竞争下,加锁、解锁都会导致比较多的上下文切换和调度延时,引起性能问题。
- 一个线程池有锁会导致其他需要此锁的线程阻塞。
- 数据量大时,独占锁会导致效率低下。
- 可能导致优先级高的线程等待优先级低的线程释放锁,引起性能问题。
乐观锁
总是假设最好的情况,假设每次拿数据都默认别人不会修改,所以不会上锁,在更新时判断在此期间有没有人修改过这个数据。乐观锁适用于多读的场景,这样可以提高吞吐量。
乐观锁可以用CAS或版本号机制实现。
CAS
概念见上文。
Java对CAS的支持:
jdk1.5之后新增java.util.concurrent.atomic包下的AtomicXXX类。
建立在CAS的基础之上,在性能上都会有很大的提升。
例:在AtomicInteger中,有getAndIncrement方法,将i++这个操作封装为原子操作。
练习:运行1000个线程,使用乐观锁进行自增操作,观察结果。
public class test {
private static int value1 = 0; //线程不安全
private static AtomicInteger value2 = new AtomicInteger(0);//CAS
public static void main(String[] args) {
for(int i=0; i<1000; i++){
new Thread(){
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
value1++;
value2.getAndIncrement();
}
}.start();
}
//查看活跃线程数目
//活跃数目要大于2,主要原因是idea工具的原因,会有一个monitor线程用于监控
while(Thread.activeCount() > 2){
Thread.yield();
}
System.out.println("线程不安全:"+value1);
System.out.println("乐观锁:"+value2);
}
}
unsafe是用来帮助Java访问操作系统底层资源的类,如分配释放、释放内存
版本号机制
共享数据中增加一个字段version,表示该数据的版本号,如果当前的数据发生改变,版本号加1。对数据操作前先进行判断,如果当前version与之前拿到的version一致才会进行操作。
10 死锁
造成死锁的条件
- 互斥条件
某个资源在某一时间段内只能由一个线程占用。 - 不可抢占条件
线程所得到的资源在未使用完毕前,资源申请者不能强行夺取资源占有者手中的资源。 - 占有且申请条件
线程至少已经占有一个资源,此时又申请新的资源。 - 循环且等待条件
一个线程等待其他线程释放资源,其他线程又在等待另外的线程释放资源,直到最后一个线程等待第一个线程释放资源,这使得所有的线程锁住。
死锁的预防:打破产生死锁的四个必要条件其中一个或者多个
常见的死锁问题
-
交叉锁可能引起程序的死锁问题
-
内存不足
并发请求系统内存,如果当前系统内存不足,也有可能出现死锁的问题。
假设一个线程要获取30MB的内存才可执行完毕,而系统可用内存只有20MB。
thread1 已经获取了10MB内存,thread2 已经获取了20MB内存,造成互相等待资源释放。 -
一问一答式的数据交换
服务器开启某个端口,等待客户端去访问,客户端发起访问请求等到接收服务器端返回的资源,
可能由于网络问题服务器端错过了客户端的请求,造成互相等待。 -
死循环引起的死锁
哲学家就餐问题
问题描述:现有5位哲学家围坐就餐。他们的左右手边都有一支筷子,而每个人都需要一双筷子才能就餐。现在所有的哲学家都握住了自己右手边的筷子,他们每人都占有了一份资源,却都在等待其他资源释放才能执行完毕,同时构成了环,所以造成了死锁。
代码示例
class Chopstick{
protected String name;
public Chopstick(String name) {
this.name = name;
}
}
public class PhilosopherProblem extends Thread{
private String name;
private Chopstick left;
private Chopstick right;
public PhilosopherProblem(String name, Chopstick left, Chopstick right) {
this.name = name;
this.left = left;
this.right = right;
}
@Override
public void run() {
synchronized (left){
System.out.println(name+"拿到了"+left.name+"号筷子");
synchronized (right){
System.out.println(name+"拿到了"+right.name+"号筷子");
System.out.println(name+"正在吃");
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(name+"释放了"+left.name+"号筷子和"+right.name+"号筷子");
}
}
public static void main(String[] args) {
//五只筷子
Chopstick chopstick1 = new Chopstick("1");
Chopstick chopstick2 = new Chopstick("2");
Chopstick chopstick3 = new Chopstick("3");
Chopstick chopstick4 = new Chopstick("4");
Chopstick chopstick5 = new Chopstick("5");
//五位哲学家
new PhilosopherProblem("1",chopstick1,chopstick2).start();
new PhilosopherProblem("2",chopstick2,chopstick3).start();
new PhilosopherProblem("3",chopstick3,chopstick4).start();
new PhilosopherProblem("4",chopstick4,chopstick5).start();
new PhilosopherProblem("5",chopstick5,chopstick1).start();
}
}
结果可能出现:
解决方案:
class ChopSticks{
//key表示筷子的编号,value表示筷子可用状态,false是可用,true是不可用
protected static HashMap<Integer, Boolean> map = new HashMap<>();
static{
map.put(0, true);
map.put(1, true);
map.put(2, true);
map.put(3, true);
map.put(4, true);
}
}
class Philosopher implements Runnable{
public synchronized void getChopsticks(){
String currentName = Thread.currentThread().getName();
int leftChop = currentName.charAt(currentName.length()-1)-'0';
int rightChop = (leftChop+1) % 5; //leftChop+1;
while(!ChopSticks.map.get(leftChop) || !ChopSticks.map.get(rightChop)){
//有一个为true表示当前这个筷子正在被其他哲学家所使用
//当前线程需要阻塞等待
try {
this.wait(); //释放锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}
ChopSticks.map.put(leftChop, false);
ChopSticks.map.put(rightChop, false);
System.out.println(Thread.currentThread().getName() + " got the chopsticks " + leftChop +
" and "+rightChop);
}
public synchronized void freeChopsticks(){
String currentName = Thread.currentThread().getName();
int leftChop = currentName.charAt(currentName.length()-1)-'0';
int rightChop = (leftChop+1) % 5; //leftChop+1;
ChopSticks.map.put(leftChop, true);
ChopSticks.map.put(rightChop, true);
this.notifyAll();//唤醒等待当前这双筷子的哲学家
}
@Override
public void run() {
//获得左右两边筷子
getChopsticks();
System.out.println(Thread.currentThread().getName() + " is eating ");
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//释放手中的筷子
freeChopsticks();
}
}
public class TestDemo10 {
public static void main(String[] args) {
ChopSticks chopSticks = new ChopSticks();
Runnable philosopher = new Philosopher();
for(int i=0; i<5; i++){
new Thread(philosopher,"thread" + String.valueOf(i)).start();
}
}
}
结果
银行家算法
问题描述:现有五位商人(线程)向三位银行家(系统)借钱(资源),每位商人都有需要钱的总数(最大资源数)、已经借到的数目(已分配资源数)、还需要的钱数(还需要的资源数)。现在规定,商人必须从三位银行家那借到所有钱款,处理完成后才能归还,如果没有全部借到则不能归还。银行家的资金有限,为了保证自己借出去的钱可以解决商人的问题且如数归还,必须计划一个借款顺序。例如,当前线程可用资源:3,3,2,如果系统全给线程1,线程1没有得到所有需要的资源,就会进入等待,但是系统也因为资源没有释放无法给其他线程给而陷入等待,则会造成死锁问题。现不妨假设先把资源交给线程2,完全足够,线程2又会释放一些资源,则可以继续分配。在确认分配完成前,需要进行安全检测,必须计算出分配完资源后,至少存在一个方案可以分配完剩余的所有资源,那么称此次分配是安全的。
线程 | 最大资源数 | 已分配资源数 | 还需要的资源数 |
---|---|---|---|
线程1 | 8,5,3 | 0,1,1 | 8,4,2 |
线程2 | 3,2,2 | 2,0,0 | 1,2,2 |
线程3 | 9,0,2 | 3,0,2 | 6,0,0 |
线程4 | 2,2,2 | 2,1,1 | 0,1,1 |
线程5 | 4,3,3 | 0,0,2 | 4,3,1 |
代码示例
public class test {
//可用资源
private static int[] avaliable = new int[]{3,3,2};
//每个线程最大资源数
private static int[][] max = new int[][]{{8,5,3}, {3,2,2}, {9,0,2},{2,2,2}, {4,3,3}};
//每个线程已分配的资源
private static int[][] alloction = new int[][]{{0,1,1}, {2,0,0}, {3,0,2}, {2,1,1}, {0,0,2}};
//每个线程需要的资源数
private static int[][] need = new int[][]{{8,4,2}, {1,2,2}, {6,0,0}, {0,1,1}, {4,3,1}};
public static void showData(){
System.out.println("线程编号 最大需求 已分配 还需要");
for(int i=0; i<5; i++){
System.out.print(i+" ");
for(int j=0; j<3; j++){
System.out.print(max[i][j]+" ");//i表示线程号 j表示资源数
}
for(int j=0; j<3; j++){
System.out.print(alloction[i][j]+" ");//i表示线程号 j表示资源数
}
for(int j=0; j<3; j++){
System.out.print(need[i][j]+" ");//i表示线程号 j表示资源数
}
System.out.println();
}
}
//分配资源 requestNum表示所请求的线程号 request[]当前线程所请求的资源数
public static boolean allocate(int requestNum, int request[]){
if(request[0] > need[requestNum][0] || request[1] > need[requestNum][1] || request[2] > need[requestNum][2]){
System.out.println("请求的资源数目超过了当前这个线程还需要的资源数目");
return false;
}
if(request[0] > avaliable[0] || request[1] > avaliable[1] || request[2] > avaliable[2]){
System.out.println("目前没有足够的资源分配,必须等待");
return false;
}
//预分配资源给请求的线程
for(int i=0; i<3; i++){
//可分配的资源-请求资源数量
avaliable[i] = avaliable[i] - request[i];
//已经分配的资源alloction + 请求资源数目
alloction[requestNum][i] = alloction[requestNum][i] + request[i];
//还需要资源数need - 请求的资源数
need[requestNum][i] = need[requestNum][i] - request[i];
}
//进行安全性检查,true表示剩余资源能够满足其余线程的资源请求,false表示无法满足
boolean flag = checkSafe();
if(flag){
for(int j=0; j<3; j++){
avaliable[j] = avaliable[j] + alloction[requestNum][j];
}
System.out.println("能够安全分配");
return true;
}else{
//不能够通过安全性检查,撤销之前预分配的资源
System.out.println("不能够安全分配");
for(int i=0; i<3; i++){
//可分配的资源+请求资源数量
avaliable[i] = avaliable[i] + request[i];
//已经分配的资源alloction - 请求资源数目
alloction[requestNum][i] = alloction[requestNum][i] - request[i];
//还需要资源数need + 请求的资源数
need[requestNum][i] = need[requestNum][i] + request[i];
}
return false;
}
}
public static boolean checkSafe(){
/**
* 预分配操作:
* 循环遍历其余线程,查看可用资源是否能够满足其余线程的资源请求,
* 如果满足则进行下一次的遍历,如果不满足直接判断下一个线程
* 如果有可分配方案,则此次分配是一个安全分配。
*/
int i = 0;
int[] avaliable1 = new int[avaliable.length];
for(int a = 0;a<avaliable1.length;a++){
avaliable1[a] = avaliable[a];
}
boolean[] finish = new boolean[5];
while(i < 5){
if(finish[i] == false && need[i][0] <= avaliable1[0] && need[i][1] <= avaliable1[1] && need[i][2] <= avaliable1[2]){
System.out.print("分配成功的线程为:"+i+" ");
//执行成功之后还需要将所有资源释放
for(int j=0; j<3; j++){
avaliable1[j] = avaliable1[j] + alloction[i][j];
}
System.out.println();
finish[i] = true;//表示当前线程已经执行完
i = 0;
}else{
i++;
}
}
//while循环结束之后,所有finish标识都为true,表示所有线程都已经执行完
for(int m=0; m<5; m++){
if(finish[m] == false){
return false;
}
}
return true;
}
public static void main(String[] args) {
showData();
System.out.println("当前系统可用资源:");
for(int i=0; i<3; i++){
System.out.print(avaliable[i] + " ");
}
System.out.println();
//请求线程资源存放的数组
int[] request = new int[3];
int requestNum;
String source[] = new String[]{"A", "B", "C"};
Scanner s = new Scanner(System.in);
String choice = new String();
while(true){
System.out.print("请输入要请求的线程编号:");
requestNum = s.nextInt();
System.out.println("请输入要请求的资源数目:");
for(int i=0; i<3; i++){
System.out.print(source[i]+"资源的数目:");
request[i] = s.nextInt();
}
//分配资源
allocate(requestNum, request);
System.out.print("是否再次请求分配(y/n):");
choice = s.next();
if(choice.equals("n")){
break;
}
showData();
System.out.println("当前系统可用资源:");
for(int i=0; i<3; i++){
System.out.print(avaliable[i] + " ");
}
}
}
}
结果测试
线程通信
本节将详细说明synchronzied加锁的线程的Object类的wait/notify/notifyAll方法。
- wait()/notify()/notifyAll()是final native的方法,不能够被重写。
- 通过某一个对象调用它的wait()方法,obj.wait(),表示使得调用这个方法
的线程阻塞,并且当前线程必须要拥有当前这个对象的monitor lock。 - 调用某一个对象的notify()方法,obj.notify(),表示唤醒其中一个调用
了obj.wait()方法的线程,如果存在多个线程,当前这个方法只能唤醒一个。 - 调用某一个对象的notifyAll()方法,obj.notifyAll(),表示唤醒所有调
用了obj.wait()方法的线程去争抢锁。
为什么这三个方法不是Thread类中声明的方法,而是Object类中声明的
方法?
线程与锁是多对一的关系。每个对象都拥有对象锁(monitor lock),当某一个线程等待某一个对象的锁,需要使用对象去操作,即表明需要获取当前对象锁的线程等待/唤醒。
public class TestDemo12 {
public static void main(String[] args) {
String lock = new String("test");
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
/**
* 问题:IllegalMonitorStateException,没有用到监视器锁,所以会出现
* 异常
*/
String lock = new String("test");
synchronized (lock){
try {
lock.wait();
System.out.println("the current running thread is "+Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* wait()之后使得当前线程阻塞,wait方法之后的代码是不会执行的,释放当前所拥有的monitor lock
*/
String lock = new String("test");
new Thread("A"){
@Override
public void run() {
synchronized (lock){
try {
lock.wait();
System.out.println("the current running thread is "+Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
new Thread("B"){
@Override
public void run() {
synchronized (lock){
System.out.println("开始notify time: "+System.currentTimeMillis());
lock.notify();
System.out.println("结束notify time: "+System.currentTimeMillis());
}
}
}.start();
/**
* 结果:开始->结束->running
* notify()方法执行后并不会立即释放锁
*/
String lock = new String("test");
Thread thread = new Thread("A"){
@Override
public void run() {
synchronized (lock){
try {
lock.wait();
System.out.println("the current running thread is "+Thread.currentThread().getName());
} catch (InterruptedException e) {
System.out.println("the thread has been interrupted and the state is "+Thread.currentThread().isInterrupted());
}
}
}
};
thread.start();
thread.interrupt();
/**
* wait是可中断方法,可中断方法会收到中断异常InterruptedException,同时interrupt标识也会被擦除
*/
}
}
生产者与消费者问题:
现有一生产者和一万个消费者,还有一个阻塞队列。生产者将生产的东西放入队列中,消费者来取走。队列满了,生产者无法继续;队列空了,消费者无法继续。需用到wait和notify函数来控制,不会造成互相等待。
class BlockingQueue<E>{
private final LinkedList<E> queue = new LinkedList<>();
private static int max; //表示阻塞队列存储元素的最大个数
private static final int DEFAULT_MAX_VALUE = 10;
public BlockingQueue(){
this(DEFAULT_MAX_VALUE);
}
public BlockingQueue(int max){
this.max = max;
}
//生产数据
public void put(E value){
synchronized (queue){
//判断当前队列是否有位置存放新生产的数据
while(queue.size() >= max){
System.out.println(Thread.currentThread().getName() +" :: queue is full");
try {
//没有位置,当前生产数据的线程需要阻塞
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+":: the new value " +value+" has been produced");
queue.addLast(value);
queue.notifyAll();
}
}
//消费数据
public E take(){
synchronized (queue){
//判断当前队列是否存在可消费的数据
while(queue.isEmpty()){
System.out.println(Thread.currentThread().getName() +" :: queue is empty");
try {
//不存在,则调用消费数据的线程阻塞
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
E result = queue.removeFirst();
queue.notifyAll();
System.out.println(Thread.currentThread().getName()+":: the value " +result + " has been consumed");
return result;
}
}
}
public class ProducerAndConsumerDemo {
public static void main(String[] args) {
BlockingQueue<Integer> queue = new BlockingQueue<Integer>();
new Thread("Producer"){
@Override
public void run() {
while(true){
queue.put((int)(Math.random() * 1000));
}
}
}.start();
for(int i = 0;i<100000;i++){
new Thread("Consumer"+String.valueOf(i)){
@Override
public void run() {
while(true){
queue.take();
try {
TimeUnit.MILLISECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
}
}
}
但是以上代码有个问题是:当消费者很多时,消费者很容易唤醒其他消费者,从而造成资源浪费。改进方案:消费者定向唤醒生产者。
ReentrantLock
tryLock(timeout) 可定时的
lockInterruptibly() 可响应中断
tryLock() 可轮询
ReetrantLock的使用
Lock lock = new ReentrantLock();
lock.lock();//加锁
try{
//共享代码
}finally{
lock.unlock(); //释放锁
}
ReentrantLock释放锁是显式的,有可能会忘记清除锁,使用起来更加"危险"。
ReentrantLock是否是可重入锁?
XXXXXXXXXXXXXXXXXXXXXXX
ReetrantLock的实现
构造函数
// 默认创建非公平锁,即谁运气好,谁先获得cpu使用权,哪个线程就先获取执行
public ReentrantLock()
//传参true,就创建公平锁,即第一次获取锁的线程,在下一次执行也会优先获取锁
public ReentrantLock(boolean fair)
AbstractQueuedSynchronizer
AQS队列,先进先出。
底层:Node相连的双向链表。
获取锁
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
- tryAcquire 以独占锁的模式去获取当前的锁 可能成功也可能失败
- addWaiter 把当前的线程包装成一个节点Node插入到队列当中
- acquireQueued 尝试获取选择一个线程,获取锁
获取锁首先判断当前节点前驱是否为head,这是获取锁的资格
如果成功获取锁,则设置当前节点置为head节点;如果未成功获取锁,则根据
前驱节点判断当前线程是否要阻塞,shouldParkAfterFailedAcquire方法
在前驱状态不为signal的情况下会循环尝试获取锁,如果为signal则需要阻塞,
等待前驱节点释放锁之后将其唤醒。
释放锁:
Condition
实现线程间的通信
await():将满足当前Condition条件的线程加入到等待队列,释放其中的锁。
signal():唤醒一个在等待队列中的线程。
signalAll():唤醒所有在等待队列中的线程。
Condition con = lock.newCondition();
con.await()
注意事项:
- 使用await()必须要使用lock.lock()加锁,否则会抛出IllegalMonitorStateException。
- 使用await()使得当前线程阻塞,await()之后的代码不会执行。
- await()方法调用之后会释放当前的lock。
- signal()方法唤醒一个线程,但是该方法执行之后不会立即释放锁,出了锁的作用域才会将锁释放掉。
- await方法是可中断方法,可中断方法会收到中断异常InterruptException异常。
synchronized关键字下的 对象.wait/notify(All)方法 只有一个等待通道,无法根据逻辑区分线程,而Reentrant下的每个lock可以定义多个Condition对象,意味着有多个条件,即多个线程等待队列,需要让不同 Condition对象.await/signal(All)方法 才能唤醒其他等待队列上的线程。
练习:有三个线程A,B,C,需要线程交替打印ABCABCABCABC…, 打印10次,借助await/signal/signalAll实现。
0 1 2 3 4 5 6 7 8 9
A B C A B C A B C A
A:count % 3 = 0
B:count % 3 = 1
C:count % 3 = 2
public class ReentrantLockTest {
private static final Lock lock = new ReentrantLock(true);
private static final Condition A = lock.newCondition();
private static final Condition B = lock.newCondition();
private static final Condition C = lock.newCondition();
private static int count = 0;
public static void main(String[] args) {
Thread threadA = new Thread(){
@Override
public void run() {
lock.lock();
try {
for(int i = 0;i<10;i++){
while(count % 3 != 0){
A.await();
}
System.out.print("A");
count++;
B.signalAll();
}
}catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
};
Thread threadB = new Thread(){
@Override
public void run() {
lock.lock();
try {
for(int i = 0;i<10;i++){
while(count % 3 != 1){
B.await();
}
System.out.print("B");
count++;
C.signalAll();
}
}catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
};
Thread threadC = new Thread(){
@Override
public void run() {
lock.lock();
try{
for(int i = 0;i<10;i++){
while(count % 3 != 2){
C.await();
}
System.out.print("C ");
count++;
A.signalAll();
}
}catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
};
threadA.start();
threadB.start();
threadC.start();
}
}
练习:使用ReentrantLock中Condition实现生产者消费者模型
读写锁
ReadWriteLock lock = new ReentrantReadWriteLock();
共享读,但只能一个写,即读读不互斥、读写互斥、写写互斥。
读远远大于写的高并发情况下,一般会使用读写锁。
public class ReentrantReadWriteLockTest {
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private int num = 0;
public void read(){
lock.readLock().lock();
try{
for(int i = 0;i<3;i++){
System.out.println(Thread.currentThread().getName()+"正在读");
}
}finally {
lock.readLock().unlock();
}
}
public void write(int newNum){
lock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName()+"正在写"+num);
num = newNum;
System.out.println(Thread.currentThread().getName()+"写完了"+num);
}finally {
lock.writeLock().unlock();
}
}
}
class Main{
public static void main(String[] args) {
ReentrantReadWriteLockTest rwLock = new ReentrantReadWriteLockTest();
for(int i = 1;i<=5;i++){
new Thread("read"+i){
@Override
public void run() {
rwLock.read();
}
}.start();
}
for(int i = 1;i<=5;i++){
new Thread("write"+i){
@Override
public void run() {
rwLock.write(new Random().nextInt(10));
}
}.start();
}
}
}
结果
线程安全的集合
Collections.synchronizedXXX() 方法可以将线程非安全的集合以组合的方式封装为线程安全的集合(给get, set, add 等操作都加了 mutex 对象锁)。
Vector:ArrrayList 方法加上synchronized关键字,读读互斥,读写互斥。
HashTable:HashMap 方法加上synchronized关键字,读读互斥,读写互斥。
CopyOnWriteArrayList:非快速失败机制的代表。读读不互斥,写时拷贝一份新的数组对其操作,结束后将原来集合指向新的集合来完成写操作。源码里使用ReentrantLock可重入锁来保证不会有多个线程同时拷贝一份数组。
ConcurrentHashMap:高效,因为其锁的力度小。
属性介绍:
- table 哈希桶 默认是一个大小为16的数组
- nextTable 扩容时新生成的数组,大小为原数组的2倍
- sizeCtl 控制table的初始化和扩容
-1 代表table正在初始化。
-N 代表有N-1线程对map进行并发操作。
如果table未初始化,代表table需要初始化的大小。
如果table已经初始化完成,代表table的容量,默认是table大小的0.75倍。 - Node 保存key,value,hash值。
- ForwadingNode 特殊Node节点,当map发生扩容,ForwadingNode会发挥作用。
线程池
在jdk1.5之后,Executor接口用于创建线程池。
多线程计数最大限度地发挥了多喝处理器地计算能力,线程的数量和系统性能是抛物线的关系。线程数量过多,反而性能下降。
为什么要引入线程池?
- 创建和销毁线程的时间的和可能会远远大于线程的执行时间。
- 线程也需要占用内存空间,大量的线程占用的内存资源会比较多,可能导致OOM异常。
- 大量的线程回收会给GC带来很大压力。
- 大量的线程会抢占cpu的资源,cpu不停的进行各个线程的上下文切换。
线程池就是事先创建若干个可执行的线程放入一个池(容器)中,有任务需要执行时,从池子中获取一个线程执行任务,不用自行创建,执行完后不需要销毁线程而是将当前的线程归还到线程池当中,从而减少创建和销毁线程所带来的性能的开销。
线程池的优势:线程资源重复利用,降低系统资源的消耗,提高响应速度,提高线程的可管理性,线程池可以统一的调优和监控。
一个线程池的必备要素:
- 线程数量管控功能
- 任务队列:用于缓存提交的任务。
- 任务拒绝策略:任务队列是一个有界的队列,线程数量达到上限并且任务队列已满时,应拒绝任务提交者的请求。
- 线程工厂(加工当前线程)
- 任务队列需要限制值:limit。
线程池的使用
四种线程池
- 单一数量的线程池
class MyCallable1 implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName()+"::hello world");
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"::hello world,goodbye");
return (int)(Math.random()*100);
}
}
public class ExecutorTest1 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService pool = Executors.newSingleThreadExecutor();
MyCallable1 myCallable1 = new MyCallable1();
Future<Integer> result = pool.submit(myCallable1);
System.out.println(result.get());
pool.shutdown();
}
}
- 固定数量的线程池
ExecutorService pool = Executors.newFixedThreadPool(4);
- 周期性的线程池
class MyRunnable1 implements Runnable{
@Override
public void run() {
System.out.println("任务执行时间:"+new Date().getSeconds());
System.out.println(Thread.currentThread().getName()+"::hello world");
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"::hello world,goodbye");
}
}
public class ExecutorTest1 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ScheduledExecutorService pool = Executors.newScheduledThreadPool(5);
MyRunnable1 myRunnable1 = new MyRunnable1();
System.out.println("任务提交时间:"+new Date().getSeconds());
pool.schedule(myRunnable1,3,TimeUnit.SECONDS); //延迟3秒执行的任务
pool.shutdown();
}
}
- 可缓存的线程池
class MyCallable1 implements Callable<Integer> {
private CountDownLatch latch;
public MyCallable1(CountDownLatch latch){
this.latch = latch;
}
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName()+"::hello world");
latch.countDown();
return (int)(Math.random()*100);
}
}
public class ExecutorTest1 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService pool = Executors.newCachedThreadPool();
CountDownLatch latch = new CountDownLatch(30);
MyCallable1 myCallable1 = new MyCallable1(latch);
for (int i = 0; i < 30; i++) {
pool.submit(myCallable1);
}
try {
latch.await();
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println("main thread end");
pool.shutdown();
}
}
可以看到有重复,当有任务提交时,优先重复使用线程池中的空闲线程,但是如果线程池中没有空闲线程则会创建线程。
提交
execute() 和submit() 的区别
- void execute(Runnable command),无返回值。
- Future submit(Runnable task) / (Runnable task,T result) / (Callable task),有返回值。提交实现Runable的task也返回null。