线程安全.

目录:

一、线程不安全的条件
二、怎么解决线程安全问题
    ——同步和异步的理解
    ——模拟两个线程对同一个账户取款(不安全的 并发取款)
    ——解决以上代码不安全问题  synchronized关键字:
    ——synchronized关键字出现在实例方法上:    
三、总结: synchronized的三种写法
四、哪些变量有线程安全问题
五、死锁(要会写 有可能面试写)
六、守护线程
七、计时器

一、线程不安全的条件

三个条件:
条件1:多线程并发。
条件2:有共享数据。(方法区和堆区资源是共享的,new 一个银行账户是在堆区开辟的对象内存)
条件3:共享数据有修改的行为。
满足以上3个条件之后,就会存在线程安全问题

线程安全._第1张图片

二、怎么解决线程安全问题

当多线程并发的环境下,有共享数据,并且这个数据还会被修改,此时就存在
    线程安全问题,怎么解决这个问题?
        线程排队执行。(不能并发)。
        用排队执行解决线程安全问题。
        这种机制被称为:线程同步机制。

        专业术语叫做:线程同步,实际上就是线程不能并发了,线程必须排队执行。
    怎么解决线程安全问题呀?
    使用"线程同步机制"。

        线程同步就是线程排队了,线程排队了就会牺牲一部分效率,没办法,数据安全
        第一位,只有数据安全了,我们才可以谈效率。数据不安全,没有效率的事儿。

同步和异步的理解

异步编程模型:
    线程t1和线程t2,各自执行各自的,t1不管t2,t2不管t1,
    谁也不需要等谁,这种编程模型叫做:异步编程模型。
    其实就是:多线程并发( 效率较高。)
    异步就是并发。
同步编程模型:
    线程t1和线程t2,在线程t1执行的时候,必须等待t2线程执行
    结束,或者说在t2线程执行的时候,必须等待t1线程执行结束,
    两个线程之间发生了等待关系,这就是同步编程模型。
    效率较低。线程排队执行。
    同步就是排队。 

模拟两个线程对同一个账户取款(不安全的 并发取款)

 账户类:

package com.bipowernode.javase.thread.threadsafe;
/*
    银行账户
    不使用线程同步机制,多线程对同一个账户进行取款,出现线程安全问题
 */
public class Account {
    // 属性
    // 账号
    private String actNo;
    // 余额
    private double balance;

    // 构造方法
    // 有参构造
    public Account(String actNo, double balance) {
        this.actNo = actNo;
        this.balance = balance;
    }


    // getter and setter

    public String getActNo() {
        return actNo;
    }

    public void setActNo(String actNo) {
        this.actNo = actNo;
    }

    public double getBalance() {
        return balance;
    }

    public void setBalance(double balance) {
        this.balance = balance;
    }

    // 取款的方法
    // 线程t1和t2并发这个取款的方法.....
    public void withdraw(double money){
        // 取款之前的余额
        double before =this.balance;
        // 取款之后的余额
        double after =before -money;    // 到此处用户已经取到钱了
        // 更新余额
        // 思考:t1执行到这里了,但还没有来得及执行这行代码,t2线程进来withdraw方法了 此时一定出问题
        // 假设t1走到这里还没有到更新余额(没更新账户余额还是10000) 那么t2也并发过来了 那么t1和t2都取到了5000 而且账户余额还剩5000

        try {
            // 睡眠5s (检验一定会出毛病)
            Thread.sleep(1000*5);   // 假设先进来的t1 让t1睡眠5s再更新余额(此时账户余额还是10000) 那么此时在更新余额睡眠的情况下t2线程过来调用取款方法 最终t1和t2都取了5000元 最终银行账户余额还有5000 就出安全问题了
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        this.setBalance(after);

    }

}

两个线程同时对同一个银行账户取款(线程并发):

package com.bipowernode.javase.thread.threadsafe;

public class ThreadAccount extends Thread{
    // 两个线程必须共享同一个账户对象
    private Account act;    // 把银行账户类指向act引用 

    // 通过构造方法传递过来账户对象
    public ThreadAccount(Account act) {
        this.act = act;
    }

    // 重写run()方法
    @Override
    public void run() {
        // 这里 当两个线程开启start的时候,就会并发这里面的代码(线程并发)
        // 这里跑的是子线程的代码
        // run()方法执行表示取款操作
        // 假设取款5000
        // 多线程并发这个取款方法
        double money =5000;
        act.withdraw(money);
        System.out.println("账户:"+act.getActNo()+"取款"+money+"成功"+"账户剩余:"+act.getBalance());


    }
}

程序测试:

package com.bipowernode.javase.thread.threadsafe;
// 程序测试

public class Test01 {
    public static void main(String[] args) {
        // 创建银行账户对象(只创建1个)
        Account account =new Account("act-001",10000);
        // 创建两个线程
        ThreadAccount ta1 =new ThreadAccount(account);
        ThreadAccount ta2 =new ThreadAccount(account);    // 当ta1线程更新账户余额不及时的时候 ta2拿到的也是10000元 二者都是10000元的情况下取5000 最终还剩5000 银行账户安全有问题了
        // 设置name
        ta1.setName("线程ta1");
        ta2.setName("线程ta2");

        // 启动线程取款
        ta1.start();
        ta2.start();
        
    }
}

输出结果:

线程安全._第2张图片

解决以上代码不安全问题  synchronized关键字:

银行账户类:

package com.bipowernode.javase.thread.threadsafe;
/*
    银行账户
 */
public class Account {
    // 账户
    private String actNo;
    // 账户余额
    private double money;

    // 构造方法
    public Account(String actNo, double money) {
        this.actNo = actNo;
        this.money = money;
    }

    // setter and getter方法

    public String getActNo() {
        return actNo;
    }

    public void setActNo(String actNo) {
        this.actNo = actNo;
    }

    public double getMoney() {
        return money;
    }

    public void setMoney(double money) {
        this.money = money;
    }

    // 取款的方法
    public void withdraw(double money){
        // 以下这几行代码必须是线程排队的,不能并发
        // 一个线程把这里的代码全部执行结束之后,另一个线程才能进来。
        /*
        线程同步机制的语法是:
            synchronized(){     // (线程共享对象)
                // 线程同步代码块
            }
            synchronized后面小括号中传的这个“数据”是相当关键的。
            这个数据必须是多线程共享的数据,才能达到多线程排队(如在此程序当中,因为两个线程t1和t2都是对同一个账户Account类进行取款操作的
            所以这个括号里面(线程共享对象)应该是this, 因为在Account类当中this代表Account这个对象)

            ()中写什么?
            那要看你想让哪些线程同步。
            假设t1、t2. t3. t4、t5 ,有5个线程
            你只希望t1 t2 t3排队, t4 t5不需要排队。怎么办?
            你一定要在()中写一个t1 t2 t3共享的对象。而这个
            对象对于t4 t5来说不是共享的。

            这里的共享对象是:账户对象。
            账户对象是共享的,那么this就是账户对象吧! ! !(从Test01测试代码当中可以看出当创建两个线程的时候,传的是同一个Account对象)
            不一定是this ,这里只要是多线程共享的那个对象就行。

            在java语言中,任何一个对象都有“一把锁”,其实这把锁就是标记。(只是把它叫做锁)
             [也就是说synchronized后面的括号是线程共享对象 而对象都有一把锁]
            100个对象, 100把锁。1个对象1把锁。

            以下代码的执行原理?
            1、假设t1和t2线程并发,开始执行以下代码的时候,肯定有一个先一个后。
            2、假设t1先执行了,遇到了synchronized,这个时候自动找“后面共享对象”的对象锁,
            找到之后,并占有这把锁,然后执行同步代码块中的程序,在程序执行过程中一直都是
            占有这把锁的。直到同步代码块代码结束,这把锁才会释放。
            3、假设t1线程已经占有这把锁,此时t2线程也遇到synchronized关键字,也会去占有后面
            共享对象的这把锁,结果这把锁被t1占有, t2只能在同步代码块外面等待t1的结束,
            直到t1把同步代码块执行结束了, t1会归还这把锁,此时t2终于等到这把锁,然后
            t2占有这把锁之后,进入同步代码块执行程序。【也就是说一个茅坑两个人抢着占用,先到的进去锁着门拉,结束后开锁第二个人关上门拉】
            这样就达到了线程排队执行。
            这里需要注意的是:这个共享对象一定要选好了。这个共享对象一定是你需要排队
            执行的这些线程对象所共享的。



         */
        synchronized (this){
            // 取款前账户余额
            double before =this.getMoney();
            // 取款后的余额
            double after =before -money;

            // 模拟更新账户余额延迟
            try {
                Thread.sleep(1000*5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 更新账户余额
            this.setMoney(after);
        }
    }
}


创建线程:

package com.bipowernode.javase.thread.threadsafe;

public class ThreadAccount extends Thread{
    // 拿账户对象
    private Account act;

    // 通过构造方法拿到账户对象
    public ThreadAccount(Account act){
        this.act =act;
    }

    public void setAct(Account act) {
        this.act = act;
    }

    // 重写run()方法 这里是子线程的运行
    @Override
    public void run() {
        // 进行取款的操作
        // 两个线程start之后,并发执行run()方法
        // 我们在这里模拟两者对账户余额的取款操作
        // 思考: 首先我们肯定要拿到对象才能调用取款的操作把
        // 调用银行账户的取款方法
            double money =5000;
            this.act.withdraw(money);
            System.out.println("账户:"+act.getActNo()+"取款"+money+"成功,"+"账户余额:"+act.getMoney());
        }

}


程序测试:

package com.bipowernode.javase.thread.threadsafe;
// 程序测试

public class Test01 {
    public static void main(String[] args) {
        // 创建账户对象
        Account account =new Account("act-01",10000);
        // 一个对象"一把锁" 这里new的Account是一个对象 里面两个线程 那么这两个线程见到synchronized关键字就需要进行抢锁排队
        // 当线程没有碰到synchronized关键字的时候不需要进行抢锁排队 
        // 创建两个线程
        ThreadAccount ta1 =new ThreadAccount(account);
        ThreadAccount ta2 =new ThreadAccount(account);

        Account account1 =new Account("act-02",10000);
        // 这里又重新创建了一个对象,那么这个对象就有"一把锁"供ta3线程使用
        // 创建线程
        ThreadAccount ta3 =new ThreadAccount(account1);

        // 设置name
        ta1.setName("线程ta1");
        ta2.setName("线程ta2");

        // 开启线程
        ta1.start();
        ta2.start();
        ta3.start();
        // 开启线程后,ta1和ta2线程会抢时间片 同时并发执行run()方法里面的代码
        // 我们这里让run()方法里面进行取款的操作
    }
}

输出结果:

线程安全._第3张图片

synchronized关键字出现在实例方法上:

package com.bipowernode.javase.thread.threadsafe;
/*
    银行账户
 */
public class Account {
    // 账户
    private String actNo;
    // 账户余额
    private double money;

    // 构造方法
    public Account(String actNo, double money) {
        this.actNo = actNo;
        this.money = money;
    }

    // setter and getter方法

    public String getActNo() {
        return actNo;
    }

    public void setActNo(String actNo) {
        this.actNo = actNo;
    }

    public double getMoney() {
        return money;
    }

    public void setMoney(double money) {
        this.money = money;
    }
    /*
        在实例方法上可以使用synchronized吗? 可以的
            synchronized出现在实例方法上,一定锁的是this
            没得挑,只能是this了 不能是其他的对象了
            所以这里方式不灵活

            另外还有一个缺点:synchronized出现在实例方法上,
            表示整个方法体都需要同步,可能会无故扩大同步的范围,
            导致程序的执行效率降低
     */

    // 取款的方法
    public synchronized void withdraw(double money){

            // 取款前账户余额
            double before =this.getMoney();
            // 取款后的余额
            double after =before -money;

            // 模拟更新账户余额延迟
            try {
                Thread.sleep(1000*5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 更新账户余额
            this.setMoney(after);
        }
}


 线程安全._第4张图片

三、总结: synchronized的三种写法

第一种:同步代码块
    灵活
    synchronized (线程共享对象) {
        同步代码块;
    }

第二种:在实例方法上使用synchronized
      表示共享对象一定是this
      并且同步代码块是整个方法体。执行效率会降低

第三种:在静态方法上使用synchronized
      表示找类锁。
      类锁永远只有1把。
      就算创建了100个对象,那类锁也只有一把

      
      对象锁: 1个对象1把锁,100个 对象100把锁。
      类锁: 100个对象,也可能只是1把类锁。
 

 四、哪些变量有线程安全问题

    java中的三大变量:
        静态变量:在方法区
        局部变量:在栈中
        实例变量:在堆中
    
    以上三大变量中:
    局部变量永远都不会存在线程安全问题(因为开辟线程就是重新开辟一个栈,多线程就是多个栈执行代码)
    因为局部变量不共享。(一个线程一个栈。)
    局部变量在栈中。所以局部变量永远都不会共享。
    实例变量在堆中,堆只有1个。
    静态变量在方法区中,方法区只有1个。
    堆和方法区都是多线程共享的,所以可能存在线程安全问题。

线程安全._第5张图片

五、死锁(要会写 有可能面试写)

package com.bipowernode.javase.thread;
/*
死锁代码一定要会写
一般面试的时候要求你会写
只有会写,才能在以后的开发中注意到这个事儿
因为死锁很难调试

 */
public class DeadLock {
    public static void main(String[] args) {
        // 创建两个对象
        Object o1 =new Object();
        Object o2 =new Object();
        // 创建两个线程
        // my和my1两个线程同时共享o1和o2对象
        MyThreadL my =new MyThreadL(o1,o2);
        MyThreadL1 my1 =new MyThreadL1(o1,o2);

        // 开启线程
        my.start();
        my1.start();

    }
}

class MyThreadL extends Thread{
    Object o1;
    Object o2;
    // 通过构造方法拿到o1和o2对象
    public MyThreadL(Object o1,Object o2){
        this.o1 =o1;
        this.o2 =o2;
    }
    @Override
    public void run() {
        synchronized (o1){

            // 模拟一下5s延迟
            try {
                Thread.sleep(1000*5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (o2){

            }
        }

    }
}

class MyThreadL1 extends Thread{
    Object o1;
    Object o2;
    // 通过构造方法拿到o1和o2对象
    public MyThreadL1(Object o1,Object o2){
        this.o1 =o1;
        this.o2 =o2;
    }
    @Override
    public void run() { // 因为两个线程是共享o1和o2的 同时并发o1,o2
        synchronized (o2){
            // 模拟5s延迟 让线程1和线程2的锁僵持住
            // (也就是 第一个线程执行5s后需要锁o2 而o2锁被第二个线程占用着等待着第一个线程o1锁释放,那么就僵持住了)
            try {
                Thread.sleep(1000*5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (o1){

            }
        }
    }
}

线程安全._第6张图片

 六、守护线程

守护线程
java语言中线程分为两大类:
    一类是:用户线程
    一类是:守护线程(后台线程)

    其中具有代表性的就是:垃圾回收线程(守护线程)。
守护线程的特点:
    一般守护线程是一个死循环,所有的用户线程只要结束,守护线程自动结束。

    (假设保洁阿姨,当我们开会时制造的垃圾[用户线程运行]的时候保洁阿姨[守护线程]开始打扫卫生,
    咱们离开会场后保洁阿姨也离开了会场)
    
    注意:主线程main方法是一个用户线程。
守护线程用在什么地方呢?
    每天00:00的时候系统数据自动备份。
    这个需要使用到定时器,并且我们可以将定时器设置为守护线程。
    一直在那里看着,每到00:00的时候就备份一次,所有的用户线程如
    果结束了,守护线程自动退出,没有必要进行数据备份了

当没有被守护都是两个线程的时候输出结果: (一直死循环)

线程安全._第7张图片

当成守护后,(用户线程结束,守护线程也结束):

t1.setDaemon(true); 方法
package com.bipowernode.javase.thread;

// 守护线程
public class ThreadTest10 {
    public static void main(String[] args) {
        // 创建备份数据线程对象
        BakDataTread bak =new BakDataTread();
        // 创建线程对象
        Thread t1 =new Thread(bak);
        t1.setName("备份数据的线程");
        // 设置为守护    [当用户线程main结束后,守护线程虽然是死循环但也会跟着结束]
        t1.setDaemon(true);

        // 开启线程
        t1.start();

        // 主线程(用户线程)
        for (int i=0;i<=9;i++){
            System.out.println("用户线程 ---->"+i);
            // 睡1s
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }


    }
}


// 假设一个备份数据的线程
class BakDataTread implements Runnable{

    @Override
    public void run() {
        // 死循环
        int i=0;
        while (true){
            System.out.println(Thread.currentThread().getName()+"----> "+i++);
            // 睡一秒
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

 七、计时器

Timer类当中的
schedule(定时任务,第一次执行的时间,间隔多久执行一次);方法
定时任务:(TimerTask task)  是一个实现Runnable的抽象类 

定时器的作用:
间隔特定的时间,执行特定的程序。
    每周要进行银行账户的总账操作。
    每天要进行数据的备份操作。

在实际的开发中,每隔多久执行一段特定的程序,这种需求是很常见的,
那么在java中其实可以采用多种方式实现:
    可以使用sleep方法,睡眠,设置睡眠时间,没到这个时间点醒来,执行
    任务。这种方式是最原始的定时器。
    (比较low)
在java的类库中已经写好了一个定时器: jalya.util.Timer,可以直接拿来用。
不过,这种方式在目前的开发中也很少用,因为现在有很多高级框架都是支持
定时任务的。
在实际的开发中,目前使用较多的是spring框架中提供的SpringTask框架,
这个框架只要进行简单的配置,就可以完成定时器的任务。

package com.bipowernode.javase.thread;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;

// 使用定时器指定定时任务
public class ThreadTest11 {
    public static void main(String[] args) throws ParseException {
        // 创建定时器对象
        Timer timer =new Timer();
        // Timer timer1 =new Timer(true);   // 守护线程的方式

        // 指定定时任务
        // timer.schedule(定时任务,第一次执行的时间,间隔多久执行一次);


        // 设置第一次执行的时间
        // 假设指定到 2022-4-6 11:10:30 开始第一次的执行
        SimpleDateFormat simpleDateFormat =new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date FirstTime =simpleDateFormat.parse("2022-4-6 11:22:30");

        // 指定定时任务    (也可以用匿名内部类)
        timer.schedule(new LogTimer(),FirstTime,1000*10);

        // 定时任务:(TimerTask task)  是一个实现Runnable的抽象类     TimerTask task =new LogTimer(); // 多态

    }
}

// 编写一个定时任务类
// 假设这是一个记录日志的定时任务
class LogTimer extends TimerTask{
    @Override   // 要重写抽象类当中的抽象方法
    public void run() {
        // 编写你需要执行的任务就行了
        SimpleDateFormat simpleDateFormat =new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date date =new Date();
        String s =simpleDateFormat.format(date);

        System.out.println(s + "成功完成了一次数据备份~");
    }
}

线程安全._第8张图片

你可能感兴趣的:(笔记,java)