线程安全是指在多线程并发访问时,程序仍能保持正确的行为和数据的完整性。即多个线程可以同时访问某个对象或方法,但不会损坏数据的一致性和正确性。线程安全的实现需要遵循一些规则,如使用同步机制保证多个线程对共享资源的访问互斥,避免竞态条件等。线程安全对于多线程编程非常重要,因为多个线程之间的相互影响和竞争可能会导致程序出现意想不到的错误和异常。
多线程中出现线程安全问题的情况包括:
1. 资源竞争:多个线程同时对同一个共享资源进行读写操作,可能会导致某些线程读到了不正确的数据或者覆盖了其他线程修改的数据,从而出现数据不一致或者数据丢失的情况。
2. 死锁:在多个线程之间存在互相等待的情况,其中每个线程都在等待其他线程释放资源,导致所有线程都无法继续执行,最终导致程序崩溃。
3. 线程间通信问题:多个线程之间需要进行通信时,如果没有合适的同步机制,可能会导致某些线程不停地等待或者阻塞,从而影响程序的执行效率或者导致死锁。
4. 指令重排问题:在多线程环境下,CPU 为了提高执行效率可能会对指令进行重排,如果对于不同线程执行的指令顺序没有保证,可能会导致某些线程读到的数据不正确,从而导致程序出现错误。
5. 缓存一致性问题:由于每个线程都有自己的缓存,多个线程之间读写共享变量时,可能会存在缓存不一致的情况,从而导致数据出现错误。
资源竞争
public class Main {
public static void main(String[] args) {
SharedResource sharedResource = new SharedResource();
Thread thread1 = new Thread(new MyRunnable(sharedResource));
Thread thread2 = new Thread(new MyRunnable(sharedResource));
thread1.start();
thread2.start();
}
}
class SharedResource {
private int count = 0;
public void increment() {
count++;
}
public int getCount() {
return count;
}
}
class MyRunnable implements Runnable {
private final SharedResource sharedResource;
public MyRunnable(SharedResource sharedResource) {
this.sharedResource = sharedResource;
}
@Override
public void run() {
for (int i = 0; i < 100000; i++) {
sharedResource.increment();
}
System.out.println("Thread " + Thread.currentThread().getName() + " finished, count = " + sharedResource.getCount());
}
}
这个示例代码定义了一个共享资源 SharedResource
,它包含一个计数器 count
,以及两个 MyRunnable
线程,它们都共享这个计数器。MyRunnable
的 run
方法中通过调用 sharedResource.increment()
方法来递增计数器的值。虽然这个方法看起来很简单,但是同时执行两个线程调用它时会发生竞争。如果两个线程同时读取 count
的值,递增它,然后再写入 count
的新值,就可能导致计数器的值不正确。
那么该如何解决这个线程安全问题呢?
我们可以使用锁机制来为这段代码解决这个问题。
创建锁有几种方式,一是使用synchronized关键字为代码块或类添加锁,还有是使用Lock类手动添加锁。
public class ThreadSafe {
public static void main(String[] args) {
SharedResource sharedResource = new SharedResource();
MyRunnable myRunnable = new MyRunnable(sharedResource);
Thread thread1 = new Thread(myRunnable,"Thread1");
Thread thread2 = new Thread(myRunnable,"Thread2");
thread1.start();
thread2.start();
}
}
class SharedResource {
private int count = 0;
public void increment() {
count++;
}
public int getCount() {
return count;
}
}
class MyRunnable implements Runnable {
private final SharedResource sharedResource;
public MyRunnable(SharedResource sharedResource) {
this.sharedResource = sharedResource;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
synchronized (this) {
sharedResource.increment();
System.out.println(Thread.currentThread().getName() + "|" + sharedResource.getCount());
try {
Thread.sleep(5);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
}
上面是使用sychronized关键字对代码块进行加锁操作,在synchronized()中需要为它添加一个监视器,这个监视器需要在不同线程中取相同的对象,因此在上面的例子中可以使用this取到myRunnable对象或者是使用shareResource对象作为监视器,需要注意,如果不同线程取到了不同的监视器,那么这个锁就不能起到线程安全的作用。比如如果你创建了两个Thread对象,并且监视器使用的是this,那么这个锁不能起到线程安全的作用。
public class ThreadSafe {
public static void main(String[] args) {
SharedResource sharedResource = new SharedResource();
MyRunnable myRunnable = new MyRunnable(sharedResource);
Thread thread1 = new Thread(myRunnable,"Thread1");
Thread thread2 = new Thread(myRunnable,"Thread2");
thread1.start();
thread2.start();
}
}
class SharedResource {
private int count = 0;
public void increment() {
count++;
}
public int getCount() {
return count;
}
}
class MyRunnable implements Runnable {
private final SharedResource sharedResource;
public MyRunnable(SharedResource sharedResource) {
this.sharedResource = sharedResource;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
Resourceincrement(sharedResource);
System.out.println(Thread.currentThread().getName() + "|" + sharedResource.getCount());
try {
Thread.sleep(5);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
public synchronized void Resourceincrement(SharedResource sharedResource){
sharedResource.increment();
}
}
上面的代码是使用synchronized关键字为方法设置锁,你可以将需要访问公共资源的方法使用synchronized关键字修饰,它会自动添加一个this监视器,因此在使用的时候需要注意。
public void run() {
Lock lock = new ReentrantLock();
for (int i = 0; i < 100; i++) {
lock.lock();
try {
Thread.sleep(5);
sharedResource.increment();
System.out.println(Thread.currentThread().getName() + "|" + sharedResource.getCount());
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
lock.unlock();
}
}
}
你也可以使用Lock来手动添加一个锁,Lock是JUC(java.util.concurrent)包下的一个接口,在java 1.5 版本引入的线程同步工具,用于保证多线程下安全的访问共享资源。
在多线程下访问共享资源时, 访问前加锁,访问后解锁,解锁的操作一般放入finally块中。
Lock的底层使用了AQS抽象队列同步器,主要使用了几个方法来维护锁的使用
tryAcquire:会尝试通过CAS获取一次锁。
addWaiter:将当前线程加入双向链表(等待队列)中
acquireQueued:通过自旋,判断当前队列节点是否可以获取锁。
lock大量使用CAS+自旋。因此根据CAS特性,lock建议使用在低锁冲突的情况下。目前java 1.6以后,官方对synchronized做了大量的锁优化(偏向锁、自旋、轻量级锁)。在非必要的情况下,建议使用synchronized做同步操作。
死锁
死锁是指两个或多个线程互相等待对方释放资源而处于一种无法继续执行的状态。为避免死锁,可以采用以下几种方法:
-
避免多个线程同时持有多个锁。
-
统一锁的获取顺序,避免不同的线程按不同的顺序获取锁。
-
尽量缩小锁的范围,避免长时间占用锁,尽快释放不需要的锁。
-
尽量避免在持有锁的情况下调用其他不受控制的方法。