线程安全是并发编程中的一个重大关注点,所谓线程安全就是要保证在多个线程间读写共享数据保证正确性,如果不能保证,那就不是线程安全的了。
synchronized、ReentrantLock都是用来实现线程间同步,访问同步的代码需要先获得锁,保证每次都只能有一个线程进入同步块,代码执行完毕后释放锁,从而保证了线程间的安全。
所以在实现线程安全中需要考虑的核心问题就是“谁的锁,锁的谁”,只要记住这条规则,就大体上能够达到线程安全性的要求。
锁藏在什么地方?
每个类都有一把锁,暂且叫类锁,所有这个类的实例都可以获取这个类的锁;
每个实例也都有一把锁,暂且叫实例锁,类锁和实例锁不是相同的,只有实例自己才能获得自己的实例锁,不同的实例获得实例锁是不一样的,这个很关键。
谁的锁,锁的谁?
先从synchronized开始分析,还是以代码示例方式讲讲更好理解。
关键字synchronized有多种用法:
1.用在实例方法上:
public synchornized someMethod(){
...
}
谁的锁:当前实例的实例锁,不同实例获得的实例锁在这里不是同一把锁;
锁的谁:锁的是这个方法代码块,即线程要进入这个方法就必须先获得这个实例的锁,同一个实例互斥。
2.用在静态方法上:
public static synchornized someMethod(){
...
}
谁的锁:当前类的类锁,即该类的所有实例共享的锁
锁的谁:锁的是这个方法代码块,即线程要进入这个方法就必须先获得这个类的锁,该类的所有实例互斥。
3.用在表达式上:
synchronized(表达式){
...
}
谁的锁:表达式如果是个实例,则是该实例的实例锁,同一个实例互斥;如果表达式是this,则是当前类的实例的实例锁,等同于方式用在实例方法上,当前类的同一个实例互斥;如果表达式是个Class,则是这个类的锁,即线程要进入这个同步代码块需要先获取这个类的锁,该类的所有实例互斥;
锁的谁:锁的是synchronized里面的代码块,根据表达式的不同,实现不同的互斥目的。
所以以下几种情况都是错误的……………
先来个数据共享类吧:
public class Item {
private int value;
public Item(int value) {
this.value = value;
}
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
}
1.完全没有同步,这个不用说了。
public class LockerTest implements Runnable {
private static final Item item = new Item(0);
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
increase();
}
}
private void increase() {
int value = item.getValue();
value++;
item.setValue(value);
}
public static void main(String[] args) throws InterruptedException {
LockerTest test = new LockerTest();
Thread t1 = new Thread(test);
Thread t2 = new Thread(test);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(item.getValue());
}
}
2.表面上是加锁了,但是….不是同一个实例,锁不住的,这相当于一个线程各种拿各自的实例锁,线程间不是同步互斥!
只需要把increase改成静态方法的就ok了,用类锁,达到线程互斥效果。
public class LockerTest implements Runnable {
private static final Item item = new Item(0);
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
increase();
}
}
private synchronized void increase() {
int value = item.getValue();
value++;
item.setValue(value);
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread( new LockerTest());
Thread t2 = new Thread( new LockerTest());
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(item.getValue());
}
}
3.表面上是加锁了,但是….不是同一个实例,锁不住的!效果更上面的是一样的!修改为用同一个实例创建线程就好了,用实例锁搞定!
public class LockerTest implements Runnable {
private static final Item item = new Item(0);
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
increase();
}
}
private void increase() {
synchronized (this) {
int value = item.getValue();
value++;
item.setValue(value);
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new LockerTest());
Thread t2 = new Thread(new LockerTest());
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(item.getValue());
}
}
说说ReentrantLock,重入锁
ReentrantLock,重入锁,是synchronized的升级版,比synchronized功能更强大一些,在使用上是可以完全替代synchronized.
性能上,在JDK6开始,synchronized做了大量优化,二者性能相差不大了,但在JDK5重入锁要大大的高于synchronized。
实例说话:
public class LockerTest implements Runnable {
private static final Item item = new Item(0);
private static ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
try {
lock.lock();
increase();
} finally {
lock.unlock();
}
}
}
private void increase() {
int value = item.getValue();
value++;
item.setValue(value);
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new LockerTest());
Thread t2 = new Thread(new LockerTest());
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(item.getValue());
}
}
ReentrantLock是静态实例,则说明该lock是类的锁,所有该类的实例同步互斥;
否则是实例锁,只有同一个实例拥有这个锁,同实例访问lock和unlock之间的代码块是同步互斥的。
相比synchronized重入锁有如下几个优势:
1.可以反复进入,同一个线程获得几个锁,在释放的时候也需要同等释放,要不然会死锁的;
2.支持锁中断lockInterruptiblity(),防死锁,优先响应中断;
3.支持限时获取锁,lock.tryLock(int,TimeUnit);在给定的时间范围捏尝试获取锁;
4.支持尝试获取锁,tryLock不带参数,如果获取不到则立刻返回false,而不等待锁;
5.公平锁ReentrantLock(boolean fair),排队获取锁,性能相对低下;
6.配合Condition,可以让线程在合适的时间等待,得到通知后继续执行。