java个人笔记-多线程并发下,数据的安全问题

目 录

  • 多线程并发下,数据的安全问题
    • 引言
    • 什么时候存在安全问题?
    • 怎么解决线程安全问题
    • 同步编程模型和异步编程模型
      • 同步代码块例题
      • 面试题
      • 死锁代码实例
    • synchronized的三种写法
    • 实际开发中怎么解决线程安全问题?
  • 线程的其他内容
    • 守护线程
    • 定时器
    • 实现线程的第三种方式
    • Java中的生产者和消费者模式
    • wait()和notify()方法
    • 生产者消费者模式

多线程并发下,数据的安全问题

引言

我们编写的程序需要放到一个多线程的环境下运行,更需要关注的是这些数据在多线程环境下运行是否安全。

什么时候存在安全问题?

多行程在以下三个条件存在时会存在安全问题

条件1.多线程并发
条件2.有共享数据
条件3.共享数据有修改行为

Tip:
  局部变量是不存在安全问题的,因为永远不会共享(局部变量在栈中)
  常量也不存在安全问题,因为不可被修改
  堆和方法区都是多线程共享的,所以可能存在线程安全问题
  
如果使用局部变量的话:
建议使用StringBuilder,因为局部变量不存在线程安全问题

怎么解决线程安全问题

解决线程的安全问题可以通过线程排队执行(不能并发)
这种机制被称为:线程同步机制

同步编程模型和异步编程模型

异步编程模型
线程t1和线程t2,各自执行各自的,谁也不用管谁
异步(并发,效率较高)

同步编程模型
线程t1和线程t2,在线程t1(t2)执行的时候,必须等待t2(t1)执行结束
俩个线程之间发生等待关系
同步(排队,效率低,安全)

同步代码块
关键字synchronized() 线程同步机制,线程排队执行
括号中的数据必须是多线程共享的数据
在java语言中,任何一个对象都有“一把锁”(标记)

  1. 假设t1和t2线程并发,并且共享同一个对象,开始执行代码的时候,肯定一个先一个后,
  2. 假设t1先执行,遇到了synchronized,这时候自动找“共享对象”的对象锁,找到之后,并占有这把锁,然后执行同步代码块中的程序,在执行过程中一直都是占有这把锁的,直到同步代码块结束,这把锁才会释放。
  3. 假设t1已经占有这把锁,此时t2也遇到synchronized,也会去占有共享对象的这把锁,但是t1已经占有了这把锁,所以t2只能等待t1执行结束后才能占有这把锁。

同步代码块例题

代码举例:
创建银行账户类:

public class Account {
    private String name;
    private double money;

    public Account() {
    }

    public Account(String name, double money) {
        this.name = name;
        this.money = money;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public double getMoney() {
        return money;
    }

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

    //取款方法
    public void withdraw(double money){
        //同步代码块
        //关键字synchronized 线程同步机制,线程排队执行
        //括号中的数据必须是多线程共享的数据
        //这里的共享对象是账户对象
        synchronized (this) {
            Double befor = this.money;
            double after = befor - money;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.setMoney(after);
        }
    }
}

创建线程来模拟取款

public class ThreadMoney extends Thread{
    private Account act;

    //通过构造方法传递过来的账户对象
    public ThreadMoney(Account act) {
        this.act = act;
    }
    public void run(){
        double money = 5000;
        act.withdraw(money);
        System.out.println(Thread.currentThread().getName() +
                "取了" + money + ",剩余" + act.getMoney());
    }
}

测试类:创建两个线程同时取款

public class Test {
    public static void main(String[] args) {
        Account act=new Account("lisi",10000);
        //线程t1
        Thread t1=new ThreadMoney(act);
        //线程t2
        Thread t2=new ThreadMoney(act);
        t1.start();
        t2.start();
    }
}

结果:

Thread-0取了5000.0,剩余5000.0
Thread-1取了5000.0,剩余0.0

t1和t2在执行期间进行了线程同步(排队)
若没有同步,可能会出现以下情况:

Thread-0取了5000.0,剩余5000.0
Thread-1取了5000.0,剩余5000.0

原因是什么呢?
t1、t2同时执行,同时取钱(异步),这时候原来有10000,二者都取了5000之后才setMoney()修改余额,
两次修改都是从10000——>5000,所以最后返回余额当然是5000(但实际上两者都取了5000)。
这就肯定不对了。所以必须要用到synchronized线程同步机制

面试题

面试题:问t1线程启动后,t2线程会不会等待t1线程先执行完再执行?
不会,因为t2线程执行的是doOther方法,doOther方法并没有被synchronized锁住

代码如下:

public class Test1 {
    public static void main(String[] args) {
        Myclass mc=new Myclass();
        Thread t1=new MyThread(mc);
        Thread t2=new MyThread(mc);
        t1.setName("t1");
        t2.setName("t2");
        t1.start();
        try {
            Thread.sleep(1000);//保证t1先执行
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t2.start();
    }
}

class MyThread extends Thread{
    private Myclass myclass;

    public MyThread(Myclass myclass) {
        this.myclass = myclass;
    }

    public void run(){
        if(Thread.currentThread().getName().equals("t1"))
            myclass.doSome();
        if(Thread.currentThread().getName().equals("t2"))
            myclass.doOther();
    }
}
class Myclass{
    public synchronized void doSome(){
        System.out.println("doSome begin");
        try {
            Thread.sleep(1000*5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("doSome over");
    }

    public void doOther(){
        System.out.println("doOther begin");
        System.out.println("doOther over");
    }
}

死锁代码实例

俩个线程t1、t2同时共享o1,o2两个对象
t1先锁o1,再锁o2
t2先锁o2,再锁o1
此时如果t1锁住o1,t2锁住o2,会陷入僵持。

代码实例:

public class Deadlock {
    public static void main(String[] args) {
        Object o1=new Object();
        Object o2=new Object();

        Thread t1=new MyThread1(o1,o2);
        Thread t2=new MyThread2(o1,o2);
        t1.start();
        t2.start();
    }
}

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

            }
        }
    }
}

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

            }
        }
    }
}

synchronized的三种写法

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

第二种:
 在实例方法上使用synchronized
  表示共享对象一定是this
  并且同步代码块一定是方法体

第三种:
 在静态方法上使用synchronized
  表示找类锁
  类锁永远只有一把

实际开发中怎么解决线程安全问题?

第一种:尽量使用局部变量代替“实例变量和静态变量”

第二种:如果必须是实例变量,那么可以考虑创建多个对象,这样实例变量的内存就不共享了。(一个线程对应一个对象)

第三种:如果不能使用局部变量,对象也不能创建多个,这时候就只能选择synchronized了(线程同步机制)

线程的其他内容

守护线程

java语言中线程分为两大类:
一类是:用户线程
一类是:守护线程(后台线程)
其中具有代表性的是:垃圾回收机制(守护线程)

守护线程的特点:
一般守护线程是一个死循环,所有的用户线程只要结束,守护线程自动结束在守护线程执行前(.start())写上:setDaemon(true);

定时器

作用:间隔特定的时间,执行特定的程序
java中已经写好了一个定时器:java.util.Timer(开发中很少用)
实际开发中使用较多的是Spring框架中提供的SpringTask框架。

实现线程的第三种方式

实现Callable接口(JDK8新特性)
这种方式实现的线程可以获取线程的返回值。
之前两种:继承Thread、继承Runnable 这两种方法没有返回值。

Java中的生产者和消费者模式

wait()和notify()方法

  1. wait和notify方法不是线程对象的方法,是java中任何一个java对象
    都有的方法
  2. wait() 方法作用:
    Object o=new Object();
    o.wait();
    表示:让正在o对象上活动的线程进入等待状态,无期限等待,直到被唤醒
  3. notify() 方法的作用:
    Object o=new Object();
    o.notify();
    表示:让正在o对象上等待的线程被唤醒
    o.notifyAll();
    表示:唤醒o对象上处于等待的所有线程

生产者消费者模式

什么是“生产者和消费者模式”?
 生产线程负责生产,消费线程负责消费。
 生产线程和消费线程要达到均衡
 这是一种特殊的业务要求,在这中特殊的情况下使用wait方法和notify方法
java个人笔记-多线程并发下,数据的安全问题_第1张图片

wait和notify方法建立在线程同步基础之上,因为多线程要同时操作一个仓库,有线程安全问题。

模拟这样一个需求:
仓库采用List集合
List集合中假设只能存1个元素。
1个元素表示仓库满了
如果List集合中元素个数为0就表示仓库空了。

生产一个消费一个

代码实例:

生产线程:

//生产线程
class Producer implements Runnable{
    //仓库
    private List list;

    public Producer(List list) {
        this.list = list;
    }

    @Override
    public void run() {
        //一直生产(死循环模拟一直生产)
        while (true){
            //给仓库对象上锁
            synchronized (list) {
                if (list.size() > 0) {//大于0说明仓库满了(最多存一个)
                    //暂停生产
                    //当前线程进入等待状态,并且释放list集合的锁
                    try {
                        list.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                //若仓库没满,可以生产
                Object obj=new Object();
                list.add(obj);
                System.out.println(Thread.currentThread().getName()+"--->"+obj);
                //唤醒消费者进行消费
                list.notify();
            }
        }
    }
}

消费线程:

//消费线程
class Consumer implements Runnable{
    //仓库
    private List list;

    public Consumer(List list) {
        this.list = list;
    }
    @Override
    public void run() {
        //一直消费
        while (true){
            //给仓库对象上锁
            synchronized (list){
                //仓库若已经空了
                if(list.size()==0){
                    //暂停消费
                    //当前线程进入等待状态,并且释放list集合的锁
                    try {
                        list.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                //若仓库没空,进行消费
                Object obj=list.remove(0);
                System.out.println(Thread.currentThread().getName()+"--->"+obj);
                //唤醒生产者生产
                list.notify();
            }
        }
    }
}

测试类:

public class ThreadTest {
    public static void main(String[] args) {
        //创建一个仓库对象
        List list=new ArrayList();
        //创建两个线程对象
        //生产者
        Thread t1=new Thread(new Producer(list));
        //消费者
        Thread t2=new Thread(new Consumer(list));
        t1.setName("生产者线程");
        t2.setName("消费者线程");
        t1.start();
        t2.start();
    }
}

实现结果:
生产一个消费一个,并不多生产,也不超消费。

生产者线程--->java.lang.Object@eb6ad02
消费者线程--->java.lang.Object@eb6ad02
生产者线程--->java.lang.Object@34ac4d0c
消费者线程--->java.lang.Object@34ac4d0c
生产者线程--->java.lang.Object@46369de7
消费者线程--->java.lang.Object@46369de7
…………

你可能感兴趣的:(java个人笔记-多线程并发下,数据的安全问题)