JUC学习札记

2022-10-16
预计阅读时间:15分钟

I.基础理论:

1.1 线程和进程

进程:系统进行资源分配和调度的基本单位

线程:操作系统能够进行运算调度的最小单位(分配处理器时间资源的基本单元)

1.2 线程中的状态

Thread.State,有以下状态:

NEW(创建)、 RUNNABLE(准备就绪)、 BLOCKED(阻塞)、 WAITING(不见不散)、 TIMED_WAITING(过期不候)、 TERMINATED(终结)

1.3 sleepwait方法的区别

  • sleepThread的静态方法;而waitObject的方法,任何对象示例都可以调用

  • sleep不会释放锁,因此也不许要占用锁,wait会释放锁,但调用的前提是当前线程占有锁,代码要在synchronized

  • 它们都可以被interrupted方法中断

1.4 并发和并行的区别

并发:同一时间多个线程访问同一资源

并行:多项一起执行,执行完毕后进行汇总

1.5 用户线程和守护线程

1.用户线程:主线程结束,用户线程还在运行,jvm存活

2.守护线程:没有用户线程了,都是守护线程,jvm结束

II.核心知识点:

2.1多线程简单应用

  • 例1.卖票场景:
class Ticket{
 	//票数
    private int number = 30;
    //操作方法:卖票
    public synchronized void sale(){
        if(number >= 0){
            System.out.println(Thread.currentThread()
                                     .getName+":卖出"+(number--)+",剩下:"+number);
        }
    }
}
public class SaleTicket{
    //创建多个线程,调用资源类的操作方法
    public static void main(String[] args){
     	//创建Ticket对象
    	Ticket ticket = new Ticket();
    	//创建3个线程
    	new Thread(new Runnable(){
            @Override
        	public void run(){
           		//调用卖票方法
                for(int i = 0; i < 40; i++){
                    ticket.sale();
                }
        	}
    	},"saler1").start();
        
        new Thread(new Runnable(){
            @Override
        	public void run(){
           		//调用卖票方法
                for(int i = 0; i < 40; i++){
                    ticket.sale();
                }
        	}
    	},"saler2").start();
        
        new Thread(new Runnable(){
            @Override
        	public void run(){
           		//调用卖票方法
                for(int i = 0; i < 40; i++){
                    ticket.sale();
                }
        	}
    	},"saler3").start();
    }
}

使用synchronized关键字修饰的方法能够自动实现加锁和解锁,如需手工进行加锁解锁操作,则可以使用lock接口(如果没有及时释放锁,可能导致出现死锁的现象)

  • 例2.使用lock接口实现卖票场景:
class Ticket{
 	//票数
    private int number = 30;
    //创建可重入锁
    private final ReentrantLock lock = new ReentrantLock();
    //操作方法:卖票
    public void sale(){
        //上锁
        lock.lock();
        try{
            //判断是否有票
            if(number >= 0){
            System.out.println(Thread.currentThread()
                                     .getName+":卖出"+(number--)+",剩下:"+number);
        	}
        }finally{
            //解锁
            lock.unlock();
        }   
    }
}
public class saleTicket{
    public static void main(String[] args){
        //创建Ticket对象
    	Ticket ticket = new Ticket();
        //创建3个线程,runnable为函数式接口,使用lambda表达式创建对象
        new Thread(()->{
            for(int i = 1; i<40; i++){
                ticket.sale();
            }
        },"sale1").start();
        
        new Thread(()->{
            for(int i = 1; i<40; i++){
                ticket.sale();
            }
        },"sale2").start();
        
        new Thread(()->{
            for(int i = 1; i<40; i++){
                ticket.sale();
            }
        },"sale3").start();
    }
}

2.2线程间通信

  • 例3.使用synchronized实现2个线程操作同一变量:

在数值判断时使用if,会产生虚假唤醒的情况,因此使用if进行判断的位置需要用while进行替换,使其在唤醒和正常执行的情况下都必须进行数值判断,避免出现预期外的结果产生

class Share{
    //初始值
    private int number = 0;
    
    //数值增加方法
    public synchronized void incr() throws InterruptedException{
        //判断,如果number值不等于0时等待其值改变后做加法操作
        if(number != 0){
            this.wait();
        }
        //上方判断应替换为下面的形式
        while(number != 0){
            this.wait();
        }
        //如果number值等于0,则执行自增操作
        number++;
        System.out.println(Thread.currentThread().getName()+"::"+number);
        //通知其他线程
        this.notifyAll();
    }
    
    //数字减少方法
    public synchronized void decr() throws InterruptedException{
        //判断,如果number值不等于1时等待其值改变后做减法操作
        if(number != 1){
            this.wait();
        }
        //上方判断应替换为下面的形式
        while(number != 0){
            this.wait();
        }
        //如果number值等于1,则执行自减操作
        number--;
        System.out.println(Thread.currentThread().getName()+"::"+number);
        //通知其他线程
        this.notifyAll();
    }
}
public class testThread{
    //创建多个线程调用资源类的操作方法
    Share share = new Share();
    //创建两个线程
    new Thread(()->{
       	for(int i = 0; i < 0; i++){
            try{
               share.incr();
            }catch(InterruptedException e){
               e.printStackTrace();
            }
        }
    },"AA").start();
    
    new Thread(()->{
       	for(int i = 0; i < 0; i++){
            try{
               share.incr();
            }catch(InterruptedException e){
               e.printStackTrace();
            }
        }
    },"BB").start();
}

/** 使用Lock接口实现线程间通信**/
//创建资源类
class Share{
    private int number = 0;

    //创建Lock
    private final Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    //+1操作
    public void incr() throws InterruptedException {
        //上锁
        lock.lock();
        try {
            while (number != 0){
                condition.await();
            }
            //执行+1
            number ++;
            System.out.println(Thread.currentThread().getName() + "::" + number);

            //通知其他线程
            condition.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            //解锁
            lock.unlock();
        }
    }

    //-1操作
    public void decr() throws InterruptedException {
        //上锁
        lock.lock();
        try {
            while (number != 1){
                condition.await();
            }
            number --;
            System.out.println(Thread.currentThread().getName() + "::" + number);

            //通知其他线程
            condition.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            //解锁
            lock.unlock();
        }
    }

}
public class ThreadDemo {

    public static void main(String[] args) {
        Share share = new Share();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    share.incr();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"t1").start();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    share.decr();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"t2").start();
        
    }

}

  • 例4.线程间定制通信:
/**
* 启动3个线程,按照如下要求:
* t1打印5次,t2打印10次,t3打印15次
* 进行10轮
*/
class SharedResource {

    private int flag = 1;

    //创建Lock对象
    private Lock lock = new ReentrantLock();

    //创建三个Condition对象
    private Condition c1 = lock.newCondition();
    private Condition c2 = lock.newCondition();
    private Condition c3 = lock.newCondition();

    public void print5(int loop) throws InterruptedException {
        lock.lock();
        try {
            while (flag != 1){
                c1.await();
            }
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + "::" + i + ":轮数" +loop);
            }
            flag = 2;
            c2.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void print10(int loop) throws InterruptedException {
        lock.lock();
        try {
            while (flag != 2){
                c2.await();
            }
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName() + "::" + i + ":轮数" +loop);
            }
            flag = 3;
            c3.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void print15(int loop) throws InterruptedException {
        lock.lock();
        try {
            while (flag != 3){
                c3.await();
            }
            for (int i = 0; i < 15; i++) {
                System.out.println(Thread.currentThread().getName() + "::" + i + ":轮数" +loop);
            }
            flag = 1;
            c1.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

public class ThreadDemo1 {
    public static void main(String[] args) {
        SharedResource sharedResource = new SharedResource();

        new Thread(()->{
            for (int i = 1; i < 11; i++) {
                try {
                    sharedResource.print5(i);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"t1").start();

        new Thread(()->{
            for (int i = 1; i < 11; i++) {
                try {
                    sharedResource.print10(i);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"t2").start();

        new Thread(()->{
            for (int i = 1; i < 11; i++) {
                try {
                    sharedResource.print15(i);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"t3").start();
    }
}

Lock接口和synchronized关键字的区别:

  1. Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现
  2. synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象的发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁
  3. Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断
  4. 通过Lock可以知道有没有成功获取锁,而synchronized无法办到
  5. Lock可以提高多个线程进行读操作的效率

在锁竞争不激烈的情况下,使用synchronized关键字和Lock接口加锁的性能是差不多的,但是在大量线程竞争资源时,则使用Lock加锁的性能要远远优于synchronized

Synchronized实现同步的基础:Java中的每一个对象都可以作为锁

具体表现为以下3种方式:对于普通方法,锁是当前实例对象;对于静态同步方法,锁是当前Class对象;对于同步方法块,锁是当前括号中配置的对象

ReentrantLock在创建时可以通过构造器中设置true/false来选择使用公平锁和非公平锁,使用前者使得各个县城都有机会访问资源,使用后者可能会出现线程抢占资源的情况(但是执行效率高于公平锁)

synchronizedLock都是可重入锁(递归锁):

前者隐式加锁、释放锁;后者手动加锁、释放锁。即拿到外层锁后内层的锁可以无障碍进入

2.3 死锁

**死锁:**两个或两个以上的进程在执行过程中,因为争夺资源而造成的一种互相等待的现象,如果没有外力干涉,它们无法再执行下去

**造成死锁的原因:**系统资源不足、进程运行推进顺序不合适、资源分配不当

手写死锁代码:

public class DeadLockDemo {

    //创建两个对象
    static Object a = new Object();
    static Object b = new Object();

    public static void main(String[] args) {

        new Thread(()->{
            synchronized (a){
                System.out.println(Thread.currentThread().getName()+":持有锁a,试图获取锁b");
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (b){
                    System.out.println(Thread.currentThread().getName()+":获得锁b");
                }
            }
        },"A").start();

        new Thread(()->{
            synchronized (b){
                System.out.println(Thread.currentThread().getName()+":持有锁b,试图获取锁a");
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (a){
                    System.out.println(Thread.currentThread().getName()+"获得锁a");
                }
            }
        },"B").start();
    }
}

验证是否产生死锁:

  1. 使用jps -l列出Java进程定位当前进程id
  2. 使用JVM自带的堆栈跟踪工具jstack <进程id>查看栈信息,如果出现Found 1 deadlock.则可以确定出现死锁

使用Callable接口创建线程:

Callable的实现类不能直接传入Thread的构造方法中,需要传入FutureTask(一个Runnable实现类)的构造器,再将FutureTask对象传入Thread构造器中

class MyThread implements Callable{

    @Override
    public Integer call() {
        System.out.println(Thread.currentThread().getName()+":使用callable创建线程");
        return 200;
    }
}

public class CallableDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask<Integer> futureTask = new FutureTask(()->{
            System.out.println(Thread.currentThread().getName()+":使用callable创建线程");
            return 1024;
        });

        new Thread(futureTask,"callT").start();
        new Thread(new FutureTask<Integer>(new MyThread()),"callT1").start();

        while (!futureTask.isDone()){
            System.out.println("wait ...");
        }

        System.out.println(futureTask.get());
        System.out.println(futureTask.get());

        System.out.println(Thread.currentThread().getName()+":执行结束!");

    }
}

FutureTask类的特点:

主线程依次向下执行,可以单开几个线程做其他事情,最后做汇总,汇总只须做一次

CallableRunnable之间的区别:

  1. 是否具有返回值
  2. 是否抛出异常
  3. 需要重写的方法,前者重写call()后者重写run()

读写锁

常见的几种锁:

乐观锁:支持并发操作,使用版本号控制事务提交,后提交事务的线程无法完成对数据的操作

悲观锁:不支持并发操作,每个线程操作操作前后需要上锁和释放锁,效率低下

行锁:对正在被操作的行加锁,未加锁的行可以被正操作(会发生死锁)

表锁:操作某条记录时对整张表进行加锁,其他的线程无法对该表进操作

读锁(共享锁):当两个线程对一条记都有读和修改的操作时,会出现死锁

写锁(独占锁):在两个线程对两条记录都有写操作时,会出现死锁

读写锁简单应用:

class MyCache {
    //创建集合
    private volatile Map<String, Object> map = new HashMap<>();

    //读写锁
    private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();

    //放数据
    public void put(String key, Object value) {
        //添加写锁
        rwLock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName()+":正在写操作" + key);
            //暂停一会
            TimeUnit.MICROSECONDS.sleep(300);
            //放数据
            map.put(key, value);
            System.out.println(Thread.currentThread().getName()+":写完了"+ key);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            //释放写锁
            rwLock.writeLock().unlock();
        }
    }

    //读数据
    public Object get(String key) {
        //添加读锁
        rwLock.readLock().lock();
        Object result = null;
        try {
            System.out.println(Thread.currentThread().getName()+":正在读操作" + key);
            //暂停一会
            TimeUnit.MICROSECONDS.sleep(300);
            //取数据
            result = map.get(key);
            System.out.println(Thread.currentThread().getName()+":读完了"+ key);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            //释放读锁
            rwLock.readLock().unlock();
        }
        return result;
    }

}
public class ReadWriteLockDemo {
    public static void main(String[] args) {
        MyCache mc = new MyCache();

        //创建线程放数据
        for (int i = 0; i < 5; i++) {
            final int num  = i;
            new Thread(()->{
                mc.put(num+"", num+"");
            },String.valueOf(i)).start();
        }

        //创建线程读数据
        for (int i = 0; i < 5; i++) {
            final int num = i;
            new Thread(()->{
                mc.get(num+"");
            },String.valueOf(i)).start();
        }
    }
}

---------------------控制台输出------------------------

0:正在写操作0
0:写完了0
1:正在写操作1
1:写完了1
2:正在写操作2
2:写完了2
3:正在写操作3
3:写完了3
4:正在写操作4
4:写完了4
0:正在读操作0
1:正在读操作1
2:正在读操作2
3:正在读操作3
4:正在读操作4
0:读完了0
2:读完了2
3:读完了3
1:读完了1
4:读完了4

使用不同下的状态:

无锁 独占锁(synchronized、ReentrantLock) 读写锁(ReentrantReadWriteLock)
情况 多线程抢夺资源 每次只能由一个线程进行操作 两个读操作之间可以进行共享,两个写操作之间只能由其中一个线程执行操作
缺陷 操作混乱,容易产生各种问题 无论哪种操作都不能共享 读完才可以进行写操作,写操作时可以进行读操作;容易造成锁饥饿

jdk8中写锁降级为读锁的过程

获取写锁 -> 获取读锁 -> 释放写锁 -> 释放读锁

注:读锁不能升级为写锁

2.4阻塞队列

阻塞队列特点:

image-20230313121934287

当队列是空的,从队列中获取元素的操作将会被阻塞

当队列是满的,向队列中添加元素的操作将会被阻塞

试图从空的队列中获取元素的线程将会被阻塞,直到其他的线程向空的队列中插入新的元素

试图向已满的队列中插入元素的线程将会被阻塞,直到其他的线程从队列中移除一个或多个元素或者完全清空,使得队列变得空闲起来并继续新增元素

BlockingQueue中的核心方法

方法类型 抛出异常 特殊值 阻塞 超时
插入 add(e) offer(e) put() offer(e,time,unit)
移除 remove() poll() take() poll(time,unit)
检查 element() peek() / /
抛出异常 当阻塞队列满时,再往队列中add插入元素会抛出IllegalStateException:Queue full;当阻塞队列空时,再往队列中remove移除元素会抛出NoSuchElementException
特殊值 插入方法,成功返回true,失败返回false;移除方法,成功返回出队列的元素,队列里没有就返回null
一直阻塞 当阻塞队列满时,生产者线程继续往队列中put元素,队列会一直阻塞生产者线程直到take数据or响应中断退出;当阻塞队列空时,消费者线程试图从队列中take元素,队列会一直阻塞消费者线程直到队列可用
超时退出 当阻塞队列满时,队列会阻塞生产者线程一定时间,超过限时后生产者线程会退出

2.5线程池

线程池概念

线程池是一种线程使用模式,线程过多会带来调度开销,进而影响局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。

优势:线程池的工作主要是控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超出数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行。

特点:降低资源消耗、提高响应速度、提高线程的可管理性

Java中的线程池是通过Executor框架实现的,该框架中用到了ExecutorExecutorsExecutorServiceThreadPoolExecutor这几个类

image-20230313194339273

常用线程池类型

/**一池n线程**/
ExecutorService threadPool1 = Executors.newFixedThreadPool(10);

/**一池一线程**/
ExecutorService threadPool2 = Executors.newSingleThreadExecutor();

/**可扩容线程池**/
ExecutorService threadPool3 = Executors.newCachedThreadPool();

try {
    for (int i = 0; i < 50; i++) {
      	//执行execute方法后,线程才会创建
        threadPool3.execute(()->{
        		System.out.println(Thread.currentThread().getName()+":正在办理业务!");
        });
    }
} catch (Exception e) {
    e.printStackTrace();
} finally {
    threadPool3.shutdown();
}

上述3种类型线程池底层都是使用ThreadPoolExecutor来实现。同时,也可以使用ThreadPoolExecutor来自定义线程池,自定义线程池时的7个参数如下所示:

/** 1.常驻线程数量**/
int corePoolSize,
/** 2.最大线程数量**/
int maximumPoolSize,
/** 3.线程存活时间**/
long keepAliveTime,
/** 4.时间单位**/
TimeUnit unit,
/** 5.阻塞队列**/
BlockingQueue<Runnable> workQueue,
/** 6.线程工厂(用来创建线程)**/
ThreadFactory threadFactory,
/** 7.拒绝策略 **/
RejectedExecutionHandler handler
// 4种拒绝策略
-CallerRunsPolicy:将任务回退到调用者,从而降低任务流量
-AbortPolicy:默认,直接抛出RejectedExecutionException异常
-DiscardPolicy:默认丢弃无法处理的任务,不做任何处理也不抛出异常
-DiscardOldestPolicy:抛弃阻塞队列中等待最久的任务

注意:【强制】通常情况下不允许通过Executors创建线程,而是通过ThreadPoolExecutor的方式自定义创建线程池,为了规避资源耗尽的风险:

Executors返回线程池对象的弊端

  1. FixedTHreadPoolSingleThreadPool允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM
  2. CachedThreadPoolScheduledThreadPool允许创建线程数量为Integer.MAX_VALUE,从而导致OOM

使用ThreadPoolExecutor自定义线程池示例:

public class CustomThreadPoolDemo {
    public static void main(String[] args) {
        ExecutorService threadpool = new ThreadPoolExecutor(7,
                10,
                2L,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(3),
                new CustomizableThreadFactory("devon-thread"),
                new ThreadPoolExecutor.DiscardPolicy());

        try {
            for (int i = 0; i < 20; i++) {
                threadpool.execute(()->{
                    System.out.println(Thread.currentThread().getName()+":正在受理业务!");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadpool.shutdown();
        }
    }
}

--------------------------控制台输出----------------------------
devon-thread1:正在受理业务!
devon-thread3:正在受理业务!
devon-thread2:正在受理业务!
devon-thread4:正在受理业务!
devon-thread5:正在受理业务!
devon-thread6:正在受理业务!
devon-thread7:正在受理业务!
devon-thread1:正在受理业务!
devon-thread1:正在受理业务!
devon-thread8:正在受理业务!
devon-thread8:正在受理业务!
devon-thread9:正在受理业务!
devon-thread7:正在受理业务!
devon-thread2:正在受理业务!
devon-thread1:正在受理业务!
devon-thread2:正在受理业务!
devon-thread10:正在受理业务!
devon-thread7:正在受理业务!
devon-thread3:正在受理业务!
devon-thread8:正在受理业务!

Process finished with exit code 0

								------ 持续更新中,敬请期待 ------
上一篇 CSS复习小记