线程安全

一、分析线程安全

昨天写了线程安全的案例-模拟List的安全隐患,得出的结论是,在多线程中,出现了共享数据,需要加锁处理;锁的注意事项: 1.同一个锁对象 2.锁的范围; 今天继续分析线程安全,通过实现Runnable方式和继承Thread方式;看看他们的区别

实现任务

//案例:买票系统,5个窗口共卖1000张票;观察卖票过程:
//例如: 窗口1正则卖第1000张票;
//      窗口5正则卖第999张票;
//      窗口3正则卖第998张票;
//      ...
//      窗口3正则卖第1张票;
//分析:创建5个窗口-5个线程    票数-共享数据(安全隐患)
//方式1:实现Runnable方式
//问题1:出现部分重票问题
//原因:共享数据有篡改数据--有隐患;
//解决方案:加锁  锁住共享数据操作;  需要考虑锁的范围
//问题2:出现负数
//原因:临界点判断出现了问题
//处理方案: 在锁里继续判断;大于0才买票

//优化升级:买完票应该要有退出的提示,且提示应该是完整的
//使用while(true)循环,表示只有一个出口退出即可

//锁的分类:同步代码块   同步方法
class Task implements Runnable{
	private int ticket = 1000;  //1000张票-共享数据 
	@Override
	public void run() {
		while(true) {
			/*  
			//同步代码块方式
			synchronized (this) { //"lock"  静态对象
				if(ticket>0) {
					//Thread.currentThread().getName():实现任务获取的线程名
					System.out.println(Thread.currentThread().getName()+"正在卖第"+ticket+"张票");
					ticket--;
				}else {
					System.out.println(Thread.currentThread().getName()+"已经卖完了");
					break;
				}
			}*/
			
			if(save()) {  //同步方法调用
				break;
			}
		}
	}
	//同步方法: 如何确保同一个锁对象?谁调的方法就是哪个对象--this(同一个锁对象)
	private synchronized boolean save() {  
		if(ticket>0) {
			//Thread.currentThread().getName():实现任务获取的线程名
			System.out.println(Thread.currentThread().getName()+"正在卖第"+ticket+"张票");
			ticket--;
			return false; //返回false表示还可继续循环卖票
		}else {
			System.out.println(Thread.currentThread().getName()+"已经卖完了");
			return true;  //结束卖票
		}
	}
}
public class Test1 {
	public static void main(String[] args) {
		Task task = new Task();
		for(int i=1;i<=5;i++) {
			new Thread(task,"窗口"+i).start();
		}
	}
}

继承Thread

//案例:买票系统,5个窗口共卖1000张票;观察卖票过程:
//方式2:继承Thread方式
//问题1:每new线程对象,都有一份独立的属性--卖了5000张
//处理方案:将属性变为共享数据   属性+static--静态属性值维护一份
//问题2:会出现部分重票问题----原因是this不是同一个锁对象
//解决方案:锁对象变为字符串常量
class MyThread extends Thread{
	private static int ticket = 1000;  //1000张票-共享数据 
	
	public  MyThread(String name) {
		super(name);
	}
	@Override
	public void run() {
		while(true) { 
			//同步代码块方式
			synchronized ("lock") { 
				if(ticket>0) {
					System.out.println(getName()+"正在卖第"+ticket+"张票");
					ticket--;
				}else {
					System.out.println(getName()+"已经卖完了");
					break;
				}
			}
		}
	}
}
public class Test2 {
	public static void main(String[] args) {
		for(int i=1;i<=5;i++) {
			new MyThread("窗口00"+i).start();
		}
	}
}

创建区别

实现任务 VS 继承Thread

实现任务:

里面的属性是共享数据;同步锁可使用this;获取线程名需使用Thread.currentThread().getName()

继承Thread:

属性+static才是共享数据;同步锁不能使用this;获取线程名直接使用getName()

线程状态

线程状态—阻塞

基本状态—启动状态 等待状态—-sleep,join

阻塞状态:在执行的现在中加锁,进入阻塞状态;其他线程只能等待; 等待释放锁资源后,其他就绪状态线程才能继续执行

//回顾之前学习过的线程安全: //ArrayList VS Vector(安全) //StringBuilder VS StringBuffer(安全) //HashMap VS Hashtable(安全)

二、死锁

概述

有一个线程进入到锁里后,没有退出;其他的线程进不去,则构成死锁

同步代码块和同步方法都是自动释放锁资源,所以不容易出现死锁;如果需要演示死锁案例;在同步锁中需要进行锁嵌套;

注意:此处只是为了测试死锁,才进行的锁嵌套; 以后使用时尽量避免锁嵌套(避免死锁)

锁嵌套

//死锁案例:两个线程,各自执行自身的代码
class MyThread extends Thread{
	private boolean a;  //标记判断
	public MyThread(boolean a) {
		this.a = a;
	}
	@Override
	public void run() {
		if(a) {
			synchronized ("A") { //线程1
				System.out.println("进入线程1的A锁");
				synchronized ("B") {
					System.out.println("进入线程1的B锁");
				}
			}
		}else {
			synchronized ("B") { //线程2
				System.out.println("进入线程2的B锁");
				synchronized ("A") {
					System.out.println("进入线程2的A锁");
				}
			}
		}
	}
}
public class Test1 {
	public static void main(String[] args) {
		new MyThread(true).start();  //线程1
		
		new MyThread(false).start(); //线程2
	}
}

三、生产者消费者

生产者和消费者的线程安全模型,代表类很多种应用中的场景; 例如:生活中的生产产品入库;消费产品出库;

//案例:生产者消费者线程案例,生产者一直负责生产,消费者一直负责消费
//分析:生产者线程和消费者线程都操作同一个库存(共享数据)
//问题:
//1.会出现还没有等到生产,就已经消费了的情况  
//2.数据混乱:生产者线程数量+1后,直接打印出了消费数据

//处理方案:
//数据混乱--需要加锁来解决
//没有生产提前消费的问题:线程等待的功能,判断件数为0,一直等待,无限期等待,直到发指令
//wait()--等待;  notify--唤醒(唤醒等待)

public class Test1 {
	public static void main(String[] args) {
		Store store = new Store();
		//生产者和消费者都操作同一个库存
		new Producter(store).start();
		
		new Customer(store).start();
	}
}
public class Producter extends Thread { //生产者线程
	private Store store;
	public Producter(Store store) { //从外面传入库存对象
		this.store = store;
	}
	@Override
	public void run() {
		while(true) {//循环生产
			try {
				store.push();    //生产者负责入库
			} catch (Exception e) {
				e.printStackTrace();
			} 
		}
	}
}
public class Customer extends Thread {  //消费者线程
	private Store store;  //消费者操作库存
	public Customer(Store store) {
		this.store = store;
	}
	@Override
	public void run() {
		while(true) { //循环消费
			try {
				store.pop();    //消费者出库
			} catch (InterruptedException e) {
				e.printStackTrace();
			}  
		}
	}
}
//仓满和仓空由库存决定--在库存中判断
public class Store {
	private int count; //库存数量的标记--共享数据,库存对象只有一个,那么成员也只有一个
	public void push() throws InterruptedException {//负责生产的功能
		synchronized (this) { //this可以表示同一个锁对象
			if(count>=20) {  //仓满则停止生产
				this.wait();  //等待;释放锁资源
			}
			count++;
			System.out.println("已经生产了第"+count+"件货");
			
			this.notify(); //唤醒,唤醒等待的线程(唤醒对方)
		}
	}
	public void pop() throws InterruptedException { //负责消费的功能
		synchronized (this) {
			if(count<=0) {//仓空停止消费
				this.wait();   //等待;释放锁资源
			}
			System.out.println("已经消费了第"+count+"件货");
			count--;
			
			this.notify(); //唤醒,唤醒生产者
		}
	}
}

四、线程池

概述

线程池:就是装线程的容器,预先在容器中创建指定个数的线程对象;当用户需要时,直接从容器中获取;用完了,再回收到线程池中。

之前创建线程的方式:创建线程对象后,执行完毕则销毁线程对象;如果程序中需要频繁创建线程时,会非常影响性能及消耗内存的资源

好处:减少了创建和销毁线程的数目,提升了性能及减少了资源的消耗

线程池的复用机制:线程对象使用完后,重新再回到线程池;可以交给其他用户继续使用

内部实现: 创建集合;集合中都是存线程对象;有用户使用,则移除当前集合元素;用完后,则添加回集合中。

应用

//线程池:
class Task implements Runnable{
	@Override
	public void run() {
		for(int i=1;i<=10;i++) {
			System.out.println(Thread.currentThread().getName()+"-->"+i);
		}
	}
}
public class Test1 {
	public static void main(String[] args) {
		//在线程池中创建单个线程对象,通过单个线程对象去处理多个任务
		//ExecutorService es = Executors.newSingleThreadExecutor();
		//带缓冲区的线程池:有多少个任务,则会产生多少个线程对象的处理
		//ExecutorService es = Executors.newCachedThreadPool();
		//带复用机制的线程池:指定线程池个数,进行复用
		ExecutorService es = Executors.newFixedThreadPool(2);//(常用)
		es.submit(new Task()); //任务接口Runnable
		es.submit(new Task());
		es.submit(new Task());  //有三个任务,则谁先执行完,再执行第3个--复用
		
		es.shutdown();  //线程池的终止
	}
}

Callable接口

在线程池的执行中,有两个接口表示执行线程任务的接口;Runnable和Callable接口

区别:Callable接口可以带返回值;线程的执行结果需要返回时,可以选择Callable

//需求:使用两个线程并发的执行1~50,51~100的和;并汇总结果
//线程池中有Callable接口,可以带返回结果
public class Test2 {
	public static void main(String[] args) throws InterruptedException, ExecutionException {
		ExecutorService es = Executors.newFixedThreadPool(2);
		Future<Integer> fu1 = es.submit(new Callable<Integer>() {
			@Override
			public Integer call() throws Exception { //线程1
				int sum = 0;
				for(int i=1;i<=50;i++) {
					sum += i;
				}
				return sum;
			}
		});
		Future<Integer> fu2 = es.submit(new Callable<Integer>() {
			@Override
			public Integer call() throws Exception { //线程2
				int sum = 0;
				for(int i=51;i<=100;i++) {
					sum += i;
				}
				return sum;
			}
		});
		int sum = fu1.get()+fu2.get();  //get是阻塞方法--和join类似
		System.out.println("和为:"+sum);
		es.shutdown();
	}
}

问题:什么是异步?,什么是同步?,在多线程中如何体现的?

异步:多个线程并发执行–随机互抢资源

同步:加了同步锁,只运行一个线程执行

上述案例中,fu1和fu2线程的执行就是异步的;get方法是同步的,等待多线程处理完,才进行汇总; 加锁也是属于同步里面的。

五、总结与作业

总结

1.分析线程安全(重点)
实现任务的线程安全;继承Thread的线程安全
加锁-锁的注意事项   同步代码块和同步方法的应用区别
创建线程的区别--在线程安全中的区别;线程状态-阻塞
2.死锁
死锁;锁嵌套的应用
3.生产者与消费者模型
线程安全的模型;生产者线程,消费者线程,共同操作库存
wait,notify用法
4.线程池(重点)
概述;好处; 应用
Runnable接口与Callble接口的区别

作业

1.利用线程输出从a到z的26个字母,每隔一秒钟输出一个字母

2.描述synchronized关键字的作用

3.简单描述什么是死锁

4.卖100张票,创建2个线程一起卖  使用继承Thread的同步代码块和同步方法完成
问题:同步方法必须加static才能锁得住,为什么?


5.模拟接力跑,1000米,10个人,每人跑100米,1个人跑完下个人接着跑(synchronized)
打印信息提示:线程n跑完100米,还剩900米..

选做题(不交)
6.实现A向银行卡存钱,B从银行卡取钱 
  6.1 创建BankCard银行卡,属性为money余额
  6.2 创建存钱的线程AddMoney
  6.3 创建取钱的线程GetMoney

晨考

1.java 有几种方式创建线程?分别是什么?
答:分别是继承Thread和实现Runnable接口
2.线程池有什么用?
答:线程池能够快速调取线程,减少了创建和销毁线程的数目。提升性能,降低损耗。
3.wait(),notify(),notifyAll()分别有什么作用
答:
4.锁的注意事项有哪些?什么情况下锁对象可以使用this?