多线程安全

一、线程不安全

1. 共享变量

  • 多个线程对共享变量的并发修改,导致结果并不像预期中那样
package com.nike.erick.d03;

public class Demo01 {
    private static int number = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread firstThread = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100000; i++) {
                    number++;
                }
            }
        });

        Thread secondThread = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100000; i++) {
                    number--;
                }
            }
        });

        firstThread.start();
        secondThread.start();

        firstThread.join();
        secondThread.join();

        System.out.println("number: " + number);
    }
}

2. 原因

2.1 字节码指令

  • 线程拥有自己的工作栈内存,读数据时会从主内存中拿,写完后会将数据写回主存
  • i++在实际执行的时候,是一系列指令,一系列指令就会导致指令交错

在这里插入图片描述

2.2 指令交错

  • 指令交错存在于多线程之间
  • 线程上下文切换,引发不同线程内指令交错,最终导致上述操作结果不会为0

在这里插入图片描述

2.3 概念

  • 线程不安全: 只会存在于多个线程共享的资源
  • 临界区: 对共享资源的多线程读写操作的代码块
  • 竞态条件: 多个线程在在临界区内,由于代码的指令交错,对共享变量资源的争抢
多线程  读    共享资源  没问题
多线程  读写  共享资源  可能线程不安全(指令交错)

二、synchronized

  • 一种阻塞式解决线程安全问题的方案,包含几种不同的写法

1. 同步代码块

1.1 基本语法

  • 对象锁,只要为同一个对象,为任意对象(除基本类型)
- synchronized: 一种阻塞式的,用来解决多线程访问共享资源引发的不安全的解决方案
- synchronized: 可在不同代码粒度进行控制
- synchronized: 保证了《临界区代码的原子性(字节码)》,不会因为线程的上下文切换而被打断
- synchronized: 必须保证对对同一个对象加锁(Integer.value(0))
package com.nike.erick.d03;

public class Demo01 {
    private static int number = 0;

    /*任何对象都可以,只要保证是引用类型*/
    private static Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        /*粗粒度的锁
          细粒度的锁:也可以加在每个for循环中*/
        Thread firstThread = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lock) {
                    for (int i = 0; i < 100000; i++) {
                        number++;
                    }
                }
            }
        });

        Thread secondThread = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lock) {
                    for (int i = 0; i < 100000; i++) {
                        number--;
                    }
                }
            }
        });

        firstThread.start();
        secondThread.start();

        firstThread.join();
        secondThread.join();

        System.out.println("number: " + number);
    }
}

1. 2. 原理

1. a线程获取锁,执行代码
2. 其他线程这个时候要进入,无法获取锁资源,就会被block,进入  《等待队列》
   同时进入上下文切换
   
3. a线程执行完毕后,释放锁资源。唤醒其他线程,进行cpu的争夺

在这里插入图片描述

1.3 面向对象改进

package com.nike.erick.d03;

public class Demo03 {
    private static Room room = new Room();

    public static void main(String[] args) throws InterruptedException {

        Thread firstThread = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 1000; i++) {
                    room.decrement();
                }
            }
        });

        Thread secondThread = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 1000; i++) {
                    room.increment();
                }
            }
        });

        firstThread.start();
        secondThread.start();
        firstThread.join();
        secondThread.join();

        System.out.println("result:" + room.getValue());
    }
}

class Room {
    private int counter;

    // 锁对象是当前对象
    public void increment() {
        synchronized (this) {
            counter++;
        }
    }

    public void decrement() {
        synchronized (this) {
            counter--;
        }
    }

    public int getValue() {
        synchronized (this) {
            return counter;
        }
    }
}

2. 同步方法

2.1 成员方法

  • 同步成员方法和同步代码块效果一样,必须保证同步代码块的锁对象是this对象
  • 可能锁粒度不太一样
  • 同步方法的锁对象是this,即当前对象
@Data
class Calculator {
    private int number;

    public void incr() {
        synchronized (this) {
            for (int i = 0; i < 10000; i++) {
                number++;
            }
        }
    }
    // 同步方法
    public synchronized void decr() {
        for (int i = 0; i < 10000; i++) {
            number--;
        }
    }
}

2.2 静态成员方法

  • 锁对象:锁用的是类的字节码对象: Calculator.class
@Data
class Calculator {
    private static int number;

    public int getNumber() {
        return number;
    }

    public static void incr() {
        for (int i = 0; i < 10000; i++) {
            synchronized (Calculator.class) {
                number++;
            }
        }
    }

    public static synchronized void decr() {
        for (int i = 0; i < 10000; i++) {
            number--;
        }
    }
}

3. 锁对象

  • 同步代码块,必须保证用的锁是同一个对象,但是不能为基本数据类型
  • 同步成员方法的锁对象是this
  • 同步静态成员方法的锁对象是当前类的字节码对象 .class
  • 如果多个线程对共享变量读写,但是部分线程没有加锁保护,依然线程不安全

三、线程安全场景

1. 成员变量/静态变量

1. 没被多线程共享:        则线程安全
2. 被多线程共享:
     2.1 如果只读,   则线程安全
     2.2 如果有读写, 则可能发生线程不安全问题

2. 局部变量

2.1 线程安全

每个线程的方法都会创建单独的栈内存,局部变量保存在自己当前方法的栈桢内
局部变量线程私有

1. 局部变量是基础数据类型时: 是线程安全的
2. 但局部变量是应用类型时:   可能不安全
  2.1 如果该对象没有逃离方法的作用访问,则线程安全
  2.2 如果该对象逃离方法的作用范围,则可能线程不安全 《引用逃离》

在这里插入图片描述

2.2 引用逃离

  • 如果一个类不是final类,那么就可能被继承
  • 被继承的时候发生方法覆盖,覆盖方法如果创建新的线程,就可能发生局部变量不安全
// 安全
package com.nike.erick.d01;

import java.util.ArrayList;
import java.util.List;

public class Demo05 {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        SafeCounter safeCounter = new SafeCounter();
        safeCounter.operation(list);
        System.out.println(list);
    }
}

class SafeCounter {
    public void operation(List<String> list) {
        for (int i = 0; i < 10000; i++) {
            addElement(list);
            deleteElement(list);
        }
    }

    public void addElement(List<String> list) {
        list.add("HELLO");
    }

    public void deleteElement(List<String> list) {
        list.remove(0); // 移除元素
    }
}
package com.nike.erick.d01;

import java.util.ArrayList;
import java.util.List;

public class Demo05 {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        UnsafeCounter unsafeCounter = new UnsafeCounter();
        unsafeCounter.operation(list);
        System.out.println(list);
    }
}

class SafeCounter {
    public void operation(List<String> list) {
        for (int i = 0; i < 10000; i++) {
            addElement(list);
            deleteElement(list);
        }
    }

    public void addElement(List<String> list) {
        list.add("HELLO");
    }

    public void deleteElement(List<String> list) {
        list.remove(0); // 移除元素
    }
}

class UnsafeCounter extends SafeCounter {
    @Override
    public void deleteElement(List<String> list) {
        /*开启了新的线程来改变*/
        // index out of bound
        // 相当于把删除的操作延迟或提前了
        new Thread(() -> list.remove(0)).start();
    }
}
  • 避免线程安全类变为不安全类: 不要让一个类的方法被重写
  • 具体做法:final修饰禁止继承,或对可能引起安全的方法加private

3. 线程安全类

3.1 JDK

  • 多个线程同时调用他们同一个实例的方法时,线程安全
  • 线程安全类中的方法的组合,不一定线程安全
- String
- Integer
- StringBuffer
- Random
- Vector
- Hashtable
- java.util.concurrent包下的类
package com.nike.erick.d03;

import java.util.Hashtable;
import java.util.concurrent.TimeUnit;

public class Demo04 {

    /*共享资源*/
    private static Hashtable<String, String> hashtable = new Hashtable<>();

    public static void main(String[] args) throws InterruptedException {

        Thread firstThread = new Thread(() -> combinedMethod(hashtable));

        Thread secondThread = new Thread(() -> combinedMethod(hashtable));

        firstThread.start();
        secondThread.start();
        firstThread.join();
        secondThread.join();
        System.out.println(hashtable.size());
    }

    // 方法的组合不是 线程安全的
    // 如果要线程安全,必须将组合方法也设置成 synchronized
    public static void combinedMethod(Hashtable<String, String> hashtable) {
        if (null == hashtable.get("name")) {
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            hashtable.put("name", "erick");
            System.out.println(Thread.currentThread().getName());
            hashtable.put(Thread.currentThread().getName(), "00");
        }
    }
}

在这里插入图片描述

3.2 不可变类

  • 类中属性都是final,不能修改
  • 如 String,Integer
#  实现线程安全类的问题
- 无共享变量
- 共享变量不可变
- synchronized互斥修饰
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值