Java多线程超详解

  • Post author:
  • Post category:java




进程与线程

线程指的是进程中的一个执行场景,也就是执行流程

  • 每一个进程都是一个应用程序,都有独立的内存空间
  • 同一个进程中的线程共享其进程中的内存和资源(共享的内存是堆内存和方法区内存,栈内存不共享,每个线程都有自己的栈内存。)



什么是进程

一个进程对应一个应用程序,例如在Windows操作系统中打开QQ,在Java开发环境下启动jvm,就表示启动了一个线程。在同一个操作系统中,可以同时启动多个线程。



多进程有啥用

单进程计算机只能做一件事情。

对于单核计算机来说,在同一时间点上,游戏进程和音乐进程不会在同时运行。由于计算机将在两个进程之间频繁切换,切换速度很快,让人感觉到游戏和音乐在同时进行。

多进程的作用并不在于提高执行速度,而是提高cpu的使用率。



什么是线程

线程是一个进程中的执行场景,一个进程可以启动多个线程。



多线程有啥用

多线程也不是为了提高执行速度,而是提高应用程序的使用率。

可以给我们人一种错觉:感觉多个线程在同时并发进行。



Java程序的运行原理

Java命令会启动Java虚拟机,等于启动了一个应用程序,启动了一个进程。该进程会自动启动一个“主线程”,这个主线程之后便去调用某个类的main方法。所以main方法运行在主线程中。在此之前的所有程序都是单线程的.



线程的创建和启动

Java虚拟机的主线程入口是main方法,用户可以自己创建线程,创建方式有二:

  • 继承Thread类
  • 实现Runnable接口(推荐使用Runnable接口)



继承Thread类


继承java.lang.Thread+重写run方法

定义线程+创建线程+启动线程

 public class ThreadTest {
	public static void main(String[] args) {
		//创建线程
		Thread t=new Processor();//多态
		
		//启动线程
		t.start();
		//执行瞬间结束,告诉JVM再分配一个新的栈给t线程
		//run不需要程序员手动调用,系统线程启动后自动调用run方法
		
		//t.run();//这是普通方法调用,没有start只有run这样做程序只有一个线程
		//run方法结束之后,下面程序才能继续执行
		
		//这段代码在主线程中运行
		for(int i=0;i<10;i++) {
			System.out.println("main-->"+i);
		}
		
		//有了多线程之后,main方法结束只是在主线程栈中没有方法栈帧
		//但是在其他线程或者是其他栈中还有栈帧
		//main方法结束后,程序可能还在运行
	}

}
//定义一个线程
class Processor extends Thread{
	//这里是重写run方法
	public void run() {
		for(int i=0;i<30;i++) {
			System.out.println("run-->"+i);
		}
	}
	
}

在这里插入图片描述

如图,一个线程一个栈哈。


贴出运行结果:

main-->0
main-->1
main-->2
main-->3
main-->4
main-->5
run-->0
main-->6
main-->7
main-->8
main-->9
run-->1
run-->2
run-->3
run-->4
run-->5
run-->6
run-->7
run-->8
run-->9
run-->10
run-->11
run-->12
run-->13
run-->14
run-->15
run-->16
run-->17
run-->18
run-->19
run-->20
run-->21
run-->22
run-->23
run-->24
run-->25
run-->26
run-->27
run-->28
run-->29

可以看到主线程和t线程其实是不断切换的。



Runnable接口


编程一个类实现java.lang.Runnable接口+实现run方法



定义线程+创建线程+启动线程

public class ThreadTest1 {

	public static void main(String[] args) {
		//创建线程
		Thread t=new Thread(new Processor1());
		//Thread类的构造方法里的参数可以是Runnable类型
		
		//启动线程
		t.start();
	
		for(int i=0;i<30;i++) {
			System.out.println("main->>"+i);
		}
	}

}
//推荐这种方式,一个类实现接口之外保留了类的继承
class Processor1 implements Runnable{
	public void run() {
		for(int i=0;i<10;i++) {
			System.out.println("run->>"+i);
		}
	}
}

贴出运行结果:

main->>0
main->>1
main->>2
run->>0
run->>1
run->>2
run->>3
run->>4
run->>5
main->>3
run->>6
run->>7
run->>8
run->>9
main->>4
main->>5
main->>6
main->>7
main->>8
main->>9
main->>10
main->>11
main->>12
main->>13
main->>14
main->>15
main->>16
main->>17
main->>18
main->>19
main->>20
main->>21
main->>22
main->>23
main->>24
main->>25
main->>26
main->>27
main->>28
main->>29



线程的生命周期

线程的生命周期分为五个阶段:


新建+就绪+运行+阻塞+消亡


在这里插入图片描述


新建

:采用new语句创建完成


就绪

:执行start后


运行

:占用CPU时间


阻塞

:执行了wait语句、执行了sleep语句和等待某个对象锁,等待输入的场合


终止

:退出run方法

假设这里我们有三个线程t1,t2,t3

t1,t2,t3去争夺时间片,谁抢到了就去运行,当时间片时间不够,例如第一次t1抢到了结果时间不够,便返回成为就绪状态,重新开始抢夺时间片,如果t1再次抢夺到了时间片,那么t1将从上次没执行完的地方开始执行。

我们从程序执行结果就可以看到:

main->>0
main->>1
main->>2//主线程拿到时间片,奈何时间不够
//主线程进入就绪状态,重新抢时间片
//这一次t线程抢夺到了
//进入运行状态
run->>0
run->>1
run->>2
run->>3
run->>4
run->>5
//时间不够,t线程它发出了嘤嘤嘤的叫声,就绪去了
//主线程又抢到了,并且从上次没执行完的地方开始执行
main->>3
run->>6
run->>7
run->>8
run->>9
//t线程执行完了,它又发出了嘤嘤嘤的叫声,并英勇消亡
main->>4
main->>5
main->>6
main->>7
main->>8
main->>9
main->>10
main->>11
main->>12
main->>13
main->>14
main->>15
main->>16
main->>17
main->>18
main->>19
main->>20
main->>21
main->>22
main->>23
main->>24
main->>25
main->>26
main->>27
main->>28
main->>29



线程的调度与控制

通常我们的计算机只有一个CPU,CPU在某一个时刻只能执行一条指令。在单CPU的机器上线程不是并行运行的,只有在多个CPU上线程才可以并行运行。

Java虚拟机要负责线程的调度,取得CPU的使用权,目前有两种调度模型:分时调度模型和抢占式调度模型。Java使用抢占式调用模型。


分时调用模型

:所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间片。


抢占式调用模型

:优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的CPU时间片相对多一点。



三个方法:

  1. 获取当前线程对象Thread.currentThread();
  2. 给线程取名t.getName(“t1线程”);
  3. 获取线程的名字t.getName();
public class ThreadTest2 {
	public static void main(String[] args) {
		//如何获取当前线程对象
		Thread t=Thread.currentThread();
		//t保存的内存地址指向的线程是“主线程对象”
		
		//获取线程的名字
		System.out.println(t.getName());
		
		Processor2 p=new Processor2();
		Thread t1=new Thread(p);
		t1.start();
		//Thread-0
		
		Thread t2=new Thread(new Processor2());
		t2.start();
		//Thread-1
		
	}
}
//定义线程
class Processor2 implements Runnable{
	public void run() {
		Thread t=Thread.currentThread();
		//t保存的内存地址指向的线程是“t1线程对象”
		System.out.println(t.getName());
	}
}

程序运行结果为

main

Thread-0

Thread-1

并且我们也可以利用setName()方法来给线程取名:

Processor2 p=new Processor2();
		Thread t1=new Thread(p);
		t1.setName("t1线程");
		t1.start();
		//t1线程
		
		Thread t2=new Thread(new Processor2());
		t2.setName("t2线程");
		t2.start();
		//t2线程



线程优先级

线程优先级高的获取的CPU时间片相对多一些

优先级:1-10

最低1,最高10,默认5

public class ThreadTest3 {

	public static void main(String[] args) {
		Thread t1=new Processor3();
		t1.setName("t1线程");
		
		Thread t2=new Processor3();
		t2.setName("t2线程");
		
		System.out.println(t1.getPriority());
		System.out.println(t2.getPriority());//默认优先级是5

		//设置优先级
		t1.setPriority(1);
		t2.setPriority(10);
		
		//启动线程
		t1.start();
		t2.start();
	

	}

}
//定义模型
class Processor3 extends Thread{
	public void run() {
		for(int i=0;i<50;i++) {
			System.out.println(Thread.currentThread().getName()+"-->"+i);
			//获取当前线程的名字
			 
		}
	}
	
}



Thread.sleep()

1.Thread.sleep(毫秒);

2.sleep方法是一个静态方法

3.该方法作用为阻塞当前线程,腾出CPU让给其他线程。

public class ThreadTest4 {

	public static void main(String[] args) throws InterruptedException {
		Thread t1=new Processor4();
		t1.setName("t1线程");
		t1.start();
		
		//阻塞主线程
		for(int i=0;i<10;i++) {
			System.out.println(Thread.currentThread().getName()+"-->"+i);
			Thread.sleep(500);
		}
		
	}

}
class Processor4 extends Thread{
	//Thread中的run方法不抛出异常
	//所以重写run方法之后,在run方法的声明位置上不能使用throws
	//所以run方法中的异常只能用try...catch...
	public void run() {
		for(int i=0;i<10;i++) {
			System.out.println(Thread.currentThread().getName()+"-->"+i);
			try {
				 Thread.sleep(1000);//让当前线程阻塞1000毫秒
			}catch(InterruptedException e) {
				e.printStackTrace();
			}
			
		}
	}
}
public class ThreadTest5 {

	public static void main(String[] args) throws Exception {
		//创建线程
		Thread t=new Processor5();
		
		//启动线程
		t.setName("t");
		t.start();
		
		//休眠
		//Thread.sleep(5000);
		t.sleep(5000);//阻塞了主线程,等同于Thread.sleep(),和t线程无关
		System.out.println("延时五秒");
		
	}

}
class Processor5 extends Thread{
	public void run() {
		for(int i=0;i<3000;i++) {
			System.out.println(Thread.currentThread().getName()+"-->"+i);
		}
	}
}



某线程正在休眠,如何打断他的休眠

(此时,线程发出了嘤嘤嘤的声音)

靠的是异常处理机制

public class ThreadTest6 {

	public static void main(String[] args) throws Exception{
		Thread t=new Thread(new Processor6());
		//现在我们需要在启动线程5秒钟之后打断线程休眠
		
		//给睡懒觉的线程起个名字
		t.setName("让我多睡会");
		
		//启动线程
		t.start();
		
		//主线程睡了5s之后
		Thread.sleep(5000);
		
		//打断t的休眠
		t.interrupt();
	}

}
class Processor6 implements Runnable{
	public void run() {
		try {
			Thread.sleep(10000000);
			System.out.println("hello");//不会运行了
		}catch(InterruptedException e) {//通过异常处理机制中断休眠
			e.printStackTrace();
		}
		
		for(int i=0;i<10;i++) {
			System.out.println(Thread.currentThread().getName()+"-->"+i);
			
		}
		
	}
}



如何正确的更好的终止一个正在运行的线程

需求:线程启动5s之后终止

通常定义一个标记,来判断标记的状态停止线程的运行

public class ThreadTest7 {
	public static void main(String[] args) throws Exception {
		Processor7 p=new Processor7();		
		Thread t=new Thread(p);
		t.setName("t线程");
		t.start();
		
		//5秒之后终止
		Thread.sleep(5000);
		//终止
		p.run=false;
		
	}

}

class Processor7 implements Runnable{
	boolean run=true;
	
	public void run() {
		for(int i=0;i<10;i++) {
			
			if(run) {
				try {
				Thread.sleep(1000);
			}catch(Exception e) {}
			System.out.println(Thread.currentThread().getName()+"-->"+i);	
			}
			else {
				return;
			}
			
					
		}
	}
}



Thread.yield

该方法是一个静态方法。它与sleep类似,只是不能由用户指定暂停多长时间,yield时间不固定。并且yield方法只能让同优先级的线程有执行的机会。

即使当前线程由运行状态变为就绪状态。cpu会从众多的运行状态里选择,也就是说,当前也就是刚刚那个线程还是有可能会被再次执行到的,并不是说一定会执行其他线程而该线程在下一次中不会执行到了。

public class ThreadTest8 {

	public static void main(String[] args) {
		Thread t=new Processor8();
	
		t.setName("t线程");
		
		t.start();
		//在主线程中
		for(int i=0;i<100;i++) {
			System.out.println(Thread.currentThread().getName()+"-->"+i);
			
		}
		
	}

}

class Processor8 extends Thread{
	public void run() {
		for(int i=0;i<100;i++) {
			System.out.println(Thread.currentThread().getName()+"-->"+i);
			if(i%20==0) {
				Thread.yield();
			}
		}
	}
}



t.join

此方法为成员方法。当前线程可以调用另一个线程的join方法,调用后当前线程会被阻塞不再执行,直到被调用的线程执行完毕,当前线程才会执行。

public class ThreadTest9 {

	public static void main(String[] args) throws Exception{
		Thread t=new Thread(new Processor9());
		t.setName("t线程");
		t.start();
		
		//合并线程
		t.join();//t线程和主线程合并
		//现在只有当t线程执行完毕时,才会执行主线程
		
		//主线程内
		for(int i=0;i<10;i++) {
			System.out.println(Thread.currentThread().getName()+"-->"+i);
		}

	}

}
class Processor9 implements Runnable{
	public void run() {
		for(int i=0;i<5;i++) {
			try {
				Thread.sleep(1000);
			}catch(InterruptedException e) {
				
			}
			System.out.println(Thread.currentThread().getName()+"-->"+i);
			
		}
	}
}

运行结果为:

t线程-->0
t线程-->1
t线程-->2
t线程-->3
t线程-->4
main-->0
main-->1
main-->2
main-->3
main-->4
main-->5
main-->6
main-->7
main-->8
main-->9



线程的同步(加锁)

异步编程模型:t1线程执行它自己,t2线程执行它自己,两个线程谁也不等谁

同步编程模型:t1线程和t2线程执行,t1线程必须等t2线程执行结束之后才能执行



为什么要引入线程同步?

为了数据的安全。尽管应用程序的使用率变低。线程同步机制使程序等同于单线程。



什么条件下使用线程同步?

第一:必须是多线程环境

第二:多线程环境共享同一个数据

第三:共享的数据涉及到修改操作

以下程序演示取款例子:



版权声明:本文为weixin_44798518原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。