服务器之家:专注于服务器技术及软件下载分享
分类导航

PHP教程|ASP.NET教程|Java教程|ASP教程|编程技术|正则表达式|C/C++|IOS|C#|Swift|Android|VB|R语言|JavaScript|易语言|vb.net|

服务器之家 - 编程语言 - Java教程 - JAVA多线程线程安全性基础

JAVA多线程线程安全性基础

2021-11-16 13:24闻人此生 Java教程

这篇文章主要介绍了如何测试Java类的线程安全性,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下

线程安全性

一个对象是否需要是线程安全的,取决于它是否被多个线程访问,而不取决于对象要实现的功能

什么是线程安全的代码

核心:对 共享的 和 可变的 状态的访问进行管理。防止对数据发生不受控的并发访问。

何为对象的状态?

状态是指存储在对象的状态变量(例如实例或静态域)中的数据。还可能包括 其他依赖对象 的域。

eg:某个HashMap的状态不仅存储在HashMap对象本身,还存储在许多Map.Entry对象中。

JAVA多线程线程安全性基础

总而言之,在对象的状态中包含了任何可能影响其外部可见行为的数据。

何为共享的?

共享的 是指变量可同时被多个线程访问

何为可变的?

可变的 是指变量的值在其生命周期内可以发生变化。试想,如果一个共享变量的值在其生命周期内不会发生变化,那么在多个

线程访问它的时候,就不会出现数据不一致的现象,自然就不存在线程安全性问题了。

什么是线程安全性

当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,达到预期的效果,那么就称这个类是线程安全的。

如下启动10个线程,每个线程对inc执行1000次递增,并添加一个计时线程,预期效果应为10000,而实际输出值为6880,是一个小于10000的值,并未达到预期效果,因此INS类不是线程安全的,整个程序也不是线程安全的。原因是递增操作不是原子操作,并且没有适当的同步机制

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package hgh0808;
public class Test {
    public static void main(String[] args){
        for(int i = 0;i < 10;i++){
            Thread th = new Thread(new CThread());
            th.start();
        }
        TimeThread tt = new TimeThread();
        tt.start();
        try{
            Thread.sleep(21000);
        }catch(Exception e){
            e.printStackTrace();
        }
        System.out.println(INS.inc);
    }
}
---------------------------------------------------------------------
package hgh0808;
import java.util.concurrent.atomic.*;
public class TimeThread extends Thread{
    @Override
    public void run(){
        int count = 1;
        for(int i = 0;i < 20;i++){
            try{
                Thread.sleep(1000);
            }catch(Exception e){
                e.printStackTrace();
            }
            System.out.println(count++);
        }
    }
}
---------------------------------------------------------------------
package hgh0808;
public class CThread implements Runnable{
    @Override
    public void run(){
        for(int j = 0;j < 1000;j++){
            INS.increase();
        }
    }
}
---------------------------------------------------------------------
package hgh0808;
public class INS{
    public static volatile int inc = 0;
    public static void increase(){
            inc++;
    }
}
=====================================================================

执行结果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
6880

通过synchronized加锁机制,对INS类实现同步,如下得到了正确的运行结果,很容易可以看出,主调代码中并没有任何额外的同步或协同,此时的INS类是线程安全的,整个程序也是线程安全的

?
1
2
3
4
5
6
7
8
9
package hgh0808;
public class INS{
    public static volatile int inc = 0;
    public static void increase(){
        synchronized (INS.class){
            inc++;
        }
    }
}

执行结果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
10000

如何编写线程安全的代码
------------------------------------------------------------------------------------------------
如果当多个线程访问同一个可变的状态变量时没有使用合适的同步,那么程序就会出现错误,像上文中进行同步之前的代码
有三种方式可以修复这个问题:
*不在线程之间共享该状态变量
*将状态变量修改为不可变的变量
*在访问状态变量时使用同步
前两种方法是针对 共享 和 不变 这两个属性(见上文)解决问题,在有些情境下会违背程序设计的初衷(比如上文中INS类中的inc变量不可能不变,且在多核处理器的环境下为了提高程序性能,就需要多个线程同时处理,这样变量就必然要被多个线程共享)。
基于此,我们针对第三种方式------ 在访问状态变量时使用同步 展开讨论
在讨论第三种方式之前,我们先介绍几个简单的概念
原子性 :一个操作序列的所有操作要么不间断地全部被执行,要么一个也没有执行
竞态条件 :当某个计算的正确性取决于多个线程的的交替执行时序时,就会发生竞态条件。通俗的说,就是某个程序结果的正确性取决于运气时,就会发生竞态条件。(竞态条件并不总是会产生错误,还需要某种不恰当的执行时序)
常见的竞态条件类型:
*检查–执行(例如延迟初始化)
*读取–修改–写入(例如自增++操作)
针对以上两种常见的竞态条件类型,我们分别给出例子

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
延迟初始化(检查--执行)
--------------------------------------------------------------------
package hgh0808;
import java.util.ArrayList;
public class Test1 {
    public ArrayList<Ball> list;
    public ArrayList<Ball> getInstance(){
        if(list == null){
            list = new ArrayList<Ball>();
        }
        return list;
    }
}
class Ball{
}

大概逻辑是先判断list是否为空,若为空,创建一个新的ArrayList对象,若不为空,则直接使用已存在的ArrayList对象,这样可以保证在整个项目中list始终指向同一个对象。这在单线程环境中是完全没有问题的,但是如果在多线程环境中,list还未实例化时,A线程和B线程同时执行if语句,A和B线程都会认为list为null,A和B线程都会执行实例化语句,造成混乱。

?
1
2
3
自增++操作(读取--修改--写入)
------------------------------------------------------------------------
参考上文中为改进之前的代码(对INS类中inc的自增)

以上两个例子告诉我们,必须添加适当的同步策略,保证复合操作的原子性,防止竞态条件的出现

策略一:使用原子变量类,在java.util.concurrent.atomic包中包含了一些原子变量类

JAVA多线程线程安全性基础

?
1
2
3
4
5
6
7
8
9
10
11
package hgh0808;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
 
public class INS{
    public static AtomicInteger inc = new AtomicInteger(0);
    public static void increase(){
        inc.incrementAndGet();
    }
}

执行结果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
10000

值得注意的是,只有一个状态变量时,可以通过原子变量类实现线程安全。但是如果有多个状态变量呢?

设想一个情景

多个线程不断产生1到10000的随机数并且发送到一个计算线程,计算线程每获取一个数字n,就计算sinx在[0,n]上的积分并打印到控制台上,为了提高程序性能,设计一个缓存机制,保存上次的数字n和积分结果(两个状态变量)。如果本次的数字和上次的数字相等,直接打印积分结果,避免重复计算。

看代码:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
package hgh0808;
import java.util.concurrent.atomic.AtomicReference;
 
public class Calculate extends Thread{
    private final AtomicReference<Double> lastNumber  = new AtomicReference<Double>();  //缓存机制,原子变量类
    private final AtomicReference<Double> lastRes = new AtomicReference<Double>();      //缓存机制,原子变量类
    private static final double N = 100000;    //将区间[0,e]分成100000份,方便定积分运算
    public void service() throws Exception{
        getData();
        Thread.sleep(10000);   //等待MyQueue队列中有一定数量的元素后,再开始从其中取元素
        while(true){
            Double e;
                if(!MyQueue.myIsEmpty()){
                     e = MyQueue.myRemove();
                }else{
                    return;
                }
            if(e.equals(lastNumber.get())){
                System.out.println(lastNumber.get()+" "+lastRes.get());
            }else{
                Double temp = integral(e);
                lastNumber.set(e);
                lastRes.set(temp);
                System.out.println(e+" "+temp);
            }
            Thread.sleep(2000);
        }
    }
    public void getData(){   //创建并启动四个获取随机数的线程,这四个线程交替向MyQueue队列中添加元素
        Thread1 th1 = new Thread1();
        Thread2 th2 = new Thread2();
        Thread3 th3 = new Thread3();
        Thread4 th4 = new Thread4();
        th1.start();
        th2.start();
        th3.start();
        th4.start();
    }
    public Double integral(double e){    //计算定积分
        double step = (e-0)/N;
        double left = 0,right = step;
        double sum = 0;
        while(right <= e){
            double mid = left+(right-left)/2;
            sum+=Math.sin(mid);
            left+=step;
            right+=step;
        }
        sum*=step;
        return sum;
    }
}
---------------------------------------------------------------------
package hgh0808;
import java.util.LinkedList;
public class MyQueue {      //由于LinkedList是线程不安全的,因此需要将其改写为线程安全类
    private static LinkedList<Double> queue = new LinkedList<>();
    synchronized public static void myAdd(Double e){
        queue.addLast(e);
    }
    synchronized public static void myClear(){
        queue.clear();
    }
    synchronized public static int mySize(){
        return queue.size();
    }
    synchronized public static boolean myIsEmpty(){
        return queue.isEmpty();
    }
    synchronized public static double myRemove(){
        return queue.removeFirst();
    }
}
-----------------------------------------------------------------------
package hgh0808;
import java.util.Random;
public class Thread1 extends Thread{
    private double data;
    @Override
    public void run(){
        while(true){
            Random random = new Random();
            data = (double) (random.nextInt(10000)+1);
            if(MyQueue.mySize() > 10000){     //由于从队列中取元素的速度低于四个线程向队列中加元素的速度,因此队列的长度是趋于扩张的,当达到一定程度时,清空队列
                MyQueue.myClear();
            }
            MyQueue.myAdd(data);
            try {
                Thread.sleep(1000);
            }catch(Exception e){
                e.printStackTrace();
            }
        }
    }
}
------------------------------------------------------------------------
package hgh0808;
import java.util.Random;
public class Thread2 extends Thread{
    private double data;
    @Override
    public void run(){
        while(true){
            Random random = new Random();
            data = (double) (random.nextInt(10000)+1);
            if(MyQueue.mySize() > 10000){
                MyQueue.myClear();
            }
            MyQueue.myAdd(data);
            try {
                Thread.sleep(1000);
            }catch(Exception e){
                e.printStackTrace();
            }
        }
    }
}
-----------------------------------------------------------------------
package hgh0808;
import java.util.Random;
public class Thread3 extends Thread{
    private double data;
    @Override
    public void run(){
        while(true){
            Random random = new Random();
            data = (double) (random.nextInt(10000)+1);
            if(MyQueue.mySize() > 10000){
                MyQueue.myClear();
            }
            MyQueue.myAdd(data);
            try {
                Thread.sleep(1000);
            }catch(Exception e){
                e.printStackTrace();
            }
        }
    }
}
------------------------------------------------------------------------
package hgh0808;
import java.util.Random;
public class Thread4 extends Thread{
    private double data;
    @Override
    public void run(){
        while(true){
            Random random = new Random();
            data = (double) (random.nextInt(10000)+1);
            if(MyQueue.mySize() > 10000){
                MyQueue.myClear();
            }
            MyQueue.myAdd(data);
            try {
                Thread.sleep(1000);
            }catch(Exception e){
                e.printStackTrace();
            }
        }
    }
}

只看Calculate线程,不看其他线程和MyQueue中的锁机制,本问题的焦点在于Calculate线程中对多个状态变量的同步策略

存在问题:

尽管对lastNumber和lastRes的set方法的每次调用都是原子的,但仍然无法同时更新lastNumber和lastRes;如果只修改了其中一个变量,那么在这两次修改操作之间,其它线程将发现不变性条件被破坏了。换句话说,就是没有足够的原子性

**当在不变性条件中涉及多个变量时,各个变量间并不是彼此独立的,而是某个变量的值会对其它变量的值产生约束。因此当更新某一个变量时,需要在同一个原子操作中对其他变量同时进行更新。

改进 ================>加锁机制 内置锁 synchronized

之所以每个对象都有一个内置锁,只是为了免去显式地创建锁对象

synchronized修饰方法就是横跨整个方法体的同步代码块

非静态方法的锁-----方法调用所在的对象

静态方法的锁-----方法所在类的class对象

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public class Calculate extends Thread{
    private final AtomicReference<Double> lastNumber  = new AtomicReference<Double>();  //缓存机制,原子变量类
    private final AtomicReference<Double> lastRes = new AtomicReference<Double>();      //缓存机制,原子变量类
    private static final double N = 100000;    //将区间[0,e]分成100000份,方便定积分运算
    public void service() throws Exception{
        getData();
        Thread.sleep(10000);   //等待MyQueue队列中有一定数量的元素后,再开始从其中取元素
        while(true){
            Double e;
            synchronized (this){    //检查--执行 使用synchronized同步,防止出现竞态条件
                if(!MyQueue.myIsEmpty()){
                     e = MyQueue.myRemove();
                }else{
                    return;
                }
            }
            if(e.equals(lastNumber.get())){
                System.out.println(lastNumber.get()+" "+lastRes.get());
            }else{
                Double temp = integral(e);
                synchronized (this) {     //两个状态变量在同一个原子操作中更新
                    lastNumber.set(e);
                    lastRes.set(temp);
                }
                System.out.println(e+" "+temp);
            }
            Thread.sleep(2000);
        }
    }
    public void getData(){   //创建并启动四个获取随机数的线程,这四个线程交替向MyQueue队列中添加元素
        Thread1 th1 = new Thread1();
        Thread2 th2 = new Thread2();
        Thread3 th3 = new Thread3();
        Thread4 th4 = new Thread4();
        th1.start();
        th2.start();
        th3.start();
        th4.start();
    }
    public Double integral(double e){    //计算定积分
        double step = (e-0)/N;
        double left = 0,right = step;
        double sum = 0;
        while(right <= e){
            double mid = left+(right-left)/2;
            sum+=Math.sin(mid);
            left+=step;
            right+=step;
        }
        sum*=step;
        return sum;
    }
}

对于包含多个变量的不变性条件中,其中涉及的所有变量都需要由同一个锁来保护

?
1
2
3
4
synchronized (this) {     //两个状态变量在同一个原子操作中更新
                    lastNumber.set(e);
                    lastRes.set(temp);
                }

锁的重入

如果某个线程试图获得一个已经由它自己持有的锁,那么这个请求就会成功,“重入”意味着获取锁的操作的粒度是‘线程',而不是‘调用'。

重入的一种实现方式 :

为每个锁关联一个获取计数值和一个所有者线程。当计数值为0时,这个锁就被认为是没有被任何线程持有。当线程请求一个未被持有的锁时,JVM将记下锁的持有者,并且将获取计数值置为1。如果同一个线程再次获取这个锁,计数值将递增,当线程退出同步代码块时,计数器会相应地递减。当计数值为0时,这个锁将被释放。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
如果内置锁不可重入,那么以下这段代码将发生死锁(每个doSomething方法在执行前都会获取Father上的内置锁)
----------------------------------------------------------------------
public class Father{
  public synchronized void doSomething(){
  }
}
 
public class Son extends Father{
   @Override
   public synchronized void doSomething(){
       System.out.println("重写");
       super.doSomething();
   }
}

线程安全性与性能和活跃性之间的平衡

活跃性:是否会发生死锁饥饿等现象
性能:线程的并发度
不良并发的应用程序:可同时调用的线程数量,不仅受到可用处理资源的限制,还受到应用程序本身结构的限制。幸运的是,通过缩小同步代码块的作用范围,可以平衡这个问题。
缩小作用范围的原则====>当执行时间较长的计算或者可能无法快速完成的操作时,一定不能持有锁!!!

总结

本篇文章就到这里了,希望能给你带来帮助,也希望您能够多多关注服务器之家的更多内容!

原文链接:https://blog.csdn.net/Sqrt1230/article/details/119604604

延伸 · 阅读

精彩推荐