并发编程为我们带来了很多便利, 但同时也带来了线程安全问题。
线程安全
线程安全性的定义:
当多个线程访问某一个类时, 这个类始终能表示出正确的行为, 那么就称这个类是线程安全的。
其产生的原因可以归结如下:
1.共享数据: 只有共享的数据才会产生带来安全性问题。 如果是方法内部声明的变量, 其是在虚拟机栈中, 为每个线程独享, 不存在安全性问题。
2.多个线程对共享数据进行同时操作。多线程对同一共享数据进行同时性的操作,此时共享数据就会影响到彼此。
由线程安全引起的问题, 在此做一个示例:
public class UnsafeThread implements Runnable { private static int count = 0; public void increase(){ count++; } public void run() { for (int i = 0; i < 2000000; i++) { increase(); } } public static void main(String[] args) { UnsafeThread myThread = new UnsafeThread(); Thread thread1 = new Thread(myThread); Thread thread2 = new Thread(myThread); thread1.start(); thread2.start(); try { thread1.join(); thread2.join(); System.out.println(count); } catch (InterruptedException e) { e.printStackTrace(); } } }执行后, 两个线程, 每个线程对 count 进行了 2000000 次自增, 预期的结果应该是 4000000, 然而, 执行后发现结果基本上都不是对的, 每次不一样, 但都比 4000000 小。为什么?
i++ 的步骤, 应该是:
读->改->写
但是, 如果在一个线程读的时候, 还没写回去, 另一个线程也读了,那么就是有一次操作相当于没有效, 导致最后的结果就会比预期的少了。
互斥锁
因此, 为了解决该这些问题, 我们想到的对策:
- 消除共享数据: 想法很好, 但有些情况下想要完全的消除是不可能的, 我们只可能尽可能的减少共享数据。
- 限定同一时刻, 只有一个线程能对共享数据进行操作, 其他线程需要等到该线程处理完之后再进行操作。
互斥锁就可以解决这类问题。
互斥锁的特点
- 线程在进入同步代码块之前会自动获取锁,并且在退出同步代码块时会自动释放锁。
- 在同一时刻, 只有一个线程能持有这个锁。当线程
A尝试获取一个由线程B持有的锁时, 线程A必须等待或阻塞, 直到线程B释放了这个锁。 如果B不释放这个锁, 则A就需要一直等待。- 可重入性: 指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。
内置锁 synchronized
在本文章, 我们来讨论 Java 所提供的一种内置的互斥锁, 使用 synchronized 来修饰:
