多线程系列二(锁)
使用多线程可以最大限度的利用CPU,提高程序的执行效率,但是同样也会产生线程安全的问题,如何保证线程安全,就涉及到了今天博客中要介绍的锁。
加锁可以使用synchronized和lock两种方式,经过试验可以看出,当测试的数据量足够多的时候,使用lock方式在性能上要更快一些,在下面我们分别介绍这两种方式:
Synchronized加锁方式分为在同步方法和同步块级别上进行锁住对象。
同步方法:
public static synchronized void output2(String name){ int len=name.length();
for(int i=0;i<len;i++){ System.out.print(name.charAt(i)); } System.out.println();
} |
同步块:
static class Outputer{ public void output(String name){ int len=name.length(); synchronized (Outputer.class) { for(int i=0;i<len;i++){ System.out.print(name.charAt(i)); } System.out.println(); }
} } |
比较这两种方式,一般选择使用同步块的方式,哪个对象需要加上锁我们就在该对象所在的操作上加锁,如果在整个类或者整个方法上加锁,可能这个方法涉及到了一些别的变量就会被锁住,很容易发生死锁。
Lock加锁方式:
在javaAPI中,有一个java.util.concurrent.locks.Lock接口的实现类,ReentrantLock与ReentrantReadWriteLock的内部类中的ReadLock与WriteLock;分别叫重入锁,读入锁,写入锁。
下面是使用ReentrantLock的例子:
//输出类 static class Outputer{ Lock lock=new ReentrantLock();
public void output(String name){ int len=name.length(); lock.lock(); try{ for(int i=0;i<len;i++){ System.out.print(name.charAt(i)); } System.out.println(); }finally{ lock.unlock(); } } } |
每一个方法在加锁之后,在执行完锁对象的操作都要进行锁的释放。Lock()方法的源代码实现中有两个具体的实现类,NonfairSync与FairSync,刚好对应的一个是非公平锁,一个是公平锁。下面是别人总结的关于ReentrantLock采用非公平锁的时候程序执行的顺序:
ReentrantLock在采用非公平锁构造时,首先检查锁状态,如果锁可用,直接通过CAS设置成持有状态,且把当前线程设置为锁的拥有者。 如果当前锁已经被持有,那么接下来进行可重入检查,如果可重入,需要为锁状态加上请求数。如果不属于上面两种情况,那么说明锁是被其他线程持有, 当前线程应该放入等待队列。 在放入等待队列的过程中,首先要检查队列是否为空队列,如果为空队列,需要创建虚拟的头节点,然后把对当前线程封装的节点加入到队列尾部。由于设置尾部节点采用了CAS,为了保证尾节点能够设置成功,这里采用了无限循环的方式,直到设置成功为止。 在完成放入等待队列任务后,则需要维护节点的状态,以及及时清除处于Cancel状态的节点,以帮助垃圾收集器及时回收。如果当前节点之前的节点的等待状态小于1,说明当前节点之前的线程处于等待状态(挂起),那么当前节点的线程也应处于等待状态(挂起)。挂起的工作是由LockSupport类支持的,LockSupport通过JNI调用本地操作系统来完成挂起的任务(java中除了废弃的suspend等方法,没有其他的挂起操作)。 在当前等待的线程,被唤起后,检查中断状态,如果处于中断状态,那么需要中断当前线程。 |
下面是一个应用读写锁的示例:
package com.dmsd.thread;
import java.util.Random; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockTest { public static void main(String[] args) { final Queue3 q3 = new Queue3(); for(int i=0;i<3;i++) { new Thread(){ public void run(){ while(true){ q3.get(); } }
}.start();
new Thread(){ public void run(){ while(true){ q3.put(new Random().nextInt(10000)); } }
}.start(); }
} }
class Queue3{ private Object data = null;//共享数据,只能有一个线程能写该数据,但可以有多个线程同时读该数据。 ReadWriteLock rwl = new ReentrantReadWriteLock(); public void get(){ rwl.readLock().lock(); try { System.out.println(Thread.currentThread().getName() + " be ready to read data!"); Thread.sleep((long)(Math.random()*1000)); System.out.println(Thread.currentThread().getName() + "have read data :" + data); } catch (InterruptedException e) { e.printStackTrace(); }finally{ rwl.readLock().unlock(); } }
public void put(Object data){
rwl.writeLock().lock(); try { System.out.println(Thread.currentThread().getName() + " be ready to write data!"); Thread.sleep((long)(Math.random()*1000)); this.data = data; System.out.println(Thread.currentThread().getName() + " have write data: " + data); } catch (InterruptedException e) { e.printStackTrace(); }finally{ rwl.writeLock().unlock(); }
} } |
读写锁应用的优势在于读的次数远远多于写的次数,任何需要修改的数据都需要在锁中进行。上面代码示例中对于一个共享变量,只能有一个线程写这个数据,但可以多个线程读这个数据,读的时候加读锁,写的时候加写锁,也就是说多个读锁不互斥,但是读锁与写锁之间是互斥的。