废话不多说,现在边学边写,学到啥写啥,可能很乱
1.synchronized
这个关键字就不多说了,Java中一说到锁就会想到这个关键字,关于这个关键字的学习可以自己找别的文章看。
2.ReentrantLock
java.util.concurrent.lock
中的Lock
框架是锁定的一个抽象,它允许把锁定的实现作为 Java 类,而不是作为语言的特性来实现。ReentrantLock
类实现了Lock
,它拥有与synchronized
相同的并发性和内存语义,但是添加了类似锁投票、定时锁等候和可中断锁等候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。(换句话说,当许多线程都想访问共享资源时,JVM 可以花更少的时候来调度线程,把更多时间用在执行线程上。)
这是引用别人博客中的一段解释,就这样看文字可能也不好理解,直接撸代码吧!
package com.coderzhangch.lock;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author zhangqi
* @date 2018年4月12日
*/
public class MyReentrantLock {
/**
* java.util.concurrent.lock 中的Lock框架是锁定的一个抽象,它允许把锁定的实现作为 Java 类, 而不是作为语言的特性来实现。这就为Lock
* 的多种实现留下了空间,各种实现可能有不同的调度算法、性能特性或者锁定语义。 ReentrantLock 类实现了Lock ,它拥有与synchronized 相同的并发性和内存语义,
* 但是添加了类似锁投票、定时锁等候和可中断锁等候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。 (换句话说,当许多线程都想访问共享资源时,JVM 可以花更少的时候来调度线程,把更多时间用在执行线程上。)
*
* @param args
*/
private Lock lock = new ReentrantLock();
// 定义一个输出的方法
public void output() {
/**
用synchronized锁定一个代码块通常是这样的
synchronized (this) {
for (int i = 0; i < text.length(); i++) {
System.out.println(text.charAt(i));
}
}
*/
//用ReentrantLock锁定代码块
// 1.拿到锁
//lock.lock();
try {
//2.要锁定的代码块放在try中
for (int i = 0; i < 100; i++) {
number--;
System.out.println(Thread.currentThread().getName()+":"+number);
}
} finally {
//3.由于lock必须手动释放锁,为了保证锁能够释放,在finally中释放
//lock.unlock();//释放
}
}
private int number = 10000;
public static void main(String[] args) {
final MyReentrantLock myReentrantLock = new MyReentrantLock();
//搞10个线程跑
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
myReentrantLock.output();
}
}).start();
}
}
}
上面这些代码是ReentrantLock和synchronized锁定一个代码块的操作\
先看看没有锁的时候的打印(部分):
有脏数据,并且抢着执行
再看看,放开注释:
// 1.拿到锁
lock.lock();
try {
//2.要锁定的代码块放在try中
for (int i = 0; i < 100; i++) {
number--;
System.out.println(Thread.currentThread().getName()+":"+number);
}
} finally {
//3.由于lock必须手动释放锁,为了保证锁能够释放,在finally中释放
lock.unlock();//释放
}
}
输出(篇幅问题,只截取部分):
Thread-1:9903
Thread-1:9902
Thread-1:9901
Thread-1:9900
Thread-7:9899
Thread-7:9898
Thread-7:9897
Thread-7:9896
Thread-7:9803
Thread-7:9802
Thread-7:9801
Thread-7:9800
Thread-3:9799
Thread-3:9798
Thread-3:9797
Thread-3:9796
Thread-3:9795
Thread-3:9703
Thread-3:9702
Thread-3:9701
Thread-3:9700
Thread-4:9699
Thread-4:9698
Thread-4:9697
Thread-4:9696
Thread-4:9695
Thread-4:9694
可以看到,使用了lock锁以后,每个线程都是执行完100次循环才放开锁,其它线程才能得到锁,并且也不存在脏数据
3.ReadWriteLock
上例中展示的是和synchronized相同的功能,那Lock的优势在哪里?
先来看看这种场景,对于共享数据的读写,如果用synchronized做的话是这样的效果:
package com.coderzhangch.lock;
import java.util.Random;
/**
* @author zhangqi
* @date 2018年4月12日
*/
public class MyReadWriteLock {
//共享数据
private int data ;
public synchronized void set(int data) {
System.out.println(Thread.currentThread().getName() + "准备写入数据");
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.data = data;
System.out.println(Thread.currentThread().getName() + "写入" + this.data);
}
public synchronized void get() {
System.out.println(Thread.currentThread().getName() + "准备读取数据");
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "读取" + this.data);
}
public static void main(String[] args) {
// final Data data = new Data();
final MyReadWriteLock data = new MyReadWriteLock();
// final RwLockData data = new RwLockData();
//写入
for (int i = 0; i < 3; i++) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 5; j++) {
data.set(new Random().nextInt(30));
}
}
});
t.setName("Thread-W" + i);
t.start();
}
//读取
for (int i = 0; i < 3; i++) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 5; j++) {
data.get();
}
}
});
t.setName("Thread-R" + i);
t.start();
}
}
}
保证每个线程对于共享数据的可见性,避免读写到脏数据
输出:
Thread-W0准备写入数据
Thread-W0写入28
Thread-W0准备写入数据
Thread-W0写入15
Thread-R2准备读取数据
Thread-R2读取15
Thread-R2准备读取数据
Thread-R2读取15
Thread-R2准备读取数据
Thread-R2读取15
Thread-R2准备读取数据
Thread-R2读取15
Thread-R2准备读取数据
Thread-R2读取15
Thread-R1准备读取数据
Thread-R1读取15
Thread-R1准备读取数据
Thread-R1读取15
Thread-R1准备读取数据
Thread-R1读取15
Thread-R1准备读取数据
Thread-R1读取15
Thread-R0准备读取数据
Thread-R0读取15
Thread-R0准备读取数据
Thread-R0读取15
Thread-R0准备读取数据
Thread-R0读取15
Thread-R0准备读取数据
Thread-R0读取15
Thread-R0准备读取数据
Thread-R0读取15
Thread-W2准备写入数据
Thread-W2写入7
Thread-W2准备写入数据
Thread-W2写入29
Thread-W2准备写入数据
Thread-W2写入6
Thread-W1准备写入数据
Thread-W1写入16
Thread-W1准备写入数据
Thread-W1写入14
Thread-W1准备写入数据
Thread-W1写入11
Thread-W2准备写入数据
Thread-W2写入7
Thread-W2准备写入数据
Thread-W2写入17
Thread-R1准备读取数据
Thread-R1读取17
Thread-W0准备写入数据
Thread-W0写入6
Thread-W1准备写入数据
Thread-W1写入10
Thread-W1准备写入数据
Thread-W1写入18
Thread-W0准备写入数据
Thread-W0写入0
Thread-W0准备写入数据
Thread-W0写入8
可以看到,并没有读到脏数据,因为都加了锁,但是对于读取可以看到,读取与读取之间也是互斥的,这样就不合理了,读取之间应该是不互斥的。
像这种场景我们就应该用读写锁ReadWriteLock:
package com.coderzhangch.lock;
import java.util.Random;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* @author zhangqi
* @date 2018年4月12日
*/
public class MyReadWriteLock {
private int data;// 共享数据
// 创建一个读写锁
private ReadWriteLock rwl = new ReentrantReadWriteLock();
public void set(int data) {
rwl.writeLock().lock();// 取到写锁
try {
System.out.println(Thread.currentThread().getName() + "准备写入数据");
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.data = data;
System.out.println(Thread.currentThread().getName() + "写入" + this.data);
} finally {
rwl.writeLock().unlock();// 释放写锁
}
}
public void get() {
rwl.readLock().lock();// 取到读锁
try {
System.out.println(Thread.currentThread().getName() + "准备读取数据");
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "读取" + this.data);
} finally {
rwl.readLock().unlock();// 释放读锁
}
}
public static void main(String[] args) {
final MyReadWriteLock data = new MyReadWriteLock();
// 写入
for (int i = 0; i < 3; i++) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 5; j++) {
data.set(new Random().nextInt(30));
}
}
});
t.setName("Thread-W" + i);
t.start();
}
// 读取
for (int i = 0; i < 3; i++) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 5; j++) {
data.get();
}
}
});
t.setName("Thread-R" + i);
t.start();
}
}
}
输出:
Thread-W0准备写入数据
Thread-W0写入18
Thread-W0准备写入数据
Thread-W0写入24
Thread-W0准备写入数据
Thread-W0写入22
Thread-W0准备写入数据
Thread-W0写入0
Thread-W0准备写入数据
Thread-W0写入28
Thread-W1准备写入数据
Thread-W1写入16
Thread-W1准备写入数据
Thread-W1写入2
Thread-W1准备写入数据
Thread-W1写入0
Thread-W1准备写入数据
Thread-W1写入6
Thread-W1准备写入数据
Thread-W1写入11
Thread-W2准备写入数据
Thread-W2写入9
Thread-W2准备写入数据
Thread-W2写入6
Thread-W2准备写入数据
Thread-W2写入11
Thread-R0准备读取数据
Thread-R1准备读取数据
Thread-R2准备读取数据
Thread-R0读取11
Thread-R2读取11
Thread-R1读取11
Thread-W2准备写入数据
Thread-W2写入1
Thread-W2准备写入数据
Thread-W2写入11
Thread-R0准备读取数据
Thread-R2准备读取数据
Thread-R1准备读取数据
Thread-R1读取11
Thread-R1准备读取数据
Thread-R0读取11
Thread-R0准备读取数据
Thread-R2读取11
Thread-R2准备读取数据
Thread-R2读取11
Thread-R0读取11
Thread-R0准备读取数据
Thread-R1读取11
Thread-R1准备读取数据
Thread-R2准备读取数据
Thread-R0读取11
Thread-R1读取11
Thread-R1准备读取数据
Thread-R2读取11
Thread-R0准备读取数据
Thread-R2准备读取数据
Thread-R2读取11
Thread-R1读取11
Thread-R0读取11
可以看到,读写还是互斥,写之间也互斥,但是读与读之间并不互斥。这就是读写锁之于synchronized的优势。
4.线程间通信Condition
Condition可以替代传统的线程间通信,用await()替换wait(),用signal()替换notify(),用signalAll()替换notifyAll()。
——为什么方法名不直接叫wait()/notify()/nofityAll()?因为Object的这几个方法是final的,不可重写!
传统线程的通信方式,Condition都可以实现。
注意,Condition是被绑定到Lock上的,要创建一个Lock的Condition必须用newCondition()方法。
Condition的强大之处在于它可以为多个线程间建立不同的Condition
看JDK文档中的一个例子:假定有一个绑定的缓冲区,它支持 put 和 take 方法。如果试图在空的缓冲区上执行 take 操作,则在某一个项变得可用之前,线程将一直阻塞;如果试图在满的缓冲区上执行 put 操作,则在有空间变得可用之前,线程将一直阻塞。我们喜欢在单独的等待 set 中保存put 线程和take 线程,这样就可以在缓冲区中的项或空间变得可用时利用最佳规划,一次只通知一个线程。可以使用两个Condition
实例来做到这一点。
——其实就是java.util.concurrent.ArrayBlockingQueue的功能
上面一段是引用别人博客的一段
Object中的wait()/notify()/nofityAll()方法是为synchronized而准备的,只能绑定在这个关键字上,而Condition是可以绑定在Lock上也可以绑定synchronized关键字。
下面就来实现一个阻塞队列来体会一下Condition
package com.coderzhangch.lock;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author zhangqi
* @date 2018年4月12日
*/
public class MyBlockingQueueWithCondition {
// 创建Lock对象
private Lock lock = new ReentrantLock();
// 获取一个操作写线程的Condition,相当于我调用writeLock.signal()方法唤醒的肯定是在做写操作的线程
private Condition writeLock = lock.newCondition();
// 获取一个操作读线程的Condition,相当于我调用readLock.signal()方法唤醒的肯定是在做读操作的线程
private Condition readLock = lock.newCondition();
private Object[] queue = new Object[100];// 创建一个缓存队列
private int writeIndex;// 创建一个变量记录写索引
private int readIndex;// 创建一个变量记录读索引
private int count;// 创建一个变量记录缓存队列中的数量
public int getWriteIndex() {
return writeIndex;
}
public int getReadIndex() {
return readIndex;
}
public int getCount() {
return count;
}
/**
* 写入队列的方法
*
* @throws InterruptedException
*/
public void put(Object object) throws InterruptedException {
// 先锁住
lock.lock();
try {
// 线程一进来,先判断队列是不是满了
while (count == queue.length) {
// 如果满了,就让写线程等待
writeLock.await();
}
// 走到这就证明队列没有满,那么就把东西写到对应的索引上去,因为我们用writeIndex来记录写的索引了,所以很方便
queue[writeIndex] = object;
System.out.println("当前执行写操作");
System.out.println("当前队列中读的索引是:" + readIndex);
System.out.println("当前队列中写的索引是:" + writeIndex);
System.out.println("当前队列中元素的数量:" + count);
if (++writeIndex == queue.length) {
writeIndex = 0;
}
count++;
// 唤醒读线程
readLock.signal();
} finally {
// 释放锁
lock.unlock();
}
}
/**
* 从队列中拿数据的方法
*
* @throws InterruptedException
*/
public Object take() throws InterruptedException {
// 先锁住
lock.lock();
try {
while (count == 0) {
// 如果队列空了,就让读线程等待
readLock.await();
}
// 如果到这儿了就证明队列中有数据
Object object = queue[readIndex];
System.out.println("当前执行读操作");
System.out.println("当前队列中读的索引是:" + readIndex);
System.out.println("当前队列中写的索引是:" + writeIndex);
System.out.println("当前队列中元素的数量:" + count);
if (++readIndex == queue.length) {
readIndex = 0;
}
count--;
// 唤醒写线程
writeLock.signal();
return object;
} finally {
// 释放锁
lock.unlock();
}
}
public static void main(String[] args) {
final ExecutorService writeThreadPool = Executors.newFixedThreadPool(2);
final ExecutorService readThreadPool = Executors.newFixedThreadPool(5);
final MyBlockingQueueWithCondition blockingQueueWithCondition = new MyBlockingQueueWithCondition();
final Random random = new Random();
while (true) {
writeThreadPool.execute(new Runnable() {
@Override
public void run() {
try {
// 不断的朝队列中写随机数
int nextInt = random.nextInt(30);
blockingQueueWithCondition.put(nextInt);
System.out.println("写入的数据:" + nextInt);
System.out.println();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
readThreadPool.execute(new Runnable() {
@Override
public void run() {
try {
// 不断的读取队列中的数据
Thread.sleep(20);
Object take = blockingQueueWithCondition.take();
System.out.println("读到的数据:" + take);
System.out.println();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
}
}
这段代码输出的是一直到队列满了读线程才会去读,这个我搞不懂,感觉代码也没有问题。今天太晚了,等回头有时间再回来研究研究这个吧。
这个博客是为了学习别人的一篇博客而写的,不算自己写的。附上原博主的链接:https://blog.youkuaiyun.com/vking_wang/article/details/9952063