java线程安全问题
临界资源
临界资源是一次仅允许一个进程使用的共享资源。各进程采取互斥的方式,实现共享的资源称作临界资源。属于临界资源的硬件有,打印机,磁带机等;
软件有消息队列,变量,数组,缓冲区等。诸进程间采取互斥方式,实现对这种资源的共享。
竞态条件
当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。
导致竞态条件发生的代码区称作临界区。 在临界区中使用适当的同步操作就可以避免竞态条件,如使用synchronized或者加锁机制。
线程安全
允许被多个线程同时执行的代码称作线程安全的代码。线程安全的代码不包含竞态条件。
线程安全出现问题的例子:
当多个线程同时操作一个变量时,可能会造成变量的脏读脏写(类似于mysql)
package com.company;
public class Main {
public static void main(String[] args) {
Test test = new Test();
//创建20个线程
for (int i =1;i<=20;i++){
new Thread(()->{
for (int j =1;j<=1000;j++){
test.incA();
}
},"测试"+i).start();
}
while(Thread.activeCount() > 2){ //main, gc,说明还存在其他线程在执行
Thread.yield();//线程礼让
}
System.out.println(Thread.currentThread().getName() + "\t int类型的number最终值:" + test.a());
}
}
class Test{
public int a;
public int a(){
return a;
}
public void incA(){
a++;
}
}
复制
执行结果:
/Users/tioncico/Library/Java/JavaVirtualMachines/openjdk-14.0.1/Contents/Home/bin/java -javaagent:/Applications/IDE/IntelliJ IDEA.app/Contents/lib/idea_rt.jar=56786:/Applications/IDE/IntelliJ IDEA.app/Contents/bin -Dfile.encoding=UTF-8 -classpath /Users/tioncico/IdeaProjects/test/out/production/untitled104 com.company.Main
main int类型的number最终值:19893
复制
可看到 本来是20个线程*1000次递增,但是实际值却小于20000,这个情况就属于非线程安全的一种
如何实现线程安全?
volatile关键字
通过volatile修饰属性,此属性将直接修改内存,不经过线程内部缓存和重排序
volatile关键字可以保证属性操作的可见性和有序性,但是不能保证原子性
可见性
指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改一个共享变量时,另一个线程马上就能看到。比如:用volatile修饰的变量,就会具有可见性。
有序性
有序性是指在单线程环境中, 程序是按序依次执行的.
而在多线程环境中, 程序的执行可能因为指令重排而出现乱序
指令重排
指令重排是指在程序执行过程中, 为了性能考虑, 编译器和CPU可能会对指令重新排序.
原子性
子性是指一个操作是不可中断的. 即使是在多个线程一起执行的时候,
一个操作一旦开始,就不会被其它线程干扰.
volatile可见性案例:
package com.company;
import java.util.concurrent.TimeUnit;
public class Main {
public static void main(String[] args) {
Test test = new Test();
//创建1个线程
new Thread(()->{
System.out.println(Thread.currentThread().getName() + "\t 正在执行");
try {
TimeUnit.SECONDS.sleep(3);//留出时间使得主线程代码执行
test.setA(100);
System.out.println(Thread.currentThread().getName() + "\t int类型的值为:" + test.a());
} catch (InterruptedException e) {
e.printStackTrace();
}
},"演示").start();
while(test.a==0){//如果一直为0,则一直循环
}
System.out.println(Thread.currentThread().getName() + "\t int类型的number值为:" + test.a());
}
}
class Test{
public int a=0;
public int a(){
return a;
}
public void incA(){
a++;
}
public void setA(int a){
this.a = a;
}
}
复制
由于没有volatile关键字,主线程main一直获取到的值是0,所以循环不会中断
增加volatile关键字:
class Test{
public volatile int a=0;
public int a(){
return a;
}
public void incA(){
a++;
}
public void setA(int a){
this.a = a;
}
}
复制
volatile无法解决原子性问题:
主要原因为:
线程1拿到了a=0的值,并且0++变成了1 但是其实在同一时刻,线程1-20都拿到了a=0的值,都++变成了1,就会导致线程写入覆盖,最后就会导致值小于20000;
AtomicIntegrer原子类
虽然volatile无法实现原子性,但是可以通过java.util.concurrent.AtomicInteger 类
保存数据实现原子性操作:
class Test{
public AtomicInteger a = new AtomicInteger();
public int a(){
return a.get();
}
public void incA(){
a.getAndIncrement();
}
}
复制
结果:
/Users/tioncico/Library/Java/JavaVirtualMachines/openjdk-14.0.1/Contents/Home/bin/java -javaagent:/Applications/IDE/IntelliJ IDEA.app/Contents/lib/idea_rt.jar=62725:/Applications/IDE/IntelliJ IDEA.app/Contents/bin -Dfile.encoding=UTF-8 -classpath /Users/tioncico/IdeaProjects/test/out/production/untitled104 com.company.Main
main int类型的number最终值:20000
复制
synchronized关键字
synchronized关键字可对某个方法进行加锁,使得该方法同一时刻只能一个线程访问:
class Test {
public int a;
public int a() {
return a;
}
public synchronized void incA() {
a++;
}
}
复制
运行结果:
- 点赞
- 收藏
- 关注作者
评论(0)