黑马毕向东Java课程笔记(day11):多线程(第一部分)——进程与线程+线程创建+线程安全与同步代码块+同步锁/死锁

  • 多线程好文:添加链接描述
  • 锁机制:synchronized、Lock、Condition、volatile(原子性可见性)——参考添加链接描述
    1、进程与线程概述
      首先,对于CPU执行每一个程序,其实都是在快速切换程序,一个核的CPU只能同时执行一个任务,因为CPU切换很快,我们反应不过来。
      所谓进程,字面意思上理解就是“是一个正在执行中的程序”。每一个进程执行都有一个执行顺序,该顺序是一个执行路径,或者叫一个控制单元。对于一个进程,它还有可能分为多条执行路径(如进程迅雷下载可能分为多路径进行下载,这多条路径同时发送下载请求到服务端,这样就可以多路径同时下载,效率更高),这每一条执行路径就称为线程。
      也就是说,线程是进程中的内容,每一个应用程序(进程)至少有一个线程,因为线程是进程独立的控制单元,或者说是执行路径,线程在控制着进程的执行。(结合视频11-1,10分30秒开始处对进程线程进行理解)
      对于java,它有一个编译进程和一个运行进程。Java 虚拟机启动的时候会有一个进程java.exe,该进程中至少一个线程负责java程序的执行,而且这个线程运行的代码存在于main方法中,该线程称之为主线程。其实更细节说明jvm,jvm启动不止一个线程,还有负责垃圾回收机制的线程。(一个进程中有多个线程在执行,这就是多线程)。
    关于进程与线程的深入理解,参考如下文章:
    深入理解进程线程

2、线程的创建
  我们写代码的时候也想自定义创建一些线程,让某些代码可以同时执行。
  线程对象是可以产生线程的对象。比如在Java平台中Thread对象,Runnable对象。线程,是指正在执行的一个指点令序列。在java平台上是指从一个线程对象的start()开始,运行run方法体中的那一段相对独立的过程。相比于多进程,多线程的优势有:

  1. 进程之间不能共享数据,线程可以;
  2. 系统创建进程需要为该进程重新分配系统资源,故创建线程代价比较小;
  3. Java语言内置了多线程功能支持,简化了java多线程编程。
      如何在自定义的代码中,自定义一个线程呢?java已经提供了对线程这类事物的描述,就是Thread类。

2.1、继承Thread类创建线程类
  通过继承Thread类创建线程类的具体步骤和具体代码如下:
1)定义一个继承Thread类的子类,并重写该类的run()方法;
  目的:将自定义代码存储在run方法,让线程运行。

2)创建Thread子类的实例,即创建了线程对象;

3)调用该线程对象的start()方法启动线程。
  该方法两个作用:启动线程,调用run方法。

class Demo extends Thread
{
   
	public void run()
	{
   
		System.out.println("thread run");
	}
}

public class ThreadDemo
{
   
	public static void main(String[] args) 
	{
   
		Demo d = new Demo();//建立好一个继承Thread类的类Demo的对象,相当于创建好一个线程
		d.start();
		//start():使该线程开始执行,Java 虚拟机调用该线程的 run 方法。 
//		结果是两个线程并发地运行,当前线程(从调用返回给 start 方法)和另一个线程(执行其 run 方法)。 
		d.run();//直接写d.run(),这就相当于没有开启另一个线程,而是直接调用的run()方法,就是直接执行完run(),再执行下面代码,只有一个主线程。

		Thread t = new Thread();//这样也相当于创建一个线程
		t.start();//开启线程,start()调用run()非法
		//但是这样我们不知道要运行什么代码,因为run()非法没有重写,没有运行内容
		//而且开启线程是为了运行自己制定的代码,上面这样设置线程没有意义
	}
}
//结果是:thread run

  再看下面这个例子,我们发现2个线程(主线程以及Demo线程)是穿插打印的。(参考视频11-2的11分30秒开始处的解释)这个结果说明2个线程在同时执行。(一核CPU在一个时候只能执行一个进程,看起来是在同时执行多个进程,其实CPU是在切换进程,更细一点说,CPU是在切换线程)如果CPU有多核,就可以达到同时执行的效果。

class Demo extends Thread
{
   
	public void run()
	{
   
		for(int x=0 ; x<60 ; x++)
			System.out.println("thread run"+x);
	}
}

public class ThreadDemo
{
   
	public static void main(String[] args) 
	{
   
		Demo d = new Demo();
		d.start();
		
		for(int x=0 ; x<60 ; x++)
			System.out.println("demo run"+x);
	}
}

在这里插入图片描述
  发现运行结果每一次都不同。因为多个线程都获取cpu的执行权,cpu执行到谁,谁就运行。明确一点,在某一个时刻,只能有一个程序在运行(多核除外),cpu在做着快速的切换,以达到看上去是同时运行的效果。我们可以形象把多线程的运行行为在互相抢夺cpu的执行权。
  这就是多线程的一个特性:随机性,谁抢到谁执行,至于执行多长,cpu说的算。
  为什么要覆盖run方法呢?Thread类用于描述线程,该类就定义了一个功能,用于存储线程要运行的代码。该存储功能就是run方法。也就是说Thread类中的run方法,用于存储线程要运行的代码。

  练习:

class Demo extends Thread
{
   
	private String name;
	Demo(String name)
	{
   
		this.name = name;
	}
	public void run()
	{
   
		for(int x=0 ; x<60 ; x++)
			System.out.println(name+"运行"+x);
	}
}

public class ThreadDemo
{
   
	public static void main(String[] args) 
	{
   
		//注意,不需要创建多个Demo继承Thread,因为只要new一个Demo的对象就是创建一个新的线程
		Demo d1 = new Demo("线程一");
		Demo d2 = new Demo("线程二");
		d1.start();
		d2.start();
				
		for(int x=0 ; x<60 ; x++)
			System.out.println("主线程运行"+x);	
	}
}

  接下来说一下线程运行状态,线程运行有4种常见状态(当然还有其他状态,如阻塞):被创建,运行,冻结,消亡,这4种状态之间的关系如下图:(参考视频11-5的解释)
在这里插入图片描述
  需要说明的是,运行状态具有运行资格,也有执行权,当CPU挂起这个线程,进入阻塞状态,阻塞状态具有运行资格(可以运行),但是没有执行权(CPU执行它),当CPU又进入这个线程使其执行,又会从阻塞状态回到运行状态。
  另一方面,运行状态可以通过sleep(time)或者wait()进入冻结状态,sleep(时间)到就会返回,而wait()需要notify()方法使其返回。冻结状态放弃了运行资格(CPU的执行权在这个线程手上,是这个线程自己不想执行,放弃资格,进入冻结状态的时候他也有执行权,因为CPU在这个线程)。冻结状态返回的时候会先返回阻塞状态(有运行资格,没有CPU执行权),因为它返回的时候又取回自己的运行资格,但是它冻结返回时,不知道CPU还在不在这个线程执行。所以先回阻塞,如果CPU在这个线程执行,就恢复执行权返回运行状态;如果CPU不在这个线程,就现在阻塞状态等待CPU回来给他赋予执行权。

线程的五大状态及其转换
1、resume与suspended一起使用, wait与notify(notifyAll)一起使用, sleep会让线程暂时不执行 。	suspend()resume() 方法:两个方法配套使用,suspend()使得线程进入阻塞状态,并且不会自动恢复,必须其对应的 resume() 被调用,才能使得线程重新进入可执行状态。

2、线程从创建、运行到结束总是处于下面五个状态之一:新建状态、就绪状态、运行状态、阻塞状态及死亡状态。
	1)新建状态(New): 当用new操作符创建一个线程时, 例如new Thread(r),线程还没有开始运行,此时线程处在新建状态。 当一个线程处于新生状态时,程序还没有开始运行线程中的代码
	2)就绪状态(Runnable)
	一个新创建的线程并不自动开始运行,要执行线程,必须调用线程的start()方法。当线程对象调用start()方法即启动了线程,start()方法创建线程运行的系统资源,并调度线程运行run()方法。当start()方法返回后,线程就处于就绪状态。
	处于就绪状态的线程并不一定立即运行run()方法,线程还必须同其他线程竞争CPU时间,只有获得CPU时间才可以运行线程。因为在单CPU的计算机系统中,不可能同时运行多个线程,一个时刻仅有一个线程处于运行状态。因此此时可能有多个线程处于就绪状态。对多个处于就绪状态的线程是由Java运行时系统的线程调度程序(thread scheduler)来调度的。
	3)运行状态(Running)
	当线程获得CPU时间后,它才进入运行状态,真正开始执行run()方法.
	4)阻塞状态(Blocked)
	线程运行过程中,可能由于各种原因进入阻塞状态: 1>线程通过调用sleep方法进入睡眠状态; 2>线程调用一个在I/O上被阻塞的操作,即该操作在输入输出操作完成之前不会返回到它的调用者; 3>线程试图得到一个锁,而该锁正被其他线程持有; 4>线程在等待某个触发条件; ......
	所谓阻塞状态是正在运行的线程没有运行结束,暂时让出CPU,这时其他处于就绪状态的线程就可以获得CPU时间, 进入运行状态。
	5)死亡状态(Dead)
	有两个原因会导致线程死亡:
	i) run方法正常退出而自然死亡,
	ii) 一个未捕获的异常终止了run方法而使线程猝死。 为了确定线程在当前是否存活着(就是要么是可运行的,要么是被阻塞了),需要使用isAlive方法。如果是 可运行或被阻塞,这个方法返回true; 如果线程仍旧是new状态且不是可运行的, 或者线程死亡了, 则返回false.

在这里插入图片描述

  当我们创建的线程一多,我们就需要一些标识来区分线程,线程也有自己的名称。想获取线程名称,我们就得获取线程的对象。
  示例1:获取线程名称

/*
线程都有自己默认的名称,格式为:Thread-编号,该编号从0开始。
*/
class Demo extends Thread
{
   
	private String name;
	Demo(String name)
	{
   
		this.name = name;
	}
	public void run()
	{
   
		for(int x=0 ; x<60 ; x++)
			//此处用this代表Demo类的对象调用Demo父类Thread的getName()方法来获取线程名称
			System.out.println(this.getName()+"运行"+x);
	}
}

public class ThreadDemo
{
   
	public static void main(String[] args) 
	{
   
		Demo d1 = new Demo("线程一");
		Demo d2 = new Demo("线程二");
		d1.start();
		d2.start();
				
		for(int x=0 ; x<60 ; x++)
			System.out.println("主线程运行"+x);	
	}
}

  示例2:设置取线程名称

/*
此处不用setName()方法设置线程名称,Thread类有一个Thread(String name)的构造方法,因此我们在初始化的时候就可以设置线程名称。
我们可以通过Thread类的有参构造方法,直接给线程名称赋值,而不需要通过setName()方法
也就得说,只要我们在子类中调用父类Thread的有参数的构造方法,我们就可以直接给线程对象赋值
thread类中有类似如下代码。
class Thread
{
	private String name;//这是存储线程名称的变量
	Thread(String name)
	{
		this.name = name;//通过Thread有参构造方法给线程名称赋值
	}
}
*/
class Demo extends Thread
{
   
	Demo(String name)
	{
   
		super(name);//调用父类Thread类有参的构造方法,并将想设置的名称用name参数传入
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值