本片博客给大家通过模拟卖票问题讲解线程安全问题
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