多线程篇三

多线程篇三

如笔者理解有误,欢迎交流指正⭐


线程安全

划重点!!!

什么是线程安全

想对线程安全做一个具体清晰的解释很困难,为什么这么说?当然是具体情况具体分析.(就像很多人都喜欢吃面条 有的人喜欢吃炸酱 有的人喜欢吃刀削【其他不具体拓展 因为饿了w】可以具体细分)

但是!我们可以这样认为 如果多线程环境下代码运行的结果是符合我们预期的 ,即使在单线程环境应得的结果,则说明这个线程是安全的.

产生线程安全的原因
抢占式执行(系统内核)

上我们熟悉的实例代码

// 线程安全
public class Demo13 {
    // 此处定义一个 int 类型的变量
    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        Object locker = new Object();
        Object locker2 = new Object();

        Thread t1 = new Thread(() -> {
            // 对 count 变量进行自增 5w 次
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });
        Thread t2 = new Thread(() -> {
            // 对 count 变量进行自增 5w 次
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });

        t1.start();
        t2.start();
        
        t1.join();
        t2.join();

        // 预期结果应该是 10w
        System.out.println("count: " + count);
    }
}

我们的预期情况是输出100000 但是实际运行结果如下
在这里插入图片描述
img
不是预期值且可以发现多次运行的结果并不相同
这是为什么?

  for (int i = 0; i < 50000; i++) {
                count++;
}

这部分代码就是典型的线程安全问题.
t1和t2这两个单独的线程在单独执行过程中,毫无疑问没有任何问题.
但是t1和t2两个线程并发执行上述循环,会出现逻辑上的问题.

        t1.start();
        t2.start();
        
        t1.join();
        t2.join();

这样是t1和t2同时执行,多个线程执行上述代码时,由于线程之间的调度顺序是"随机"的,在某些调度顺序下会出现逻辑问题.

站在CPU的角度上,count++是由CPU的三个指令实现的.
1.load 把数据从内存中读取到CPU中
2.add 把寄存器中的数据进行+1
3.save 把寄存器中的数据保存到内存中
在上述3个步骤执行过程中,其实有无数种排列组合的方式.
img

img


上述代码执行完之后发现了bug
两个线程本应该是分别自增1次,但2个线程在自增过程中并没有累加而是各自独立运行,故预期得到2,实际只有1.【确实够"随机" 笑】
注意 我们得到的"随机值"一定小于100000但也存在小于50000的值(如果t1自增一次的过程中 t2自增多次【如2次】相当于自增了3次 只有一次生效了)
img
怎么样保持t1执行完再执行t2呢?
控制t1执行时t2未启动即可.

// 线程安全
public class Demo13 {
    // 此处定义一个 int 类型的变量
    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        Object locker = new Object();
        Object locker2 = new Object();

        Thread t1 = new Thread(() -> {
            // 对 count 变量进行自增 5w 次
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });
        Thread t2 = new Thread(() -> {
            // 对 count 变量进行自增 5w 次
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });

        // 如果没有这俩 join, 肯定不行的. 线程还没自增完, 就开始打印了. 很可能打印出来的 count 就是个 0
        t1.start();
        t1.join();

        t2.start();
        t2.join();

        // 预期结果应该是 10w
        System.out.println("count: " + count);
    }
}
同一个变量被两个线程都修改

解决方法:

1.一个线程对同一个变量进行修改 ok
2.两个线程针对不同变量进行修改
3.两个线程针对一个变量读取

非原子的修改操作
什么是原子性

比如我(线程A)登录一个手游(一段代码),在我未退出游戏时,我们的游戏好友(线程B)也可以登录游戏组队打本.(打断我在游戏中的隐私)这就是不具备原子性
听起来是不是和抢占式很像?真聪明hh
如果线程不是“抢占”的就算没有原子性

一个java语句不一定是原子的,也不一定只是一条指令

上述count++先进行 了读取再进行修改操作
若一段逻辑中存在要根据一定条件决定是否修改也存在类似的问题

解决方法:想办法让count++一次性被被CPU完成上述3个步骤

内存可见性问题

可见性指的是一个线程共享变量值的修改能被其他线程看到
Java内存模型(JMM)Java虚拟机规范了Java内存模型
目的是屏蔽掉各种硬件和操作系统的内存访问差异 以实现Java程序在各种平台下都能达到一致的并发效果
img
线程之间共享变量存储在主内存中
每一个线程都有自己的"工作内存"
当线程要读取一个共享变量的时候 会先把变量从主内存拷贝到工作内存中 再从工作内存读取数据
当线程要修改一个共享变量的时候 会先修改工作内存中的副本 再同步回主内存
**工作内存像是一个"枢纽" 其实就是CPU的寄存器和高速缓存 **

指令重排序问题

比如我们用代码实现
1.去茶话弄取餐
2.去菜鸟拿快递
3.去买曹氏

如果是在单线程情况下,JVM、CPU指令集会对其进行优化,不止是规规矩矩按1->2->3执行,而是可以按1->3->2执行(其他顺序也可以)这就叫指令重排序.

如何解决上述问问题呢?
简单暴力:加锁!

解决线程安全问题的方法
synchronized关键字
特性一互斥

synchronized会起到互斥效果,某个线程执行到某个对象的synchronized中时,其他线程如果也执行到同一个对象的synchronized
synchronized用的锁存在于Java对象里.

synchronized () {
         count++;
    }

()中需要表示一个用来加锁的对象.对象是什么不重要,只是通过这个对象区分两个线程是都在竞争一个同一个锁

如果两个线程是针对同一个对象加锁 就会出现锁竞争

如果不是争对同一个对象进行加锁就是正常的并发执行

 for (int i = 0; i < 50000; i++) {
                synchronized (locker) {
                    count++;
                }
            }

这样写两个线程的执行顺序就会相互影响 可以理解为**“并发执行"转变为"串行执行”**(两个线程同时尝试对一个对象加锁,出现锁竞争,一个线程能拿到线程袭击执行,另一个线程只能阻塞等待.等前一个线程释放锁之后,才机会拿到锁继续执行.)
img

特性二刷新内存

synchronized工作过程:
1.获得互斥锁
2.从主存拷贝变量的最新副本到工作内存中
3.执行代码
4.将更改后的共享变量的值刷新到主内存
5.释放互斥锁

synchronized也能保存内存可见性【待考证 扒源码一探究竟😀】

特性三可重入

synchronized同步块对同一条线程来说是可重入的,不会出现把自己锁死的情况.(指的就是一个线程连续对一把锁加锁两次不会出现死锁即为"可重入")
让锁记录哪个线程让它被锁住 后续再加锁的时候 如果加锁线程就是持有锁的线程就直接加锁成功
比如说你翘课了 刚好老师点名你不在 你就被老师标记了
下一次老师点名肯定不会拒绝会再点一次你(无恶意 问就是血泪史)
如果换成一个经常坐前排的同学那就不会是这个结果了.

public class Demo15 {
    private static Object locker = new Object();

    public static void func1() {
        synchronized (locker) {
            func2();
        }
    }

    public static void func2() {
        func3();
    }

    public static void func3() {
        func4();
    }

    public static void func4() {
        synchronized (locker) {

        }
    }


    public static void main(String[] args) {

    }
}

tips

无论有多少层都是要在最外层才能释放锁
可以引用计数器来确定有多少层或者记录是否真的释放锁成功(计数器值为0即可)

死锁

1.一个线程针对一把锁连续加锁两次,如果不是可重入锁,就是死锁.
比如把钥匙锁在屋里了进门又需要钥匙就为死锁.


public class Lock {
    public static Object locker = new Object();
    public static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for(int i = 0; i < 50000; i++) {
                synchronized (locker) {
                    synchronized (locker) {
                        count++;
                    }
                }
            }
        });
 
        Thread t2 = new Thread(() -> {
           for(int i = 0; i < 50000; i++) {
               synchronized (locker) {
                   count++;
               }
           }
        });
 
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }

2.两个线程,两把锁(无论是不是可重入锁都会死锁)


public class Lock2 {
    public static Object A = new Object(), B = new Object();
 
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
           synchronized (A) {
               //sleep一下,是给t2时间,让t2也能拿到B
               try {
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
               //尝试获取B,并没有释放A
               synchronized (B) {
                   System.out.println("t1拿到了两把锁");
               }
           }
        });
 
        Thread t2 = new Thread(() -> {
           synchronized (B) {
               //sleep一下,是给t1时间,让t1能拿到A
               try {
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
               //尝试获取A,并没有释放B
               synchronized (A) {
                   System.out.println("t2 拿到了两把锁");
               }
           }
        });
 
        t1.start();
        t2.start();
    }
}

出现死锁是因为两个线程都卡在了获取对方已经得到锁的位置.
n个线程得到m把锁
OS课上老师讲过的哲学家就餐问题

死锁的四个必要条件

1.互斥使用
一个线程拿到锁A另一个线程也想拿到锁A,就需要阻塞等待.

2.不可抢占
一个线程拿到锁之后其他线程想拿到这个锁只能等待线程A释放这个锁

3.请求保持
一个线程拿到锁A之后,在获取A的基础上还想获取锁B.(吃着碗里的看着锅里的)

4.循环等待
两个或多个线程都想互相获取对方的锁,都进入等待队列.(最容易被破坏的 但修改加锁顺序可避免 )

解决死锁的方案也就是破坏上述四个条件任何一个即可
1)引入一个额外的锁
2)去掉一个线程
3)引入计数器
4)引入加锁规则【比较推荐 普世性高->sychronized】

synchronized的使用方法
1)修饰代码块

锁任意对象

public class Demo {
    private Object locker = new Object();
    
    public void method() {
        Synchronized (locker) {
        
        }
    }

锁任意对象

public class Demo {
    
    public void method() {
        Synchronized (this) {
        
        }
    }
2)修饰实例方法

注意 锁对象是什么不重要 重要的是两个线程中的对象是否是一个对象

class Counter {
    public int count;
    
    synchronized public void increase() {
        //此时使用this作为锁对象
        count++;
    }
    public void increase() {
    synchronized(this) {
        count++;
    }
  }
}

3)修饰静态方法

针对类对象加锁

synchronized public static void increase1() {
    
}

public static void increase2() {
    synchronized(Counter.class) {
        //Couter.class ->类对象
    }
}

注意 类对象在一个java进程中是唯一的(代码中写了一个Counter的类对象,不会有多个)

Java标准库中的新城安全机制

Java 标准库中很多都是线程不安全的. 这些类可能会涉及到多线程修改共享数据, 又没有任何加锁措施.
如:

ArrayList,LinkedList,HashMap,TreeMap,HashSet,TreeSet,StringBuilder

有一些是线程安全的.使用了一些锁机制来控制.
如:

Vector(不推荐使用),HashTable(不推荐使用),ConcurrentHashMap,StringBuffer(不推荐使用)

还有的是没有加锁,但因为不涉及修改的特殊类,也是线程安全的.

String
Java标准库中的新城安全机制

Java 标准库中很多都是线程不安全的. 这些类可能会涉及到多线程修改共享数据, 又没有任何加锁措施.
如:

ArrayList,LinkedList,HashMap,TreeMap,HashSet,TreeSet,StringBuilder

有一些是线程安全的.使用了一些锁机制来控制.
如:

Vector(不推荐使用),HashTable(不推荐使用),ConcurrentHashMap,StringBuffer(不推荐使用)

还有的是没有加锁,但因为不涉及修改的特殊类,也是线程安全的.

String

未完待续.❀❀❀

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值