线程安全问题

本文探讨了线程安全的重要性,尤其是在服务器环境中,分享了数据并发问题的条件、线程同步解决方案,如synchronized的使用技巧,以及如何通过对象锁定避免死锁。通过银行取款模拟实例和死锁案例解析,提供了解决线程安全问题的方法和常见陷阱。
摘要由CSDN通过智能技术生成

1.为什么线程安全是重点?

项目是在服务器中运行,而线程的定义、线程对象的创建、线程的启动并不需要我们编写代码。我们需要关注的是,自己编写的程序在多线程并发的环境下是否安全。

2.什么时候数据在多线程并发的情况下会出现安全问题?

条件1:多线程并发
条件2:有共享数据
条件3:共享数据有修改行为
同时具备以上三个条件就会出现线程安全问题。

3.怎么解决线程安全问题?

用线程排队执行解决线程安全问题,这种机制称为:“线程同步机制”,简称线程同步,提高安全性降低效率。

4.线程异步和线程同步

异步编程模型:线程t1和线程t2各自执行各自的,谁也不需要等谁,实际上就是多线程并发。
同步编程模型:线程t1和线程t2需要互相等待,同步就是排队,效率较低。

5.模拟银行取钱(不使用线程同步)

账户类:

public class Acount {
    private String user;//账户
    private double banlence;//余额
    public Acount() {
    }
    public Acount(String user, double banlence) {
        this.user = user;
        this.banlence = banlence;
    }
    public String getUser() {
        return user;
    }
    public void setUser(String user) {
        this.user = user;
    }
    public double getBanlence() {
        return banlence;
    }
    public void setBanlence(double banlence) {
        this.banlence = banlence;
    }
    //取款方法
    public void withdrew(double money){
        //取款前的余额
        double before=this.getBanlence();
        //取款后的余额
        double after=before-money;
        //更新余额  当前线程还未设置余额时另外一个线程进入本方法就会出问题
        setBanlence(after);
    }
}

取款行为:

public class AcountThread extends Thread{
    //两个线程共享同一个账户
    private Acount act=null;
    public AcountThread(Acount act){
        this.act=act;
    }
    @Override
    public void run() {
        //取款5000
        act.withdrew(5000);
        System.out.println("账户"+act.getUser()+"取款成功!取款金额"+act.getBanlence());
    }
}

测试两个线程同时取款:

public class Test {
    public static void main(String[] args) {
        Acount acount=new Acount("张三",100000);
        Thread thread1=new AcountThread(acount);
        Thread thread2=new AcountThread(acount);
        thread1.start();
        thread2.start();
    }
}

6.使用线程同步机制synchronized 取款

//取款方法
    public void withdrew(double money){
        //以下代码应该排队,不能并发
        //取款前的余额
        /**
         * synchronized (这个括号中的参数十分重要) {
         * }
         *
         * 参数该怎么写?假设t1,t2,t3,t4,t5共用5个线程,只希望前三个排队,后两个并发。
         * 则参数应该写成t1,t2,t3共享的对象并且对于t4,t5来说不是共享的
         * 
         * synchronized的括号里面写共享对象!共享对象!共享对象!
         */
        synchronized (this) {
            double before = this.getBanlence();
            //取款后的余额
            double after = before - money;
            //更新余额  当前线程还未设置余额时另外一个线程进入本方法就会出问题
            setBanlence(after);
        }
    }

synchronized 原理:当线程遇到synchronized 关键字,就会去找共享对象的对象锁,找到之后占有这把锁,其他线程无法进入,直到执行完同步代码块释放锁,下一个等待的线程就可以进入。
稍作修改:当obj作为一个实例变量的时候,也是被共享的,并且每个账户只有一个obj(若obj为局部变量就不可以,因为每次调方法就会new一个新对象),所以可以作为synchronized 的参数。哪怕写个字符串“abc”都行,因为这是在字符串常量池中共享且唯一的。

public class Acount {
    private String user;//账户
    private double banlence;//余额
    Object obj=new Object();
    .....
    //取款方法
    public void withdrew(double money){
        synchronized (obj) {
            double before = this.getBanlence();
            //取款后的余额
            double after = before - money;
            //更新余额  当前线程还未设置余额时另外一个线程进入本方法就会出问题
            setBanlence(after);
        }
    }
}

7.哪些变量有线程安全问题【重要】

1.静态变量---->存储在方法区【共享,不安全】
2.实例变量---->存储在堆内存【共享,不安全】
3.局部变量---->存储在栈内存【不共享,安全,一个线程一个栈】

所以,局部变量和常量不会有线程安全问题,成员变量可能会有线程安全问题。

8.synchronized 放在实例方法上

当synchronized 放在实例方法上,锁的一定是this,不可能是其他对象,所以这种方式不灵活;而且整个方法体都会同步,可能会无故扩大同步的范围,导致程序的执行效率较低。
如果共享的对象是this,而且要同步的代码块是整个方法体,那么建议使用这种方式。

 public synchronized  void withdrew(double money){
            double before = this.getBanlence();
            //取款后的余额
            double after = before - money;
            //更新余额  当前线程还未设置余额时另外一个线程进入本方法就会出问题
            setBanlence(after);
    }

例如StringBuffer线程安全,StringBuilder线程不安全,所以在使用局部变量时,由于局部变量也是线程安全的,所以可以使用StringBuilder。
ArrayList是非线程安全的
Vector是线程安全的
HashMap、Hashset是非线程安全的
HashTable是线程安全的

9.小练习

1.一个类中,一个方法有synchronized,一个方法没有synchronized,两个线程分别执行两个方法时是否会排队。
上锁对象:

class MyClass{
    public synchronized void doSome(){
        System.out.println("doSome---->begin");
        try {
            Thread.sleep(1000*10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("doSome---->end");
    }
    public void doOther(){
        System.out.println("doOther---->begin");
        System.out.println("doOther---->end");
    }
}

调用方法:

class MyThread extends Thread{
    private MyClass mc;
    public MyThread(MyClass mc){
        this.mc=mc;
    }
    @Override
    public void run() {
        if (Thread.currentThread().getName().equals("t1")){
            mc.doSome();
        }
        if (Thread.currentThread().getName().equals("t2")){
            mc.doOther();
        }
    }
}
public class Test01 {
    public static void main(String[] args) {
        MyClass myClass=new MyClass();
        MyThread myThread1 = new MyThread(myClass);
        MyThread myThread2 = new MyThread(myClass);
        myThread1.setName("t1");
        myThread2.setName("t2");
        myThread1.start();
        try {
            Thread.sleep(1000*5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        myThread2.start();
    }
}

答案:不会排队,doOther()方法没有synchronized 关键字,不会去找对象锁,直接执行。
2.将上述代码的doOther()方法加上synchronized 关键字,是否会排队
答案:会排队,两个方法锁住的是同一个对象mc,所以要排队。
3.doOther()方法加上synchronized 关键字并且实例化两个MyClass对象分别给两个线程,是否还会排队。
答案:不会排队,锁住的不是同一个对象。
4.将synchronized方法加上static关键字变为静态方法,依旧实例化两个MyClass对象给两个线程,会不会排队?
答案:会排队,静态方法是类锁,不管对象有多少个,类锁只有一把,需要等待。

class MyClass{
    public synchronized static void doSome(){
        System.out.println("doSome---->begin");
        try {
            Thread.sleep(1000*10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("doSome---->end");
    }
    public synchronized static void doOther(){
        System.out.println("doOther---->begin");
        System.out.println("doOther---->end");
    }
}
class MyThread extends Thread{
    private MyClass mc;
    public MyThread(MyClass mc){
        this.mc=mc;
    }
    @Override
    public void run() {
        if (Thread.currentThread().getName().equals("t1")){
            mc.doSome();
        }
        if (Thread.currentThread().getName().equals("t2")){
            mc.doOther();
        }
    }
}
public class Test01 {
    public static void main(String[] args) {
        MyClass myClass1=new MyClass();
        MyClass myClass2=new MyClass();
        MyThread myThread1 = new MyThread(myClass1);
        MyThread myThread2 = new MyThread(myClass2);
        myThread1.setName("t1");
        myThread2.setName("t2");
        myThread1.start();
        try {
            Thread.sleep(1000*5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        myThread2.start();
    }
}

10.什么是死锁?

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

11.产生死锁的条件

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

避免死锁:不要嵌套使用synchronized 。

public class DeadLock {
    public static void main(String[] args) {
        Object o1=new Object();
        Object o2=new Object();
        //两个线程共享o1,o2
        Thread t1= new myThread01(o1,o2);
        Thread t2= new myThread02(o1,o2);
        t1.start();
        t2.start();
    }
}

class myThread01 extends Thread{
    Object o1;
    Object o2;
    public myThread01(Object o1, Object o2) {
        this.o2 = o1;
        this.o2 = o2;
    }
    @Override
    public void run() {
        synchronized (o1){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (o2){
            }
        }
    }
}

class myThread02 extends Thread{
    Object o1;
    Object o2;
    public myThread02(Object o1, Object o2) {
        this.o1 = o1;
        this.o2 = o2;
    }
    @Override
    public void run() {
        synchronized (o2){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (o1){
            }
        }
    }
}

12.在开发中怎么解决线程安全问题

大量使用synchronized 会让程序效率降低,不到万不得已别用。
方案一:尽量使用局部变量代替实例变量和静态变量。
方案二:如果必须使用实例变量,那么尽量创建多个对象,这样对象不共享,一个线程一个对象。
方案三:如果不能使用局部变量也不能创建多个对象,那就使用synchronized (可以写在方法内,实例方法上,静态方法上)

13.守护线程

java中有两种线程:用户线程和守护线程,例如JVM的垃圾回收就是守护线程。用户线程一结束守护线程就会结束,哪怕守护线程是个死循环。
通过setDaemon(true)方法设置守护线程,要在线程开启之前。

 t1.setDaemon(true);
 t1.start();
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值