java线程安全问题分析、3种解决办法

本文探讨了在Java中多线程环境下卖票导致的线程安全问题,包括卖出不存在的票和卖出相同票的情况。通过分析问题原因,提出三种解决方案:同步代码块、同步方法和Lock锁(ReentrantLock)。同步代码块和同步方法通过对象锁确保同一时间只有一个线程执行关键代码,而Lock锁提供了更细粒度的控制,允许在执行完同步代码后立即释放锁,提高了程序效率。
摘要由CSDN通过智能技术生成

目录

一 线程不安全

1.1 代码

1.2 解析

解析--卖出不存在的票

解析--卖出相同的票

1.3 解决办法

二 解决方法1:同步代码块

2.1 代码

 2.2 解析

三 解决方法2:同步方法

3.1 代码

 3.2 说明

*四 解决方法3:Lock锁

代码


一 线程不安全

1个窗口卖100张票:单线程不会出现线程安全
3个窗口卖100张票:各卖各的不同票(多线程程序,没有访问共享数据,不会产生问题)
3个窗口卖100张票:可以卖相同的一张票(多线程访问了共享的数据,会产生安全问题)

1.1 代码


3个窗口卖100张票:可以卖相同的一张票(多线程访问了共享的数据,会产生安全问题)

 线程不安全的代码

package threadSafe;

/**
 * 实现卖票安全
 */
public class MyThreadSafe implements Runnable{
    private int ticketAccount = 100;
    @Override
    public void run() {
        while(ticketAccount>0){
            //为了提高线程出现安全的问题,睡眠一下,提高概率
            try {
                Thread.sleep(200);
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"正在卖第"+(100-ticketAccount+1)+"张票");
            ticketAccount--;
        }


    }
}
package threadSafe;


public class MainMethod {
    public static void main(String[] args) {

        //创建1个实现类,实现共享票源
        MyThreadSafe myThreadSafe =  new MyThreadSafe();
        //创建3个线程模拟3个窗口卖票
        Thread t0 = new Thread(myThreadSafe);
        Thread t1 = new Thread(myThreadSafe);
        Thread t2 = new Thread(myThreadSafe);
        t0.start();
        t1.start();
        t2.start();
    }
}

执行结果

卖出不存在的的票

卖出相同的一张票

1.2 解析


解析--卖出不存在的票


ticketAccount=1 剩最后一张票的时候:

t0线程抢到了CPU的执行权,进入循环体,sleep了
t2线程抢到了CPU的执行权,进入循环体,sleep了
t1线程抢到了CPU的执行权,进入循环体,sleep了

t2醒了抢到cpu的执行权,打印正在卖第100张票,执行ticketAccount--
ticketAccount=0,0不大于0,t2就停止循环了

t1醒了抢到cpu的执行权,在循环体里继续执行,此时ticketAccount=0,打印正在卖第101张票,执行ticketAccount--,
ticketAccount=-1,-1不大于0,t1就停止循环了

t0醒了抢到cpu的执行权,在循环体里继续执行,此时ticketAccount=-1,打印正在卖第102张票,执行ticketAccount--,
ticketAccount=-2,-2不大于0,t0就停止循环了

解析--卖出相同的票


t1 t2 同时执行到了正在卖第44张票,这时候还没执行ticketAccount--

1.3 解决办法


上述的线程安全问题不能使之出现,
我们可以让一个线程在访问共享数据的时候,(不管它有没有失去cpu的执行权),让其他线程只能等待,
等待当前线程执行完了,再允许其他线程继续执行.
保证始终一个线程在卖票就OK了

java引入了线程同步机制,有3种方法完成线程同步操作:
1 同步代码块
2 同步方法
3 锁机制

 

二 解决方法1:同步代码块

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

2.1 代码

package threadSafe;

/**
 * 实现卖票安全
 */
public class MyThreadSafe implements Runnable {
    private int ticketAccount = 100;

    //创建一个锁对象
    Object object = new Object();

    @Override
    public void run() {
        //同步代码块
        synchronized (object) {
            while (ticketAccount > 0) {
                //为了提高线程出现安全的问题,睡眠一下,提高概率
                try {
                    Thread.sleep(200);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "正在卖第" + (100 - ticketAccount + 1) + "张票");
                ticketAccount--;

            }
        }


    }
}

备注:main方法的代码同之前不变

 结果

 2.2 解析

同步技术的原理:

使用了一个锁对象,叫同步锁(也叫对象锁、对象监视器)
锁对象的作用:把同步代码块锁住,只让一个线程在同步代码块中执行

t0抢到了cpu的执行权,执行run()方法,遇到了同步码块,这时,t0会检查同步代码块是否有锁对象,
发现有,就会获取到锁对象,进入到同步中执行.

t1抢到了cpu的执行权,执行run()方法,遇到了同步码块,这时,t1会检查同步代码块是否有锁对象,
发现没有,t1就会进入到阻塞状态,会一直等待t0线程归还锁对象.
一直等到t0线程执行完同步中的代码,会把锁对象归还给同步代码块,t1才能获取到锁对象,进入到同步中执行.

总结:
同步中的线程,没有执行完毕,不会释放锁
同步外的线程,没有锁,进不去同步

但是会出现一个问题:程序频繁地判断锁,获取锁,释放锁,程序的效率会降低.

三 解决方法2:同步方法

格式:

public synchronized 返回值 method(){
    可能会产生线程安全的代码
}

3.1 代码

package threadSafe2;

/**
 * 实现卖票安全
 */
public class MyThreadSafe implements Runnable {
    private int ticketAccount = 50;

    @Override
    public void run() {
        //同步方法
        saleTicket();

    }

    //同步方法
    public synchronized void saleTicket() {
        while (ticketAccount > 0) {
            //为了提高线程出现安全的问题,睡眠一下,提高概率
            try {
               Thread.sleep(200);
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "正在卖第" + (50 - ticketAccount + 1) + "张票");
            ticketAccount--;

        }
    }

}

备注:main方法的代码同之前不变

结果

 3.2 说明

 同步方法也会把方法内部的代码锁住,只让一个线程执行.
同步方法的锁对象是谁?是new的实现类对象:new MyThreadSafe(),就是this
静态同步方法的锁对象是谁?是本类的class文件对象 (不能是this,this是创建对象之后产生的,静态方法优先于对象)

*四 解决方法3:Lock锁

提供了比 synchronized 更广泛的锁定操作

Lock接口中的方法:
    lock()
    unlock()

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

代码

package threadSafe3;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 实现卖票安全
 */
public class MyThreadSafe implements Runnable {
    private int ticketAccount = 80;

    //1 在成员位置创建一个ReentrantLock对象
    Lock lock = new ReentrantLock();
    @Override
    public void run() {
        //2 在可能会出现安全问题的代码前,调用Lock接口中的方法lock:加同步锁
        lock.lock();
        while (ticketAccount > 0) {
            //为了提高线程出现安全的问题,睡眠一下,提高概率
            try {
                Thread.sleep(200);

            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "正在卖第" + (80 - ticketAccount + 1) + "张票");
            ticketAccount--;
        }
        //3 在可能会出现安全问题的代码后,调用Lock接口中的方法unlock():释放锁
        lock.unlock();

    }



}

备注:main方法的代码同之前不变

结果

关于线程的状态和线程的通信请点这里

Java中使用多线程处理JDBC操作Oracle数据库时,可能会遇到一些常见问题。通常这些问题包括: 1. **资源安全**:如果没有正确地管理数据库连接(Connection)和Statement对象,多线程环境可能导致并发访问导致数据一致性问题。每个线程应该拥有自己的连接实例,并确保关闭不再使用的资源。 ```java ThreadLocal<Connection> connectionHolder = new ThreadLocal<>(); public Connection getConnection() { if(connectionHolder.get() == null) { try { connectionHolder.set DriverManager.getConnection("jdbc:oracle:thin:@your_url:port/service_name", username, password); } catch (SQLException e) { // handle exception } } return connectionHolder.get(); } ``` 2. **死锁**:如果线程间并发执行涉及锁的SQL语句,如SELECT FOR UPDATE,可能会发生死锁,特别是在事务处理中。 3. **线程安全性**:某些Oracle驱动(如ODBC)可能不是线程安全的,这意味着多个线程共享同一个DriverManager实例会引发异常。解决办法是为每个线程创建独立的DriverManager实例。 4. **连接池**:未充分利用数据库连接池技术(如Apache Commons DBCP、HikariCP等),可能会因频繁创建和销毁连接而降低性能。使用连接池可以更好地管理和复用连接。 5. **并发控制**:使用批处理(Batching)或多结果集(Multiple Result Sets)能提高性能,但如果处理不当,也会影响线程间的同步。 如果你遇到问题,可能需要检查错误日志,分析SQL语句执行顺序以及是否遵守了最佳实践。常见的错误代码如SQLException,如CONNECTION NOT FOUND或者ORA-00060等也需要特别关注。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值