Java多线程2(线程安全)

线程安全问题

一 为什么要进行线程同步?

java允许多线程并发控制,当多个线程同时操作一个可共享资源变量时(如对其进行增删改查操作),会导致数据不准确,而且相互之间产生冲突。所以加入同步锁以避免该线程在没有完成操作前被其他线程调用,从而保证该变量的唯一性和准确性。

二 使用synchronized关键字解决线程安全问题

共享数据,多个线程共同操作的变量,比如买票问题中的tickets就是共享数据。
同步监视器就是锁,锁必须是这几个线程共享的,唯一的。当t1线程拿到这个锁时,t2t3线程就拿不到了。

1.同步代码块

synchronized(同步监视器)
{需要同步的代码}

1.1
继承Thread类的方法,使用同步代码块解决线程不安全,慎用this。因为继承方式是造了三个Thread类的对象,那就对应了三把锁。

synchronized (obj),可以在类中构造静态对象保证唯一性
synchronized(Thread.class),可以直接使用类名作为锁

1.2
Runnable接口的方法,因为Thread类的唯一性,锁比较灵活。

例如:synchronized (this)

2.同步方法(同步函数)

2.1
继承Thread类的方法,使用同步方法解决线程不安全
同步方法依然需要使用同步监视器,只是不需要显示的声明

1.非静态的同步方法:同步监视器相当于this
2.静态的同步方法:同步监视器相当于类本身,类.class
————————————————————————————————————————
private static synchronized void run()
假如直接对run()方法上锁,run()方法存在与Thread继承类中,实现时可能造多个对象不唯一,static相当于加了类锁,保证了唯一性

2.2
Runnable接口的方法,因为Thread类的唯一性,可以直接上锁

public synchronized void run()

三 练习

1.继承Thread类,同步代码块

/**
 * @author Tony Stark
 * @create 2022-08-24 21:58
 * 继承Thread类的方法,使用同步代码块解决线程不安全,慎用this。
 *
 * 说白了,这把锁必须是这几个线程共享的,唯一的。当t1线程拿到这个锁时,t2t3线程就拿不到了
 * 如果这里使用this,因为继承方式是造了三个Thread类的对象,那就对应了三把锁。
 *
 *synchronized(同步监视器)
 *      {需要同步的代码}
 * 共享数据,多个线程共同操作的变量,比如tickets就是共享数据
 * 同步监视器:就是锁。任何一个类的对象都可以充当锁。
 */

class MyThread extends Thread{
    private static int tickets = 100;

    private static Object obj = new Object();

    @Override
    public void run(){
        while(true){
            synchronized (obj){
            //方法二:synchronized (Thread.class) {
                //这里不能使用this。可以使用类名,或者将一个static的obj对象作为锁
                if (tickets > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    System.out.println(Thread.currentThread().getName() + "售票,票号 " + tickets);
                    tickets--;
                } else {
                    break;
                }
            }
        }
    }
}
public class window1 {
    public static void main(String[] args){
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();

        t1.setName("t1");
        t2.setName("t2");
        t3.setName("t3");

        t1.start();
        t2.start();
        t3.start();
    }
}

2.继承Thread类,同步方法

/**
 * @author Tony Stark
 * @create 2022-08-24 22:34
 * 继承Thread类的方法,使用同步方法解决线程不安全
 *
 * 同步方法依然需要使用同步监视器,只是不需要显示的声明
 * 1.非静态的同步方法:同步监视器相当于this
 * 2.静态的同步方法:同步监视器相当于类本身,类.class
 */
class MyThread2 extends Thread{
    private static int tickets = 100;


    @Override
    public void run() {
        while (true) {
            show();//这里要注意,不是不能把run()写成synchronized方法,而是这里不能把run方法里面的while包进去
        }
    }
    private static synchronized void show(){
//继承的时候如果使用同步方法,需要把方法设置为静态
        if (tickets > 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println(Thread.currentThread().getName() + "售票,票号 " + tickets);
            tickets--;
        }
    }

}
public class window2 {
    public static void main(String[] args){
        MyThread2 t1 = new MyThread2();
        MyThread2 t2 = new MyThread2();
        MyThread2 t3 = new MyThread2();

        t1.setName("t1");
        t2.setName("t2");
        t3.setName("t3");

        t1.start();
        t2.start();
        t3.start();
    }
}

3.继承Runnable接口,使用同步代码块和同步方法

/**
 * @author Tony Stark
 * @create 2022-08-23 8:23
 * 静态方法是类锁,不论创建了几个对象,类锁只有一把
 * 继承接口的方法,使用同步代码块和同步方法两种解决办法解决线程安全问题
 */
class Window implements Runnable{
    private int tickets = 100;
    @Override
    public synchronized void run(){//这等同于同步代码块,同步监视器是this
        while(true){
            //while如果在代码块里面,某一个线程抢到锁之后会把票买完再释放锁。
            //synchronized (this) {
                if (tickets > 0) {

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    System.out.println(Thread.currentThread().getName() + "售票"
                            + ",票号为 " + tickets);
                    tickets--;
                } else {
                    break;
                }
           // }
        }
    }
}
public class window3{
    public static void main(String[] args){
        Window w = new Window();

        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();
    }
}

四 实例:账户取钱问题

1.创建一个账户Account类(账号,余额,构造器,getset方法,取款withdraw方法)
2.创建AccountThread继承Thread,取款是多线程操作 (定义Account属性,构造器接收参数,重写run方法)
3.Test

线程安全:

1.Account类
public class Account{
    private String acton;//账号
    private double balance;
    public Account(){
    }
    public Account(String acton,double balance){
        this.acton = acton;
        this.balance = balance;
    }
    public String getAct(){
        return acton;
    }
    public void setAct(String acton){
        this.acton = acton;
    }
    public double getBalance(){
        return balance;
    }
    public void setBalance(double balance){
        this.balance = balance;
    }

    Object obj = new Object();
    Object obj2 = new Object();
    /*Account类只有一个对象act,那么自然只有一个Object对象obj1或obj2
    obj1或者obj2都是共享对象,当t1拿到锁,t2就不会进入synchronized锁住的代码
    所以和this一样,obj1和obj2可作为参数传入synchronized()
    */


    public void withdraw(double money){
        Object obj3 = new Object();
        //withdraw在run()方法中,t1线程会创建一个obj3对象,t2也会,所以obj3不是共享对象,不能作为参数
        synchronized (obj) {
            double before = this.getBalance();
            double after = before - money;
            //在这里模拟网络延迟,100%会出现问题
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //t1线程执行到了这里,还没有来得及执行这行更新余额的代码,t2就进入了withdraw方法
            this.setBalance(after);
        }
    }
    /*
    实例变量:在堆中,堆只有一个
    静态变量:在方法区中,方法区只有一个
    局部变量:在栈中

    局部变量与常量永远不会共享,堆和方法区都是多线程共享的,可嫩存在线程安全问题
     */
}
/**
 * 这是用继承的方式创建的,只不过传入了一个参数Account
 */
public class AccountThread extends Thread{
    private Account act;
    public AccountThread(Account act){
        //act对象只有一个,两个线程对这一个账户操作
        this.act = act;
    }
    public void run(){
        double money = 5000;
        act.withdraw(money);
        /*
        这里也可以
        synchronized(act){act.withdraw(money)}
        但是效率更低,是act不能是this,AccountThread两个对象,对应两把锁
         */
        System.out.println("线程"+ Thread.currentThread().getName()+
                "账户"+act.getAct()+ " 取款成功,余额 "+act.getBalance());

    }

}
public class Test {
    public static void main(String args[]){
        Account act = new Account("act-001",10000);

        //下面将继承类实例化的时候使用了多态
        Thread t1 = new AccountThread(act);
        Thread t2 = new AccountThread(act);

        t1.setName("t1");
        t2.setName("t2");

        t1.start();
        t2.start();
    }
}

五 解决线程安全问题的另一种方式:Lock锁

参考文章

/**
 * 解决线程安全问题的方式三:Lock锁  --- JDK5.0新增
 *
 * 1. 面试题:synchronized 与 Lock的异同?
 *   相同:二者都可以解决线程安全问题
 *   不同:synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器
 *        Lock需要手动的启动同步(lock()),同时结束同步也需要手动的实现(unlock())
 *
 * 2.优先使用顺序:
 * Lock  同步代码块(已经进入了方法体,分配了相应资源)  同步方法(在方法体之外)
 
 */
class Window implements Runnable{

    private int ticket = 100;
    //1.实例化ReentrantLock
    private ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        while(true){
            try{

                //2.调用锁定方法lock()
                lock.lock();

                if(ticket > 0){

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    System.out.println(Thread.currentThread().getName() + ":售票,票号为:" + ticket);
                    ticket--;
                }else{
                    break;
                }
            }finally {
                //3.调用解锁方法:unlock()
                lock.unlock();
            }

        }
    }
}

public class LockTest {
    public static void main(String[] args) {
        Window w = new Window();

        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值