JUC并发编程(面试2)

2 篇文章 0 订阅

1.ArrayList线程不安全问题
2.HashSet线程不安全问题
3.HashMap线程不安全问题
4.虚假唤醒问题
5.公平锁/非公平锁
6.可重入锁
7.Callable
8.CountDownLatch
9.CyclicBarrier
10.Semaphore
11.读写锁
12.锁降级
13.阻塞队列BlockingQueue
14.线程池
15.线程池七大参数
16.线程池四种拒绝策略
17.自定义线程池

ArrayList线程不安全问题 点击观看尚硅谷视频

案例:

public class A {
    public static void main(String[] args) {
        //创建arraylist集合
        List<String> list = new ArrayList<>();

        for (int i = 0; i < 50; i++) {
            new Thread(() -> {
                //向集合中添加内容
                list.add(UUID.randomUUID().toString().substring(0, 8));
                //从集合中获取内容
                System.out.println(list);
            }, String.valueOf(i)).start();
        }
    }
}

解决方案:

  • Vector (sync)
  • Collections (工具类sync)
  • CopyOnWriteArrayList (写时复制技术:并发读独立写)

Vector(jdk1.0)

public class A {
    public static void main(String[] args) {
        //创建Vector集合
        List<String> list = new Vector<>();

        for (int i = 0; i < 50; i++) {
            new Thread(() -> {
                //向集合中添加内容
                list.add(UUID.randomUUID().toString().substring(0, 8));
                //从集合中获取内容
                System.out.println(list);
            }, String.valueOf(i)).start();
        }
    }
}

Collections

public class A {
    public static void main(String[] args) {
        List<String> list =Collections.synchronizedList(new ArrayList<>());

        for (int i = 0; i < 50; i++) {
            new Thread(() -> {
                //向集合中添加内容
                list.add(UUID.randomUUID().toString().substring(0, 8));
                //从集合中获取内容
                System.out.println(list);
            }, String.valueOf(i)).start();
        }

    }
}

CopyOnWriteArrayList(juc->java.util.concurrent包)

public class A {
    public static void main(String[] args) {
        List<String> list =new CopyOnWriteArrayList<>();

        for (int i = 0; i < 50; i++) {
            new Thread(() -> {
                //向集合中添加内容
                list.add(UUID.randomUUID().toString().substring(0, 8));
                //从集合中获取内容
                System.out.println(list);
            }, String.valueOf(i)).start();
        }
    }
}


HashSet线程不安全问题

解决方案

  • CopyOnWriteArraySet
public class A {
    public static void main(String[] args) {
        Set<String> list =new CopyOnWriteArraySet<>();

        for (int i = 0; i < 50; i++) {
            new Thread(() -> {
                //向集合中添加内容
                list.add(UUID.randomUUID().toString().substring(0, 8));
                //从集合中获取内容
                System.out.println(list);
            }, String.valueOf(i)).start();
        }
    }
}


HashMap线程不安全问题

解决方案

  • ConcurrentHashMap
public class A {
    public static void main(String[] args) {
        Map<String,String> map =new ConcurrentHashMap<>();

        for (int i = 0; i < 50; i++) {
            new Thread(() -> {
                //向集合中添加内容
                map.put(UUID.randomUUID().toString().substring(0, 8),"a");
                //从集合中获取内容
                System.out.println(map);
            }, String.valueOf(i)).start();
        }
    }
}


wait虚假唤醒问题

案例:
假设有四个线程A、B、C、D同时启动,我们定义A和B为加法线程,C和D为减法线程,每个线程执行5次回到原点,我们的期望结果是:0,1,0,1,0,1…0,1,0

代码:

public class A {
    public static void main(String[] args) {
        Data data = new Data();
        //生产者线程A
        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "A").start();

        //生产者线程B
        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "B").start();

        //消费者线程C
        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "C").start();

        //消费者线程D
        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "D").start();
    }


    //数据类
    static class Data {
        //表示数据个数
        private int number = 0;

        public synchronized void increment() throws InterruptedException {
            if (number != 0) {
                this.wait();
            }
            number++;
            System.out.println(Thread.currentThread().getName() + "生产了数据:" + number);
            this.notify();
        }

        public synchronized void decrement() throws InterruptedException {
            if (number == 0) {
                this.wait();
            }
            number--;
            System.out.println(Thread.currentThread().getName() + "消费了数据:" + number);
            this.notify();
        }
    }
}

执行:
在这里插入图片描述
出现上述情况原因:

假设:

  • 第1步、A抢到锁执行 ++ 1
  • 第2步、A执行notify发现没有人wait,继续拿着锁执行 ,A判断不通过,A阻塞 1
  • 第3步、B抢到锁 ,B判断不通过,B阻塞  1
  • 第4步、C 抢到锁 执行--    0
  • 第5步、C 执行Notify 唤醒A, A执行++ 1
  • 第6步、A 执行notify唤醒B ,B执行++ 2 (注意这个地方恰巧唤醒B,那么B 从哪阻塞的就从哪唤醒,B继续执行wait下面的++操作,导致出现2

解决方案: 将代码中的两个if变为while。因为改为while后,当线程被唤醒后他会进行while循环做判断,只有符合条件才会执行代码块后的代码,不符合条件会继续wait下去,等待下一次被唤醒。

但是,又会出现下面的结果:(会偶然发生)
在这里插入图片描述
导致这种偶然情况的原因是:假设当前有A,B,C三个线程在等待池中等待,D线程执行了notify()方法后会唤醒其中的一个线程,同时由于D线程执行完了整个临界区的代码会进入锁池。而唤醒的线程又刚好是进行-1操作的C线程,C线程进入锁池,又刚好锁池中的C线程抢到了锁,这时C线程被唤醒后进行while循环判断,判断后发现符合wait条件,继续释放锁在等待区等待。这时锁池中的D线程抢到对象锁执行临界区代码,进行while判断,发现符合wait条件,进入等待池中等待,自此A,B,C,D四个线程全部处在等待池等待其他线程唤醒,但是已经再没有其他线程可以唤醒他们了,就进入死锁状态。

解决办法: 将代码中的notify()改为notifyAll()



公平锁/非公平锁

公平锁: 公平锁会维护一个先进先出的线程等待队列,多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。

  • 优点:所有的线程都能得到资源,不会饿死在队列中
  • 缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。

非公平锁: 多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。

  • 优点:可以减少CPU唤醒线程的开销,整体的吞吐效率高,CPU也不必取唤醒所有线程,会减少唤起线程的数量。
  • 缺点:你们可能也发现了,这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死

具体实现:

  • synchronized: 非公平锁

  • ReentrantLock(JUC包下,父接口为Lock)

    • public ReentrantLock() {sync = new NonfairSync();},调用无参构造创建非公平锁
    • public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync();},调用有参构造创建,传入参数为false创建非公平锁,true为公平锁。


可重入锁

  • 可重入锁指的是可重复可递归调用的锁

具体实现:

  • synchronized(自动上锁解锁)
  • ReentrantLock(手动上锁解锁,一次上锁对应一次解锁,如果上锁和解锁的次数不匹配,则会出现死锁发生)
  • ReentrantReadWriteLock(读写锁)


Callable接口 (JUC包)

Callable接口(JUC包)与Runnable接口(java.lang包)区别

  • 抽象方法名: Callable接口中的抽象方法名为call(),Runnable接口中的抽象方法名为run()。
  • 是否有返回值: Callable接口中的抽象方法有返回值,Runnable接口中的抽象方法无返回值。
  • 是否可抛出异常: Callable接口中的call方法可以抛出异常,Runnable接口中的run()不可以抛出异常。


CountDownLatch(JUC包下)

跳转链接

CountDownLatch概念

CountDownLatch是一个同步工具类,用来协调多个线程之间的同步,或者说起到线程之间的通信(而不是用作互斥的作用)。

CountDownLatch能够使一个线程在等待另外一些线程完成各自工作之后,再继续执行。使用一个计数器进行实现。计数器初始值为线程的数量。当每一个线程完成自己任务后,计数器的值就会减一。当计数器的值为0时,表示所有的线程都已经完成一些任务,然后在CountDownLatch上等待的线程就可以恢复执行接下来的任务。

CountDownLatch的用法

CountDownLatch典型用法:1、某一线程在开始运行前等待n个线程执行完毕。将CountDownLatch的计数器初始化为new CountDownLatch(n),每当一个任务线程执行完毕,就将计数器减1 countdownLatch.countDown(),当计数器的值变为0时,在CountDownLatch上await()的线程就会被唤醒。一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。

CountDownLatch典型用法:2、实现多个线程开始执行任务的最大并行性。注意是并行性,不是并发,强调的是多个线程在某一时刻同时开始执行。类似于赛跑,将多个线程放到起点,等待发令枪响,然后同时开跑。做法是初始化一个共享的CountDownLatch(1),将其计算器初始化为1,多个线程在开始执行任务前首先countdownlatch.await(),当主线程调用countDown()时,计数器变为0,多个线程同时被唤醒。

CountDownLatch的不足

CountDownLatch是一次性的,计算器的值只能在构造方法中初始化一次,之后没有任何机制再次对其设置值,当CountDownLatch使用完毕后,它不能再次被使用。

案例:

班上有6个同学,等到这6个同学都走完,班长锁门。

代码:

public class B {

    public static void main(String[] args) throws Exception {
        //定义初始值
        CountDownLatch countDownLatch = new CountDownLatch(6);
        for (int i = 1; i <= 6; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "号同学走了");
                //计数 -1
                countDownLatch.countDown();
            }, String.valueOf(i)).start();
        }
        //计数!=0 等待
        countDownLatch.await();
        System.out.println("班长锁门");
    }
}


CyclicBarrier(JUC)

public class B {
    private static final int NUMBER = 7;
    public static void main(String[] args) {
        //创建CyclicBarrier
        CyclicBarrier cyclicBarrier=new CyclicBarrier(NUMBER,()->{
           System.out.println("龙珠集齐,神龙召唤");
        });

        //收集龙珠
        for(int i=1;i<=NUMBER;i++){
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+"号龙珠已获得");
                try {
                    //等待
                    cyclicBarrier.await();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            },String.valueOf(i)).start();
        }
    }
}


Semaphore(JUC)



读写锁

ReentrantReadWriteLock(JUC包)

  • 写锁(独占锁/排他锁) :new ReentrantReadWriteLock().writeLock()
  • 读锁(共享锁) :new ReentrantReadWriteLock().readLock()

案例:

public class B {
    public static void main(String[] args) {
        MyCache myCache = new MyCache();
        //创建线程放数据
        for (int i = 1; i <= 5; i++) {
            final int num = i;
            new Thread(() -> {
                try {
                    myCache.put(num + "", num + "");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            },String.valueOf(i)).start();
        }

        //创建线程取数据
        for (int i = 1; i <= 5; i++) {
            final int num = i;
            new Thread(() -> {
                try {
                    myCache.get(num+"");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            },String.valueOf(i)).start();
        }
    }

}

class MyCache {
    //创建map集合
    private volatile Map<String, Object> map = new HashMap<>();
    //创建读写锁对象
    private ReadWriteLock rwLock = new ReentrantReadWriteLock();

    //写数据
    public void put(String key, Object value) throws InterruptedException {
        //添加写锁
        rwLock.writeLock().lock();

        map.put(key, value);
        System.out.println(Thread.currentThread().getName() + "正在写操作" + key);
        Thread.sleep(3000);
        //放数据
        map.put(key, value);
        System.out.println(Thread.currentThread().getName() + "写完了" + key);

        //释放写锁
        rwLock.writeLock().unlock();
    }

    //取数据
    public Object get(String key) throws InterruptedException {
        //添加读锁
        rwLock.readLock().lock();

        System.out.println(Thread.currentThread().getName() + "正在读操作" + key);
        Thread.sleep(3000);
        System.out.println(Thread.currentThread().getName() + "读完了" + key);

        //释放读锁
        rwLock.readLock().unlock();

        return map.get(key);


    }
}


锁降级

写锁可以降级为读锁:写的过程可以进行读操作。

public class DD {
    private static ReadWriteLock rwLock = new ReentrantReadWriteLock();

    public static void main(String[] args) {
		//先获取写锁
        rwLock.writeLock().lock();
        System.out.println("写操作");
		//再获取读锁
        rwLock.readLock().lock();
        System.out.println("读操作");

        rwLock.readLock().unlock();
        rwLock.writeLock().unlock();

    }
}

结果
在这里插入图片描述

但是读锁不可以升级为写锁。

public class DD {
    private static ReadWriteLock rwLock = new ReentrantReadWriteLock();

    public static void main(String[] args) {
        //先获取读锁
        rwLock.readLock().lock();
        System.out.println("读操作");
        
        //再获取写锁
        rwLock.writeLock().lock();
        System.out.println("写操作");

        rwLock.readLock().unlock();
        rwLock.writeLock().unlock();

    }
}

结果:
在这里插入图片描述



阻塞队列
在这里插入图片描述

  • 当队列是空的,从队列中获取元素的操作被阻塞。
  • 当队列是满的,从队列中存放元素的操作被阻塞。

阻塞: 在某一种情况下,线程被挂起的状态叫线程阻塞。

BlockingQueue(JUC包下)

优点: 使用者无需关心什么时候需要阻塞线程,什么时候需要唤醒线程,BlockingQueue会对线程进行管理。

BlockingQueue(接口)
实现类(五种):

  • ArrayBlockingQueue

基于数组的阻塞队列实现,在ArrayBlockingQueue内部,维护了一个定长数组,以便缓存队列中的数据对象,这是一个常用的阻塞队列,除了一个定长数组外,ArrayBlockingQueue 内部还保存着两个整形变量,分别标识着队列的头部和尾部在数组中的位置。ArrayBlockingQueue是由数组结构组成的有界阻塞队列。

ArrayBlockingQueue在生产者放入数据和消费者获取数据,都是共用同一个锁对象由此也意味着两者无法真正并行运行


  • LinkedBlockingQueue

LinkedBlockingQueue是由链表结构组成的有界阻塞队列。默认大小为Integer.MAX_VALUE


  • DelayQueue

DelayQueue中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。DelayQueue 是一个没有大小限制的队列,因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞。使用优先级队列实现的延迟无界阻塞队列。


  • PriorityBlockingQueue

支持优先级排序的无界阻塞队列。


  • SynchronousQueue

只存储单个元素的阻塞队列。


核心方法 点击观看
在这里插入图片描述



线程池

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

它的主要特点为:

  • 降低资源消耗: 通过重复利用已创建的线程降低线程创建和销毁造成的销耗。
  • 提高响应速度: 当任务到达时,任务可以不需要等待线程创建就能立即执行。
  • 提高线程的可管理性: 线程是稀缺资源,如果无限制的创建,不仅会销耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
  • Java 中的线程池是通过Executor框架实现的,该框架中用到了ExecutorExecutors,ExecutorService ,ThreadPoolExecu tor这几个类。


线程池的七大参数
在这里插入图片描述

线程池的四种拒绝策略
在这里插入图片描述

自定义线程池
在这里插入图片描述
自定义线程池案例

public class ThreadPool01 {
	public static void main(String[] args) {
		ExecutorService threadPoor = new ThreadPoolExecutor(2, 5, 2L, TimeUnit.SECONDS,
				new ArrayBlockingQueue<>(3),Executors.defaultThreadFactory(),
				new ThreadPoolExecutor.AbortPolicy());
		
		try {
			for(int i=0;i<10;i++) {
				threadPoor.execute(()->{
					System.out.println("你好");
				});
			}
		}catch(Exception e) {
			System.out.println("任务太多,无法处理");
		}finally {
			System.out.println("执行完毕");
		}
		
	}

}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值