同步
同步,字面来看,有点一起工作的意思。但在线程同步中,“同”意为协同、互相配合。
比如:
A、B两个线程,并不是说两个线程必须同时一起工作,而是说互相配合工作,在某个时间可能线程A要等线程B去工作,之后线程A才能继续工作。如果理解不了,可以参考java线程(2)——模拟生产者与消费者中的例子。
思考:
为什么会有线程同步?上面例子中线程A为什么要等B工作之后才能继续工作?
在生产者和消费者的例子中,作为消费者,如果生产者还没生产,他就没办法消费只能等着,否则就会出现问题。同样,生产者也不能无止尽的生产,毕竟篮子的容量是有限的。
如果这个例子看不出来问题的严重性,我们可以想想银行取钱的例子,出现问题这后果就相当严重了。所以,多线程如果使用不当,很容易出现线程不安全的问题。尤其涉及同步问题时,一定要小心使用。
实现方式
主要有两种:synchronized和lock,前者被称为内置锁,后者叫外置锁。
关于这两者的不同之处,介绍之后再比较。
1、 synchronized关键字
在消费者和生产者的例子中,我们使用到了synchronized关键字,将篮子的“取”和“放”都做了限制,来实现线程同步。
2、lock
先来看一下他的接口定义,也是在包java.util.concurrent下。
package java.util.concurrent.locks;
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
void unlock();
Condition newCondition();
}
在上篇博客中介绍了一个使用synchronized关键字实现打印当前时间的方法,我们这里来修改一下。
/**
* 获得当前时间
*/
public static void getTime() {
lock.lock();
try {
System.out.println("1、进入获取时间方法=====");
Date date = new Date();
DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String time = format.format(date);
System.out.println("2、" + Thread.currentThread().getName() + ":"
+ time);
System.out.println("3、获取时间成功=====");
System.out.println();
} finally {
lock.unlock();
}
}
//调用
static Lock lock=null;
public static void main(String[] args) {
lock = new ReentrantLock();
for (int i = 0; i < 100; i++) {
new Thread(new Runnable() {
@Override
public void run() {
getTime();
}
}).start();
}
}
结果同样满足条件:
读写锁
Lock中的另一个亮点就是对读写锁的支持。在读写锁中,把对共享资源的访问者划分为读者和写者,读者进行读访问,写者进行写操作。但是,一个读写锁同时能有一个或多个读者,但不能同时既有读者又有写者。
java中的类为ReadWriteLock,提供了读锁和写锁的方法,返回类型都是Lock
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
举个例子:
在例子中,data为所要读写的数据,提供了读和写的方法。
private Object data = null;// 共享数据
/**
* 读数据
*/
public void get() {
try {
//准备数据
System.out.println(Thread.currentThread().getName()
+ " be ready to read data!");
//sleep一段时间,主要为了更明显的体现出差别,无实际意义
Thread.sleep((long) (Math.random() * 1000));
//打印取出数据
System.out.println(Thread.currentThread().getName()
+ "have read data :" + data);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 写数据
*
* @param data
*/
public void put(Object data) {
try {
System.out.println(Thread.currentThread().getName()
+ " be ready to write data!");
Thread.sleep((long) (Math.random() * 1000));
this.data = data; // 给data赋值
System.out.println(Thread.currentThread().getName()
+ " have write data: " + data);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
执行结果:
从结果上看,当程序刚启动时,多个读写线程同时触发,等待执行。无法实现“多读”,也影响了写的操作。
那么,现在来看下加上读写锁之后的效果。
ReadWriteLock rwl = new ReentrantReadWriteLock();
/**
* 读数据
*/
public void get(){
rwl.readLock().lock(); //读锁
try {
....略
} catch (InterruptedException e) {
e.printStackTrace();
}finally{
rwl.readLock().unlock(); //释放读锁
}
}
与上面没有使用读写锁相比,最大的不同在于,他可以实现“多读”,并且“读”与“写”互斥。例如:Thread0,2,4进入之后,可以同时读数据。并没有多条写数据同时写。
点睛之笔
1)Lock与ReadWriteLock
以Lock第一个例子ReentrantLock来说,他实现了标准的互斥操作,有点“独占”的感觉。这种情况下,不允许多读,多写,读写等情况发生。
读写锁ReadWriteLock中,允许多个读线程同时访问一个资源,但不允许多个写线程同时访问。更符合实际操作和需要。
2)Lock与synchronized
synchronized比较武断,只能实现互斥,无法实现读写锁的允许多读操作。在并发量比较小的情况下,他是一个不错的选择。但并发量较大的话,他的性能下降很严重。
Lock锁技术除了第一点介绍的优点外,他还具有可重入性,即可以进行多次加锁,也更具公平性。不过为了避免死锁,需要在finally中释放锁。