最通俗易懂的JUC多线程并发编程

JUC多线程并发编程(一)

什么是JUC?

JUC是java.util.concurrent的简写,官方只是用它作为一种命名,多线程编程主要是用这个包下里的类,称为JUC多线程编程。

传统的Sychronized锁

以下代码,在多线程并发的情况下,会存在问题,输出的结果会非常的乱。

public static void main(String[] args) {
        Num num = new Num();
        new Thread(()->{
                for (int i =1;i<=50;i++){
                    num.decrement();
                }
            },"a").start();
        new Thread(()->{
                for (int i =1;i<=50;i++){
                    num.decrement();
                }
            },"b").start();
        new Thread(()->{
            for (int i =1;i<=50;i++){
                num.decrement();
            }
        },"c").start();
    }
    public static class Num{
        private int number=40;

        public  void decrement(){
            if (number>0) {
                number--;
                System.out.println(Thread.currentThread().getName()+"   "+number);
            }

        }

传统的解决方法呢?就是在我们方法上加一个Synchronized关键字

        public Sychronized void decrement(){
            if (number>0) {
                number--;
                System.out.println(Thread.currentThread().getName()+"   "+number);
            }

Synchronized的作用:当多个线程同时对一个对象的一个方法进行操作,只有一个线程能够抢到锁。因为一个对象只有一把锁,一个线程获取了该对象的锁之后,其他线程无法获取该对象的锁,就不能访问该对象的其他synchronized实例方法,但是可以访问非synchronized修饰的方法。

lock锁

Lock所是一个接口,其所有的实现类为

ReentrantLock(可重入锁)
ReentrantReadWriteLock.ReadLock(可重入读写锁的读锁)
ReentrantReadWriteLock.WriteLock(可重入读写锁的写锁)
用lock锁解决上述问题:

		ReentrantLock lock=new ReentrantLock()
        public void decrement(){            
            try{
            if (number>0) {
            	lock.lock();
            	number--;
           	 	System.out.println(Thread.currentThread().getName()+"   "+number);
            }catch(exception e){
            	e.printStack();
            }finally{
            	lock.unlock();
            }     
            }

lock锁和Synchronizd锁的作用是一样,没有什么区别,只是换了一种形式,但是这两者的不同后面再说

可重入锁:如果锁具备可重入性,则称为可(可以)重(再次)入(进入同步域,即同步代码块/方法)锁(同步锁)。可重入就是指某个线程已经获得某个锁,可以再次获取相同的锁而不会出现死锁。也就是说当一个类里面有同步方法A,同步方法A又实现了同步方法B,这时一个线程调用同步方法A,获取锁,当执行了到方法B时,会再次获取锁,获取的时同一把锁,这样就叫可重入锁,能有效的防止死锁。

公平锁:指的是非常公平,线程会依次获取锁,顺序执行。

非公平锁:指的是会出现插队现象,某些线程会直接先执行。

Synchronized和Lock的区别

Synchronizd:
1.是Java类中的一个内置关键字
2.无法获取锁的状态,也就是不能这个锁是否被占有和空闲
3.自动释放锁
4.线程一在获得锁的情况下阻塞了,第二个线程就只能傻傻的等着
5.是不可中断的、非公平的、可重入锁
6.适合锁少量的同步代码
7.有代码块锁和方法锁

Lock:

1.是java的一个类
2.可判断是否获取了锁
3.需手动释放锁,如果不释放会造成死锁
4.线程一在获得锁的情况下阻塞了,可以使用tryLock()尝试获取锁
5.非公平的、可判断的、可重入锁
6.适合锁大量的同步代码
7.只有代码块锁
8.使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(拥有更多的子类)

传统的生产者与消费者问题

public static void main(String[] args) {
        Num num = new Num();
        new Thread(()->{
            for (int i=1;i<=10;i++){
                try {
                    num.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"A").start();

        new Thread(()->{
            for (int i=1;i<=10;i++){
                try {
                    num.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"B").start();
        class Num{
    private  int count=0;

    public synchronized void increment() throws InterruptedException {
        if (count!=0){
            this.wait();
        }
        count++;
        this.notifyAll();
        System.out.println(Thread.currentThread().getName()+"  "+ count);
    }
    public synchronized void decrement() throws InterruptedException {
        if (count==0){
            this.wait();
        }
        count--;
        this.notifyAll();
        System.out.println(Thread.currentThread().getName()+"  "+ count);
    }
}

生产者与消费者问题属于是线程之间的通信。
线程之间的通信主要有:

wait():指的是会让当前线程进入休眠状态,但不是阻塞状态
notify():会唤醒当前对象上一个休眠状态的线程
notifyAll():会唤醒当前对象的所有线程,会按照线程的优先级调度执行

角色:
生产者:负责生产产品的线程
消费者:负责消费的产品的线程

解决线程之间通信的方法有:
1.管程法:
指的是,会创建一个容器类,里面存入一个缓冲区数组,主要用来存储产品的数量,然后再通过判断产品数量的大小来确保线程之间的通信
2.信号灯法:
指的是,在产品类里去设置一个信号flag,当信号变化的时候,就可以通知和唤醒线程,确保线程之间的通信。

在生产者与消费者通信的过程主要分为:判断等待,业务,唤醒,而在多线程的情况下(两个线程以上),如果你用的是if来判断等待,会存一个虚假唤醒的情况。什么是虚假唤醒呢?虚假唤醒指的是当有多个线程阻塞后,被其中一个线程唤醒,此时只有一个线程是在作用有效范围内的,其他线程都是多余的,这时会造成线程安全问题。
那为什么用if判断就会出现虚假唤醒呢?拿两个加法线程A、B来说,比如A先执行,执行时调用了wait方法,那它会等待,此时会释放锁,那么线程B获得锁并且也会执行wait方法,两个加线程一起等待被唤醒。此时减线程中的某一个线程执行完毕并且唤醒了这俩加线程,那么这俩加线程不会一起执行,其中A获取了锁并且加1,执行完毕之后B再执行。如果是if的话,那么A修改完num后,B不会再去判断num的值,直接会给num+1。如果是while的话,A执行完之后,B还会去判断num的值,因此就不会执行。所以说用while判断能有效的解决虚假唤醒的问题。

	  public synchronized void increment() throws InterruptedException {
        while (count!=0){
            this.wait();
        }
        count++;
        this.notifyAll();
        System.out.println(Thread.currentThread().getName()+"  "+ count);
    }
    public synchronized void decrement() throws InterruptedException {
        while (count==0){
            this.wait();
        }
        count--;
        this.notifyAll();
        System.out.println(Thread.currentThread().getName()+"  "+ count);
    }

lock版的生产者与消费者问题

	 ReentrantLock lock = new ReentrantLock();
    Condition condition = lock.newCondition();
    public  void increment() throws InterruptedException {

        try {
            lock.lock();
            while (count!=0){
                condition.await();
            }
            count++;
            condition.signal();
            System.out.println(Thread.currentThread().getName()+"  "+ count);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

    }
    public  void decrement() throws InterruptedException {
        try {
            lock.lock();
            while (count==0){
                condition.await();
            }
            count--;
            condition.signal();
            System.out.println(Thread.currentThread().getName()+"  "+ count);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

lock版的线程之间的通信:
通过lock.newCondition产生一个condition对象
1.用condition.await()方法替代传统版的wait()方法,进入休眠状态
2.用condition.signal()方法替代传统版的notifyAll()方法,唤醒线程

两者之间作用都是相同的,差别不大,但是lock版的总会有好处,如果有这样的一个需要,需要按自定义来唤醒线程,此时lock的condition就能实现精准唤醒,而传统版的是无法实现的。可以通过lock产生多个condition对象,每一个condition对象对应一个线程,此时我们可以在当前线程在唤醒的时候,可以指定一个condition来唤醒,这样就实现了按自定义顺序来唤醒线程.

8锁现象

1.当有两个实例方法加上synchronized关键字,同一个对象,有两个线程a,b(a的代码在b前)去执行两个实例方法,此时无论无何都会先执行a线程调用的实例方法,因为a线程首先拿到锁,谁先拿到锁就会先执行
2.让线程a睡3秒,结果会依然是先执行a线程调用的实例方法
3.加上一个普通方法,线程b实现普通方法,此时的结果会是,由于a睡3秒,先执行就是普通方法,也就是说普通方法不受锁的影响
4.不同对象分别实现两个实例方法,此时的结果将是睡3秒的线程之后执行,线程b先执行
5.当实例方法变为静态方法后,锁的就是class类模板,结果依然是线程a先执行
6.实例方法变为静态方法并用不同对象,结果依然是线程a先执行
7.当一个实例方法一个静态方法,让同一个对象去调用,结果是线程b先执行,因为这两者的锁不同,互相不影响
8.当一个实例方法一个静态方法,让不同对象去调用,结果是线程b先执行,一个锁模板,一个锁对象,自然就不影响

不安全的集合类

list
为什么list是线程不安全的?在单线程是安全的,当多线程同时对list使用add()方法,此时就会出现并发修改异常(ConcurrentModificationException)
那怎么解决这个问题呢?
1.用vector来替代list,它是内部加锁,是一个线程安全类,底层的add方法使用了synchronized关键字。

 public synchronized boolean add(E e) {
        modCount++;
        add(e, elementData, elementCount);
        return true;
    }

2.用Collections.synchronizedList(list)包装list,也是内部加锁,能让list变得多线程安全
3.使用juc里的CopyOnWriteArrayList替代list(推荐使用)写入时复制,读写分离的思想。源码如下,使用的是lock锁

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();
        }
    }

这样写时复制实现了读写分离,我们不需要在读的时候加锁(之前读需要加锁是因为读写不能同时进行,但一旦给读加了锁,那么读也不能同时进行,就降低了并发效率)

但是,我们每“写入”一个元素就要复制扩容一次数组,是非常耗时耗资源的,所以当我们需要写入较多数据的时候,CopyOnArrayList就不那么合适了。

set
set集合在多线程的情况下,使用add方法也是不安全,也会报并发修改异常异常
将不安全的集合变成安全集合的方法:

1.Set set = Collections.synchronizedSet(new HashSet<>());
2.Set set = new CopyOnWriteArrayListSet<>();

map
也是不安全的集合类

变成安全的方法:

1.Map<String,String> map = Collections.synchronizedMap(new HashMap<>());
无论读取还是写入,都会进行加锁,当并发级别特别高,线程之间在任何操作上都会进行等待,效率低。

2.Map<String,String> map = new ConcurrentHashMap<>();
采用分段锁技术,其中Segment继承于ReentrantLock。不会像HashTable(线程安全) 那样不管是put还是get操作都需要做同步处理,理论上ConcurrentHashMap支持CurrentLevel(Segment数组数量)的线程并发。每当一个线程占用锁访问一个Segment时,不会影响其他的Segment.

callable

callable和runnable类似,都是线程执行的接口,但callable有:
1.使用call方法,有返回值,使用futureTask.get()方法获得,但会阻塞,一般放到程序的最后
2.可以抛出异常
3.Callable与Future结合,实现利用Future来跟踪异步计算的结果

**new Thread(new Runnable())**这是一种形式,**new Thread(new FutureTask())**这种形式,**new FutureTask()**里面包含的参数是一个实现callable接口的实现类,线程的启动,需要借助我们FutrueTask来启动线程.
异步计算:
用不同的线程去做不同的事,最后再进行数据汇总并返回结果。是分布式计算的一种实现方式,开发人员不必花费太多的硬件成本,即可通过软件方式以更低的成本实现大规模运算需要的处理能力。

在一些业务比较复杂,某些方法计算比较耗时的时候单线程就无法快速返回结果,用户响应时间变长,这个时候就要使用异步计算来加快数据的处理。

over

本文借助于狂神说Java:bilibili.com/video/BV1B7411L7tE?p=14加上自己的理解做了一份笔记.

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值