lock
简介
Lock接口有以下方法
- void lock(); 获得
- void lockInterruptibly(); 获取锁定除非当前线程是interrupted
- Condition newCondition();返回一个新Condition绑定到该实例
- bollean tryLock()只有在调用时才可以获得锁.
- boolean try lock (long time ,TimeUnit unit);如果在给定的等待时间内是空闲的,并且当前线程尚未得到interrupted则获取该锁
- void unlock();释放锁
lock有以下的实现类
ReentrantLock :可重入锁
ReentranReadWriteLock.ReadLock:可重入锁的读锁
ReentrantReadWriteLock.WriteLock:可重入锁的写锁
锁
公平锁
优点:所有的线程都能得到资源,不会饿死在队列中.
缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大
非公平锁
优点:可以减少cpu唤醒线程的开销,整体的吞吐效率会高,cpu也不必唤醒所有线程,会年少线程的数量
缺点:导致队列中的线程一直获取不到锁或者长时间获取不到锁,导致饿死
实例
代码
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class Main implements Runnable {
static int flag=0;
Lock lock=new ReentrantLock();
@Override
public void run() {
lock.lock();
try {
for (int i=0;i<10000;i++){
flag++;
}
}finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
Main main=new Main();
Thread thread=new Thread(main,"a");
Thread thread2=new Thread(main,"b");
thread.start();
thread2.start();
thread.join();
thread2.join();
System.out.println(flag);
}
}
synchronized与Lock的区别
1.synchronized是关键字,而lock是一个接口
2.synchronized会自动释放锁,而lock必须手动释放锁
3.synchronized是不可中断的,Lock可以中断也可以不中断
4.通过Lock可以知道线程有没有拿到锁,而synchronized
4.synchronized能锁住方法和代码块,而Lock只能锁住代码块
5.lock可以使用读锁提高多线程效率.
6.synchronized是非公平锁,ReentrantLock可以控制是否是公平锁
生产者消费者问题
synchronized版本
public class Main{
//线程交替执行
//俩个线程操作同一个变量
public static void main(String[] args) {
Data data=new Data();
new Thread(()->{
for(int i=0;i<10;i++){
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"a").start();
new Thread(()->{
for (int i=0;i<10;i++){
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"b").start();
}
}
//等待,业务,通知
class Data{
public int number=0;
public synchronized void increment() throws InterruptedException {
//用if判断的时候,会出现虚假唤醒问题,所以等待最好是在while循环中
while (number!=0){
//等待
this.wait();
}
number++;
System.out.println(Thread.currentThread().getName()+"=>"+number);
//通知其他线程去唤醒
this.notify();
}
public synchronized void decrement() throws InterruptedException {
while (number==0){
this.wait();
//等待
}
number--;
System.out.println(Thread.currentThread().getName()+"=>"+number);
//通知其他线程唤醒
this.notify();
}
}
lock版本
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Main{
//线程交替执行
//俩个线程操作同一个变量
public static void main(String[] args) {
Data2 data=new Data2();
new Thread(()->{
for(int i=0;i<10;i++){
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"a").start();
new Thread(()->{
for (int i=0;i<10;i++){
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"b").start();
}
}
//等待,业务,通知
class Data2{
public int number=0;
Lock lock =new ReentrantLock();
Condition condition=lock.newCondition();
public void increment() throws InterruptedException {
try {
lock.lock();
//业务代码
while(number!=0){
//等待
condition.await();
}
number++;
System.out.println(Thread.currentThread().getName()+"=>"+number);
condition.signalAll();
}finally {
lock.unlock();
}
}
public void decrement() throws InterruptedException {
try {
lock.lock();
//业务代码
while(number==0){
//等待
condition.await();
}
number--;
System.out.println(Thread.currentThread().getName()+"=>"+number);
condition.signalAll();
}finally {
lock.unlock();
}
}
}
condition实现精准通知唤醒
通过signal精确指定执行流程
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class Main{
public static void main(String[] args) {
Data data=new Data();
new Thread(()->{
for (int i=0;i<10;i++){
data.printA();
}
},"a").start();
new Thread(()->{
for (int i=0;i<10;i++){
data.printB();
}
},"b").start();
new Thread(()->{
for (int i=0;i<10;i++){
data.printC();
}
},"c").start();
}
}
class Data{
private Lock lock=new ReentrantLock();
private Condition condition1=lock.newCondition();
private Condition condition2=lock.newCondition();
private Condition condition3=lock.newCondition();
private int number =1;
public void printA(){
lock.lock();
try {
//判断->执行->通知
while(number!=1){
//等待
condition1.await();
}
System.out.println(Thread.currentThread().getName()+"=>AAAAAAA");
//唤醒指定的人
number=2;
condition2.signal();//唤醒指定的线程
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void printB(){
lock.lock();
try {
while (number!=2){
condition2.await();
}
System.out.println(Thread.currentThread().getName()+"=>BBBBBBB");
number=3;
condition3.signal();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void printC(){
lock.lock();
try {
while (number!=3){
condition3.await();
}
System.out.println(Thread.currentThread().getName()+"=>BBBBBBB");
number=1;
condition1.signal();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
集合类安全问题
传统的类,列入arraylist,set,hashmap等
ReentranLock
相当于与synchronized它具备以下的特点
- 可中断
- 可以设置超时时间
- 可以设置为公平锁
- 支持多个条件变量
基本语法
import java.util.concurrent.locks.ReentrantLock; class Main{ static ReentrantLock lock=new ReentrantLock(); public static void main(String[] args) { lock.lock(); try { }finally { lock.unlock();//释放锁 } } }
与synchronized一样,都支持可重入
特性
可重入
可重入是指同一个线程如果首次得到了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁如果是不可重入锁,那么第二次获得锁时,自己也会被锁住
可打断
如果不喜欢线程一直等,希望等待可以被终止,就可以用lockInterruptibly()实现
import java.util.concurrent.locks.ReentrantLock;
class Main{
static ReentrantLock lock=new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(()->{
try{
System.out.println("尝试获得锁");
//如果没有竞争那么此方法就会获取lock对象锁
//如果有竞争就进入阻塞队列,可以被其他线程用interruput
lock.lockInterruptibly();
}catch (InterruptedException e){
e.printStackTrace();
System.out.println("没有获得锁,正在返回");
return ;
}
try {
System.out.println("获取到锁");
}finally {
lock.unlock();
}
},"t1");
lock.lock();
t1.start();
Thread.sleep(1);
System.out.println("打断");
//这样就可以对原来的线程进行打断,如果用的是lock就没有办法进行打断
t1.interrupt();
}
}
以上代码的执行结果是
尝试获得锁
打断
java.lang.InterruptedException
at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
at Main.lambda$main$0(Main.java:12)
at java.lang.Thread.run(Thread.java:745)
没有获得锁,正在返回
锁超时
可打断是一种主动的避免死锁的方法,而锁超时是一种被动的防止死锁的方法
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
class Main{
static ReentrantLock lock=new ReentrantLock();
public static void main(String[] args) {
Thread t1=new Thread(()->{
System.out.println("尝试获得锁");
//trylock()获取锁,如果过去到了返回true反之返回false
try {
//可以传入时间参数来指定等待的时间,前面单数是时间,后面的参数是单位
if (!lock.tryLock(1, TimeUnit.SECONDS)){
System.out.println("获取不到锁");
return;
}
} catch (InterruptedException e) {
//这里抛出的异常是因为,trylock的等待也可以打断,因此如果设置了打断时间,就会抛出这个异常
//处理的方式也是获取不到所并进行返回
e.printStackTrace();
System.out.println("获取不到锁");
return;
}
try {
System.out.println("获取到了锁");
}finally {
lock.unlock();
}
},"t1");
lock.lock();
t1.start();
}
}
锁
synchronized和reentrantlock一样默认创建的是非公平锁,但是reentrantlock可以在构造方法中传入true指定为公平锁
公平锁
优点:所有的线程都能得到资源,不会饿死在队列中.
缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大
非公平锁
优点:可以减少cpu唤醒线程的开销,整体的吞吐效率会高,cpu也不必唤醒所有线程,会年少线程的数量
缺点:导致队列中的线程一直获取不到锁或者长时间获取不到锁,导致饿死
公平一般没有必要!会降低并发度
原理
先从构造器开始看默认为非公平锁的实现
public ReentrantLock() {
sync = new NonfairSync();
}
先来看看枷锁非公平锁
final void lock() {
if (compareAndSetState(0, 1))//尝试用compareAndSetState试图把expect的状态改成1
//如果修改成功了,就会把Owner线程改成当前线程
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
以下是成功时候的情况
当竞争出现的时候会调用acquire方法
public final void acquire(int arg) {
//如果是真则执行逻辑与的下半部分
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))//创建一个节点对象,并加入到等待队列里
selfInterrupt();
}
当竞争出现的时候
会执行以下操作
- cas尝试将state由0改为1,结果失败
- 进入tryacquire逻辑,这是state已经是1,结果仍失败
接下来进入addWaiter逻辑,构造node队列
- 途中黄色的三角表示该node的waitStatus状态,其中0为默认正常状态
- node创建是懒惰的
- 其中第一个node称为哑元或哨兵,用来占位,并不关联线程
之后会进入acquireQueued逻辑
- acquireQueued会在一个死循环中不断尝试获得锁,失败后进入park阻塞
- 如果自己是紧邻这head(排列第二位),那么在此tryAcquire尝试获取锁,当这是state仍未1,失败
- 进入shouldParkAfterFailedAcquire逻辑,将前去的node,即head的waitStatus改为-1,这次返回false
- shouldParkAfterFailedAcquire执行完毕回到acquireQueued,再次tryAcquire尝试获取锁,当然这时state仍为1,失败
- 当再次进入shouldParkAferFailedAcquire 时,这时因为其前驱node 的waitStatus已经是-1,这次返回true
- 进入partAndCheckInterrupt,Thread-1 park(灰色表示)
如果有多个线程竞争失败,变成这个样子
- 当前队列不为null,并且head的waitStatus=-1,进入unparkSuccessor流程
- 找到队列中离head最近的一-个Node (没取消的),unpark 恢复其运行,本例中即为
Thread-1 - 回到Thread-1的acquireQueued流程
如果加锁竞争成功(没有竞争)会设置
- exclusiveOwnerThread 为Thread-1, state= 1
- head指向刚刚Thread-1所在的Node,该Node清空Thread
- 原本的head因为从链表断开,而可被垃圾回收
如果这时候有其它线程来竞争(非公平的体现),例如这时有Thread-4来了
总结
这里就是通过CAS(乐观锁)去修改state的值。lock的基本操作还是通过乐观锁来实现的
没有获取到锁会干以下的事情
- tryAcquire:会尝试再次通过CAS获取一次锁。
- addWaiter:将当前线程加入上面锁的双向链表(等待队列)中
- acquireQueued:通过自旋,判断当前队列节点是否可以获取锁。
ReentrantReadWriteLock
特性
ReentrantReadWriteLock(读写锁)
当数据库操作读远高于写时,这时候可以用读写锁,让读与读可以并发,提高性能
提供一个数据容器类内部分别用读锁保护数据的read()方法,写锁保护数据的write()方法
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class Main{
public static void main(String[] args) {
//以下实验是俩个读锁同时进行读取操作
DataContainer dataContainer=new DataContainer();
new Thread(()->{
dataContainer.read();
},"t1").start();
new Thread(()->{
dataContainer.read();
},"t2").start();
//读和写同时进行,会先执行完读操作,再执行写操作
new Thread(()->{
dataContainer.write();
},"t3").start();
}
}
class DataContainer{
private Object data;
private ReentrantReadWriteLock rw=new ReentrantReadWriteLock();//读写锁对象
private ReentrantReadWriteLock.ReadLock r=rw.readLock();
private ReentrantReadWriteLock.WriteLock w=rw.writeLock();
public Object read(){
System.out.println("获取读锁");
r.lock();
try {
System.out.println("读取");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return data;
}finally {
System.out.println("释放读锁");
r.unlock();
}
}
public void write(){
System.out.println("获取写锁");
w.lock();
try {
System.out.println("写锁");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}finally {
System.out.println("释放写锁");
w.unlock();
}
}
}
根据以上得出,读与读可以并发,写写与读写不能进行并发
注意
- 读锁不支持条件变量(写锁支持)
- 重入时不支持升级:如果有读锁的情况下获取写锁,会导致读取写锁永久等待
- 重入时降级支持:即持有写锁的情况下去获取毒读锁
集合类安全问题
在arraylist中如果使用多线程去操作arraylist这个类会出现并发修改异常
以下方法可以有效运行
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;
class Main{
public static void main(String[] args) {
//这样就变得线程安全了
List<Object> list = Collections.synchronizedList(new ArrayList<>());
// List<String>list=new CopyOnWriteArrayList<>(); //也可以使用一个线程安全的类
for(int i=1;i<=10;i++){
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
CopyOnWriteArrayList
copyonwrite写入时复制,cow,计算机程序设计的一种优化策略;
多个线程调用的时候,list读取的时候,固定的写入(覆盖)
在读写的时候避免覆盖造成数据问题
读写分离
copyWriteArrayList与vector对比
copyWriteArrayList中add使用的是lock锁,vector使用的是synchronized