Java学习6----(多线程、线程池)

1.多线程

两个概念:
(1)并发:指两个或者多个事件在同一时间段内发送。
(2)并行:指两个或者多个事件在同一时刻发送。

线程与进程:
(1)进程:指的是一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一个之星过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。
(2)线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中可以有多个线程。

线程调度两种方法:分时调度和抢占式调度;

1.1多线程示例代码:

//我的线程类 继承线程类
public class MyTread extends Thread {
    //重写Thread类中的run方法,设置线程任务
    @Override
    public void run() {
    	String name = getName();//获取线程名称,例如Thread-0、Thread-1
        //或 Thread.currentThread().getName();
        for(int i = 0; i < 20; i++) {
            System.out.println("run:"+i);
        }
    }
}
//测试类
public class Demo {
	public static void main(String[] args) {//主线程名称为main
		MyTread mt = new MyThread();
		mt.start();
		for(int i = 0; i < 20; i++) {
			System.out.println("main:"+i);
		}
	}
}
/*
结果是,main是按顺序输出,run也是按顺序输出,但是两个线程会抢夺cpu资源,因此会出现穿插输出的现象。
*/

1.2 多线程原理

JVM执行main方法,找操作系统开辟一条main方法通向cpu的路径,即main线程(主线程),cpu通过这个线程,执行main方法。而当执行到

MyTread mt = new MyTread();

开辟一条通向cpu的新路径用来执行run方法,当mt.start()时,开始执行run方法。

此时,对于cpu而言,就有了两条执行的路径,cpu就有了选择的权力。相当于两个线程在争夺cpu的执行时间,谁争夺到了就执行谁的代码。

1.3 多线程存储理解

首先,要知道,java代码中各种方法是存在栈(方法栈)中;java代码中的对象,是存在堆(堆内存)中。一个方法栈代表一个线程。

当执行main方法时,将main方法压入第一个栈。
(1)对于单线程来说,若在main方法中调用其他方法,则将其他方法压入同一个栈中,以后进先出的原则,先执行完新压入的方法再继续执行main方法。
(2)对于多线程来说,即main方法中创建一个新线程时,就会开辟一个新的栈,在新的栈中执行新线程。这样,cpu就会在两个栈中进行选择执行代码。

其一个明显的好处就是,多个线程之间互不影响。
在这里插入图片描述

1.4 线程常用方法

(1)获取线程名称
使用Thread类中的getName()方法获取线程名称;
String getName();

(2)获取当前正在执行的线程对象的引用
static Thread currentThread();

实例见1.1

(3)使得当前正在执行的线程以毫秒数暂停,暂停线程暂停。

public class demo {
	public static void main(String[] args) {
		//因为sleep是静态方法,因此通过类名调用它
		for(int i = 1; i<=60;i++){
			System.out.println(i);
			try{
				Thread.sleep(1000);//即实现每秒输出一次
			} catch(InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}

(4)java.lang.Runnable
该方法是另一种创建线程的方式。Runnable接口应该由那些打算通过某一线程执行其实例的类来实现。
类必须定义一个称为run的无参数方法。
创建步骤如下:
1.定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
2.创建Runnable实现类的实例,并以此作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
3.调用线程对象的start()的方法来启动线程。

//Runnable实现类
public class RunnableImp1 implements Runnable {
	@Override
	public void run() {
		for(int i = 0; i < 20; i++) {
			System.out.println(Thread.currentThread().getName() + i);
		}
	}
}

//Runnable实现类
public class RunnableImp2 implements Runnable {
	@Override
	public void run() {
		for(int i = 0; i < 20; i++) {
			System.out.println("hello" + i);
		}
	}
}

//主方法
public class demo {
	public static void main(String[] args) {
		//创建Runnable接口的实现类对象
		RunnableImp1 run = new RunnableImp1();
		
		//创建Thread类对象,构造方法中传递Runnable接口的实现类对象
		//Thread t = new Thread(run);
		//体现Runnable接口使程序的易扩展性
		Thread t = new Thread(new RunnableImp2());//打印hello+i
		t.start();
		for(int i = 0; i < 20 ; i++) {
			System.out.println(Thread.currentThread().getName()+"-->"+i);
		}
	}
}
/*
同1.1代码效果
*/

(5)Runnable创建多线程相对于Thread创建的好处
Runnable接口创建多线程的好处:
a:避免了单继承的局限性:一个类只能继承一个类,类继承了Thread类就不能继承其他的类。但实现了Runnable接口,还可以继承其他的类,实现其他的接口。
b:增强了程序的扩张性,降低了程序的耦合性(解耦)。实现Runnable接口的方式,把设置线程任务和开启线程进行了分离(解耦)。创建Thread类对象,调用start方法,来开启多线程。

1.5匿名内部类方式实现线程的创建

其匿名内部类的作用是简化代码。主要方法是把子类继承父类,重写父类的方法,创建子类对象一步完成;或者是把实现类接口、重写接口的方法、创建实现类对象合成一步完成。

匿名内部类的最终产物:子类/实现类对象,而这个类没有名字。
格式:

new 父类/接口() {
	重写父类/接口中的方法
};
public class Demo {
	public static void main(String[] args) {
		//线程的父类是Thread
		//new MyThread().start();
		new Thread(){
			@Override
			public void run() {
				for(int i = 0; i < 20; i++) {
					System.out.println(Thread.currentThread().getName()+"-->"+"Hello");
				}
			}
		}.start();

		//线程的接口Runnable
		//Runnable r = new RunnableImp();
		Runnable r = new Runnable() {
			@Override
			public void run() {
				for(int i = 0; i < 20; i++) {
					System.out.println(Thread.currentThread().getName()+"-->"+"Hello");
				}
			}
		};
		new Thread(r).start();
		//或者更简单的嵌套进去
		new Thread(new Runnable() {
			@Override
			public void run() {
				for(int i = 0; i < 20; i++) {
					System.out.println(Thread.currentThread().getName()+"-->"+"Hello");
				}
			}
		}).start();
	}
}

1.6 线程安全问题

以卖票问题举例,若三个窗口在卖同一套的100张票,就会出现问题。比如有两个窗口在卖同一张票,就会出现一张票两次售卖的问题。从多线程的角度来看,即多线程访问了共同的数据,会产生线程安全问题。

代码观察

//定义多个线程共享的票源
public class RunnableImpl implements Runnable {
	private int ticket = 100;
	//设置线程任务:卖票
	@Override
	public void run() {
		//先判断票是否存在
		while(true) {
			try {
                sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
			if(ticket>0) {
				System.out.println(Thread.currentThread().getName()+"-----正在卖第"+ticket+"张票);
				ticket--;
			}
			else
				break;
		}
	}
}

//创建三个线程进行测试
public class Demo {
	public static void main(String[] args) {
		RunnableImpl run = new RunnableImpl();
		Thread t0 = new Thread(run);
		Thread t1 = new Thread(run);
		Thread t2 = new Thread(run);
		t0.start();
		t1.start();
		t2.start();
	}
}

1.7 线程安全问题解决

线程安全问题有三种解决方式。

1.7.1 第一种方案:使用同步代码块

synchronized(锁对象) {
	可能会出现线程安全问题的代码(访问了共享数据的代码)
}

注意:
1.通过代码块中的对象,可以使用任意的对象;
2.但必须保证多个线程中使用的锁对象是同一个;
3.锁对象的作用是,把同步代码锁住,只让一个线程在同步代码中执行

//锁对象
public class RunnableImpl implements Runnable {
	private int ticket = 100;
	Object obj = new Object();
	
	//设置线程任务:卖票
	@Override
	public void run() {
		//先判断票是否存在
		while(true) {
			//同步代码块
			synchronized (obj) {
					if(ticket>0) {
					System.out.println(Thread.currentThread().getName()+"-----正在卖第"+ticket+"张票);
					ticket--;
				}
				else
					break;
			}
		}
	}
}

同步技术的原理是,使用了一个锁对象,相当于是对象监视器。
当3个线程一起抢夺cpu的执行权时,谁抢到了就执行run方法进行卖票。

  1. 假设当t0抢到cpu的执行权,执行run方法,遇到synchronized代码块,这时t0会检查synchronized代码块是否有锁对象,发现有锁对象,该线程就会获取锁对象,然后进入到同步中执行。
  2. 此时,当t1抢到了cpu的执行权,执行run方法,遇到synchronized代码块,这时t1会检查synchronized代码块是否有锁对象。发现没有锁对象,则t1进入组合状态,会一直等待t0线程执行完同步中的代码,当执行完成,线程会把锁对象归还给同步代码块。此时t1才能获取到锁对象,从而进入到代码块中执行代码。

总结:同步中的线程,没有执行完毕不会释放锁,同步外的线程没有锁进不去同步代码块中,这样就保证了只能有一个线程在同步中执行共享数据,保证了线程安全。但是程序也会因为频繁的判断锁,获取锁,释放锁,而导致程序的效率降低。

1.7.2 第二种方案:使用同步方法(静态和非静态)

使用步骤:
1.把访问了共享数据的代码抽取出来,放到一个方法中;
2.在方法上添加synchronized修饰符

定义方法的格式:

修饰符 synchronized 返回值类型 方法名(参数列表) {

}
//定义多个线程共享的票源
public class RunnableImpl implements Runnable {
	private int ticket = 100;
	//设置线程任务:卖票
	
	@Override
	public void run() {
		//先判断票是否存在
		while(true) {
			payTicket();	
		}
	}

	public synchronized void payTicket() {
		if(ticket>0) {
				System.out.println(Thread.currentThread().getName()+"-----正在卖第"+ticket+"张票);
				ticket--;
			}
			else
				break;
		}
	}
}

注意:定义一个同步方法,同步方法也会把方法内部的代码锁住,只让一个线程执行,同步方法的锁对象就是实现类对象new RunnableImpl(),也就是this。

静态同步方法的锁对象不能this,this时创建对象之后产生的,而静态方法优先于对象。静态方法的锁对象时本类的class属性–>class文件对象。

//定义多个线程共享的票源
public class RunnableImpl implements Runnable {
	private int ticket = 100;
	//设置线程任务:卖票
	
	@Override
	public void run() {
		//先判断票是否存在
		while(true) {
			payTicket();	
		}
	}

	public static void payTicket() {
		synchronized(RunnableImpl.class) {
			if(ticket>0) {
					System.out.println(Thread.currentThread().getName()+"-----正在卖第"+ticket+"张票);
					ticket--;
				}
				else
					break;
		}	
	}
}

1.7.3 第三种方案:Lock锁

java.util.cuncurrent.lock接口
Lock实现提供了比使用synchronized方法和语句可获得的更广泛的锁定操作。
Lock接口中的方法:
void lock() //获取锁
void unlock() //释放锁

使用步骤:
1.在成员位置创建一个ReentrantLock对象;
2.在可能会出现安全问题的代码前调用Lock接口中的方法lock获取锁;
3.在可能会出现安全问题的代码后调用Lock接口中的方法unlock释放锁;

//定义多个线程共享的票源
public class RunnableImpl implements Runnable {
	private int ticket = 100;
	//设置线程任务:卖票
	//1.在成员位置创建一个ReentrantLock对象
	Lock l = new ReentrantLock();
	
	@Override
	public void run() {
		//先判断票是否存在
		while(true) {
			//在可能会出现安全问题的代码前调用Lock接口中的方法lock获取锁;
			l.lock();
			if(ticket>0) {
				try {
					Thread.sleep(10);
					System.out.println(Thread.currentThread().getName()+"-----正在卖第"+ticket+"张票);
					ticket--;
				} catch(InterruptedException e) {
					e.printStackTrace();
				} finally {
					l.unlock();//无论程序是否异常,都会把锁释放,以提高程序效率。
				}
			}
		}
	}
}

1.8 线程状态概述

在这里插入图片描述

1.9 线程间的通信

线程间的通信常用到函数wait(), notify(), notifyall().

调用wait和notify方法需要注意的细节:
(1)wait方法和notify方法必须要由同一个锁对象调用。因为:对应的锁对象可以通过notify唤醒使用同一个锁对象的调用的wait方法后的线程。
(2)wait方法与notify方法是属于Object类的方法,因为锁对象可以是任意对象,而任意对象的所属类都是继承类Object类的。
(3)wait方法与notify方法必须要在同步代码块中或者是同步函数中使用。因为必须要通过锁对象调用这两个方法。

/*
等待唤醒机制案例(线程间的通信):线程之间的通信
	创建一个顾客线程:告知要的商品数量,调用wait方法,放弃cpu执行,进入WAITING状态;
	创建一个老板线程:花5秒做商品,做好后调用notify()方法,唤醒顾客线程拿商品;

注意:
	老板和顾客必须使用同步代码块包裹起来,保证等待和唤醒只有一个在执行;
	同步使用的锁对象必须保证唯一;
	只有锁对象才能调用wait和notify方法;

Object类中方法
void wait()
	在其他线程调用此对象的notify()方法或者notifyAll()方法前,使当前线程等待;
void notify()
	唤醒在此对象监视器上等待的单个线程;
	然后会继续执行wait方法之后的代码;
*/

public calss Demo01WaitAndNotify {
	public static void main(String[] args) {
		//创建锁对象,保证唯一
		Object obj = new Object();
		//创建一个顾客线程(消费者)
		new Thread(){ //匿名类
			@Override
			public void run() {
				synchronized (obj) {
					System.out.println("告诉老板顾客要的商品数量");
					//调用wait方法,放弃cpu的执行,进入到WAITING状态
					try {
						obj.wait();
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					//唤醒之后执行的代码
					system.out.println("顾客我拿到商品啦");
				}
			}
		}.start();

		new Thread(){
			@Override
			public void run() {
				try {
					Thread.sleep(5000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				synchronized(obj) {
					system.out.println("老板5秒后,告知顾客OK了")
					obj.notify();
				}
			}
		}.start();
	}
}

1.10 线程间的通信 实例

需求分析:
生产者(包子铺)类:
是一个线程类,可以继承thread;
设置线程任务(run):生产包子;
对包子状态进行判断
true:有包子

  • 包子铺调用wait方法进入等待状态

false:没有

  • 包子铺生产包子
    交替生产两种包子(i%2==0)
    包子铺生产好了修改包子状态为true
    唤醒吃货线程,吃包子

注意:
1)包子铺线程和吃货线程关系是通信的(互斥);
2)必须同时同步技术保证两个线程只有一个执行;
3)锁对象必须保证唯一,可以使用包子对象作为锁对象;
4)包子铺和吃货的类需要把包子对象作为参数传递进来;
因此需要在成员未知创建一个包子变量
使用带参构造方法,为包子铺变量赋值

消费者(吃货)类,继承Thread
设置线程任务(run):吃包子
对包子状态进行判断:
false:没有包子
吃货调用wait方法进入等待
true:有包子
吃货吃包子,知道吃完
修改包子状态为false
吃货唤醒包子铺线程,生产


public class BaoZi {
	String pi;
	String xian;
	boolean flag = false;
}

public class BaoZiPu extends Thread {
	private BaoZi bz;
	
	public BaoZiPu(BaoZi bz) {
		this.bz = bz;
	}
	
	@Override
	public void run() {
		int count = 0;
		//同步技术
		while(true) {
			synchronized(bz) {
				if(bz.flag == true) {
					try {
						bz.wait();
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
				// 被唤醒后执行包子铺生产包子,交替生产两种
				if(count%2 == 0) {
					//生产三仙包子
					bz.pi = "薄皮";
					bz.xian = "三鲜馅";
				} else {
					bz.pi = "冰皮";
					bz.xian = "牛肉”;
				}
				count++;
				System.out.println("包子铺正在生产:"+bz.pi+bz.xian+“包子”);
				try {
					Thread.sleep(3000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				//包子生产好,状态为有包子
				bz.flag = true;
				bz.notify();
				System.out.println("包子铺生产好包子");
			}
		}
	}
}

public class ChiHuo extends Thread {
	private BaoZi bz;
	public ChiHuo(BaoZi bz) {
		this.bz = bz;	
	}
	
	@Override
	public void run() {
			while(true){
				synchronized(bz) {
					if(bz.flag == false) {
						try {
							bz.wait();
						} catch (InterruptedException e) {
							e.printStackTrace();
						}
					}
					System.out.ptintln("吃货正在吃包子"+bz.pi+bz.xian+"的包子");
					bz.flag = false;
					bz.notify();
					System.out.println("吃货把:"+bz.pi+bz.xian+包子铺开始生产包子");		
					System.out.println("+++++++++++++++++");
				}
			}
	}
}

public class Demo {
	public static void main(String[] args) {
		BaoZi bz = new BaoZi();
		new BaoZiPu(bz).start();
		new ChiHuo(bz).start();
	}
}

2. 线程池

如果并发线程数量很多,但每个线程都是执行一个时间很短的任务就结束了,这样频繁创建和销毁线程非常消耗时间。线程池是为了使得线程可以复用,即执行完一个任务,并不销毁,而是可以继续执行其他任务。线程池实际上就是一个容器,LinkedList。

其优点是:
1)降低资源消耗;2)提高响应速度;3)提高线程的可管理性。

线程池图解如下:
在这里插入图片描述

2.1 线程池代码实现

线程池是在JDK1.5之后提供的;
java.util.concurrent.Execetors:线程池的工厂类,用来生成线程池
Executors类中的静态方法:

  • static ExecutorService newFixedThreadPool(int nThreads) 创建一个可重用的固定线程数的线程池
  • 参数:int nThreads:创建线程池中包含的线程数量;
  • 返回值是ExecutorService接口,返回的是ExecutorService接口的实现类对象,我们可以使用ExecutorService接口接收(面向接口编程)

java.util.concurrent.ExecutorService:线程池接口
用来从线程池中获取线程,调用start方法,执行线程任务;
submit(Runnable task) //提交一个Runnable任务用于执行
关闭/销毁线程池的方法:void shutdown()

线程池的使用步骤:
1)使用线程池的工厂类Executors里面提供的静态方法newFixedThreadPool生产一个指定线程数量的线程池;
2)创建一个类,实现Runnable接口,重写run方法,设置线程任务;
3)调用ExecutorService中的方法submit,床底线程任务(实现类),开启线程,执行run方法
4)调用ExecutorService中的方法shutdown销毁线程池


public class Demo01ThreadPool {
	public static void main(String[] args) {
		ExecutorService es = Executors.newFixedThreadPool(2);
		es.submit(new RunnableImpl());
	}
}

public class RunnableImpl implements Runnables {
	@Override
	public void run() {
		System.out.println(Thread.currentThread().getName()+"创建了一个新的线程");
	}
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值