多线程的线程安全问题,Java线程安全问题
本文给大家带来了一些java的知识,主要介绍了多线程的一些相关问题,包括线程的安装,线程锁定和线程不安全的原因,线程安全的标准类等等。希望对你有帮助。
如何解决写爬虫IP受阻的问题?立即使用。
本文介绍了Java多线程中的线程安全问题。这里的安全问题不是指黑客攻击导致的安全问题,线程安全问题是指多线程抢先执行导致的bug问题。
1.线程安全概述
1.1什么是线程安全问题
首先我们要明白,操作系统中线程的调度是抢占式的,或者说是随机的,这就造成了线程的执行顺序是不确定的。有些代码执行顺序不影响程序的运行结果,但有些代码执行顺序改变了,重写后的运行结果会受到影响,从而导致程序出现bug。在多线程并发中会导致错误的代码称为
接下来介绍一个线程安全问题的典型例子,整数自增量问题。
1.2一个存在线程安全问题的程序
有一天,老师布置了这样一道题:用两个线程增加变量计数10万次,每个线程承担5万个自增任务,变量计数的初始值为0。
这个问题很简单,我们可以口头算算最后的结果。答案是10万元。
小明做事非常迅速,很快就写出了下面的代码:
类别计数器{
private int计数;
公共无效增加(){
this.count
}
public int getCount() {
返回this.count
} }公共类Main11 {
私有静态final int CNT=50000
私有静态最终计数器Counter=new Counter();
公共静态void main(String[] args)引发InterruptedException {
Thread thread1=新线程(()- {
for(int I=0;我CNTi ) {
counter . increase();
}
});
Thread thread2=新线程(()- {
for(int j=0;j CNTj ) {
counter . increase();
}
});
thread 1 . start();
thread 2 . start();
thread 1 . join();
thread 2 . join();
system . out . println(counter . get count());
}}按理说结果应该是10万。让我们来看看运行结果:
跑的结果不到10万。你可以试着运行这个程序,你会发现每次运行的结果都不一样,但大多数情况下,结果会比预期值小。我们来分析一下为什么会这样。
2.线程加锁与线程不安全的原因
2.1案例分析
上面我们用多线程运行了一个程序,把一个值为0的变量增加了10万倍,但是最后的实际结果比我们预期的结果要小。原因是线程调度的顺序是随机的,导致线程间自增长指令集的交叉,导致运行时有两次自增长但只有一次自增长,所以得到的结果会太小。
我们知道自动增量操作可以包含以下指令:
将内存中变量的值载入寄存器。您可能希望将此操作记录为LOAD。如果在寄存器中执行自增操作,也可以将此操作记录为add。将寄存器的值保存到内存中,这个操作就像保存一样容易记住。让我们画一个时间线来总结几种常见的情况:
情况1:线程间指令集,没有交叉,运行结果和预期一样。图中寄存器A表示线程1使用的寄存器,寄存器B表示线程2使用的寄存器,后续情况相同。情况2:线程间存在指令集交集,运行结果低于预期。情况3:线程间指令集完全交叉,实际结果低于预期。
根据上述情况,发现线程运行中没有交叉指令时运行结果是正常的,但一旦有交叉,自增运算的结果会减1。综上,可以得出一个结论,即由于自增操作不是原子性的,多线程并发执行很可能会导致指令交叉执行,从而产生线程安全问题。
那怎么解决上面的线程不安全问题呢?当然,那是锁定对象。
2.2线程加锁
2.2.1什么是加锁
为了解决“抢先执行”带来的线程安全问题,我们可以锁定被操作对象。当线程获得对象的锁时,它将锁定该对象。如果其他线程需要执行对象的任务,需要等待线程完成对象的任务后才能执行。
举个例子,假设你要去银行的ATM机存钱或者取钱。每个自动取款机通常都在一个单独的小房子里。这间小房子有一扇门和一把锁。当你进去使用自动取款机时,门会自动锁上。这个时候如果有人要取钱,它是不能进去用ATM的,直到你用完了出来。那么这里的“你”就相当于一根线,ATM就相当于一个物体,小房子就相当于一把锁。
java中最常用的锁定操作是使用synchronized关键字来锁定。
2.2.2如何加锁
同步会有互斥的效果。当一个线程在一个对象的synchronized中执行时,如果其他线程也在同一个对象的synchronized中执行,它们将阻塞等待。
进入线程的同步代码块相当于锁定,退出同步代码块相当于解锁。
java中的加锁操作可以通过使用synchronized关键字来实现,其常见用法如下:
方式1:使用synchronized关键字来修饰一个公共方法,它会给方法所在的对象添加一个锁。
比如以上面的自增程序为例,尝试使用synchronized关键字进行锁定。如下,我锁定了increase方法,这个方法实际上锁定了一个对象,这个锁定的对象是这个。本质上,锁定操作是修改这个对象头的标志位。
类别计数器{
private int计数;
同步公共void增加(){
this.count
}
public int getCount() {
返回this.count
}}多线程自增的主要方法如下。synchronized的其他用法后面会用同样的栗子介绍,所以后面就不列出这段代码了。
公共类Main11 {
私有静态final int CNT=50000
私有静态最终计数器Counter=new Counter();
公共静态void main(String[] args)引发InterruptedException {
Thread thread1=新线程(()- {
for(int I=0;我CNTi ) {
counter . increase();
}
});
Thread thread2=新线程(()- {
for(int j=0;j CNTj ) {
counter . increase();
}
});
thread 1 . start();
thread 2 . start();
thread 1 . join();
thread 2 . join();
system . out . println(counter . get count());
}}}看运行结果:方式2:使用synchronized关键字锁定代码段,但是需要显式指定锁定的对象。
例如:
类别计数器{
private int计数;
公共无效增加(){
同步(这){
this.count
}
}
public int getCount() {
返回this.count
}}运行结果:方式3:使用synchronized关键字修饰静态方法相当于锁定了当前类的类对象。
类别计数器{
私有静态int计数;
同步公共静态void增加(){
数数;
}
public int getCount() {
返回this.count
}}运行结果:
这些是常见的用法。对于线程锁(线程取锁),如果两个线程同时取一个对象的锁,就会出现锁竞争。两个线程同时获取两个不同对象的锁,不会发生锁竞争。
对于synchronized这个关键词,它的英文意思是同步,但是同步在计算机中有很多意思。比如多线程,这里的同步就是“互斥”的意思;在IO或网络编程中,同步指的是“异步”,与多线程无关。
synchronized 的工作过程:
获取互斥锁将变量的最新副本从主内存复制到工作内存。执行代码,将更改后的共享变量的值刷新到主内存中。释放互斥锁。同步的同步块可以为同一个线程重新进入,不会出现锁死自己的问题,也就是死锁。关于死锁的后续文章会再次介绍。
综上所述,同步关键词锁定具有如下性质:互斥、内存刷新和可重入。
Synchronized关键字也相当于一个监视锁。如果不锁,就用wait方法(线程等待的一种方法,后面会详细介绍),就会抛出非法的monitor异常。这个异常的原因是没有锁。
2.2.3再析案例
锁定自增代码后,我们来分析一下为什么所有线程都是安全的。我们先列出代码:
类别计数器{
private int计数;
同步公共void增加(){
this.count
}
public int getCount() {
返回this.count
} }公共类Main11 {
私有静态final int CNT=50000
私有静态最终计数器Counter=new Counter();
公共静态void main(String[] args)引发InterruptedException {
Thread thread1=新线程(()- {
for(int I=0;我CNTi ) {
counter . increase();
}
});
Thread thread2=新线程(()- {
for(int j=0;j CNTj ) {
counter . increase();
}
});
thread 1 . start();
thread 2 . start();
thread 1 . join();
thread 2 . join();
system . out . println(counter . get count());
}}多线程并发执行的时候,上次分析过没有指令集交叉不会有问题。所以这里只讨论如何保证指令交叉后的线程安全。你最好记住锁定是锁定,解锁是解锁。两个线程的运行过程如下:
线程1首先获得目标对象的锁,锁定对象,它处于锁定状态。当线程2开始执行自递增操作时,它将被阻塞。在线程1的自递增操作完成并处于解锁状态之前,线程2将准备好执行线程2的自递增操作。
锁后线程化是串行执行,和单线程区别不大。多线程没用吗?但是方法被锁定后,线程运行方法时会被锁定,方法运行后会自动解锁。此外,大多数操作的并发执行不会引起线程安全,只有少数修改操作可能引起线程安全问题。所以多线程的运行效率整体上要比单线程高很多。
2.3线程不安全的原因
首先,线程不安全的根源在于线程之间的调度充满了随机性,导致原有的逻辑被改变,从而产生线程不安全。这个问题解决不了,我们也无能为力。
多个线程写入(修改)同一个资源,对资源的修改不是原子性的,这可能导致线程不安全,类似于数据库事务。
由于编译器优化,无法保证内存可见性,即当一个线程频繁读取同一个变量时,会直接从寄存器中读取值,而不是从内存中读取,这样当内存值被修改时,线程就不会感知到变量被修改了,从而导致线程安全问题(这是编译器优化的结果,现代编译器也有超越Java的类似优化),因为与寄存器相比, 从内容中读取数据的效率要低得多,所以编译器会尽可能用相同的逻辑优化代码。 在单线程的情况下不会翻车,多线程就不一定了,比如下面这段代码:
导入Java . util . scanner;公共类Main12 {
private static int isQuit
公共静态void main(String[] args) {
Thread thread=新线程(()- {
while (isQuit==0) {
}
System.out.println (thread线程执行完毕!);
});
thread . start();
Scanner sc=新扫描仪(system . in);
System.out.println(请输入isQuit的值,如果不是0 thread就停止执行!);
is quit=sc . nextint();
System.out.println(主线程执行完毕!);
}}运行结果:
从运行结果可以知道,在isQuit输入后线程并没有停止,这也是编译器优化导致线程无法感知内存可见性,从而导致线程不安全的原因。
我们可以使用volatile关键字来确保内存可见性。
我们可以用volatile关键字来修饰isQuit,以确保内存可见性。
导入Java . util . scanner;公共类Main12 {
volatile私有静态int isQuit
公共静态void main(String[] args) {
Thread thread=新线程(()- {
while (isQuit==0) {
}
System.out.println (thread线程执行完毕!);
});
thread . start();
Scanner sc=新扫描仪(system . in);
System.out.println(请输入isQuit的值,如果不是0 thread就停止执行!);
is quit=sc . nextint();
System.out.println(主线程执行完毕!);
}}运行结果:
synchronized与volatile关键字的区别:synchronized关键字可以保证原子性,但是能否保证内存可见性要看情况(上面的栗子是不可以的),而volatile关键字只能保证内存可见性而不能保证原子性。
保证内存可见性就是禁止编译器进行上述优化。
导入Java . util . scanner;公共类Main12 {
private static int isQuit
//锁定对象
私有静态最终对象锁=新对象();
公共静态void main(String[] args) {
Thread thread=新线程(()- {
同步(锁定){
while (isQuit==0) {
}
System.out.println (thread线程执行完毕!);
}
});
thread . start();
Scanner sc=新扫描仪(system . in);
System.out.println(请输入isQuit的值,如果不是0 thread就停止执行!);
is quit=sc . nextint();
System.out.println(主线程执行完毕!);
}}运行结果:
编译器优化不仅会导致内存可见的问题,还会导致线程安全问题。指令重排序也是编译器优化之一,即编译器会智能调整代码执行顺序(同时保持原有逻辑不变),从而提高程序运行效率。单线程没问题,多线程可能会翻车。知道原因就好。
3.线程安全的标准类
Java Java标准库是线程不安全的。这些类可能涉及多线程,以在没有任何锁定措施的情况下修改共享数据。比如ArrayList、LinkedList、HashMap、TreeMap、HashSet、TreeSet、StringBuilder。
但也有一些是线程安全的,使用一些锁机制来控制,比如Vector(已弃用)、HashTable(已弃用)、ConcurrentHashMap(推荐)、StringBuffer。
此外,虽然没有锁定,但它不涉及“修改”,仍然是线程安全的,如String。
在网上安全问题上,你也可能遇到JMM模式。我想在这里补充一点,JMM实际上是重新包装了操作系统中的寄存器、缓存和内存。在JMM,寄存器和缓存被称为工作存储器,存储器被称为主存储器。
缓存分为L1、L2和L3。从L1到L3,空间越来越大,最大的比内存空间小,最小的比寄存器空间大,而且访问速度越来越慢,最慢的比内存访问速度快,最快的没有寄存器访问速度快。
4.Object类提供的线程等待方法
除了在线程类中可以实现线程等待的方法,如join、sleep,在对象类中也提供了与线程等待相关的方法。
当
序号 | 方法 | 说明 |
---|---|---|
1 | public final void wait() throws InterruptedException | 释放锁并使线程进入WAITING状态 |
2 | public final native void wait(long timeout) throws InterruptedException; | 相比于方法1,多了一个最长等待时间 |
3 | public final void wait(long timeout, int nanos) throws InterruptedException | 相比于方法2,等待的最长时间精度更大 |
4 | public final native void notify(); | 唤醒一个WAITING状态的线程,并加锁,搭配wait方法使用 |
5 | public final native void notifyAll(); | 唤醒所有处于WAITING状态的线程,并加锁(很可能产生锁竞争),搭配wait方法使用 |
公共类TestDemo12 {
公共静态void main(String[] args)引发InterruptedException {
Thread thread=新线程(()- {
尝试{
thread . sleep(5000);
} catch (InterruptedException e) {
e . printstacktrace();
}
System.out.println(执行完毕!);
});
thread . start();
system . out . println(" before wait ");
thread . wait();
system . out . println(" after wait ");
}}看运行结果:
果然抛出了一个IllegalMonitorStateException,因为wait方法的执行步骤是:先释放锁,然后让线程等待。你现在还没锁,那怎么开锁呢?所以这个异常会被抛出,但是执行notify是无害的。
wait方法通常与notify方法一起使用。前者可以释放锁,让线程等待,后者可以获取锁,让线程继续执行。这个组合拳的流程图如下:
现在,有两个任务由两个线程执行。假设线程2在线程1之前执行,请写一个多线程程序使任务1在任务2之前完成,其中线程1执行任务1,线程2执行任务2。
这个需求可以通过使用等待/通知来实现。
课程任务{
公共void任务(int i) {
System.out.println(任务 I 已完成!);
} }公共类WiteNotify {
//锁定对象
私有静态最终对象锁=新对象();
公共静态void main(String[] args)引发InterruptedException {
Thread thread1=新线程(()- {
同步(锁定){
任务Task 1=new Task();
task 1 . task(1);
//通知线程2,线程1的任务已经完成。
system . out . println( before notify );
lock . notify();
system . out . println( after notify );
}
});
Thread thread2=新线程(()- {
同步(锁定){
任务Task 2=new Task();
//等待线程1的任务1完成执行
system . out . println(" before wait ");
尝试{
lock . wait();
} catch (InterruptedException e) {
e . printstacktrace();
}
task 2 . task(2);
system . out . println(" after wait ");
}
});
thread 2 . start();
Thread.sleep(十);
thread 1 . start();
}}运行结果:
推荐:《java视频教程》以上是Java多线程的线程安全的详细内容。更多请关注我们的其他相关文章!
郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。