做性能测试的同学使用最多的就是LoadRunner和Jemter工具了吧,能够使用洪荒之力模拟多用户同时请求服务器,来观察服务器端的负载情况并定位性能瓶颈,听上去挺高大上的。无论任何性能工具,核心原理都离不开多线程。如何实现多线程?如何定位异常状态的线程找到性能瓶颈呢?别急,开始我们的多线程之旅吧~
什么是多线程?
举个简单的例子,比如你去一家餐馆吃饭,餐馆只有一个服务员,那么这个服务员给你点菜的时候,别的人就得等着。但如果这个餐厅有3个服务员A,B,C,那么同一时刻就可以给3个顾客(甲乙丙)去点菜,每个顾客点了不同的2道菜。我们把餐馆理解成一个进程,服务员A,B,C理解为3个线程,后厨做菜的厨师是cpu(假设是单核的,一个cpu)。
从A,B,C 三个服务员同时接待 3个顾客(甲乙丙)这个表象看线程是同步,并发执行的,但是厨师在做菜的过程中是有先后之分的,厨师会把甲乙丙三个人的菜分开来做,做完甲的菜,立刻开始做乙的菜,乙的菜可能需要时间蒸的时候,会去做丙的菜,就这样不停的切换做着甲乙丙三个顾客的菜,而在甲乙丙顾客看来他们桌子上都有菜吃,误以为是同事做出来的。但严格意义上讲,同一时刻只有一个线程运行,但是用户会觉得是多个线程同时运行的。
Java多线程实现
java多线程实现主要有三种方式:继承thread类,实现runnable接口,使用ExecutorService、Callable、Future实现有返回结果的多线程。其中前两种方式线程执行完后都没有返回值,只有最后一种是带返回值的。这里我们只谈前两种。
1、继承Thread类实现多线程
我们先看一个例子:
package thread;
- public class Thread_1 extends Thread{
-
- private String name;
-
- public Thread_1(){
-
- }
-
- public Thread_1(String name){
- this.name = name ;
-
- }
-
- public void run(){
- for (int i = 0;i<5 ; i++){
- System.out.println(name + "运行" +i);
- }
- }
-
- public static void main(String[] args){
- Thread_1 h1 = new Thread_1("A");
- Thread_1 h2 = new Thread_1("B");
- h1.run();
- h2.run();
-
- }
- }
我们看一下运行结果:
A运行0
A运行1
A运行2
A运行3
A运行4
B运行0
B运行1
B运行2
B运行3
B运行4
我们会发现这些都是顺序执行的,并没有多线程执行,为什么呢?因为我们直接调用了run方法,启动线程唯一的方法是通过Thread类调用start()方法,
start()方法是一个native方法,即本地操作系统方法,它将启动一个新线程,并执行run()方法,我们通过自己的类直接extend Thread,并复写run()方法,就可以启动新线程并执行自己定义的run()方法。
如果看一些start的源代码就会更容易理解:
- public synchronized void start() {
-
-
-
-
-
-
-
- if (threadStatus != 0 || this != me)
- throw new IllegalThreadStateException();
- group.add(this);
- start0();
- if (stopBeforeStart) {
- stop0(throwableFromStop);
- }
- }
- private native void start0();
我们看最后,说明此处调用的start0(),这个方法用了native关键字。
2、通过实现Runnable接口
Thread本质上也是实现了Runnable接口的一个实例,如果自己的类已经extends另一个类,就无法直接继承thread类,必须实现一个Runnable接口。
我们在看一个小例子:
- package thread;
-
- public class runnable_2 implements Runnable{
-
- private String name ;
- public runnable_2(){
-
- }
-
- public runnable_2(String name){
- this.name = name;
- }
- @Override
- public void run() {
-
- for(int i =0;i <5 ;i++){
- System.out.println(name +"运行"+i);
- }
-
- }
- public static void main(String[] args){
- runnable_2 h1 = new runnable_2("线程A");
- Thread demo = new Thread(h1);
- runnable_2 h2 = new runnable_2("线程B");
- Thread demo1 = new Thread(h2);
- demo.start();
- demo1.start();
- }
-
- }
运行结果:
线程A运行0
线程B运行0
线程B运行1
线程A运行1
线程B运行2
线程A运行2
线程B运行3
线程A运行3
线程B运行4
线程A运行4
我们是选择thread 类还是实现runnable接口呢?
其实thread类也是实现Runnable接口的:
- class Thread implements Runnable {
-
- public void run() {
- if (target != null) {
- target.run();
- }
- }
- }
Thread和Runnable的区别?
如果一个类继承Thread,则不适合资源共享,如果实现了Runnable接口,则很容易资源共享。
看下面的例子:
- package thread;
-
-
-
-
-
- public class thread_share_3 extends Thread{
- private int count = 5;
- public void run(){
- for(int i=0;i<10;i++){
- if(count >0){
- System.out.println(Thread.currentThread().getName()+"="+count--);
- }
- }
- }
- public static void main(String[] args){
- thread_share_3 h1 = new thread_share_3();
- thread_share_3 h2 = new thread_share_3();
- thread_share_3 h3 = new thread_share_3();
- h1.start();
- h2.start();
- h3.start();
-
- }
-
- }
运行接口
Thread-1=5
Thread-3=5
Thread-2=5
Thread-3=4
Thread-1=4
Thread-3=3
Thread-2=4
Thread-2=3
Thread-3=2
Thread-1=3
Thread-3=1
Thread-2=2
Thread-1=2
Thread-1=1
Thread-2=1
我们把count看成是一个需要共享的资源,比如我们的抢票系统,系统里有5张票,3个人去抢票,实际上每个人都抢了5张票。一共15张票。
我们换成Runnale接口试一下:
- package thread;
-
- public class runnable_share_4 implements Runnable {
-
- private int count = 5;
-
- public static void main(String[] args){
- runnable_share_4 h1 = new runnable_share_4();
- Thread t1 = new Thread(h1,"1号窗口");
- t1.start();
- Thread t2 = new Thread(h1,"2号窗口");
- t2.start();
- Thread t3 = new Thread(h1,"3号窗口");
- t3.start();
-
- }
-
- @Override
- public void run() {
- for(int i=0;i<10;i++){
- if(count >0){
- System.out.println("count"+"正在卖"+count--);
- }
- }
-
-
- }
-
- }
运行结果:
count正在卖5
count正在卖3
count正在卖4
count正在卖1
count正在卖2
这里我们看到3个线程都共享了同一个实例,实现了资源的共享,3个线程抢5张票。
- 那么为什么Thread类不能共享同一个实例呢?我们试试呗。
- </pre><pre code_snippet_id="1856515" snippet_file_name="blog_20160829_6_1221599" name="code" class="java" style="font-size: 14px;">package thread;
-
- public class Thread_1 extends Thread{
-
- private String name;
-
- public Thread_1(){
-
- }
-
- public Thread_1(String name){
- this.name = name ;
-
- }
-
- public void run(){
- for (int i = 0;i<5 ; i++){
- System.out.println(name + "运行" +i);
- }
- }
-
- public static void main(String[] args){
- Thread_1 h1 = new Thread_1("A");
- Thread_1 h2 = new Thread_1("B");
-
-
- h1.start();
- h1.start();
-
-
- }
- }
在main方法里,我们对同一个实例都start()开启线程,看会出现什么情况呢?
运行结果:
A运行0Exception in thread "main"
A运行1
A运行2
A运行3
A运行4
java.lang.IllegalThreadStateException
at java.lang.Thread.start(Thread.java:671)
at thread.Thread_1.main(Thread_1.java:28)
总结一下吧:
实现Runnable接口比继承Thread类所具有的优势:
1):适合多个相同的程序代码的线程去处理同一个资源
2):可以避免java中的单继承的限制
3):增加程序的健壮性,代码可以被多个线程共享,代码和数据独立。
线程调度
线程有5种基本状态:
1、新建状态(new):当创建线程对象后,就进入了新建状态,比如:Thread t = new Thread();
2、 就绪状态(runnable):当线程对象被创建后,其他线程调用了该线程的start()方法,该线程就进入了队列,说明此线程做好了准备,随时等待cpu调度执行。
3、运行状态(running)当cpu时间片分给就绪状态的线程时,该线程就真正的执行起来,进入了运行状态。
4、阻塞状态(blocked)阻塞状态是因为线程因为某种原因放弃了cpu的使用权,暂时停止运行,直到线程进入就绪状态,才有机会运行。
阻塞状态分为3种情况:
等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;
同步阻塞 -- 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
其他阻塞 -- 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
5、死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
处于阻塞状态的线程往往存在性能瓶颈,也是我们需要特别关注的。如果是因为竞争同步锁引发的死锁或者的响应时间的延长,需要找到这些处于blocked状态的线程在等待什么资源,通常情况下是数据库连接或者是日志的输出。
注:转自 http://blog.youkuaiyun.com/weiweicao0429/article/details/52353866