写在前面:
文章内容是通过个人整理以及参考相关资料总结而出,难免会出现部分错误
如果出现错误,烦请在评论中指出!谢谢
1 ReentrantLock
相对于synchronized它具备以下特点:
- 可中断
- 可以设置超时时间
- 可以设置公平锁
- 支持多个条件变量
同synchronized一样,ReentrantLock同样支持可重入
1.1 可重入
其实对于可重入,我们之前已经有了或多或少的了解
可重入是指当同一个线程首次获得这把锁,那么因为它是这把锁的拥有者;因此有权利再次获取这把锁
如果是不可重入锁,那么第二次获得锁时,自己也会被锁住(因为在第一次获取的时候已经被上锁了,那么第二次没办法获得)
可重入测试
public class Demo4 {
static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
lock.lock();
try {
method01();
} finally {
lock.unlock();
}
}
private static void method01() {
lock.lock();
try {
// 业务代码
} finally {
lock.unlock();
}
}
}
测试结果
程序正常执行并退出,说明ReentrantLock在两次重入并没有问题,说明ReentrantLock是可重入锁
1.2 可打断
1.2.1 测试synchronized不可打断
@Slf4j
public class Demo05 {
static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
log.info("尝试获取锁");
synchronized (Demo05.class) {
}
log.info("t1被打断了,放弃获取锁");
},"t1");
synchronized (Demo05.class) {
t1.start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("即将打断t1....");
t1.interrupt();
while (true) {
}
}
}
}
这里设计的思路就是:
1、首先对于线程t1尝试获取对象锁也就是class对象,如果获取不到那么t1会一直阻塞,就不会执行"放弃获取锁"
2、然而在t1线程启动之前,main线程已经拿到了锁并且才启动t1
3、main线程调用t1.interrupt()
尝试打断t1,我们知道现在t1肯定处于阻塞队列中,如果可以被打断,那么t1的打断标记就会被重置,继续运行,并且捕获到异常
测试结果
可以看到main线程一直处于循环中没有放弃锁,而t1线程也并没有被打断而放弃锁
1.2.2 测试ReentrantLock可打断
@Slf4j
public class Demo005 {
static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
log.info("尝试获取锁");
try {
lock.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
log.info("t1被打断了,放弃获取锁");
} finally {
lock.unlock();
}
},"t1");
lock.lock();
t1.start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("即将打断t1");
t1.interrupt();
}
}
这里的逻辑和synchronized差不多:
1、注意这里t1尝试获取锁调用的lock.lockInterruptibly()
方法,其实从名字中就可以听出来这是可打断的获取锁
2、如果t1被打断,那么就会抛出异常,然后异常被捕获之后就继续向下执行
3、而main线程的逻辑跟之前差不多
测试结果
而预想的情况一直,t1线程被打断之后就抛出了异常,并且重置了标志位,然后继续向下运行
这里需要提一点,如果t1调用的是lock.lock()方法依旧不可被打断
1.3 超时锁
上面我们提到的可打断实际上就是为了避免线程死等,但是这种方式是通过其他线程来进行调用
而下面将要了解的超时锁就是自主的避免死等
@Slf4j
public class Demo0005 {
static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
log.info("尝试获取锁");
try {
if (!lock.tryLock(1,TimeUnit.SECONDS)) {
log.debug("获取不到锁");
return;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
log.info("t1被打断了,放弃获取锁");
} finally {
lock.unlock();
}
},"t1");
lock.lock();
t1.start();
}
}
这里的逻辑就是:
1、线程t1通过lock.tryLock()
里面传入时间来判断规定时间内是否可以获取到锁,如果获取不到就返回false;那么下面的步骤就不会进行
2、而main线程和之前一样,就是获取到锁之后不放锁,只是这里不会再打断t1
测试结果
tryLock()
方法尝试获取锁,传入时间单位和时长作为参数,当在规定时间内没有获取到锁就返回false,获取到就返回true
1.4 公平锁
当ReentrantLock构造器中不传入参数时,创建的锁就是非公平锁
当ReentrantLock构造器中传入的参数为true时则创建锁就是公平锁,传入false表示非公平锁
其实我们可以看出来,ReentrantLock内部有一个sync属性,这个属性如果是一个FairSync对象那就是公平锁,反之亦然
公平锁和非公平锁的区别实际上就是公平锁按照进入阻塞队列(EntrySet)的顺序先到先执行
而非公平锁不会按照先进先执行,而是随机选择
实际上FairSync和NonfairSync这两个类就是ReentrantLock的内部类
/**
* Sync object for non-fair locks
*/
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
1.5 条件变量
乍一听条件变量不知道是什么,实际上synchronized中也有条件变量,就是之前在将原理时的waitSet,当条件不满足时就进入waitSet等待唤醒
那么ReentrantLock的条件变量比synchronized强大之处在于,它支持多个条件变量(相当于存在多个waitSet,当条件不满足时可以进入不同的waitSet,而唤醒时也可以精准的唤醒不同的waitSet)
这里首先进行一个测试:
@Slf4j
public class Demo6 {
static ReentrantLock lock = new ReentrantLock();
static boolean hasCigarette = false;
static boolean hasTakeout = false;
static Condition CigaretteWaitSet = lock.newCondition();
static Condition TakeoutWaitSet = lock.newCondition();
public static void main(String[] args) {
new Thread(() -> {
lock.lock();
try {
log.info("有烟没?[{}]",hasCigarette);
while (!hasCigarette) {
log.info("没烟,先歇会");
try {
CigaretteWaitSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.info("有烟没?[{}]",hasCigarette);
if (hasCigarette) {
log.info("可以开始干活了");
} else {
log.info("没干成活");
}
} finally {
lock.unlock();
}
},"t1").start();
new Thread(() -> {
lock.lock();
try {
log.info("有外卖没?[{}]",hasCigarette);
while (!hasTakeout) {
log.info("没外卖,先歇会");
try {
TakeoutWaitSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.info("有外卖没?[{}]",hasTakeout);
if (hasTakeout) {
log.info("可以开始干活了");
} else {
log.info("没干成活");
}
} finally {
lock.unlock();
}
},"t2").start();
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
lock.lock();
try {
hasTakeout = true;
log.info("外卖来了");
TakeoutWaitSet.signalAll();
} finally {
lock.unlock();
}
},"t3").start();
}
}
首先对场景做一个解释:对于t1线程只有烟到了才干活,对于t2线程只有外卖到了才干活,对于t3线程送外卖
1、t1线程获取到锁之后先判断是否有烟,如果没有,就到CigaretteWaitSet(烟休息室)去等待,而且这里用的是while循环,即使每次被唤醒之后依然需要判断是否需要继续等待,直到有烟t1线程才开始干活
2、t2线程和t1线程的设计思路一致
3、t3线程获取到锁之后先修改外卖的标志位,然后唤醒TakeoutWaitSet(外卖休息室)中的所有线程
4、实际上这时就是精确的唤醒TakeoutWaitSet中的线程,而不是唤醒所有的线程,那么TakeoutWaitSet的线程就可以做下一次循环判断
测试结果
这就是条件变量,可以实现线程的精准唤醒
1.6 条件变量-生产者消费者模型
1.6.1 未精准通知版
资源类
@Slf4j
public class Resource {
private int num = 0;
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
public void increment() throws InterruptedException {
lock.lock();
try {
while (num != 0) {
condition.await();
}
num++;
log.info(Thread.currentThread().getName() + "\t" + num);
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void decrement() throws InterruptedException {
lock.lock();
try {
while (num == 0) {
condition.await();
}
num--;
log.info(Thread.currentThread().getName() + "\t" + num);
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
Condition接口实例和Lock实例进行绑定,因此可以通过Condition实例对持有锁和处于阻塞状态的代码块进行操作
Condition接口实例等同于替换了对象监视器(Object monitor)方法(wait,notify,notifyAll),替换为(await,sign,signAll)
实际上当while判断条件成立时,await方法也会类似于wait方法
释放锁
(注意:sleep方法虽然阻塞但是不会释放锁
)可以看出外部的lock锁是为了实现互斥,而内部的condition是为了实现同步
1.6.2 精准通知版
资源类
@Slf4j
public class Resource2 {
private int number = 1;
private Lock lock = new ReentrantLock();
private Condition condition1 = lock.newCondition();
private Condition condition2 = lock.newCondition();
private Condition condition3 = lock.newCondition();
public void print5() {
lock.lock();
try {
while (number != 1) {
condition1.await();
}
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName());
}
number = 2;
condition2.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void print10() {
lock.lock();
try {
while (number != 2) {
condition2.await();
}
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName());
}
number = 3;
condition3.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void print15() {
lock.lock();
try {
while (number != 3) {
condition3.await();
}
for (int i = 0; i < 15; i++) {
System.out.println(Thread.currentThread().getName());
}
number = 1;
condition1.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
同一个lock对象,但是从lock对象中映射出三个condition对象(也就是一把锁多个钥匙)
在进行判断时如果满足条件就用自己的钥匙上锁,当不满足时就执行操作,操作之后调用指定钥匙去开锁(也就是唤醒指定的操作)
线程
public class Processor {
public static void main(String[] args) {
Resource2 resource2 = new Resource2();
new Thread(resource2::print5,"A").start();
new Thread(resource2::print10,"B").start();
new Thread(resource2::print15,"C").start();
}
}
2 八锁现象
假设现在有一个Phone资源类,提供了两种操作方式
class Phone {
public synchronized void sendEmail() {
System.out.println("sendEmail...");
}
public synchronized void sendSms() {
System.out.println("sendSms...");
}
}
2.1 标准访问
public class Lock8 {
public static void main(String[] args) throws InterruptedException {
Phone phone = new Phone();
new Thread(phone::sendEmail,"A").start();
TimeUnit.SECONDS.sleep(2);
new Thread(phone::sendSms,"B").start();
}
}
当线程A就绪之后,main线程休眠2秒钟之后,才令B线程就绪,所以一定会先打印邮件再打印短信
2.2 邮件方法休眠4秒钟
修改资源类
class Phone {
public synchronized void sendEmail() {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("sendEmail...");
}
public synchronized void sendSms() {
System.out.println("sendSms...");
}
}
执行线程
public class Lock8 {
public static void main(String[] args) throws InterruptedException {
Phone phone = new Phone();
new Thread(phone::sendEmail,"A").start();
TimeUnit.SECONDS.sleep(2);
new Thread(phone::sendSms,"B").start();
}
}
实际上还是先打印邮件,虽然邮件睡眠4秒钟
然而由于main线程休眠2秒钟,这是邮件线程已经抢夺到执行权,并且加锁;而slepp方法不会释放锁
2.3 新增普通方法hello(),先打印邮件还是hello
修改资源类
class Phone {
public synchronized void sendEmail() {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("sendEmail...");
}
public synchronized void sendSms() {
System.out.println("sendSms...");
}
public void hello() {
System.out.println("hello...");
}
}
这里的hello方法并没有加锁
线程操作
public class Lock8 {
public static void main(String[] args) throws InterruptedException {
Phone phone = new Phone();
new Thread(phone::sendEmail,"A").start();
TimeUnit.SECONDS.sleep(2);
new Thread(phone::hello,"B").start();
}
}
结果是先打印hello,再打印邮件
因为邮件线程虽然执行时先抢到锁,然而hello线程并没有上锁,只用等待main线程休眠2秒钟之后,就可以就绪--运行
2.4 两部手机,先打印邮件还是短信
资源类不变,线程操作
public class Lock8 {
public static void main(String[] args) throws InterruptedException {
Phone phone = new Phone();
Phone phone2 = new Phone();
new Thread(phone::sendEmail,"A").start();
TimeUnit.SECONDS.sleep(2);
new Thread(phone2::sendSms,"B").start();
}
}
结果是先打印短信,再打印邮件
因为两部手机存在两把锁,实际上两个线程持有的锁不是同一个,也就是说邮件线程及时休眠不释放锁,也不妨碍短信线程
实际上这里的锁是
对象锁
,两个不同的Phone对象持有的对象锁也不同
2.5 两个静态同步方法,同一部手机
修改资源类
class Phone {
public static synchronized void sendEmail() {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("sendEmail...");
}
public static synchronized void sendSms() {
System.out.println("sendSms...");
}
public void hello() {
System.out.println("hello...");
}
}
修改线程操作
public class Lock8 {
public static void main(String[] args) throws InterruptedException {
Phone phone = new Phone();
new Thread(() -> {
phone.sendEmail();
},"A").start();
TimeUnit.SECONDS.sleep(2);
new Thread(() -> {
phone.sendSms();
},"A").start();
}
}
结果是先打印邮件,再打印短信
因为这里拿到的同步锁实际上Phone类的class对象,不管怎么样都只有唯一一个
2.6 两个静态同步方法,两部手机
修改线程操作
public class Lock8 {
public static void main(String[] args) throws InterruptedException {
Phone phone = new Phone();
Phone phone2 = new Phone();
new Thread(() -> {
phone.sendEmail();
},"A").start();
TimeUnit.SECONDS.sleep(2);
new Thread(() -> {
phone2.sendSms();
},"A").start();
}
}
结果依然是先打印邮件,再打印短信
因为同步锁是Phone类的class对象
2.7 一个普通、一个静态同步方法,两部手机
修改资源类
class Phone {
public static synchronized void sendEmail() {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("sendEmail...");
}
public synchronized void sendSms() {
System.out.println("sendSms...");
}
public void hello() {
System.out.println("hello...");
}
}
修改线程操作
public class Lock8 {
public static void main(String[] args) throws InterruptedException {
Phone phone = new Phone();
new Thread(() -> {
phone.sendEmail();
},"A").start();
TimeUnit.SECONDS.sleep(2);
new Thread(() -> {
phone.sendSms();
},"A").start();
}
}
结果是先打印短信,再打印邮件
因为短信线程拿到的是phone的对象锁,而邮件线程拿到的是Phone类的class对象锁,线程之间互不干扰
2.8 一个静态、一个普通同步方法,两部手机
修改线程操作
public class Lock8 {
public static void main(String[] args) throws InterruptedException {
Phone phone = new Phone();
Phone phone2 = new Phone();
new Thread(() -> {
phone.sendEmail();
},"A").start();
TimeUnit.SECONDS.sleep(2);
new Thread(() -> {
phone2.sendSms();
},"A").start();
}
}
结果依然是先打印短信,再打印邮件
因为本质上邮件线程持有的是class对象锁,而短信线程持有的是phone2对象锁
3 集合分析
3.1 ArrayList分析
假设一种场景,创建一个ArrayList,通过不同的线程进行读写操作
public class NotSafeDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
for (int i = 0; i < 30; i++) {
new Thread(() -> {
list.add(UUID.randomUUID().toString().substring(0,8));
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
通过for循环建立了30个线程对list进行读写操作,而ArrayList线程不安全,不同的线程执行存在随机性,可能某一个线程在进行写操作时,另外一个线程也在进行写操作
所以执行时会报出ConcurrentModificationException
(并发修改异常)
Exception in thread "8" Exception in thread "20" Exception in thread "15" Exception in thread "21" Exception in thread "23" Exception in thread "28" java.util.ConcurrentModificationException
为什么会出现这种情况呢?(或者说为什么AarrayList线程不安全呢)
分析下ArrayList的源码
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1,size - index);
elementData[index] = element;
size++;
}
add方法本身没有加锁,在高并发的情况下任何一个线程都可以直接对其进行读写
Vector线程安全,为什么呢?
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
add方法通过synchronized关键字加锁,保证了一个线程在进行操作时,其他的线程只能阻塞
然而虽然保证了线程的安全,却极大的降低了效率(这也是为什么vector不常用的原因)
3.1.1 通过Collections.synchronizedList优化
现在已经知道ArrayList线程不安全,如何解决该问题呢?
可以通过Collections.synchronizedList(new ArrayList<>())
操作原有的ArrayList使得线程安全
synchronizedList
方法是如何执行的呢?
public static <T> List<T> synchronizedList(List<T> list) {
return (list instanceof RandomAccess ?
new SynchronizedRandomAccessList<>(list) :
new SynchronizedList<>(list));
}
首先判断参数中的list是否属于RandomAccess接口的实现类对象,不管是否返回的实际上都是加锁的对象
3.1.2 创建CopyOnWriteArrayList对象
单线程时可以使用ArrayList,高并发则使用CopyOnWriteArrayList
那么为什么CopyOnWriteArrayList在高并发情况下使用呢?
因为CopyOnWriteArrayList不仅能在保证线程安全的前提下,达到数据的最终一致性,那么是如何实现的呢?
1、首先CopyOnWriteArrayList中维护了一个数组和一个lock锁
2、当进行写操作时
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return {@code true} (as specified by {@link Collection#add})
*/
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
首选拿出类中维护的lock锁并上锁,之后获取到当前的数组对象,然后通过
Arrays.copyOf
方法复制了当前的数组并使长度加1,然后将添加的元素放入数组中,最后通过set方法将新的数组对象返回;然后再解锁核心点:
1、复制原有的数组,不影响读操作
2、进行写操作时加锁
3、当进行读操作时
/**
* {@inheritDoc}
*
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E get(int index) {
return get(getArray(), index);
}
读数据时并没有加锁,虽然不是即时获取到最新的数据,然而也保证了数据的最终一致性
3.2 HashSet分析
类似于Arraylist,HashSet同样线程不安全
同样可以通过Collections.synchronizedSet()
令原有的HashSet保证线程安全,然而这样会极大降低读写效率
同样的,也可以使用new CopyOnWriteArraySet<>()
在保证线程安全的前提下,提高读写效率
3.2.1 扩展
通过源码看出,HashSet底层维护了一个HashMap和一个Object对象
那么这两个属性的作用是什么呢?
public HashSet() {
map = new HashMap<>();
}
从HashSet的构造器中看出,map就是一个HashMap对象
当调用add方法时
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
本质上就是向map中放数据,key就是add方法的参数,而value实际上就是一个固定值,也就是一开始维护的Object对象
当调用remove方法时
public boolean remove(Object o) {
return map.remove(o)==PRESENT;
}
本质上就是从map中移除一个数据,我们知道map的remove方法返回的是对应key的value,而value就是一个固定值PRESENT
总结如下:
1、HashSet底层就是HashMap,只不过只使用了HashMap的key(因为key唯一),而value就是一个固定值Object对象
3.3 HashMap分析
类似于Arraylist,HashMap同样线程不安全
同样可以通过Collections.synchronizedMap()
令原有的HashMap保证线程安全,然而这样会极大降低读写效率
然而并不存在CopyOnWriteArrayMap<>()
,而是ConcurrentHashMap<>()
3.3.1 ConcurrentHashMap分析
//TODO
4 Callable接口
我们知道如果需要新建一个线程,必须通过new Thread()
这种方式实现
但是参考下Thread的构造器
还可以在构造器中传入Runable接口或者ThreadGroup线程组
那么如何使用Callable接口呢?
我们知道构造器中没有Callable接口作为参数,所以就必须想办法将Callable接口和Thread构造器联系起来
那么可以将Runable接口作为桥梁将两边联系起来
看下Runbale接口在API中的定义:
已知实现类中有一个FutureTask类,而FutureTask类又是什么样的呢?
FutureTask类的构造器中可以传入一个Callable接口对象,那么就将Callable接口和Thread联系到一起
4.1 Callable接口测试
public class CallableDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask futureTask = new FutureTask(new MyThread());
new Thread(futureTask,"A").start();
System.out.println(futureTask.get());
}
}
class MyThread implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println("线程被调用");
return 1024;
}
}
首先MyThread继承了Callable接口(并指定泛型,泛型就是call方法的返回值类型)
然后在主线程中定义了FutureTask对象传入MyThread对象为参数,进而创建线程
最后调用了futureTask的get方法获取call方法中的返回值
个人理解:FutureTask类(从名字上是未来任务),异步执行,而get方法就是获取异步执行之后的结果
4.2 Callable接口和Runable接口的区别
Callable | Runable |
---|---|
方法存在返回值(返回值就是实现Callable接口时定义的泛型) | 没有返回值 |
可以抛异常 | 不能抛异常 |
落地方法为call方法 | 落地方法为run方法 |
4.3 Callable接口细节
4.3.1 Callable接口中的思想
我们知道FutureTask对象可以通过get方法获取线程执行之后的结果,其实这本质就是异步的思想
假设每个步骤执行过程中需要5秒钟,而步骤3执行需要20秒
正常情况下多个步骤如果是同步执行的话,就是串行化执行,那么执行时间就是35秒
当进行异步操作时,对于步骤3另起一个线程,那么步骤1,2,4执行完成需要15秒,而在步骤3执行时定义为FutrueTask对象,从而主线程执行完成之后只需要等待主线程5秒钟,并接收到步骤3的结果;从而执行时间为20秒
也就是说对于异步操作,我们只需要给异步操作一个新的线程,并对该线程进行监听,在此期间可以继续执行其他操作,当监听到异步操作的线程结束,就接收对应的返回值,从而提高效率
4.3.2 FutureTask.get()方法一般放在结尾
我们可以尝试下不将FutureTask.get()方法放在结尾会出现什么情况呢?
public class CallableDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask futureTask = new FutureTask(new MyThread());
new Thread(futureTask,"A").start();
System.out.println(futureTask.get());
System.out.println("其他步骤已完成");
}
}
class MyThread implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println("线程被调用");
TimeUnit.SECONDS.sleep(3);
return 1024;
}
}
结果:
实际上结果中先阻塞着等待线程结果计算之后再打印"其他步骤已完成"
本质上get()方法阻塞,如果将get()放在前面就会影响main线程的执行,故FutureTask对象结果的计算需要放在最后避免阻塞其他线程
4.3.3 FutureTask任务多线程并发访问时只会执行一次
测试:
public class CallableDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask futureTask = new FutureTask(new MyThread());
new Thread(futureTask,"A").start();
new Thread(futureTask,"B").start();
System.out.println(futureTask.get());
System.out.println("其他步骤已完成");
}
}
class MyThread implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println("线程被调用");
TimeUnit.SECONDS.sleep(3);
return 1024;
}
}
结果:
原因:
5 JUC辅助类
5.1 CountDownLatch
假设一个场景,主线程下面有6个子线程,主线程结束必须要在子线程都结束之后才能结束
模拟场景:
public class CountDownLatchDemo {
public static void main(String[] args) {
for (int i = 0; i < 6; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "已结束");
},String.valueOf(i)).start();
}
System.out.println(Thread.currentThread().getName() + "已结束");
}
}
结果:
0已结束
4已结束
2已结束
main已结束
3已结束
1已结束
5已结束
可以看出main线程可以会在其他线程没有结束之前结束,那么如何控制该顺序呢?
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(6);
for (int i = 0; i < 6; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "已结束");
latch.countDown();
},String.valueOf(i)).start();
}
latch.await();
System.out.println(Thread.currentThread().getName() + "已结束");
}
}
CountDownLatch就是用于控制某个线程在其他线程结束之后才能执行
5.1.1 构造器
/**
* Constructs a {@code CountDownLatch} initialized with the given count.
*
* @param count the number of times {@link #countDown} must be invoked
* before threads can pass through {@link #await}
* @throws IllegalArgumentException if {@code count} is negative
*/
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
传入的参数count从注释中可以看出就是其他线程被调用的次数
5.1.2 countDown方法
/**
* Decrements the count of the latch, releasing all waiting threads if
* the count reaches zero.
*
* <p>If the current count is greater than zero then it is decremented.
* If the new count is zero then all waiting threads are re-enabled for
* thread scheduling purposes.
*
* <p>If the current count equals zero then nothing happens.
*/
public void countDown() {
sync.releaseShared(1);
}
从注释中可以看出就是每次调用时就会使计数减少1,当计数值为0时就会唤醒被阻塞的线程
5.1.3 理解
其实就是调用latch.await()
方法时在计数为0之前main线程会一直阻塞
而其他线程执行之后调用latch.countDown()
方法时就会将计数减少1
5.2 CyclicBarric
我们知道CountDownLatch是倒着开始计数,而CyclicBarric就是正着开始计数,当数量到达指定的值之后,该进程就不会再阻塞
5.2.1 构造器
/**
* Creates a new {@code CyclicBarrier} that will trip when the
* given number of parties (threads) are waiting upon it, and which
* will execute the given barrier action when the barrier is tripped,
* performed by the last thread entering the barrier.
*
* @param parties the number of threads that must invoke {@link #await}
* before the barrier is tripped
* @param barrierAction the command to execute when the barrier is
* tripped, or {@code null} if there is no action
* @throws IllegalArgumentException if {@code parties} is less than 1
*/
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
}
从注释中看出第一个参数parties
就是指定需要阻塞等待多少个线程执行结束,而barrierAction
参数就是不再阻塞时执行的线程
5.2.2 await方法
/**
* Waits until all {@linkplain #getParties parties} have invoked
* {@code await} on this barrier.
*/
public int await() throws InterruptedException, BrokenBarrierException {
try {
return dowait(false, 0L);
} catch (TimeoutException toe) {
throw new Error(toe); // cannot happen
}
}
await方法就是在其他线程执行操作结束之后进行调用,每调用一次CyclicBarrier的属性parties
就会减少1,当数值为0时就会调用CyclicBarrier中的线程
5.2.3 测试
public class CyclicBarrierDemo {
public static void main(String[] args) {
CyclicBarrier barrier = new CyclicBarrier(7,()->{
System.out.println("召唤神龙");
});
for (int i = 0; i < 7; i++) {
final int temp = i;
new Thread(() -> {
System.out.println("第" + temp + "颗龙珠");
try {
barrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
},String.valueOf(i)).start();
}
}
}
首先定义了一个CyclicBarrier指定了需要等待多少个线程先执行,定义了自己的线程执行操作
在其他线程进行操作结束之后通过调用CyclicBarrier对象的await方法减少
parties
属性的数量
5.3 Semaphore
信号量,主要用于多线程的并发控制和资源的互斥,其实也就是操作系统中的互斥信号量和同步信号量
假设一种情况,资源和线程都有多个,但是线程的数量大于资源的数量
public class SemaphoreDemo {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(3);
for (int i = 0; i < 6; i++) {
new Thread(() -> {
try {
semaphore.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "获取了资源");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();
System.out.println(Thread.currentThread().getName() + "释放了资源");
}
},String.valueOf(i)).start();
}
}
}
通过Semaphore构造器创建了一个资源数量为3的信号量
紧接着创建了6个线程,不同线程之间通过
semaphore.acquire()
获取资源,明显当资源数量为0时,其他线程就会阻塞假设线程休眠3秒就是进行了操作,最后通过
semaphore.release()
释放了资源,释放之后就可以通知其他阻塞的线程
获取该资源
5.3.1 构造器
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
/**
* Creates a {@code Semaphore} with the given number of
* permits and the given fairness setting.
* @param fair {@code true} if this semaphore will guarantee
* first-in first-out granting of permits under contention,
* else {@code false}
*/
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
构造器中如果传入两个参数,第一个参数代表资源数量,第二参数代表判断是否公平
5.3.2 acquire方法
/**
* Acquires a permit from this semaphore, blocking until one is
* available, or the thread is {@linkplain Thread#interrupt interrupted}.
*
* <p>Acquires a permit, if one is available and returns immediately,
* reducing the number of available permits by one.
*/
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
通过调用已经创建的Semaphore对象的acquire方法获取一个资源
5.3.3 release方法
/**
* Releases a permit, returning it to the semaphore.
*
* <p>Releases a permit, increasing the number of available permits by
* one. If any threads are trying to acquire a permit, then one is
* selected and given the permit that was just released. That thread
* is (re)enabled for thread scheduling purposes.
*
* <p>There is no requirement that a thread that releases a permit must
* have acquired that permit by calling {@link #acquire}.
* Correct usage of a semaphore is established by programming convention
* in the application.
*/
public void release() {
sync.releaseShared(1);
}
通过调用已经创建的Semaphore对象的release方法释放一个资源
5.4 ReadWriteLock
多个线程同时读同一个资源类没有任何问题,为了满足并发量,读取共享资源可以同时进行
但是如果有一个线程想要写共享资源,就不应该再有其他线程可以对该资源进行读或写
总结下来就是
1、读-读共享
2、写-读独占
3、写-写独占
如果假设读写都不加锁的条件下,会出现什么现象呢?
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCache cache = new MyCache();
for (int i = 0; i < 5; i++) {
final String temp = String.valueOf(i);
new Thread(() -> {
cache.put(temp,temp);
},temp).start();
}
for (int i = 0; i < 5; i++) {
final String temp = String.valueOf(i);
new Thread(() -> {
cache.get(temp);
},temp).start();
}
}
}
class MyCache {
private volatile HashMap<String, Object> map = new HashMap<>();
public void put(String key,Object value) {
System.out.println(Thread.currentThread().getName() + "准备写入数据" + key);
map.put(key,value);
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "写入数据完成");
}
public Object get(String key) {
System.out.println(Thread.currentThread().getName() + "准备读取数据");
Object res = map.get(key);
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "读取数据完成" + res);
return res;
}
}
这里读和写都没有加锁,同时分别开了5个线程用于读和写
结果:
当线程0准备写入数据时,其他线程直接抢占了CPU执行权,很明显这和事务的ACID违背(原子性)
5.4.1 使用ReadWriteLock控制读写
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCache cache = new MyCache();
for (int i = 0; i < 5; i++) {
final String temp = String.valueOf(i);
new Thread(() -> {
cache.put(temp,temp);
},temp).start();
}
for (int i = 0; i < 5; i++) {
final String temp = String.valueOf(i);
new Thread(() -> {
cache.get(temp);
},temp).start();
}
}
}
class MyCache {
private volatile HashMap<String, Object> map = new HashMap<>();
private ReadWriteLock lock = new ReentrantReadWriteLock();
public void put(String key,Object value) {
lock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "准备写入数据" + key);
map.put(key,value);
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "写入数据完成");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.writeLock().unlock();
}
}
public Object get(String key) {
lock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "准备读取数据");
Object res = map.get(key);
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "读取数据完成" + res);
return res;
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.readLock().unlock();
}
return null;
}
}
首先ReadWriteLock只是一个接口,具体操作时需要使用实现类ReentrantReadWriteLock
我们只是要对写操作进行控制,那么为什么读操作还要加锁呢?
为了防止在进行读操作时阻止写操作,但是如果多个读操作可以同步进行
读锁是共享锁,多个读操作共享;写锁是排他锁,其他任何操作不能共享
5.4.2 writeLock和readLock
public class ReentrantReadWriteLock {
/** Inner class providing readlock */
private final ReentrantReadWriteLock.ReadLock readerLock;
/** Inner class providing writelock */
private final ReentrantReadWriteLock.WriteLock writerLock;
public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
public ReentrantReadWriteLock.ReadLock readLock() { return readerLock; }
类中维护了两个属性分别为ReadLock对象和WriteLock对象,分别对读写进行控制
故当资源类涉及到读写操作时,读操作可以使用读锁,写操作可以使用写锁
而ReadLock和WriteLock都实现了Lock接口,因此可以使用lock方法和unlock方法进行控制
结果:
每次进行写操作时都不会有其他的线程占用资源(资源独占)
当进行完写操作,再进行读操作时,多个读操作可以共享资源
个人公众号目前正初步建设中,如果喜欢可以关注我的公众号,谢谢!