JUC总结

1,IDEA准备环境

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

1,什么是JUC

JUC: java.util .concurrent工具包的简称。这是一个处理线程的工具包,JDK 1.5开始出现的。

  • java.util.concurrent

  • java.util.concurrent.atomic

  • java.util.concurrent.locks

2,线程和进程

进程:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。

线程:通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义,线程可以利用进程所有拥有的资源。在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位,由于线程比进程小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效的提高系统多个程序间并发执行的程度。

线程和进程总结:

进程:就是操作系统中运行的一个程序,QQ.exe, music.exe, word.exe ,这就是多个进程。

线程:每个进程中都存在一个或者多个线程,比如用word写文章时,就会有一个线程默默帮你定时自动保存。

并发 / 并行

并发:一个CPU以极快的速度交替执行多个任务。
(串行,逻辑上同时发生,就是表面看上去是同时发生的。)

并行:多个CPU同时执行多个任务。
(并行,物理上同时执行,就是实际上真的同时执行。)

详细解释

并发和并行是两个非常容易混淆的概念。它们都可以表示两个或多个任务一起执行,但是偏重点有点不同。并发偏重于多个任务交替执行,而多个任务之间有可能还是串行的。并发逻辑上的同时发生,而并行物理上的同时发生。然而并行的偏重点在于”同时执行”。

并发的动机:在计算能力恒定的情况下处理更多的任务, 就像我们的大脑, 计算能力相对恒定, 要在一天中处理更多的问题, 我们就必须具备多任务的能力. 现实工作中有很多事情可能会中断你的当前任务, 处理这种多任务的能力就是你的并发能力。

并行的动机:用更多的CPU核心更快的完成任务. 就像一个团队, 一个脑袋不够用了, 一个团队来一起处理一个任务。

线程种操作系统的五大状态

  1. 创建状态
  2. 就绪状态
  3. 运行状态
  4. 阻塞状态
  5. 死亡状态

在这里插入图片描述

线程的六大状态

public enum State {
    //1,线程刚创建
    NEW,
    //2,在JVM中正在运行的线程
    RUNNABLE,
    //3,线程阻塞
    BLOCKED,
    //4,等待状态
    WAITING,
    //5,调用sleep() join() wait()方法可能导致线程处于等待状态
    TIMED_WAITING,
    //6,线程执行完毕,已经退出
    TERMINATED;
}

在这里插入图片描述

wait / sleep 的区别

1、来自不同的类

​ 这两个方法来自不同的类分别是,sleep来自Thread类,和wait来自Object类。

2、有没有释放锁(释放资源)

​ 最主要是sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。

​ sleep是线程被调用时,占着cpu去睡觉,其他线程不能占用cpu,os认为该线程正在工作,不会让出系统资源,wait是进入等待池等待,让出系统资源,其他线程可以占用cpu。

​ sleep(100L)是占用cpu,线程休眠100毫秒,其他进程不能再占用cpu资源,wait(100L)是进入等待池中等待,交出cpu等系统资源供其他进程使用,在这100毫秒中,该线程可以被其他线程notify,但不同的是其他在等待池中的线程不被notify不会出来,但这个线程在等待100毫秒后会自动进入就绪队列等待系统分配资源,换句话说,sleep(100)在100毫秒后肯定会运行,但wait在100毫秒后还有等待os调用分配资源,所以wait100的停止运行时间是不确定的,但至少是100毫秒。

​ 就是说sleep有时间限制的就像闹钟一样到时候就叫了,而wait是无限期的除非用户主动notify

3、使用范围不同

​ wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用

synchronized(x){
	//或者wait()
	x.notify()
}

3,Synchronized锁

Java 中的 synchronized 关键字可以在多线程环境下用来作为线程安全的同步锁

Java中的对象锁类锁:java的对象锁和类锁在锁的概念上基本上和内置锁是一致的,但是,两个锁实际是有很大的区别的,对象锁是用于对象实例方法,或者一个对象实例上的,类锁是用于类的静态方法或者一个类的class对象上的。

我们知道,类的对象实例可以有很多个,但是每个类只有一个class对象,所以不同对象实例的对象锁是互不干扰的,但是每个类只有一个类锁。但是有一点必须注意的是,其实类锁只是一个概念上的东西,并不是真实存在的,它只是用来帮助我们理解锁定实例方法和静态方法的区别的。

synchronized 关键字主要有以下几种用法:

  • 非静态方法的同步;
  • 静态方法的同步;
  • 代码块。

总结:修饰加了static方法的synchronized锁就是类锁,否则是方法锁。

//1,对象锁
//方法对象锁
class Ticket{ // 资源类
	private int number = 30;
    public synchronized void saleTicket(){
        if (number>0){
            System.out.println(Thread.currentThread().getName() + "卖出第 " + (number--) + "票,还剩下:"+number);
        }
    }
}

//代码块对象锁
public void test(){
    synchronized (this) {
        // TODO
    }
}

//2,类锁
//方法类锁
public static synchronized void test(){
    // TODO
}

//代码块类锁
public static void test(){
    synchronized (TestSynchronized.class) {
        // TODO
    }
}

4,Lock锁(重点)

Lock是一个接口,目录:java.util.concurrent.locks。

实现类:
ReentrantLock , ReentrantReadWriteLock.ReadLock , ReentrantReadWriteLock.WriteLock

public class SaleTicketTest2 {
    public static void main(String[] args) throws Exception { // main一切程序的入口
        Ticket ticket = new Ticket();
        new Thread(()->{for (int i = 1; i <=40; i++) ticket.saleTicket();}, "A").start();
        new Thread(()->{for (int i = 1; i <=40; i++) ticket.saleTicket();}, "B").start();
        new Thread(()->{for (int i = 1; i <=40; i++) ticket.saleTicket();}, "C").start();
    }
}

class Ticket{ // 资源类
    private Lock lock = new ReentrantLock();
    private int number = 30;
    
	public void saleTicket(){
        //上锁
		lock.lock();
		try {
			if (number>0){
				System.out.println(
                    Thread.currentThread().getName() + "卖出第" + (number--) + "票,还剩下:"+number);
             }
  		} catch (Exception e) {
			e.printStackTrace();
		} finally {
            //解锁
			lock.unlock();
         }
	}
}

5,synchronized 和 lock 区别

  1. 首先synchronized是java内置关键字,在jvm层面,Lock是个java类

  2. synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;

  3. synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁);
    Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;

  4. 两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。
    如果线程1阻塞,使用synchronized线程2会一直等待下去
    Lock锁不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;

  5. synchronized的锁可重入、不可中断、非公平;
    Lock锁可重入、可判断、默认非公平(可在构造对象时传入true构造公平锁)。

    //默认使用非公平锁
    Lock lock1 = new ReentrantLock();
    
    //构造时指定使用公平锁
    Lock lock2 = new ReentrantLock(true);
    
  6. Lock锁适合大量同步代码的同步问题,synchronized锁适合代码少量的同步问题。

6,生产者和消费者

synchronized版本

public class test1 {

    public static void main(String[] args) {
        PSClass psClass = new PSClass();
        Lock lock = new ReentrantLock(true);

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

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

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

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

    }
}

class PSClass{

    private int number = 0;

    /**
     * 生产方法:当number为0时生产
     */
    public synchronized void increment() throws InterruptedException {
        while (number!=0){
            wait();
        }
        number++;
        System.out.println(Thread.currentThread().getName()+"-->"+number);
        notifyAll();
    }

    /**
     * 消费方法:当number!=0时消费
     * @throws InterruptedException
     */
    public synchronized void decrement() throws InterruptedException {
        while (number==0){
            wait();
        }
        number--;
        System.out.println(Thread.currentThread().getName()+"-->"+number);
        notifyAll();
    }

}
虚假唤醒

注意:使用while防止虚假唤醒,上面4个线程,两个加,两个减。

因为wait会释放锁,等他在重新获取锁的时候,使用if会从释放的这个地方接着向下执行,有线程安全问题。
使用while会一直循环判断,不会有线程安全问题。

lock版本

public class Test2 {
    //主方法种基本没变。
	public static void main(String[] args) {

        PSClass2 psClass = new PSClass2();

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

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

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

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

    }
}

class PSClass2{

    private int number = 0;
	//使用Lock类实现锁
    private Lock lock = new ReentrantLock();
    //使用Condition实现等待和唤醒功能
	//等待:wait() ; 唤醒:signal(),signalAll() ;
    private Condition condition = lock.newCondition();

    /**
     * 生产方法:当number为0时生产
     */
    public void increment() throws InterruptedException {
        lock.lock();
        try {
            while (number!=0){
                condition.await();
            }
            number++;
            System.out.println(Thread.currentThread().getName()+"-->"+number);
            condition.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

    }

    /**
     * 消费方法:当number!=0时消费
     * @throws InterruptedException
     */
    public void decrement() throws InterruptedException {
        lock.lock();
        try {
            while (number==0){
                condition.await();
            }
            number--;
            System.out.println(Thread.currentThread().getName()+"-->"+number);
            condition.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

    }

}
lock锁实现流程
//1,上锁
lock.lock();
try {
    while (number==0){
        //2,等待,相当于wait()
        condition.await();
    }
    number--;
    System.out.println(Thread.currentThread().getName()+"-->"+number);
    //唤醒,相当于notify(),notifyAll().
    condition.signalAll();
} catch (Exception e) {
    e.printStackTrace();
} finally {
	//解锁
    lock.unlock();
}

synchronized对比lock

synchronized:
  1. 上锁解锁:synchronized
  2. 等待:wait()
  3. 唤醒:notify()notifyAll()
lock:

Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();

  1. 上锁解锁:lock.lock(),lock.unlock()
  2. 等待:condition.await()
  3. 唤醒:condition.signal(),condition.signalAll()

精准唤醒

使用指定condition和标志位
public class Test3 {
    public static void main(String[] args) {
        PSClass3 psClass3 = new PSClass3();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                psClass3.incrementA();
            }
        },"A").start();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                psClass3.incrementB();
            }
        },"B").start();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                psClass3.incrementC();
            }
        },"C").start();

    }
}

class PSClass3{

    private Lock lock = new ReentrantLock();
    private Condition condition1 = lock.newCondition();
    private Condition condition2 = lock.newCondition();
    private Condition condition3 = lock.newCondition();
    private int flag = 1;

    public void incrementA(){
        lock.lock();
        try {
            //判断,执行,唤醒
            while (flag != 1){
                condition1.await();
            }
            System.out.println(Thread.currentThread().getName()+"-->flag="+flag);
            flag = 2;
            condition2.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void incrementB(){
        lock.lock();
        try {
            //判断,执行,唤醒
            while (flag != 2){
                condition2.await();
            }
            System.out.println(Thread.currentThread().getName()+"-->flag="+flag);
            flag = 3;
            condition3.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void incrementC(){
        lock.lock();
        try {
            //判断,执行,唤醒
            while (flag != 3){
                condition3.await();
            }
            System.out.println(Thread.currentThread().getName()+"-->flag="+flag);
            flag = 1;
            condition1.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

7,8锁现象

前提1:对象锁

class MyLock{
    public synchronized void sendMsg() throws InterruptedException {
        TimeUnit.SECONDS.sleep(4);
        System.out.println("发短信");
    }

    public synchronized void callMe(){
        System.out.println("打电话");
    }

    public void hello(){
        System.out.println("hello");
    }
}
1、标准访问,邮件先获取锁,请问先打印邮件还是短信?
// 邮件,短信
2、邮件方法暂停4秒钟,请问先打印邮件还是短信?
// 邮件,短信
3、新增一个普通方法hello()没有同步,先调用邮件暂停4秒然后hello(),请问先打印邮件还是hello?
// hello(),邮件。因为即使邮件先调用获取了锁,但是hello()没有锁。
4、两部手机、请问先打印邮件还是短信?
// 短信,邮件。两把锁,邮件要暂停4秒。

前提2:类锁

class MyLock2{
    public static synchronized void sendMsg() throws InterruptedException {
        TimeUnit.SECONDS.sleep(4);
        System.out.println("发短信");
    }

    public static synchronized void callMe(){
        System.out.println("打电话");
    }

    public void hello(){
        System.out.println("hello");
    }
}
5、两个静态同步方法,同一部手机,请问先打印邮件还是短信?
// 邮件,短信。同一把锁
6、两个静态同步方法,2部手机,请问先打印邮件还是短信?
// 邮件,短信。虽然使用两部手机(对象),但这个是类锁,所以使用一把锁。
7、一个普通同步方法,一个静态同步方法,同一部手机,请问先打印邮件还是短信?
// 短信,邮件。邮件使用类锁暂停4秒,短信使用对象锁,总共两把锁,即使只有一部手机(对象)。
8、一个普通同步方法,一个静态同步方法,2部手机,请问先打印邮件还是短信?
// 短信,邮件。邮件使用类锁暂停4秒,短信使用对象锁,总共两把锁,两部手机(对象)。

总结

// 1,对象锁。锁的是实例对象,多个对象拥有多把锁。
class MyLock{
    public synchronized void callMe(){
        System.out.println("打电话");
    }
    public void callMe(){
        synchronized (this){
            System.out.println("打电话");
        }
    }
}

// 2,类锁。锁的是当前类Class,多个对象都只有一把锁。
class MyLock{
    public static synchronized void callMe(){
        System.out.println("打电话");
    }
    public static void callMe(){
        synchronized (MyLock.class){
            System.out.println("打电话");
        }
    }
}

8,java.util.concurrent.locks包

接口

Condition //Condition因素出Object监视器方法( wait , notify和notifyAll )成不同的对象,以得到具有多个等待集的每个对象,通过将它们与使用任意的组合的效果Lock个实现。  
Lock //Lock实现提供比使用 synchronized方法和语句可以获得的更广泛的锁定操作。  
ReadWriteLock //A ReadWriteLock维护一对关联的locks ,一个用于只读操作,一个用于写入。  

AbstractOwnableSynchronizer //可以由线程专有的同步器。  
AbstractQueuedLongSynchronizer //AbstractQueuedSynchronizer的一个版本,其中同步状态保持为long 。  
AbstractQueuedSynchronizer //提供一个框架,用于实现依赖先进先出(FIFO)等待队列的阻塞锁和相关同步器(信号量,事件等)。  
LockSupport //用于创建锁和其他同步类的基本线程阻塞原语。  
ReentrantLock //一个可重入互斥Lock具有相同的基本行为和语义的隐式监视器锁定使用访问synchronized方法和语句,但功能更强大。  
ReentrantReadWriteLock //ReadWriteLock的实现支持类似的语义到ReentrantLock 。  
ReentrantReadWriteLock.ReadLock //该锁由方法 ReentrantReadWriteLock.readLock()返回。  
ReentrantReadWriteLock.WriteLock //锁方法 ReentrantReadWriteLock.writeLock()返回。  
StampedLock //一种基于能力的锁,具有三种模式用于控制读/写访问。  

9,集合类不安全

ArrayList

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

        for (int i = 1; i <= 10; i++) {
            new Thread(()->{
                list.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(Thread.currentThread().getName()+"-->"+list);
            },String.valueOf(i)).start();
        }
        System.out.println("========================");
        System.out.println(list);
    }

}

//输出 java.util.ConcurrentModificationException 并发修改异常,因为ArrayList不是线程安全的。
//Exception in thread "3" Exception in thread "5" Exception in thread "1" Exception in thread "4" java.util.ConcurrentModificationException

ArrayList线程不安全解决方法
List<String> list = new Vector<>();
//1,使用Vector代替,底层使用synchronized保证线程安全,但是效率低。

List<String> list = new ArrayList<>();
List<String> safeList = Collections.synchronizedList(list);
//2,使用Collections工具类转换为线程安全List,底层也是使用synchronized。

List<String> list = new CopyOnWriteArrayList<>();
//3,使用CopyOnWriteArrayList代替,底层使用Lock保证线程安全,效率比synchronized高。

写入时复制(CopyOnWrite)思想

写入时复制(CopyOnWrite,简称COW)思想是计算机程序设计领域中的一种优化策略。其核心思想是,如果有多个调用者(Callers)同时要求相同的资源(如内存或者是磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者视图修改资源内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明的(transparently)。此做法主要的优点是如果调用者没有修改资源,就不会有副本(private copy)被创建,因此多个调用者只是读取操作时可以共享同一份资源。

读写分离,写时复制出一个新的数组,完成插入、修改或者移除操作后将新数组赋值给array

CopyOnWriteArrayList为什么并发安全且性能比Vector好

Vector是增删改查方法都加了synchronized,保证同步,但是每个方法执行的时候都要去获得锁,性能就会大大下降,而CopyOnWriteArrayList 只是在增删改上加锁,但是读不加锁,在读方面的性能就好于Vector,CopyOnWriteArrayList支持读多写少的并发情况。

Set

public class SetTest {
    public static void main(String[] args) {

        Set<String> set = new HashSet<>();
        for (int i = 1; i <= 10; i++) {
            new Thread(()->{
                set.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(set);
            },String.valueOf(i)).start();
        }
    }
}

//输出 java.util.ConcurrentModificationException 并发修改异常,因为HashSet不是线程安全的。
HashSet线程不安全解决方法
Set<String> set = new HashSet<>();
Set<String> safeSet = CollechashSettions.synchronizedSet(set);
//1,使用Collections工具类转换为线程安全的set,底层使用synchronized实现。

Set<String> set = new CopyOnWriteArraySet<>();
//2,使用线程安全的CopyOnWriteArraySet,底层使用lock实现。
HashSet底层使用HashMap实现

Map

public class MapTest {
    public static void main(String[] args) {
        HashMap<String, String> map = new HashMap<>();

        for (int i = 1; i <= 10; i++) {
            new Thread(()->{
                map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0,5));
                System.out.println(map);
            },String.valueOf(i)).start();
        }
    }
}

//输出 java.util.ConcurrentModificationException 并发修改异常,因为HashMap不是线程安全的。
HashMap线程不安全解决方法
Map<String, String> map = new HashMap<>();
Map<String, String> safeMap = Collections.synchronizedMap(map);
//1,使用Collections工具类转换为线程安全的Map,底层使用synchronized实现。

Map<String, String> map = new ConcurrentHashMap<>();
//2,使用线程安全的ConcurrentHashMap,底层使用lock实现。
hashMap底层是数组+链表+红黑树

需要网上查找对应版本信息!!!!!!!!!!!!!

https://blog.csdn.net/qq_41345773/article/details/92066554

10,java.util.concurrent包

接口

BlockingDeque<E> //A Deque另外支持在检索元素时等待deque变为非空的阻塞操作,并且在存储元素时等待空白在deque中可用。  
BlockingQueue<E> //A Queue还支持在检索元素时等待队列变为非空的操作,并在存储元素时等待队列中的空间变得可用。 
Callable<V> //返回结果并可能引发异常的任务。  
CompletableFuture.AsynchronousCompletionTask //标识接口,用于标识 async方法生成的 async任务。  
CompletionService<V> //一种将新异步任务的生产与已完成任务的结果消耗相分离的服务。  
CompletionStage<T> //可能的异步计算的阶段,当另一个完成时间完成时,执行一个动作或计算一个值。  
ConcurrentMap<K,V> //A Map提供线程安全和原子性保证。  
ConcurrentNavigableMap<K,V> //A ConcurrentMap支持NavigableMap操作,并且递归地为其可导航的子地图。  
Delayed //一个混合样式界面,用于标记在给定延迟后应该执行的对象。  
Executor //执行提交的对象Runnable任务。  
ExecutorService //一个Executor ,提供方法来管理终端和方法,可以产生Future为跟踪一个或多个异步任务执行。  
ForkJoinPool.ForkJoinWorkerThreadFactory //创建新的工厂ForkJoinWorkerThread s。  
ForkJoinPool.ManagedBlocker //用于扩展管理并行性的接口,用于运行在ForkJoinPool中的任务。  
Future<V> //A Future计算的结果。  
RejectedExecutionHandler //ThreadPoolExecutor无法执行的任务的处理程序。  
RunnableFuture<V> //A Future那是Runnable 。  
RunnableScheduledFuture<V> //A ScheduledFuture那是Runnable 。  
ScheduledExecutorService //一个ExecutorService ,可以调度命令在给定的延迟之后运行,或定期执行。  
ScheduledFuture<V> //可以取消延迟结果的行动。  
ThreadFactory //根据需要创建新线程的对象。  
TransferQueue<E> //A BlockingQueue ,其中生产者可等待消费者接收元素。  

AbstractExecutorService //提供ExecutorService执行方法的默认实现。  
ArrayBlockingQueue<E> //一个有限的blocking queue由数组支持。  
CompletableFuture<T> //A Future可以明确完成(设置其值和状态),并可用作CompletionStage ,支持在完成后触发的依赖功能和动作。  
ConcurrentHashMap<K,V> //支持检索的完全并发性和更新的高预期并发性的哈希表。  
ConcurrentHashMap.KeySetView<K,V> //ConcurrentHashMap视图为Set的密钥,其中可以通过映射到公共值来选择添加。  
ConcurrentLinkedDeque<E> //基于链接节点的无界并发deque 。  
ConcurrentLinkedQueue<E> //基于链接节点的无界线程安全queue 。  
ConcurrentSkipListMap<K,V> //一个可扩展的并发ConcurrentNavigableMap实现。  
ConcurrentSkipListSet<E> //基于ConcurrentSkipListMap的可扩展并发NavigableSet 实现 。  
CopyOnWriteArrayList<E> //的一个线程安全的变体ArrayList ,其中所有可变操作( add , set ,等等)通过对底层数组的最新副本实现。  
CopyOnWriteArraySet<E> //一个Set使用内部CopyOnWriteArrayList其所有操作。  
CountDownLatch //允许一个或多个线程等待直到在其他线程中执行的一组操作完成的同步辅助。  
CountedCompleter<T> //A ForkJoinTask ,当触发时执行完成操作,并且没有剩余的待处理操作。  
CyclicBarrier //允许一组线程全部等待彼此达到共同屏障点的同步辅助。  
DelayQueue<E extends Delayed> //Delayed元素的无界 Delayed元素,其中元素只能在其延迟到期时才被使用。  
Exchanger<V> //线程可以在成对内配对和交换元素的同步点。  
ExecutorCompletionService<V> //A CompletionService使用提供的Executor执行任务。  
Executors //工厂和工具方法Executor , ExecutorService , ScheduledExecutorService , ThreadFactory和Callable在此包中定义的类。  
ForkJoinPool //一个ExecutorService运行ForkJoinTask s。  
ForkJoinTask<V> //在ForkJoinPool内运行的任务的抽象基类。  
ForkJoinWorkerThread //一个由ForkJoinPool管理的线程,执行ForkJoinTask s。  
FutureTask<V> //可取消的异步计算。  
LinkedBlockingDeque<E> //基于链接节点的可选限定的blocking deque 。  
LinkedBlockingQueue<E> //基于链接节点的可选限定的blocking queue 。  
LinkedTransferQueue<E> //基于链接节点的无界TransferQueue 。  
Phaser //一个可重复使用的同步屏障,功能类似于CyclicBarrier和CountDownLatch,但支持更灵活的使用。  
PriorityBlockingQueue<E> //一个无界的blocking queue使用与PriorityQueue类相同的排序规则,并提供阻止检索操作。  
RecursiveAction //递归结果ForkJoinTask 。  
RecursiveTask<V> //递归结果ForkJoinTask 。  
ScheduledThreadPoolExecutor //A ThreadPoolExecutor还可以调度在给定延迟之后运行的命令,或定期执行。  
Semaphore //一个计数信号量。  
SynchronousQueue<E> //A blocking queue其中每个插入操作必须等待另一个线程相应的删除操作,反之亦然。  
ThreadLocalRandom //与当前线程隔离的随机数生成器。  
ThreadPoolExecutor //一个ExecutorService ,使用可能的几个合并的线程执行每个提交的任务,通常使用Executors工厂方法配置。  
ThreadPoolExecutor.AbortPolicy //被拒绝的任务的处理程序,抛出一个 RejectedExecutionException 。  
ThreadPoolExecutor.CallerRunsPolicy //一个被拒绝的任务的处理程序,直接在 execute方法的调用线程中运行被拒绝的任务,除非执行程序已被关闭,否则任务被丢弃。  
ThreadPoolExecutor.DiscardOldestPolicy //被拒绝的任务的处理程序,丢弃最旧的未处理请求,然后重试 execute ,除非执行程序被关闭,否则任务被丢弃。  
ThreadPoolExecutor.DiscardPolicy //被拒绝的任务的处理程序静默地丢弃被拒绝的任务。  

枚举类

TimeUnit //A TimeUnit表示给定的粒度单位的持续时间,并提供了跨单位转换的实用方法,并在这些单元中执行定时和延迟操作。
// TimeUnit.SECONDS.sleep(1);  
// 相当于Thread.sleep(1000);

11,Callable

FutureTask

public class FutureTask<V> implements RunnableFuture<V>
    
// 构造方法
FutureTask(Callable<V> callable) 
//创建一个 FutureTask ,它将在运行时执行给定的 Callable 。  
FutureTask(Runnable runnable, V result) 
//创建一个 FutureTask ,将在运行时执行给定的 Runnable ,并安排 get将在成功完成后返回给定的结果。  


boolean cancel(boolean mayInterruptIfRunning) 
//尝试取消执行此任务。  
protected void done() 
//此任务转换到状态 isDone (无论是正常还是通过取消)调用的受保护方法。  
V get() 
//等待计算完成,然后检索其结果。  
V get(long timeout, TimeUnit unit) 
//如果需要等待最多在给定的时间计算完成,然后检索其结果(如果可用)。  
boolean isCancelled() 
//如果此任务在正常完成之前取消,则返回 true 。  
boolean isDone() 
//返回 true如果任务已完成。  
void run() 
//将此未来设置为其计算结果,除非已被取消。  
protected boolean runAndReset() 
//执行计算而不设置其结果,然后将此将来重置为初始状态,如果计算遇到异常或被取消,则不执行此操作。  
protected void set(V v) 
//将此未来的结果设置为给定值,除非此未来已被设置或已被取消。  
protected void setException(Throwable t) 
//导致这个未来报告一个ExecutionException与给定的可抛弃的原因,除非这个未来已经被设置或被取消。  

FutureTask实现Callable接口

由于Thread开启线程只能接收Runable接口的实现类。

如果想要在Thread中使用Callable接口,需要使用Runable的实现类FutureTask,其构造方法需要传入一个Callable实现类。

可使用FutureTask的get方法接收Callable方法中的返回值。

public class CallableTest {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask<String> futureTask = new FutureTask<>(new MyCallable());
        new Thread(futureTask).start();
        String result = futureTask.get();
        System.out.println(result);
    }
}

class MyCallable implements Callable<String>{

    @Override
    public String call() throws Exception {
        System.out.println("进入了callable的call方法。");
        return "hello zhuzhu";
    }
}

Callable于Runable的区别

  1. 有返回值。
  2. 支持泛型。
  3. 可抛出异常。
  4. 方法名不同:call()和run()。

12,常用的辅助类(必会)

CountDownLatch

作用:允许一个或多个线程等待直到在其他线程中执行的一组操作完成的同步辅助。

CountDownLatch(int count) 
//构造一个以给定计数 CountDownLatch CountDownLatch。 

void await() 
//导致当前线程等到锁存器计数到零,除非线程是 interrupted 。  
boolean await(long timeout, TimeUnit unit) 
//使当前线程等待直到锁存器计数到零为止,除非线程为 interrupted或指定的等待时间过去。  
void countDown() 
//减少锁存器的计数,如果计数达到零,释放所有等待的线程。  
long getCount() 
//返回当前计数。  
String toString() 
//返回一个标识此锁存器的字符串及其状态。  
案例
public class CountDownLatchTest {

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(6);

        for (int i = 1; i <= 6; i++) {
            new Thread(()->{
                System.out.println(Thread.currentThread().getName());
                countDownLatch.countDown();
            },String.valueOf(i)).start();
        }
		//如果一直没有倒数到0,就会一直卡在这里。
        countDownLatch.await();
        System.out.println("完成测试");
    }

}
/**
    2
    3
    4
    1
    6
    5
    完成测试
*/
使用了CountDownLatch的await()保证计数完成后才执行后面的输出语句。

CyclicBarrier

//允许一组线程全部等待彼此达到共同屏障点的同步辅助。

CyclicBarrier(int parties) 
//创建一个新的 CyclicBarrier ,当给定数量的线程(线程)等待它时,它将跳闸,并且当屏障跳闸时不执行预定义的动作。  
CyclicBarrier(int parties, Runnable barrierAction) 
//创建一个新的 CyclicBarrier ,当给定数量的线程(线程)等待时,它将跳闸,当屏障跳闸时执行给定的屏障动作,由最后一个进入屏障的线程执行。 

int await() 
//等待所有 parties已经在这个障碍上调用了 await 。  
int await(long timeout, TimeUnit unit) 
//等待所有 parties已经在此屏障上调用 await ,或指定的等待时间过去。  
int getNumberWaiting() 
//返回目前正在等待障碍的各方的数量。  
int getParties() 
//返回旅行这个障碍所需的聚会数量。  
boolean isBroken() 
//查询这个障碍是否处于破碎状态。  
void reset() 
//将屏障重置为初始状态。  
案例
public class CyclicBarrierTest {
    public static void main(String[] args){
        CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{
            System.out.println("集齐7颗龙珠召唤神龙。");
        });

        for (int i = 1; i <= 7; i++) {
            int tempInt = i;
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+"当前收集了" + tempInt + "颗龙珠");
                try {
				  //这里的await()相当于计数加1。
                    cyclicBarrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            },String.valueOf(i)).start();
        }
    }
}

Semaphore

//一个计数信号量。

Semaphore(int permits) 
//创建一个 Semaphore与给定数量的许可证和非公平公平设置。  
Semaphore(int permits, boolean fair) 
//创建一个 Semaphore与给定数量的许可证和给定的公平设置。  

void acquire() 
//从该信号量获取许可证,阻止直到可用,或线程为 interrupted 。  
void acquire(int permits) 
//从该信号量获取给定数量的许可证,阻止直到所有可用,否则线程为 interrupted 。  
void acquireUninterruptibly() 
//从这个信号灯获取许可证,阻止一个可用的。  
void acquireUninterruptibly(int permits) 
//从该信号量获取给定数量的许可证,阻止直到所有可用。  
int availablePermits() 
//返回此信号量中当前可用的许可数。  
int drainPermits() 
//获取并返回所有可立即获得的许可证。  
protected Collection<Thread> getQueuedThreads() 
//返回一个包含可能正在等待获取的线程的集合。  
int getQueueLength() 
//返回等待获取的线程数的估计。  
boolean hasQueuedThreads() 
//查询任何线程是否等待获取。  
boolean isFair() 
//如果此信号量的公平设置为真,则返回 true 。  
protected void reducePermits(int reduction) 
//缩小可用许可证的数量。  
void release() 
//释放许可证,将其返回到信号量。  
void release(int permits) 
//释放给定数量的许可证,将其返回到信号量。  
String toString() 
//返回一个标识此信号量的字符串及其状态。  
boolean tryAcquire() 
//从这个信号量获得许可证,只有在调用时可以使用该许可证。  
boolean tryAcquire(int permits) 
//从这个信号量获取给定数量的许可证,只有在调用时全部可用。  
boolean tryAcquire(int permits, long timeout, TimeUnit unit) 
//从该信号量获取给定数量的许可证,如果在给定的等待时间内全部可用,并且当前线程尚未 interrupted 。  
boolean tryAcquire(long timeout, TimeUnit unit) 
//如果在给定的等待时间内可用,并且当前线程尚未 到达 interrupted,则从该信号量获取许可。  
案例
public class semapgoreTest {
    public static void main(String[] args) {
        //停车场只有三个车位
        Semaphore semaphore = new Semaphore(3);

        //同时有6辆车驶入停车场
        for (int i = 1; i <= 6; i++) {
            final int number = i;
            new Thread(()->{
                try {
                    //车开进车位,只有前三辆车能获取,剩下的等待release()
                    semaphore.acquire();
                    System.out.println(number+"车开进了车位。");
                    //在车位停了两秒钟
                    TimeUnit.SECONDS.sleep(3);
                    System.out.println(number+"车开走了。");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
				  //车开走了然后release()
                    semaphore.release();
                }
            },String.valueOf(i)).start();
        }
    }
}
解释:

在信号量上我们定义两种操作:

  1. acquire(获取)

    • 当一个线程调用 acquire 操作时,他要么通过成功获取信号量(信号量-1)
    • 要么一直等下去,直到有线程释放信号量,或超时
  2. release (释放)

    • 实际上会将信号量的值 + 1,然后唤醒等待的线程。
主要使用场景

信号量主要用于两个目的:一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制

13,读写锁

java.util.concurrent.locks

无锁案例

public class ReadWriteLock {
    public static void main(String[] args) {
        NoLock noLock = new NoLock();
        for (int i = 0; i < 5; i++) {
            final int number = i;
            new Thread(()->{
                noLock.put(String.valueOf(number),String.valueOf(number));
            },String.valueOf(i)).start();
        }

        for (int i = 0; i < 5; i++) {
            final int number = i;
            new Thread(()->{
                noLock.get(String.valueOf(number));
            },String.valueOf(i)).start();
        }
    }
}

class NoLock{
    private volatile Map<String,Object> map = new HashMap<>();

    public void put(String key,Object value){
        System.out.println(Thread.currentThread().getName() + "写入:" + key);
        map.put(key, value);
        System.out.println(Thread.currentThread().getName() + "写入成功咯~~~~");

    }
    public void get(String key){
        System.out.println(Thread.currentThread().getName() + "读取:" + key);
        map.get(key);
        System.out.println(Thread.currentThread().getName() + "读取成功咯~~~~");

    }
}

/**
输出结果:写入的时候插队,会导致数据错乱
0写入:0
4写入:4
3写入:3
1写入:1
1写入成功咯~~~~
2写入:2
2写入成功咯~~~~
3写入成功咯~~~~
4写入成功咯~~~~
0写入成功咯~~~~
0读取:0
1读取:1
1读取成功咯~~~~
0读取成功咯~~~~
2读取:2
3读取:3
2读取成功咯~~~~
4读取:4
3读取成功咯~~~~
4读取成功咯~~~~
*/

ReadWriteLock案例

public class ReadWriteLockTest {
    public static void main(String[] args) {
        HasReentrantReadWriteLock hasReentrantReadWriteLock = new HasReentrantReadWriteLock();

        for (int i = 0; i < 5; i++) {
            final int number = i;
            new Thread(()->{
                hasReentrantReadWriteLock.put(String.valueOf(number),String.valueOf(number));
            },String.valueOf(i)).start();
        }
        for (int i = 0; i < 5; i++) {
            final int number = i;
            new Thread(()->{
                hasReentrantReadWriteLock.get(String.valueOf(number));
            },String.valueOf(i)).start();
        }
    }
}

class HasReentrantReadWriteLock{
    private volatile Map<String,Object> map = new HashMap<>();
    private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    public void put(String key,Object value){
        readWriteLock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "写入:" + key);
            map.put(key, value);
            System.out.println(Thread.currentThread().getName() + "写入成功咯~~~~");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readWriteLock.writeLock().unlock();
        }
    }

    public void get(String key){
        readWriteLock.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "读取:" + key);
            map.get(key);
            System.out.println(Thread.currentThread().getName() + "读取成功咯~~~~");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readWriteLock.readLock().unlock();
        }
    }
}

/**
输出:每次都是等一个线程写入完毕才开始另外一个线程的写入操作,保证了数据的安全性。
1写入:1
1写入成功咯~~~~
0写入:0
0写入成功咯~~~~
2写入:2
2写入成功咯~~~~
3写入:3
3写入成功咯~~~~
4写入:4
4写入成功咯~~~~
0读取:0
0读取成功咯~~~~
1读取:1
2读取:2
2读取成功咯~~~~
4读取:4
1读取成功咯~~~~
4读取成功咯~~~~
3读取:3
3读取成功咯~~~~
*/

总结

独占锁(写锁):指该锁一次只能被一个线程锁持有。对于ReentranrLockSynchronized 而言都是独占锁。

共享锁(读锁):该锁可被多个线程所持有。保证并发时的高效率。

对于ReentrantReadWriteLock来说:

其读锁(ReentrantReadWriteLock.ReadLock)是共享锁;

写锁(ReentrantReadWriteLock.WriteLock)是独占锁。

14,阻塞队列BlockingQueue

集合类之间继承关系

Collection

  • List
    • ArrayList
    • LinkedList
  • Set
    • HashSet (Class)
    • TreeSet (Class)
  • Queue
    • AbstractQueue (Class) 非阻塞队列
    • BlockingQueue (interface) 阻塞队列
      • ArrayBlockingQueue // 由数组结构组成的有界阻塞队列。
      • LinkedTransferQueue // 由链表结构组成的有界阻塞队列。
      • SynchronousQueue // 不存储元素的阻塞队列,也即单个元素的队列。
    • BlockingDeque (interface)
    • Deque (interface) 双端队列
      • ArrayDeque (Class)
      • LinkedBlockingDeque

阻塞队列

阻塞队列是一个队列,在数据结构中起的作用如下图:

在这里插入图片描述

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

  2. 当队列是满的,从队列中添加元素的操作将会被阻塞。

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

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

阻塞队列的用处:

  1. 在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤起。
  2. 线程池。

为什么需要 BlockingQueue?

好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue 都给你一手包办了。

在 concurrent 包发布以前,在多线程环境下,我们每个程序员都必须自己去控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。

ArrayBlockingQueue常用API

方法抛出异常返回特殊值(true,false,neull)一直阻塞超时退出
插入add(e)offer(e)put(e)offer(e,time,unit)
移除remove()poll()take()poll(time,unit)
查看队首element()peek()--

SynchronousQueue 同步队列

SynchronousQueue 没有容量。
与其他的 BlockingQueue 不同,SynchronousQueue是一个不存储元素的 BlockingQueue 。
每一个put操作必须要等待一个take操作,否则不能继续添加元素,反之亦然。

15,线程池(重点)

池化技术

通过预先创建好多个线程,放在池中,这样可以在需要使用线程的时候直接获取,避免多次重复创建、销毁带来的开销,可以大大的提高资源的利用率,提升性能等。

在编程领域,比较典型的池化技术有:线程池、连接池、内存池、对象池等。

线程池的优势

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

它的主要特点为:线程复用,控制最大并发数,管理线程。
第一:降低资源消耗,通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
第二:提高响应速度。当任务到达时,任务可以不需要等待线程创建就能立即执行。
第三:提高线程的可管理性,线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系
统的稳定性,使用线程池可以进行统一分配,调优和监控。

Executors

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

在这里插入图片描述

Executors创建线程池的三大方法
newFixedThreadPool(int capacity)

执行长期任务性能好,创建一个线程池,一池有N个固定的线程,有固定线程数的线程。

public class MyThreadPoolDemo {
    public static void main(String[] args) {
        // 池子大小 5
        ExecutorService threadPool = Executors.newFixedThreadPool(5);

        try {
            // 模拟有10个顾客过来银行办理业务,池子中只有5个工作人员受理业务
            for (int i = 1; i <= 10; i++) {
                threadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName()+" 办理业务");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdown(); // 用完记得关闭
        }
    }
}
newSingleThreadExecutor()

创建容量为1的线程池,只有一个线程。

public class MyThreadPoolDemo {
    public static void main(String[] args) {
        // 有且只有一个固定的线程
        ExecutorService threadPool = Executors.newSingleThreadExecutor();

        try {
            // 模拟有10个顾客过来银行办理业务,池子中只有1个工作人员受理业务
            for (int i = 1; i <= 10; i++) {
                threadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName()+" 办理业务");});
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdown(); // 用完记得关闭
        }
    }
}
newCachedThreadPool()

执行很多短期异步任务,线程池根据需要创建新线程,但在先构建的线程可用时将重用他们。 可扩容,遇强则强。

public class MyThreadPoolDemo {
    public static void main(String[] args) {
    // 一池N线程,可扩容伸缩
        ExecutorService threadPool = Executors.newCachedThreadPool();

        try {
            // 模拟有10个顾客过来银行办理业务,池子中只有N个工作人员受理业务
            for (int i = 1; i <= 10; i++) {
                // 模拟延时看效果

                // try {
                //      TimeUnit.SECONDS.sleep(1);
                // } catch (InterruptedException e) {
                //      e.printStackTrace();
                //  }
                threadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName()+" 办理业务");});
            }

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdown(); // 用完记得关闭
        }

    }
}

ThreadPoolExecutor

查看三大方法的底层源码,发现本质都是调用了ThreadPoolExecutor。

ThreadPoolExecutor源码
// ThreadPoolExecutor源码
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
    if (corePoolSize < 0 ||
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.acc = System.getSecurityManager() == null ?
        null :
    AccessController.getContext();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}
ThreadPoolExecutor的七大参数
1. corePollSize:
核心线程数。在创建了线程池后,线程中没有任何线程,等到有任务到来时才创建线程去执行任务。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中。
    
2. maximumPoolSize:最大线程数。表明线程中最多能够创建的线程数量,此值必须大于等于13. keepAliveTime:空闲的线程保留的时间。
    
4. TimeUnit:空闲线程的保留时间单位。
    TimeUnit.DAYS;  //天
    TimeUnit.HOURS;  //小时
    TimeUnit.MINUTES;  //分钟
    TimeUnit.SECONDS;  //秒
    TimeUnit.MILLISECONDS; //毫秒
    TimeUnit.MICROSECONDS; //微妙
    TimeUnit.NANOSECONDS;  //纳秒

5. BlockingQueue< Runnable>:阻塞队列,存储等待执行的任务。
	参数有ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue。
    
6. ThreadFactory:线程工厂,用来创建线程,一般默认即可。

7. RejectedExecutionHandler:队列已满,而且任务量大于最大线程的异常处理策略。有以下取值
	ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。(默认)
	ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
	ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
	ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务。
异常处理策略

注意看上面的异常处理策略。

阿里巴巴开发手册

线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

说明:Executors返回的线程池对象的弊端如下:

​ 1)FixedThreadPool和SingleThreadPool:

阻塞队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM

​ 2)CachedThreadPool和ScheduledThreadPool:

最大线程数为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM

OutOfMemory(内存资源耗尽)

使用ThreadPoolExecutor创建线程池的案例

public class MyThreadPoolDemo {
	public static void main(String[] args) {
		// 获得CPU的内核数
        System.out.println(Runtime.getRuntime().availableProcessors());

        // 自定义 ThreadPoolExecutor
        ExecutorService threadPool = new ThreadPoolExecutor(
            2,
            5,
            2L,
            TimeUnit.SECONDS,
            new LinkedBlockingDeque<>(3),
            Executors.defaultThreadFactory(),
            new ThreadPoolExecutor.DiscardPolicy());
        try {
            // 模拟有6,7,8,9,10个顾客过来银行办理业务,观察结果情况
            // 最大容量为:maximumPoolSize + workQueue = 最大容量数
            for (int i = 1; i <= 19; i++) {
                threadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName()+" 办理业务");});
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdown(); // 用完记得关闭
        }
	}
}
获得CPU的内核数
Runtime.getRuntime().availableProcessors();
//使用这个代码获取当前计算机的CPU的内核数,然后用这个数去设置线程的最大线程数maximumPoolSize。

拓展:最大线程怎么定义

CPU密集型程序

一个计算为主的程序(专业一点称为CPU密集型程序)。多线程跑的时候,可以充分利用起所有的cpu核心,比如说4个核心的cpu,开4个线程的时候,可以同时跑4个线程的运算任务,此时是最大效率。

但是如果线程远远超出cpu核心数量 反而会使得任务效率下降,因为频繁的切换线程也是要消耗时间的。
因此对于cpu密集型的任务来说,线程数等于cpu数是最好的了。

IO密集型

如果是一个磁盘或网络为主的程序(IO密集型)。一个线程处在IO等待的时候,另一个线程还可以在CPU里面跑,有时候CPU闲着没事干,所有的线程都在等着IO,这时候他们就是同时的了,而单线程的话此时还是在一个一个等待的。我们都知道IO的速度比起CPU来是慢到令人发指的。所以开多线程,比方说多线程网络传输,多线程往不同的目录写文件,等等。

此时 线程数等于IO任务数是最佳的。

16,四大函数式接口(必需掌握)

包:java.util.function。

Java 内置核心四大函数式接口,可以使用lambda表达式。

函数式接口方法参数类型返回类型作用
Function<T,R> 函数型接口R apply(T t)TR对T类型参数进行操作,返回R类型结果。
Predicate 断定型接口Boolean test(T t)TBoolean判断T类型参数是否符合约束,返回Boolean值。
Consumer 消费型接口void accept(T t)Tvoid对T类型参数进行操作。
Supplier 供给型接口T get()-T操作数据,返回T类型结果。

图解

在这里插入图片描述

Function<T, R>

public class FITest {
    public static void main(String[] args) {
        Function<Integer, String> function = new Function<Integer, String>() {
            @Override
            public String apply(Integer integer) {
                return String.valueOf(integer);
            }
        };

        Function<Integer,String> newFunc = (s)->{
            return "输出的是:"+s;
        };

        String string = newFunc.apply(11);
        System.out.println(string);
    }
}

Predicate

public class FITest {
    public static void main(String[] args) {
        Predicate<Integer> predicate = new Predicate<Integer>() {
            @Override
            public boolean test(Integer integer) {
                return integer > 26 ;
            }
        };

        Predicate<Integer> newPred = (t)->{
            return t>30;
        };

        boolean test = newPred.test(87);
        System.out.println(test);
    }
}

Consumer

public class FITest {

    public static void main(String[] args) {
        Consumer<String> consumer = new Consumer<String>() {
            @Override
            public void accept(String s) {
                System.out.println(s);
            }
        };

        Consumer<String> newCon = (t) -> {System.out.println(t);};

        Consumer<String> newCon1=  System.out::print;

        newCon1.accept("keep on!");
        
    }
}

Supplier

public class FITest {

    public static void main(String[] args) {
        Supplier<String> supplier = new Supplier<String>() {
            @Override
            public String get() {
                return "输出一段话";
            }
        };
        Supplier<String> newSup = ()->{
            return "写点什么";
        };
        
        String s = newSup.get();
        System.out.println(s);

    }
}

17,System.out::print

//两种写法的等价。
Consumer<String> newCon = (t) -> {System.out.println(t);};
Consumer<String> newCon1=  System.out::print;

//forEach案例
public static void main(String[] args) {
    List<Integer> list = Arrays.asList(1, 2, 3, 4);
    list.forEach((t)->{
        System.out.println(t);
    });
    list.forEach(System.out::println);
}

18,Stream流式计算

java程序员必备技能:lambda表达式,链式编程,函数式接口,stream流式计算。

流(Stream)到底是什么呢?

是数据渠道,用于操作数据源(集合、数组等)所生成的元素序列。

“集合讲的是数据,流讲的是计算!”

特点:

  1. Stream 自己不会存储元素。

  2. Stream 不会改变源对象,相反,他们会返回一个持有结果的新Stream。

  3. Stream 操作是延迟执行的。这意味着他们会等到需要结果的时候才执行。

java.util.stream包

//java.util.stream包

//Interface接口
BaseStream 
Collector 
DoubleStream 
DoubleStream.Builder 
IntStream 
IntStream.Builder 
LongStream 
LongStream.Builder 
Stream 
Stream.Builder 

//Classes类
Collectors 
StreamSupport 

//Enums枚举类
Collector.Characteristics

Stream

//回顾Collection,stream()将collection集合转为stream.
default Stream<E> stream()  //返回以此集合作为源的顺序 Stream 。 
    
//Stream常用方法
void forEach(Consumer<? super T> action) 
//对此流的每个元素执行操作。 
<R> Stream<R> map(Function<? super T,? extends R> mapper) 
//返回由给定函数应用于此流的元素的结果组成的流。 
<R,A> R collect(Collector<? super T,A,R> collector) 
//使用 Collector对此流的元素执行 mutable reduction Collector 。  
Stream<T> filter(Predicate<? super T> predicate) 
//返回由与此给定谓词匹配的此流的元素组成的流。   
Stream<T> limit(long maxSize) 
//返回由此流的元素组成的流,截短长度不能超过 maxSize 。  
Stream<T> sorted() 
//返回由此流的元素组成的流,根据自然顺序排序。  
Stream<T> sorted(Comparator<? super T> comparator) 
//返回由该流的元素组成的流,根据提供的 Comparator进行排序。  
long count() 
//返回此流中的元素数。
Optional<T> max(Comparator<? super T> comparator) 
//根据提供的 Comparator返回此流的最大元素。  
Optional<T> min(Comparator<? super T> comparator) 
//根据提供的 Comparator返回此流的最小元素。
Object[] toArray() 
//返回一个包含此流的元素的数组。  
Stream<T> distinct() 
//返回由该流的不同元素(根据 Object.equals(Object) )组成的流。  

在这里插入图片描述

案例

public class StreamTest {

    public static void main(String[] args) {
        User amy = new User(12, "Amy", 32);
        User jason = new User(13, "Jason", 12);
        User eden = new User(14, "Eden", 28);
        User jack = new User(15, "Jack", 19);
        User lucy = new User(16, "Lucy", 5);

        List<User> list = Arrays.asList(amy, jason, eden, jack, lucy);
        //list.forEach(System.out::println);

        /**
         * 题目:请按照给出数据,找出同时满足以下条件的用户
         * 1、全部满足偶数ID
         * 2、年龄大于24
         * 3、用户名转为大写
         * 4、用户名字母倒排序
         * 5、只输出一个用户名字 limit
         */

        //lambda表达式写法
        list.stream()
                .filter((u)->{return u.getId()%2==0;})
                .filter((u)->{return u.getAge()>24;})
                .map((u)->{return u.getName().toUpperCase();})
                .sorted(((o1, o2) -> {return o2.compareTo(o1);}))
                .limit(1)
                .forEach(System.out::println);

		//不适用lambda表达式
        list.stream().filter(new Predicate<User>() {
            @Override
            public boolean test(User user) {
                return user.getId() %2 ==0;
            }
        }).filter(new Predicate<User>() {
            @Override
            public boolean test(User user) {
                return user.getAge() > 24;
            }
        }).map(new Function<User, String>() {
            @Override
            public String apply(User user) {
                return user.getName().toUpperCase();
            }
        }).sorted(new Comparator<String>() {
            @Override
            public int compare(String o1, String o2) {
                return o2.compareTo(o1);
            }
        }).limit(1).forEach(System.out::println);
        
    }

}

class User{
    private int id;
    private String name;
    private int age;

	//无参,有参,toString,getter,setter
}

19,ForkJoin分支合并

什么是ForkJoin

从JDK1.7开始,Java提供Fork/Join框架用于并行执行任务,它的思想就是讲一个大任务分割成若干小任务,最终汇总每个小任务的结果得到这个大任务的结果。

这种思想和MapReduce很像(input --> split --> map --> reduce --> output)

主要有两步:

  • 第一、任务切分;
  • 第二、结果合并。

在这里插入图片描述

它的模型大致是这样的:线程池中的每个线程都有自己的工作队列(PS:这一点和ThreadPoolExecutor不同,ThreadPoolExecutor是所有线程公用一个工作队列,所有线程都从这个工作队列中取任务),当自己队列中的任务都完成以后,会从其它线程的工作队列中偷一个任务执行,这样可以充分利用资源。

工作窃取

先做完任务的工作线程会从其他未完成任务的线程尾部依次获取任务去执行。这样就可以充分利用CPU的资源。

优点是充分利用线程进行并行计算,并减少了线程间的竞争。

其缺点是在某些情况下还是存在竞争,比如双端队列里只有一个任务时。并且消耗了更多的系统资源,比如创建多个线程和多个双端队列。
在这里插入图片描述

ForkJoinPool

WorkQueue是一个ForkJoinPool中的内部类,它是线程池中线程的工作队列的一个封装,支持任务窃取,可以合理的提高运行和计算效率。

每个线程都有一个WorkQueue,而WorkQueue中有执行任务的线程(ForkJoinWorkerThread owner),还有这个线程需要处理的任务(ForkJoinTask<?>[] array)。那么这个新提交的任务就是加到array中。

ForkJoinTask

ForkJoinTask代表运行在ForkJoinPool中的任务。

主要方法:

fork() 在当前线程运行的线程池中安排一个异步执行。简单的理解就是再创建一个子任务。

join() 当任务完成的时候返回计算结果。

invoke() 开始执行任务,如果必要,等待计算完成。

子类: Recursive:递归

RecursiveAction 一个递归无结果的ForkJoinTask(没有返回值)

RecursiveTask 一个递归有结果的ForkJoinTask(有返回值)

案例

public class ForkJoinTest extends RecursiveTask<Long> {
    private Long start;
    private Long end;
    private static final Long middle = 10000L;

    public ForkJoinTest(Long start, Long end) {
        this.start = start;
        this.end = end;
    }

    @Override
    protected Long compute() {
        Long flag = end - start;
        if (flag < middle){
            Long sum = 0L;
            for (long i = start; i <= end; i++) {
                sum += i;
            }
            return sum;
        }else {
            //使用Fork Join分支合并
            Long border = (start+end) >> 1;
            ForkJoinTest forkJoinLeft = new ForkJoinTest(start, border);
            forkJoinLeft.fork();
            ForkJoinTest forkJoinRight = new ForkJoinTest(border + 1, end);
            forkJoinRight.fork();
            long result = forkJoinLeft.join() + forkJoinRight.join();
            return result;
        }
    }
}
public class ForkJoinDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        test1();
        test2();
        test3();
    }

    public static void test1(){
        //普通迭代方法
        Long startTime = System.currentTimeMillis();
        Long start = 0L;
        Long end = 20_0000_0000L;
        for (Long i = 0L; i <= end; i++) {
            start += i;
        }
        Long endTime = System.currentTimeMillis();
        System.out.println("普通迭代方法运行了:" + (endTime - startTime) + "秒,结果为:"+start);
    }

    public static void test2() throws ExecutionException, InterruptedException {
        //ForkJoin
        Long startTime = System.currentTimeMillis();
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        ForkJoinTest forkJoinTest = new ForkJoinTest(0L, 20_0000_0000L);
        ForkJoinTask<Long> result = forkJoinPool.submit(forkJoinTest);
        Long value = result.get();
        Long endTime = System.currentTimeMillis();
        System.out.println("ForkJoin方法运行了:" + (endTime - startTime) + "秒,结果为:"+value);
    }

    public static void test3(){
        //Java 8 并行流LongStream的实现
        Long startTime = System.currentTimeMillis();
        long result = LongStream.rangeClosed(0L, 20_0000_0000L).parallel().reduce(0, Long::sum);
        Long endTime = System.currentTimeMillis();
        System.out.println("LongStream方法运行了:" + (endTime - startTime) + "秒,结果为:"+result);
    }
}

//输出结果:
普通迭代方法运行了:13159毫秒,结果为:2000000001000000000
ForkJoin方法运行了:6423毫秒,结果为:2000000001000000000
LongStream方法运行了:222毫秒,结果为:2000000001000000000

核心代码

ForkJoinPool forkJoinPool = new ForkJoinPool();
//该类为继承了ForkJoinTask接口的子类RecursiveTask
ForkJoinTest forkJoinTest = new ForkJoinTest(0L, 20_0000_0000L);
ForkJoinTask<Long> result = forkJoinPool.submit(forkJoinTest);
Long value = result.get();

20,异步回调

概述

Future设计的初衷:对将来某个时刻会发生的结果进行建模。

当我们需要调用一个函数方法时。如果这个函数执行很慢,那么我们就要进行等待。但有时候,我们可能并不急着要结果。

因此,我们可以让被调用者立即返回,让他在后台慢慢处理这个请求。对于调用者来说,则可以先处理一些其他任务,在真正需要数据的场合再去尝试获取需要的数据。

它建模了一种异步计算,返回一个执行运算结果的引用,当运算结束后,这个引用被返回给调用方。在Future中出发那些潜在耗时的操作把调用线程解放出来,让它能继续执行其他有价值的工作,不再需要等待耗时的操作完成。

Future的优点:比更底层的Thread更易用。要使用Future,通常只需要将耗时的操作封装在一个Callable对象中,再将它提交给ExecutorService。

为了让程序更加高效,让CPU最大效率的工作,我们会采用异步编程。首先想到的是开启一个新的线程去做某项工作。再进一步,为了让新线程可以返回一个值,告诉主线程事情做完了,于是乎Future粉墨登场。然而Future提供的方式是主线程主动问询新线程,要是有个回调函数就爽了。所以,为了满足Future的某些遗憾,强大的CompletableFuture随着Java8一起来了。

图解

在这里插入图片描述

Future

boolean cancel(boolean mayInterruptIfRunning) //尝试取消执行此任务。  
V get() //等待计算完成,然后检索其结果。  
V get(long timeout, TimeUnit unit) //如果需要等待最多在给定的时间计算完成,然后检索其结果。  
boolean isCancelled() //如果此任务在正常完成之前被取消,则返回 true 。  
boolean isDone() //返回 true如果任务已完成。  

CompletableFuture

static CompletableFuture<Void> runAsync(Runnable runnable) 
//返回一个新的CompletableFuture,它在运行给定操作后由运行在 ForkJoinPool.commonPool()中的任务 异步完成。  
static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor) 
//返回一个新的CompletableFuture,它在运行给定操作之后由在给定执行程序中运行的任务异步完成。  

static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier) 
//返回一个新的CompletableFuture,它通过在 ForkJoinPool.commonPool()中运行的任务与通过调用给定的供应商获得的值 异步完成。  
static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor) 
//返回一个新的CompletableFuture,由给定执行器中运行的任务异步完成,并通过调用给定的供应商获得的值。  

T get() 
//等待这个未来完成的必要,然后返回结果。  
T get(long timeout, TimeUnit unit) 
//如果有必要等待这个未来完成的给定时间,然后返回其结果(如果有的话)。  
CompletableFuture<T> whenComplete(BiConsumer<? super T,? super Throwable> action) 
//返回与此阶段相同的结果或异常的新的CompletionStage,当此阶段完成时,使用结果(或 null如果没有))和此阶段的异常(或 null如果没有))执行给定的操作。  
CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T,? super Throwable> action) 
//返回一个与此阶段相同结果或异常的新CompletionStage,当此阶段完成时,执行给定操作将使用此阶段的默认异步执行工具执行给定操作,结果(或 null如果没有))和异常(或 null如果没有)这个阶段作为参数。  
CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T,? super Throwable> action, Executor executor) 
//返回与此阶段相同的结果或异常的新的CompletionStage,当此阶段完成时,使用提供的执行者执行给定的操作,如果没有,则使用结果(或 null如果没有))和异常(或 null如果没有))作为论据。  

runAsync案例

public class FutureTest {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "没有返回,ok");
        });

        System.out.println("111111"); // 先执行
        TimeUnit.SECONDS.sleep(3); //异步的,总运行时间3秒多。
        future.get();
    }
}

//输出
111111
ForkJoinPool.commonPool-worker-9没有返回,ok

supplyAsync案例

public class FutureTest {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> {
            //int i = 3/0;
            return 1024;
        });

        CompletableFuture<Integer> future2 = future1.whenComplete((t, u) -> {
            System.out.println("===t:" + t);  //正常结果
            System.out.println("===u:" + u);  //报错的信息
        }).exceptionally((e) -> {
            System.out.println("=======exception:" + e.getMessage()); //结果异常,非正常结束
            return 101;
        });
        Integer integer = future2.get();
        System.out.println(integer);
    }
}

//正常运行时输出
===t:1024
===u:null
1024
//出现异常时输出
===t:null
===u:java.util.concurrent.CompletionException: java.lang.ArithmeticException: / by zero
=======exception:java.lang.ArithmeticException: / by zero
101

21,JMM

什么是JMM

JMM 本身是一种抽象的概念,并不真实存在,它描述的是一组规则或者规范~

JMM即为JAVA 内存模型(java memory model)。因为在不同的硬件生产商和不同的操作系统下,内存的访问逻辑有一定的差异,结果就是当你的代码在某个系统环境下运行良好,并且线程安全,但是换了个系统就出现各种问题。Java内存模型,就是为了屏蔽系统和硬件的差异,让一套代码在不同平台下能到达相同的访问结果。JMM从java 5开始的JSR-133发布后,已经成熟和完善起来。

JMM规定了内存主要划分为主内存工作内存两种。此处的主内存和工作内存跟JVM内存划分(堆、栈、方法区)是在不同的层次上进行的,如果非要对应起来,主内存对应的是Java中的对象实例部分,工作内存对应的是中的部分区域,从更底层的来说,主内存对应的是硬件的物理内存工作内存对应的是寄存器和高速缓存。

JVM在设计时候考虑到,如果JAVA线程每次读取和写入变量都直接操作主内存,对性能影响比较大,所以每条线程拥有各自的工作内存,工作内存中的变量是主内存中的一份拷贝,线程对变量的读取和写入,直接在工作内存中操作,而不能直接去操作主内存中的变量。但是这样就会出现一个问题,当一个线程修改了自己工作内存中变量,对其他线程是不可见的,会导致线程不安全的问题。因为JMM制定了一套标准来保证开发者在编写多线程程序的时候,能够控制什么时候内存会被同步给其他线程

JMM关于同步的规定

  1. 线程解锁前,必须把共享变量的值刷新回主内存。
  2. 线程加锁前,必须读取主内存的最新值到自己的工作内存。
  3. 加锁解锁是同一把锁。

JMM的内存模型

线程A感知不到线程B操作了值的变化!如何能够保证线程间可以同步感知这个问题呢?只需要使用Volatile关键字即可!volatile 保证线程间变量的可见性,简单地说就是当线程A对变量X进行了修改后,在线程A后面执行的其他线程能看到变量X的变动,更详细地说是要符合以下两个规则 :

  1. 线程对变量进行修改之后,要立刻回写到主内存。
  2. 线程对变量读取的时候,要从主内存中读,而不是缓存。

各线程的工作内存间彼此独立,互不可见,在线程启动的时候,虚拟机为每个内存分配一块工作内存,不仅包含了线程内部定义的局部变量,也包含了线程所需要使用的共享变量(非线程内构造的对象)的副本,即,为了提高执行效率。

内存交互操作

内存交互操作有8种,虚拟机实现必须保证每一个操作都是原子的,不可在分的(对于double和long类型的变量来说,load、store、read和write操作在某些平台上允许例外)。

  1. lock (锁定):作用于主内存的变量,把一个变量标识为线程独占状态。
  2. unlock (解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
  3. read (读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
  4. load (载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中。
  5. use (使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令。
  6. assign (赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中。
  7. store (存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用。
  8. write (写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。

在这里插入图片描述

JMM对这八种指令的使用,制定了如下规则:

  1. 不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write。
  2. 不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存。
  3. 不允许一个线程将没有assign的数据从工作内存同步回主内存。
  4. 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变量实施use、store操作之前,必须经过assign和load操作。
  5. 一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁。
  6. 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值。
  7. 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量。
  8. 对一个变量进行unlock操作之前,必须把此变量同步回主内存。

JMM对这八种操作规则和对volatile的一些特殊规则就能确定哪里操作是线程安全,哪些操作是线程不安全的了。但是这些规则实在复杂,很难在实践中直接分析。所以一般我们也不会通过上述规则进行分析。更多的时候,使用java的happen-before规则来进行分析。

happens-before字面翻译过来就是先行发生,A happens-before B 就是A先行发生于B?

不准确!在Java内存模型中,happens-before 应该翻译成:前一个操作的结果可以被后续的操作获取。讲白点就是前面一个操作把变量a赋值为1,那后面一个操作肯定能知道a已经变成了1。

我们再来看看为什么需要这几条规则?

因为我们现在电脑都是多CPU,并且都有缓存,导致多线程直接的可见性问题。详情可以看我之前的文章

面试官:你知道并发Bug的源头是什么吗?

所以为了解决多线程的可见性问题,就搞出了happens-before原则,让线程之间遵守这些原则。编译器

还会优化我们的语句,所以等于是给了编译器优化的约束。不能让它优化的不知道东南西北了!

22,Volatile

Thread.yield()是在主线程中执行的,意思只要还有除了GC和main线程之外的线程在跑,主线程就让出cpu不往下执行。

什么是Volatile

volitile 是 Java 虚拟机提供的轻量级的同步机制,三大特性:

  1. 保证可见性
  2. 不保证原子性
  3. 禁止指令重排

验证可见性

public class Test {
    //如果不加volatile,主线程更改了number的值,另外一条线程不会知道number值已经更新,会一直循环。
    //加了volatile,number的值一改,while循环就停止了。
    private volatile static int number = 0;

    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            while (number==0){
                //什么也不做
            }
        }).start();

        TimeUnit.SECONDS.sleep(2);
        number = 1;
        System.out.println(number);

    }
}

验证不保证原子性

原子性理解:

不可分割,完整性,也就是某个线程正在做某个具体的业务的时候,中间不可以被加塞或者被分割,需要整体完整,要么同时成功,要么同时失败。

public class MyVolatile {
    private volatile static int number = 0;
//    private volatile static AtomicInteger number = new AtomicInteger();

    public static void add(){
//        number.getAndIncrement();
        number++;
    }

    public static void main(String[] args) {

        for (int i = 0; i < 20; i++) {
            new Thread(()->{
                for (int j = 0; j < 1000; j++) {
                    add();
                }
            }).start();
        }

        while (Thread.activeCount() > 2){
            Thread.yield();
        }

        System.out.println(number);
        
    }
}
//三次输出结果:19542,17587,15194。说明volatile 不能保证原子性!!!
解决方法
  1. add()方法加锁,synchronized,Lock,ReadWriteLock读写锁
  2. 使用线程安全的,原子性的java.util.concurrent.atomic包下的数据类。
//解决方法1
public synchronized static void add(){
    number++;
}

//解决方法2
private volatile static AtomicInteger number = new AtomicInteger();
public static void add(){
	number.getAndIncrement();
}

禁止指令重排

volatile 实现了禁止指令重排优化,从而避免 多线程环境下程序出现乱序执行的现象。

先了解一个概念,内存屏障(Memory Barrier)又称内存栅栏,是一个CPU 指令,它的作用有两个:

  1. 保证特定操作的执行顺序。
  2. 保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。

由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条 Memory Barrier 则会告诉编译器和CPU,不管什么指令都不能和这条 Memory Barrier 指令重排序,也就是说,通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。内存屏障另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。

总结

volatile可以保证可见性,不能保证原子性,由于设置了内存屏障,可以禁止指令重排

22,java.util.concurrent.atomic包

Class

//Classes类
AtomicBoolean 
AtomicInteger 
AtomicIntegerArray 
AtomicIntegerFieldUpdater 
AtomicLong 
AtomicLongArray 
AtomicLongFieldUpdater 
AtomicMarkableReference 
AtomicReference 
AtomicReferenceArray 
AtomicReferenceFieldUpdater 
AtomicStampedReference 
DoubleAccumulator 
DoubleAdder 
LongAccumulator 
LongAdder 

AtomicInteger

//Constructor
AtomicInteger() 
创建一个新的AtomicInteger,初始值为 0AtomicInteger(int initialValue) 
用给定的初始值创建一个新的AtomicInteger。  

//method
int accumulateAndGet(int x, IntBinaryOperator accumulatorFunction) 
//使用将给定函数应用于当前值和给定值的结果原子更新当前值,返回更新后的值。  
int addAndGet(int delta) 
//将给定的值原子地添加到当前值。  
boolean compareAndSet(int expect, int update) 
//如果当前值 ==为预期值,则将该值原子设置为给定的更新值。  
int decrementAndGet() 
//原子减1当前值。  
double doubleValue() 
//返回此值 AtomicInteger为 double一个宽元转换后。  
float floatValue() 
//返回此值 AtomicInteger为 float一个宽元转换后。  
int get() 
//获取当前值。  
int getAndAccumulate(int x, IntBinaryOperator accumulatorFunction) 
//使用给定函数应用给当前值和给定值的结果原子更新当前值,返回上一个值。  
int getAndAdd(int delta) 
//将给定的值原子地添加到当前值。  
int getAndDecrement() 
//原子减1当前值。  
int getAndIncrement() 
//原子上增加一个当前值。  
int getAndSet(int newValue) 
//将原子设置为给定值并返回旧值。  
int getAndUpdate(IntUnaryOperator updateFunction) 
//用应用给定函数的结果原子更新当前值,返回上一个值。  
int incrementAndGet() 
//原子上增加一个当前值。  
int intValue() 
//将 AtomicInteger的值作为 int 。  
void lazySet(int newValue) 
//最终设定为给定值。  
long longValue() 
//返回此值 AtomicInteger为 long一个宽元转换后。  
void set(int newValue) 
//设置为给定值。  
String toString() 
//返回当前值的String表示形式。  
int updateAndGet(IntUnaryOperator updateFunction) 
//使用给定函数的结果原子更新当前值,返回更新的值。  
boolean weakCompareAndSet(int expect, int update) 
//如果当前值 ==为预期值,则将值设置为给定更新值。  

23,彻底玩转单例模式

饿汉式

public class Hungry {
    private final static Hungry hungry = new Hungry();
    private Hungry(){}
    public static Hungry getInstance(){
        return hungry;
    }
}

class HungryTest{
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Hungry hungry = Hungry.getInstance();
        System.out.println(hungry);
        Hungry hungry1 = Hungry.getInstance();
        System.out.println(hungry1);

	    //测试发现,使用反射可以破坏单例模式。
        Constructor<Hungry> constructor = Hungry.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        Hungry hungry2 = constructor.newInstance();
        System.out.println(hungry2);

        Hungry hungry3 = constructor.newInstance();
        System.out.println(hungry3);

		//多线程下是安全的,因为在加载类的时候就已经生成对象了。
		for (int i = 0; i < 5; i++) {
            new Thread(()->{
                Hungry hungry = Hungry.getInstance();
                System.out.println(hungry);
            }).start();
        }
    }
}
//输出结果
com.phliny.singleton.Hungry@74a14482
com.phliny.singleton.Hungry@74a14482
com.phliny.singleton.Hungry@1540e19d
com.phliny.singleton.Hungry@677327b6
//多线程输出结果
com.phliny.singleton.Hungry@3354d998
com.phliny.singleton.Hungry@3354d998
com.phliny.singleton.Hungry@3354d998
com.phliny.singleton.Hungry@3354d998
com.phliny.singleton.Hungry@3354d998

懒汉式

public class LazyMan {

    private static LazyMan lazyMan;

    private LazyMan(){
        System.out.println(Thread.currentThread().getName()+":已经进入了构造方法");
    };

    public static LazyMan getInstance(){
        if (lazyMan==null){
            lazyMan = new LazyMan();
        }
        return lazyMan;
    };
}

class LazyManTest{

    public static void main(String[] args) {

        for (int i = 0; i < 5; i++) {
            new Thread(()->{
                LazyMan lazyMan = LazyMan.getInstance();
                System.out.println(lazyMan);
            }).start();
        }
    }
}

//输出,说明多线程下懒汉式不安全。
Thread-0:已经进入了构造方法
Thread-3:已经进入了构造方法
Thread-1:已经进入了构造方法
com.phliny.singleton.LazyMan@632a10ea
Thread-2:已经进入了构造方法
Thread-4:已经进入了构造方法
com.phliny.singleton.LazyMan@7245297c
com.phliny.singleton.LazyMan@6218fb2e
com.phliny.singleton.LazyMan@3354d998
com.phliny.singleton.LazyMan@4cc3632

DCL懒汉式

public class LazyMan {

    private static LazyMan lazyMan;

    private LazyMan(){
        System.out.println(Thread.currentThread().getName()+":已经进入了构造方法");
    };

    public static LazyMan getInstance(){
        if (lazyMan==null){
            synchronized (LazyMan.class){
                if (lazyMan==null){
                    lazyMan = new LazyMan();
                }
            }
        }
        return lazyMan;
    };
}

class LazyManTest{

    public static void main(String[] args) {

        for (int i = 0; i < 5; i++) {
            new Thread(()->{
                LazyMan lazyMan = LazyMan.getInstance();
                System.out.println(lazyMan);
            }).start();
        }
    }
}
//输出结果
Thread-0:已经进入了构造方法
com.phliny.singleton.LazyMan@54437fbd
com.phliny.singleton.LazyMan@54437fbd
com.phliny.singleton.LazyMan@54437fbd
com.phliny.singleton.LazyMan@54437fbd
com.phliny.singleton.LazyMan@54437fbd

DCL懒汉式+volatile

DCL懒汉式的单例,在极端情况还是可能会有一定的问题。因为不是原子性操作,至少会经过三个步骤:

  1. 分配对象内存空间

  2. 执行构造方法初始化对象

  3. 设置instance指向刚分配的内存地址,此时instance !=null;

由于指令重排,导致A线程执行 lazyMan = new LazyMan();的时候,可能先执行了第三步(还没执行第二步),此时线程B又进来了,发现lazyMan已经不为空了,直接返回了lazyMan,并且后面使用了返回的lazyMan,由于线程A还没有执行第二步,导致此时lazyMan还不完整,可能会有一些意想不到的错误。

为了避免指令重排:在上面DCL单例模式增加一个volatile关键字。

private static volatile LazyMan lazyMan;

反射破坏单例模式

public class LazyMan {

    private static volatile LazyMan lazyMan;

    private LazyMan(){
        System.out.println(Thread.currentThread().getName()+":已经进入了构造方法");
    };

    public static LazyMan getInstance(){
        if (lazyMan==null){
            synchronized (LazyMan.class){
                if (lazyMan==null){
                    lazyMan = new LazyMan();
                }
            }
        }
        return lazyMan;
    };
}

class LazyManTest{

    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {

        Constructor<LazyMan> constructor = LazyMan.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        LazyMan lazyMan1 = constructor.newInstance();
        LazyMan lazyMan2 = constructor.newInstance();
        LazyMan lazyMan3 = constructor.newInstance();

        System.out.println(lazyMan1);
        System.out.println(lazyMan2);
        System.out.println(lazyMan3);
    }
}
//输出
main:已经进入了构造方法
main:已经进入了构造方法
main:已经进入了构造方法
com.phliny.singleton.LazyMan@74a14482
com.phliny.singleton.LazyMan@1540e19d
com.phliny.singleton.LazyMan@677327b6
解决方法1
private LazyMan(){
    System.out.println(Thread.currentThread().getName()+":已经进入了构造方法");
    synchronized (LazyMan.class){
        if (lazyMan != null){
            throw new RuntimeException("不要试图用反射破坏单例模式");
        }
    }
};

在私有的构造函数中做一个判断,如果lazyMan不为空,说明lazyMan已经被创建过了,如果正常调用getInstance方法,是不会出现这种事情的,所以直接抛出异常!

但是这种写法还是有问题:

上面我们是先正常的调用了getInstance方法,创建了LazyMan对象,所以第二次用反射创建对象,私有构造函数里面的判断起作用了,反射破坏单例模式失败。但是如果破坏者干脆不先调用getInstance方法,一上来就直接用反射创建对象,我们的判断就不生效了:

解决方法2
public class LazyMan {

    private static volatile LazyMan lazyMan;

    private static boolean flag = false;

    private LazyMan(){
        System.out.println(Thread.currentThread().getName()+":已经进入了构造方法");
        synchronized (LazyMan.class){
            if (!flag){
                flag = true;
            }else {
                throw new RuntimeException("不要试图用反射破坏单例模式");
            }
        }
    };
    
    public static LazyMan getInstance(){
        if (lazyMan==null){
            synchronized (LazyMan.class){
                if (lazyMan==null){
                    lazyMan = new LazyMan();
                }
            }
        }
        return lazyMan;
    };
}

class LazyManTest{

    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
//        LazyMan lazyMan = LazyMan.getInstance();
//        System.out.println(lazyMan);
        Constructor<LazyMan> constructor = LazyMan.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        LazyMan lazyMan1 = constructor.newInstance();
        LazyMan lazyMan2 = constructor.newInstance();
        LazyMan lazyMan3 = constructor.newInstance();

        System.out.println(lazyMan1);
        System.out.println(lazyMan2);
        System.out.println(lazyMan3);
        
    }
}
//输出
main:已经进入了构造方法
main:已经进入了构造方法
Exception in thread "main" java.lang.reflect.InvocationTargetException

在这里,我定义了一个boolean变量flag,初始值是false,私有构造函数里面做了一个判断,如果flag=false,就把flag改为true,但是如果flag等于true,就说明有问题了,因为正常的调用是不会第二次跑到私有构造方法的,所以抛出异常。

看起来很美好,但是还是不能完全防止反射破坏单例模式,因为可以利用反射修改flag的值。

破解上面的解决方法2
class LazyManTest{

    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {

        Constructor<LazyMan> constructor = LazyMan.class.getDeclaredConstructor();
        constructor.setAccessible(true);

		//获取判断字段flag,并设置为可访问的(针对private字段)
        Field flag = LazyMan.class.getDeclaredField("flag");
        flag.setAccessible(true);

		//每次通过反射创建对象后,将falg重新设置为false,就不会抛出异常。
        LazyMan lazyMan1 = constructor.newInstance();
        flag.set(lazyMan1,false);
        
 		//每次通过反射创建对象后,将falg重新设置为false,就不会抛出异常。
        LazyMan lazyMan2 = constructor.newInstance();
        flag.set(lazyMan2,false);
        
        LazyMan lazyMan3 = constructor.newInstance();

        System.out.println(lazyMan1);
        System.out.println(lazyMan2);
        System.out.println(lazyMan3);

    }
}

枚举

枚举类型是Java 5中新增特性的一部分,它是一种特殊的数据类型,之所以特殊是因为它既是一种类(class)类型却又比类类型多了些特殊的约束,但是这些约束的存在也造就了枚举类型的简洁性、安全性以及便捷性。

枚举是目前最推荐的单例模式的写法,因为足够简单,不需要开发自己保证线程的安全,同时又可以有效的防止反射破坏我们的单例模式。

简单测试
public enum  MyEnum {
    INSTANCE;

    public static MyEnum getInstance(){
        return INSTANCE;
    }
}

class MyEnumTest{

    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        //符合单例模式,hashcode是一样的
        MyEnum instance = MyEnum.getInstance();
        MyEnum instance1 = MyEnum.getInstance();
        System.out.println(instance.hashCode());
        System.out.println(instance1.hashCode());

        Constructor<MyEnum> constructor = MyEnum.class.getDeclaredConstructor();
        MyEnum instance2 = constructor.newInstance();
        System.out.println(instance2);
    }

}
//报错,提示NoSuchMethodException,表示没有这个构造器!!!
1956725890
1956725890
Exception in thread "main" java.lang.NoSuchMethodException: com.phliny.singleton.MyEnum.<init>()

为什么呢?因为底层的话确实是没有无参构造器,那Enum枚举类的底层是怎样的呢?我们通过jad 进行反编译看看。

通过jad 进行反编译
jad -sjava EnumSingleton.class
# 会生成一个java文件
Parsing EnumSingleton.class... Generating EnumSingleton.java

我们点开里面的源码

package 53554F8B6A215F0F;

public final class EnumSingleton extends Enum{

	public static EnumSingleton[] values(){
		return (EnumSingleton[])$VALUES.clone();
    }

    public static EnumSingleton valueOf(String name){
		return (EnumSingleton)Enum.valueOf(53554F8B6A215F0F/EnumSingleton, name);
    }
	//发现Enum底层是有一个带String和int的构造器的。
    private EnumSingleton(String s, int i)
    	super(s, i);
  	}
	
	public EnumSingleton getInstance(){
		return INSTANCE;
    }
	public static final EnumSingleton INSTANCE;
	private static final EnumSingleton $VALUES[];
	static {
		INSTANCE = new EnumSingleton("INSTANCE", 0);
		$VALUES = (new EnumSingleton[] {INSTANCE});
    }
}
尝试破坏Enum枚举类的单例模式
class MyEnumTest{

    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        //符合单例模式,hashcode是一样的
        MyEnum instance = MyEnum.getInstance();
        MyEnum instance1 = MyEnum.getInstance();
        System.out.println(instance.hashCode());
        System.out.println(instance1.hashCode());

		//获取上面发现的带String和int的构造方法
        Constructor<MyEnum> constructor = MyEnum.class.getDeclaredConstructor(String.class,int.class);
        constructor.setAccessible(true);
        MyEnum instance2 = constructor.newInstance();
        System.out.println(instance2);
    }

}
//输出,IllegalArgumentException异常
1956725890
1956725890
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
newInstance方法
@CallerSensitive
    public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException
    {
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class<?> caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, null, modifiers);
            }
        }
		//newInstance方法中设定了不能用发射破坏枚举
        if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
        ConstructorAccessor ca = constructorAccessor;   // read volatile
        if (ca == null) {
            ca = acquireConstructorAccessor();
        }
        @SuppressWarnings("unchecked")
        T inst = (T) ca.newInstance(initargs);
        return inst;
    }
总结

使用反射不能破坏枚举类的单例模式。

静态内部类

public class StaticInnerClassTest {

    private StaticInnerClassTest(){};

    public static StaticInnerClassTest getInstance(){
        return Inner.staticInnerClassTest;
    }

    private static class Inner{
        private static final StaticInnerClassTest staticInnerClassTest = new StaticInnerClassTest();

    }
}

24,深入理解CAS

CAS(CompareAndSwap)

比较当前工作内存中的值和主内存中的值,如果相同则执行规定操作,否则继续比较直到主内存和工作内存中的值一致为止。

CAS应用

CAS 有3个操作数,内存值V,旧的预期值A,要修改的更新值B。且仅当预期值A 和 内存值 V 相同时,将内存值 V 修改为B,否则什么都不做。

CAS的缺点

1、循环时间长开销很大。

可以看到源码中存在 一个 do…while 操作,如果CAS失败就会一直进行尝试。

2、只能保证一个共享变量的原子操作。

当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作。但是:

对多个共享变量操作时,循环CAS就无法保证操作的原子性,这时候就可以用锁来保证原子性。

3、引出来 ABA 问题???

25,原子引用

问题1

原子类 AtomicInteger 的ABA问题谈谈?原子更新引用知道吗?

CAS => UnSafe => CAS 底层思想 => ABA => 原子引用更新 => 如何规避ABA问题

问题2

ABA问题怎么产生的?

CAS会导致 “ABA问题”。狸猫换太子。

CAS算法实现一个重要前提:需要取出内存中某时刻的数据并在当下时刻比较并交换,那么在这个时间差内会导致数据的变化。

比如说一个线程one从内存位置V中取出A,这个时候另一个线程two也从内存中取出A,并且线程two进行了一些操作将值变成了B,然后线程two又将 V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后线程one操作成功。尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。

案例

import java.util.concurrent.atomic.AtomicReference;
	public class AtomicReferenceDemo {
    public static void main(String[] args) {
        User zhangsan = new User("zhangsan", 22);
        User lisi = new User("lisi", 25);
        AtomicReference<User> atomicReference = new AtomicReference<>();
        atomicReference.set(zhangsan); // 设置
        System.out.print(atomicReference.compareAndSet(zhangsan,lisi));
        System.out.println(atomicReference.get().toString());
        System.out.print(atomicReference.compareAndSet(zhangsan,lisi));
        System.out.println(atomicReference.get().toString());
	}
}

class User{
	String username;
	int age;

	public User(String username, int age) {
		this.username = username;
		this.age = age;
	}
	//toString方法
}

要解决ABA问题

我们就需要加一个版本号

/**
2   * ABA 问题的解决   AtomicStampedReference
3   */
public class ABADemo {
	static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
	public static void main(String[] args) {
		new Thread(()->{
			atomicReference.compareAndSet(100,101);
			atomicReference.compareAndSet(101,100);
        },"T1").start();
		new Thread(()->{
			// 暂停一秒钟,保证上面线程先执行
			try {
				TimeUnit.SECONDS.sleep(1);
             } catch (InterruptedException e) {
				e.printStackTrace();
             }
			System.out.println(atomicReference.compareAndSet(100, 2019)); 
             // 虽然修改成功,但是里面的数据已经是更改过的了。
			System.out.println(atomicReference.get());
		},"T2").start();
	}
}
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicStampedReference;
/**
* ABA 问题的解决   AtomicStampedReference
* 注意变量版本号修改和获取问题。不要写错
*/
public class ABADemo {

	static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100,1);

	public static void main(String[] args) {
		//T1线程
		new Thread(()->{
			int stamp = atomicStampedReference.getStamp(); // 获得版本号
			System.out.println("T1 stamp 01=>"+stamp);
			// 暂停1秒钟,保证下面线程获得初始版本号
			try {
				TimeUnit.SECONDS.sleep(1);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			atomicStampedReference.compareAndSet(100, 101,		atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);

			System.out.println("T1 stamp 02=>"+atomicStampedReference.getStamp());

			atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);

			System.out.println("T1 stamp 03=>"+atomicStampedReference.getStamp());
        },"T1").start();
        
		//T2线程
		new Thread(()->{
			int stamp = atomicStampedReference.getStamp(); // 获得版本号
			System.out.println("T2 stamp 01=>"+stamp);
			// 暂停3秒钟,保证上面线程先执行
			try {
				TimeUnit.SECONDS.sleep(3);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			boolean result = atomicStampedReference.compareAndSet(100, 2019, stamp, stamp + 1);

			System.out.println("T2 是否修改成功 =>"+ result);
			System.out.println("T2 最新stamp =>"+atomicStampedReference.getStamp());
			System.out.println("T2 当前的最新值 =>"+atomicStampedReference.getReference());
		},"T2").start();
	}
}

26,Java锁

公平锁和非公平锁

公平锁:是指多个线程按照申请锁的顺序来获取锁,类似排队打饭,先来后到。

非公平锁:是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比现申请的线程优先获取锁,在高并发的情况下,有可能会造成优先级反转或者饥饿现象。

对于Synchronized而言,也是一种非公平锁。

可重入锁

可重入锁(也叫递归锁)

指的是在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。

也就是说,线程可以进入任何一个它已经拥有的锁,所同步着的代码块。 好比家里进入大门之后,就可以进入里面的房间了;

ReentrantLock、Synchronized 就是一个典型的可重入锁;

可重入锁最大的作用就是避免死锁。

synchronized
package com.kuang;

/**
* 可重入锁(也叫递归锁)
* 指的是同一线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码
* 在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。
*/
public class ReentrantLockDemo {
    public static void main(String[] args) throws Exception {
        Phone phone = new Phone();
        // T1 线程在外层获取锁时,也会自动获取里面的锁
        new Thread(()->{
            phone.sendSMS();
        },"T1").start();
        new Thread(()->{
            phone.sendSMS();
        },"T2").start();
    }
}

class Phone{

	public synchronized void sendSMS(){
        System.out.println(Thread.currentThread().getName()+" sendSMS");
        //两个synchronized锁
		sendEmail();
	}

	public synchronized void sendEmail(){
		System.out.println(Thread.currentThread().getName()+" sendEmail");
	}
}
ReentrantLock
package com.kuang;

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

/**
* 可重入锁(也叫递归锁)
* 指的是同一线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码
* 在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。
*/
public class ReentrantLockDemo {
	public static void main(String[] args) throws Exception {
        Phone phone = new Phone();
        // T1 线程在外层获取锁时,也会自动获取里面的锁
        new Thread(phone,"T1").start();
        new Thread(phone,"T2").start();
	}
}

class Phone implements Runnable{

	Lock lock = new ReentrantLock();
	@Override
	public void run() {
		get();
	}
	public void get(){
		lock.lock();
		// lock.lock(); 锁必须匹配,如果两个锁,只有一个解锁就会失败
		try {
			System.out.println(Thread.currentThread().getName()+" get()");
			//set方法里面也有锁
			set();
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			lock.unlock();
			// lock.lock();
		}
	}

	public void set(){
		lock.lock();
		try {
			System.out.println(Thread.currentThread().getName()+" set()");
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			lock.unlock();
		}
	}
}

自旋锁(spinlock)

是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。

源代码
unsafe.getAndAddInt()
public final int getAndAddInt(Object var1, long var2, int var4) {
	int var5;
	do {
		// 获取传入对象的地址
		var5 = this.getIntVolatile(var1, var2);
		// 比较并交换,如果var1,var2 还是原来的 var5,就执行内存偏移+1; var5 + var4
	} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
	return var5;
}
测试代码
package com.kuang;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

public class SpinLockDemo {
	// 原子引用线程, 没写参数,引用类型默认为null
	AtomicReference<Thread> atomicReference = new AtomicReference<>();

	//上锁
	public void myLock(){
		Thread thread = Thread.currentThread();
		System.out.println(Thread.currentThread().getName()+"==>mylock");
		// 自旋
		while (!atomicReference.compareAndSet(null,thread)){
 		}
	}

	//解锁
	public void myUnlock(){
		Thread thread = Thread.currentThread();
		atomicReference.compareAndSet(thread,null);
		System.out.println(Thread.currentThread().getName()+"==>myUnlock");
	}

	// 测试
	public static void main(String[] args) {
		SpinLockDemo spinLockDemo = new SpinLockDemo();
		new Thread(()->{
			spinLockDemo.myLock();
			try {
				TimeUnit.SECONDS.sleep(5);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			spinLockDemo.myUnlock();
		},"T1").start();
        
		try {
			TimeUnit.SECONDS.sleep(1);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}

		new Thread(()->{
			spinLockDemo.myLock();
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
			spinLockDemo.myUnlock();
		},"T2").start();
	}
}

死锁

死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那它们都将无法推进下去,如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否者就会因为争夺有限的资源而陷入死锁。

产生死锁主要原因

1、系统资源不足

2、进程运行推进的顺序不合适

3、资源分配不当

测试
import java.util.concurrent.TimeUnit;

public class DeadLockDemo {

	public static void main(String[] args) {
		String lockA = "lockA";
		String lockB = "lockB";
		new Thread(new HoldLockThread(lockA,lockB),"T1").start();
		new Thread(new HoldLockThread(lockB,lockA),"T2").start();
	}
}

class HoldLockThread implements Runnable{

	private String lockA;
	private String lockB;
	public HoldLockThread(String lockA, String lockB) {
        this.lockA = lockA;
        this.lockB = lockB;
	}
	public void run() {
		synchronized (lockA){
			System.out.println(Thread.currentThread().getName()+"lock:"+lockA+"=>get"+lockB);
			try {
				TimeUnit.SECONDS.sleep(2);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			synchronized (lockB){
				System.out.println(Thread.currentThread().getName()+"lock:"+lockB+"=>get"+lockA);
  			}
		}
	}
}
解决方法

拓展java自带工具操作:

  1. 查看JDK目录的bin目录
  2. 使用 jps -l 命令定位进程号
  3. 使用 jstack 进程号 找到死锁查看

结果:显示两个线程互相等待对方的锁。

Java stack information for the threads listed above:
===================================================
"T2":
	at com.kuang.HoldLockThread.run(DeadLockDemo.java:43)
	- waiting to lock <0x00000000d5b87298> (a java.lang.String)
	- locked <0x00000000d5b872d0> (a java.lang.String)
	at java.lang.Thread.run(Thread.java:748)

"T1":
	at com.kuang.HoldLockThread.run(DeadLockDemo.java:43)
	- waiting to lock <0x00000000d5b872d0> (a java.lang.String)
	- locked <0x00000000d5b87298> (a java.lang.String)
	at java.lang.Thread.run(Thread.java:748)

Found 1 deadlock.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值