秋招突击——7/14——多线程编程(join关键字、AtomaticInteger、ReentrantLock)

7 篇文章 0 订阅

引言

  • 今天主要是将剩下的几个关键字基本原理讲一下,以及对应编程题联系一下,分别是
    • join
    • reentranlock
    • 线程池
    • AtomicInteger

基础知识

Lock接口(reentranlock)

特点

  • 需要显示的进行加锁和解锁,增加了操作的灵活性
  • 提供了可中断获取锁的特性
  • 提供了超时获取锁的特性
  • 实现Lock接口的API都是基于AQS提供能力实现的
  • ReentrantLock是继承了lock接口
    • 内部聚合了一个AQS同步器,通过调用AQS的方法来包装实现的

AQS同步器和Lock锁之间的差异

  • lock是面向锁的使用者,定义了使用者和锁的交互接口,隐藏了底层实现细节
  • AQS是面向锁的实现者,简化了锁的实现方式,屏蔽了同步状态的管理、线程排队等底层操作
    • 锁和同步器很好的隔离了使用者和实现者所关注的领域

我们作为锁的使用者AQS了解就行

基本使用
  • 需要声明特定的对象ReentrantLock,然后使用try-catch确保锁必须被释放
Lock lock = new ReentrantLock();
lock.lock()
try{
}finally{
	lock.unlock();
}

finally一定要确保锁释放

提供的API

  • lock()

    • 正常锁,加锁
  • lockInterruptibly()

    • 可中断锁,会响应中断,在锁的获取过程中可以中断该线程
    • 会抛出InterruptedException异常
  • tryLock()

    • 尝试非阻塞获取锁,调用该方法后的立刻返回
    • 能够获取返回true,不能获取返回false
  • tryLock(long time,TimeUnit unit)

    • 存在以下三种情况,会抛出异常InterruptedException
      • 当前线程在超时时间内获得了锁
      • 当前线程在超时时间内被中断
      • 超时时间结束,返回false
  • unlock()

    • 正常解锁
  • Condition newCondition()

    • 有点难懂了这个,没有代码和样例
    • 获取等待通知组件,该组件和当前的锁绑定
    • 当前线程只有获取了锁,才能调用该组件的wait方法
    • 调用后,当前线程将释放锁
Lock和Synchronized的对比

相同点

  • 都是用来保护资源线程安全,都可以保证共享变量的可见性
  • 都拥有可重入的特点

不同点

  • 加解锁控制的差别

    • synchronized关键字是通过JVM实现的内置锁
    • lock是需要手动控制,并且unlock一定要放入finally中
  • synchronized不够灵活

    • synchronized只能一个线程获取锁,其他线程只能等待
    • lcok可以通过lockInterruptibly,在获取锁等待过程中中断退出,或者使用trylock尝试获取锁,能获取就获取,不能获取就干别的事,不会阻塞
  • 锁被线程拥有的数量

    • synchornized只能被一个线程拥有
    • lock可以被多个线程拥有,针对读写锁,就是多个锁拥有

是否支持公平锁和非公平锁
* synchronized只支持非公平锁
* lock都支持

如何选择

  • 能不用锁就不用锁,推荐使用线程安全的工具类,java.util.concurrent进行操作
  • 如果要用先选synchronized,更加安全,实在不行,用lock
重入锁ReentrantLock

如何实现可重入

  • 已经获取一个锁的线程,可以再次获取该锁而不被阻塞
  • 线程再次获取锁
    • 锁需要识别再次获取锁的线程是否是已经获取的锁的线程相同。
  • 锁的最终释放
    • 通过AQS中同步状态state实现,当计数为零,释放成功。

公平锁和非公平锁

  • reentrantlock有三个内部类分别是的FairSync和NonFairSync,这两个分别实现公平锁和非公平锁,然后的Sync是另外两个类的父类
    在这里插入图片描述
    Sync类
  • 继承自AQS抽象方法
  • 重要方法说明
    • lock加锁方法,抽象方法并没有具体实现,留给子类实现的,FairSync和NonFairSync会根据自己的要求实现对应的公平锁和非公平锁
    • nonfairTryAcquire:非公平锁获取锁
    • isHeldExclusively:判断资源是否被当前线程占用
    • isLocked:资源是否被占用

FairSync类和NonfairSync类

  • 两者方法唯一不同的位置,判断条件多了hasQueuedPredecessors()方法
  • 加入队列中当前节点是否有前驱节点的判断
    • 返回true,说明是队列中第一个元素,直接获取对应的元素
    • 返回false,必须要等前面的线程获取并释放锁,才能操作

构造函数

  • 构造函数中,默认是使用非公平锁,如果要使用公平锁,需要传入true
读写锁ReentrantReadWriteLock
  • 读写锁通过维护的一对锁,一个读锁和一个写锁,通过分离锁和写锁,并发性更高
  • 读锁允许多个线程同时读,然后写锁会阻塞其他读线程和其他写线程

特性

  • 公平性选择:同时支持公平锁和非公平锁
  • 支持重入:读锁和写锁都支持多重获取
  • 锁降级:通过特定的顺序,能够将写锁降级为读锁
    • 获取写锁,在获取读锁,在释放写锁,写锁就降级为读锁了

读写锁接口和实例

  • getReadLockCount
    • 返回当前读锁被线程获取的次数
  • getReadHoldCount
    • 返回当前线程获取读锁的次数
  • isWriteLocked
    • 判定写锁是否被获取
  • getWriteHoldCount
    • 返回当前写锁被获取的次数

读写锁的实现分析

读写状态设计

  • 基于AQS同步器实现同步功能,使用state维护重入性
    • 高16位,维护读状态
    • 低16位,维护写状态
      在这里插入图片描述

支持锁降级,不支持锁升级

使用方式

  • 后续的加锁和解锁是相同的
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

class Main{
   ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
   Lock r = rwl.readLock();
   Lock w = rwl.writeLock();
   
   
}
Condition接口
  • 同监视器方法,也实现了类似wait、wait(long timeout)、notify(),notifyAll()等方法,和lock配合,可以实现等待\通知模式

Condition接口与实例

  • Condition定义了等待/通知两种类型的方法,当前线程调用这些方法需要获取到Condition对象关联的锁

    • condition对象是由Lock对象创建出来的,Condition依赖Lock对象。
  • 使用示例如下:必须使用lock.newCondition()创建对应的condition语句

    • 和lock对象一样,都需要创建对应对象。
import java.awt.*;
import java.util.Date;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

class Main{
    // 定义一个lock对象
    ReentrantLock lock = new ReentrantLock();
    Condition cd = lock.newCondition();

    public void conditionAwait(){
        lock.lock();
        try{
            cd.await();
        }
        catch(InterruptedException e){
            e.printStackTrace();
        }finally{
            lock.unlock();
        }
    }

    public void conditionSignal(){
        lock.lock();
        
        cd.signal();
       
        lock.unlock();
        
    }
}

condition使用方法

  • await()throws InterruptedException

    • 当前线程进入等待状态,直到被同时signal或者中断
  • awaitUninterruptibly

    • 当前线程等待被通知,并且不响应中断
  • awaitNanos(long nanosTimeout)

    • 当前线程进入等待状态,直到被通知或者超时或者中断
    • 返回的剩余的时间,返回0或者负数,说明超时了
  • awaitUntil(Date deadline) throws Interrupted Exception

    • 当前线程进入等待状态直到被通知、中断或者到达预期时间
    • 返回true,没到指定时间就被通知
    • 返回false,到了指定时间,返回false
  • signal

    • 唤醒一个等待在condition上的线程,
  • signalAll

    • 唤醒所有等待在condition上的线程

实现原理

  • 是AQS的内部类,每一个condition都维护一个队列
  • 等待队列
    • 一个condition一个等待队列,维护头节点和尾节点
    • 一个同步器拥有一个同步队列和多个等待队列,condition对象可以创建多个
  • 通知和等待
    • 通知和等待都是通过移动condition队列和同步队列的头节点和尾节点实现

Join关键字

  • Thread类的一个方法,让一个线程等待另一个线程的完成
    • 如果线程A调用了线程B的join方法,线程A会被暂停执行,直到线程B执行完毕后,线程A才会继续执行!
具体使用

单使用一个join(),无限期等待,直到线程结束

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

class Main{
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(()->{
            System.out.println("Thread 1 is Running");
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("Thread 1 is Finished");
        });

        thread.start();
        thread.join();

        System.out.println("Main Thread is Running");
    }
}
  • 没加join
    在这里插入图片描述
  • 加了join
    • 在main线程内部,thread调用的join方法,所以main要等待thread线程执行完毕

在这里插入图片描述
指定等待时间

  • 这里就是等待了3ms,但是线程运行需要的执行10ms,所以出现了交叉输出的情况。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

class Main{
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(()->{
            System.out.println("Thread 1 is Running");
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("Thread 1 is Finished");
        });

        thread.start();
        thread.join(3);

        System.out.println("Main Thread is Running");
    }
}

在这里插入图片描述

原子类

CAS
  • CAS是Compare-And-Swap,对比交换。
    • 一条CPU原子指令,先让CPU先进行比较两个值是否相等,然后原子地更新值。
    • 基于硬件实现的,JMM将其封装为AtomicInteger类调用。
    • 需要输入两个数值,分别是旧的和新的,操作期间,先比较一下旧的有没有发生变化,如果没发生就用新的替换,如果发生了,啥都不变
具体使用
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;
class Main{
    public static AtomicInteger count = new AtomicInteger(0);

    public static void increment(){
        count.getAndIncrement();
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(Main::increment);
        Thread t2 = new Thread(Main::increment);
        Thread t3 = new Thread(Main::increment);
        t1.start();
        t2.start();
        t3.start();
        Thread.sleep(10);

        System.out.println(count);
    }
}
CAS问题
  • CAS是乐观锁,然后synchronized是悲观锁
  • CAS面临的问题

ABA问题

  • 原来是A,然后改成了B,后来又变成了A,检查不出来是不是变过了。
  • 场景,两次扣款,中途汇款
  • 解决方法
    • 使用版本号

循环开销大

  • 自旋CAS如果长时间不成功,会给CPU带来非常大的开销的。

只能保证一个共享变量的原子操作

原子操作类

三个常用的Atomic原子类

  • AtomicBoolean
  • AtomicInteger
  • AtomicLong

常用的方法

  • get:获取当前值
  • getAndSet:获取当前值并且设置一个新的值
  • getAndIncrement:获取当前值并且自增1
  • getAndDecrement:获取当前值并且自减1

编程练习

使用读写锁ReentrantLock实现一个Cache

  • 使用ReentantLock实现一个cache,保证get读、put写(更新,并且返回旧的值)以及clear清楚等操作的安全性。
  • 通过读锁来实现保证get方法,通过写锁来保证put方法
static class Cache{
    public static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    public static ReentrantReadWriteLock.ReadLock r = rwl.readLock();
    public static ReentrantReadWriteLock.WriteLock w = rwl.writeLock();
    public static HashMap<Integer,Integer> map = new HashMap<>();

    public int get(int key){
        r.lock();
        try {
            return map.get(key);
        }finally {
            r.unlock();
        }
    }

    public int set(int key,int val){
        w.lock();
        try{
            return map.put(key,val);
        } finally {
            w.unlock();
        }
    }
    
    public void clear(){
        w.lock();
        try{
            map.clear();
        }finally {
            w.unlock();
        }
    }

}
  • 就是一个读写锁的获取还有使用问题,记住了就好了!

线程交叉打印12A34B56C使用ReentrantLock实现

import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

class Main{
    public static ReentrantLock lock = new ReentrantLock();
    public static Condition cd = lock.newCondition();
    public static boolean printNum = true;
    static class PrintNum implements Runnable{
        @Override
        public void run(){
            for(int i = 1;i <= 52;i ++){
                lock.lock();
                try {
                    while (!printNum) cd.await();
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
                System.out.print(i);
                System.out.print(++i);
                printNum = false;
                cd.signalAll();
                lock.unlock();
            }
        }
    }

    static class PrintChar implements Runnable{
        @Override
        public void run(){
            for(char i = 'A';i <= 'Z';i ++){
                lock.lock();
                try {
                    while (printNum) cd.await();
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
                System.out.print(i);
                printNum = true;
                cd.signalAll();
                lock.unlock();
            }
        }
    }


    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new PrintNum());
        Thread t2 = new Thread(new PrintChar());
        t1.start();
        t2.start();

    }
}
  • 这里基本上使用的方式和synchronized的一摸一样,都是使用同样的对象锁进行测试。
参考实现
  • 这里是创建了两个condition,相当于一个读者一个写者进程,两者相互通信。
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

class Main{
    public static ReentrantLock lock = new ReentrantLock();
    public static Condition numCondition = lock.newCondition();
    public static Condition charCondition = lock.newCondition();
    public static boolean printNum = true;
    static class PrintNum implements Runnable{
        @Override
        public void run(){
            for(int i = 1;i <= 52;i ++){
                lock.lock();
                try {
                    while (!printNum) numCondition.await();
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
                System.out.print(i);
                System.out.print(++i);
                printNum = false;
                // 唤醒沉睡的输出字母的线程
                charCondition.signalAll();
                lock.unlock();
            }
        }
    }

    static class PrintChar implements Runnable{
        @Override
        public void run(){
            for(char i = 'A';i <= 'Z';i ++){
                lock.lock();
                try {
                    while (printNum) charCondition.await();
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
                System.out.print(i);
                printNum = true;
                numCondition.signalAll();
                lock.unlock();
            }
        }
    }


    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new PrintNum());
        Thread t2 = new Thread(new PrintChar());
        t1.start();
        t2.start();

    }
}

这里就相当于是一个读者和写者的过程,同一个lock可以创建多个condition

T1、T2、T3三个线程按照顺序执行

  • 这里使用的是join关键字,创建三个线程实现一下,然后使用join连接一下就行
class Main{


    static class Task implements Runnable{
        int taskId;
        public Task(int taskId){
            this.taskId = taskId;
        }

        @Override
        public void run(){
            System.out.println("Task " + taskId + " is running");
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("Task " + taskId + " finished");
        }
    }


    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new Task(1));
        Thread t2 = new Thread(new Task(2));
        Thread t3 = new Thread(new Task(3));



        t1.start();
        t2.start();
        t3.start();

         t1.join();
        t2.join();
        t3.join();
    }
}

在这里插入图片描述

  • 好吧,这里又不会了,这里只能保证三个线程在main线程之前完成,难道要创建位全局变量,然后在顺次传入他们的构造函数中。

一下子又不知道了!

参考实现
  • 这里要确保上一个线程执行完毕之后,下一个线程在启动!
import java.util.HashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

class Main{


    static class Task implements Runnable{
        int taskId;
        public Task(int taskId){
            this.taskId = taskId;
        }

        @Override
        public void run(){
            System.out.println("Task " + taskId + " is running");
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("Task " + taskId + " finished");
        }
    }


    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new Task(1));
        Thread t2 = new Thread(new Task(2));
        Thread t3 = new Thread(new Task(3));



        t1.start();
        t1.join();
        
        t2.start();
        t2.join();

        t3.start();

        t3.join();
    }
}

在这里插入图片描述
这里对于join的理解有深刻了

  • join之前的线程必须要执行完毕,然后才会执行下面的,如果要保证顺次执行,就要安排他们的启动顺序也是顺次的!
  • 如果不加join,顺次启动,还是会有先后顺序,如果把join按照顺序插入在start中,就会保证在下一个启动之前,上一个已经完成了!

总结

  • 明天在花时间的去看看AQS同步器、线程池的相关基础知识,这个看完Java多线程编程就算是完全学完了,题目也练习完了,然后在重新背一遍八股。
  • 好吧,我承认,这个相当于又拖了一天,主要是今天有点太浪了,没怎么看!明天周一了,得加把劲了!开始继续刷题,并且把项目继续推进!
  • 总算是写完了,今天早点睡吧,明天早点起!新的周一开始了,没有面试就好好学习!
  • 13
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值