八、Java多线程(1)

1 线程概念

1.1 进程与线程

进程:是一个正在执行中的程序。每一个进程执行都有一个执行顺序。该顺序是一个执行路径,或者叫一个控制单元。
线程:就是进程中的一个独立的执行单元。线程在控制着进程的执行。一个进程中至少有一个线程。
单线程与多线程
单线程程序:程序只有一条执行路径
多线程程序:程序有多条执行路径

e.g
Java VM 启动的时候会有一个进程java.exe.
该进程中至少一个线程负责java程序的执行。而且这个线程运行的代码存在于main方法中。该线程称之为主线程。
扩展:其实更细节说明jvm,jvm启动不止一个线程,主线程在运行的同时,还有负责垃圾回收机制的线程。
JVM启动是多线程的,最低有两个线程启动了,包括主线程和垃圾回收线程

为什么使用多线程:同时运行多份代码,提高程序运行效率

1.2 创建线程(一)

通过对api的查找,java已经提供了对线程这类事物的描述。就Thread类。
1、创建线程的第一种方式:继承Thread类。
步骤:

  1. 定义类继承Thread。
  2. 复写Thread类中的run方法。
    目的:将自定义代码存储在run方法。让线程运行。
  3. 调用线程的start方法,
    该方法两个作用:启动线程,调用run方法。

2、运行情况:发现运行结果每一次都不同。

  1. 因为多个线程都获取cpu的执行权。cpu执行到谁,谁就运行。
    明确一点,在某一个时刻,只能有一个程序在运行。(多核除外)
  2. cpu在做着快速的切换,以达到看上去是同时运行的效果。

我们可以形象把多线程的运行行为在互相抢夺cpu的执行权。
这就是多线程的一个特性:随机性。谁抢到谁执行,至于执行多长,cpu说的算。

3、为什么要覆盖run方法
Thread类用于描述线程。
该类就定义了一个功能,用于存储线程要运行的代码。该存储功能就是run方法。
也就是说Thread类中的run方法,用于存储线程要运行的代码。

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

class ThreadDemo 
{
	public static void main(String[] args) 
	{
		//for(int x=0; x<4000; x++)
		//System.out.println("Hello World!");

		Demo d = new Demo();//创建好一个线程。
		//d.start();//开启线程并执行该线程的run方法。
		d.run();//仅仅是对象调用方法。而线程创建了,并没有运行。

		for(int x=0; x<60; x++)
			System.out.println("Hello World!--"+x);
	
	}
}

1.3 线程的五种状态

在这里插入图片描述

说明:

  1. 创建线程
    使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。
  2. 就绪状态
    当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度
  3. 运行状态
    如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
  4. 阻塞状态
    如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:
    等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态,需要被动 notify()唤醒才可以到运行状态。
    同步阻塞:线程在获取 synchronized同步锁失败(因为同步锁被其他线程占用)。
    其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。sleep()时间结束后自动进入运行状态。

1.4 获取线程名称

当我们创建多线程时,想要区分哪个线程,可以获取线程的名称
原来线程都有自己默认的名称。Thread-编号 该编号从0开始。
static Thread currentThread():获取当前线程对象。
getName(): 获取线程名称。

设置线程名称:setName或者构造函数。

	Test(String name)
	{
		super(name);
	}
	public void run()
	{
		for(int x=0; x<60; x++)
		{
			System.out.println((Thread.currentThread()==this)+"..."+this.getName()+" run..."+x);
		}
	}

1.5 创建线程(二)

创建线程的第二种方式:实现Runable接口
步骤

  1. 定义类实现Runnable接口
  2. 覆盖Runnable接口中的run方法。将线程要运行的代码存放在该run方法中。
  3. 通过Thread类建立线程对象。
  4. 将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数。
  5. 调用Thread类的start方法开启线程并调用Runnable接口子类的run方法。

两种创建线程的实现方式和继承方式有什么区别呢
实现方式好处:避免了单继承的局限性,不然就只能继承thread类了
在定义线程时,建立使用实现方式
两种方式区别:
继承Thread:线程代码存放Thread子类run方法中。
实现Runnable:线程代码存在接口的子类的run方法。

/*买票小程序*/
class Ticket implements Runnable//extends Thread
{
	private  int tick = 100;
	public void run()
	{
		while(true)
		{
			if(tick>0)
			{
				System.out.println(Thread.currentThread().getName()+"....sale : "+ tick--);
			}
		}
	}
}

class  TicketDemo{
	public static void main(String[] args) 
	{
		Ticket t = new Ticket();
		//共享一个ticket资源
		Thread t1 = new Thread(t);//创建了一个线程;
		Thread t2 = new Thread(t);//创建了一个线程;
		t1.start();
		t2.start();
	}
}

两种创建线程方式的总结
在这里插入图片描述

2 多线程安全问题

在订票小程序中通过分析发现,打印出0,-1,-2等错票。
多线程的运行出现了安全问题。

2.1 问题的原因

当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误。
总结:
线程安全问题

  1. 是否是多线程环境
  2. 是否有共享数据
  3. 是否有多条语句操作共享数据

2.2 解决办法

原则:对多条操作共享数据的语句,只能让一个线程都执行完。在执行过程中,其他线程不可以参与执行。

Java对于多线程的安全问题提供了专业的解决方式。
就是同步代码块

synchronized(对象)
{
	需要被同步的代码
}

对象如同锁。持有锁的线程可以在同步中执行。
没有持有锁的线程即使获取cpu的执行权,也进不去,因为没有获取锁。
同步类比:火车上的卫生间—经典(厕所有人加锁无人开锁)。

同步的前提:必须保证同步中只能有一个线程在运行。

  1. 必须要有两个或者两个以上的线程。
  2. 必须是多个线程使用同一个锁。

好处:解决了多线程的安全问题。
弊端:当线程很多时,每个线程都要去判断锁,较为消耗资源,

class Ticket implements Runnable
{
	private  int tick = 1000;
	Object obj = new Object();
	public void run()
	{
		while(true)
		{
			synchronized(obj)
			{
				if(tick>0)
				{
					//try{Thread.sleep(10);}catch(Exception e){}
					System.out.println(Thread.currentThread().getName()+"....sale : "+ tick--);
				}
			}
		}
	}
}

class  TicketDemo2{
	public static void main(String[] args) 
	{
		Ticket t = new Ticket();
		Thread t1 = new Thread(t);
		Thread t2 = new Thread(t);
		Thread t3 = new Thread(t);
		t1.start();
		t2.start();
		t3.start();
	}
}

2.3 同步的两种方式

另外同步有两种方式,同步代码块和同步函数
1、同步函数用的是哪一个锁呢?
函数需要被对象调用。那么函数都有一个所属对象引用。就是this。
所以同步函数使用的锁是this。
同步代码块的锁是程序员指定的对象

class Bank
{
	private int sum;
	//Object obj = new Object();
	public synchronized void add(int n)
	{
		//synchronized(obj)
		//{
			sum = sum + n;
			try{Thread.sleep(10);}catch(Exception e){}
			System.out.println("sum="+sum);
		//}
	}
}

2、如果同步函数被静态static修饰后,使用的锁是什么呢?
通过验证,发现不是this。因为静态方法中也不可以定义this。静态进内存是,内存中没有本类对象,但是一定有该类对应的字节码文件对象。类名.class 该对象的类型是Class
3、静态的同步方法,使用的锁是该方法所在类的字节码文件对象。 类名.class

synchronized(Ticket.class)
{
	if(tick>0)
	{
		try{Thread.sleep(10);}catch(Exception e){}
		System.out.println(Thread.currentThread().getName()+"....code : "+ tick--);
	}
}

如何确定同步问题:该程序是否有安全问题,如果有,如何解决?
如何找问题:

  1. 明确哪些代码是多线程运行代码。
  2. 明确共享数据。(一般成员变量会涉及到共享数据的问题)
  3. 明确多线程运行代码中哪些语句是操作共享数据的。

设计模式中关于同步的面试例子
懒汉式的特点在于实例的延迟加载,懒汉式会有缺点,在多线程访问时,会出现安全问题。通过加同步代码块来解决,但是会有效率问题,可以用双重判断的方式,加的锁是该类的字节码文件对象。

class Single
{
	private static Single s = null;
	private Single(){}

	public static  Single getInstance()
	{
		if(s==null)
		{
			synchronized(Single.class)
			{
				if(s==null)
					//--->A;
					s = new Single();
			}
		}
		return s;
	}
}

2.4 线程安全类回顾

StringBuffer、Vector、HashTable
线程安全,但是效率比较低的
在这里插入图片描述

2.5 JDK5以后提供Lock接口

虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock。

Lock:
		void lock(): 获取锁。
 		void unlock():释放锁。  
 ReentrantLock是Lock的实现类.

2.6 死锁情况

同步弊端:效率低,同步中嵌套同步会产生死锁问题
死锁
两个或两个以上的线程在争夺资源的过程中,发生的一种相互等待的现象。

public void run() {
	if (flag) {
		synchronized (MyLock.objA) {
			System.out.println("if objA");
			synchronized (MyLock.objB) {//A线程拿了A锁想要B锁
				System.out.println("if objB");
			}
		}
	} else {
		synchronized (MyLock.objB) {
			System.out.println("else objB");
			synchronized (MyLock.objA) {//B线程拿了B锁想要A锁,形成互相等待
				System.out.println("else objA");
			}
		}
	}
}

3 线程间通信

线程间通信问题:不同种类的线程针对同一个资源的操作
多个线程操作同一个资源,做不同的操作

3.1 生产者与消费者

一些例子:电影院买票,只出不进;但是商家卖东西是有卖出,有进货

Java提供的等待唤醒机制
在这里插入图片描述

wait():
notify();
notifyAll();
方法与锁相关,必须通过锁对象调用

为什么这些方法不定义在Thread类中呢?
这些方法的调用必须通过锁对象调用,而我们刚才使用的锁对象是任意对象,可以被任意对象调用的方法定义Object类中。
所以,这些方法必须定义在Object类中。

3.2 匿名内部类创建多线程

匿名内部类的格式:
new 类名或者接口名() {
重写方法;
};
本质:是该类或者接口的子类对象。
e.g

// 继承Thread类来实现多线程
new Thread() {
	public void run() {
		for (int x = 0; x < 100; x++) {
			System.out.println(Thread.currentThread().getName() + ":"
					+ x);
		}
	}
}.start();

// 实现Runnable接口来实现多线程
new Thread(new Runnable() {
	@Override
	public void run() {
		for (int x = 0; x < 100; x++) {
			System.out.println(Thread.currentThread().getName() + ":"
					+ x);
		}
	}
}) {
}.start();

4 线程池

线程池的好处:线程池里的每一个线程代码结束后,并不会死亡,而是再次回到线程池中成为空闲状态,等待下一个对象来使用
如何实现线程的代码呢?

  1. 创建一个线程池对象,控制要创建几个线程对象。
    public static ExecutorService newFixedThreadPool(int nThreads)
    
  2. 这种线程池的线程可以执行:
    可以执行Runnable对象或者Callable对象代表的线程
    做一个类实现Runnable接口。
  3. 调用如下方法即可
    Future<?> submit(Runnable task)
    <T> Future<T> submit(Callable<T> task)
    
  4. 我就要结束,可以吗?
    可以。
    e.g:
// 创建一个线程池对象,控制要创建几个线程对象。
// public static ExecutorService newFixedThreadPool(int nThreads)
ExecutorService pool = Executors.newFixedThreadPool(2);

// 可以执行Runnable对象或者Callable对象代表的线程
pool.submit(new MyRunnable());
pool.submit(new MyRunnable());

//结束线程池
pool.shutdown();

5 任务定时器

定时器:可以让我们在指定的时间做某件事情,还可以重复的做某件事情。
依赖Timer和TimerTask这两个类:
Timer:定时

public Timer()
public void schedule(TimerTask task,long delay)
public void schedule(TimerTask task,long delay,long period)
public void cancel()

TimerTask:任务

6 回顾

  1. 什么是多线程
  2. 有几种方式实现多线程,用代码实现
  3. 如何获取和设置线程的名称
  4. 线程的常见方法
  5. 线程的生命周期图
  6. 线程安全产生的原因
  7. 如何解决线程安全的问题

1:多线程有几种实现方案,分别是哪几种?
两种。
继承Thread类
实现Runnable接口
扩展一种:实现Callable接口。这个得和线程池结合。

2:同步有几种方式,分别是什么?
两种。
同步代码块
同步方法
3:启动一个线程是run()还是start()?它们的区别?
start();
run():封装了被线程执行的代码,直接调用仅仅是普通方法的调用
start():启动线程,并由JVM自动调用run()方法

4:sleep()和wait()方法的区别
sleep():必须指时间;不释放锁。
wait():可以不指定时间,也可以指定时间;释放锁。

5:为什么wait(),notify(),notifyAll()等方法都定义在Object类中
因为这些方法的调用是依赖于锁对象的,而同步代码块的锁对象是任意锁。
而Object代码任意的对象,所以,定义在这里面。

6:线程的生命周期图
新建 – 就绪 – 运行 – 死亡
新建 – 就绪 – 运行 – 阻塞 – 就绪 – 运行 – 死亡
建议:画图解释。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值