通过线程池同时执行多个线程,这个时候需要特别注意共享变量的并发访问,如果不做处理,很容易出现线程安全问题。
Java通过synchronized关键字支持线程同步,可用于同步方法或者同步代码块。
当一个共享变量被volatile修饰时,它会保证修改的值会立即被保存到主存,当有其他线程需要读取时,它会去内存中读取新值。
package cn.thread4;
/**
* 测试volatile
* 保持数据可见性(同步),但不保持原子性(读取-操作-返回)
* 现很少使用,因为机器性能高,很少出现cpu忙不过来的情况
* @author Chill Lyn
*
*/
public class TestVolatile {
private static volatile int num = 0;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
// 死循环保持cpu忙碌状态
while (num == 0) {
}
}).start();
// 一秒后更改数值,循环停止
Thread.sleep(1000);
num = 1;
}
}
而普通共享变量不能保证可见性,因为其被修改后,什么时候被写入主存是不确定的,当其他线程读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。
Java在内部使用所谓的监视器monitor,也称为监视器锁monitor lock或内在锁intrinsic lock来管理同步。监视器绑定在对象上。例如,当使用同步方法时,每个方法都共享相应对象的相同监视器。所有的隐式监视器都实现了重入reentrant特性。重入的意思是锁绑定在当前线程上。线程可以安全地多次获取相同的锁,而不会产生死锁。(例如同步方法调用相同对象的另一个同步方法时)
并发API支持多种显式锁,它们由Lock接口规定,用于替代synchronized的隐式锁。锁对细粒度的控制提供多种方法,因此比隐式监控器具有更大开销
ReentrantLock是互斥锁,与通过synchronized访问的隐式监视器具有相同行为,但是具有扩展功能,就像它的名称一样,这个锁实现类重入特性,就是隐式监控器一样。
package com.test.lock;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Arith {
private int number = 0;
// 创建一个锁对象
private Lock lock = new ReentrantLock();
public void increament() {
lock.lock(); // 锁住对象
try {
number ++;
}finally {
lock.unlock(); // 释放锁
}
}
public int getNumber() {
return number;
}
}
package com.test.lock;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class Test {
public static void main(String[] args) {
ExecutorService service = Executors.newCachedThreadPool();
Arith arith = new Arith();
Runnable task = new Runnable() {
@Override
public void run() {
arith.increament();
}
};
for(int i=1;i<=10000;i++) {
service.submit(task);
}
service.shutdown();
try {
boolean b = service.awaitTermination(60, TimeUnit.DAYS);
if(b) {
System.out.println("执行结果:"+arith.getNumber());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
锁可以通过lock()来获取,通过unlock()释放。吧代码包装在try-finally代码块中来确保异常情况下的解锁非常重要。这个方法时线程安全的,就像同步副本那样。如果另一个线程已经拿到锁了,再次调用lock会阻塞当前线程,直到锁被释放。在任意给定时间内,只有一个线程可以拿到锁。tryLock方法时lock的替代,它尝试拿锁而不阻塞当前线程。
ReadWriteLock接口规定了锁的另一种概念,包含用于读写访问的一对锁。读写锁的理念是,只要没有任何线程写入变量,并发读取变量通常是安全的。所以读锁可以同时被多个线程持有,只要没有线程持有写锁。这样可以提升性能和吞吐量,因为读取比写入更频繁。
package com.test.lock;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockTest {
public static void main(String[] args) {
ExecutorService service = Executors.newCachedThreadPool();
Map<String, String> map = new HashMap<String, String>();
ReadWriteLock lock = new ReentrantReadWriteLock();
Runnable write = new Runnable() {
@Override
public void run() {
lock.writeLock().lock(); // 获取写锁
try {
System.out.println(Thread.currentThread()+"-->写入数据");
map.put("home", "shanghai");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}finally {
lock.writeLock().unlock();
}
}
};
Runnable read = new Runnable() {
@Override
public void run() {
lock.readLock().lock();
try {
System.out.println(Thread.currentThread()+"-->"+map.get("home"));
System.out.println(Thread.currentThread()+"---读取完毕");
}finally {
lock.readLock().unlock();
}
}
};
service.submit(read);
service.submit(read);
service.submit(write);
}
}
上面的例子在暂停5秒之后,首先获取写锁来向map添加新值。在这个任务完成之前,另一个任务被启动,尝试读取map中的元素,当执行这一代码示例时,你会注意到读任务需要等待写任务完成。在释放了写锁之后,读任务才会执行,并且同时打印结果。它们不需要相互等待完成,因为读锁可以安全同步获取,只要没有其他线程获取了写锁。
Java8自带了一种新锁,StampedLock,它同样支持读写锁。与ReadWriteLock不同的,StampedLock的锁方法会返回表示为Long的标记。可以使用这些标记来释放锁,或者检查锁是否有效。此外StampedLock支持乐观锁optimistic locking模式。
StampedLock lock=new StampedLock();
...
long stamp=0;
try{
stamp=lock.writeLock();
}finally{
lock.unlockWrite(stamp);
}
...
long stamp=0;
try{
stamp=lock.readLock();
}finally{
lock.unlockRead(stamp);
}
通过readLock()或readLock()来获取读锁或写锁会返回一个标记,它可以在稍后用于在finally中解锁。要记住StampedLock并没有实现重入特性。每次调用加锁都会返回一个新的标记,并且在没有可用的锁时阻塞,即使相同线程已经拿到锁了。所以需要额外注意不要出现死锁。
就像前面的ReadWriteLock例子那样,两个读任务都需要等待写锁释放。之后两个读任务同时在控制台打印信息,因为多个读操作不会相互阻塞,只要没有线程拿到写锁。
乐观的读锁通过调用tryOptimisticRead()获取,它总是返回一个标记而不阻塞当前线程,无论锁是否真正可用,如果已经有写锁被拿到,返回的标记为0。总是需要通过lock.validate(stamp)检查标记是否有效
long stamp=lock.tryOptimisticRead();
try{
System.out.println(lock.validate(stamp));
sleep(1000);
System.out.println(lock.validate(stamp));
}finally{
lock.unlock(stamp);
}
乐观锁在刚刚拿到锁之后是有效地。和普通的读锁不同的是,乐观锁不阻止其他线程同时获取写锁。在第一个线程暂停1秒之后,第二个线程拿到写锁而无需等待乐观锁被释放。此时,乐观的读锁就失效了。甚至当写锁被释放时,乐观的读锁仍然处于无效状态。所以在使用乐观锁时,需要每次在访问任何共享变量之后都要检查锁,来确保读锁仍然有效。有时,将读锁转换为写锁而不用再次解锁和加锁十分实用。StampedLock为这种目的提供了tryConvertToWriteLock(stamp)方法:
long stamp=lock.readLock();
try{
stamp=lock.tryConvertToWriteLock(stamp);
if(stamp==0L){
System.out.println("cannot convert");
stamp=lock.writeLock();
}
}finally{
lock.unlock(stamp);
}