微信公众号:
每天学Java
如有问题或建议,请公众号留言
最近建立一个公众号,希望大家多多关注。
首先要明确锁有很多种,它们是根据锁的状态或者锁的特性,锁的设计来进行分类的。下面来看一下有哪几种锁(这里只说明一部分)
在第二篇文章中我们使用的 synchronized 关键字就是一宗非公平锁,ReentrantLock是一种可通过构造函数设置是否公平的锁,默认是非公平锁。公平锁是什么呢?
既然公平,那就是根据申请获取锁的顺序来获取锁,他的优点是不会造成饥饿现象。那么非公平锁就是不会按照顺序来获取锁,这种情况下吞吐量变大,但是会造成饥饿现象,某些线程可能永远获取不到锁。
这里通过ReentrantLock看一下公平锁和非公平锁,当然大家也可以自己实现:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ELock {
static int i =0;
public static void main(String[] args) {
ELock eLock=new ELock();
// for (i=0;i<40;i++) {
eLock.Thread1();
eLock.Thread2();
eLock.Thread3();
// }
}
private Lock lock=new ReentrantLock();
public void Thread1(){
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
lock.lock();
try {
System.out.println(Thread.currentThread().getName()+"获取到锁");
}finally {
lock.unlock();
}
}
},"Thread1").start();
}
public void Thread2(){
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
lock.lock();
try {
System.out.println(Thread.currentThread().getName()+"获取到锁");
}finally {
lock.unlock();
}
}
},"Thread2").start();
}
public void Thread3(){
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
lock.lock();
try {
System.out.println(Thread.currentThread().getName()+"获取到锁");
}finally {
lock.unlock();
}
}
},"Thread3").start();
}
}
实验多少次都是:
Thread1
Thread1获取到锁
Thread2
Thread2获取到锁
Thread3
Thread3获取到锁
但是for循环注释去掉之后你就会发现,乱七八糟的,那是因为在循环中,你下次循环开始的时候,上次循环中某些线程已经开始加锁,可能在后续循环比之前的循环先得到锁,不能说明他不是公平锁
但是当你把true去掉后:
Thread1
Thread3
Thread2
Thread1获取到锁
Thread2获取到锁
Thread3获取到锁
并非按照顺序来获取锁,通常我们为了提高网站的吞吐量都会设置非公平锁。大家有兴趣可以自己实现下公平锁,我也会单独发一篇自己实现的公平锁。
独享锁就不多说了,从名字就可看出来就是独占锁之后其他线程不能获取锁,ReentrantLock是一种独享锁。共享锁也叫读锁,在Java中有一个接口叫做ReadWriteLock。也就是读写锁,它是读写锁一种实现。这种锁不同于独享锁,读读之间互斥,写写之间互斥,读写互斥,也就是在读与读中锁是共享的。
那我们来看一下读写锁:
private Lock lock=new ReentrantLock();
private ReadWriteLock rwLock = new ReentrantReadWriteLock();
public void readThread1(){
new Thread(new Runnable() {
@Override
public void run() {
rwLock.readLock().lock();
try {
System.out.println("readThread1:"+System.currentTimeMillis());
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
finally {
rwLock.readLock().unlock();
}
}
}).start();
}
public void readThread2(){
new Thread(new Runnable() {
@Override
public void run() {
rwLock.readLock().lock();
try {
System.out.println("readThread2:"+System.currentTimeMillis());
Thread.sleep(1000);
}catch (InterruptedException e) {
e.printStackTrace();
}finally {
rwLock.readLock().unlock();
}
}
}).start();
}
public void writeThread2(){
new Thread(new Runnable() {
@Override
public void run() {
rwLock.writeLock().lock();
try {
System.out.println("writeLock:"+System.currentTimeMillis());
Thread.sleep(1000);
}catch (InterruptedException e) {
e.printStackTrace();
}finally {
rwLock.writeLock().unlock();
}
}
}).start();
}
执行结果有这么几种可能
readThread1:1528006765902
readThread2:1528006765902
writeLock:1528006766905
或者
readThread1:1528006631536
writeLock:1528006631656
readThread2:1528006632658
或者
writeLock:1528006709103
readThread1:1528006710141
readThread2:1528006710141
我们可以看到读读之间获取锁基本上是同时的,但是读写之间是互斥,读-写-读是互斥进行的。
这里需要说明一下:使用读写锁的程序不一定会比互斥锁的程序快。在一些写操作比较多或是本身需要同步的地方并不多的程序中我们应该使用互斥锁,而在读操作远大于写操作的一些程序中我们应该使用读写锁来进行同步
在第二篇文章说明过synchronized 的缺点是粒度比较粗,在HashTable是一个线程安全的集合,但是现在很少使用,在面试中有很多次问到我:为什么使用 ConcurrentHashMap,就是因为它里面使用的是分段锁,而不是锁住整个集合,它只是锁住部分段。在JDK1.8中ConcurrentHashMap取消了Segment分段锁的数据结构,取而代之的是数组+链表(红黑树)的结构。而对于锁的粒度,调整为对每个数组元素加锁(Node)链表(n>8时)替换成红黑树时防止链表深度过深,且ConcurrentHashMap采用无锁算法(CAS)解决并发问题,属于乐观锁的范围(不在过多叙述了,在后续集合篇中会继续聊,大家可以自己去查阅一些资料,因为在面试中,集合被问到的次数一点也不少于多线程)。
乐观锁是认为不会发生并发冲突,只在提交操作时检查是否违反数据完整性。 乐观锁不能解决脏读的问题。悲观锁:认为一定会并发冲突,出现资源的抢占,需要屏蔽一切可能违反数据完整性的操作。这种锁可以聊到很多方面的知识,比如数据库的行级锁,表级锁,悲观锁在数据update前对数据库加入行级锁,但是如果没用到索引就会变成表级锁,锁住整个表。再有就是乐观锁不能解决脏读可以联想到数据库的事物隔离级别分别解决事物并发问题:脏读,不可重复读,幻读。
自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。这种方式增加了CPU的消耗,但是减少了线程切换消耗。