十五、《多线程+线程安全》

1、文章介绍

  • 线程是什么
  • 实现创建启动线程的方式1
  • 实现创建启动线程的方式2
  • Thread类常用方法
  • 线程同步3种方式

2、线程是什么

2.1、线程常识引入

  1. 我们知道CPU是计算机中央处理器,用来处理任务的,那么他是如何处理任务的,我们计算机存在一个任务管理器,查阅线程,
  2. 一个进程就是一个软件,对应多个线程,多线程软件运行速度比较快,比如迅雷,快播等..下载速度比较快

2.2、什么是线程

  1. 任务管理器可以有多个进程,每个进程运行的都是可执行程序,一个可执行程序就是一个软件,可执行程序的本质就是在计算机当中运行的一块代码

进程:可以看成是在计算机当中运行的一块代码

线程:可以看成是在计算机当中运行的一小块代码

2.3、线程与进程的关系

  1. 一个进程中可以有多个线程,至少得有一个线程;
  2. 上面说一个进程可以狭隘的看成是一大段代码,那其实线程也是一段代码
  3. 线程是进程中的最小单位;
  4. 也可以把线程看成是一个轻量级的进程

注意常识 : 计算机安装一个软件,软件是由代码构成,当启动一个软件之后,代码被放入内存中,为什么会放入到内存中,因为内存的读写速度比较快,这时候CPU就开始处理内存当中的任务,也就是当前软件程序[ 代码 ]运行起来了。

2.4、CPU如何处理任务?

  1. 在单位时间时间片上只能执行一个线程
  2. CPU看到内存中有很多的线程,CPU在单位时间片(时间片:很微小的时间单位)上高速切换线程执行

2.5、多线程下载软件为什么快

  1. 问题:很多使用多线程技术开发的软件,下载速度比较快,例如,迅雷.....某些软件,QQ影音,快播..迅雷影音等等...为什么下载速度会比较快

  1. 假设上面软件都是运行在同一台电脑上面,两款软件运行,肯定是由一个CPU在处理该任务
  2. CPU处理任务最小单位是线程,CPU是通过资源分配的方式,在多个线程之间,以时间片(时间片:很微小的时间单位)为单位,高速切换内存中要执行的线程任务。
  3. 在同一个时间片上,只能处理一个线程
  4. 在CPU的眼中,只看到内存中有很多线程,大家都是平等的,获取到CPU处理的机会是均等的,CPU会平均分配资源给每一个线程
  5. 假设每个线程执行一分钟,快播软件占用CPU时间为三分钟,迅雷占用CPU处理任务的时间为1分钟,自然快播处理的任务会更多,下载的内容更多

2.6、总结线程的作用

  1. 线程的作用:看下面两种理解方式:
  2. 可以将代码中(软件)的某些独立的功能包装起来,单独作为任务交给CPU处理!
  3. 将需做的某个功能封装成一个线程体,该线程可以独立的获得CPU分配的资源

从而实现多功能同时运行。

3、实现创建启动线程的方式一

3.1、自定义第一个线程场景描述

  1. 开发一个游戏(LOL),实现的功能一边玩游戏,一边播放背景音乐

3.2、实现流程分析

  1. 游戏的本质也是软件:该软件包含两项功能
    1. 玩游戏,暂时使用一个打印语句来代替该功能的演示
    2. 播放背景音乐,暂时也使用一个打印语句来代替该功能

 

3.3、代码实现

思考:根据上面的分析如何下手写?

  1. 将功能代码写在哪里
    1. 将功能主体代码写到Thread类中run方法里面?
    2. 如果Thread类当中,写了游戏功能,那么播放音乐怎么办???
    3. 所以不能写在Thread类当中
  2. 我们需要自己定义类继承Thread类,不但具有里面的东西,还具有Thread类的特性,自定义类也是一个线程类,然后覆写run方法,然后把我们的代码写在我们覆写的run方法里面,然后启动
  3. 根据上面的场景我们需要创建哪些类?
    1. 玩游戏的线程类
    2. 放音乐的线程类
    3. 测试类:创建①  ②的对象,然后调用start方法启动
  4. 代码清单:
    public class GameThread extends Thread {
    	@Override
    	public void run() { // 包装独立的功能
    		for (int i = 0; i < 100; i++) {
    			System.out.println(i+"  吃鸡......");
    		}
    	}
    }
    public class MusicThread extends Thread{
    	@Override
    	public void run() {
    		for (int i = 0; i < 100; i++) {
    			System.out.println(i+"  凉凉.....");
    		}
    	}
    }

    测试代码:

    public class Test {
    	public static void main(String[] args) {
    		GameThread gt = new GameThread();
    		MusicThread mt = new MusicThread();
    		gt.start();
    		mt.start();
    	}
    }

3.4、小结:创建启动线程的方式一(继承Thread类)

  1. 先明确我们需要把什么事情封装成一个线程对象
  2. 自定义一个类 extends  Thread
  3. 覆写run方法,在这里写1步中的代码
  4. 创建一个自定义类的对象 t
  5. 启动线程  t.start();
  6. 注意执行过程:本质是代码执行到一个位置之后,如果切换到另一个线程,在切换回来,那么会从刚才切换走的代码位置继续执行:产生线程安全问题的原因,就在此...

 

4、线程注意事项

4.1、直接调用run方法和start的区别?

  1. 可以直接调用run方法,但是没有启动一个独立的线程;
  2. 只有调用start 才会启动一个独立的线程;

4.2、自己启动的线程和主线程有关系吗?

  1. 直接写一个最简单的hello word 程序,就有一个主线程
  2. 一个线程一旦启动就是独立的了,和创建启动它的环境没有直接的包含关系
public class Test2 {
/** 
    测试主线程执行完毕我们自定义的线程还是会继续执行(前提就是主线程完了,自定义的线程还没有执行        完)
*/
	public static void main(String[] args) {
		new ThreadTest().start();
		for (int i = 0; i < 100; i++) {
			System.out.println("main"+i);
		}
	}
}


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

5、经典案例:多线程售票示例

5.1、需求分析

 

思考:上面示例应该这么下手?

        1.票,用什么来存放票:票池

                可以使用一个变量int num = 50;来表示票池,当卖出一个张票,该变量就自减一次num--

        2.本质就是要创建启动线程(流程)

                明确需要把什么事情封装成独立的线程对象

                卖(一张)票的操作

                怎么操作

                判断是否有票,如果有票就卖一张

                票总数减一

        3.怎么实现:

                自定义一个类 extends Thread类

                实现功能自定义类覆写Thread类当中的run方法,实现伪代码功能

                创建线程对象调用start方法启动线程

问题:----------------------->

  1. 共享问题,用来存放票量的,变量应该如何被多个窗口共享
  2. 应该自定义几个类?1

之前游戏有不同功能,写了不同类,卖票都是一个功能,所以定义一个类就行了

  1. 应该创建几个对象?3

三个窗口,可以创建三个对象来表示三个窗口

5.2、代码实现及分析

5.2.1、代码实现版本1:

问题:为什么只卖了三张票 且 票号都是50

  1. 三张:线程对象run方法当中为线程主体:该程序只会执行一次,所以线程主体程序结束了,线程任务也就完成了。
  2. 都是票号50:每个线程主体都是独立的,各自使用自己的实例变量初始值都是50

 5.2.2、代码版本2:解决售票3张的问题票号固定50:使用while(num>0)直到票池销售完毕

public class Ticket extends Thread {
	int num = 50;
	@Override
	public void run() {
		while(num>0){
			System.out.println("您的票号 : " + num);
			num--;	
		}
	}
}

 

问题:为什么总售票数量150

  1. 每个线程体都是独立的,num是实例变量,每个线程对象都有自己的一个num变量,各自销售50张

5.2.3、代码版本3:解决售票数量150问题:给num变量添加static 是的num多个线程对象共同享有一个变量

public class Ticket extends Thread {
	static int num = 50;
	@Override
	public void run() {
		while(num>0){
			System.out.println("您的票号 : " + num);
			num--;	
		}
	}
}

 

问题:为什么销售的是51张

  1. 因为线程安全

5.2.4、最终代码:处理线程安全及业务需求,销售一张票应该添加if判断

public class Ticket extends Thread {
	static int num = 50;
	@Override
	public void run() {
		while(num>0){
			if(num>0){
				System.out.println("您的票号 : " + num);
				num--;					
			}
		}
	}
}

6、实现创建启动线程方式二

6.1、分析线程实现方式2

6.1.1、线程创建方式我们已经体验过一种了,不过线程我们还有其他方式可以实现,Thread             impldements Runable这个接口

6.1.2、分析Runable接口与Thread类之间的关系:

6.1.3、Thread线程类本质是实现Runnable接口

  1. 通过查看API得知,Thread当中的run方法不是来自于自身,而是通过实现Runable接口里面的run方法,从而实现某个类的实例,可以通过线程的方式实现功能,类必须定义一个名为run的无参数方法
  2. 本质Thread也是通过实现接口来实现线程功能的
  3. 如果自定义一个类,完全可以通过实现该接口从而,通过线程实现功能

3.1.4、自定义类通过实现Runable的方式来实现线程,如何启动

  1. 通过实现Runable实现线程的,自定义类,的对象A。放在一个空壳的Thread线程对象当中
  2. 然后通过该对象来调用start方法启动线程A

 代码清单:

public class TicketThread implements Runnable{
	private int num = 50;  
	public void run() {
		// 最终的代码
		while(num>0){
			System.out.println("您的票号是:"+num);
			num--;
		}
	}
}

测试代码:

TicketThread tt = new TicketThread();
Thread t1 = new Thread(tt);
Thread t2 = new Thread(tt);
Thread t3 = new Thread(tt);
		
t1.start();
t2.start();
t3.start();

问题:      

  为什么上面没有static,也只销售50张票左右,而没有销售150张,本质其实只创建了一个对象,在被三个线程对象共享

7、继承Thread 和实现Runnable的区别

  1. 继承有局限,Java中类只能够单继承
  2. 实现的方式,我们的类在业务上可以继承它本应该有的类,同时可以实现接口变成一个线程类
  3. 关于数据共享的问题:就看所谓被共享的数据所在的类的对象被创建了几个

8、Thread类

8.1、线程休眠sleep

8.1.1、什么是线程休眠

线程类Thread当中有一个static void sleep(long millis)方法,在指定的毫秒数内让当前正在执行的线程休眠

System.out.println(1);
Thread.sleep(5000); // 休眠5秒钟之后才执行下一个语句		
System.out.println(2);

注意 : 当前正在执行的线程就是主线程

8.1.2、线程休眠应用

  1. 可以做倒计时:代码清单:
    // ①创建了一个顶层窗体的对象
    JFrame frame = new JFrame();
    Button button = new Button();
    button.setFont(new Font("宋体", 66, 80));
    button.setForeground(Color.red);
    frame.add(button);
    // ②设置顶层窗体的大小
    frame.setSize(1000, 500);
    // ③设置顶层窗体的位置居中
    frame.setLocationRelativeTo(null); // 居中
    // ④设置窗体可见
    frame.setVisible(true);
    for (int i = 600; i >=0; i--) {
    	button.setLabel("你生命剩下的时间:"+i+"");
    	Thread.sleep(1000);
    }
  2. 可以用来模拟网络延迟

9、线程同步[解决线程安全问题]

9.1、为什么需要线程同步

  1. 解决问题: 线程安全问题(例如1单例模式的懒汉模式;例如2多线程模拟多窗口售票-)

9.2、线程同步方式一:同步代码

9.2.1、基本语法结构

synchronized (同步监听对象) {

        可能引发线程安全问题的代码

}

上面的结构相当于把{ }中的代码捆绑成一个整体,线程只能够一个一个的进来,执行完一个,下一 个才能进来

9.2.2、语法特点:

  1. 上面的同步监听对象可以是任意的对象;
  2. 保证所有的线程共享一个同步监听对象的;也就是保证被同步监听对象是被所有线程共享的。
  3. 很多时候可以写this,但是请先参照②
  4. 常用的方式:使用类的字节码对象   XXX.class

9.2.3、示例演示:

  1. 同步代码块方式①同步整块代码
synchronized (Ticket.class) {
	while(num > 0){ // 循环判断,是否有就卖
		System.out.println(getName()+" 您的票号是:"+num);
		num--;
	}
}

结果:一个线程卖完

原因:把整个循环同步了,一旦有一个线程进来就会执行完里面的代码,其它线程才能进来

重新思考:我们需要真正的同步什么代码?  保证每销售一张票的操作是同步的就可以了

        2. 同步代码块方式② 同步关键业务代码

synchronized (TicketThread.class) {
	if(num>0){
		System.out.println(this.getName()+"您的票号是:"+num);
		num--;
	}
}

结果:有0   -1  感觉判断失效了

原因:假设仅剩下一张票,所同步内容没有判断是否还有票,当第二线程易进入while结构体,但其他线程正在执行销售最后一张票,之后num=0;但第二线程已经进入while结构体,会执行同步代码,销售num=0的这张票

        3. 同步代码块方式③同步关键业务代码

public class TicketThread extends Thread{
	private static int num = 50;
	public void run() {
		while(num>0){ // 只应该同步销售的一张票的操作代码
			synchronized (TicketThread.class) {
				// 下面的代码是销售一张票,每卖一张票的前提判断是否有票
				if(num>0){
					System.out.println(this.getName()+"您的票号是:"+num);
					num--;
				}
			}
		}
	}
}

或者:将关键业务代码提出来包装成一个方法

public class TicketThread extends Thread{
	private static int num = 50;
	public void run() {
		while(num>0){
			saleOne();
		}
	}
	private void saleOne(){ // 写一个方法:销售一张票
		// 只应该同步销售的一张票的操作代码
		synchronized (TicketThread.class) {
			// 下面的代码是销售一张票,每卖一张票的前提判断是否有票
			if(num>0){
				System.out.println(this.getName()+"   您的票号是:"+num);
				num--;
			}
		}
	}
}

9.3、线程同步方式二:同步方法

  1. 就是在需要被同步的方法上面加关键字  synchronized
  2. 加的位置 :在返回值类型的前面
  3. 不需要也不能够显示的写同步监听对象
  4. 如果是一个非static的方法,那么同步监听对象就是this;
  5. 如果是static修饰的方法,那么同步监听对象就是当前方法所在的类的字节码对象

售票示例同步方法代码清单:

public class TicketThread implements Runnable{
	private int num = 50;
	public void run() {
		while(num>0){
			saleOne();
		}
	}
	synchronized private void saleOne(){
		if(num>0){
			System.out.println("   您的票号是:"+num);
			num--;
		}
	}
}

9.4、线程同步方式三:锁机制

1. 学习方式(查找API文档方式)锁-- Lock(API)接口  ---XXX实现类

public class Ticket extends Thread {
	public Ticket(String name) {
		super(name);
	}
	static Lock lock = new ReentrantLock(); // 必须保证多个线程访问的是同一把锁
	static int num = 50;
	@Override
	public void run() {
		while(num > 0){ // 循环判断,是否有就卖
			lock.lock();
			try {
				if(num>0){ // 判断卖一张票的操作
					System.out.println(getName()+" 您的票号是:"+num);
					num--;
				}	
			} finally {
				lock.unlock();
			}	
		}
	}
}

2. 结果: 没有同步到

3. 原因:lock是一个实例变量,因此创建了3个TicketThread对象就有3个lock对象,没有同步到

4. 解决办法: static lock  ; 或者使用实现的方式

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值