狂神说springboot笔记_狂神说JUC学习笔记+补充(一)

1、什么是JUC?

指的是java.util包下的三个工具类:

  1. java.util.concurrent
  2. java.util.concurrent.atomic
  3. java.util.concurrent.locks

实现多线程的三种方式:

  1. 继承Thread类
  2. 实现Runnable接口
  3. 实现Callable接口

Runnable没有返回值、效率相比于Callable相对较低!

业务:普通的线程代码 Thread

本篇笔记篇幅较长,若想一气呵成地看完就看本篇就行。如果想一点点地看,把其中的部分抽取出来做成了4个子笔记,点击链接看对应子笔记就行。两种方式的内容相同。

1.什么是JUC?(内容较少,就未抽取)

2.java线程和进程演绎法5919:java线程和进程学习子笔记

3.Lock锁中的synchonized部分演绎法5919:synchronized学习笔记

4.8锁现象演绎法5919:8锁现象学习子笔记

5.不安全的集合类演绎法5919:不安全的集合类学习子笔记

2、线程和进程

进程:是一个程序,一个进程包含多个线程,且至少包含一个。

Java默认有两个线程:main 和 GC。Java是不能开启线程的,底层是调用start0()是一个native方法,由底层的C++方法编写。java无法直接操作硬件。

并发:CPU单核,多个线程共用一个资源,快速交替方式达到并行的假象。本质->充分利用cpu资源。

并发:CPU多核,多个线程同时执行

System.out.println(Runtime.getRuntime().availabelProcessors());//获取cpu核数

线程有几个状态:

public enum State{
      NEW,//新生
      RUNNABLE,//运行
      BLOCKED,//阻塞
      BLOCKED,//等待
      TIMED_WAITING,//超时等待
      TERMINATED;//终止
}

wait和sleep的区别

wait:来自Object类,释放锁,必须在同步代码块中使用,不需要捕获异常

让当前线程进入等待状态,当别的其他线程调用notify()或者notifyAll()方法时,当前线程进入就绪状态。wait方法必须在同步上下文中调用。也就是说,如果想要调用wait方法,前提是必须获取对象上的锁资源。当wait方法调用时,当前线程会释放已获取的对象锁资源,并进入等待队列,其他线程就可以尝试获取对象上的锁资源。

sleep:来自Thread类是一个静态方法,不会释放锁!(睡着了),可以在任何地方使用,必须要捕获异常

让当前线程休眠指定时间。休眠时间的准确性依赖于系统时钟和CPU调度机制。不释放以获取的锁资源,如果sleep方法在同步上下文中调用,那么其他线程是无法进入当前同步快或者同步方法中的。可通过interrupt()方法来唤醒休眠线程。

3.Lock锁

3.1传统的synchronized锁:队列锁

1.sychronized的作用:

在并发编程中会存在线程安全问题,主要原因是存在共享数据和多线程共同操作共享数据。关键字sychronized可以保证在同一时刻只有一个线程可以执行某个方法或某个代码块(临界区),同时synchronized可以保证一个线程的变化可见。

只有共享资源读写访问才需要同步化,如果不是共享资源就没有必要同步。

2.三种应用方式

  • 修饰实例方法,对当前实例进行加锁,进入同步代码前需获得当前实例的锁
  • 修饰静态方法,对类对象加锁,要先获得当前类对象的锁
  • 修饰代码块,指定加锁对象,对给定对象加锁,要先获得给定对象的锁

2.1同步方法(静态方法、实例方法)

用sychronized修饰的方法就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。

synchronized public void doWorlk(){
     ...
}

对于非静态类方法,同步锁就是this(实例对象)

对于静态类方法,同步锁是当前方法所在类的字节码对象(类对象)

2.2同步代码块

syschronized(同步锁){
  ....
}

同步锁:在任何时候最多只能有一个线程有同步锁,其他线程只能在代码块外等着。

为保证每个线程都能正常执行原子操作,Java引入了线程同步机制。java程序运行使用任何对象作为同步监听对象,但一般的,我们把当前并发访问的共同资源作为同步监听对象。

3.举个栗子:

package demo01;

public class test01 {
    public static void main(String[] args) throws InterruptedException {
        // 并发:多线程操作同一个资源类,
        Ticket ticket = new Ticket();
        // @FunctionalInterface 函数式接口
        new Thread(() -> {
            for (int i = 1; i < 20; i++) {
                //把资源类丢入线程
                ticket.sale();
            }
        }, "A").start();
        new Thread(() -> {
            for (int i = 1; i < 20; i++) {
                ticket.sale();
            }
        }, "B").start();
        new Thread(() -> {
            for (int i = 1; i < 20; i++) {
                ticket.sale();
            }
        }, "C").start();
    }
}

class Ticket {
    //共卖40张票
    private int number = 30;

    // 卖票的方式 
    public synchronized void sale() {
        if (number > 0) {
            System.out.println(Thread.currentThread().getName() + "卖出了第" + (number--) + "票,剩余:" + number);
        }
    }
}

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

3.2Lock接口

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

  • ReentrantLock(可重入锁)
  • ReentrantReadWriteLock.ReadLock(可重入读写锁的读锁)
  • ReentrantReadWriteLock.WriteLock(可重入读写锁的写锁)

Lock实现提供了比使用synchronized方法和语句可获得的更广泛的锁定操作。Lock接口的实现允许锁在不同的作用范围内获取和释放,并允许以任何顺序获取和释放多个所。随着灵活的增加,也带来了更多的责任。不使用块结构锁就失去了使用synchronized方法和语句时会出现的锁自动释放功能。在大多数情况下,应使用以下语句:

Lock l = ...; 
     l.lock();//加锁
     try {
         // access the resource protected by this lock
     } finally {
         l.unlock();//解锁
     }

加锁和解锁出现在不同作用范围中是,需谨慎确保锁定是所执行的所有代码用try-finally或try-catch保护,以确保在必要时释放锁。

公平锁:十分公平,遵循先来后到

非公平锁:十分不公平,出现插队现象

可重入锁:如果锁具备可重入性,则称为可(可以)重(再次)入(进入同步域,即同步代码块/方法)锁(同步锁)。可重入就是指某个线程已经获得某个锁,可以再次获取相同的锁而不会出现死锁。

3.3synchronized和Lock锁的区别

synchronized:

  • 是java内置的关键字
  • 无法获取锁的状态
  • 会自动释放锁
  • 线程一在获得锁的情况下阻塞了,第二个线程就只能傻傻的等着
  • 是不可中断的、非公平的、可重入锁
  • 适合锁少量的同步代码
  • 有代码块锁和方法锁

Lock:

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

4.8锁现象

掌握8中锁的现象永远知道锁的是谁!

package lock8;

import java.util.concurrent.TimeUnit;

public class Test01 {
    public static void main(String[] args) {
        Phone phone = new Phone();

        new Thread(() -> {
            phone.call();
        }, "A").start();
        new Thread(() -> {
            phone.send();
        }, "B").start();
    }
}

class Phone {
    public synchronized void send() {
        System.out.println("发短信");
    }

    public synchronized void call() {
        //现象二时添加,让线程先睡4秒种
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("打电话");
    }
}

现象一:两个方法都使用synchronized关键字,先执行打电话

现象二:让线程先睡4秒,结果依然是先打电话

原因:synchromized锁的是方法的调用者,并且开启的两个线程方法使用的是同一把锁,那么就会出现谁先拿到谁先执行的现象。及时我们让call方法sleep了4秒,依然是call方法先执行。

package lock8;

import java.util.concurrent.TimeUnit;

public class Test02 {
    public static void main(String[] args) {
        Phone2 phone1 = new Phone2();
        Phone2 phone2 = new Phone2();

        new Thread(() -> {
            phone1.call();
        }, "A").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -> {
//            phone1.hello();
            phone2.send();
        }, "B").start();
    }
}

class Phone2 {
    public synchronized void send() {
        System.out.println("发短信");
    }

    public synchronized void call() {
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("打电话");
    }
    public void hello() {
        System.out.println("hello");
    }
}

现象三:在之前代码的基础上,新添加一个普通的方法,此时先执行hello方法

现象四:新实例化一个phone对象,使用不同的对象去调用方法,此时先执行发短信

原因:普通方法没有锁就不是同步方法不受锁的影响,又由于时间的延迟,所以先打印hello.现象四中,使用了不同的对象,以至于是锁的对象不是同一个,所以先发短信。

package lock8;

import java.util.concurrent.TimeUnit;

public class Test03 {
    public static void main(String[] args) {
        Phone3 phone1 = new Phone3();
        Phone3 phone2 = new Phone3();

        new Thread(() -> {
            phone1.call();
        }, "A").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -> {
            //phone1.send();
            phone2.send();
        }, "B").start();
    }
}

class Phone3 {
    public static synchronized void send() {
        System.out.println("发短信");
    }

    public static synchronized void call() {
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("打电话");
    }
}

现象五:在之前代码的基础上,方法前添加static关键字,先执行打电话

现象六:再添加一个对象,使用不同的对象进行方法的打印,依然是先执行打电话

原因:当我们添加了static关键字以后,此时我们先打印打电话,不仅是因为我们拿到的是同一个对象的锁,还因为我们的锁是直接锁的该类的Class模板。当我们再新添加一个对象时,由于我们使用了static,直接锁在模板上,所以依然是先执行打电话

package lock8;

import java.util.concurrent.TimeUnit;

public class Test04 {
    public static void main(String[] args) {
        Phone4 phone1 = new Phone4();
        Phone4 phone2 = new Phone4();

        new Thread(() -> {
            phone1.call();
        }, "A").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -> {
//            phone1.send();
            phone2.send();
        }, "B").start();
    }
}

class Phone4 {
    public synchronized void send() {
        System.out.println("发短信");
    }

    public static synchronized void call() {
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("打电话");
    }
}

现象七:当去掉一个锁方法的static关键字以后,先打印发短信

现象八:当我们新建一个对象后,是有不同的对象去调用,还是先打印发短信

原因:现象7中,只添加了一个static关键字,模板只有一部分被锁,则先打印发短信。现象8中,又新建了一个对象,因为锁的东西不一样(打电话锁模板,发短信锁对象),所以先执行发短信。

只有当锁的对象或模板时同一个的时候,才能借助调用的顺序来执行。

5.不安全的集合类

list

不安全类是什么?

不安全类是指在多线程并发的时候不能保证数据正确性的类,通常是由于这些类并没有加锁造成的。

为什么不设计成加锁的?

其实,在list之前有个集合类vector,它是内部加锁,它是一个线程安全类。不优先使用它的原因是加锁可以保证数据的正确性,但却降低了并发效率。list单线程安全,多线程不安全。并发条件下会产生ConcurrentModificationException异常(并发修改异常)

如何做到保证数据的正确性呢?

  1. vector替代list(并发效率低)
  2. 用Collections.synchronizedList(list)包装list(有synchronized修饰的方法效率低)
  3. 使用juc里的CopyOnWriteArrayList替代list(推荐使用)写入时复制,读写分离的思想。

CopyOnWriteArrayList 写时复制,读写分离

既能保证数据的正确性,又不会使并发效率变低。它的add源码:

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

也是不安全的集合类

将不安全的集合变成安全集合的方法:

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

HashSet底层是什么?

//默认的空参初始化方法
public HashSet() {
	map = new HashMap<>();
}

//使用HashSet的add方法,依然是调用HashMap的底层put方法
public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}

也就是说HashSet的底层就是HashMap

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.

参考博客:

JUC(一)_默辨的博客-CSDN博客

Java中的sleep与wait区别_helloworld的专栏-CSDN博客

可重入锁详解(什么是可重入)_w8y56f的专栏-CSDN博客

不安全的集合类list以及解决方法_qq_17690301的博客-CSDN博客

【Java并发编程之深入理解】Synchronized的使用_青苔小榭-CSDN博客

synchronized理解_蓝胖子bot的博客-CSDN博客

[java] synchronized关键字用法及实现原理详解

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值