因为我们知道java是平台无关性语言,所以java为了忽略各个平台带来的不同的内存差异,定义了自己的JMM(java内存模型)
即对象产生是放在java堆中,线程使用的是java栈,每一次需要用到对象中的变量时,就要从java堆中抓取该变量的副本,然后进行修改使用后,再把使用后的变量放回java堆中,也就是所谓的read-load-use过程。正因为这样的过程不是原子性的,那么当多线程进行操作时,就会产生线程问题,下面给出一个不安全的例子。
定义一个接口,方便对不安全类的改进
public interface Count {
void add();
int getCount();
}
下面定义一个不安全类
public class NotSecureCount implements Count{
private int count = 0 ;
public void add(){
count++;
}
public int getCount(){
return this.count;
}
}
打开五个线程,每个线程分别调用一次add()方法,我们期望得到的结果是5,然后事实并不是这样
public class MyThread extends Thread {
private Count count;
public MyThread(Count count){
this.count=count;
}
@Override
public void run() {
this.count.add();
}
public static void main(String[] args){
Count count = new NotSecureCount();
for(int i=0;i<5;i++){
Thread thread = new MyThread(count);
thread.start();
}
Thread.sleep(5000);//等五个线程做完事情
System.out.println("五个线程作用后的结果:"+count.getCount());
}
}
结果是:
五个线程作用后的结果:4
正因为可视性问题,原子性问题(count++不是原子性操作,count=count+1),所以出现了作用效果不合我们意愿。那么下面我们可以通过几种方法对该非安全类进行改进
1.隐式锁,又称线程同步synchronized
那我们对我们的Count实现类做一次修改,如下:
public class SynchronizedCount implements Count{
private int count = 0;
// private byte[] lock = new byte[1];
@Override
public synchronized void add() {//在方法上面添加隐式锁
count++;
/*synchronized(lock){//或者添加同步代码块,这样的话可以减少里面,
//当一个对象持有该锁时,那么其余被该锁锁定的代码块也不能进入
count++;
}*/
}
@Override
public int getCount() {
return count;
}
}
java关键字synchronized提供了隐式锁的功能,可以让线程有序的使用代码块,但是这样不适高并发的情况下,因为这样相当于是串行的,后面的线程必须等前面的线程释放锁之后才能进入代码块,那么有了隐式锁,那显锁呢?
2.显示锁Lock和ReentrantLock,ReentrantReadWriteLock
Lock:这一个锁的接口,主要定义了锁的获取与释放等相关信息。
ReentantLock:可重入锁,是隐式锁的显式使用,区别在于该锁比隐式锁支持的吞吐量比较多,显示锁需要自己手动释放锁,是Lock接口的实现类。
ReentrantReadWriteLock:可重入读写锁,是可重入锁的改进版本,细化了读与写两个方面,他支持的功能是这样的:
加读锁时:读-读 可以进行并发处理,读-写 互斥,要等读锁释放才能进行读
加写锁时:写-写 互斥,写-读 互斥
接下来看修改后的安全类
关于可重入锁
public class ReentrantCount implements Count{
private int count = 0;
private ReentrantLock reentrantLock = new ReentrantLock();//使用同一个锁
@Override
public void add() {
reentrantLock.lock();//加锁
count++;
reentrantLock.unlock();//释放锁,如果代码快会抛出异常的话就要把释放锁放在finally里面
}
@Override
public int getCount() {
return this.count;
}
}
关于读写锁,为了更好的展示读写锁的功能所在,我对count实现类进行一次改进,写锁功能同上不必累述,测试读锁并发时的情况
public class ReadWriteCount implements Count{
private int count = 0;
private ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
private Lock readLock = reentrantReadWriteLock.readLock();
private Lock writeLock = reentrantReadWriteLock.writeLock();
@Override
public void add() {
writeLock.lock();
count++;
writeLock.unlock();
}
@Override
public int getCount() {
readLock.lock();//加锁
System.out.println("即将对count进行获取");
System.out.println("count:"+count);
System.out.println("获取结束");
readLock.unlock();//解锁
return count;
}
}
使用五个线程并发
public class MyThread extends Thread {
private Count count;
public MyThread(Count count,String name){
super(name);
this.count=count;
}
@Override
public void run() {
this.count.getCount();
}
public static void main(String[] args) throws InterruptedException{
Count count = new ReadWriteCount();
for(int i=0;i<5;i++){
Thread thread = new MyThread(count,i+"");
thread.start();
}
}
}
结果:
即将对count进行获取
即将对count进行获取
即将对count进行获取
count:0
获取结束
即将对count进行获取
即将对count进行获取
count:0
获取结束
count:0
获取结束
count:0
获取结束
count:0
获取结束
可以看到加读锁时,都是读锁的时候可以容纳并发的状态,所以又提高了并发量,不得不佩服java线程的这种锁思想