共享带来的问题及解决方案
2023-12-09 本文已影响0人
我可能是个假开发
一、共享带来的问题
@Slf4j
public class Test2 {
static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
count++;
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
count--;
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("{}", count);
}
}
16:33:49.787 [main] DEBUG juc.thread.Test2 - 101
以上的结果可能是正数、负数、零。因为 Java 中对静态变量的自增,自减并不是原子操作。
i++(i 为静态变量),产生的 JVM 字节码指令:
{
static int count;
descriptor: I
flags: ACC_STATIC
public juc.thread.Test2();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 6: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Ljuc/thread/Test2;
public static void main(java.lang.String[]) throws java.lang.InterruptedException;
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=4, locals=3, args_size=1
0: new #2 // class java/lang/Thread
3: dup
4: invokedynamic #3, 0 // InvokeDynamic #0:run:()Ljava/lang/Runnable;
9: ldc #4 // String t1
11: invokespecial #5 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;Ljava/lang/String;)V
14: astore_1
15: new #2 // class java/lang/Thread
18: dup
19: invokedynamic #6, 0 // InvokeDynamic #1:run:()Ljava/lang/Runnable;
24: ldc #7 // String t2
26: invokespecial #5 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;Ljava/lang/String;)V
29: astore_2
30: aload_1
31: invokevirtual #8 // Method java/lang/Thread.start:()V
34: aload_2
35: invokevirtual #8 // Method java/lang/Thread.start:()V
38: aload_1
39: invokevirtual #9 // Method java/lang/Thread.join:()V
42: aload_2
43: invokevirtual #9 // Method java/lang/Thread.join:()V
46: getstatic #10 // Field log:Lorg/slf4j/Logger;
49: ldc #11 // String {}
51: getstatic #12 // Field count:I
54: invokestatic #13 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
57: invokeinterface #14, 3 // InterfaceMethod org/slf4j/Logger.debug:(Ljava/lang/String;Ljava/lang/Object;)V
62: return
LineNumberTable:
line 12: 0
line 18: 15
line 24: 30
line 25: 34
line 26: 38
line 27: 42
line 28: 46
line 29: 62
LocalVariableTable:
Start Length Slot Name Signature
0 63 0 args [Ljava/lang/String;
15 48 1 t1 Ljava/lang/Thread;
30 33 2 t2 Ljava/lang/Thread;
Exceptions:
throws java.lang.InterruptedException
MethodParameters:
Name Flags
args
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: ldc #15 // class juc/thread/Test2
2: invokestatic #16 // Method org/slf4j/LoggerFactory.getLogger:(Ljava/lang/Class;)Lorg/slf4j/Logger;
5: putstatic #10 // Field log:Lorg/slf4j/Logger;
8: iconst_0
9: putstatic #12 // Field count:I
12: return
LineNumberTable:
line 5: 0
line 8: 8
}
Java 的内存模型如下,完成静态变量的自增,自减需要在主存和工作内存中进行数据交换
image.png
临界区 Critical Section
一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区
- 一个程序运行多个线程本身是没有问题的
- 问题出在多个线程访问共享资源
- 多个线程读共享资源其实也没有问题
- 在多个线程对共享资源读写操作时发生指令交错,就会出现问题
static int counter = 0;
static void increment()
// 临界区
{
counter++;
}
static void decrement()
// 临界区
{
counter--;
}
竞态条件 Race Condition
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件
二、解决方案
为了避免临界区的竞态条件发生,可以有以下方案
- 阻塞式:synchronized,Lock
- 非阻塞式:原子变量
1.synchronized
@Slf4j
public class Test2 {
static int count = 0;
static Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (lock){
count++;
}
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (lock){
count--;
}
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("{}", count);
}
}
19:51:50.506 [main] DEBUG juc.thread.Test2 - 0
优化:
@Slf4j
public class Test17 {
public static void main(String[] args) throws InterruptedException {
Room room = new Room();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
room.increment();
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
room.decrement();
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("{}", room.getCounter());
}
}
class Room {
private int counter = 0;
//方法上的相当于锁住了当前对象this
public synchronized void increment() {
counter++;
}
public synchronized void decrement() {
counter--;
}
//为了保证读取到的是正确的结果,而不是中间状态的结果,所以也要加锁
public synchronized int getCounter() {
return counter;
}
}
方法上的 synchronized,锁住当前对象this
class Test{
public synchronized void test() {
}
}
//等价于
class Test{
public void test() {
synchronized(this) {
}
}
}
静态方法上的synchronized,锁住当前类对象
class Test{
public synchronized static void test() {
}
}
等价于
class Test{
public static void test() {
synchronized(Test.class) {
}
}
}
class Number{
public static synchronized void a() {
sleep(1);
log.debug("1");
}
public static synchronized void b() {
log.debug("2");
}
}
public static void main(String[] args) {
Number n1 = new Number();
Number n2 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n2.b(); }).start();
}
因为静态方法锁的是类对象,所以n1和n2是一个类对象,能互斥。
结果:1s 后12, 或 2 1s后 1
public class ThreadUnsafeTest {
public static void main(String[] args) {
ThreadUnsafe test = new ThreadUnsafe();
for (int i = 0; i < 2; i++) {
new Thread(() -> test.method1(100), "Thread" + i).start();
}
}
}
class ThreadUnsafe{
ArrayList<String> list = new ArrayList<>();
public void method1(int loopNumber) {
for (int i = 0; i < loopNumber; i++) {
method2();
method3();
}
}
private void method2() {
list.add("1");
}
private void method3() {
list.remove(0);
}
}
可能存在线程安全问题:
Exception in thread "Thread1" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
at java.util.ArrayList.rangeCheck(ArrayList.java:659)
at java.util.ArrayList.remove(ArrayList.java:498)
at juc.thread.ThreadUnsafe.method3(ThreadUnsafeTest.java:37)
at juc.thread.ThreadUnsafe.method1(ThreadUnsafeTest.java:28)
at juc.thread.ThreadUnsafeTest.lambda$main$0(ThreadUnsafeTest.java:18)
at java.lang.Thread.run(Thread.java:748)
由于add操作不是原子的,所以存在两个线程同时去add时,最后的size被后来的add覆盖,导致两次add操作,size仍然是1,这时,两个线程再同时remove,size只有1,就会出现下标越界。
变成局部变量,则不会出现线程安全问题:
public class ThreadSafeTest {
public static void main(String[] args) {
ThreadSafe test = new ThreadSafe();
for (int i = 0; i < 2; i++) {
new Thread(() -> test.method1(400), "Thread" + i).start();
}
}
}
class ThreadSafe{
public void method1(int loopNumber) {
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < loopNumber; i++) {
method2(list);
method3(list);
}
}
private void method2(ArrayList<String> list) {
list.add("1");
}
private void method3(ArrayList<String> list) {
list.remove(0);
}
}
每个线程调用时会创建其不同实例,没有共享。
如果通过继承的方式,把变量共享出去了,则可能存在线程安全问题
public class ThreadExtendUnsafeTest {
public static void main(String[] args) {
ThreadUnsafeSub test = new ThreadUnsafeSub();
for (int i = 0; i < 2; i++) {
new Thread(() -> test.method1(500), "Thread" + i).start();
}
}
}
class ThreadExtendUnsafe{
public void method1(int loopNumber) {
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < loopNumber; i++) {
method2(list);
method3(list);
}
}
public void method2(ArrayList<String> list) {
list.add("1");
}
public void method3(ArrayList<String> list) {
list.remove(0);
}
}
class ThreadUnsafeSub extends ThreadExtendUnsafe{
@Override
public void method3(ArrayList<String> list) {
new Thread(()->list.remove(0)).start();
}
}
Exception in thread "Thread-999" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
at java.util.ArrayList.rangeCheck(ArrayList.java:659)
at java.util.ArrayList.remove(ArrayList.java:498)
at juc.thread.ThreadUnsafeSub.lambda$method3$0(ThreadExtendUnsafeTest.java:46)
at java.lang.Thread.run(Thread.java:748)
三、常见线程安全类
- String
- Integer
- StringBuffer
- Random
- Vector
- Hashtable
- java.util.concurrent 包下的类
这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。
Hashtable table = new Hashtable();
new Thread(()->{
table.put("key", "value1");
}).start();
new Thread(()->{
table.put("key", "value2");
}).start();
- 每个方法是原子的,多个线程操作单个方法,可以保证原子性。
- 但它们多个方法的组合不是原子的,需要在这多个方法外面再加锁才能保证原子性