本文主要说明的是Java开发中涉及到的多线程编程的资源共享问题,即同步问题。
目前Java中可以支持资源同步的方法有:
- volatile关键字
- synchronized关键字
但是上述两种方法还是有些许区别的,以下举例来探讨。
首先我们来看如下代码,
int n = 0;
// 最终结果不足5000
public void testOfVolatile() {
// n是一般的对象属性,启用多线程去增加它的大小,但是最终无法保证是累加的预期值。
// 因为线程中不同步,并且CPU在执行的时候会使用缓存,指令执行数据是在缓存-内存中执行的,导致
// 多线程访问的时候数据不是同步状态。
FutureTask<Integer> futureTask = new FutureTask<Integer>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
System.out.println("Thread " + Thread.currentThread().getId() + " started, n: " + n);
int i = 0;
while(i < 10 * 1000) {
n ++;
i ++;
}
System.out.println("Thread " + Thread.currentThread().getId() + " finished, n: " + n);
return i;
}
});
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("Thread " + Thread.currentThread().getId() + " started, n: " + n);
int i = 0;
while(i < 10 * 1000) {
n ++;
i ++;
}
i = 0;
System.out.println("Thread " + Thread.currentThread().getId() + " finished, n: " + n);
}
};
Thread thread1 = new Thread(futureTask);
Thread thread2 = new Thread(runnable);
Thread thread3 = new Thread(runnable);
Thread thread4 = new Thread(runnable);
Thread thread5 = new Thread(runnable);
thread1.start();
thread2.start();
thread3.start();
thread4.start();
thread5.start();
// join当前线程
try {
thread1.join();
thread2.join();
thread3.join();
thread4.join();
thread5.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("final n: " + n);
}
如上述代码所示,对象属性n是一个普通的变量,在测试方法中,直接启用了5个线程来进行对n的增加,每个线程都增加10000的增量。期望是能够让n从0变化到50000。但是实际结果却不是这么回事,实际结果只有40000多,不到50000。这是因为该变量n,是一个普通变量,多线程访问该变量的时候不会处理同步问题,即每个线程在访问该变量的时候,不一定变量n的值是当前最新的状态,也许已经被其他线程改掉,但是还没有写入CPU的缓存中,导致变量数据不同步。
volatile
这个关键字描述的功能即表示,当前变量在被不同线程访问的时候是能够保证最新状态,即最新值的,但是在线程写入的时候却不会保证同步,所以不会造成线程阻塞,理论上它依旧不是线程安全的。
同样我们使用violatile关键字来修饰一个变量,看下面的代码:
volatile int m = 0;
// 最终结果不足50000
private void testOfVolatile2() {
// m是volatile修饰的对象属性,启用多线程去增加它的大小,但是最终无法保证是累加的预期值。
// 因为volatile仅保持可见性,但是不保证同步性,所以它不是线程安全的,仅仅保证不会阻塞。
// volatile保证读取的是最新的值,但是不保证写的时候是同步状态。
FutureTask<Integer> futureTask = new FutureTask<Integer>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
System.out.println("Thread " + Thread.currentThread().getId() + " started, m: " + m);
int i = 0;
while(i < 10 * 1000) {
m ++;
i ++;
}
System.out.println("Thread " + Thread.currentThread().getId() + " finished, m: " + m);
return i;
}
});
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("Thread " + Thread.currentThread().getId() + " started, m: " + m);
int i = 0;
while(i < 10 * 1000) {
m ++;
i ++;
}
i = 0;
System.out.println("Thread " + Thread.currentThread().getId() + " finished, m: " + m);
}
};
Thread thread1 = new Thread(futureTask);
Thread thread2 = new Thread(runnable);
Thread thread3 = new Thread(runnable);
Thread thread4 = new Thread(runnable);
Thread thread5 = new Thread(runnable);
thread1.start();
thread2.start();
thread3.start();
thread4.start();
thread5.start();
// join当前线程
try {
thread1.join();
thread2.join();
thread3.join();
thread4.join();
thread5.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("final m: " + m);
}
通过上述代码,我们得到的结果依旧不是50000,只是在某些线程开始工作访问m变量的时候,与先前一个结束的线程最终的m值相同,即表示volatile保证了可见性。但是无法在其写入的时候与其他线程保持同步,即不具备原子性。如此依赖,volatile是轻量级同步,它是在准备访问对象的时候不从CPU的缓存中读取,而是直接从主存中读取,而写入的时候则也是直接写入主存。
需要注意的是,volatile只能够修饰变量,不能够修饰方法。任何依赖于之前值的操作, 如i++, i = i *10使用volatile都不安全,
而诸如get/set, boolean这类可以使用volatile。
synchronized
synchronized关键字在实际应用中应该遇到很多,主要是通过对象锁来控制不同线程对同一变量对象的访问。我们先来看下如下代码:
public synchronized void plusNSync() {
n ++;
}
// 最终结果刚好50000
private void testOfSynchronized() {
FutureTask<Integer> futureTask = new FutureTask<Integer>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
System.out.println("Thread " + Thread.currentThread().getId() + " started, n: " + n);
int i = 0;
while(i < 10 * 1000) {
plusNSync();
i ++;
}
System.out.println("Thread " + Thread.currentThread().getId() + " finished, n: " + n);
return i;
}
});
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("Thread " + Thread.currentThread().getId() + " started, n: " + n);
int i = 0;
while(i < 10 * 1000) {
plusNSync();
i ++;
}
i = 0;
System.out.println("Thread " + Thread.currentThread().getId() + " finished, n: " + n);
}
};
Thread thread1 = new Thread(futureTask);
Thread thread2 = new Thread(runnable);
Thread thread3 = new Thread(runnable);
Thread thread4 = new Thread(runnable);
Thread thread5 = new Thread(runnable);
thread1.start();
thread2.start();
thread3.start();
thread4.start();
thread5.start();
// join当前线程
try {
thread1.join();
thread2.join();
thread3.join();
thread4.join();
thread5.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("final n: " + n);
}
在该段代码中,我们继续使用普通对象属性n,该变量未作任何修饰,但是我们为它增加了一个synchronized关键字修饰的方法。该方法使用当前的测试类对象锁来完成线程访问同步,而累加n的方法也仅仅在该方法中进行,每个线程都调用该方法plusNSync()。结果自然明了,最终的结果显示50000。
这里表示synchronized是重量级同步,不仅能够修饰变量,还能够修饰方法,它具备可见性及原子性。
但是需要注意:
- synchronized对象锁是针对堆内存中的对象,而不是栈中的对象引用,因此,当对象引用改变之后,那么当前执行环境下的同步锁也就失效了。
- 不要使用String类型的对象作为同步对象,因为String在String池中的不确定性也会导致各种问题。
ThreadLocal
对于ThreadLocal的误解主要有:
- ThreadLocal为解决多线程程序的并发问题提供了一种新的思路
- ThreadLocal的目的是为了解决多线程访问资源时的共享问题
还有很多文章在对比 ThreadLocal 与 synchronize 的异同。既然是作比较,那应该是认为这两者解决相同或类似的问题。上面的描述,问题在于,ThreadLocal 并不解决多线程 共享 变量的问题。既然变量不共享,那就更谈不上同步的问题。
对于ThreadLocal的合理解释,ThreadLoal 变量,它的基本原理是,同一个 ThreadLocal 所包含的对象(对ThreadLocal< String >而言即为 String 类型变量),在不同的线程中有不同的副本,及维护各自的变量对象。那么这里几点要注意:
- 因为每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用。这是也是 ThreadLocal 命名的由来
- 既然每个 Thread 有自己的实例副本,且其它 Thread 不可访问,那就不存在多线程间共享的问题
- 既无共享,何来同步问题,又何来解决同步问题一说?
既然如此,那么ThreadLocal的合理应用场景又是如何?引用它的官方解释如下:
“ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被private static
修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。”
总体来说,ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景。后文会通过实例详细阐述该观点。另外,该场景下,并非必须使用 ThreadLocal ,其它方式完全可以实现同样的效果,只是 ThreadLocal 使得实现更简洁。
我们来看如下代码:
public class TestOfThreadLocal {
// Thread name: Thread-1, ThreadLocal hashCode: 1001240990, Instance hashCode: 896159899, Value: 0
// Thread name: Thread-2, ThreadLocal hashCode: 1001240990, Instance hashCode: 297626270, Value: 0
// Thread name: Thread-0, ThreadLocal hashCode: 1001240990, Instance hashCode: 413036867, Value: 0
// Thread name: Thread-2, ThreadLocal hashCode: 1001240990, Instance hashCode: 297626270, Value: 01
// Thread name: Thread-1, ThreadLocal hashCode: 1001240990, Instance hashCode: 896159899, Value: 01
// Thread name: Thread-2, ThreadLocal hashCode: 1001240990, Instance hashCode: 297626270, Value: 012
// Thread name: Thread-0, ThreadLocal hashCode: 1001240990, Instance hashCode: 413036867, Value: 01
// Thread name: Thread-2, ThreadLocal hashCode: 1001240990, Instance hashCode: 297626270, Value: 0123
// Thread name: Thread-1, ThreadLocal hashCode: 1001240990, Instance hashCode: 896159899, Value: 012
// innerClass thread Thread-2 finished.
// Thread name: Thread-0, ThreadLocal hashCode: 1001240990, Instance hashCode: 413036867, Value: 012
// Thread name: Thread-1, ThreadLocal hashCode: 1001240990, Instance hashCode: 896159899, Value: 0123
// Thread name: Thread-0, ThreadLocal hashCode: 1001240990, Instance hashCode: 413036867, Value: 0123
// innerClass thread Thread-0 finished.
// innerClass thread Thread-1 finished.
// Whole finished.
// 如上述结果所示,在当前测试类中,创建了一个单独的对象InnerClass,
// 所以在多线程操作innerClass的方法时,实际是同一个InnerClass对象。
// InnerClass对象内直接引用了静态内部类Counter类的静态变量couner,
// 类型为ThreadLocal。所以在理论上来说,ThreadLocal是内存共享的。
// 但是ThreadLocal封装的StringBuilder对象,在三个线程中却包含了3个内容,
public static void main(String[] args) {
int thread = 3;
// start 3 threads
final CountDownLatch latch = new CountDownLatch(thread);
final InnerClass innerClass = new InnerClass();
for (int i = 1 ; i <= thread; i ++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 4; j ++) {
innerClass.add(String.valueOf(j));
innerClass.print();
}
System.out.println("innerClass thread " + Thread.currentThread().getName() + " finished.");
latch.countDown();
}
}).start();
}
try {
latch.await(); // 等待latch计数到0
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Whole finished.");
}
private static class InnerClass {
public void add(String string) {
StringBuilder stringBuilder = Counter.counter.get();
Counter.counter.set(stringBuilder.append(string));
}
public void print() {
System.out.printf("Thread name: %s, ThreadLocal hashCode: %s, Instance hashCode: %s, Value: %s\n",
Thread.currentThread().getName(),
Counter.counter.hashCode(),
Counter.counter.get().hashCode(),
Counter.counter.get().toString());
}
}
private static class Counter {
// counter是Counter静态内部类的静态变量,理论上来说进程中是唯一的
private static ThreadLocal<StringBuilder> counter = new ThreadLocal<StringBuilder>() {
@Override
protected StringBuilder initialValue() {
return new StringBuilder();
}
};
}
}
从上述结果可见,
- Counter类为静态内部类,其ThreadLocal变量为静态变量,理论上来说,进程内存中应该只有一份实例。
- Counter类在多线程中被调用的时候,ThreadLocal对象内包裹的StringBuilder却产生了不同的对象副本,即被clone 了一样。
- 每个StringBuilder的结果最终都是打印为0123,即表示每个线程之间对于StringBuilder没有产生任何影响。
Lock
除了Sychronized关键字使用对象锁完成同步外,Java还提供了一种锁,Lock。
Lock与synchronized使用上来说,有如下区别:
- Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现,synchronized是在JVM层面上实现的,不但可以通过一些监控工具监控synchronized的锁定,而且在代码执行时出现异常,JVM会自动释放锁定,但是使用Lock则不行,lock是通过代码实现的,要保证锁定一定会被释放,就必须将 unLock()放到finally{} 中;
- synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
- Lock可以让等待锁的线程响应中断,线程可以中断去干别的事务,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
- 通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
- Lock可以提高多个线程进行读操作的效率。
在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。
举个例子:
当有多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作会发生冲突现象,但是读操作和读操作不会发生冲突现象。但是采用synchronized关键字来实现同步的话,就会导致一个问题:如果多个线程都只是进行读操作,所以当一个线程在进行读操作时,其他线程只能等待无法进行读操作。因此就需要一种机制来使得多个线程都只是进行读操作时,线程之间不会发生冲突,通过Lock就可以办到。另外,通过Lock可以知道线程有没有成功获取到锁。这个是synchronized无法办到的。
下面我们就来探讨一下java.util.concurrent.locks包中常用的类和接口。
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
首先要说明的就是Lock,通过查看Lock的源码可知,Lock是一个接口。
下面来逐个讲述Lock接口中每个方法的使用,lock()、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly()是用来获取锁的。unLock()方法是用来释放锁的。
在Lock中声明了四个方法来获取锁,那么这四个方法有何区别呢?
lock()
lock()方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。
由于在前面讲到如果采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此一般来说,使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。通常使用Lock来进行同步的话,是以下面这种形式去使用的:
Lock lock = ...;
lock.lock();
try{
//处理任务
}catch(Exception ex){
}finally{
lock.unlock(); //释放锁
}
tryLock()
tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。
tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。
所以,一般情况下通过tryLock来获取锁时是这样使用的:
Lock lock = ...;
if(lock.tryLock()) {
try{
//处理任务
}catch(Exception ex){
}finally{
lock.unlock(); //释放锁
}
}else {
//如果不能获取锁,则直接做其他事情
}
lockInterruptibly()
lockInterruptibly()方法比较特殊,当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就使说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。
由于lockInterruptibly()的声明中抛出了异常,所以lock.lockInterruptibly()必须放在try块中或者在调用lockInterruptibly()的方法外声明抛出InterruptedException。
因此lockInterruptibly()一般的使用形式如下:
public void method() throws InterruptedException {
lock.lockInterruptibly();
try {
//.....
}
finally {
lock.unlock();
}
}
注意,当一个线程获取了锁之后,是不会被interrupt()方法中断的。因为本身在前面的文章中讲过单独调用interrupt()方法不能中断正在运行过程中的线程,只能中断阻塞过程中的线程。因此当通过lockInterruptibly()方法获取某个锁时,如果不能获取到,只有进行等待的情况下,是可以响应中断的。而用synchronized修饰的话,当一个线程处于等待某个锁的状态,是无法被中断的,只有一直等待下去。
ReentrantLock
ReentrantLock,意思是“可重入锁”,关于可重入锁的概念在下一节讲述。ReentrantLock是唯一实现了Lock接口的类,并且ReentrantLock提供了更多的方法。下面通过一些实例看具体看一下如何使用ReentrantLock。
lock
private ArrayList<String> list = new ArrayList<>();
private Lock lock = new ReentrantLock();
// lock()方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。
private static void testOfLock() {
final TestOfLock testOfLock = new TestOfLock();
Thread thread1 = new Thread() {
@Override
public void run() {
testOfLock.lockInsert(Thread.currentThread());
}
};
Thread thread2 = new Thread() {
@Override
public void run() {
testOfLock.lockInsert(Thread.currentThread());
}
};
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("final list: " + testOfLock.list.toArray());
}
public void lockInsert(Thread thread) {
lock.lock(); // 获取锁
try {
System.out.println("线程" + thread.getName() + "得到了锁");
for (int i = 0; i < 5; i ++) {
list.add(String.valueOf(i));
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
System.out.println("线程" + thread.getName() + "释放了锁");
}
}
tryLock()
private ArrayList<String> list = new ArrayList<>();
private Lock lock = new ReentrantLock();
// tryLock()方法是有返回值的,
// 它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,
// 也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。而后的动作也就不执行了。
private static void testOfTryLock() {
final TestOfLock test = new TestOfLock();
Thread thread1 = new Thread() {
@Override
public void run() {
test.tryLockInsert(Thread.currentThread());
}
};
Thread thread2 = new Thread() {
@Override
public void run() {
test.tryLockInsert(Thread.currentThread());
}
};
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("final list: " + test.list.toArray());
}
public void tryLockInsert(Thread thread) {
if (lock.tryLock()) {
try {
System.out.println("线程" + thread.getName() + "得到了锁");
for (int i = 0; i < 5; i ++) {
list.add(String.valueOf(i));
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
System.out.println("线程" + thread.getName() + "释放了锁");
}
} else {
System.out.println("线程" + thread.getName() + "获取锁失败");
}
}
lockInterruptibly()
private ArrayList<String> list = new ArrayList<>();
private Lock lock = new ReentrantLock();
// 当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。
// 也就使说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,
// 而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。
// 由于lockInterruptibly()的声明中抛出了异常,所以lock.lockInterruptibly()必须放在
// try块中或者在调用lockInterruptibly()的方法外声明抛出InterruptedException.
private static void testOfInterruptLock() {
TestOfLock test = new TestOfLock();
Thread thread1 = test.new MyThread(test); // 必须使用对象new方法来创建内部类
Thread thread2 = test.new MyThread(test);
thread1.start();
thread2.start();
// 让当前线程等待2秒钟后,直接中断thread2
try {
Thread.sleep(2000l);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread2.interrupt();
}
public void interruptLockInsert(Thread thread) throws InterruptedException {
lock.lockInterruptibly(); //注意,如果需要正确中断等待锁的线程,必须将获取锁放在外面,然后将InterruptedException抛出
try {
System.out.println("线程" + thread.getName() + "得到了锁");
long startTime = System.currentTimeMillis();
for (;;) { // 不停循环,尽量延长测试时间
if (System.currentTimeMillis() - startTime > Integer.MAX_VALUE) {
break;
}
// 插入数据
}
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println("线程" + thread.getName()+"执行 finally");
lock.unlock();
System.out.println("线程" + thread.getName() + "释放了锁");
}
}
class MyThread extends Thread {
private TestOfLock mTest = null;
public MyThread(TestOfLock test) {
this.mTest = test;
}
@Override
public void run() {
try {
mTest.interruptLockInsert(Thread.currentThread());
} catch (InterruptedException e) {
// e.printStackTrace();
System.out.println("线程 " + Thread.currentThread().getName() + " 被中断");
}
}
}
运行之后,发现thread2能够被正确中断。这个就是跟synchronized的区别点。
参考文章: