【操作系统】线程安全相关问题总结

简介: 【操作系统】线程安全相关问题总结

1.线程不安全的原因:

(下面这段代码:)

class Counter{
    public int count = 0;
    public void increase(){
        count++;
    }
}
public class Demo1 {
    private static Counter counter = new Counter();
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        Thread thread2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println("Counter:"+ counter.count);
    }
}
  1. 在写多线程代码的时候,就需要考虑到在任意一种调度的情况下都是能够运行出正确的结果的
  2. 抢占式执行,线程不安全的重要原因。多个线程的调度执行过程是随机的,内核上也是这样实现的,没有办法修改!
  3. 多个线程同时修改一个变量,必须要满足多个线程、同时和修改这三个操作就会造成线程的不安全。有的时候可以调整代码从这里来下手解决线程安全问题,但是普适性不高。
  4. 线程操作不是原子的,原子表示不可分割的最小单位。解决线程安全最常见的方法就是把多个操作通过特殊的手段打包成一个原子操作。count++这样的操作本质上是3个CPU指令,第一是把内存的数据读取到CPU寄存器上;第二是把CPU的寄存器中的值进行+1;第三是把寄存器中的值写回到内存中。而CPU执行指令都是以一个指令为单位进行执行,一个指令就相当于CPU上的最小单位,一个指令如果执行一半就不会被调度走
  5. 内存可见性问题,在JVM优化的背景下引入的bug。
  6. 指令重排序,在JVM优化的背景下引入的bug。

2.如何让代码的线程安全:

  1. 想办法把多个操作变成一个原子操作,怎么让count++变成原子?加锁!在count++之前先加锁,在count++之后再解锁。加锁和解锁之间只有当前调度的线程才可以修改,别的线程只能阻塞等待BLOCKED状态
  2. 举个例子:俩个男生追同一个女生,A追到了,B就要阻塞等待直到女生分手,此时就发生了锁竞争。这里的A和B就是俩个线程锁对象就是女生。再举个例子:还是俩个男生追不同的女生,A追到了女生1,A并不影响B追女生2,这种A和B的行动是互不干扰的,不存在锁竞争。
  3. 如何给对象加锁呢,这里用到关键字synchronized,当进入方法的时候加锁,方法执行完毕解锁。锁具有独占特性,如果当前锁没人来加,加锁操作就会成功;如果当前锁已经被人加上了,加锁操作就会阻塞等待
  4. 本来线程调度就是随机的过程,一旦俩个组的load、add、save交织在一起就会产生线程安全问题。使用了锁之后,这俩组就能够串行执行了,一个执行完才会轮到下一个。这样就避免了线程不安全的问题。这个加锁的操作把并发变成了串行,会减慢代码的执行效率!
  5. 加完锁不是说CPU一口气就执行完了,中间也是有可能会有调度切换,即使t1切换走了,t2仍然是BLOCKED状态无法再在CPU上运行。线程调度出CPU,但是锁没释放,还是不能运行。
  6. 线程安全不是加了锁就一定安全,而是通过加锁让并发修改同一个变量=>串行修改同一个变量,在才会安全。如果加锁的方式不对那么不一定能解决线程安全问题。
  7. 如果只给一个线程加锁就不会涉及到锁竞争也就不会由并发修改=>串行修改。A加了锁,B就乖乖的阻塞等待,遵守规则才是安全的!
  8. 锁的代码越多叫做锁的粒度越大/越粗,锁的代码越少叫做锁的粒度越小/越细。
  9. synchronized的其他用法:synchronized的其他用法: 1.修饰方法。则锁对象就是this!
public synchronized void increase(){
        count++;
}
  1. 2.修饰代码块,可以把进行加锁的逻辑放到synchronized代码块中,也可以起到加锁作用。 如果一个方法中,有些代码需要加锁,有些不需要就可以使用这种修饰代码块的方式 格式synchronized(),括号里面填写的东西是要针对哪个对象加锁(被用来锁的对象称为锁对象) 写法1:
synchronized(this){
    //大部分情况下无脑写this没问题,具体看使用的常场景
}
写法2:
public Object locker = new Object();
public void int increase(){ 
    synchronized(locker){ 
        count++; 
    } 
}
写法3:
public static Locker locker = new Locker();
public void int increase(){
    synchronized(locker){
        count++;
    }
}
  1. 在Java中任意对象都可以在synchronized里面作为锁对象。写多线程代码的时候,不关心这个锁对象究竟是谁是哪种形态,只是关心俩个线程是否是锁同一个对象,只有锁同一个对象才有竞争,锁不同的对象就没有竞争。
  2. 锁对象只是用来控制线程之间的互斥的,是针对同一个对象加锁就会出现互斥,针对不同对象加锁就不会互斥。

3.synchronized的使用:

无论锁对象是什么形态是什么类型,核心原则都是俩个线程争一个锁对象就有竞争不同锁对象就没竞争。有锁竞争的目的是为了保证线程安全

方法1.synchronized里面写的锁对象是this,谁调用了increase,就是针对谁加锁。下面这俩个线程都是针对counter对象进行加锁,因为是在针对同一个对象加锁所以这俩个线程执行的时候会出现互斥的情况。

class Counter{
    public int count;
    public void increase(){
        synchronized (this){
            count++;
        }
    }
}
public class Demo3 {
    public static Counter counter = new Counter();
    public static void main(String[] args) {
        Thread t1 = new Thread(()-> {
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        Thread t2 = new Thread(()-> {
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("counter="+counter.count);
    }
}

方法2.线程t1里面的increase里的this代表counter,此时是针对counter加锁。线程t2里面的increase里的this代表counter2,此时是针对counter2加锁。这俩线程对不同的对象加锁,这里就不会出现锁竞争就不会出现阻塞等待问题。写法2不涉及线程安全问题!

class Counter{
    public int count;
    public void increase(){
        synchronized (this){
            count++;
        }
    }
}
public class Demo3 {
    public static Counter counter = new Counter();
    public static Counter counter2 = new Counter();
    public static void main(String[] args) {
        Thread t1 = new Thread(()-> {
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        Thread t2 = new Thread(()-> {
            for (int i = 0; i < 50000; i++) {
                counter2.increase();
            }
        });
        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("counter="+counter.count);
    }
}

方法3.这个代码中,counter对象是同一个,对应的counter里面的locker就是同一个对象,此时仍然是俩个线程针对同一个对象加锁,会存在锁竞争。这个写法和写法1从线程安全的角度来看没有区别!

class Counter{
    public int count;
    public Object locker = new Object();
    public void increase(){
        synchronized (locker){
            count++;
        }
    }
}
public class Demo3 {
    public static Counter counter = new Counter();
    public static void main(String[] args) {
        Thread t1 = new Thread(()-> {
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        Thread t2 = new Thread(()-> {
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("counter="+counter.count);
    }
}

方法4.这种写法的locker是不同的对象,也就意味着这里是针对俩个不同对象加锁就不涉及锁竞争。

class Counter{
    public int count;
    public Object locker = new Object();
    public void increase(){
        synchronized (locker){
            count++;
        }
    }
}
public class Demo3 {
    public static Counter counter = new Counter();
    public static Counter counter2 = new Counter();
    public static void main(String[] args) {
        Thread t1 = new Thread(()-> {
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        Thread t2 = new Thread(()-> {
            for (int i = 0; i < 50000; i++) {
                counter2.increase();
            }
        });
        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("counter="+counter.count);
    }
}

方法5.这种写法的locker是一个静态成员静态成员是唯一的。虽然创建了counter和counter2这俩个实例但是这俩的locker其实是同一个locker,因此这里也就会发生锁竞争。

class Counter{
    public int count;
    static public  Object locker = new Object();
    public void increase(){
        synchronized (locker){
            count++;
        }
    }
}
public class Demo3 {
    public static Counter counter = new Counter();
    public static Counter counter2 = new Counter();
    public static void main(String[] args) {
        Thread t1 = new Thread(()-> {
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        Thread t2 = new Thread(()-> {
            for (int i = 0; i < 50000; i++) {
                counter2.increase();
            }
        });
        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("counter="+counter.count);
    }
}

方法6.线程t1对locker加锁,线程t2对counter加锁,此时俩线程是针对不同对象加锁不会产生锁竞争。

class Counter{
    public int count;
    static public  Object locker = new Object();
    public void increase(){
        synchronized (locker){
            count++;
        }
    }
    public void increase2(){
        synchronized (this){
            count++;
        }
    }
}
public class Demo3 {
    public static Counter counter = new Counter();
    public static void main(String[] args) {
        Thread t1 = new Thread(()-> {
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        Thread t2 = new Thread(()-> {
            for (int i = 0; i < 50000; i++) {
                counter.increase2();
            }
        });
        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("counter="+counter.count);
    }
}

方法7.类对象Counter.class在JVM进程中只有一个,如果多个线程针对类对象加锁,势必就会锁竞争。

class Counter{
    public int count;
    public void increase(){
        //这里的锁对象变成了个类对象,类对象在JVM中只有一个
        synchronized (Counter.class){
            count++;
        }
    }
}
public class Demo3 {
    public static Counter counter = new Counter();
    public static Counter counter2 = new Counter();
    public static void main(String[] args) {
        Thread t1 = new Thread(()-> {
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        Thread t2 = new Thread(()-> {
            for (int i = 0; i < 50000; i++) {
                counter2.increase();
            }
        });
        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("counter="+counter.count);
    }
}

4.synchronized的特性:

互斥、刷新内存、可重入。

(下面重点解释可重入):

  1. 一个线程连续针对一把锁,加锁俩次,就可能造成死锁。在第一次加锁,可以加锁成功;第二次加锁就会加锁失败,就会在第二次加锁这里阻塞等待,等到第一把锁被解锁,第二次加锁才能成功,第一把锁解锁则要求执行完synchronized代码块,也就是要求第二把锁能加锁成功这里就形成了矛盾!
  2. 针对上面的情况,不会产生死锁的话,这样的锁就叫做“可重入锁”;会产生死锁,这个锁就叫做“不可重入锁”synchronized是可重入的
  3. 如何解决这种问题,避免死锁呢,线程t第一次尝试对this来加锁,this这个锁里面就记录了是线程t加的锁第二次进行加锁的时候,锁看见还是线程t就直接通过了,没有任何负面影响,不会阻塞等待!举个例子:向女生表白如果妹妹接受了(加锁成功),拒绝你(加锁失败);当你俩在一起的时候,你再说一些甜言蜜语时,妹妹肯定也会接受。这就叫不会阻塞等待!
  4. 如何解决这种问题,避免死锁呢,引入一个计数器,每次加锁计数器就++,每次解锁计数器就--,如果计数器为0此时的加锁操作才是真加锁,如果计数器为0此时的解锁操作才是真解锁。
  5. 可重入锁的实现要点:第一是要让锁持有线程对象,记录是谁加了锁;第二是维护一个计数器,用来衡量什么时候是真加锁,什么时候是真解锁,什么时候是直接放行。

如果对您有帮助的话,

不要忘记点赞+关注哦,蟹蟹

如果对您有帮助的话,

不要忘记点赞+关注哦,蟹蟹

如果对您有帮助的话,

不要忘记点赞+关注哦,蟹蟹

相关实践学习
CentOS 7迁移Anolis OS 7
龙蜥操作系统Anolis OS的体验。Anolis OS 7生态上和依赖管理上保持跟CentOS 7.x兼容,一键式迁移脚本centos2anolis.py。本文为您介绍如何通过AOMS迁移工具实现CentOS 7.x到Anolis OS 7的迁移。
相关文章
|
15天前
|
调度 开发者 Python
深入浅出操作系统:进程与线程的奥秘
在数字世界的底层,操作系统扮演着不可或缺的角色。它如同一位高效的管家,协调和控制着计算机硬件与软件资源。本文将拨开迷雾,深入探索操作系统中两个核心概念——进程与线程。我们将从它们的诞生谈起,逐步剖析它们的本质、区别以及如何影响我们日常使用的应用程序性能。通过简单的比喻,我们将理解这些看似抽象的概念,并学会如何在编程实践中高效利用进程与线程。准备好跟随我一起,揭开操作系统的神秘面纱,让我们的代码运行得更加流畅吧!
|
4月前
|
UED 开发者 Python
探索操作系统的心脏:理解进程与线程
【8月更文挑战第31天】在数字世界的海洋中,操作系统犹如一艘巨轮,其稳定航行依赖于精密的进程与线程机制。本文将揭开这一机制的神秘面纱,通过深入浅出的语言和直观的代码示例,引领读者从理论到实践,体验进程与线程的魅力。我们将从基础概念出发,逐步深入到它们之间的联系与区别,最后探讨如何在编程实践中高效运用这些知识。无论你是初学者还是有经验的开发者,这篇文章都将为你的技术之旅增添新的航标。
|
14天前
|
算法 调度 开发者
深入理解操作系统:进程与线程的管理
在数字世界的复杂编织中,操作系统如同一位精明的指挥家,协调着每一个音符的奏响。本篇文章将带领读者穿越操作系统的幕后,探索进程与线程管理的奥秘。从进程的诞生到线程的舞蹈,我们将一起见证这场微观世界的华丽变奏。通过深入浅出的解释和生动的比喻,本文旨在揭示操作系统如何高效地处理多任务,确保系统的稳定性和效率。让我们一起跟随代码的步伐,走进操作系统的内心世界。
|
1月前
|
Linux 调度 C语言
深入理解操作系统:进程和线程的管理
【10月更文挑战第32天】本文旨在通过浅显易懂的语言和实际代码示例,带领读者探索操作系统中进程与线程的奥秘。我们将从基础知识出发,逐步深入到它们在操作系统中的实现和管理机制,最终通过实践加深对这一核心概念的理解。无论你是编程新手还是希望复习相关知识的资深开发者,这篇文章都将为你提供有价值的见解。
|
1月前
深入理解操作系统:进程与线程的管理
【10月更文挑战第30天】操作系统是计算机系统的核心,它负责管理计算机硬件资源,为应用程序提供基础服务。本文将深入探讨操作系统中进程和线程的概念、区别以及它们在资源管理中的作用。通过本文的学习,读者将能够更好地理解操作系统的工作原理,并掌握进程和线程的管理技巧。
44 2
|
1月前
|
调度 Python
深入浅出操作系统:进程与线程的奥秘
【10月更文挑战第28天】在数字世界的幕后,操作系统悄无声息地扮演着关键角色。本文将拨开迷雾,深入探讨操作系统中的两个基本概念——进程和线程。我们将通过生动的比喻和直观的解释,揭示它们之间的差异与联系,并展示如何在实际应用中灵活运用这些知识。准备好了吗?让我们开始这段揭秘之旅!
|
3月前
|
存储 消息中间件 资源调度
「offer来了」进程线程有啥关系?10个知识点带你巩固操作系统基础知识
该文章总结了操作系统基础知识中的十个关键知识点,涵盖了进程与线程的概念及区别、进程间通信方式、线程同步机制、死锁现象及其预防方法、进程状态等内容,并通过具体实例帮助理解这些概念。
「offer来了」进程线程有啥关系?10个知识点带你巩固操作系统基础知识
|
2月前
|
算法 安全 调度
深入理解操作系统:进程与线程的管理
【10月更文挑战第9天】在数字世界的心脏跳动着的,不是别的,正是操作系统。它如同一位无形的指挥家,协调着硬件与软件的和谐合作。本文将揭开操作系统中进程与线程管理的神秘面纱,通过浅显易懂的语言和生动的比喻,带你走进这一复杂而又精妙的世界。我们将从进程的诞生讲起,探索线程的微妙关系,直至深入内核,理解调度算法的智慧。让我们一起跟随代码的脚步,解锁操作系统的更多秘密。
40 1
|
1月前
|
Linux 调度
探索操作系统核心:进程与线程管理
【10月更文挑战第24天】在数字世界的心脏,操作系统扮演着至关重要的角色。它不仅是计算机硬件与软件之间的桥梁,更是管理和调度资源的大管家。本文将深入探讨操作系统的两大基石——进程与线程,揭示它们如何协同工作以确保系统运行得井井有条。通过深入浅出的解释和直观的代码示例,我们将一起解锁操作系统的管理奥秘,理解其对计算任务高效执行的影响。
|
3月前
|
资源调度 算法 调度
深入浅出操作系统之进程与线程管理
【9月更文挑战第29天】在数字世界的庞大舞台上,操作系统扮演着不可或缺的角色,它如同一位精通多门艺术的导演,精心指挥着每一个进程和线程的演出。本文将通过浅显的语言,带你走进操作系统的内心世界,探索进程和线程的管理奥秘,让你对这位幕后英雄有更深的了解。