黑马程序员---Java基础---多线程

时间:2023-02-20 10:31:33
-------  android培训 java培训 、期待与您交流! ----------

多线程
一、线程的概念 

进程:是一个正在执行中的程序,每一个进程都有一个执行顺序是,该顺序是一个执行路径或者叫一个控制单元。

线程:是进程中的一个独立的控制单元,线程在控制着进程的执行。一个进程中至少有一个线程。 

多线程的意义:宏观上使多个作业被同时执行,提高了效率。


二、线程的特性

其实在多线程状态下,某一时刻,cpu只能执行一个线程,所以只能有一个程序在运行(多核除外),cpu在多个线程

间做着快速切换动作,使多个线程看似在同时运行,也可以理解为多个线程在抢夺cpu的执行权,谁抢到,cpu就执行

谁,这是多线程的一个特性:随机性


三、线程的创建方式

1.方式1:继承Thread类

(1)步骤

1> 定义一个类继承Thread类

2> 复写Thread类的run()方法

3> 建立对象,也就是创建了一个线程。调用start()方法启动线程

(2)为什么要继承Thread类并复写run()方法呢?

在java中,Thread类用于描述线程。

该类定义了一个功能用于存储线程要运行的代码,该功能就是run方()法。

线程调用start()方法启动线程后,JVM会自动调用该线程的run()方法。

(3)线程名

线程有默认的名称,格式:“Thread-编号” 编号从0开始

我们可以通过setname方法或者构造函数给线程自定义名称

例子:

package test;

class MyThread extends Thread
{
	//覆盖run()方法
	public void run()
	{
		System.out.println(Thread.currentThread().getName() + "--mythread");
	}
}
public class Test9
{

	/**
	 * @param args
	 */
	public static void main(String[] args)
	{
		//创建两个线程
		MyThread mythread1 = new MyThread();
		MyThread mythread2 = new MyThread();
		
		//启动这两个线程
		mythread1.start();
		mythread2.start();
	}
}


2.方式2:实现Runnable类
(1)步骤

1> 定义一个类实现Runnable接口

2> 在类中实现Runnable接口中的run方法

3> 通过Thread类建立线程对象

4> 将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数

5> 调用Thread类的start()方法开启线程

例子:

package test;

class MyThread implements Runnable
{
	//覆盖run()方法
	public void run()
	{
		System.out.println(Thread.currentThread().getName() + "--mythread");
	}
}
public class Test9
{

	/**
	 * @param args
	 */
	public static void main(String[] args)
	{
		//创建一个Runnable子类对象
		MyThread mythread = new MyThread();
		
		//创建两个线程,并将Runnable子类对象传入
		Thread t1 = new Thread(mythread);
		Thread t2 = new Thread(mythread);
		
		//启动这两个线程
		t1.start();
		t2.start();
	}
}


3.两种创建线程的方式的区别

实现方式避免了单继承的局限性。

继承:线程执行的代码存放在Thread子类的run方法中。

实现:线程执行的代码存放在Runnable接口的子类的run方法中。

四、线程的状态

黑马程序员---Java基础---多线程

五、多线程的安全问题

1.安全问题的由来

由于线程的随机性,当多条语句在操作多个线程共享的数据时,一个线程对多条语句只执行了一部分,还没有执行

完,另一个线程参与进来执行,导致共享数据的错误。

如下面这个例子:有一个商品,如果生产者生产的是“basketball”,那么消费者得到的用处就是“play”,如果生产的是面包,消费者得到的用处就是吃掉,但是程序在运行过程中出现了“basketball”和 “吃的”这样的对应关系,显然是不对的。

例子:

package com.itheima;

//定义一个商品类
class Goods
{
	String name;
	String use;
}

//定义生产者类,用来生产商品
class Producer implements Runnable
{
	Goods good;
	Producer(Goods good)
	{
		this.good = good;
	}
	@Override
	public void run()
	{
		boolean flag = true;
		while(true)
		{
			if(flag)
			{
				//生产者生产一个篮球
				good.name = "basketball";
				good.use = "play";
				flag = false;
			}
			else 
			{
				//生产者生产一个面包
				good.name = "面包";
				good.use = "吃的";
				flag = true;
			}
		}
	}
}

//定义消费者类,用来消费商品
class Consumer implements Runnable
{
	Goods good;
	Consumer(Goods good)
	{
		this.good = good;
	}
	@Override
	public void run()
	{
		while(true)
		{
			//消费者消费商品
			System.out.println(good.name + "---" + good.use);
		}
	}
}
public class Test027
{

	/**
	 * 有一个商品,生产者生产出来什么东西,消费者就消费什么东西。
	 * @param args
	 */
	public static void main(String[] args)
	{
		//定义一个商品对象
		Goods good = new Goods();
		
		//将这个商品和生产者、消费者关联
		Producer producer = new Producer(good);
		Consumer consumer = new Consumer(good);
		
		//生产者开始生产,消费者开始消费
		new Thread(producer).start();
		new Thread(consumer).start();
	}

}
2.安全问题的分析

以上面的程序为例,我们分析一下问题产生的原因:生产者生产“basketball”---“play”,然后再生产“面包”,注意,当生产者在生产面包的过程中,执行了good.name = "面包";这句话后,由于线程的随机性,此刻如果cpu的执行权被消费者抢到,那么消费者消费的就是“面包”---“play”,因为生产者还没有执行good.use = "吃的";


六、多线程安全问题的解决

1.安全问题解决办法

针对多线程安全问题产生的原因,我们可以采取相应的解决办法:对多条操作共享数据的语句,只能让一个线程执行完,在执行过程中其他线程不可以参与进来,可以用synchronized同步代码块来解决。

2.synchronized同步代码块

(1)格式

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

对象如同一把锁,持有锁的线程可以在同步中执行。没有锁的线程即使获取cpu的执行权,也进不去,因为没有获取锁。

(2)同步的前提

1> 必须要有两个或者两个以上的线程

2> 必须是多个线程使用同一个锁

(3)同步代码块的利弊

1> 解决了多线程的安全问题

2> 在运行中需要判断锁状态,较为耗费资源

(4)怎么确定哪些代码应该放在同步代码块中??

1> 明确哪些代码是多线程运行的代码

2> 明确多线程的共享数据

3> 明确多线程运行代码中哪些是操作共享数据的

上面的程序我们加上同步代码块:

package com.itheima;

//定义一个商品类
class Goods
{
	String name;
	String use;
}

//定义生产者类,用来生产商品
class Producer implements Runnable
{
	Goods good;
	Producer(Goods good)
	{
		this.good = good;
	}
	@Override
	public void run()
	{
		boolean flag = true;
		while(true)
		{
			synchronized(good) //同步代码块,我们用good作为锁,在程序中它是唯一的对象
			{
				if(flag)
				{
					//生产者生产一个篮球
					good.name = "basketball";
					good.use = "play";
					flag = false;
				}
				else 
				{
					//生产者生产一个面包
					good.name = "面包";
					good.use = "吃的";
					flag = true;
				}
			}			
		}
	}
}

//定义消费者类,用来消费商品
class Consumer implements Runnable
{
	Goods good;
	Consumer(Goods good)
	{
		this.good = good;
	}
	@Override
	public void run()
	{
		while(true)
		{
			synchronized(good)    //同步代码块
			{
				//消费者消费商品
				System.out.println(good.name + "---" + good.use);
			}
			
		}
		
	}
}
public class Test028
{

	/**
	 * @param args
	 */
	public static void main(String[] args)
	{
		Goods good = new Goods1();
		Producer producer = new Producer(good);
		Consumer consumer = new Consumer(good);
		new Thread(producer).start();
		new Thread(consumer).start();
	}

}
3.同步函数

(1)格式
在函数上加上synchronized修饰符即可,如public void synchronized method(){};

(2)同步函数用的是哪一个锁呢? 

函数需要被对象调用,函数都有一个所属对象引用,就是this,所以同步函数用的锁是this

如果同步函数被static修饰,用的锁就不再是this,而是该方法所在的类的字节码文件对象,即类名.class

七、线程间通信

1.线程间通信:等待唤醒机制

需求:多条流水线生产手机,每部手机都有唯一的编号,多条流水线来销售手机,生产一部手机就销售一部手机,然

后再生产下一步手机,如生产“手机001”,销售“手机001”,再生产“002”,再销售“手机002”。这时用之前的

生产者消费者的程序就满足不了需求了,因为生产者生产篮球后,还可以拥有执行权,继续生产面包。在生产者生产

产品后,需要放弃执行权,让消费者消费,然后消费者放弃执行权,让生产者生产。

引入Object的wait()、notify()、notifyAll()三个方法

wait():放弃cpu执行权,放弃锁

notify():唤醒等待的线程

notifyAll():唤醒等待的所有线程

针对需求的代码:

package com.itheima;

class Phone
{
	private String name;
	private int number = 0;
	private boolean flag = false;
	public synchronized void setName(String name)
	{
		while(flag) //如果手机未卖出,生产者wait。这里用if,线程被唤醒后不会再判断flag,应该用while
			try
			{
				this.wait();
			} catch (InterruptedException e)
			{
				// TODO 自动生成的 catch 块
				e.printStackTrace();
			}
		this.name = name;
		System.out.println(Thread.currentThread().getName() + "生产--" + this.name + ++number);
		flag = true;      //生产手机后将标识符改变
		this.notifyAll();   //唤醒销售线程
//		this.notify(); //使用notify()最终会使所有线程处于冻结状态
	}
	public synchronized void sellPhone()
	{
		while(!flag) //如果手机未生产,销售wait。这里用if,线程被唤醒后不会再判断flag,应该用while
			try
			{
				this.wait();
			} catch (InterruptedException e)
			{
				// TODO 自动生成的 catch 块
				e.printStackTrace();
			}
		System.out.println(Thread.currentThread().getName() + "消费------" + this.name + number);
		flag = false;     //销售后修改标识符
		this.notifyAll();  //唤醒生产线程
//		this.notify();//使用notify()最终会使所有线程处于冻结状态
	}
}

//生产者
class PhoneProducer implements Runnable
{
	private Phone phone;
	PhoneProducer(Phone phone)
	{
		this.phone = phone;
	}
	@Override
	public void run()
	{	
		while(true)
			phone.setName("华为");
	}
}

//销售者
class PhoneConsumer implements Runnable
{
	private Phone phone;
	PhoneConsumer(Phone phone)
	{
		this.phone = phone;
	}
	@Override
	public void run()
	{
		while(true)
		phone.sellPhone();
	}
}
public class Test030
{

	/**
	 * @param args
	 */
	public static void main(String[] args)
	{
		Phone phone = new Phone();
		
		PhoneProducer pp = new PhoneProducer(phone);
		PhoneConsumer pc = new PhoneConsumer(phone);
		
		new Thread(pp).start();
		new Thread(pp).start();
		new Thread(pc).start();
		new Thread(pc).start();
	}

}

2. 线程间通讯

其实就是多个线程在操作同一个资源,但是各个线程操作的动作不同。

wait() notify() notifyAll()都使用在同步中,因为要对持有监视器(锁)的线程操作,同步中才有监视器(锁)。

3.为什么这些方法都定义在Object类中?

这些方法在操作同步中的线程时,需要标示线程所持有的锁,因为只有持有同一把锁的线程,才可以相互唤醒。

而锁可以是任意对象,所以这些方法定义在所有类的超类Object中。

4.对应多个生产者消费者,为什么要定义while判断标记?

原因是让被唤醒的线程再一次判断标记

5.对应多个生产者消费者,为什么使用notifyAll()?

因为需要唤醒对方线程,只用notify 容易出现只唤醒本方线程的情况,最后导致程序中的所有线程都冻结 

6. JDK1.5中提供了多线程升级解决方案

将同步synchronized替换成Lock操作

将Object中的wait notify notifyAll替换了Condition对象,该对象可以通过Lock获取。

实现了本方线程只唤醒对方线程。

例子:

package com.itheima;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class Phone1
{
	private String name;
	private int number = 0;
	private boolean flag = false;
	
	//定义一个锁
	Lock lock = new ReentrantLock();
	
	//定义两个Condition对象,一个锁可以对应多个Condition对象
	Condition pro = lock.newCondition();
	Condition con = lock.newCondition();
	
	public void setName(String name) throws InterruptedException
	{
		lock.lock(); //上锁
		try
		{
			while(flag) 
				pro.await(); //生产者等待
			this.name = name;
			System.out.println(Thread.currentThread().getName() + "生产--" + this.name + ++number);
			flag = true;
			con.signal(); //唤醒销售者
		}
		finally
		{
			//释放锁的代码一定要执行
			lock.unlock();
		}
		
	}
	public void sellPhone() throws InterruptedException
	{
		lock.lock();
		try
		{
			while(!flag) 
				con.await();  //销售者等待
			System.out.println(Thread.currentThread().getName() + "消费------" + this.name + number);
			flag = false;
			pro.signal();  //唤醒生产者
		}
		finally
		{
			//释放锁的代码一定要执行
			lock.unlock();  
		}
	}
}

class PhoneProducer1 implements Runnable
{
	private Phone1 phone;
	PhoneProducer1(Phone1 phone)
	{
		this.phone = phone;
	}
	@Override
	public void run()
	{	
		while(true)
			try
			{
				phone.setName("华为");
			} catch (InterruptedException e)
			{
				// TODO 自动生成的 catch 块
				e.printStackTrace();
			}
	}
}

class PhoneConsumer1 implements Runnable
{
	private Phone1 phone;
	PhoneConsumer1(Phone1 phone)
	{
		this.phone = phone;
	}
	@Override
	public void run()
	{
		while(true)
			try
			{
				phone.sellPhone();
			} catch (InterruptedException e)
			{
				// TODO 自动生成的 catch 块
				e.printStackTrace();
			}
	}
}
public class Test031
{

	/**
	 * @param args
	 */
	public static void main(String[] args)
	{
		Phone1 phone = new Phone1();
		
		PhoneProducer1 pp = new PhoneProducer1(phone);
		PhoneConsumer1 pc = new PhoneConsumer1(phone);
		
		new Thread(pp).start();
		new Thread(pp).start();
		new Thread(pc).start();
		new Thread(pc).start();
	}

}


八、线程的其他方法

1.停止线程

(1)由于stop()方法已过时,停止线程的方式只有一种,run()方法结束。

开启多线程运行,运行代码通常是循环结构,只要控制住循环,就可以让run方法结束,也就是线程结束。

例子:

package com.itheima;

class MyThread1 implements Runnable
{
	//定义标识符
	private boolean flag = true;
	@Override
	public void run()
	{
		int num = 1;
		while(flag)
		{
			System.out.println(Thread.currentThread().getName() + "...." + num++);
			if(num == 5)
				flag=false; //修改标识符,控制循环
		}
	}
}
public class Test032
{

	/**
	 * @param args
	 */
	public static void main(String[] args)
	{
		MyThread1 mythread = new MyThread1();
		new Thread(mythread).start();
		new Thread(mythread).start();
		
		for(int x = 0; x <=60; x++)
		{
			System.out.println(Thread.currentThread().getName() + "..." + x);
		}
	}

}
(2)特殊情况

当线程处于冻结状态,就不会读取到标示符,线程就不会结束。

这时需要对线程的冻结状态进行清除,强制让线程恢复到运行状态中,需要用到Thread类提供的interrupt方法

例子:

package com.itheima;

class MyThread2 implements Runnable
{
	//定义标识符
	private boolean flag = true;
	@Override
	public synchronized void run()
	{
		int num = 1;
		while(flag)
		{
			try
			{
				wait(); //线程等待
			} catch (InterruptedException e)
			{
				// TODO 自动生成的 catch 块
				e.printStackTrace();
				flag = false; //修改标识符,控制循环
			}
			System.out.println(Thread.currentThread().getName() + "...." + num++);
			if(num == 5)
				flag = false; //修改标识符,控制循环
		}
	}
}
public class Test033
{

	/**
	 * @param args
	 */
	public static void main(String[] args)
	{
		MyThread2 mythread = new MyThread2();
		Thread t1 = new Thread(mythread);
		Thread t2 = new Thread(mythread);
		
		t1.start();
		t2.start();
		
		for(int x = 0; x <=60; x++)
		{
			System.out.println(Thread.currentThread().getName() + "..." + x);
			if(x == 60)
			{
				//中断线程的冻结状态
				t1.interrupt();
				t2.interrupt();
			}
		}
		System.out.println("over");
	}

}
2.join()方法

当A线程执行到了B线程的join方法时,A线程就会等B线程执行完后再执行。

join可以用来临时加入线程执行。

3.yield()方法

暂停当前正在执行的线程对象,并执行其他线程。