0
点赞
收藏
分享

微信扫一扫

线程安全(Thread Safe)

认真的老去 2022-04-29 阅读 61

1.线程安全

线程不安全:单看代码“没有问题”的情况下,但结果是错误的(无法 100% 得到预期结果)

线程安全:代码的运行结果应该是 100% 符合预期

2.线程不安全现象出现的原因

1.站在开发者的角度

1)多个线程之间操作同一块数据了(共享数据)——不仅仅是内存数据

2)至少有一个线程在修改这块儿共享数据

多个线程中至少有一个对共享数据做修改(写)操作

2.系统角度

会出现线程安全原因:

我们的预期 r++ 或者 r-- 是一个原子性操作(全部完成 or 全部没完成),但实际执行起来,保证不了原子性,所以会出错。

系统角度分析出现线程不安全的原因

1.内存可见性问题

内存可见性:一个线程对数据的操作,很可能其他线程是无法感知的。甚至,某些情况下,会被优化成完全看不到的结果。

2.代码重排序导致的问题

我们的写程序,往往是经过中间很多环节优化的结果,并不保证最终执行的语言和我们写的语句是一模一样的。

所谓重排序就是:执行的指令和不书写指令并不一致。

JVM 规定了一些重排序的基本原则:happend-before 规则

简要的解释:JVM 要求,无论怎么优化,对于单线程的视角,结果不应该有改变。但并没有规定多线程环境的情况(并不是不想规定,而是不能规定)导致在多线程环境下可能出问题。

3.小结线程安全问题

1.所谓什么是线程安全

  1. 程序的线程安全:运行结果100% 符合预期(这个标准无法实操,只是为了理解)
  2. Java 语境下,常说某个类、对象是线程安全的:这个类、对象的代码中已经考虑了处理多线程的问题了,如果只是“简单”使用,可以不考虑线程安全的问题。ArrayList 就不是线程安全的。——ArrayList 实现中,完全没考虑过线程安全的任何问题。无法直接使用在多线程环境(多个线程同方式操作同一个ArrayList)

2.作为程序员如何考虑线程安全的问题

  1. 尽可能让几个线程之间不做数据共享,各干各的。就不需要考虑线程安全问题了。比如:4个线程虽然处理的是同一个数组,但提前划好范围,各做各的,就没问题了
  2. 如果非要有共享操作,尽可能不去修改,而是只读操作  static final int COUNT = ...;   即使多个线程同时使用这个 COUNT 也无所谓的
  3. 一定会出现线程问题了,问题的原因从系统角度讲:1)原子性被破坏了2)由于内存可见性问题,导致某些线程读取到了“脏数据”  3)由于代码重排序导致的线程之间关于数据的配合出问题了

所以,我们需要学习一些机制,目标和JVM 沟通,避免上述问题的发生

4.一些常见类的线程安全问题

ArrayList、LinkedList、PriorityQueue、TreeMap、TreeSet、HashMap、HashSet 、StringBuilder 都不是线程安全的。

ArrayList为什么不是线程安全的?——多个线程同时对一个 ArrayList 对象有修改操作时,结果会出错。

1.有没有共享数据:ArrayList对象共享,array属性和size属性是共享的

2.多线程有没有写操作?有。array[size] = e;size++; 也是修改

3.原子性(90%情况都是原子性破坏) or 可见性 or 代码重排序 ?

        array[size] = e;size++; 应该是原子的,但目前不是

线程安全:Vector、Stack、Dictionary、StringBuffer 。这几个类都是 Java 设计失败的产品。最好不要出现这些类。

5.锁(lock)

synchronized 锁        同步锁/monitor锁(监视器锁)

1.语法

  1. 修饰方法(普通、静态方法) => 同步方法        synchronized int add(...){...}
  2. 同步代码块        synchronized(引用){ ... }
public class Main {
    // sync 修饰普通方法
    public synchronized int add(){
        return 0;
    }

    // sync 修饰静态方法
    private synchronized static int sub(){
        return 0;
    }

    // sync 同步代码块
    public void method(){
        Object o = new Object();
        synchronized (o){

        }
    }

    // sync 同步代码块
    public void method1(){
        Object o = new Object();
        synchronized (Main.class){

        }
    }
}

2.锁

锁理论上,就是一段数据(一段被多个线程之间共享的数据)

所有两种状态:锁上(locked)、打开(unlocked)

尝试加锁的内部操作:

1.整个尝试加锁的操作已经被 JVM 保证了原子性

if(lock == false){ 
    // 说明这个锁没有被锁上   
    lock = true;// 当前线程把锁 
    lock return;// 正常向下执行一些语句
}
 // 如果锁已经 locked(true) 了

Queue<线程> 该锁的阻塞队列 queue = ... ;

queue.add(Thread.currentThread()); 等待这把锁被释放

// 既然我们无法获取到锁,所以就应该让出 CPU 

Thread.currentThread().state = 阻塞;

Thread.yield();  // 理解成让出 CPU

由此我们得出:

无锁:语句1;语句2;

有锁:语句1;sync(ref){ 语句2;... } 

语句1的执行时间 和 语句2的执行时间相隔很久。甚至极端情况下,语句2 再也不会被执行 都是有可能的。

3.释放锁

这个过程由系统保证了原子性

当多个线程 都有加锁操作时 并且 申请的是同一把锁时 ,会造成 加锁 代码(临界区) 解锁

临界区代码会互斥(互相排斥)着进行。和临界区的代码是不是同一份代码无关

public class Main {
    // 这个对象用来当锁对象
    static Object lock = new Object();

    static class MyThread1 extends Thread {
        @Override
        public void run() {
            synchronized (lock) {
                for (int i = 0; i < 1_0000; i++) {
                    System.out.println("我是张三");
                }
            }

            for (int i = 0; i < 1_0000; i++) {
                System.out.println("我是王五");
            }
        }
    }

    static class MyThread2 extends Thread {
        @Override
        public void run() {
            synchronized (lock) {
                for (int i = 0; i < 1_0000; i++) {
                    System.out.println("我是李四");
                }
            }

            for (int i = 0; i < 1_0000; i++) {
                System.out.println("我是赵六");
            }
        }
    }

    public static void main(String[] args) {
        Thread t1 = new MyThread1();
        t1.start();

        Thread t2 = new MyThread2();
        t2.start();
    }
}

临界区的代码是必须在持有锁的前提下才能执行的。

张三、李四是有锁的,王五、赵六是没有锁的

张三、李四是互斥的;张三赵六不是互斥的;王五、李四不是互斥的;张三、王五是一个线程的;李四、赵六是一个线程的

最终结果会是 先打印 张三,张三释放锁之后,才会打印李四,因为王五、李四不是互斥的,所以会是王五、李四交替打印,最后打印赵六。

小练习:

 s1 = new SomeClass();        s2 = new SomeClass();        s1 = s3

序号t1线程t2线程t1 是否加锁,哪把锁t2 是否加锁,哪把锁是否互斥
1

s1.m1()

s1.m1()

加锁

s1 指向的对象

加锁

s1 指向的对象

互斥
2

s1.m1()

s3.m1()

加锁

s1 指向的对象

加锁

s3 指向的对象

s1 == s3

互斥

3

s1.m1()

s2.m1()

加锁

s1 指向的对象

加锁

s2 指向的对象

s1 != s2

不互斥

4

s1.m1()

s1.m2()

加锁

s1 指向的对象

加锁

SomeObject.class对象

不是同一个对象

不互斥

5s1.m1()s1.m3()

加锁

s1 指向的对象

无加锁

不互斥
6s1.m1()s1.m4()

加锁

s1 指向的对象

加锁

s1 指向的对象

互斥
7s1.m1()s3.m4()

加锁

s1 指向的对象

加锁

s3 指向的对象

s1 == s3

互斥

8s1.m1()s1.m5()

加锁

s1 指向的对象

加锁

SomeClass.class 对象

不互斥
9s1.m2()s1.m5

加锁

SomeClass.class 对象

加锁

SomeClass.class 对象

互斥
10s1.m1()s2.m6()

加锁

s1.o1 指向的对象

加锁

s2.o1 指向的对象

不互斥
11s1.m6()s2.m6()

加锁

s1.o1 指向的对象

加锁

s2.o1 指向的对象

不互斥
12s1.m6()s3.m6()

加锁

s1.o1 指向的对象

加锁

s3.o1 指向的对象

s1 == s3

互斥

13s1.m2()s1.m7()

加锁

SomeClass.clss 对象

加锁

SomeClass.o2 加锁

不互斥
14s1.m7()s1.m7()

加锁

SomeClass.o2 加锁

加锁

SomeClass.o2 加锁

互斥
15s1.m7()s3.m7()

加锁

SomeClass.o2 加锁

加锁

SomeClass.o2 加锁

互斥
16s1.m7()s2.m7()

加锁

SomeClass.o2 加锁

加锁

SomeClass.o2 加锁

互斥

6.synchronized加锁操作

加锁操作使得互斥(synchronized 和我们一起配合(我们需要正确地使用 synchronized))

synchronized的作用:原子性、可见性、重排序

1.保证了临界区的原子性

这两种加锁的方式,加锁的“粒度”不同

左边:加锁粒度较细                                                        右边:加锁粒度较粗

粗略的,加锁粒度越细,并发性可能性越高。粒度不是越粗越好,也不是越细越好。

 

 这种加锁是不正确的,两个指向的对象不同,不是一把锁。达不到我们想做到的互斥(原子性的保证)

用 sync 来保证原子性,需要通过正确的加锁,来保证代码间的互斥。

2.synchronized 在有限程度上可以保证内存可见性

临界区期间的数据读写,不做保证:1.可能读的数据,再次被别的线程更改了,就看不到了   2.期间是否有数据同步回主内存

3.synchronized 也可以给代码重排序增加一定的约束

s1;s2;s3 加锁;s4;s5;s6;解锁;s7;s8;s9;

s1、s2、s3 之间不做保证

s4、s5、s6 之间不做保证

s7、s8、s9 之间不做保证

s4、s5、s6 不会重排序到加锁之前的,解锁之后的

s1、s2、s3 不会到加锁之后

s7、s8、s9 不会到解锁之前的

7.lock锁

java.util.concurrent,locks.Lock;

 // 加锁,但允许被中断;加锁失败后返回false
 // 相较于sync 锁,加锁策略更灵活
 void lock();
 void lockInterruptibly();
 boolean tryLock();
 boolean tryLock(long time, TimeUnit unit) throw InterruptedException;
 // 解锁操作
 boolean unlock();

java 中方法的结束以两种形式出现:1.正常结束,并返回 2.异常结束,抛出异常

几种情况可能导致该方法结束?

1.超时时间内,加锁成功了,正常返回,返回true

2.超时时间到了,加锁失败,正常返回,返回false

3.超时时间内,加锁还没成功 但是 线程被终止了,异常返回,捕获到 InterruptedException

8.ReentrantLock 锁

Lock lock = new ReentrantLock();
lock.lock();
try {
    // 临界区代码
} finally {
    lock.unlock();  // 将unlock 放到finally 里,确保任何情况下都能解锁
}

9.synchronized 锁 VS juc 下的锁

synchronized 锁:代码的书写就保证一定有锁的释放

sync(锁){...}        只有一种类型的锁        一直请求锁

juc 下的锁:是可能忘记写 lock.unlock() 导致锁一直不释放的

                书写更灵活:可以一个方法加锁,到另一个方法中解锁

                锁的类型更加灵活:公平锁、非公平锁、读写锁、独占锁、可重入锁、不可重入锁

                锁的加锁策略更灵活:1.一直请求锁 2.带中断 3.尝试请求 4.带超时的尝试

10. 线程状态——阻塞状态

(blocked、waiting、timed_waiting)

blocked:专指请求 synchronized 锁失败时的状态

waiting              VS                timed_waiting

lock.lock()                             一般带时间

lock.lockInterruptly               Thread.sleep(...)、t.join(5000)、lock.tryLock(5s)

t.join()

11.状态转移图

举报

相关推荐

0 条评论