带你走进多线程的世界(多线程实现方式)

做性能测试的同学使用最多的就是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() {
        /**
     * This method is not invoked for the main method thread or "system"
     * group threads created/set up by the VM. Any new functionality added
     * to this method in the future may have to also be added to the VM.
     *
     * A zero status value corresponds to state "NEW".
         */
        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() {
		// TODO Auto-generated method stub
		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;
/**
 * 继承Thread类,不能资源共享
 * @author shangwei
 *
 */
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--);
			}
		}
		// TODO Auto-generated method stub
		
	}

}



运行结果:

count正在卖5

count正在卖3

count正在卖4

count正在卖1

count正在卖2



这里我们看到3个线程都共享了同一个实例,实现了资源的共享,3个线程抢5张票。
那么为什么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();
		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状态的线程在等待什么资源,通常情况下是数据库连接或者是日志的输出。








你可能感兴趣的:(性能测试,java开发)