Java学习:锁、线程死锁与安全相关探究

2个月前发布 gsjqwyl
14 0 0

文章标题:

Java学习:关于锁、线程死锁及安全的相关剖析

文章内容:

目录

一、对象的锁属性

1.作用

2.实现

2.1线程单代码块竞争锁

2.2锁对象判持有层信息

3.修饰

二、线程卡住

死锁

解决方法

三、线程安全2

1.不安全原因

2.措施


一、对象的锁属性

Java中每个类的实例对象都拥有一个自带属性的对象头内存区域,其中包含着锁相关的属性。

1.作用

该锁属性的作用是对相对应的代码块进行调度,使得同一时间只有单线程能够执行此代码块。

2.实现

2.1线程单代码块竞争锁

一个锁对象初始时持有层数为零,此时只有单线程能够竞争到该锁去执行对应代码块。当线程竞争到锁进入一层代码块时,持有层数就会增加一层,当执行完该层代码块退出时,持有层数就会减少一层。而且竞争到锁的代码块能够为其子代码块可重入地共享锁。

2.2锁对象判持有层信息

锁对象会依据线程代码块的持有层数信息来进行判断。当持有层数为非零时,该线程代码块受锁保障;当持有层数为零时,该线程代码块会处于阻塞状态,从而获得竞争锁的机会。

3.修饰

(1)synchronized修饰静态方法时,是以类对象为锁对象来对方法的整个代码块进行加锁。

(2)synchronized修饰非静态方法时,是以this本实例对象为锁对象来对方法的整个代码块进行加锁。

class Counter {
    public int count;

    //increase1、2等效:
    synchronized public void increase1() {
        count++;
    }
    public void increase2() {
        synchronized (this) {
            count++;
        }
    }

    //increase3、4等效:
    synchronized public static void increase3() {

    }
    public static void increase4() {
        synchronized (Counter.class) {

        }
    }
}

二、线程卡住

当遇到阻碍时尝试寻找其他路径进行单路线搜索解决问题,当没有其他路径或者其他路径出现循环阻碍情况时,就会卡住无法解决。

死锁

当锁导致的阻碍出现循环阻塞情况时,就会形成死锁。

解决方法

通过删除嵌套锁的代码结构或者统一锁的获取顺序,就能够破坏锁请求的环路结构,从而解决死锁问题。例如:线程统一先获取小锁再获取大锁。

class Test {
    private static Object locker1 = new Object();
    private static Object locker2 = new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (locker1) {
                // sleep 原先确保 t1和t2都分别拿到loker1和loker2后 再进行后续动作 发现死锁
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                synchronized (locker2) {
                    System.out.println("加锁成功!");
                }
            }
        });
        Thread t2 = new Thread(() -> {
            synchronized (/*locker2->*/locker1) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                synchronized (/*locker1->*/locker2) {
                    System.out.println("加锁成功!");
                }
            }
        });
        t1.start();
        t2.start();
    }
}

三、线程安全2

当一个线程在读取数据时,另一个线程进行写入操作,此时读取线程会忽略这种写入情况。

线程安全1:多线程对同一变量写指令的互碎拆

1.不安全原因

数据读取流向

(硬盘->)内存->寄存器->cpu

读硬盘到内存相对读内存到寄存器很慢,读内存到寄存器相对读寄存器到cpu又很慢】

编译器为了提高代码执行效率,在保证单线程视野逻辑不变的前提下会调整生成优化执行的代码内容,但在多线程环境下,可能会出现因视野缺陷带来的错误优化:在保障的单线程视野下,读取无变化的内存到寄存器时可能会简化为只首次读取不变的一次,而略去了外线程修改内存后的读取。

其中,主内存是真正的固定位置的内存,工作内存是不固定的存储数据的“非内存”空间,通常包含各种cpu寄存器和缓存。

2.措施

使用volatile关键字能够确保不会对内存进行略读,保障了内存的可见性。

class Test {
    private static volatile int isQuit = 0;// volatile 确定不会对isQuit进行内存略读

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while (isQuit == 0) {
                // 循环体里啥都没干,此时意味着这个循环,一秒钟就会执行很多很多次
                // 编译器在单线程视野内 对内存无变的isQuit 可能会 只首读不变的一次,以略读来提升效率
            }
            System.out.println("t1 退出!");
        });
        t1.start();

        Thread t2 = new Thread(() -> {
            System.out.println("请输入 isQuit: ");
            Scanner scanner = new Scanner(System.in);
            // 一旦用户输入的值不为0 就会使t1线程执行结束
            isQuit = scanner.nextInt();
        });
        t2.start();
    }
}
© 版权声明

相关文章

没有相关内容!

暂无评论

none
暂无评论...