多线程-day08

多线程

今日学习内容:

  • 线程的创建和启动
  • 线程的生命周期
  • 线程生命周期中的各个状态
  • 操作线程的多个方法
  • 线程的同步代码块
  • 线程的同步方法

今日学习目标:

  • 了解进程和线程的区别
  • 认识主线程和后台线程
  • 掌握使用继承方式编写多线程
  • 掌握使用实现方式编写多线程
  • 区分继承和实现方式编写多线程的优劣
  • 掌握线程的生命周期图
  • 掌握线程多个状态之间的转换关系
  • 了解操作线程的方法
  • 掌握使用同步代码块和同步方法实现线程的同步操作

24. 线程与并发

24.1. 理解线程与进程的区别(了解)

进程:是指一个内存中运行的应用程序,每个进程都有自己独立的一块内存空间,一个进程中可以有多个线程(1,n)。比如在Windows的任务管理器中,一个运行的xx.exe就是一个进程。

线程:是指进程中的一个执行任务(控制单元),一个进程中可以运行多个线程,多个线程可共享数据。

多进程:操作系统中同时运行的多个程序。

多线程:在同一个进程中同时运行的多个任务。

一个进程至少有一个线程,为了提高效率,可以在一个进程中开启多个控制单元,这就是多线程。

24.2. 主线程 main(了解)

在运行一个简单的Java程序的时候,就已经存在了两个线程,一个是主线程,一个是后台线程——维护的垃圾回收。主线程很特殊,在启动JVM的时候自动启动的。

24.3. 线程的创建和启动(掌握)

方式一,继承Thread类:

  • 自定义类继承Thread
  • 覆写run方法
  • 创建自定义类对象
  • 自定义类对象调用start方法
class MyThread extends Thread {
	public void run() {
		//线程体,线程启动时,会自动调用本方法,所有这里是我们写代码的主体部分
	}
}

public class ExceptionDemo {
	public static void main(String[] args) {
		MyThread t = new MyThread();
		t.start();//调用Thread的start方法,JVM会自动调用run方法。
	}
}

方式二,实现Runnable 接口

  • 自定义类实现Runnable接口
  • 覆写run方法
  • 创建自定义类对象
  • 把自定类的对象作为Thread类构造器参数,并调用Thread对象start方法
class MyRunnable implements Runnable {
	public void run() {
		//线程体,线程启动时,会自动调用本方法,所有这里是我们写代码的主体部分
	}
}

public class ThreadDemo2 {
	public static void main(String[] args) {
		MyRunnable target = new MyRunnable();
		Thread t = new Thread(target);
		t.start();
	}
}

第一种使用起来方便,启动一个线程也方便,很多功能都在Thread类中定义好了;

第二种方式启动得依赖于Thread,因为本身Runnable中只有run方法,请看Thread的构造方法。

24.3.1. 线程体-run方法(掌握)

不管哪种方式创建的线程,都得覆写run 方法,因为这是线程体方法,该方法在线程启动之后会自动被调用。

public void run() {
	//线程体,线程启动时,会自动调用本方法,所有这里是我们写代码的主体部分
}

线程的执行随机性:

一旦一个线程启动之后就是一个独立的线程,等待CPU的调度分配资源,不会因为启动它的外部线程结束而结束。

class MyThread extends Thread {
	public void run() {
		//自定义线程中的for循环打印i,打印顺序是完全随机的。
		for (int i = 0; i < 10; i++) {
			System.out.println("MyThread  ==> " + i);
		}
	}
}

public class Demo {
	public static void main(String[] args) {
		MyThread mt = new MyThread();
		mt.start();
		//主线程中的for循环打印i
		for (int i = 0; i < 10; i++) {
			System.out.println("main  ==> " + i);
		}
	}
}

多次运行该程序,观察每次运行的结果。

24.3.2. 线程的启动(掌握)

​ 启动线程必须调用线程类Thread中的start方法,该方法应该由Thread类的一个实例来调用,下面是方法签名:

public void start() 

底层会调用该线程的 run 方法。

只有调用了线程对象的start方法才会开启一个新的线程,如果是直接调用对象的run方法不会开启新的线程,只是一个单线程。

注意:启动一个新线程,不能使用run()方法,只能使用start方法。

24.4. 线程生命周期和状态(掌握)

在这里插入图片描述

  • 新建:当程序使用new创建一个线程后,该线程处于新建状态,此时他和其他java对象一样,仅仅由Java虚拟机为其分配内存并初始化成员变量值。
  • 可运行状态:RUNNABLE状态实际上可细分成两种状态,READY和RUNNING。分别表示就绪状态和运行状态。
    • 就绪状态:当线程对象调用start()方法后,该线程处于就绪状态,进入线程队列排队。此时该状态线程并未开始执行,仅表示可以运行了。至于该线程何时运行,取决于CPU调度器的调度。
    • 运行状态:表示某线程对象被CPU调度器的调度,执行线程体。就绪状态和运行状态是可以相互切换的,切换的原因依旧参照CPU调度器调度了哪一个线程。
  • 阻塞状态:正在运行的线程遇到某个特殊情况如,同步、等待I/O操作完成等。 进入阻塞状态的线程让出CPU资源,并暂时停止自己的执行。
  • 等待状态:有时一个可运行状态线程转变成等待状态,它会等待另一个线程来执行一个任务,一个等待状态的线程只有通过另一个线程通知它转到可运行状态,才能继续执行。(唤醒)
  • 计时等待状态:进入等待状态的线程如果没有其他线程对象来唤醒它,那么该线程对象将一直等待下去,此时我们可以考虑设计一个类似于闹钟一样的提示器,该提示器会在等待特定时间段之后唤醒该线程对象。
  • 终止状态:即死亡状态,表示线程终止。当线程成功执行完成或线程抛出未捕获的Exception或Error或调用线程的stop方法(易导致死锁,不推荐)。
24.5. 操作线程的方法(掌握)
24.5.1. join方法(了解)

​ join方法的主要作用就是同步,它可以使得线程之间的并行执行变为串行执行。

​ 比如在A线程中调用了B线程的join()方法时,表示只有当B线程执行完毕时,A线程才能继续执行。

24.5.2.sleep方法(了解)

sleep方法让正在执行的线程暂停一段时间,进入阻塞状态,常常用来模拟网络延迟等。

sleep(long milllis) throws InterruptedException //毫秒为单位

调用sleep()后,在指定时间段之内,该线程不会获得执行的机会

24.5.3. 线程的优先级(了解)

​ 每个线程都有优先级,优先级的高低只和线程获得执行机会的次数多少有关。并不是说优先级高的就一定先执行,哪个线程的先运行取决于CPU的调度;

Thread对象的setPriority(int x)和getPriority()用来设置和获得优先级。

24.5.4. 后台线程(了解)

所谓后台线程,一般用于为其他线程提供服务。也称为守护线程。JVM的垃圾回收就是典型的后台线程。

**特点:**若所有的前台线程都死亡,后台线程自动死亡。

Thread对象setDaemon(true)用来设置后台线程。

setDaemon(true)必须在start()调用前,否则抛IllegalThreadStateException异常。

25. 线程安全性

25.1. 继承方式VS实现方式(掌握)

当多线程并发访问同一个资源时,会导致线程出现安全性的原因,看案例。

案例:现有50个苹果,现在有请三个童鞋(小A、小B、小C)上台表演吃苹果。

因为ABC三个人可以同时吃苹果,此时得使用多线程技术来实现这个案例。
分析: 可以定义三个线程对象,并启动线程.
第一步:每一个同学吃苹果的时候:先展示自己拿到手上苹果的编号,如12336...
第二步:再吃掉苹果(意味着苹果的总数少一个)

使用继承方式

class Person extends Thread {
	private int num = 50;//苹果总数

	public Person(String name) {
		super(name);
	}

	public void run() {
		for (int i = 0; i < 50; i++) {
			if (num > 0) {
				System.out.println(super.getName() + "吃了编号为:" + num-- + "的苹果");
			}
		}
	}
}

public class ThreadDemo1 {
	public static void main(String[] args) {
		//创建三个线程,吃苹果
		new Person("小A").start();
		new Person("小B").start();
		new Person("小C").start();
	}
}

使用继承方式完成该案例的时候,会发现A、B、C都各自吃了50个苹果,为何?

使用实现方式

class Apple implements Runnable {
	private int num = 50;//苹果总数
	public void run() {
		for (int i = 0; i < 50; i++) {
			if (num > 0) {
				System.out.println(Thread.currentThread().getName() 
+ "吃了编号为:" + num-- + "的苹果");
			}
		}
	}
}

public class ThreadDemo2 {
	public static void main(String[] args) {
		Apple a = new Apple();
		//创建三个线程,吃苹果
		new Thread(a, "小A").start();
		new Thread(a, "小B").start();
		new Thread(a, "小C").start();
	}
}

在使用实现方式的时候,我们发现A、B、C一共吃了50个苹果,为何?

通过吃苹果比赛,分析继承方式和实现方式的区别:

继承方式:

  • Java中类是单继承的,如果继承了Thread了,该类就不能再有其他的直接父类了。
  • 从操作上分析,继承方式更简单,获取线程名字也简单。
  • 从多线程共享同一个资源上分析,继承方式不能多个线程共享同一个资源。

实现方式:

  • Java中类可以多实现接口,此时该类还可以继承其他类,并且还可以实现其他接口(设计上,更优雅)。
  • 从操作上分析,获取线程名字也比较复杂,得使用Thread.currentThread()来获取当前线程的引用。
  • 从多线程共享同一个资源上分析,实现方式可以多线程共享同一个资源。
25.2. 线程同步(掌握)

当多线程并发访问同一个资源对象的时候,可能出现线程不安全的问题。

但是,分析打印的结果,有时候发现没有问题:

意识:看不到问题,不代表没有问题,可能是我们经验不够,或者说问题出现的不够明显。

那么可以使用线程休眠来模拟网络延迟,让问题来得更明显一些:

Thread.sleep(10);//当前线程睡10毫秒,当前线程休息着,让其他线程去抢资源.      

在程序中并不是使用Thread.sleep(10)之后程序才出现问题,而是使用之后,问题更明显,休眠的时间越久问题越明显,一般用10或100即可,具体根据情况而定。

class Apple implements Runnable {
	private int num = 50;//苹果总数

	public void run() {
		try {
			for (int i = 0; i < 50; i++) {
				if (num > 0) {
					Thread.sleep(10);
					System.out.println(
                       Thread.currentThread().getName()+ "吃了编号为:" + num-- + "的苹果"
                    );
				}
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}

分析运行结果,为什么有错误的结果。

在这里,打印苹果的编号和苹果数量的减少,应该是一个原子操作,也就说是一个不能分割的操作,两个步骤之间不能被其他线程插一脚。

第一步:System.out.println(Thread.currentThread().getName()
 + "吃了编号为:" + num + "的苹果");
第二步:num --;

**解决方案:**保证打印苹果编号和苹果总数减1操作,必须同步完成。

**解决思路:**A线程获得同步锁进入操作的时候,B和C线程只能在外等着,A操作结束,释放同步锁。A和B和C才有机会去抢同步锁(谁获得同步锁,谁才能执行代码)。

通俗例子:A、B、C三个人去抢厕所的雅间,为了保证安全规定谁抢到了必须上锁,把其他人排除外雅间外面。若A抢到了,进入后应该立马上锁,B和C只能在外等着,当A释放锁出来的时候,A、B、C又开始尝试抢资源。

  • 方式1:同步代码块
  • 方式2:同步方法
25.2.1. 同步代码块(掌握)

同步代码块语法:

synchronized(同步锁){
     //需要同步操作的代码
}

同步锁,又称之为同步监听对象/同步锁/同步监听器/互斥锁:

为了保证每个线程都能正常执行原子操作,Java引入了线程同步机制。

对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁。

Java程序允许使用任何对象作为同步监听对象,一般的,我们把当前并发访问的共同资源作为同步监听对象,比如此时三个线程的共同资源Apple对象。

注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就执行,其他的线程只能在代码块外等着。

class Apple implements Runnable {
	private int num = 50;//苹果总数

	public void run() {
		try {
			for (int i = 0; i < 50; i++) {
				synchronized (this) {
					if (num > 0) {
						Thread.sleep(10);
						System.out.println(Thread.currentThread().getName() 
+ "吃了编号为:" + num-- + "的苹果");
					}
				}
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}

此时的同步锁this表示Apple对象,而程序中Apple对象只有一份,故可以作为同步锁。

25.2.2. 同步方法(掌握)

使用synchronized修饰的方法,就叫做同步方法。保证A线程执行该方法的时候,其他线程只能在方法外等着。

synchronized  public  void  doWork(){
     ///TODO
}

此时同步锁是谁——其实就是,调用当前同步方法的对象:

  • 对于非static方法,同步锁就是this。
  • 对于static方法,同步锁就是当前方法所在类的字节码对象。
class Apple implements Runnable {
	private int num = 50;//苹果总数

	public void run() {
		for (int i = 0; i < 50; i++) {
			this.eat();
		}
	}

	synchronized private void eat() {
		try {
			if (num > 0) {
				Thread.sleep(10);
				System.out.println(
                    Thread.currentThread().getName() + "吃了编号为:" + num-- + "的苹果"
                );
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}

25.2.3. synchronized的优劣(掌握)

好处:保证了多线程并发访问时的同步操作,避免线程的安全性问题。

缺点:使用synchronized的方法/代码块的性能要低一些。

建议:尽量减小synchronized的作用域。

面试题:

  1. StringBuilder和StringBuffer的区别
  2. 说说ArrayList和Vector的区别
  3. HashMap和Hashtable的区别

通过源代码会发现,主要就是方法有没有使用synchronized的区别,比如StringBuilder和StringBuffer。

在这里插入图片描述

因此得出结论:使用synchronized修饰的方法性能较低,但是安全性较高,反之则反。

学习优势:

1.包含java前后端从 0 ->1 全过程教学, 内容全面, 知识点不遗漏, 学完即可参加实际工作.
2.课程为目前项目开发常用的技术知识,向用人单位对标,学以致用。那些脱离实际,废弃不用的,太前沿的框架技术前期不建议学。
3.一起学习,打卡,一起交流,希望能营造一个和线下一样的学习环境。

需要进微信学习交流群, 或者想领取代码,文档,全套视频的同学请+v:lmm99964

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值