Java高并发理论篇
作为IT程序猿,掌握多线程是作为服务器开发人员不可缺少的技能之一,同时在单核CPU的情况下,为了模拟多核的情况,我们也必须掌握多线程编程的问题,下来我们了解一下并行编程:
一:前提知识的相关概念
同步、异步:
同步异步通常形容方法调用,
同步:在方法调用中,同步方法指的是方法的执行必须有序进行,当前方法的执行必须在上一个方法的结束;即有序执行
异步:在方法调用中,方法的执行在另一个线程中真实地执行,当前的调用线程可以处理别的事情;即更像是一个命令实施者,具体的工作由别的线程完成,
并发、并行:
并发:多个任务轮流争取CPU的资源,多个任务交替执行,看起来好像多个程序一起执行;
其实还是多个任务在共享一个CPU;
并行:每个线程使用一个CPU,真正的一起执行;
临界区:
表示一个公共资源或者共享数据,可以被多个线程使用;每一次,只能有一个线程使用它,一旦临界区资源被专用(加锁),其他线程想要使用这个资源,就必须等待;
阻塞、非阻塞:
阻塞:当一个线程占用了临界资源,那么当其他线程想要访问临界资源的时候,就必须等待,等待会导致线程挂起,即将当前线程放入到等待队列中,直到被唤醒;
非阻塞:线程之间的执行不受共享资源的状态影响,即不会被挂起;
线程之间的锁机制引发的问题->死锁、活锁、饥饿:
死锁:线程之间互相占据对象需要的资源,又不肯释放资源,一直处理循环等待的过程,导致一直僵持的过程;例如A方法执行期间有临界资源 A1, B1;B方法执行期间有临界资源 B1,A1;而A方法需要使用B1资源后,才可以释放释放A1,的锁,而B方法需要使用A1的资源后,才可以释放B1的锁,因此造成 A 等待 B1, B等待A1,造成线程的一直等待;
饥饿:线程因为某种原因(例如线程的优先级比较低)一直等待资源,无法获取所需要的资源,导致一直无法执行;
活锁:两个线程互相主动将资源释放,让给对方,但没有一个线程可以同时拿到所有资源而正常执行;
进程,线程:
进程是系统进行资源分配和调度的基本单位,进程是基本执行的实体,是线程的容器,运行一个程序,就相当于运行一个进程。当我们程序有很多小的任务需要执行的时候,我们使用进程进行任务之间的切换,比较费时,且数据交换麻烦,因此引入线程;
线程:是程序执行的最小单位,线程之间可共享数据,线程之间切换的成本较低,一个进程包含很多线程;
多线程的一些性质:
原子性:Atomicity
原子性指的是一个操作不可能被中断,即如果多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰;
对于32位操作系统,long型数据的读写不是原子性的。
可见性:Visibility
可见性是指当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道这个修改;对于串行来说,不存在这个问题,此问题只针对于并行;
有序性:Ordering
当程序执行时,有可能会进行指令重拍,重拍后的指令和原指令的顺序未必一致;
线程的状态:
创建New,启动Runnable,阻塞Blocked,停止Waiting,结束Terminated,有限等待Time_Waiting;
New新建状态表示刚刚创立的简称,这个线程还没有开始执行,需要等到start()方法调用时,才表示线程执行;
Runnable运行状态表示线程所需的一些资源都已经准备好了,可以执行
Blocked阻塞状态,表示当遇到Synchronized同步块是,就会进行blocked阻塞状态,这个线程会暂停执行,直到获取请求的锁
Waiting/Time Waiting:Waiting等待状态会进入一个无时间限制的等待,Time_Waiting会进行一个有时间的等待;
Terminated结束:当线程执行完毕,就会进行结束阶段;
线程的方法:
终止线程stop():
Thread提供了一个stop()方法,stop()方法会立即终止一个线程,并且会立即释放这个线程所持有的锁,而锁机制是用来维持对象的一致性,所以直接释放锁会导致数据有可能不一致,stop在eclipse中被标记为废弃的方法,所以尽量不要使用stop()方法来停止线程;
线程中断Interrupt():
中断就是让目标线程停止执行,线程中断并不会使线程立即退出,而给线程发送一个通知,告知目标线程,Thread.interrupt()方法,会通知目标线程中断,设置中断标志位,中断标志位表示当前线程已经被中断,Thread.isInterupted()用来判断线程是否中断,Thread.interrupted()方法用来判断当前线程的中断状态,但同时会清除当前线程的中断标志位状态;
睡眠sleep():
Thread.sleep()方法使当前线程休眠,进入阻塞状态(暂停执行),如果线程在睡眠状态被中断,将会抛出IterruptedException中断异常。
等待方法wait()和notify():
这两个方法不在Thread类中,而是输出Object类,即线程A中,调用obj.wait()方法,那么线程A就会停止继续执行,转为等待状态,直到obj.notify()方法为止;当一个线程调用Object.wait(),那线程会进入object对象的等待队列,当调用object.notify()被调用时,它就会从这个等待队列中,随机选择一个线程,并将其唤醒。Object.wait()方法并不能随便调用,它必须包含在对应的synchronized语句中,无论wait还是notify都需要首先获得目标对象的一个监视器(锁),wait()方法在执行后,会释放这个监视器;Object对象的notifyAll()被调用的时候,它会唤醒这个等待队列中所有等待的线程,而不是随机选择一个;
挂起suspend():
线程挂起suspend和继续执行resume,是一对相反的操作,被挂起的线程必须要等到resume()后,才能继续执行;
suspend()在导致线程暂停的同时,并不会去释放任何锁的资源,即它还持有对象的锁,因此,其他任何线程想要访问锁的资源时,都会被阻塞,导致无法继续运行,直到对应的线程进行了resume()操作,被挂起的操作才会继续执行;
等待线程结束join()、谦让yield():
线程调用join:即这个线程需要等待依赖线程执行完毕,才能继续执行,join的本质是让调用线程wait()在当前线程对象实例上,
Thread.yield():静态方法,它会使当前线程让出CPU,但是并不代表当前线程不在执行了,而是让出CPU后,线程还会进行CPU资源的争夺,至于能不能再次获得CPU的资源,视情况而定。
线程的优先级:
线程的优先级代表线程可以获取资源的几率,优先级越高,代表越有可能获取资源,但是并不是一定的,线程的优先级内置了三个静态标量表示:
public final static int MIN_PRIORITY =1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;
线程的优先级的有效范围为1-10;
线程安全和Synchronized:
当多个线程同时访问一个共享变量的时候,会发生冲突问题,所以需要在调用之间合理的安排他们的访问顺序,使用synchronized关键字,可以对共享资源加锁,使线程之间有序的访问共享资源;
synchronized关键字应用的地方:
指定加锁对对象,给对象加锁,进入同步代码前要获得给定对象的锁;
直接作用于实例方法,相当于给当前实例加锁,进入同步代码之前要获得当前实例的锁;
直接作用于静态方法,相当于给当前类加锁,进入同步代码之前要获得当前类的锁;
当使用synchronized关键字修饰的时候,修饰对象会有一个锁,同时也会有一个等待队列/阻塞队列,当线程访问共享资源的时候,线程必须获取修饰对象的锁,才可以访问或使用这个资源,直到线程运行完毕,才会释放锁,在这期间,如果有其他线程想要访问共享资源,因为锁被占用,所以会被阻塞,放入到等待队列中,当锁被释放,会随机从等待队列中挑选一个资源,来获取锁,并使用共享资源。
注意:不同对象,实例都有自己的锁,在创建线程的时候,必须让线程都指向同一个Thread子类或Runnable接口实例,这样才能保证多个线程在工作时,对唯一的共享资源使用同一个锁,从而保证线程安全,如果在创建线程的时候,使用不同的对象,那么线程之间使用的锁就不会是同一个锁,因为每个对象,实例都有自己的锁,所以有可能造成数据不一致.
参考《Java 高并发程序设计》 --葛一鸣,郭超