当我们使用多个线程访问同一资源(可以是同一个变量、同一个文件、同一条记录等)的时候,若多个线程只有读操作
,那么不会发生线程安全问题。但是如果多个线程中对资源有读和写
的操作,就容易出现线程安全问题。
1.线程安全相关:
要考虑线程安全问题,就需要先考虑Java并发的三大基本特性:原子性、可见性以及有序性
1、原子性:原子性是指在一个操作中就是cpu不可以在中途暂停然后再调度,即不被中断操作,要不全部执行完成,要不都不执行。
2、可见性:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。若两个线程在不同的cpu,那么线程1改变了i的值还没刷新到主存,线程2又使用了i,那么这个i值肯定还是之前的,线程1对变量的修改线程没看到这就是可见性问题。
3、有序性
程序执行的顺序按照代码的先后顺序执行,在多线程编程时就得考虑这个问题。
2.如何确保线程安全问题?
解决办法:使用多线程之间使用关键字synchronized、或者使用锁(lock),或者volatile关键字。
①synchronized(自动锁,锁的创建和释放都是自动的);
②lock 手动锁(手动指定锁的创建和释放)。
③volatile关键字
如果可能会发生数据冲突问题(线程不安全问题),只能让当前一个线程进行执行。代码执行完成后释放锁,然后才能让其他线程进行执行。这样的话就可以解决线程不安全问题。
3.synchronized关键字
将可能会发生线程安全问题地代码,给包括起来,也称为同步代码块。synchronized使用的锁可以是对象锁也可以是静态资源,如×××.class,只有持有锁的线程才能执行同步代码块中的代码。没持有锁的线程即使获取cpu的执行权,也进不去。
锁的释放是在synchronized同步代码执行完毕后自动释放。
同步的前提:
1,必须要有两个或者两个以上的线程,如果小于2个线程,则没有用,且还会消耗性能(获取锁,释放锁)
2,必须是多个线程使用同一个锁
弊端:多个线程需要判断锁,较为消耗资源、抢锁的资源。
同步函数
将synchronized加在方法上
分为两种:
第一种是非静态同步函数,即方法是非静态的,使用的this对象锁。
第二种是静态同步函数,即方法是用static修饰的,使用的锁是当前类的class文件(xxx.class)。
4.死锁问题
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。简单来说就是:同步中嵌套同步,导致锁无法释放。
一旦出现死锁,整个程序既不会发生异常,也不会给出任何提示,只是所有线程处于阻塞状态,无法继续。
诱发死锁的原因:
互斥条件
占用且等待
不可抢夺(或不可抢占)
循环等待
以上4个条件,同时出现就会触发死锁。
解决死锁:
死锁一旦出现,基本很难人为干预,只能尽量规避。可以考虑打破上面的诱发条件。
针对条件1:互斥条件基本上无法被破坏。因为线程需要通过互斥解决安全问题。
针对条件2:可以考虑一次性申请所有所需的资源,这样就不存在等待的问题。
针对条件3:占用部分资源的线程在进一步申请其他资源时,如果申请不到,就主动释放掉已经占用的资源。
针对条件4:可以将资源改为线性顺序。申请资源时,先申请序号较小的,这样避免循环等待问题。
5.同步机制解决线程安全问题的思考顺序
1、如何找问题,即代码是否存在线程安全?(非常重要)
(1)明确哪些代码是多线程运行的代码
(2)明确多个线程是否有共享数据
(3)明确多线程运行代码中是否有多条语句操作共享数据
2、如何解决呢?(非常重要)
对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。即所有操作共享数据的这些语句都要放在同步范围中
6.lock关键字
可以视为synchronized的增强版,提供了更灵活的功能。该接口提供了限时锁等待、锁中断、锁尝试等功能。synchronized实现的同步代码块,它的锁是自动加的,且当执行完同步代码块或者抛出异常后,锁的释放也是自动的。
但是Lock锁是需要手动去加锁和释放锁,所以Lock相比于synchronized更加的灵活。且还提供了更多的功能比如说
tryLock()方法会尝试获取锁,如果锁不可用则返回false,如果锁是可以使用的,那么就直接获取锁且返回true
7.volatile关键字
当多个线程同时访问一个数据的时候,可能本地内存没有及时刷新到主内存,所以就会发生线程安全问题
解决办法:
当出现这种问题时,就可以使用Volatile关键字进行解决。
Volatile 关键字的作用是变量在多个线程之间可见。使用Volatile关键字将解决线程之间可见性,强制线程每次读取该值的时候都去“主内存”中取值。
只需要在flag属性上加上该关键字即可。
子线程每次都不是读取的线程本地内存中的副本变量了,而是直接读取主内存中的属性值。
volatile虽然具备可见性,但是不具备原子性。
8.synchronized、volatile和Lock之间的区别
synochronizd和volatile关键字区别:
1)volatile关键字解决的是变量在多个线程之间的可见性;而sychronized关键字解决的是多个线程之间访问共享资源的同步性。
tip: final关键字也能实现可见性:被final修饰的字段在构造器中一旦初始化完成,并且构造器没有把“this”的引用传递出去(this引用逃逸是一件很危险的事情,其它线程有可能通过这个引用访问到了"初始化一半"的对象),那在其他线程中就能看见final;
2)volatile只能用于修饰变量,而synchronized可以修饰方法,以及代码块。(volatile是线程同步的轻量级实现,所以volatile性能比synchronized要好,并且随着JDK新版本的发布,sychronized关键字在执行上得到很大的提升,在开发中使用synchronized关键字的比率还是比较大);
3)多线程访问volatile不会发生阻塞,而sychronized会出现阻塞;
4)volatile能保证变量在多个线程之间的可见性,但不能保证原子性;而sychronized可以保证原子性,也可以间接保证可见性,因为它会将私有内存和公有内存中的数据做同步。
线程安全包含原子性和可见性两个方面。
对于用volatile修饰的变量,JVM虚拟机只是保证从主内存加载到线程工作内存的值是最新的。
一句话说明volatile的作用:实现变量在多个线程之间的可见性。
synchronized和lock区别:
1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
5)Lock可以提高多个线程进行读操作的效率(读写锁)。
在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。