Java_多线程(1)

ぃ、小莉子 提交于 2020-03-24 07:16:21

前面写了这么多篇java的基础,这篇我们终于进入到了java中比较复杂的部分——java多线程。

什么是线程?

介绍线程之前,我们必须先了解下进程。

进程:是程序的一次动态执行过程。比如你打开了IE浏览器,从它打开的时刻就启动了一个进程。

多进程:多进程就像打开了多个程序,比如你边玩QQ,边用网易云听歌,还可以浏览网页,多个任务同时进行。

线程:比进程更小的执行单位,通常一个进程拥有1-n个线程。

多线程:指在同一个程序(进程)中能够同时处理多个任务,而这些任务就对应多个线程。比如浏览器可以同时下载多个图片;比如ABC三个用户同时访问淘宝,淘宝的服务器收到A用户的请求后,为A创建了一个线程,BC用户同样拥有自己对应的线程。

注意:多线程不是为了提高程序的执行速度(甚至降低性能),而是提高应用程序的使用效率。

 

线程的生命周期?

每一个生命周期也是一种状态。

1)创建:创建一个新的线程对象;

2)就绪:线程对象创建后,其他线程调用了该对象的start()方法,该线程被放入可运行的线程池中,等待获取CPU的使用权;

3)运行:就绪状态的线程获取了CPU,执行;

4)阻塞:由于某种原因,线程暂停;

5)死亡:线程执行完成或异常抛出,该线程生命周期结束。

 

Java实现多线程:

java中实现多线程有两种方法,一种是继承Thread类,一种是实现Runnable接口。

注意:准确来说,应该有第三种,即实现Callable接口,并与Future、线程池结合使用,详细请参考 https://blog.csdn.net/evankaka/article/details/51610635

1、继承Thread类

一个栗子:

public class Thread1 extends Thread{
	private String name;
	public Thread1(String name){
		this.name = name;
	}
	public void run(){
		for(int i=0;i<5;i++){
			System.out.println(name+"运行: "+i);
			try{
				sleep((int)Math.random()*10);
			}catch(InterruptedException e){
				e.printStackTrace();
			}
		}
	}
}

public class Test01 {
	public static void main(String[] args) {
		Thread1 t1 = new Thread1("A");
		Thread1 t2 = new Thread1("B");
		t1.start();
		t2.start();
	}
}

 控制台输出结果:

B运行: 0
A运行: 1
B运行: 1
A运行: 2
B运行: 2
B运行: 3
B运行: 4
A运行: 3
A运行: 4

可以多执行几次代码,发现控制台输出是无序的。

说明:程序启动运行main的时候,java虚拟机启动一个进程,主线程main在main()调用的时候被创建。随着调用Thread两个对象的start()方法,另外两个线程也启动了,这样,整个应用就在多线程下运行。

注意:start()方法调用后并不是立即执行多线程代码,而是使得该线程变成可运行态(Runnable),什么时候运行是由操作系统决定的。

从程序运行的结果可以发现,多线程程序是乱序执行的。因此,只有乱序执行的代码才有必要设计成多线程。

Thread.sleep()方法调用的目的是不让当前线程霸占该进程所获取的CPU资源,以留出一定时间给其他线程执行的机会。

实际上所有多线程代码执行顺序都是不确定的,每次执行的结果都是随机的。

 

2、实现Runnable接口

一个栗子:

public class Test02 {
	public static void main(String[] args) {
		/*Thread2 t1 = new Thread2("C");
		new Thread(t1).start();*/
		new Thread(new Thread2("C")).start();
		new Thread(new Thread2("D")).start();
	}
}

class Thread2 implements Runnable{
	private String name;
	public Thread2(String name){
		this.name = name;
	}
	
	@Override
	public void run() {
		for(int i=0;i<5;i++){
			System.out.println(name+"运行:"+i);
			try{
				Thread.sleep((int)Math.random()*10);
			}catch(InterruptedException e){
				e.printStackTrace();
			}
		}
	}
}

 控制台输出:

C运行:0
D运行:0
C运行:1
D运行:1
C运行:2
D运行:2
C运行:3
D运行:3
C运行:4
D运行:4

说明:Thread2类通过实现Runnable接口,使得该类有了多线程类的特征。run()方法是多线程程序的一个约定,所有的多线程代码都要放在run方法里面。Thread类实际上也是实现了Runnable接口的类。

在启动多线程的时候,需要先通过Thread类的构造方法Thread(Runnable target)构造出对象,然后调用Thread对象的start()方法来运行多线程代码。

实际上所有的多线程代码都是通过运行Thread的start()方法来运行的。因此,不管是以 扩展Thread类还是实现Runnable接口来实现多线程,最终还是通过Thread对象的API来控制线程的,熟悉Thread类的API是多线程编程的基础。

 

Thread和Runnable的区别:

一个类继承Thread,则不适合资源共享。但如果实现了Runnable接口的话,则很容易实现资源共享。

总结:

实现Runnable接口比继承Thread类所具有的优势:

1)适合多个相同程序代码的线程去处理同一资源;

2)可以避免java中单继承的限制;

3)代码可以被多个线程共享,代码和数据独立,增加程序的健壮性;

4)线程池只能放入实现Runnable和callable类线程,不能直接放入继承Thread的类。

注意:

main方法其实也是一个线程。在java中所有的线程都是同时启动的,至于什么时候,哪个先执行,完全看谁先得到CPU的资源。

在java中,每次程序运行至少启动两个线程。一个是main线程,一个是垃圾收集线程。因为每当使用java命令执行一个类的时候,实际上都会启动一个JVM,每个JVM就是在操作系统中启动了一个进程。

 

线程的状态转换:

如下图:

 

1、新建状态(New):新创建了一个线程对象;

2、就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待获取CPU的使用权;

3、运行状态(Running):就绪状态的线程获取了CPU,执行程序代码;

4、阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分为三种:

  1)等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中;(wait会释放持有的锁)

  2)同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中;

  3)其他阻塞:运行的线程执行sleep()或join()方法,或发出了IO请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或超时、或者IO处理完毕时,线程重新转入就绪状态           (sleep不会释放持有的锁);

5、死亡状态(Dead):线程执行完毕或因异常退出了run()方法,该线程生命周期结束。

 

线程调度:

1、调整线程优先级:java线程有优先级,优先级高的线程会获得更多的运行机会。

Java线程的优先级用整数表示,取值范围是1-10,Thread类有以下三个静态常量:

static int MAX_PRIORITY:线程可以具有的最高优先级,取值为10;

static int MIN_PRIORITY:线程可以具有的最低优先级,取值为1;

static int NORM_PRIORITY:分配给线程的默认优先级,取值为5。

注意:

1)Thread类的setPriority()和getPriority()方法分别用来设置和获取线程的优先级;

2)每个线程都有默认的优先级,主线程的默认优先级是NORM_PRIORITY;

3)线程的优先级有继承关系,比如A线程创建了B线程,那么B将和A具有同样的优先级;

4)JVM提供了10个线程优先级,但与常见的操作系统都不能很好的映射,为了程序的跨平台,我们仅仅使用以上介绍的三个静态常量。

2、线程睡眠:Thread.sleep(long millis)方法,使线程转到阻塞状态。millis参数设定睡眠时间,单位为毫秒。当睡眠结束后,就转为就绪状态(Runnable)。sleep()平台移植性好。

3、线程等待:Object类中的wait()方法,导致当前的线程等待,直到其他线程调用此对象的notify()方法或notifyAll()唤醒方法。这两个唤醒方法也是Object类中的方法,行为等价于调用wait(0)一样。

4、线程让步:Thread.yield()方法,暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程。

5、线程加入:join()方法,等待其他线程终止。在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个线程运行结束,当前线程再由阻塞转为就绪状态。

6、线程唤醒:Object类中的notify()方法,唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。选择是任意性的。线程通过调用其中一个wait()方法,在对象的监视器上等待。直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程。被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争。

 

常用函数说明:

1)sleep(long millis):在指定的毫秒数内让当前正在执行的线程休眠(暂停执行);

2)join():等待线程t终止;

  为什么要使用join()方法?在很多情况下,主线程生成并启动了子线程,如果子线程里需要进行大量的耗时计算,那么主线程往往在子线程之前结束。但我们往往需要主线程在子线程之后结束,这时就需要使用join()方法。

不加join,一个栗子:

public class Test03 {
	public static void main(String[] args) {
		System.out.println(Thread.currentThread().getName()+"主线程开始运行!");
		Thread3 t1 = new Thread3("A");
		Thread3 t2 = new Thread3("B");
		t1.start();
		t2.start();
		System.out.println(Thread.currentThread().getName()+"主线程运行结束!");
		
	}
}


class Thread3 extends Thread{
	private String name;
	public Thread3(String name){
		super(name);
		this.name = name;
	}
	public void run(){
		System.out.println(Thread.currentThread().getName()+" 线程开始运行!");
		for(int i=0;i<3;i++){
			System.out.println("子线程"+name+" 运行:"+i);
			try{
				sleep((int)Math.random()*10);
			}catch(InterruptedException e){
				e.printStackTrace();
			}
		}
		System.out.println(Thread.currentThread().getName()+" 线程运行结束!");
	}
}

 控制台输出结果:

main主线程开始运行!
main主线程运行结束!
A 线程开始运行!
子线程A 运行:0
B 线程开始运行!
子线程B 运行:0
子线程A 运行:1
子线程A 运行:2
子线程B 运行:1
子线程B 运行:2
A 线程运行结束!
B 线程运行结束!

我们发现主线程比子线程结束早。

加join,一个栗子:

public class Test03 {
	public static void main(String[] args) {
		System.out.println(Thread.currentThread().getName()+"主线程开始运行!");
		Thread3 t1 = new Thread3("A");
		Thread3 t2 = new Thread3("B");
		t1.start();
		t2.start();
		try {
			t1.join();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		try {
			t2.join();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println(Thread.currentThread().getName()+"主线程运行结束!");
		
	}
}

 控制台输出结果:

main主线程开始运行!
B 线程开始运行!
A 线程开始运行!
子线程B 运行:0
子线程A 运行:0
子线程B 运行:1
子线程B 运行:2
B 线程运行结束!
子线程A 运行:1
子线程A 运行:2
A 线程运行结束!
main主线程运行结束!

加了join()方法之后,我们发现,主线程一定会等子线程都结束才会结束。

3)yield():暂停当前正在执行的线程对象,并执行其他线程。

  yield()的作用是让当前运行线程回到可运行状态(Runnable),以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际开发中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。

结论:yield()不会使线程转到等待/睡眠/阻塞状态。大多数情况下,yield()将导致线程从运行状态转到可运行状态,但是有可能没有效果。

一个栗子:

public class Test04 {
	public static void main(String[] args) {
		Thread4 t1 = new Thread4("aa");
		Thread4 t2 = new Thread4("bb");
		t1.start();
		t2.start();
	}
}

class Thread4 extends Thread{
	public Thread4(String name){
		super(name);
	}
	public void run(){
		for(int i=0;i<50;i++){
			System.out.println(" "+this.getName()+"-------"+i);
			//当i=30的时候,该线程就会把CPU时间让掉,让其他或自己的线程执行(谁先抢到谁执行)
			if(i==30){
				this.yield();
			}
		}
	}
}

运行结果:

第一种:aa线程执行到30时把CPU时间让掉,这时bb线程抢到CPU时间并执行;

第二种:aa线程执行到30时把CPU时间让掉,这时aa线程抢到CPU时间并执行。

sleep()和yield()的区别?

sleep()使当前线程进入停滞状态,执行sleep()方法的线程在指定时间内肯定不会被执行且这个时间的长短是由程序设定的;

yield()只是使当前线程重新回到可执行状态,所以执行yield()方法的线程有可能在进入到可执行状态后被重新执行,但让出的时间是不可设定的。

sleep()方法允许较低优先级的线程获得运行机会,yield()方法只会把机会让给相同优先级的线程。

在一个操作系统中,较高优先级的线程如果没有调用sleep()方法,又没有受到 I/O阻塞,那么低优先级的线程只能等待所有高优先级的线程执行完毕才有机会运行。

 

4、setPriority():更改线程优先级:

MIN_PRIORITY=1;

MAX_PRIORITY=10;

NORM_PRIORITY=5;

 

5、interrupt():interrupt()方法不是中断某个线程,它只是给线程发送一个中断信号,让线程在无限等待时(死锁)抛出一个异常,从而结束此线程,但如果你吃掉了这个异常,那么这个线程是不会中断的。

 

6、wait():

obj.wait()与obj.notify() 必须要和 synchronized(obj) 一起使用,也就是说wait与notify是针对已获取的obj锁进行操作,从语法角度来说,obj.wait()、obj.notify必须要在 synchronized(obj){......}语句块内。

从功能上来说,wait()是线程在获取对象锁后,主动释放对象锁,同时本线程休眠,直到有其他线程调用对象的notify()方法唤醒该线程,才能继续获取对象锁,并继续执行。相应的notify()方法就是对对象锁的唤醒操作。需要注意的是notify()调用后,不是马上就释放对象锁,而是在相应的synchronized(){}语句块执行结束,自动释放锁后,JVM会在wait()对象锁的线程中随机选取一个线程,赋予其对象锁,唤醒线程,自动执行。这样就提供了在线程间同步,唤醒的操作。Thread.sleep()与Object.wait()都可以暂停当前线程,释放CPU控制权,主要区别在于Object.wait()在释放CPU的同时,释放了对象锁的控制。

一个栗子:

建立三个线程,A线程打印10次A,B线程打印10次B,C线程打印10次C,要求线程同时运行,交替打印10次ABC。

public class Thread5 implements Runnable{
	private String name;   
    private Object prev;   
    private Object self;
	
	private Thread5(String name, Object prev, Object self){
		this.name = name;
		this.prev = prev;
		this.self = self;
	}
	
	@Override
	public void run() {
		int count = 10;
		while(count > 0){
			synchronized(prev){
				synchronized(self){
					System.out.print(name);
					count--;
					
					self.notify();
				}
				try {
					prev.wait();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}
	}
	public static void main(String[] args) throws Exception {
		Object a = new Object();
		Object b = new Object();
		Object c = new Object();
		Thread5 ta = new Thread5("A",c,a);
		Thread5 tb = new Thread5("B",a,b);
		Thread5 tc = new Thread5("C",b,c);
		new Thread(ta).start();
			Thread.sleep(100);
		new Thread(tb).start();
			Thread.sleep(100);
		new Thread(tc).start();
			Thread.sleep(100);
	}
}

 控制台输出结果:

ABCABCABCABCABCABCABCABCABCABC

先来解释一下整体的思路,从大的方向上来讲,该问题为三线程间的同步唤醒操作,主要的目的就是ThreadA->ThreadB->ThreadC->ThreadA...循环执行三个线程。为了控制线程执行的顺序,那么就必须要确定唤醒、等待的顺序,所以每个线程必须同时拥有两个对象锁,才能执行。一个对象锁是prev,就是前一个线程所持有的对象锁,还有一个就是自身对象锁。主要的思想就是:为了控制执行顺序,必须先持有prev锁,也就是前一个线程要释放自身的对象锁,再去申请自身的对象锁,两者兼备时打印,之后调用self.notify()释放自身的对象锁,唤醒下一个等待线程,再调用prev.wait()释放prev对象锁终止当前线程,等待循环结束后再次被唤醒。

wait和sleep的区别?

共同点:

1)、它们都是在多线程的环境下,都可以在程序的调用处阻塞指定的毫秒数,并返回;

2)wait()和sleep()都可以通过interrupt()方法打断线程的暂停状态,从而使线程立刻抛出InterruptedException。

如果线程A希望立刻结束线程B,则可以对线程B对应的Thread实例调用interrupt()方法,如果此刻线程B正在wait/sleep/join,则线程B会立刻抛出InterruptedException,在catch{}中直接return即可安全的结束线程。

需要注意的是,InterruptedException是线程自己从内部抛出的,并不是interrupt()方法抛出的。对某一线程调用interrupt()时,如果该线程正在执行普通的代码,那么该线程根本不会抛出InterruptedException。但是,一旦该线程进入到wait()/sleep()/join()后,就会立刻抛出InterruptedException。

不同点:

1)、Thread类的方法:sleep()、yield()

         Object的方法:wait(),notify()

2)、每个对象都有一个锁来控制同步访问。Synchronized关键字可以和对象的锁交互,来实现线程的同步。

sleep方法没有释放锁,而wait方法释放了锁。

3)、sleep()睡眠时,保持对象锁,仍然占有该锁;wait()睡眠时,释放对象锁。

sleep()方法使当前线程进入停滞状态(阻塞当前线程),让出CPU使用,目的是不让当前线程霸占该进程所获取的CPU资源,以留一定时间给其他线程执行的机会;

sleep()是Thread类的static方法,因此不能改变对象的机锁,所以当在一个synchronized块中调用sleep()方法时,线程虽然休眠了,但是对象的机锁并没有被释放,其他线程无法访问这个对象(即使睡着也持有对象锁);

在sleep()方法休眠时间期满后,该线程不一定会立即执行,这是因为其他线程可能正在运行且没有被调度为放弃执行,除非此线程具有更高的优先级。

wait()方法是Object类中的方法,当一个线程执行到wait()方法时,它就进入到一个和该对象相关的等待池中,同时释放了对象的机锁,其他线程可以访问;

wait()使用notify()、notifyAll()或指定睡眠时间来唤醒当前等待池中的线程;

wait()必须放在synchronized块中。

 

常见线程名称解释:

主线程:JVM调用程序main()方法所产生的线程;

当前线程:这是个容易混淆的概念,一般指通过Thread.currentThread()来获取的线程;

后台线程:指为其他线程提供服务的线程,也称为守护线程。JVM的垃圾回收线程就是一个后台线程。用户线程和守护线程的区别在于:是否等待主线程 依赖于主线程结束而结束;

前台线程:接受后台线程服务的线程。

线程类的一些常用方法:

sleep():强迫一个线程睡眠N毫秒;

isAlive():判断一个线程是否存活;

join():等待线程终止;

activeCount():程序中活跃的线程数;

enumerate():枚举程序中的线程;

currentThread():当前线程;

isDaemon():一个线程是否为守护线程;

setDaemon():设置一个线程为守护线程;

setName():为线程设置一个名称;

wait():强迫一个线程等待;

notify():唤醒一个线程;

setPriority():设置线程优先级。

 

线程同步:

1)、线程同步的目的是为了保护多个线程访问一个资源时对资源的破坏;

2)、线程同步方法是通过锁来实现的,每个对象有且仅有一个锁,线程一旦获取了对象锁,其他访问该对象的线程就无法再访问该对象的其他非同步方法;

3)、对于静态同步方法,锁是针对这个类的,锁对象是该类的Class对象。静态和非静态方法的锁互不干预。一个线程获得锁,当在一个同步方法中,访问另外对象上的同步方法时,会获取这两个对象锁;

4)、对于同步,要时刻清醒在哪个对象上同步,这是关键;

5)、编写线程安全的类,需要时刻注意对多个线程竞争访问资源的逻辑和安全做出正确的判断,对“原子”操作做出分析,并保证原子操作期间别的线程无法访问竞争资源;

6)、当多个线程等待一个对象锁时,没有获取到锁的线程将发生阻塞;

7)、死锁是线程间相互等待锁造成的,实际开发中发生概率较小,但一旦发生死锁,程序会挂掉。

 

线程数据传递:

在传统的同步开发模式下,当我们调用一个函数时,通过这个函数的参数将数据传入,并通过这个函数的返回值来返回最终的计算结果。但在多线程的异步开发模式下,数据的传递、返回和同步开发模式有很大的区别。由于线程的运行和结束是不可预测的,因此,在传递和返回数据时就无法像函数一样通过参数和return语句来返回数据。

1)、通过构造方法传递数据

在创建线程时,必须建立一个Thread类或其子类的实例。因此,我们不难想到在调用start()方法之前通过线程类的构造方法将数据传入线程。并将传入的数据使用类变量保存起来,以便线程使用(run)。

一个栗子:

public class Thread6 extends Thread{
	private String name;
	public Thread6(String name){
		this.name = name;
	}
	
	public void run(){
		System.out.println("my name is :"+name);
	}
	public static void main(String[] args) {
		Thread6 t = new Thread6("Rain");
		t.start();
	}
}

 控制台输出:

my name is :Rain

由于这种方法是在创建线程对象的同时传递数据的,因此,在线程运行之前这些数据就已经到位了,这样就不会造成数据在线程运行后传入的现象。如果要传递更负责的数据,可以使用集合、类等数据结构。使用构造方法来传递数据虽然比较安全,但如果要传递的数据较多,就会有很多不便。由于java没有默认参数,要想实现类似默认参数的效果,就得使用重载,这样不但使构造方法本身过于复杂,又使构造方法数量上大增。因此,若想避免这种情况,就得通过类方法或类变量来传递数据。

2)、通过变量和方法传递数据

向对象中传入数据一般有两次机会,第一次机会是在建立对象时通过构造方法将数据传入,另外一次机会就是在类中定义一系列方法或属性,在建立对象后,通过对象实例逐个赋值。

一个栗子:

public class Thread04 implements Runnable{
	private String name;
	public void setName(String name){
		this.name = name;
	}
	public Thread04(){}
	@Override
	public void run() {
		System.out.println("I am "+name);
	}
	
	public static void main(String[] args) {
		Thread04 t = new Thread04();
		t.setName("Iron man");
		new Thread(t).start();
	}
}

 控制台输出:

I am Iron man

 

3)、通过回调函数传递数据:

上面讨论的两种向线程中传递数据的方法是最常用的,但这两种方法都是main方法中主动将数据传入线程类的。对于线程来说,是被动接受数据的。然而,在实际开发中,可能需要在线程运行的过程中动态的获取数据。

一个栗子:

public class Thread05 extends Thread{
	private Work work;
	public Thread05(Work work){
		this.work = work;
	}
	
	public void run(){
		Random random = new Random();
		Data data = new Data();
		int n1 = random.nextInt(1000);
		int n2 = random.nextInt(2000);
		int n3 = random.nextInt(3000);
		work.process(data, n1,n2,n3);
		System.out.println(String.valueOf(n1)+"+"+String.valueOf(n2)+"+"+String.valueOf(n3)+"="+data.value);
	}
	public static void main(String[] args) {
		Thread05 t = new Thread05(new Work());
		t.start();
	}
}

class Work{
	public void process(Data data,int n1,int n2,int n3){
		int[] a = new int[3];
		a[0] = n1;
		a[1] = n2;
		a[2] = n3;
		for(int n:a){
			data.value +=n;
		}
	}
}
class Data{
	public int value = 0;
}

执行代码,输出三个随机数的和。

关于多线程的基础,这篇博文我觉得总结的很全面了,后续会再增一篇,总结下多线程中锁的问题。

 

转自博主Evankaka 博文 https://blog.csdn.net/evankaka/article/details/44153709#t1

 

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!