写点什么

一文带你吃透多线程中的线程安全

作者:几分醉意.
  • 2022-11-24
    安徽
  • 本文字数:3039 字

    阅读完需:约 10 分钟

线程安全(重点)

关于多线程并发环境下,数据的安全问题。为什么这个是重点?以后在开发中,我们的项目都是运行在服务器当中,而服务器已经将线程的定义,线程对象的创建,线程的启动等,都已经实现完了。这些代码我们都不需要编写。最重要的是:你要知道,你编写的程序需要放到一个多线程的环境下运行,你更需要关注的是这些数据在多线程并发的环境下是否是安全的。(重点)


什么时候数据在多线程并发的环境下会存在安全问题呢?三个条件:条件 1:多线程并发。条件 2:有共享数据。条件 3:共享数据有修改的行为。满足以上 3 个条件之后,就会存在线程安全问题。


怎么解决线程安全问题呢?当多线程并发的环境下,有共享数据,并且这个数据还会被修改,此时就存在线程安全问题,怎么解决这个问题?线程排队执行。(不能并发)。用排队执行解决线程安全问题。这种机制被称为:==线程同步机制==。专业术语叫做:==线程同步==,实际上就是线程不能并发了,线程必须排队执行。


怎么解决线程安全问题?使用“线程同步机制"。线程同步就是线程排队了,线程排队了就会牺牲一部分效率,没办法,数据安全第一位,只有数据安全了,我们才可以谈效率。数据不安全,没有效率的事儿。


说到线程同步这块,涉及到这两个专业术语:==异步编程模型==:线程 t1 和线程 t2,各自执行各自的,t1 不管 t2,t2 不管 t1,谁也不需要等谁,这种编程模型叫做:异步编程模型。其实就是:多线程并发(效率较高。)异步就是并发。==同步编程模型==:线程 t1 和线程 t2,在线程 t 执行的时候,必须等待 t2 线程执行结束,或者说在 t2 线程执行的时候,必须等待 t1 线程执行结束,两个线程之间发生了等待关系,这就是同步编程模型。效率较低。线程排队执行。同步就是排队。


Java 中有三大变量?【重要的内容。】实例变量:在堆中。静态变量:在方法区。局部变量:在栈中。以上三大变量中:局部变量永远都不会存在线程安全问题。因为局部变量不共享。(一个线程一个栈。)局部变量在栈中。所以局部变量永远都不会共享。实例变量在堆中,堆只有 1 个,静态变量在方法区中,方法区只有 1 个。堆和方法区都是多线程共享的,所以可能存在线程安全问题。局部变量+常量:不会有线程安全问题。成员变量:可能会有线程安全问题。


如果使用局部变量的话:建议使用:StringBuilder.因为局部变量不存在线程安全问题。选择 stringBuildere stringBuffer 效率比较低。ArrayList 是非线程安全的。 Vector 是线程安全的。HashMap Hashset 是非线程安全的。 Hashtable 是线程安全的。


总结:synchronized有三种写法:第一种:同步代码块。 灵活synchronized(线程共享对象){  同步代码块;}第二种:在实例方法上使用synchronized表示共享对象一定是this并且同步代码块是整个方法体。
第三种:在静态方法上使用synchronized表示找类锁。类锁永远只有1把。就算创建了100个对象,那类锁也只有一把,所以这个类new出来的对象都要排队对象锁:1个对象1把锁,100个对象100把锁。类锁:100个对象,也可能只是1把类锁。
复制代码


聊一聊,我们以后开发中应该怎么解决线程安全问题?是一上来就选择线程同步吗?synchronized 不是,synchronized 会让程序的执行效率降低,用户体验不好。系统的用户吞吐量降低。用户体验差。在不得已的情况下再选择线程同步机制。第一种方案:尽量使用局部变量代替"实例变量和静态变量"。第二种方案:如果必须是实例变量,那么可以考虑创建多个对象,这样实例变量的内存就不共享了。(一个线程对应 1 个对象,100 个线程对应 100 个对象,对象不共享,就没有数据安全问题了。)第三种方案:如果不能使用局部变量,对象也不能创建多个,这个时候就只能选择 synchronized 了。线程同步机制。

同步代码块 synchronized

线程同步机制的语法是:synchronized(){  // 线程同步代码块。}synchronized后面小括号中传的这个“数据”是相当关键的。这个数据必须是多线程共享的数据。才能达到多线程排队。()中写什么?那要看你想让哪些线程同步。假设t1、t2、t3、t4、t5,有5个线程,你只希望t1t2t3排队,t4t5不需要排队。怎么办?你一定要在()中写一个t1t2t3共享的对象。而这个对象对于t4t5来说不是共享的。
复制代码


在 java 语言中,任何一个对象都有"一把锁”,其实这把便就是标记。(只是把它叫做锁。100 个对象,100 把锁。1 个对象 1 把锁。以下代码的执行原理?1、假设 t1 和 t2 线程并发,开始执行代码的时候,肯定有一个先一个后。2、线设 t1 先执行了,遇到了 synchronized,这个时候自动找“后面共享对亲”的对象锁,找到之后,并占有这把锁,然后执行同步代码块中的程序,在程序执行过程中一直都是占有这把锁的。直到同步代码块代码结束,这把锁才会释放。3、假设 t1 已经占有这把锁,此时 t2 也遇到 synchronized 关键字,也会去占有后同共享对象的这把锁,结果这把锁被 t1 占有,t2 只能在同步代码块外面等待的结束,直到 t1 把同步代码快执行结束了,t1 会归还这把锁,此时 t2 终于等到这把锁,然后 t2 占有这把锁之后,进入同步代码块执行程序。这样就达到了线程排队执行。这里需要注意的是,这个共享对象一定要选好了。这个共享对象一定是你需要排队执行的这些线程对象所共享的

实例方法上和静态方法上使用 synchronized

在实例方法上可以使用 synchronized 吗?可以的。synchronized 出现在实例方法上,一定锁的是 this 没得挑。只能是 this。不能是其他的对象了。所以这种方式不灵活。另外还有一个缺点:synchronized 出现在实例方法上,表示整个方法体都需要同步,可能会无故扩大同步的范围,导致程序的执行效率降低。所以这种方式不常用。


synchronized 使用在实例方法上有什么优点?代码写的少了。节俭了。如果共享的对象就是 this,并且需要同步的代码块是整个方法体建议使用这种方式。


synchronized 出现在静态方法上是找类锁,因为静态方法是类锁,不管创建几个对象类锁只有一把。

死锁


死锁代码要会写,一般面试官要求你会写,只有会写的,才会在以后的开发中注意这个事,因为死锁很难调试


实例


public class 死锁 {    public static void main(String[] args) {        Object a = new Object();        Object b = new Object();
//t1 和t2两个线程共享a,b Thread t1 = new aa1(a,b); Thread t2 = new aa2(a,b); t1.start(); t2.start(); }}class aa1 extends Thread{ Object o1; Object o2; public aa1(Object o1,Object o2){ this.o1 = o1; this.o2 = o2 ; } //t1执行时占o1锁,t2执行时占o2锁; // t1执行完o1,要执行o1里面的o2,但是o2已经被t2占了; // t2执行完o2,要执行o2里面的o1,但是o1已经被t1占了; //这就屎机了 //所以尽量少循环嵌套 public void run() { synchronized (o1){ //o1嵌套o2 try { sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (o2){ } } }}class aa2 extends Thread{ Object o1; Object o2; public aa2(Object o1,Object o2){ this.o1 = o1; this.o2 = o2 ; } @Override public void run() { synchronized (o2){ //o2嵌套o1 try { sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (o1){ } } }}
复制代码


发布于: 刚刚阅读数: 4
用户头像

几分醉意.

关注

还未添加个人签名 2022-10-22 加入

还未添加个人简介

评论

发布
暂无评论
一文带你吃透多线程中的线程安全_Java_几分醉意._InfoQ写作社区