一、线程安全问题
先看一下同步机制与异步机制
异步机制: 在多线程并发,对数据进行操作的时候不会进行“排队”有可能发生多个线程同时对一个数据进行操作,这个时候会存在线程安全问题(也就是平常线程执行任务)。
- 异步机制的优点: 并发执行任务,效率高。
异步机制的缺点: 存在线程安全问题。
同步机制: 在多线程并发,对数据进行操作的时候会先等待之前的线程操作完之后才会轮到下一个线程进行操作,形成了类似于排队进行操作的机制,这就是同步机制。
- 同步机制的优点: 线程排队进行操作解决了数据安全问题。
- 同步机制的缺点: 由于排队进行操作从而降低了执行效率。
同步与异步的对比
- 共同点:同步机制与异步机制都发生在多线程并发的情况下。
- 不同点:同步机制采用“排队”进行数据操作,异步机制没有采用“排队”,而是并发进行。
1.什么是线程安全问题?
概述: 多线程并发的时候共享同一个资源,在对数据进行修改的时候有可能会造成数据的值发生错误。
下面这个例子为了方便我将其写在了一起
请看以下代码。
/**
* 线程安全问题的举例:
* 背景:两个人(可看做是两个线程)同时对一个账户进行取钱操作
* 在某个人有可能的网络延迟下,取款后的余额将会发生错误
* 会出现,多取了钱,但余额没减的情况。
* 这就是线程安全问题,在多线程并发的时候
*/
public class ThreadSafe {
public static void main(String[] args) {
//创建一个账户,初始化余额为1000,让两个线程对此账户进行取款500的操作。
Account user=new Account(1000);
//创建取款线程一
Thread t1=new Thread(new TestThread1(user));
//创建取款线程二
Thread t2=new Thread(new TestThread2(user));
//修改线程名字
t1.setName("取款线程一");
t2.setName("取款线程二");
//启动两个取款线程
t1.start();
t2.start();
}
}
class Account{
public static final String name="Jack";
private double balance;
public Account(double balance) {
this.balance = balance;
}
public static String getName() {
return name;
}
public double getBalance() {
return balance;
}
//取款的方法
public void drawMoney(double money) throws InterruptedException {
if(money<=getBalance()) {
double after = getBalance() - money;
//模拟取款线程一网络延迟0.05秒
if("取款线程一".equals(Thread.currentThread().getName())) {
Thread.sleep(50);
}
//刷新取款后的余额
this.balance=after;
System.out.println(Thread.currentThread().getName()+",取款:"+money+"元"+",取款后剩余金额:"+getBalance());
}else {
System.out.println("余额不足!");
}
}
}
//线程一
class TestThread1 implements Runnable{
private Account user;
public TestThread1(Account user){
this.user=user;
}
public void run(){
try {
//调用取款方法
user.drawMoney(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//线程二
class TestThread2 implements Runnable{
private Account user;
public TestThread2(Account user){
this.user=user;
}
public void run(){
try {
//调用取款方法
user.drawMoney(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
- 这是程序的运行结果:
- 通过以上代码我们可以发现,在两个(或者是多个)线程并发取款的情况下,余额本该为0,这个时候数据就发生了错误,这就是线程安全问题。
- 由此例子可见,应该是在多线程并发共享一个资源的时候会存在线程安全问题。
- 这个例子也就是多线程的异步机制。
2. 线程安全问题应该如何解决?
既然在多线程并发的时候有线程安全问题的存在,那么我们应该如何解决线程的安全问题?
答:采用同步机制处理。
2.1 同步机制的实现
步骤:
- 创建多个线程使其对同一个对象的数据进行操作。
- 在对数据操作的时候,将数据操作的语句放入synchronized代码块中即可。
synochronized 关键字:
作用:锁住synchronized代码块中的内容,使得有共享对象的线程执行的时候会排队执行,进入synchronized代码块的线程会占有这把锁,知道这个线程执行完之后才会释放锁,下一个线程才能进入。
语法:
1.修饰要操作的语句
synchronized(共享对象){
java语句;
}2.直接修饰其方法(代表将整个方法锁住)
修饰符 synchronized 返回值类型 方法名(形参列表){
java语句;
}.
共享对象: 像上面的例子(取款的例子)中,账户对象user就是共享对象,两个线程同时对一个对象操作,这个对象就是共
享的
2.2 锁的概念
概述: 将synchronized代码块中的内容锁起来,只有有共同对象的线程才能进入,并且一个线程进入之后会占有这把对象锁其它线程就不得再进入,必须等进入的线程完成操作释放这把对象锁之后才能进入,就使得线程的执行方式变成了同步机制,即就是排队执行。
锁的分类:对象锁和类锁
- 对象锁: 每个对象都有且只有一把锁,在进入synchronized代码块的时候,对象锁会被所进入的线程占有,直到进入的线程完成操作的时候才会释放锁,被锁住的时候其它线程不能进入,只能在外等待锁的释放。
- 类锁: 每个类都只具有一把锁,类锁是唯一的。
将上个例子改为线程安全的(也就是同步机制),其它代码不变,只需将取款方法变化一下即可,请看代码。
//取款的方法
public void drawMoney(double money) throws InterruptedException {
//this代表的是当前对象,也就是账户对象。
synchronized(this) {
if (money <= getBalance()) {
double after = getBalance() - money;
//模拟取款线程一网络延迟0.05秒
if ("取款线程一".equals(Thread.currentThread().getName())) {
Thread.sleep(50);
}
//刷新取款后的余额
this.balance = after;
System.out.println(Thread.currentThread().getName() + ",取款:" + money + "元" + ",取款后剩余金额:" + getBalance());
} else {
System.out.println("余额不足!");
}
}
}
- 也可以将synchronized加在方法的修饰上(表示锁住整个方法)。
2.3 死锁情况的产生
概述: 死锁就是锁使用不当,导致程序僵持住了,不在进行下一步的执行,而且也不会报错,也不会出现异常。
- 死锁现象经常发生在synchronized嵌套使用的时候。
请看一个死锁现象的例子,请看以下代码。
public class TestThreadSafe2 {
public static void main(String[] args) {
Test t1=new Test();
Test t2=new Test();
Thread thread1=new Thread(new Thread2(t1,t2));
Thread thread2=new Thread(new Thread021(t1,t2));
thread1.setName("线程一");
thread2.setName("线程二");
thread1.start();
thread2.start();
}
}
class Thread2 implements Runnable{
Test t1;
Test t2;
public Thread2(Test t1,Test t2){
this.t1=t1;
this.t2=t2;
}
public void run() {
/*
//让其中一个线程先睡眠一会可以解除死锁
if(Thread.currentThread().getName().equals("线程一")){
try {
System.out.println(Thread.currentThread().getName()+"正在睡眠三秒");
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}*/
synchronized(t1){
System.out.println(Thread.currentThread().getName()+"正在执行"+t1);
synchronized(t2){
System.out.println(Thread.currentThread().getName()+"正在执行"+t2);
}
}
}
}
class Thread021 implements Runnable{
Test t1;
Test t2;
public Thread021(Test t1,Test t2){
this.t1=t1;
this.t2=t2;
}
public void run() {
synchronized(t2){
System.out.println(Thread.currentThread().getName()+"正在执行"+t2);
synchronized(t1){
System.out.println(Thread.currentThread().getName()+"正在执行"+t1);
}
}
}
}
class Test{
//@Override
public String toString() {
return super.toString();
}
}
解释:当两个线程,线程一和线程二运行的时候,线程一先占住了t1锁,然后在占住了t2锁,然后再释放t2锁,再释放t1锁,线程二刚好与它相反,是先占t2锁后占t1锁,但是,这个时候容易发生一个问题,如果当线程一占住了t1锁的时候,线程二占住了t2锁,那么线程一就不能进入t2锁中,而线程一占住了t1锁,那么线程二就不能进入t1锁中,因为要结束锁中的内容才能释放锁,而这个时候两个线程的执行都没有结束,所以释放不了锁,这个时候程序就会僵持住,不会进行下一步,不会报错,也不会出现异常,这就是死锁。
因为死锁不会报错,也不会出现异常,所以在程序代码较多的情况下不容易找出这个错误,因此,我们就要在写程序的时候避免死锁现象的发生,也就是不用synchronized进行嵌套使用。
二、 生产者与消费者模式
概述: 这个模式就是生产者进行生产,消费者进行消费,当生产的产品在仓库中满了的话,让生产者停止生产,让消费者消费,同样的,当仓库中的消费产品完了的时候,让消费者停止消费,让生产者进行生产。
1. 生产者与消费者模式也属于多线程并发问题,也需要考虑线程安全问题。
2. 需要用到的方法:wait()方法和notify()方法(这两个方法的介绍可以看我上一篇博客“多线程的总结(上)”)
3. 在这个例子中也有对wait()方法和notify()方法也有比较详细的介绍,就在代码最上面的注释中,也可以看我上一篇博客“多线程的总结(上)”中,也对这两个方法有所介绍。
关于消费者与生产者的例子,请看以下代码。
/**
* 使用wait()方法和notify()方法实现“生产者和消费者模式”
* 生成线程负责生产,消费线程负责消费。
*
*wait()方法和notify()方法共享数据,因此要建立在 synchronized 基础之上(这两个方法是java对象的普通方法,不是线程方法)
*
* wait()方法:o.wait() 让正在o对象上获取的线程进入等待状态,并且释放掉之前占有的o对象的锁
* notify()方法:o.notify() 唤醒在o对象上等待的线程(只是通知,不会释放o对象之前占有的锁)
*/
public class TestWaitAndNotify {
public static void main(String[] args) {
List<StudentX> list=new ArrayList<>();
Thread t1=new Thread(new Producer(list));
Thread t2=new Thread(new Consumer(list));
t1.setName("生产者线程");
t2.setName("消费者线程");
//将t1和t2线程设置为守护线程
t1.setDaemon(true);
t2.setDaemon(true);
t1.start();
t2.start();
try {
//主线程睡眠10秒
Thread.sleep(1000*10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//生产线程
class Producer implements Runnable{
private List<StudentX> list;
public Producer(List list){
this.list=list;
}
@Override
//一直生产
public void run() {
while(true) {
//保证线程安全(给list加锁)
synchronized(list) {
if(list.size()>10){
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//程序能够执行到这,说明集合未满
//继续生产
StudentX s=new StudentX(18, "贺子旗");
list.add(s);
System.out.println(Thread.currentThread().getName()+"生产了--->"+s.name);
//唤醒消费者
list.notify();
}
}
}
}
//消费线程
class Consumer implements Runnable{
private List<StudentX> list;
private int i=0;
public Consumer(List list){
this.list=list;
}
@Override
//一直消费
public void run() {
while(true){
synchronized(list) {
if(list.size()==0){
//集合空了,停止消费
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//程序能够执行到这说明集合未空
//创建一个0-10之间的随机整数
//int i = new Random().nextInt(10);
System.out.println(Thread.currentThread().getName()+"消费了--->"+list.get(i).name);
list.remove(i);
//唤醒生产者
list.notify();
}
}
}
}
class StudentX{
int age;
String name;
public StudentX(int age, String name) {
this.age = age;
this.name = name;
}
}
生产者和消费者线程不一定要设置为守护线程,视具体情况而定。
码字不易,不要白嫖,觉得有用的,可以给我点个赞。感谢!
因技术能力有限,如文中有不合理的地方,希望各位大佬指出,在下方评论区留言,谢谢,希望大家一起进步,一起成长。
如需转载请注明来源,谢谢!