Java高级应用之线程与并发(thread&concurrence)

文章目录

  • 一.线程概述
    • 1.1进程(Process)
    • 1.2线程(Thread)
    • 1.3线程与进程的形象类比
  • 二.创建线程
    • 2.1继承Thread类
      • 2.1.1 设置线程名称1⃣️
      • 2.1.2 设置线程名称2⃣️
      • 2.1.3具体案例分析
    • 2.2实现Runnable接口
      • 2.2.1设置线程名称
      • 2.2.2Runnable+匿名内部类
    • 2.3实现Callable接口
  • 三.线程的状态与转换
    • 3.1线程状态
    • 3.2线程的调度
      • 3.2.1线程的优先级
      • 3.2.2线程休眠
      • 3.2.3线程让步
      • 3.2.4线程联合
    • 3.2线程的同步
      • 3.2.1 线程安全

一.线程概述

我们知道线程thread与进程process是两个不可分割的概念,也是一对极易混淆的概念,我们了解线程之前首先了解一下进程及与线程的区别。

1.1进程(Process)

狭义定义:An instance of a computer program that is being executed(进程是正在运行的程序的实例)

进程特征:

  1. 一个进程就是一个执行中的程序,而每一个进程都有自己独立的一块内存空间和一组系统资源。每一个进程的内部数据和状态都是完全独立的。
  2. 进程是程序的一次执行过程,是系统运行程序的基本单位。

1.2线程(Thread)

定义:单个进程中执行中每个任务就是一个线程。线程是进程中执行运算的最小单位。

线程特点:

  1. 线程是一种轻量级的进程,与进程相比,线程给操作系统带来侧创建、维护、和管理的负担要轻,意味着线程的代价或开销比较小。
  2. 一个线程只能属于一个进程,但是一个进程可以拥有多个线程。多线程处理就是允许一个进程中在同一时刻执行多个任务。
    Java高级应用之线程与并发(thread&concurrence)_第1张图片

1.3线程与进程的形象类比

计算机的核心是CPU,它承担了所有的计算任务。它就像一座工厂,时刻在运行。假定工厂的电力有限,一次只能供给一个车间使用。也就是说,一个车间开工的时候,其他车间都必须停工。背后的含义就是,单个CPU一次只能运行一个任务。进程就好比工厂的车间,它代表CPU所能处理的单个任务。任一时刻,CPU总是运行一个进程,其他进程处于非运行状态。一个车间里,可以有很多工人。他们协同完成一个任务。线程就好比车间里的工人。一个进程可以包括多个线程。车间的空间是工人们共享的,比如许多房间是每个工人都可以进出的。这象征一个进程的内存空间是共享的,每个线程都可以使用这些共享内存。

二.创建线程

2.1继承Thread类

Java虚拟机从main方法依次从上到下执行main方法里的任务,为单线程。如果想要变成多线程,就需要extends Tread

具体格式

class 类名 extends Tread{
		属性
		方法
		修饰符 run(){          //重写Thread类里的run()方法
			以线程处理的程序;
			}
}

举例:

package com.Thread.test;

public class TestThread extends Thread
{
	public void run() {
		for(int i=0; i<10; i++) {
			System.out.println("TestTread----"+i);
		}
	}
}
package com.Thread.test;

public class Test
{

	public static void main(String[] args) //主线程
	{
		//创建自线程对象tt
		TestThread tt =new TestThread();
		//子线程执行任务
		tt.run();
		
		for(int i=0 ; i<10;i++) {
			System.out.println("main-----"+i);
		}
	}
}

Output:

TestTread----0
TestTread----1
TestTread----2
TestTread----3
TestTread----4
TestTread----5
TestTread----6
TestTread----7
TestTread----8
TestTread----9
main-----0
main-----1
main-----2
main-----3
main-----4
main-----5
main-----6
main-----7
main-----8
main-----9

我们发现,结果是按照先打印TestThread----i后打印main-----i,即按照main方法从上到下执行的,并不是我们想要的两个线程相互争抢,出现TestThread-----i和main-----i相互交错的形式。问题出在哪???

问题就在tt.run();
想要同时执行主线程和子线程,应当tt.start();
start()方法是Thread类中的方法:使该线程开始执行Java虚拟机调用run()方法

package com.Thread.test;

public class TestThread extends Thread
{
	public void run() {
		for(int i=0; i<10; i++) {
			System.out.println("TestTread----"+i);
		}
	}
}
package com.Thread.test;

public class Test
{

	public static void main(String[] args) //主线程
	{
		//创建自线程对象tt
		TestThread tt =new TestThread();
		//子线程执行任务
		tt.start();
		
		for(int i=0 ; i<10;i++) {
			System.out.println("main-----"+i);
		}
	}
}

Output:

main-----0
TestTread----0
TestTread----1
main-----1
TestTread----2
TestTread----3
TestTread----4
TestTread----5
TestTread----6
TestTread----7
TestTread----8
TestTread----9
main-----2
main-----3
main-----4
main-----5
main-----6
main-----7
main-----8
main-----9

出现了线程争抢现象。

但是,如果我在创建子线程以前又加入一段代码:

package com.Thread.test;

public class Test
{

	public static void main(String[] args) //主线程
	{
	    //新加入的代码:打印main(1)---i
		for(int i=0; i<10; i++) {
			System.out.println("main(1)----"+i);
		}
		
		//创建自线程对象tt
		TestThread tt =new TestThread();
		tt.start();
		
		//打印main(2)---i
		for(int i=0 ; i<10;i++) {
			System.out.println("main(2)-----"+i);
		}
	}

}

Output:

main(1)----0
main(1)----1
main(1)----2
main(1)----3
main(1)----4
main(1)----5
main(1)----6
main(1)----7
main(1)----8
main(1)----9
main(2)-----0
main(2)-----1
TestTread----0
TestTread----1
TestTread----2
TestTread----3
TestTread----4
TestTread----5
TestTread----6
main(2)-----2
main(2)-----3
main(2)-----4
TestTread----7
main(2)-----5
TestTread----8
TestTread----9
main(2)-----6
main(2)-----7
main(2)-----8
main(2)-----9

执行了多次,我们发现总是先打印main(1)—i然后main(2)—i和TestThread—i相互争抢,为什么会出现这样的情况,接下来用图来解释:
Java高级应用之线程与并发(thread&concurrence)_第2张图片
有了子线程之后,才会出现线程争抢现象,即main(2)—i 与 TestThread—i 交错出现。

2.1.1 设置线程名称1⃣️

  • 为子线程设置名称:子线程对象.setName();
  • 为主线程设置名称:Thread.currentThread().setName();
  • 子线程获取名称:this.getName();this表示调用方法的当前对象,在下面代码中this是tt
  • 主线程获取名称:Thread.currentThread().getName();
package com.Thread.test;

public class TestThread extends Thread
{
	public void run() {
		for(int i=0; i<10; i++) {
			System.out.println(this.getName()+i);//tt调用run()方法,所以this就是tt
		}
	}
}

package com.Thread.test;

public class Test
{

	public static void main(String[] args) //主线程
	{
	
		Thread.currentThread().setName("main:");
		//创建自线程对象tt
		TestThread tt =new TestThread();
		//子线程执行任务
		tt.start();
		
		for(int i=0 ; i<10;i++) {
			System.out.println(Thread.currentThread().getName()+i);
		}
	}

}
Thread-00
main:0
Thread-01
main:1
main:2
main:3
main:4
main:5
main:6
main:7
main:8
Thread-02
main:9
Thread-03
Thread-04
Thread-05
Thread-06
Thread-07
Thread-08
Thread-09

2.1.2 设置线程名称2⃣️

  • 利用有参构造器

为子线程设置名称:在TestThread的class界面引入父类Thread的带有String的构造方法
为主线程设置名称方法同上2.1.1
子,主线程获取名称方法同上2.1.1

Java高级应用之线程与并发(thread&concurrence)_第3张图片

package com.Thread.test;

public class TestThread extends Thread
{
	
	public TestThread(String name)
	{
		super(name);
		// TODO Auto-generated constructor stub
	}

	public void run() {
		for(int i=0; i<10; i++) {
			System.out.println(this.getName()+i);
		}
	}
}

package com.Thread.test;

public class Test
{

	public static void main(String[] args) 
	{
	
		Thread.currentThread().setName("main:");
		//创建子线程对象tt
		TestThread tt =new TestThread("Thread");
		//子线程执行任务
		tt.start();
		
		for(int i=0 ; i<10;i++) {
			System.out.println(Thread.currentThread().getName()+i);
		}
	}
}

2.1.3具体案例分析

马上到暑假了,准备买从伯明翰飞往北京的机票,阿联酋航空公司当天飞机票仅剩10张。还有100人需要购买当天的机票返程,阿联酋航空公司在有三种不同的途径进行网上售票,分别是阿联酋官网,携程,飞猪。请用线程知识抽象模拟售票场景。

package com.Thread.test;

public class BookingTickets extends Thread{
	
	//属性:三种途径共享10张票
	private int ticketsNumber=10;
    //带有string类型的父类构造方法
	public BookingTickets(String name)
	{
		super(name);
	}
	//重写run()方法
	public void run() {
		for(int i=0;i<100;i++) {
			if(ticketsNumber>0) {
				System.out.println("I got ticket from "+this.getName()+" and there are still "+(--ticketsNumber)+" tickets left.");
			}
		}
		
	}	
	
}

package com.Thread.test;

public class Test
{

	public static void main(String[] args){
		//分别创建三个线程,分别启动线程	
		BookingTickets emirates = new BookingTickets("Emirates");
		emirates.start();
		BookingTickets xiecheng = new BookingTickets("Xiecheng");
		xiecheng.start();
		BookingTickets feizhu = new BookingTickets("Feizhu");
		feizhu.start();
			
	}
}


Output:

I got ticket from Xiecheng and there are still 9 tickets left.
I got ticket from Xiecheng and there are still 8 tickets left.
I got ticket from Xiecheng and there are still 7 tickets left.
I got ticket from Emirates and there are still 9 tickets left.
I got ticket from Xiecheng and there are still 6 tickets left.
I got ticket from Xiecheng and there are still 5 tickets left.
I got ticket from Xiecheng and there are still 4 tickets left.
I got ticket from Xiecheng and there are still 3 tickets left.
I got ticket from Xiecheng and there are still 2 tickets left.
I got ticket from Xiecheng and there are still 1 tickets left.
I got ticket from Xiecheng and there are still 0 tickets left.
I got ticket from Emirates and there are still 8 tickets left.
I got ticket from Emirates and there are still 7 tickets left.
I got ticket from Emirates and there are still 6 tickets left.
I got ticket from Emirates and there are still 5 tickets left.
I got ticket from Emirates and there are still 4 tickets left.
I got ticket from Emirates and there are still 3 tickets left.
I got ticket from Emirates and there are still 2 tickets left.
I got ticket from Emirates and there are still 1 tickets left.
I got ticket from Emirates and there are still 0 tickets left.
I got ticket from Feizhu and there are still 9 tickets left.
I got ticket from Feizhu and there are still 8 tickets left.
I got ticket from Feizhu and there are still 7 tickets left.
I got ticket from Feizhu and there are still 6 tickets left.
I got ticket from Feizhu and there are still 5 tickets left.
I got ticket from Feizhu and there are still 4 tickets left.
I got ticket from Feizhu and there are still 3 tickets left.
I got ticket from Feizhu and there are still 2 tickets left.
I got ticket from Feizhu and there are still 1 tickets left.
I got ticket from Feizhu and there are still 0 tickets left.

我们发现并不是只有10张票,而是每一个购票途径都有10张票。
问题出在哪里???

问题在 private int ticketsNumber=10;相当于每一个线程建立都拥有了自己的10张票。
问题该如何解决???

加入静态修饰符staticprivate static int ticketsNumber=10;问题迎刃而解。此时的ticketsNumber被三个线程共享


2.2实现Runnable接口

为什么要有第二种实现多线程的方式呢?
原因:Java只允许单继承,一个子类只能继承一个父类,所以在Java中如果一个类继承了某一个类,同时又想用多线程,就不能extends Thread类,此时可以用Runable接口来实现多线程。

基本结构:

class 类名 implements Runnable{
			属性
			方法
			修饰符 run(){
				以线程处理的程序
			}
}

实例:

package com.Thread.test;

public class TestThread implements Runnable
{

	@Override
	public void run()
	{
		for(int i=0; i<10; i++) {
			
			System.out.println("Thread"+i);
		}
		
	}

}

package com.Thread.test;

public class Test1
{

	public static void main(String[] args)
	{
		TestThread tt =new TestThread();
		new Thread(tt).start();
		
		for(int i=0; i<10;i++) {
			System.out.println("main"+i);
		}

	}

}

Output:

main0
main1
main2
main3
main4
Thread0
main5
main6
Thread1
Thread2
main7
main8
main9
Thread3
Thread4
Thread5
Thread6
Thread7
Thread8
Thread9

注意⚠️:

  • 先创建对象TestThread tt =new TestThread();,然后将创建的对象tt传入新建的Thread里new Thread(tt),最后调用start()方法。上述代码使用了匿名对象的形式简化代码。

  • 拓展:这里使用了Java的静态代理模式,想了解关于静态代理模式,请前往查看主页文章:Java代理模式之静态代理模式(static proxy)「强烈推荐」
    https://blog.csdn.net/weixin_44551646/article/details/95011230

    完整版:

TestThread tt =new TestThread();
Thread th=new Thread(tt);
tt.start();

2.2.1设置线程名称

1.获取名称方法:
在实现Runnable接口时,无法使用getName来获取名字,只能用Thread.currentThread().getName()

2.设置名称方法:
设置名称时,在创建Thread对象时,可以传入线程名称

TestThread tt =new TestThread();
Thread th=new Thread(tt,“线程1); //在此传入线程名称即可!!!
tt.start();

使用匿名对象后(简化版):

TestThread tt =new TestThread();
new Thread(tt,“线程1).start();

使用匿名对象后(最简化版):

new Thread(new TestThread(),“线程1).start();

2.2.2Runnable+匿名内部类

若使用匿名内部类,基本结构:
new Thread(new Runnable(){……重写的run()方法};)
匿名内部类必须重写借助父类或者接口:new Runnable(){ }

public class Test1
{
	public static void main(String[] args)
	{
		new Thread(new Runnable(){  //若使用匿名内部类则应
				@Override
				public void run()
				{
					for(int i=0; i<10; i++) {	
						System.out.println("Thread"+i);
					}
				}
		}).start();
		
		for(int i=0; i<10;i++) {
			System.out.println("main"+i);
		}	
	}
}

想要了解更多关于匿名内部类,想查看主页文章【Java内部比较器与外部比较器+匿名内部类详细讲解】:https://blog.csdn.net/weixin_44551646/article/details/94741936#_302

2.3实现Callable接口

继承Thread类和实现Runnable接口两种方法创建多线程时,覆写的run()方法有缺点

@Override
public void run(){
      ........
}
  1. 没有返回值
  2. 不能抛出异常(该方法重写父类的方法,所以不能抛出异常)

为了解决上述两个问题,从JDK1.5以后,出现了第三种方法:实现Callable接口。

基本格式:

package com.Thread.test;

import java.util.concurrent.Callable;

public class TestThread implements Callable
{

	@Override
	public Integer call() throws Exception
	{
		
		return null;
	}
}

注意⚠️:

  1. Callable要用泛型限定可以是Integer,String……上述代码使用的Integer为例。
  2. 重写call()方法里的Integer是由上面的泛型决定的,当然也可以Integer,String……任何
  3. 重写call()方法里可以throws异常,也可以不抛出异常。
  4. 返回值类型与泛型类型相同。

三.线程的状态与转换

3.1线程状态

线程从创建到执行完成的整个过程称为线程的生命周期。一个线程在生命周期内总是处于一个状态,任何一个线程一般都具有以下五种状态:创建,就绪,运行,阻塞,终止

  • 创建状态:new 关键字和Tread类或其子类创建一个线程对象后,该线程对象就处于创建状态。它保持这个状态直到调用start()方法启动这个线程。
  • 就绪状态:线程一旦调用了start()方法后,就进入就绪状态。就绪状态的线程不一定立即运行,它处于就绪队列中,到等待JVM里线程调度器的调度。
  • 运行状态:当程序得到系统的资源后进入运行状态。
  • 阻塞状态:处于运行状态的线程因为某种特殊的情况,比如I/O操作,让出系统资源,进入阻塞状态,调度器立即调度就绪队列中的另一个线程开始运行。当阻塞事件解除后,线程由阻塞状态回到就绪状态。
  • 终止状态:线程执行完成或调用stop()方法时,进入终止状态。


什么是阻塞事件??—>Tips1
为什么阻塞状态解除后要进入就绪状态而不是运行状态???---->Tips2

3.2线程的调度

3.2.1线程的优先级

  • 线程从就绪状态到运行状态的转换依赖于线程调度,线程调度的依据之一是线程的优先级
  • 每个线程都有优先级,就绪队列中优先级高的线程被先调度的概率高,不一定是高的一定先被调用
  • 线程有10个优先级,用数字1-10表示,线程默认的优先级是5级
  • 对线程可通过setPriority(int)方法设置优先级
  • 通过getPriority()方法获取线程优先级
  • 3个常数:Thread.MIN_PRIORITY=1, Thread.MAX_PRIORITY=10, Thread.NORM_PRIORITY=5

3.2.2线程休眠

Thread.sleep(millis);括号内,如果填1000就是1秒,2000就是2秒。。。。。

package com.Thread.test;

public class TestThread extends Thread
{

	@Override
	public void run()
	{
		for(int i=0; i<10;i++) {
			System.out.println(this.getName()+"----"+i);
		}
	}
}

package com.Thread.test;

public class Test1
{

	public static void main(String[] args) throws InterruptedException
	{
		for(int i=3;i>0;i--) {
			System.out.println(i);
			Thread.sleep(1000);
		}
		
		TestThread tt = new TestThread();
		tt.start();
		
		for(int i=0; i<=10;i++) {
			System.out.println(Thread.currentThread().getName()+"----"+i);
		}

	}

}

Output:

3
2
1
Thread-0----0
Thread-0----1
Thread-0----2
Thread-0----3
Thread-0----4
Thread-0----5
Thread-0----6
Thread-0----7
Thread-0----8
Thread-0----9
main----0
main----1
main----2
main----3
main----4
main----5
main----6
main----7
main----8
main----9
main----10

在开始前,每隔1秒,倒数1个数,从3开始,3,2,1
线程才开始争抢。

3.2.3线程让步

yield()方法
线程让步是指暂停当前正在执行的线程对象,转而执行其他线程。

  • 如果当前线程优先级大于或等于线程池中其他线程的优先级,当前线程能获得更多的执行时间。
  • 如果某线程想让和它具有相同优先级的其他线程获取运行机会,使用让步方法yield()即可实现。
  • 优先级大的也不一定先执行,只是执行概率更大
  • 暂停当前执行线程对象,下一个也有可能是暂停的线程执行,也有可能是其他线程执行。
public class YieldTest extends Thread {
 
	public YieldTest(String name) {
		super(name);
	}
 
	@Override
	public void run() {
		for (int i = 1; i <= 50; i++) {
			System.out.println("" + this.getName() + "-----" + i);
			// 当i为20时,该线程就会把CPU时间让掉,让其他或者自己的线程执行(也就是谁先抢到谁执行)
			if (i == 20) {
				this.yield();
			}
		}
	}
 
	public static void main(String[] args) {
		YieldTest yt1 = new YieldTest("Tom");
		YieldTest yt2 = new YieldTest("Kevin");
		yt1.start();
		yt2.start();
	}

output:
在控制台有两种情况,第一种kevin抢到21

……
Tom-----19
Tom-----20
Kevin-----21
Kevin------22
……

第二种情况,Tom抢到21

……
Tom-----19
Tom-----20
Tom-----21
Tom-----22
Tom-----23
……

3.2.4线程联合

join()方法

package com.Thread.test;

public class TestThread extends Thread
{

	@Override
	public void run()
	{
		for(int i=0; i<10;i++) {
			System.out.println(this.getName()+"----"+i);
		}
	}
}

package com.Thread.test;

public class Test1
{
	public static void main(String[] args) throws InterruptedException
	{
		for(int i=0; i<=10;i++) {
			if(i==6) {
				TestThread tt = new TestThread();
				tt.start();
				tt.join();
			}
			System.out.println(Thread.currentThread().getName()+"----"+i);
		}

	}

}

Output:

main----0
main----1
main----2
main----3
main----4
main----5
Thread-0----0
Thread-0----1
Thread-0----2
Thread-0----3
Thread-0----4
Thread-0----5
Thread-0----6
Thread-0----7
Thread-0----8
Thread-0----9
main----6
main----7
main----8
main----9
main----10

join()方法作用就是中途加入到主线程中,这个线程先被执行,执行完了再执行其他线程。
注意⚠️:必须先start(),再join()才可以

3.2线程的同步

3.2.1 线程安全

请查看主页文章:教你弄懂线程安全(史上最详)
https://blog.csdn.net/weixin_44551646/article/details/95181050

你可能感兴趣的:(Java之高级应用)