多线程 | Lock体系


1.简介

锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁能够防止多个线程同时访问共享资源。
在Lock接口出现之前,java程序主要是靠synchronized关键字实现锁功能的,而JDK5之后,并发包中增加了lock接口,它提供了与 synchronized一样的锁功能。虽然它失去了像synchronize关键字隐式加锁解锁的便捷性,但是却拥有了锁获取释放的可操作性,可中断的获取锁以及超时获取锁等多种synchronized关键字所不具备的同步特性。
通常使用lock的形式如下:

	Lock lock = new ReentrantLock(); 
	try{
	lock.lock();   //设置当前线程同步状态,并在队列中保存线程和线程的同步状态
		 .......      //设置成功往下执行
	}finally{ 
		lock.unlock(); 
	} 

需要注意的是synchronized同步块执行完成或者遇到异常是锁会自动释放,而lock必须调用unlock()方法释放锁, 因此在finally块中释放锁。


2.Lock接口API

void lock();-----获取锁

void lockInterruptibly() throws InterruptedException----获取锁的过程能够响应中断(lock独有)

Boolean tryLock();----非阻塞式响应中断,能立即返回,获取锁返回true反之为false

boolean tryLock(long time,TimeUnit unit);----增加了超时等待机制,规定时间内未获取到锁,线程直接返回(lock独有)

void unlock();----解锁

Condition newCondition(); ----获取与lock绑定的等待通知组件,当前线程必须先获得了锁才能等待,等待会释
放锁,再次获取到锁才能从等待中返回。

Lock锁的实现原理---->AQS


3.AQS

全称:AbstractQueuedSynchronizer抽象队列同步器(Lock体系最核心)

3.1 AQS实现原理:双端队列保存线程和线程的同步状态,并通过CAS提供设置线程同步状态的方法。
如ReentranLock实现时,调用lock.lock()操作,会不停的设置线程同步状态。

AQS提供的模板方法可以分为3类:

  1. 独占式获取与释放同步状态;
  2. 共享式获取与释放同步状态;
  3. 查询同步队列中等待线程情况;
    同步组件通过AQS提供的模板方法实现自己的同步语义

3.2 AQS的应用

  1. CountDownLatch

例:多线程并发问题三种解决方法

public class CountDownLatchTest {

    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[10];
        CountDownLatch countDownLatch = new CountDownLatch(10);  //给定初始值
        for (int i = 0; i < 10; i++) {
            final int j = i;  //(匿名)内部类只能访问final变量
            threads[i] =  new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println(j);
                    countDownLatch.countDown(); //运行一次-1;
                }
            });
        }
        //要求所有线程运行结束,主线程再打印
        for (Thread t:threads) {
            t.start();
        }
         /**
         * 方式1:活跃线程数>1,main线程让步
         * 
        while (Thread.activeCount() > 1){
            Thread.yield();
        }
         */
        
        /**
         * 方式2:join()
         * 
        for (int i = 0; i < 10; i++) {
            threads[i].join();
        }
         */

        /**
         * 方式3:CountDownLatch
         */
        countDownLatch.await(); //当前线程阻塞等待,直到CountDownLatch为0
        System.out.println("所有线程运行结束");
    }
}
0
1
2
3
4
5
6
7
8
9
所有线程运行结束

  1. Semaphore
    场景:
    (1)和CountDownLatch一致。
    (2)多线程有限资源的同时访问
    使用:
    1.new Semaphore(int a): 给定a数量的初始信号量
    2.acquire(int b):获取Semaphore中b数量的信号量。获取到,信号量减少,继续执行。获取不到,当前线程阻塞等待
    3.release(int c):释放信号量,计数器增加c数量的信号量

例:模拟餐厅有2个窗口,但是有20个人要吃饭。

public class SemaphoreTest {
    public static int count  = 20;
    public static int threadTotal  = 2;
    public static void main(String[] args) throws InterruptedException {
        Semaphore semaphore = new Semaphore(threadTotal); // 初始信号量为0
        CountDownLatch countDownLatch = new CountDownLatch(count);
        for (int i = 0; i < count; i++) {
            Thread thread = new Thread(()-> {
                try {
                    Thread.sleep(50);
                    semaphore.acquire(1);
                    System.out.println("当前次饭线程:"+Thread.currentThread().getName());
                    countDownLatch.countDown();
                }catch (InterruptedException e){
                    e.printStackTrace();
                }finally {
                    semaphore.release(1);
                }
            });
            thread.start();
        }
        countDownLatch.await();
        System.out.println(Thread.currentThread().getName()+"结束,所有人次饭完毕");
    }
}
当前次饭线程:Thread-2
当前次饭线程:Thread-1
当前次饭线程:Thread-0
当前次饭线程:Thread-11
当前次饭线程:Thread-10
当前次饭线程:Thread-9
当前次饭线程:Thread-8
当前次饭线程:Thread-7
当前次饭线程:Thread-6
当前次饭线程:Thread-5
当前次饭线程:Thread-4
当前次饭线程:Thread-3
当前次饭线程:Thread-13
当前次饭线程:Thread-12
当前次饭线程:Thread-14
当前次饭线程:Thread-15
当前次饭线程:Thread-18
当前次饭线程:Thread-19
当前次饭线程:Thread-17
当前次饭线程:Thread-16
main结束,所有人次饭完毕
  1. 线程池ThreadPoolExecutor
    线程池多看看源码,会用就OK

使用线程池的三个优点如下:

  1. 降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁带来的消耗。
  2. 提高响应速度:当任务到达时,任务可以不需要等待线程创建就能立即执行。
  3. 提高线程的可管理性:使用线程池可以统一进行线程分配、调度和监控。

线程池参数

public class ThreadPool {
    public static void main(String[] args) {
        ExecutorService pool = new ThreadPoolExecutor(
                1, //核心线程数
                2, //最大线程数
                3, //时间数量
                TimeUnit.MILLISECONDS, //时间单位
                //时间数量+时间单位决定了临时线程的存活时间
                
                new ArrayBlockingQueue<>(100),  //阻塞队列
                new ThreadFactory() { //线程池创建Thread线程的工厂类,如果没有提供,就使用线程内部默认的创建线程的方法
            @Override
            public Thread newThread(Runnable r) {
                return null;
            }
        }, new ThreadPoolExecutor.CallerRunsPolicy());  // 拒绝策略(4种)

 /*     RejectedExecutionHandler rejected = null;
        rejected = new ThreadPoolExecutor.AbortPolicy();//默认,队列满了丢任务抛出异常
        rejected = new ThreadPoolExecutor.DiscardPolicy();//队列满了丢任务不异常
        rejected = new ThreadPoolExecutor.DiscardOldestPolicy();//将最早进入队列的任务删,之后再尝试加入队列
        rejected = new ThreadPoolExecutor.CallerRunsPolicy();//如果添加到线程池失败,那么主线程会自己去执行该任务
 */   
    }
}

向线程池提交任务
可以使用两个方法向线程池提交任务,分别为execute()submit()方法。
execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功。

例:使用execute()方法(传入实现Runnable的对象)

package ThreadPool;

import java.util.concurrent.*;
class RunnableThread implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println(Thread.currentThread().getName() + "、" + i);
        }
    }
}
public class execute{
    public static void main(String[] args){
        RunnableThread runnableThread = new RunnableThread();
        ThreadPoolExecutor threadPoolExecutor =
                new ThreadPoolExecutor(3,5,2000,TimeUnit.MILLISECONDS,
                        new LinkedBlockingDeque<Runnable>());
        for (int i = 0; i < 5; i++) {
            threadPoolExecutor.execute(runnableThread);
        }
    }
}

submit()方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值

get()方法会阻塞当前线程直到任务完成,而get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。

例:使用submit()方法(传入实现Callable的对象)

import java.util.concurrent.*;
class CallableThread implements Callable<String> {
    @Override
    public String call() throws Exception {
        for (int i = 0; i < 20; i++) {
            System.out.println(Thread.currentThread().getName() + "、" + i);
        }
        return Thread.currentThread().getName()+"任务执行完毕";
    }
}

public class submit {
    public static void main(String[] args){
        CallableThread callableThread = new CallableThread();
        ThreadPoolExecutor threadPoolExecutor =
                new ThreadPoolExecutor(3,5,2000,TimeUnit.MILLISECONDS,
                        new LinkedBlockingDeque<Runnable>());
        for (int i = 0; i < 5; i++) {
            Future<String> future = threadPoolExecutor.submit(callableThread);
            try {
                String str = future.get();
                System.out.println(str);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }
    }
}

4.Condition

任何一个java对象都继承于Object类,在线程间实现通信的往往会应用到Object的几个方法,比如 wait()notify(),notifyAll()几个方法实现等待/通知机制。
同样的, 在 java Lock体系下依然会有同样的方法实现等待/通知机制。
区别:从整体上来看Object的wait和notify/notify是与对象监视器配合完成线程间的等待/通知机制,而Condition与Lock配合完成等待通知机制,前者是java底层级别的,后者是语言级别的,具有更高的可控制性和扩展性。
两者除了在使用方式上不同外,在功能特性上还是有很多的不同:

  1. Condition能够支持不响应中断,而通过使用Object方式不支持;
  2. Condition能够支持多个等待队列(new 多个Condition对象),而Object方式只能支持一个;
  3. Condition能够支持超时时间的设置,而Object不支持。

使用:
(1)通过锁对象.new Condition()获取Condition对象
(2)Condition对象.await(),让当前对象阻塞等待,并释放锁(=synchronized锁对象.wait())
(3)Condition对象.signal/signalAll(),通知之前await阻塞的线程(=synchronized锁对象.notify/notifyAll())

具体例子:

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LockTest {
    public static Lock LOCK = new ReentrantLock();
    public static Condition CONDITION = LOCK.newCondition();  //线程通信
    public static void t3(){
        try {
        LOCK.lock();
            //执行业务
        }finally {
            LOCK.unlock();
        }

    }
    public static void t4(){
        try {
        	LOCK.lock();
            t3(); //Reentrant关键字的Lock包下的API:可重入锁
            //执行业务
        }finally {
            LOCK.unlock();
        }
    }

    public static void t5() {   //生产者消费者模型
        try {
        	LOCK.lock();   //=synchronized()加锁的代码
//            //执行业务
//            while (库存达到上限){
//                CONDITION.await();  //=synchronized锁对象.wait();
//            }
//            CONDITION.signal(); //=synchronized锁对象.notify();
//            CONDITION.signalAll();//=synchronized锁对象.notifyAll();

        }finally {
            LOCK.unlock();
        }

    }
}

5.ReentrantReadWriteLock - 读写锁

实现Lock接口的 ReentrantReadWriteLock - 提供的读写锁API (WirteLock和ReadLock)
场景:读写锁允许同一时刻被多个读线程访问,但是在写线程访问时,所有的读线程和其他的写线程都会被阻塞

使用:

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

public class ReadWriteLockTest {
    //ReadWriteLock为接口,ReentrantReadWriteLock为实现类
    private static ReadWriteLock LOCK = new ReentrantReadWriteLock();
    private static Lock READ_LOCK = LOCK.readLock();
    private static Lock WRITE_LOCK = LOCK.writeLock();

    public static void readFile(){  //读操作加锁
        try {
            READ_LOCK.lock();
            //IO读取
        }finally {
            READ_LOCK.unlock();
        }
    }

    public static void writeFile(){  //写操作加锁
        try {
            WRITE_LOCK.lock();
            //IO写
        }finally {
            WRITE_LOCK.unlock();
        }
    }
}

读写锁总结:

  1. 公平性选择:支持非公平性(默认)和公平的锁获取方式,吞吐量还是非公平优于公平;
  2. 重入性:支持重入,读锁获取后能再次获取,写锁获取之后能够再次获取写锁,同时也能够获取读锁;
  3. 锁只能降级:遵循获取写锁,获取读锁再释放写锁的次序。写锁能够降级成为读锁 ,读锁不能升级为写锁

优点:针对读读并发执行,提高运行效率。


6.Lock锁特点
  1. 提供两种锁:公平锁非公平锁
    如果一个锁是公平的,那么锁的获取顺序就应该符合请求上的绝对时间顺序,满足FIFO。

ReentrantLock的构造方法无参时是构造非公平锁。另外还提供了另外一种方式,可传入一个boolean值,true时为公平锁,false时为非公平锁

  1. AQS提供独占式获取同步状态共享式获取同步状态(获取锁的本质是设置线程同步状态)
    独占式:只允许一个线程获取到锁(独占锁)
    共享式:一定数量的线程共享式获取锁(共享锁)
  2. 带Reentrant关键字的Lock包下的API:可重入锁(允许多次获取同一个对象的锁)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值