多线程系列学习笔记之(二)

206 阅读7分钟

本片博客给大家通过模拟卖票问题讲解线程安全问题

1、多个线程模拟卖票问题

public class SaleTicket extends Thread{
	//非静态属性归每个对象所有,静态属性归所有对象所有
	private static int ticket = 100;
	public void run() {
		while(ticket > 0) {
			try {
				Thread.sleep(20);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println(getName() + "卖了1张票,还剩" + (ticket) + "张票");
		}
	}
}

public class TestSaleTicket {
	public static void main(String[] args) {
		SaleTicket s1 = new SaleTicket();
		s1.setName("火车站:");
		SaleTicket s2 = new SaleTicket();
		s2.setName("代售点:");
		SaleTicket s3 = new SaleTicket();
		s3.setName("网络:");
			
		s1.start();
		s2.start();
		s3.start();
		
	}
}

产生线程安全问题的根本原因

  • 1.必须有单线程环境
  • 2.必须有共享数据
  • 3.多个线程对共享数据进行修改

2、两种方式解决线程安全问题

方式一.使用同步代码块,需要一把指定的代码锁,任意对象都可以当锁

语法格式

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

public class SaleTicket extends Thread{
	//非静态属性归每个对象所有,静态属性归所有对象所有
	private static int ticket = 100;
	private static Object object = new Object();//锁对象,要保证多个线程共享同一把锁
	public void run() {
		synchronized (object) {
			while(ticket > 0) {
				try {
					Thread.sleep(20);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println(getName() + "卖了1张票,还剩" + (--ticket) + "张票");
			}
		}
	}
}

方式二2.同步方法,就是在方法上添加synchronized关键字即可

  • 1)对于非静态方法,锁是this
  • 2)对于静态方法,锁是类
public class SaleTicket implements Runnable{
	//非静态属性归每个对象所有,静态属性归所有对象所有
	private static int ticket = 100;
	public synchronized void run() {
			while(ticket > 0) {
				try {
					Thread.sleep(20);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println(Thread.currentThread().getName() + "卖了1张票,还剩" + (--ticket) + "张票");
			}
	}
}

public class TestSaleTicket {
	public static void main(String[] args) {
		SaleTicket s = new SaleTicket();
		Thread t1 = new Thread(s,"火车站");
		Thread t2 = new Thread(s,"代售点");
		Thread t3 = new Thread(s,"网络");
		t1.start();
		t2.start();
		t3.start();
		
	}
}

3、死锁

1、什么是死锁?

所谓死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。 因此我们举个例子来描述,如果此时有一个线程A,按照先锁a再获得锁b的的顺序获得锁,而在此同时又有另外一个线程B,按照先锁b再锁a的顺序获得锁。如下图所示:

2、产生死锁的原因

可归结为如下两点:

a. 竞争资源

系统中的资源可以分为两类: 可剥夺资源,是指某进程在获得这类资源后,该资源可以再被其他进程或系统剥夺,CPU和主存均属于可剥夺性资源; 另一类资源是不可剥夺资源,当系统把这类资源分配给某进程后,再不能强行收回,只能在进程用完后自行释放,如磁带机、打印机等。 产生死锁中的竞争资源之一指的是竞争不可剥夺资源(例如:系统中只有一台打印机,可供进程P1使用,假定P1已占用了打印机,若P2继续要求打印机打印将阻塞) 产生死锁中的竞争资源另外一种资源指的是竞争临时资源(临时资源包括硬件中断、信号、消息、缓冲区内的消息等),通常消息通信顺序进行不当,则会产生死锁

b. 进程间推进顺序非法

若P1保持了资源R1,P2保持了资源R2,系统处于不安全状态,因为这两个进程再向前推进,便可能发生死锁 例如,当P1运行到P1:Request(R2)时,将因R2已被P2占用而阻塞;当P2运行到P2:Request(R1)时,也将因R1已被P1占用而阻塞,于是发生进程死锁

3、死锁产生的4个必要条件?

  • 互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。
  • 请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。
  • 不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
  • 环路等待条件:在发生死锁时,必然存在一个进程--资源的环形链。

关于解决死锁的方法参考博文:blog.csdn.net/hd12370/art… 死锁示例代码

public class DeadLock extends Thread{
	boolean flag;//为了保证一个线程先执行任务A另一个线程执行任务B,定义一个标志位
	private static Object o1 = new Object();
	private static Object o2 = new Object();
	public void run() {
		if(flag == true) {
			synchronized (o1) {
				System.out.println("线程任务A");
				Thread.sleep(10);
				synchronized (o2) {
					System.out.println("线程任务B");
				}
			}
		} else {
			synchronized (o2) {
				System.out.println("线程任务B");
				synchronized (o1) {
					System.out.println("线程任务A");
				}
			}
		}
	}
}
public class TestDeadLock {
	public static void main(String[] args) {

		DeadLock d1 = new DeadLock();
		d1.flag = true;
		DeadLock d2 = new DeadLock();
		d2.flag = false;
		d1.start();
		d2.start();
	}
}

为了避免死锁,要避免锁的嵌套使用

4、线程通信(生产者消费者问题)

示例代码如下:

1.创建产品类
//产品类
public class Product {
	private String name;
	private double price;
}

2.创建生产者类,并把产品传给生产者类
public class Producer extends Thread{
	private Product product;
	public Producer(Product product) {
		super();
		this.product = product;
	}
	public void run() {
		//模拟生产两件商品,一个是袜子,8.8,一个是手机9888
		int i = 0;
		while(true) {
			if(i % 2 == 0) {
				product.setName("袜子");
				product.setPrice(8.8);
			} else {
				product.setName("手机");
				product.setPrice(9888);
			}
			System.out.println("生产者生产了:" + product.getName() + ",价格为:" + product.getPrice());
			i++;
		}
	}
}
3.创建消费者,把产品传给消费者
public class Consumer extends Thread{
	private Product product;
	public Consumer(Product product) {
		this.product = product;
	}
	public void run() {
		while(true) {
			System.out.println("消费者购买了:" + product.getName() + ",花了:" + product.getPrice());
		}
	}
}

测试
public class TestAll {
	public static void main(String[] args) {
		Product product = new Product();
		Producer producer = new Producer(product);
		Consumer consumer = new Consumer(product);
		producer.start();
		consumer.start();
	}
}

以上代码会出现线程安全问题,添加同步代码块,并用产品当锁

实现生产一件产品,消费一件产品,需要使用Object类提供的两个方法,wait(让线程等待)和notify(唤醒等待的线程)

使用条件:必须在同步代码块中使用,必须由锁对象来调用


解决死锁带来的线程安全问题

//产品类
public class Product {
	private String name;
	private double price;
	private boolean flag;//如果flag为true表示生产完成,false表示生产未完成
}

修改生产者类
public class Producer extends Thread{
	private Product product;
	public Producer(Product product) {
		super();
		this.product = product;
	}
	public void run() {
		//模拟生产两件商品,一个是袜子,8.8,一个是手机9888
		int i = 0;
		while(true) {
			synchronized (product) {
				if(product.isFlag() == false) {//表示生产未完成,开始生产
					if(i % 2 == 0) {
						product.setName("袜子");
						try {
							Thread.sleep(10);
						} catch (InterruptedException e) {
							e.printStackTrace();
						}
						product.setPrice(8.8);
					} else {
						product.setName("手机");
						product.setPrice(9888);
					}
					System.out.println("生产者生产了:" + product.getName() + ",价格为:" + product.getPrice());
					i++;
					//表示生产完成
					product.setFlag(true);
					//通知消费者去消费
					product.notify();
				} else {//生产已经完成,等待消费者消费
					try {
						product.wait();
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
				}
			}
		}
	}
}
修改消费者
public class Consumer extends Thread{
	private Product product;
	public Consumer(Product product) {
		this.product = product;
	}
	public void run() {
		while(true) {
			synchronized (product) {
				if(product.isFlag() == true) {//表示生产完成,消费者开始消费
					System.out.println("消费者购买了:" + product.getName() + ",花了:" + product.getPrice());
					//消费者消费完毕
					product.setFlag(false);
					//通知生产者去生产
					product.notify();
				} else {//表示生产未完成,等待生产者生产
					try {
						product.wait();
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
				}
			}
		}
	}
}

5、线程池

池:实质上就是一个容器 线程池:存储了多个线程的容器

如果频繁的创建线程和让线程销毁浪费cpu资源

jdk1.5以后通过java提供的类可以创建线程池

示例代码:

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

public class MyThread9 implements Runnable{
	public void run() {
		System.out.println("有一个病人来看病了");
		try {
			Thread.sleep(3000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println("医生从池子里来给病人看病:" + Thread.currentThread().getName());
		System.out.println("医生看完病了,回到池中,等待下一个病人");
		
	}

}

2.创建线程池并且使用池中的线程

public class TestMyThread9 {
	public static void main(String[] args) {
		//创建Runnable接口对象
		MyThread9 m = new MyThread9();
		//创建线程池对象,并指定线程个数
		ExecutorService pool = Executors.newFixedThreadPool(3);
		//从池中获取线程,并自动调用run方法
		pool.submit(m);
		pool.submit(m);
		pool.submit(m);
		pool.submit(m);
		pool.submit(m);
		//关闭线程池(不推荐)
		pool.shutdown();
	}
}

解决线程的遗留问题:

子类不能抛出父类处理不了的编译时异常 示例代码:

public class LaoZhang {
	public void look() throws InterruptedException{
		System.out.println("看报纸");
	}
}

public class XiaoZhang extends LaoZhang{
	public void look() throws InterruptedException,FileNotFoundException {//报错了,子类不能抛出父类处理不了的异常
		System.out.println("看手机");
		Thread.sleep(1000);
	}
}

父类异常要大于等于子类的异常


有关涉及线程安全的单例设计模式文章请参考作者博文👉juejin.cn/post/684490…

记得点赞关注👉:推荐自己的Github地址:github.com/Lmobject