Java基础_多线程

概述

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

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

一个进程中至少有一个线程。

Java VM 启动的时候会有一个进程java.exe

该进程中至少有一个线程负责java程序的执行,而且这个线程运行的代码存在于main方法中,该线程称之为主线程。

扩展:其实更细节说明jvm,jvm启动不止一个线程,还有负责垃圾回收机制的线程。

创建线程

如何在自定义的代码中自定义一个线程呢?

通过查找API文档,Java已经提供了对线程这类事物的描述,就是Thread类。

创建线程的第一种方式:继承Thread类。

步骤:

  1. 定义类继承Thread
  2. 覆写Thread类中的run方法。目的:将自定义的代码存储在run方法中,让线程运行。
  3. 调用线程的start方法,该方法有两个作用:启动线程和调用run方法。如果只调用run方法,那么线程虽然创建了,但是并没有运行,只有main线程执行run方法中的内容。
发现运行结果每次都不同,因为多个线程都获取CPU的执行权。cpu执行到谁,谁就执行。
明确一点,在某一时刻,只能有一个程序在运行(多核除外)
cpu在做着快速的切换,以达到看上去是同时运行的效果。
我们可以形象地把多线程的运行形容为在互相抢夺cpu的执行权。
这就是多线程的一个特性:随机性。谁抢到,谁执行。至于执行多长时间,cpu说了算。

为什么要覆盖run方法?
Thread类用于描述线程。
该类就定义了一个功能,用于存储线程要运行的代码,该存储功能就是run方法
也就是说Thread类中的run方法是用于存储线程要运行的代码。
/*
小练习
创建两线程,和主线程交替运行。
*/

//创建线程Test
class Test extends Thread
{
//	private String name;
	Test(String name)//通过构造函数参数指定线程名称
	{
		super(name);
//		this.name=name;
	}
	//复写run方法
	public void run()
	{
		for(int x=0;x<60;x++)
		     System.out.println(Thread.currentThread().getName()+"..run..."+x);
//		System.out.println(this.getName()+"..run..."+x);
	}
}

class  ThreadTest
{
	public static void main(String[] args) 
	{
		new Test("one+++").start();//开启一个线程
		new Test("tow———").start();//开启第二线程
		//主线程执行的代码,一定要放在创建线程语句后面,否则会先执行主线程
		for(int x=0;x<170;x++)
		System.out.println("Hello World!");
	}
}

创建线程的第二种方式:实现Runnable接口

步骤:
  1. 定义类实现Runnable接口
  2. 覆盖Runnable接口中的run方法。将线程要运行的代码存放在该run方法中
  3. 通过Thread类建立线程对象
  4. 将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数。因为自定义的run方法所属的对象是Runnable的子类对象,所以要让线程去指定对象的run方法,就必须明确该run方法所属的对象
  5. 调用Thread类的start方法开启线程,此时start方法会自动调用Runnable接口子类的run方法
/*
需求:简单的卖票程序。
多个窗口卖票。
*/
class Ticket implements Runnable
{
	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) 
	{
		//创建Runnable接口子类的实例对象
		Ticket t = new Ticket();

		//有多个窗口在同时卖票,这里用四个线程表示
		Thread t1 = new Thread(t);//创建了一个线程
		Thread t2 = new Thread(t);
		Thread t3 = new Thread(t);
		Thread t4 = new Thread(t);

		t1.start();//启动线程
		t2.start();
		t3.start();
		t4.start();
	}
}

实现方式和继承方式的区别

实现方式好处:避免了单继承的局限性
在定义线程时,建议使用实现方式
两种方式的区别:
继承Thread:线程代码存放在Thread子类run方法中
实现Runnable:线程代码存放在接口的子类的run方法中

线程运行状态


状态说明:
  • 被创建:等待启动,调用start启动。
  • 运行状态:具有执行资格和执行权。
  • 临时状态(阻塞):有执行资格,但是没有执行权。
  • 冻结状态:遇到sleep(time)方法和wait( )方法时发生。
    • wait方法释放锁,失去执行资格和执行权,调用notify方法时,获得执行资格,变为临时阻塞状态。
    • sleep方法不释放锁,只是让线程休息,所以具备执行资格,但是不具备执行权,此时也处于临时阻塞状态,sleep方法时间到,线程执行,具备执行权。
  • 消忙状态:stop( )方法,或者run方法结束。
注:当已经从创建状态到了运行状态,再次调用start( )方法时,就失去意义了,java运行时会提示线程状态异常。

获取线程名称以及对象

原来线程都有自己默认的名称:Thread-编号 该编号从0开始,通过getName();获取。

Thread类中的方法

static Thread currentThread();获取当前线程对象,和this一样,但更通用
getName():获取线程名称
设置线程名称:setName或者构造函数
注意:局部的变量在每一个线程栈内存区域中都有一份

多线程的安全问题

原因:当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行,导致共享数据的错误。
解决办法:对多条操作共享数据的语句,只能让一个线程都执行完。在执行过程中,其他线程不可以参与执行

同步代码块

Java对于多线程的安全问题,提供了专业的解决方式,就是同步代码块
synchronized(对象)
{
          需要被同步的代码//那些语句在操作共享数据,就进行同步
}
对象如同锁,持有锁的线程可以在同步中执行,没有持有锁的线程,即使获取了cpu的执行权也进不去。
/*	
给卖票程序示例加上同步代码块。
*/
class Ticket implements Runnable
{
	private int tick=100;
	Object obj = new Object();
	public void run()
	{
		while(true)
		{
			//给程序加同步,即锁
			synchronized(obj)
			{
				if(tick>0)
				{
					try
					{	
						//使用线程中的sleep方法,模拟线程出现的安全问题
						//因为sleep方法有异常声明,所以这里要对其进行处理
						Thread.sleep(10);
					}
					catch (Exception e)
					{
					}
					//显示线程名及余票数
					System.out.println(Thread.currentThread().getName()+"..sale="+tick--);
				}
			}	
		}
	}
}
同步的前提:
  1. 必须要有两个或者两个以上的线程
  2. 必须是多个线程使用同一个锁,比如输入输出都要进行同步,使用同一把锁(比如类的字符码文件对象,保证唯一性)
必须保证同步中只能有一个线程在运行
好处:解决了多线程的安全问题
弊端:多个线程都需要判断锁,较为消耗资源
如何找多线程安全问题并进行同步:
  1. 明确哪些代码是多线程运行代码
  2. 明确共享数据
  3. 明确多线程运行代码中哪些语句是操作共享数据的(共享数据不能被独立语句操作的就会出现安全问题)

同步函数

函数和同步代码块都是封装,两者结合就是同步函数
把synchronized关键字作为修饰符放在函数上就实现了同步函数,无需对象锁
将需要同步的代码单独封装为一个函数,并加上synchronized关键字就实现了同步函数
函数需要被对象调用,那么函数都有一个所属对象的引用就是this,所以同步函数使用的锁是this
class Ticket implements Runnable
{
	private int tick=100;
	Object obj = new Object();
	public void run()
	{
		while(true)
		{
			show();
		}
	}
  //直接在函数上用synchronized修饰即可实现同步
public synchronized void show()
{
		if(tick>0)
	    {
		try
		{	
			//使用线程中的sleep方法,模拟线程出现的安全问题
			//因为sleep方法有异常声明,所以这里要对其进行处理
			Thread.sleep(10);
		}
		catch (Exception e)
		{
		}
		//显示线程名及余票数
		System.out.println(Thread.currentThread().getName()+"..tick="+tick--);
	}
}	
}

静态同步函数的锁

同步函数被静态static修饰后,使用的锁不是this,因为静态方法中不可以定义this。静态进内存时,内存中没有本类对象,但是一定有该类对应的字节码文件对象:类名.class,该对象的类型是Class
静态的同步方法,使用的锁是该方法所在类的字节码文件对象,也就是类名.class
参阅单例设计模式-懒汉式
class Single
{
	private static Single s = null;
	private Single(){}
	public static Single getInstance()
	{
		if(s==null)
		{
			synchronized(Single.class)
			{
				if(s==null)
					s = new Single();
			}
		}
		return s;
	}
}//同步代码块加入判断的原因:如果第一个线程进入到第一个if代码块,此时并还没获取锁,此时虚拟机切换给第二个线程,它也进来了if代码块,并且此时获取锁,new一个对象,然后返回。接着虚拟机切换给第一个线程,它这时候才获取锁,此时如果没有第二个if判断,就会再new 一个对象,这就不能保证类的对象唯一性。

死锁

同步中嵌套同步,两个同步锁不相同,两个线程都持有锁但是不释放,程序就会发生死锁
/*
一个死锁程序
 */
//定义一个类来实现Runnable,并复写run方法
class LockTest implements Runnable
{
	private boolean flag;
	LockTest(boolean flag)
	{
		this.flag=flag;
	}
	public void run()
	{
		if(flag)
		{
			while(true)
			{
				synchronized(LockClass.locka)//a锁
				{
					System.out.println(Thread.currentThread().getName()+"------if_locka");

					synchronized(LockClass.lockb)//b锁
					{
						System.out.println(Thread.currentThread().getName()+"------if_lockb");
					}
				}
			}
		}
		else
		{
			while(true)
			{
				synchronized(LockClass.lockb)//b锁
				{
					System.out.println(Thread.currentThread().getName()+"------else_lockb");

					synchronized(LockClass.locka)//a锁
					{
						System.out.println(Thread.currentThread().getName()+"------else_locka");
					}
				}
			}
		}
	}
}

//定义两个锁
class LockClass
{
	static Object locka = new Object();
	static Object lockb = new Object();
}

class DeadLock
{
	public static void main(String[] args)
	{
		//创建2个线程,并启动
		new Thread(new LockTest(true)).start();
		new Thread(new LockTest(false)).start();
	}
}

运行结果是两个线程都会停在这里等待对方释放锁


线程间的通信

线程间的通讯其实就是多个线程在操作同一个资源,但是操作的动作不同。
为了保证该资源的唯一性,可以考虑单力设计模式(麻烦),或者在每个线程类中通过构造函数传入资源的实例引用,例:
class Res {
	String name;
	String sex;
}

class Input implements Runnable{
	private Res r;
	Input(Res r){
		this.r = r;
	}
	public void run(){
		int flag = 0;
		while(true){
			if(flag==0){
				r.name = "张三";
				r.sex = "男";
			}else{
				r.name = "李四";
				r.sex = "女";
			}
			flag = (flag+1)%2;//交替打印
		}
	}
}

class Output implements Runnable{
	private Res r;
	Output(Res r){
		this.r = r;
	}
	public void run(){
		while(true){
			System.out.println(r.name+"..."+r.sex);
		}
	}
}

public class InputOutputDemo {
	public static void main(String[] args) {
		Res r = new Res();
		
		Input in = new Input(r);
		Output out = new Output(r);
		
		Thread t1 = new Thread(in);
		Thread t2 = new Thread(out);
		
		t1.start();
		t2.start();
	}
}
运行结果:

这出现了安全问题,需加入同步,输入和输出都需要加,注意保证线程都用同一把锁进行同步
class Input implements Runnable{
	private Res r;
	Input(Res r){
		this.r = r;
	}
	public void run(){
		int flag = 0;
		while(true){
			synchronized(r){
				if(flag==0){
					r.name = "张三";
					r.sex = "男";
				}else{
					r.name = "李四";
					r.sex = "女";
				}
			}
			flag = (flag+1)%2;//交替打印
		}
	}
}

class Output implements Runnable{
	private Res r;
	Output(Res r){
		this.r = r;
	}
	public void run(){
		while(true){
			synchronized(r){
				System.out.println(r.name+"..."+r.sex);
			}
		}
	}
}

等待唤醒机制

多个线程操作共享数据时,保证数据交替被线程操作(比如输入一个数据,就输出一个数据的情况),则使用等待唤醒机制
wait();//该方法让线程进入线程池等待
notify();//该方法唤醒线程池中第一个等待的共有同一把锁的线程,该机制会导致出现生产者消费者问题(多个生产者和消费者的时候)
notifyAll();//该方法唤醒所有共有同一把锁的等待线程
都使用在同步代码中,因为要对持有监视器(锁)的线程操作,所以要使用在同步中,因为只有同步才具有锁。
为什么这些操作线程的方法要定义在Object类中?
因为这些方法在操作同步中的线程时,都必须要标示他们所操作线程持有的锁,只有同一个锁上的被等待线程可以被同一个notify唤醒,不可以对不同锁中的线程进行唤醒。也就是说等待和唤醒必须是同一个锁。而锁可以是任意对象,所以可以被任意对象调用的方法定义在Object类中。
关键代码:
class Res
{
	String name;
	String sex;
	boolean flag = false;//设置标记,判断资源状态,默认为假,表示资源为空。
}

class Input implements Runnable
{
	private Res r ;
	Input(Res r)
	{
		this.r = r;
	}
	public void run()
	{
		int x = 0;
		while(true)
		{
			synchronized(r)
			{

				if(r.flag)
					try{r.wait();}catch(Exception e){}//如果flag为真,表示资源存在,则使输入线程等待,r.wait()表示等待持有r这个锁的线程
				if(x==0)
				{
					r.name="张三";
					r.sex="男";
				}
				else
				{
					r.name="李四";
					r.sex = "女";
				}
				x = (x+1)%2;
				r.flag = true;//改变资源状态的标记
				r.notify();//唤醒共用该锁的第一个其他线程
			}
		}
	}
}

class Output implements Runnable
{
	private Res r ;
	
	Output(Res r)
	{
		this.r = r;
	}
	public void run()
	{
		while(true)
		{
			synchronized(r)
			{
				if(!r.flag)
					try{r.wait();}catch(Exception e){}
				System.out.println(r.name+"...."+r.sex);
				r.flag = false;
				r.notify();
			}
		}
	}
}

class  InputOutputDemo
{
	public static void main(String[] args) 
	{
		Res r = new Res();

		Input in = new Input(r);
		Output out = new Output(r);

		Thread t1 = new Thread(in);
		Thread t2 = new Thread(out);

		t1.start();
		t2.start();
	}
}



或者将代码优化为(使用同步函数)
class Res
{
	private String name;
	private String sex;
	private boolean flag = false;

	public synchronized void set(String name,String sex)
	{
		if(flag)
			try{this.wait();}catch(Exception e){}
		this.name = name;
		
		this.sex = sex;
		flag = true;
		this.notify();
	}
	public synchronized void out()
	{
		if(!flag)
			try{this.wait();}catch(Exception e){}
		System.out.println(name+"........"+sex);
		flag = false;
		this.notify();
	}
}

class Input implements Runnable
{
	private Res r ;
	Input(Res r)
	{
		this.r = r;
	}
	public void run()
	{
		int x = 0;
		while(true)
		{
			if(x==0)				
				r.set("张三","男");				
			else	
				r.set("李四","女");				
			x = (x+1)%2;
		}
	}
}

class Output implements Runnable
{
	private Res r ;
	
	Output(Res r)
	{
		this.r = r;
	}
	public void run()
	{
		while(true)
		{
			r.out();
		}
	}
}

class  InputOutputDemo2
{
	public static void main(String[] args) 
	{
		Res r = new Res();

		new Thread(new Input(r)).start();
		new Thread(new Output(r)).start();
	}
}

生产者消费者

问题

if(flag)
        try{this.wait();}catch(Exception e){}//当有多个生产者和消费者时,线程池中的线程在被唤醒(这里的try和catch块)后不会再次判断标记flag,会直接执行下面的语句,出现问题,比如当唤醒的是本方线程的时候,会出现生产两个商品,只消费一个或者生产一个商品,消费两个的情况
this.name = name+"--"+count++;
System.out.println(Thread.currentThread().getName()+"...生产者.."+this.name);
flag = true;
this.notify();

解决方法

class ProducerConsumerDemo //产生多个生产者消费者
{
	public static void main(String[] args) 
	{
		Resource r = new Resource();

		Producer pro = new Producer(r);
		Consumer con = new Consumer(r);

		Thread t1 = new Thread(pro);
		Thread t2 = new Thread(pro);
		Thread t3 = new Thread(con);
		Thread t4 = new Thread(con);

		t1.start();
		t2.start();
		t3.start();
		t4.start();

	}
}

class Resource
{
	private String name;
	private int count = 1;
	private boolean flag = false;
	public synchronized void set(String name)
	{
		while(flag)//将if改为while,进行循环判断
			try{this.wait();}catch(Exception e){}//新的线程会在此句醒来,接着进行while语句判断
		this.name = name+"--"+count++;
		System.out.println(Thread.currentThread().getName()+"...生产者.."+this.name);
		flag = true;
		this.notifyAll();//如果只用notify()有可能唤醒本方线程,导致线程全部等待,所以要使用notifyAll()方法唤醒全部线程
	}
 
	public synchronized void out()
	{
		while(!flag)
			try{wait();}catch(Exception e){}
		System.out.println(Thread.currentThread().getName()+"...消费者........."+this.name);
		flag = false;
		this.notifyAll();
	}
}

class Producer implements Runnable
{
	private Resource res;

	Producer(Resource res)
	{
		this.res = res;
	}
	public void run()
	{
		while(true)
		{
			res.set("+商品+");
		}
	}
}

class Consumer implements Runnable
{
	private Resource res;

	Consumer(Resource res)
	{
		this.res = res;
	}
	public void run()
	{
		while(true)
		{
			res.out();
		}
	}
}
对于多个生产者和消费者。
为什么要定义while判断标记。
原因:让被唤醒的线程再一次判断标记flag。如果不判断标记(即不判断商品是否存在),会直接再次生产或者消费一个商品,出现生产者消费者问题
为甚么要定义notifyAll?
因为需要唤醒对方(生产者和消费者双方)线程。如果只用notify,容易出现只唤醒本方线程的情况。导致程序中的所有线程都等待。
wait方法和sleep方法的区别
wait方法释放锁,持有锁的线程才具备执行资格,但不一定具备执行权,因为cpu切换到哪个线程并执行(sleep方法没有执行线程),哪个线程才具备执行权。obj.wait会使线程进入obj对象的等待集合中并等待唤醒。
sleep并不释放锁,只是让线程休息,此时该线程处于临时阻塞状态,具备执行资格,但是不具备执行权。

Lock接口和Condition接口

JDK1.5 中提供了多线程升级解决方案,显示的锁机制和锁对象上的等待唤醒操作机制

将同步Synchronized替换成现实Lock操作。
将Object中的wait,notify notifyAll,替换为Condition对象。
该对象可以通过Lock锁进行获取,一个锁上可以有多个相关的Condition对象,从而有多个等待唤醒机制,所以可以实现本方只唤醒对方而没有唤醒本方线程的操作。
Lock:替代了Synchronized
创建锁对象:Lock lock = new ReentrantLock();
该锁对象具有的方法
lock 
unlock
newCondition()
Condition:替代了Object wait notify notifyAll
await();
signal();
signalAll();
示例代码:
class Resource
{
	private Lock lock = new ReentrantLock();
	private Condition condition_pro = lock.newCondition();
	private Condition condition_con = lock.newCondition();
	public  void set(String name)throws InterruptedException
	{
		lock.lock();
		try
		{
			while(flag)
				condition_pro.await();//该方法会抛异常,通过throws关键字给调用者try&catch处理
			this.name = name+"--"+count++;
			System.out.println(Thread.currentThread().getName()+"...生产者.."+this.name);
			flag = true;
			condition_con.signal();//唤醒的只有condition_con对象上await的线程
		}
		finally
		{
			lock.unlock();//释放锁的动作一定要执行,因为一旦try中出现异常,程序中断,锁这个资源却没有释放。
		}
	}
	public  void out()throws InterruptedException
	{
		lock.lock();
		try
		{
			while(!flag)
				condition_con.await();
			System.out.println(Thread.currentThread().getName()+"...消费者........."+this.name);
			flag = false;
			condition_pro.signal();//唤醒的只有condition_pro对象上await的线程
		}
		finally
		{
			lock.unlock();
		}
	}
}

停止线程

stop方法已经过时。
如何停止线程?
只有一种,让run方法结束。
开启多线程运行,运行代码通常是循环结构,只要控制住循环,用flag作为循环条件标记,就可以让run方法结束,也就是线程结束。
在其他线程的代码中将flag进行赋值,就可以使本线程循环体结束,参见以上代码所示。

特殊情况:
当线程处于了冻结状态,就不会读取到标记。那么线程就不会结束。
当没有指定的方式让冻结的线程恢复到运行状态时,这时需要对冻结进行清除。
强制让线程恢复到运行状态中来。这样就可以操作标记让线程结束。
Thread类提供该方法 interrupt();
class StopThread implements Runnable
{
	private boolean flag =true;
	public  void run()
	{
		while(flag)
		{
			System.out.println(Thread.currentThread().getName()+"....run");
		}
	}
	public void changeFlag()
	{
		flag = false;
	}
}

class  StopThreadDemo
{
	public static void main(String[] args) 
	{
		StopThread st = new StopThread();
		Thread t1 = new Thread(st);
		Thread t2 = new Thread(st);	
		t1.start();
		t2.start();	

		int num = 0;
		while(true)
		{
			if(num++ == 3)<span style="font-family: Arial, Helvetica, sans-serif;">//当主函数线程运行到条件满足时,此时其他线程处于冻结状态,为了一并停止其他运行的线程,就调用interrupt方法将冻结的线程唤醒,改变其循环标记来结束线程运行</span>
			{
				t1.interrupt();//清除冻结状态
				t2.interrupt();
				st.changeFlag();//改变循环标记
				break;
			}
			System.out.println(Thread.currentThread().getName()+"......."+num);
		}
		System.out.println("over");
	}
}

运行结果:


Thread类中的常用方法

守护线程

Thread类的方法中
void setDaemon(boolean on) 将该线程标记为守护线程或者用户线程,当正在运行的线程都是守护线程时,Java虚拟机退出(即前台线程停止,后台线程自动结束)
该方法必须在启动线程前调用

join方法

当A线程执行到了B线程的join()方法时,A线程就会等待,等到B线程都执行完,A才会执行。
join可以用来临时加入线程执行
Thread类中的join方法:线程向主线程(main函数的线程)要cpu执行权,主线程等待该线程停止了才会执行。
t1.start();
t2.start();
t1.join();//主线程碰到谁抢执行权,就等待谁,此时与t2无关

设置优先级

抢资源的频率
void setPriority(int newPriority)//默认优先级是5,一共10级,只有1,5,10最明显,所以设置为常量:MAX_PRIORITY,MIN_PRIORITY,NORM_PRIORITY

yield方法

临时停止线程,释放执行权,可以使线程交替运行

如何使用多线程

当某些代码需要同时被执行时,就使用多线程,比如多个独立的循环体。

两种技巧写法

使用匿名内部类简写线程
new Thread()
{
	public void run()
	{
		for(){}
	}
}.start();
使用Runnable接口
Runnable r = new Runnable()
{
	public void run()
	{
		for(){}
	}
};
new Thread(r).start();

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值