线程安全的概念不容易定义,在《Java 并发编程实践》中,作者做出了如此定义:多个线程访问一个类对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,并且不需要额外的同步及在调用方法代码不必作其他的协调,这个类的行为仍然是正确的,那么这个类是线程安全的。
也就是说一堆线程去操作一个方法去控制同一个资源,由于是交替执行的,可能会出现一个数据一个线程正在运算还没来得急把数据写进去,结果被另外一个线程把这个数据的脏数据读取出去了。
这样说,可能有些朋友没有看明白,那么我们先来看一个示例,来演示一下什么是现成不安全的情况。
publicclass SafeThreadTest {
V v = new V();
publicstaticvoid main(String[] args) {
SafeThreadTest test = new SafeThreadTest();
test.test();
}
/**
* 开两个线程,分别调用V对象的打印字符串的方法
*/
publicvoid test(){
new Thread(new Runnable() {
@Override
publicvoid run() {
while(true){
v.printString("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
}
}
}).start();
new Thread(new Runnable() {
@Override
publicvoid run() {
while(true){
v.printString("BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB");
}
}
}).start();
}
/**
* 这个类负责打印字符串
* @author Administrator
*/
class V {
//创建一个锁对象
Lock lock = new ReentrantLock();
/**
* 为了能使方法运行速度减慢,我们一个字符一个字符的打印
* @param s
*/
publicvoid printString(String s){
//加锁,只允许一个线程访问
lock.lock();
try {
for(int i = 0;i<s.length();i++){
System.out.print(s.charAt(i));
}
System.out.println();
}finally{
//解锁,值得注意的是,这里锁的释放放到了finally代码块中,保证解锁工作一定会执行
lock.unlock();
}
}
}
}
使用这样的方式,与使用synchronized的功能一样,只不过这样使代码看起来更加面向对象一些,怎么加锁,怎么解锁一目了然。
另外,ReentrantLock其实比synchronized增加了一些功能,主要有:
等待可中断
这是指的当前持有锁的线程如果长期不释放锁,正在等待的线程可以放弃等待,处理其他事情。
公平锁
这个是说多个线程在等待同一个锁的时候,必须按照申请锁的时间顺序依次获得锁。synchronized中的锁是不公平锁,锁被释放的时候任何一个等待锁的线程都有机会获得锁,ReentrantLock默认也是不公平锁,可以使用构造函数使得其为公平锁。如果为true代表公平锁Lock lock = new ReentrantLock(true);
绑定条件
这个就是的条件锁。
在性能上,在jdk1.6之前的版本,使用ReentrantLock的性能要好于使用synchronized。而在jdk1.6开始,两者性能均差不多。
使用ReentrantLock类对象可以实现条件锁,这个类的对象有一个newCondition()方法,可以返回Condition对象,放在类里面作为成员变量,在被锁住的代码块里面调用里面的方法,该对象的await方法代表把该线程休眠,调用该对象的signl方法代表把这个对象休眠的那个线程叫醒。
条件锁可以实现三个线程的轮循,比如三个线程分别执行while(true)死循环来执行三个不同的方法,要求是实现三个方法的轮循执行,A方法完了是B方法,然后是C方法,然后再A方法,那就可以在ABC的方法类里面定义一个标志int 型变量代表该谁执行了,ReentrantLock,使用newCondition()方法生成三个Condition(条件)c1,c2,c3,在方法A里面如果标志不是1则使用while阻塞掉,循环中使用c1.await()让A睡眠,如果A被c1.signl()叫醒了,则执行里面需要执行的方法体,并且把标志改为2表示该B方法执行了,调用c2.signl()方法叫醒B方法,如此这般三个方法有序的轮循。
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Lunxun {
V v = new V();
public static void main(String[] args) {
Lunxun lx = new Lunxun();
lx.go();
}
private void go() {
new Thread(new Runnable() {
@Override
public void run() {
while(true){
v.m1();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
while(true){
v.m2();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
while(true){
v.m3();
}
}
}).start();
}
class V {
int token = 1;
Lock lock = new ReentrantLock();
Condition c1 = lock.newCondition();
Condition c2 = lock.newCondition();
Condition c3 = lock.newCondition();
void m1(){
lock.lock();
while(token!=1){
try {
c1.await();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println("AAAAAAAAAAAAAAAAAAAAA");
token = 2;
c2.signal();
lock.unlock();
}
void m2(){
lock.lock();
while(token!=2){
try {
c2.await();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println("BBBBBBBBBBBBBBBBBBBBB");
token = 3;
c3.signal();
lock.unlock();
}
void m3(){
lock.lock();
while(token!=3){
try {
c3.await();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println("CCCCCCCCCCCCCCCCCCCCCC");
token = 1;
c1.signal();
lock.unlock();
}
}
}
大量线程并发访问并修改和读取一个资源的时候,为了两全安全和性能,可以使用读写锁,意思是在线程进行写操作的时候,其他线程都不能进行读和写操作,而但有线程进行读操作的时候,其他线程都可以对该资源进行读操作,但不能进行写操作。
看下面一个小程序,用对象v来模拟一个数据对象,里面有读和写两个方法,开5个读线程5个写线程分别while(true)不断的读写数据,如何防止10个线程出现写与写,读和写的冲突呢?
实例如下:
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class TestReadWriteLock {
Random r = new Random();
V v = new V();
public static void main(String[] args) {
final TestReadWriteLock trwl = new TestReadWriteLock();
for (int i = 0; i < 3; i++) {
new Thread(new Runnable() {
@Override
public void run() {
while(true){
trwl.v.getdata(trwl.r.nextInt(100));
}
}
},"读线程"+ i).start();
}
for (int i = 0; i < 100; i++) {
new Thread(new Runnable() {
@Override
public void run() {
while(true){
trwl.v.setdata(trwl.r.nextInt(100));
}
}
},"写线程"+ i).start();
}
}
class V {
List<Integer> list = new ArrayList<Integer>();
ReadWriteLock rw = new ReentrantReadWriteLock() ;
public V(){
for (int i = 0; i < 100; i++) {
list.add(i);
}
}
void setdata(int i){
//rw.writeLock().lock();
//try {
System.out.println(Thread.currentThread().getName() + "正在写数据中》》》》》》》》》》》》》");
try {
Thread.sleep(r.nextInt(1000));
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
list.add(i);
System.out.println(Thread.currentThread().getName() + "写数据完毕!");
//} catch (Exception e) {
//e.printStackTrace();
//}finally{
//rw.writeLock().unlock();
//}
}
@SuppressWarnings("finally")
int getdata(int i){
//rw.readLock().lock();
int result;
//try {
System.out.println(Thread.currentThread().getName() + "正在读取数据《《《《《《《《《《《《《《");
try {
Thread.sleep(r.nextInt(1000));
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
result = list.get(i);
System.out.println(Thread.currentThread().getName() + "读取完毕!");
return result;
//} catch (Exception e) {
//// TODO Auto-generated catch block
//e.printStackTrace();
//}finally{
//rw.readLock().unlock();
//return 0;
//}
}
}
}
如果不使用读写锁,而使用synchronized我们称之为重量锁或者ReentranLock我们称之为轻量锁,虽然保证安全,但无法保证读和读不互斥。而不使用锁,将上面代码里的读写锁注释起来,则会出现在数据被写入的过程中有线程插进来读写数据。而读写锁使得安全和性能得到两全,因为毕竟读比写要频繁的多,读和读不互斥可以很大程度上提高性能。
以下是不加锁的运行结果:
读线程1正在读取数据《《《《《《《《《《《《《《
读线程2正在读取数据《《《《《《《《《《《《《《
读线程0正在读取数据《《《《《《《《《《《《《《
写线程0正在写数据中》》》》》》》》》》》》》
写线程1正在写数据中》》》》》》》》》》》》》
写线程2正在写数据中》》》》》》》》》》》》》
写线程3正在写数据中》》》》》》》》》》》》》
写线程4正在写数据中》》》》》》》》》》》》
我们会发现在0号写线程在写数据的时候,1号写线程也同时往里面写数据,这是我们最不想看到的情况。
而加上读写锁之后再看:
读线程1正在读取数据《《《《《《《《《《《《《《
读线程0正在读取数据《《《《《《《《《《《《《《
读线程2正在读取数据《《《《《《《《《《《《《《
读线程1读取完毕!
读线程2读取完毕!
读线程0读取完毕!
写线程2正在写数据中》》》》》》》》》》》》》
写线程2写数据完毕!
写线程2正在写数据中》》》》》》》》》》》》》
写线程2写数据完毕!
写线程2正在写数据中》》》》》》》》》》》》》
写线程2写数据完毕!
写线程2正在写数据中》》》》》》》》》》》》》
写线程2写数据完毕!
写线程0正在写数据中》》》》》》》》》》》》》
写线程0写数据完毕!
写线程4正在写数据中》》》》》》》》》》》》》
写线程4写数据完毕!
写线程3正在写数据中》》》》》》》》》》》》》
写线程3写数据完毕!
写线程3正在写数据中》》》》》》》》》》》》》
读线程1 2 0可以同时读数据,此时没有写线程在写数据,而当2号写线程在写数据的时候其他所有线程都在老老实实等着2号写线程把数据写完才开始进行读写操作。
再补充一点读写锁的重要功能,重入和降级(照抄API)
・重入
此锁允许 reader 和 writer 按照 ReentrantLock 的样式重新获取读取锁或写入锁。在写入线程保持的所有写入锁都已经释放后,才允许重入 reader 使用它们。
此外,writer 可以获取读取锁,但反过来则不成立。在其他应用程序中,当在调用或回调那些在读取锁状态下执行读取操作的方法期间保持写入锁时,重入很有用。如果 reader 试图获取写入锁,那么将永远不会获得成功。
・锁降级
重入还允许从写入锁降级为读取锁,其实现方式是:先获取写入锁,然后获取读取锁,最后释放写入锁。但是,从读取锁升级到写入锁是不可能的。
・锁获取的中断
读取锁和写入锁都支持锁获取期间的中断。
推荐java API文档的例子,例子使用的伪代码如下:
class CachedData {
Object data;
volatile boolean cacheValid;
ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
void processCachedData() {
rwl.readLock().lock();①
if (!cacheValid) {
// Must release read lock before acquiring write lock
rwl.readLock().unlock();②
rwl.writeLock().lock();③
// Recheck state because another thread might have acquired
// write lock and changed state before we did.
if (!cacheValid) {
data = ...④
cacheValid = true;
}
// Downgrade by acquiring read lock before releasing write lock
rwl.readLock().lock();⑤
rwl.writeLock().unlock(); // Unlock write, still hold read⑥
}
use(data);
rwl.readLock().unlock();⑦
}
}
这是一个典型的缓存应用问题,可以这样理解:假设A线程和B线程同时第一次进入processCachedData方法,均对这个方法加了读锁。这个时候cacheValid值为false,如果这个时候没有②和③处的代码,也就是说没有加写锁,会导致A线程将结果放到缓存中,B线程也会将结果放到缓存中,导致冲突。而②和③处的代码正是先释放读锁,然后加上写锁,使得另外的线程等待。然后在代码④处为缓存赋值。在代码⑤处先加上读锁,然后在代码⑥处释放写锁,然后use(data)是模拟使用数据,最后在代码⑦处释放写锁。这样保证了缓存的有效性。另外笔者指出一点:这里涉及到一个概念叫做锁降级。锁降级是说允许从写入锁降级为读取锁,其实现方式是:先获取写入锁,然后获取读取锁,最后释放写入锁。但是,不能从读锁升级到写锁。为的就是防止你直接放弃写入锁的话该线程就没有锁了,其他线程尤其是读线程一旦进入就会出现安全问题,因为你的整个读操作还没有完成,不允许写。