JUC多线程进阶

回顾

传统的synchronized:

package com.JUC;

public class test1 {
    public static void main(String[] args) {
        //并发,很多用户去操作(修改,删除)同一个资源类
        Ticket ticket = new Ticket(); //资源类
        //lambda表达式,jdk1.8新特性
        new Thread(()-> {
            for (int i = 0; i < 50; i++) {
                ticket.sale();
            }
        },"A").start();

        new Thread(()-> {
            for (int i = 0; i < 50; i++) {
                ticket.sale();
            }
        },"B").start();

        new Thread(()-> {
            for (int i = 0; i < 50; i++) {
                ticket.sale();
            }
        },"C").start();

    }
}
class Ticket{
    //资源类
    private int number = 50;

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

如果不在资源类中添加同步synchronized,数据不正常。

我们要添加synchronized同步,才会数据正常。

Lock锁

需要加锁和解锁。

以下是实现三种锁的三种方式。

三种锁的实现类

可重入锁是经常使用的。

我们进入ReentrantLock的源码中。

公平锁与非公平锁

公平锁:十分公平,一定要排队!

非公平锁:不公平,可以插队的。默认是使用非公平锁。

原因就是:耗时少就优先调用。

package com.JUC;

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

public class test2 {
    public static void main(String[] args) {
        //并发,很多用户去操作(修改,删除)同一个资源类
        Ticket ticket = new Ticket(); //资源类
        //lambda表达式,jdk1.8新特性
        new Thread(()-> {
            for (int i = 0; i < 50; i++) {
                ticket.sale();
            }
        },"A").start();

        new Thread(()-> {
            for (int i = 0; i < 50; i++) {
                ticket.sale();
            }
        },"B").start();

        new Thread(()-> {
            for (int i = 0; i < 50; i++) {
                ticket.sale();
            }
        },"C").start();

    }
}

//lock三部曲:创建对象,加锁,解锁
class Ticket2{
    //资源类
    private int number = 50;

    Lock lock = new ReentrantLock();//传统的

    //卖票的方式
    public void sale(){
        lock.lock();
        try {
            if (number > 0){
                System.out.println(Thread.currentThread().getName()+"卖出了第"+number+"张票");
                number--;
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock(); //解锁
        }

    }
}

synchronized与lock锁区别

  • synchronized是一个内置的关键字,lock是一个java类!
  • synchronized是无法获取判断锁的状态,lock可以判断是否获取到了锁。
  • synchronized会自动释放锁,lock锁需要手动释放,否则会死锁!
  • synchronized线程阻塞,后面的线程就会等待。而lock锁可能不会等待。
  • synchronized可重入锁,不可中断,非公平。Lock可重入锁,可以判断锁,默认非公平。

什么是锁?如何判断锁的是谁?

消费者生产者问题

传统的synchronized
package com.JUC;

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

        ziYuan n = new ziYuan();

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

//资源类
class ziYuan{

    //资源数量
    private int num = 0;

    //生产者
    public synchronized void increment() throws InterruptedException {
        if (num!=0){
            //等待
            this.wait();
        }
        num++;//生产
        System.out.println(Thread.currentThread().getName()+"生产到"+num);
        this.notifyAll();//唤醒其他线程
    }

    //消费者
    public synchronized void decrement() throws InterruptedException {
        if (num==0){
            //等待
            this.wait();
        }
        num--;//消费
        System.out.println(Thread.currentThread().getName()+"消费到"+num);
        this.notifyAll();//唤醒其他线程
    }
}

但是通过这种方法,两种线程还可以,但是到了三种,四种或更多的时候,线程就出现了问题。怎么来解决?

我们来看官方文档的解释,所以我们要使用推荐的while来进行判断。

虚假唤醒官方文档

防止进行虚假唤醒的方式:

package com.JUC;

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

        ziYuan n = new ziYuan();

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

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

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

//资源类
class ziYuan{

    //资源数量
    private int num = 0;

    //生产者
    public synchronized void increment() throws InterruptedException {
        while (num!=0){
            //等待
            this.wait();
        }
        num++;//生产
        System.out.println(Thread.currentThread().getName()+"生产到"+num);
        this.notifyAll();//唤醒其他线程
    }

    //消费者
    public synchronized void decrement() throws InterruptedException {
        while(num==0){
            //等待
            this.wait();
        }
        num--;//消费
        System.out.println(Thread.currentThread().getName()+"消费到"+num);
        this.notifyAll();//唤醒其他线程
    }
}
JUC版

使用到了lock,并且线程的唤醒和等待不适用了之前的方法,而是juc中的

Condition condition = lock.newCondition();//取代了之前的对象监视器方法的使用。

对比图:

两种同步的唤醒与等待

第一种是之前的同步使用的,后面是JUC使用的。

package com.JUC;

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

//JUC版的生产者消费者
public class PC2 {
    public static void main(String[] args) {

        Data n = new Data();

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

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

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    n.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"D").start();
    }
}
class Data{
    //资源数量
    private int num = 0;

    Lock lock = new ReentrantLock();
    Condition condition = lock.newCondition();//取代了之前的对象监视器方法的使用

    //生产者
    public void increment() throws InterruptedException {
        lock.lock();
        try{
            while (num!=0){
                //等待
                condition.await();  //等待
            }
            num++;//生产
            System.out.println(Thread.currentThread().getName()+"生产到"+num);
            condition.signalAll(); //唤醒全部
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
    //消费者
    public void decrement() throws InterruptedException {
        lock.lock();
        try{
            while(num==0){
                //等待
                condition.await();
            }
            num--;//消费
            System.out.println(Thread.currentThread().getName()+"消费到"+num);
            condition.signalAll(); //唤醒全部
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();//解锁
        }
    }
}

注意步骤:

  • 加锁
  • 等待
  • 消费
  • 唤醒其他线程
  • 解锁(在finally中必须执行)
生产者消费者

以上是JUC版的消费者生产者问题。

问题来了:怎么实现A-->B-->C-->D,这样有顺序的调用?

package com.JUC;

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

public class PC3 {
    public static void main(String[] args) {
        Data3 data = new Data3();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                data.A();
            }
        },"A").start();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                data.B();
            }
        },"B").start();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                data.C();
            }
        },"C").start();
    }
}

class Data3{
    private Lock lock = new ReentrantLock();
    Condition condition1 = lock.newCondition();//这里我们可以使用多个监视器
    Condition condition2 = lock.newCondition();
    Condition condition3 = lock.newCondition();

    private int number = 1;//1A,2B,3C


    public void A(){
        lock.lock();
        try {
            while (number!=1){
                //等待
                condition1.await();
            }
            System.out.println(Thread.currentThread().getName()+"   AAA");
            number = 2;
            condition2.signal(); // 唤醒线程2
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

    public void B(){
        lock.lock();
        try {
            while (number!=2){
                //等待
                condition2.await(); //线程2休息
            }
            System.out.println(Thread.currentThread().getName()+"   BBB");
            number = 3;
            condition3.signal(); // 唤醒线程3
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

    public void C(){
        lock.lock();
        try {
            while (number!=3){
                //等待
                condition3.await();
            }
            System.out.println(Thread.currentThread().getName()+"   CCC");
            number=1;
            condition1.signal();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
}

以上方法就是我们使用多个condition进行的精确唤醒。

八锁现象彻底解决锁

到底什么是锁?锁的是什么?

答案:锁的是一个对象,或者是一个Clsss。

案例一:

package com.JUC;

import java.util.concurrent.TimeUnit;

public class Lock8 {
    public static void main(String[] args) throws InterruptedException {
        Phone phone = new Phone();
        new Thread(()->{
            try {
                phone.sendMsg();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"A").start();

        //休息一秒
        TimeUnit.SECONDS.sleep(1);

        new Thread(()->{
            phone.call();
        },"B").start();
    }
}
class Phone{
    //发信息
    public synchronized void sendMsg() throws InterruptedException {
        TimeUnit.SECONDS.sleep(2);
        System.out.println("发信息");
    }

    //打电话
    public synchronized void call(){
        System.out.println("打电话");
    }
}
八锁问题

为什么发信息延迟2s,还是先发信息?

因为发信息和打电话操作的是一个锁,锁的对象都是同一个phone,无论发信息多长时间,后面的打电话也必须等着。谁先拿到,谁执行。

案例二:

在phone中加入一个hello普通方法,谁先执行?

package com.JUC;

import java.util.concurrent.TimeUnit;

public class Lock8two {
    public static void main(String[] args) throws InterruptedException {
        Phone2 phone = new Phone2();
        new Thread(()->{
            try {
                phone.sendMsg();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"A").start();

        //休息一秒
        TimeUnit.SECONDS.sleep(1);

        new Thread(()->{
            phone.hello();
        },"B").start();
    }
}
class Phone2{
    //发信息
    public synchronized void sendMsg() throws InterruptedException {
        TimeUnit.SECONDS.sleep(2);
        System.out.println("发信息");
    }

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

    //hello
    public void hello(){
        System.out.println("hello");
    }
}

先执行hello

因为hello是一个普通方法,根本没有锁,不涉及锁的影响,可以想称hello这个方法根本不去排队。

案例三:

分成两个对象进行打电话或是发短信。

package com.JUC;

import java.util.concurrent.TimeUnit;

public class Lock8Three {
    public static void main(String[] args) throws InterruptedException {
        Phone3 phone = new Phone3();
        Phone3 phone2 = new Phone3();
        new Thread(()->{
            try {
                phone.sendMsg();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"A").start();

        //休息一秒
        TimeUnit.SECONDS.sleep(1);

        new Thread(()->{
            phone2.call();
        },"B").start();
    }
}
class Phone3{
    //发信息
    public synchronized void sendMsg() throws InterruptedException {
        TimeUnit.SECONDS.sleep(2); //注意这里是休息了两秒
        System.out.println("发信息");
    }

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

由于这是两个不同的对象,所以发短信不会对打电话进行锁,发短信线程休息了2s,打电话肯定比发信息更快。

案例四:

静态方法。

package com.JUC;

import java.util.concurrent.TimeUnit;

public class Lock8Four {
    public static void main(String[] args) throws InterruptedException {
        Phone4 phone = new Phone4();
        new Thread(()->{
            try {
                phone.sendMsg();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"A").start();

        //休息一秒
        TimeUnit.SECONDS.sleep(1);

        new Thread(()->{
            phone.call();
        },"B").start();
    }
}
class Phone4{
    //发信息
    public static synchronized void sendMsg() throws InterruptedException {
        TimeUnit.SECONDS.sleep(2);
        System.out.println("发信息");
    }

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

这里因为方法是静态的!两个方法的调用者(Class)也是一样的,所以,还是发信息在前!

static就会让方法在类一加载就有了。全局只有一个Class!

案例五:

创建两个对象分别调用打电话和发信息,这两个方法都是静态的。

package com.JUC;

import java.util.concurrent.TimeUnit;

public class Lock8Five {
    public static void main(String[] args) throws InterruptedException {
        Phone3 phone = new Phone3();
        Phone3 phone2 = new Phone3();
        new Thread(()->{
            try {
                phone.sendMsg();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"A").start();

        //休息一秒
        TimeUnit.SECONDS.sleep(1);

        new Thread(()->{
            phone2.call();
        },"B").start();
    }
}
class Phone5{
    //发信息
    public static synchronized void sendMsg() throws InterruptedException {
        TimeUnit.SECONDS.sleep(2);
        System.out.println("发信息");
    }

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

结果是发信息在前,为什么?

因为两个方法是静态的,锁的就不是对象了,而是同一个Class!打电话必须等待发信息。

案例六:

两个方法,一个是静态的,一个是非静态,一个对象去调用,谁想被调用?

package com.JUC;

import java.util.concurrent.TimeUnit;

public class Lock8Six {
    public static void main(String[] args) throws InterruptedException {
        Phone6 phone = new Phone6();
        new Thread(()->{
            try {
                phone.sendMsg();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"A").start();

        //休息一秒
        TimeUnit.SECONDS.sleep(1);

        new Thread(()->{
            phone.call();
        },"B").start();
    }
}
class Phone6{
    //发信息
    public static synchronized void sendMsg() throws InterruptedException {
        TimeUnit.SECONDS.sleep(2);
        System.out.println("发信息");
    }

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

结果是打电话在前,因为打电话和发信息这时候不是一个锁,打电话锁的是对象,而发信息锁的是Class!

小结:

new出来的对象,是同一个对象的是同一把锁。

static是全局同一把锁,Class是唯一的模板。

集合类不安全

CopyOnWriteArrayList

为什么ArrayList不安全?

是两个线程同时对一个ArrayList进行增加或删除,就可能抛异常。比如一个ArrayList里的内容被两个线程同时删除,两个线程有可能会想要删除同一个内容,一个线程先完成的时候第二个线程再删,就找不到这个内容了。

案例

package com.JUC.unsafe;

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

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(list);
            },String.valueOf(i)).start();
        }
    }
}

我们发现,使用ArrayList进行多线程添加,报错了!

ArrayList不安全_进行添加

采用线程安全的CopyOnWriteArrayList

package com.JUC.unsafe;

import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;

public class ListTest {
    public static void main(String[] args){
        List<String> list = new CopyOnWriteArrayList<>();
        //三种方法可以使线程安全
        /*
        1、使用List<String> list = new Vector<>();,这个底层使用的是synchronized
        2、使用List<String> list = Collections.synchronizedList(new ArrayList<>());
        3、使用List<String> list = new CopyOnWriteArrayList<>();底层是lock锁,采用读写分离
         */

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

CopyOnWriteArrayList原理?

https://www.cnblogs.com/chengxiao/p/6881974.html

​ 我们都知道,集合框架中的ArrayList是非线程安全的,Vector虽是线程安全的,但由于简单粗暴的锁同步机

制,性能较差。而CopyOnWriteArrayList则提供了另一种不同的并发处理策略(当然是针对特定的并发场景)。

很多时候,我们的系统应对的都是读多写少的并发场景。CopyOnWriteArrayList容器允许并发读,读操作是

无锁的,性能较高。至于写操作,比如向容器中添加一个元素,则首先将当前容器复制一份,然后在新副本上执行

写操作,结束之后再将原容器的引用指向新容器。

CopyOnWriteArraySet

我们使用基本的Hashmap跑三十条线程进行添加

package com.JUC.unsafe;

import java.util.HashSet;
import java.util.Set;
import java.util.UUID;

public class SetTest {
    public static void main(String[] args) {
        Set<String> set = new HashSet<>();
        for (int i = 0; i < 30; i++) {
            new Thread(()->{
                set.add(UUID.randomUUID().toString().substring(0,6));
                System.out.println(set);
            },String.valueOf(i)).start();
        }
    }
}
hashset不安全

可以通过控制台可以发现,出现了异常。

对比CopyOnWriteArrayList,这里的set也有安全的实现类CopyOnWriteHashSet

package com.JUC.unsafe;

import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArraySet;

public class SetTest {
    public static void main(String[] args) {
//        Set<String> set = new HashSet<>(); 不安全
//        Set<String> set = Collections.synchronizedSet(new HashSet<>());这种方法也可以,使用的是synchronized
                Set<String> set = new CopyOnWriteArraySet<>();
        for (int i = 0; i < 30; i++) {
            new Thread(()->{
                set.add(UUID.randomUUID().toString().substring(0,6));
                System.out.println(set);
            },String.valueOf(i)).start();
        }
    }
}

使用CopyOnWriteArraySet安全(截图上写错了)

ConcurrentHashMap

HashMap同ArrayList,HashSet一样,都是不安全的。那么用什么来代替?

CopyOnWriteHashMap?不对,不存在这个类型,是ConcorrentHashMap。

package com.JUC.unsafe;

import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

public class HashMapTest {
    public static void main(String[] args) {
//        Map<String,Object> map = new HashMap<>();
        Map<String,Object> map = new ConcurrentHashMap<>();
        for (int i = 0; i < 20; i++) {
            new Thread(()->{
                map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0,5));
                System.out.println(map);
            }).start();
        }
    }
}

Callable

什么是Callable?

与Runnable类似,他们都是为了其实例可能由另外一个线程执行类设计的,但是用三点不同。

  • Callable可以有返回值,但是Runnable没有返回值。
  • Callable可以抛出异常
  • 与Runnable的方法不同,Callable是call(),Runnable是run();
package com.JUC.Callable;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class CallableTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask futureTask = new FutureTask(new A());
        new Thread(futureTask).start();
        //这里启动Callable的方式:
        /*
        1、由于Thread中的参数只由Runnable可以执行线程,所以我们要想Callable怎么执行多线程
        2、我们知道FutureTask是Runnable的实现类
        3、我们还发现Runnable中可以写参数Callable,所以我们把Callable放入FutureTask中可以执行多线程
         */

        Integer number = (Integer)futureTask.get(); //用get获取Callable返回的值
        System.out.println(number);
    }
}

class A implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        System.out.println("这里执行了call方法");
        return 1234;
    }
}

常用辅助类

CountDownLatch

相当于一个计数器的作用,这里必须执行完计数器所给的次数,才向下执行!

package com.JUC.fuzhulei;

import java.util.concurrent.CountDownLatch;

public class CountDownLatchTest {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch c = new CountDownLatch(5);//给定计数器的次数为5
        for (int i = 0; i < 5; i++) {
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+"执行");
                c.countDown(); //注意要写在线程内,计数器次数-1
            },String.valueOf(i)).start();
        }
        c.await();//等待计数器归零才继续向下操作
        System.out.println("结束!");
    }
}

CyclicBarrier

与CountDownLatch性质一样,不过这是一个加法计数器!并且可以在完成次数后,再执行一个自己规定的线程体。

package com.JUC.fuzhulei;

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierTest {
    public static void main(String[] args) {
        CyclicBarrier cyclicBarrier = new CyclicBarrier(5,()->{
            System.out.println("完成了5次在才可以执行本线程!");
        });

        for (int i = 0; i < 5; i++) {
            final int m = i;//注意底下的线程无法直接使用i
            new Thread(()->{
                System.out.println("执行了第"+m+"次");
                try {
                    cyclicBarrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

加法计数器

Semaphore

信号量

package com.JUC.fuzhulei;

import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

public class SemaphoreTest {
    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(1);//可以存放线程的数量

        for (int i = 0; i < 6; i++) {
            new Thread(()->{
                try {
                    semaphore.acquire();//进入
                    System.out.println(Thread.currentThread().getName()+"获得");
                    TimeUnit.SECONDS.sleep(2);
                    System.out.println(Thread.currentThread().getName()+"离开");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    //释放
                    semaphore.release();
                }

            },String.valueOf(i)).start();
        }
    }
}

读写锁

思想:读写分离,写的时候只能一个人写,读的话可以很多线程去读。实现类只有一个ReentrantReadWriteLock

读写锁
package com.JUC.test;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteLockTest {
    public static void main(String[] args) {
        MyCacheLock cache = new MyCacheLock();
        //这5个线程进行写操作
        for (int i = 0; i < 5; i++) {
            final int temp = i;
            new Thread(()->{
                cache.put(temp+"",temp);
            },String.valueOf(i)).start();
        }

        //这5个线程进行读操作
        for (int i = 0; i < 5; i++) {
            final  int temp = i;
            new Thread(()->{
                cache.read(temp+"");
            },String.valueOf(i)).start();
        }
    }
}

class MyCacheLock{
    private volatile Map<String,Object> map = new HashMap<>();
    //读写锁
    private ReadWriteLock lock = new ReentrantReadWriteLock();

    //写操作
    public void put(String key,Object value){
        try {
            lock.writeLock().lock();
            System.out.println(Thread.currentThread().getName()+"写入"+key);
            map.put(key,value);
            System.out.println(Thread.currentThread().getName()+"写入"+key+"OK");
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.writeLock().unlock();
        }

    }

    //读操作
    public void read(String key){
        try {
            lock.readLock().lock();
            System.out.println("读"+key);
            map.get(key);
            System.out.println("读"+key+"OK");
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.readLock().unlock();
        }

    }
}
读写锁的写入和读

阻塞队列

阻塞队列的关系

四组API

功能 抛出异常 有返回值,不抛出异常 会阻塞等待 超时等待,等待时间自己定
添加 add() offer() put() offer(,,)带三参数
移除 remove() poll() take() poll(,)带二参数
检查队首的元素 element() peek - -
BlockingQueue blockingQueue = new ArrayBlockingQueue(3);//代表队列中能放三个数据

blockingQueue.add("a");
blockingQueue.add("b");
blockingQueue.add("c");
blockingQueue.add("c");//由于只能放三个数据,这里add会报异常

blockingQueue.remove();
blockingQueue.remove();
blockingQueue.remove();
blockingQueue.remove();//由于只有三个数据,所以这里remove也会报异常
    
//第二组api
blockingQueue.offer("a");
blockingQueue.offer("b");
blockingQueue.offer("c");
blockingQueue.offer("d");//这里不会报异常,输出显示flase

blockingQueue.poll();
blockingQueue.poll();
blockingQueue.poll();
blockingQueue.poll();//这里输出null

//第三组api
blockingQueue.put("1");
blockingQueue.put("2");
blockingQueue.put("3");
blockingQueue.put("4");//这个程序会一直阻塞等待下去

blockingQueue.take();
blockingQueue.take();
blockingQueue.take();
blockingQueue.take();//会阻塞等待下去

//第四组API
blockingQueue.offer("a",2, TimeUnit.SECONDS);//对应3个参数:存入的值,等待时间,时间单位
blockingQueue.offer("b",2, TimeUnit.SECONDS);
blockingQueue.offer("c",2, TimeUnit.SECONDS);
blockingQueue.offer("d",2, TimeUnit.SECONDS);//这里等待2秒后,存不进去就不会等待了

blockingQueue.poll(2,TimeUnit.SECONDS);
blockingQueue.poll(2,TimeUnit.SECONDS);
blockingQueue.poll(2,TimeUnit.SECONDS);
blockingQueue.poll(2,TimeUnit.SECONDS);//等待2s,取不到值就结束

同步队列

这个同步队列只能存一个容量的数据。

package com.JUC.test;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.SynchronousQueue;

public class SynchronousQueueTest {
    public static void main(String[] args) {
        BlockingQueue<String> blockingQueue = new SynchronousQueue<>();
        new Thread(()->{
            try {
                System.out.println(Thread.currentThread().getName()+"存1");
                blockingQueue.put("1");
                System.out.println(Thread.currentThread().getName()+"存2");
                blockingQueue.put("2");
                System.out.println(Thread.currentThread().getName()+"存3");
                blockingQueue.put("3");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"A").start();

        new Thread(()->{
            try {
                System.out.println(blockingQueue.take());
                System.out.println(blockingQueue.take());
                System.out.println(blockingQueue.take());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"B").start();
    }
}

跑两个线程,A存,B取,只有B取完A中存的那个数,A才能继续存下一个数。

同步队列_只能存取一个

线程池(重点)

池化技术

线程池、连接池、内存池、对象池。。创建,销毁十分浪费资源。

池化技术:事先准备好一些资源,有人要用的话,就来池中拿,用完之后再放回来。

线程池的优点

1、降低资源的消耗

2、提高响应的速度

3、方便管理

线程复用,可以控制最大并发数,管理线程。

线程池:三大方法

(阿里巴巴规范手册强烈不建议使用,会造成oom栈溢出,原因在底下源码中)

package com.JUC.pool;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Demo1 {
    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newSingleThreadExecutor();//只有一个线程
//        Executors.newFixedThreadPool(5);//最大5个线程
//        Executors.newCachedThreadPool();

        try{
            for (int i = 0; i < 10; i++) {
                threadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName()+" OK");
                });
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            threadPool.shutdown();
        }

    }
}

方法1:线程池中只有一个线程Executors.newSingleThreadExecutor()

线程池中只有一个线程

方法二:线程池中有5个线程

线程池中含有5个线程

方法三:线程池中的线程不固定,遇强则强,遇弱则弱

自定义线程池(重点!!!)

七大参数

我们先看一下上述三个方法的底层源码:

//方法1
public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }
//方法2
public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}
//方法3
public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,//这里,使用了最大的线程,有可能早出oom
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

通过上面三个方法我们都看是来自ThreadPoolExecutor的。

//通过寻找,我们可以找到这个底层的方法,这就是7大参数!
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;
 }

举银行办业务的例子:

在银行办理业务,有5个窗口,但是只营业了两个窗口,有2个人正在办理业务,有3个人在候客区等待办理业务,如果人又来了很多,就会把三个关闭的窗口打开进行营业。如果5个营业口全部打开,又来了人办理业务,且这时候候客区也是慢的,银行就开始想办法拒绝人进来。再然后,等待一段时间,如果在一段时间内都没有业务,就重新把三个关闭的窗口再关闭。

自定义线程池的银行例子
//自定义线程池
package com.JUC.pool;

import java.util.concurrent.*;

public class MyPool {
    public static void main(String[] args) {
        ExecutorService myPool = new ThreadPoolExecutor(
                2,//正场开启营业的窗口
                5,//最大办理业务的窗口
                2,//等待时间关闭不营业的三个窗口
                TimeUnit.SECONDS,//时间单位
                new ArrayBlockingQueue<>(3),//侯客区放3个人
                Executors.defaultThreadFactory(),//创建线程的工程,一般不会变化
                new ThreadPoolExecutor.AbortPolicy()//默认的拒绝策略,银行满了,还有人进来就不处理了还报异常
        );

        try{
            //由于我们的拒绝策略,所以大于8就报异常!最大开启的窗口+侯客区
            for (int i = 0; i < 8; i++) {
                myPool.execute(()->{
                    System.out.println(Thread.currentThread().getName()+" OK");
                });
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            myPool.shutdown();
        }
    }
}

如何定义我们最大办理业务窗口?也就是说如何定义最大线程?

1、cpu密集型

获取你电脑的最大核心数,可以保持cpu效率最高!

System.out.println("最大线程数"+Runtime.getRuntime().availableProcessors());
cpu密集型获取当前服务器的最大核心数

2、IO密集型

判断你程序中十分耗I/O的线程,只要大于这个耗I/o的线程就可以了。

四种拒绝策略

四种拒绝策略
  • AbortPolicy //不处理,抛异常
    
  • CallerRunsPolicy//哪里来的哪里去,就是说这条线程不处理,让main线程处理
    
  • DiscardOldestPolicy//队列满了,不抛异常不处理
    
  • DiscardPolicy//尝试使用第一条线程的资源,竞争,如果没竞争到就不处理也不跑异常
    

四大函数式接口

新时代的程序员:lambda表达式、链式编程、函数式接口和Stream流式计算。

函数式接口:只有一个方法的接口

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

//这就是函数式接口,一个接口中只有一个方法
//可以简化编程模型,在新版本框架底层中大量应用
四大函数式接口

函数型接口

源码:

@FunctionalInterface
public interface Function<T, R> {

    /**
     * Applies this function to the given argument.
     *
     * @param t the function argument
     * @return the function result
     */
    R apply(T t);
函数型接口
package com.JUC.test;

import java.util.function.Function;

public class FunctionTest {
    public static void main(String[] args) {
       Function function = new Function<String, Integer>(){
           @Override
           public Integer apply(String s) {
               return 123;
           }
       };
        
        Function<String,Integer> function = (string)->{return 123;};//使用Lambda简化

        System.out.println(function.apply("a"));
    }
}
//有一个输入参数,有一个输出

断定性接口

源码:

@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
    //输入一个值,返回布尔类型
断定性接口
package com.JUC.test;

import java.util.function.Predicate;

public class PredicateTest {
    public static void main(String[] args) {
//        Predicate<String> predicate = new Predicate<String>() {
//            @Override
//            public boolean test(String i) {
//                if (i!=null){
//                    return false;
//                }return true;
//            }
//        };
        //简化
        Predicate<String> predicate = (i)->{
            if (i!=null)
            return false;
            return true;
        };
        System.out.println(predicate.test("q"));
    }
}

消费者型接口

@FunctionalInterface
public interface Consumer<T> {

    void accept(T t);
消费型接口
package com.JUC.test;

import java.util.function.Consumer;

public class ConsumerTest {
    public static void main(String[] args) {
        Consumer<String> consumer = new Consumer<String>() {
            @Override
            public void accept(String o) {
                System.out.println("这是消费型接口");
            }
        };

        Consumer<String> consumer1 = (o)->{
            System.out.println("简化消费型接口!");
        };
        consumer.accept("1");
        consumer1.accept("2");

    }
}

供给型接口

@FunctionalInterface
public interface Supplier<T> {

    /**
     * Gets a result.
     *
     * @return a result
     */
    T get();
}
供给型接口
package com.JUC.test;

import java.util.function.Supplier;

public class SupplierTest {
    public static void main(String[] args) {
        Supplier<Integer> supplier = new Supplier<Integer>() {
            @Override
            public Integer get() {
                return 1;
            }
        };
        Supplier<Integer> supplier1 = ()->{
            return 1;
        };//用Lambda简化后的

        System.out.println(supplier.get());
        System.out.println(supplier1.get());
    }
}

Stream流式计算

什么是stream流?

mysql、集合本质都是存储东西的。

计算应该交给stream流来进行!

案例:

package com.JUC.stream;

import com.JUC.Student;

import java.util.Arrays;
import java.util.List;

/*
现在有5个用户,进行筛选。
1、ID必须是偶数
2、年龄必须大于23岁
3、用户名转换成大写
4、只输出一个用户
 */

public class StreamTest {
    public static void main(String[] args) {
        Student s1 = new Student(1,"a",21);
        Student s2 = new Student(2,"b",22);
        Student s3 = new Student(3,"c",23);
        Student s4 = new Student(4,"d",24);
        Student s5 = new Student(6,"e",25);
        //存储到集合中
        List<Student> list = Arrays.asList(s1,s2,s3,s4,s5);
        //计算交给流
        list.stream().filter(u->{return u.getId()%2==0;})
                .filter(u->{return u.getAge()>22;})
                .map(u->{return u.getName().toUpperCase();})//转换成大写
                .sorted((ss1,ss2)->{return ss2.compareTo(ss1);})//用户名倒着排序
                .limit(1)//分页
                .forEach(System.out::println);
    }
}

Student类:

package com.JUC;

import lombok.Data;

@Data
public class Student {
    private int id;
    private String name;
    private int age;

    public Student(int id,String name,int age){
        this.id = id;
        this.name = name;
        this.age = age;
    }
}

ForkJoin

介绍:分支合并。jdk1.7出来的,并行执行任务,效率块,把大任务变成小人物,让不同的线程去解决。

特点:工作窃取

工作窃取举例

线程A和B,当B快速解决了本来线程的分配的任务后,如果A线程还没有解决,就会帮助A线程完成任务。因为是双端队列,可以窃取A的任务。

ForJoin案例

这种思想和递归一样,把一个计算任务划分到不同的线程中。

package com.JUC.test;

import java.util.concurrent.RecursiveTask;

public class ForJoinTest extends RecursiveTask<Long> {
    private Long start;
    private Long end;

    //临界值
    private Long Temp = 10000L;

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

    @Override
    public Long compute(){
        if((end-start)>Temp){
            Long sum = 0L;
            for (Long i = start; i <= end; i++) {
                sum = sum + i;
            }
            return sum;
        }else{//forkJoin
            Long middle = (start+end) / 2; //中间值
            ForJoinTest task1 = new ForJoinTest(start,middle);
            task1.fork(); //把任务压入线程队列
            ForJoinTest task2 = new ForJoinTest(middle,end);
            task2.fork(); //把任务压入线程队列
            return task1.join()+task2.join();
        }
    }
}

三种方法分别计算:

package com.JUC.test;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.stream.LongStream;

public class ForkJoinTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        test1();
        test2();
        test3();
    }
    public static void test1(){
        //普通计算 时间11493
        Long sum = 0L;
        long start = System.currentTimeMillis();
        for (Long i = 1L; i <= 10_0000_0000; i++) {
            sum = sum +i;
        }
        long end = System.currentTimeMillis();
        System.out.println("时间"+(end-start)+"   答案"+sum);
    }
    public static void test2() throws ExecutionException, InterruptedException {
        //使用ForkJoin进行计算 8801
        long start = System.currentTimeMillis();
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        ForkJoinTask<Long> task = new ForJoinDemon(0L,10_0000_0000L);
        ForkJoinTask<Long> submit = forkJoinPool.submit(task);
        Long sum = submit.get();
        long end = System.currentTimeMillis();
        System.out.println("时间"+(end-start)+"   答案"+sum);
    }

    public static void test3(){
        //使用并行流进行计算
        long start = System.currentTimeMillis();
        long sum = LongStream.range(0L,10_0000_0000L).parallel().reduce(0,Long::sum);
        long end = System.currentTimeMillis();
        System.out.println("时间"+(end-start)+"   答案"+sum);
    }
}

三种计算对比时间结果

但是!

以上是测试特别大的数据的结果,如果我们把数调小。

结果会和上面相反。

异步回调

Future设计初衷:对将来某个事件的结果进行建模

对比Ajax,他的设计和ajax思想是一样的,只不过Ajax是客户端与服务器之间的异步请求,这里是线程之间的异步。


异步回调关系

异步回调分两种情况:带返回值和不带返回值。

package com.JUC.test;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

public class asyncTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //没有返回值的异步回调
        CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(()->{
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"runAsync=>void");
        });
        System.out.println("1111");
        System.out.println(Thread.currentThread().getName()+"=>主线程");
        completableFuture.get();//获取执行结果

        System.out.println("----------------------");

        //有返回值的异步回调
        CompletableFuture<Integer> completableFuture2 = CompletableFuture.supplyAsync(()->{
            System.out.println(Thread.currentThread().getName()+"=>supplyAsync!");
            int i=10/0;
            return 1024;
        });

        System.out.println(Thread.currentThread().getName()+"=>主线程");
        System.out.println(completableFuture2.whenComplete((t,u)->{
            System.out.println("t=>"+t);// 如果正常执行,t输出正常结果
            System.out.println("u=>"+u);// 有异常,这里就输出异常!
        }).exceptionally(e->{
            System.out.println(e.getMessage());
            return 500;// 异常返回结果,相当于状态码
        }).get());
    }
}

JMM

什么是JMM?

Java内存模型,不存在的东西,概念!

java内存模型
  • lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。
  • unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
  • read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
  • load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
  • use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
  • assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  • store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
  • write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。
不可见性的举例

结合上图的案例,定义有一个变量num,在主线程修改,但是另一个线程中以原来的值为条件进行死循环。(注意:主线程休眠了2s)

package com.JUC.test;

import java.util.concurrent.TimeUnit;

public class JMMTest {
    private static int num = 0;
    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            while (num==0){
            }
        }).start();//线程1

        TimeUnit.SECONDS.sleep(2);

        num = 1;
        System.out.println("可以停止了~~~");
    }
}
由于主线程对那个线程不可见造成while不停止

通过控制台我们发现,主线程修改了num,但是死循环的线程却没有停止,,这是为什么?

Volatile

这里为什么要引入Volatile?

目的就是解决上一个遗留下的问题。怎么让两个线程保证可见性?

就是使用Volatile。

Volatile是Java虚拟机提供的轻量级的同步机制,这里你就可以想成一个简化版的synchronized,但是它比synchronized效率高上很多!

==特点==

1、保证可见性

2、不保证原子性

3、禁止指令重排

由于关键字volatile保证可见性,那我们就加上这个关键字。

package com.JUC.test;

import java.util.concurrent.TimeUnit;

public class JMMTest {
    private volatile static int num = 0;
    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            while (num==0){
            }
        }).start();//线程1

        TimeUnit.SECONDS.sleep(2);

        num = 1;
        System.out.println("可以停止了~~~");
    }
}
加上关键字后含有可见性

通过控制台信息我们可以发现,main线程修改后的值,对于另一个线程可见!

检测volatile不保证原子性的案例:

package com.JUC.test;

public class volatileTest {
    private volatile static int num = 0;

    private static void add(){
        num++;//这个不是原子性操作
    }

    public static void main(String[] args) throws InterruptedException {

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

//        TimeUnit.SECONDS.sleep(1);
        while (Thread.activeCount()>2){
            //这里就是线程数大于2的时候,main线程进行礼让,先让其他线程跑完,自己再跑,或者也可以让主线程休眠2s,肯定有两个线程,gc线程和main线程
            Thread.yield();
        }

        System.out.println(num);
    }
}

看控制台可以发现,不够20000,所以说这个关键字不保证原子性。

不保证原子性

为什么volatile不保证原子性?

假设某一时刻i=5,此时有两个线程同时从主存中读取了i的值,那么此时两个线程保存的i的值都是5, 此时A线程对i进行了自增计算,然后B也对i进行自增计算,此时两条线程最后刷新回主存的i的值都是6(本来两条线程计算完应当是7)所以说volatile保证不了原子性。

那么不使用synchronized和lock,怎么才能解决这个问题?

有方法,使用原子类!

package com.JUC.test;

import java.util.concurrent.atomic.AtomicInteger;

public class volatileTest {
    private volatile static AtomicInteger num= new AtomicInteger();

    private static void add(){
        num.getAndIncrement();
    }
    public static void main(String[] args) throws InterruptedException {

        for (int i = 1; i <= 20; i++) {
            new Thread(()->{
                for (int i1 = 1; i1 <= 1000; i1++) {
                    add();
                }
            }).start();
        }
//        TimeUnit.SECONDS.sleep(1);
        while (Thread.activeCount()>2){
            //这里就是线程数大于2的时候,main线程进行礼让,先让其他线程跑完,自己再跑,或者也可以让主线程休眠2s
            Thread.yield();
        }
        System.out.println(num);
    }
}

这里的AtomicInteger就是一个原子类。

指令重排

volatile 可以避免指令重排

内存屏障( 内存屏障(memory barrier)是一个CPU指令 ),CPU指令 作用:

保证特定的操作执行顺序

可以保证某些变量的内存可见性(利用这些特性 volatile实现了禁止指令重排)

原理: 其实就是在使用volatile的前后都使用了内存屏障,保证了操作的执行顺序

就比如num++

很多线程进来的时候,由于指令重排,可以先分配空间,但是并没有执行+1操作,就这时被认为num已经被+1了,所以就会造成最后结果错误,那么就可以使用volatile,进行禁止指令重排。

单例模式

https://www.runoob.com/design-pattern/singleton-pattern.html

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

所谓单例模式,即一个类只有一个实例化对象。如果不希望一个类产生很多对象,就要使用单例设计模式。比如:使用打印机时,只需要一个打印机实例对象,多个打印机对象会造成内存浪费;windows任务管理器只能打开一个,多个任务管理器窗口是无意义的;windows回收站也只有一个…

单例模式的核心是构造方法的私有化(即在入口处限制了对象的实例化),之后在类的内部实例化对象,并通过静态方法返回实例化对象的引用。

饿汉式:

没有用的时候,就进行了加载,会造成浪费!

是否 Lazy 初始化:

是否多线程安全:

描述:这种方式比较常用,但容易产生垃圾对象。

优点:没有加锁,执行效率会提高。

缺点:类加载时就初始化,浪费内存。

public class Singleton {  
    private static Singleton instance = new Singleton();  
    private Singleton (){}  
    public static Singleton getInstance() {  
    return instance;  
    }  
}

懒汉式:

只有用的时候才去加载!

是否 Lazy 初始化:

是否多线程安全:

描述:这种方式具备很好的 lazy loading,能够在多线程中很好的工作,但是,效率很低,99% 情况下不需要同步。
优点:第一次调用才初始化,避免内存浪费。

缺点:必须加锁 synchronized 才能保证单例,但加锁会影响效率。

package com.JUC.danli;

//懒汉式单例
public class LazyMan {

    private LazyMan() {
        System.out.println(Thread.currentThread().getName() + "OK");
    }

    private volatile static LazyMan lazyMan;

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

    //通过测试,跑10条线程,发现不安全,不保证单例
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                getInstance();
            }).start();
        }
    }
}

懒汉式 DCL :Double Check Lock双重检测锁

* 双重检测:if(lazyman == null)
* 锁:对类加锁,synchronized (Lazyman.class)
* 缺陷:可能存在指令重排,导致高并发下不安全。
创建对象时,cpu运行情况:
 *     1. 分配对象的内存空间
 *     2. 执行构造方法初始化对象
 *     3. 设置实例对象指向刚分配的内存的地址, instance = 0xfffff;
 *        线程A执行顺序可能是:  1    3    2
 *        线程B:  lazyMan = null ;
 解决方案,对实例添加volatile,禁止指令重排
 破坏单例方案:反射
 所以,懒汉式DCL 也是不安全的
package com.JUC.danli;

//懒汉式单例
public class LazyMan {

    private LazyMan(){
        System.out.println(Thread.currentThread().getName()+"OK");
    }
    private volatile static LazyMan lazyMan;

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

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                getInstance();
            }).start();
        }
    }
}

使用反射机制进行破坏!

package com.JUC.danli;
import java.lang.reflect.Constructor;
//使用反射进行破解
public class LazyMan2 {

    private static volatile LazyMan2 lazyman;
    private LazyMan2(){
        System.out.println(Thread.currentThread().getName() + "创建了对象");
    }

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

    public static void main(String[] args)throws Exception {
        //1、多线程测试
        //mutilThreadTest();
        //2、使用反射破坏单例
        reflectBreakSingle();
    }

    private static void mutilThreadTest() throws NoSuchMethodException{
        for (int i = 0; i < 10000; i++) {
            new Thread(() ->{
                LazyMan2.getInstance();
            }).start();
        }
    }

    private static void reflectBreakSingle() throws Exception{
        //使用反射破坏
        Constructor<LazyMan2> declaredConstructor = LazyMan2.class.getDeclaredConstructor(null);//获取无参构造的方法
        declaredConstructor.setAccessible(true);//无视构造器的private关键字
        LazyMan2 lazyman1 = declaredConstructor.newInstance();
        LazyMan2 lazyman2 = declaredConstructor.newInstance();
        System.out.println(lazyman1);
        System.out.println(lazyman2);
    }
}

加上标志位,防止反射破坏。

package com.JUC.danli;

import java.lang.reflect.Constructor;

//使用标志位,防止反射创建对象
public class LazyMan3 {

    private static boolean flag = false;//设置标志位

    private LazyMan3(){
        synchronized (LazyMan3.class){
            if (flag == false){
                flag = true;
            }else {
                throw new RuntimeException("不要试图使用反射破解单例模式");
            }
        }
    }
    private volatile static LazyMan3 lazyman;

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

    public static void main(String[] args)throws Exception {
        //使用反射破坏
        Constructor<LazyMan3> declaredConstructor2 = LazyMan3.class.getDeclaredConstructor(null);
        declaredConstructor2.setAccessible(true);//无视构造器的private关键字
        LazyMan3 lazyman1 = declaredConstructor2.newInstance();
        LazyMan3 lazyman2 = declaredConstructor2.newInstance();
        System.out.println(lazyman1);
        System.out.println(lazyman2);
    }
}

但是还是不安全!还可以根据反射获取那个标志位!

那到底该怎么办?

枚举类:(最安全!)

枚举类已在源码中提示不能使用反射创建对象

到底怎么样创建才可以让我们的单例模式安全呢?

使用枚举类!我们知道枚举类使用的就是单例模式,在反射的newInstance中的源码中,明确标明了不能使用反射创建枚举对象!

package com.enumTest;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public enum EnumSingle {
    //1、定义枚举类对象INSTANCE
    INSTANCE;
    //2、定义私有的被实例化对象
    private EnumSingle instance;

    //4、编写公共静态获取方法,返回被实例化的类对象。
    public EnumSingle getInstance() {
        return this.instance;
    }
}
class Test{
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        //尝试使用反射区破解枚举
        EnumSingle enumSingle = EnumSingle.INSTANCE;
        Constructor<EnumSingle> enumSingleConstructor = EnumSingle.class.getDeclaredConstructor(null);//这里获取无参构造方法
        enumSingleConstructor.setAccessible(true);
        EnumSingle enumSingle1 = enumSingleConstructor.newInstance();
        System.out.println(enumSingle==enumSingle1);
    }
}

我们首先根据获取无参构造方法,反射暴力进行创建对象


提示没有找到那个方法

但是提示没有这样的方法!

我们通过javap -p 反编译我们的class文件

进行反编译

也是范县含有那个无参的构造方法!但是java运行报错说没有。

我们再通过jad工具查找

使用jad

发现有一个带两个参的无参构造方法。idea和javap都欺骗了我们(根本就没有无参构造方法)~~~

Constructor<EnumSingle> enumSingleConstructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);//我们把无参改成两个参数的
证明枚举类是安全性最高的单例

根据这个两个带参的构造方法,我们成功的被java提示:不能使用反射创建两个对象。

CAS

什么是CAS??

package com.JUC.CAS;

import java.util.concurrent.atomic.AtomicInteger;

//Java的CAS,CompareAndSet 比较并交换
public class Demon1 {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(1);
        //如果是期望的值,就进行交换
        System.out.println(atomicInteger.compareAndSet(1,10));
        System.out.println(atomicInteger.get());
        atomicInteger.getAndIncrement();//++操作

        System.out.println(atomicInteger.compareAndSet(2,20));//期望值不是2所以,返回false
        System.out.println(atomicInteger.get());
    }
}

unsafe类

Java本身无法操作内存,而unsafe可以
atomicInteger.getAndIncrement();//++操作
//自加1的操作对应的就是下面的源码,do while自旋锁。
初识自旋锁

CAS中ABA问题

ABA问题:狸猫换太子

两个线程进行CAS操作,初始值为1,A线程操作两次,期望值是1,修改为2,然后再次从2修改到1,AB线程同时接受到初始值。这时候B线程期望值也是1,想修改为3。这时候就会出现问题!不是我们想要的结果。

解决方法?

乐观锁!修改完成之后加上版本号。

package com.JUC.CAS;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicStampedReference;

public class ABA {
    public static void main(String[] args) {
        AtomicStampedReference<Integer> a = new AtomicStampedReference<>(10,1);
        new Thread(()->{
            int stamp = a.getStamp();//获取现在的版本号
            System.out.println("a1=》"+a.getStamp());
            try {
                TimeUnit.SECONDS.sleep(2);//这里休眠2s,确保此时B线程也拿到一样的版本号
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("a修改"+a.compareAndSet(10, 100, a.getStamp(), a.getStamp() + 1));
            System.out.println("a2=》"+a.getStamp());
            System.out.println("a修改"+a.compareAndSet(100, 10, a.getStamp(), a.getStamp() + 1));
            System.out.println("a3=》"+a.getStamp());
        },"a").start();

        new Thread(()->{
            int stamp = a.getStamp();//获取现在的版本号
            System.out.println("b1=》"+a.getStamp());
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("b修改"+a.compareAndSet(10, 101, a.getStamp(), a.getStamp() + 1));
            System.out.println("b2=》"+a.getStamp());
        },"b").start();
    }
}
由于版本号b线程无法再修改了

注意integer的赋值,不要太大!

阿里开发手册

各种锁的理解

公平锁和不公平锁

公平锁:非常公平

非公平锁:不公平,可以插队,按实际的速度看是否插队。

默认非公平锁

ReentrantLock默认是一个非公平锁,如果我们加上true,就修改成立公平锁。

方法重载。

填入参数true就可改变为公平锁

可重入锁

就是相当于,进去家门之后,有两个房子,a先进了,锁上门以后,b进不来,a要是再想去第二个门,速度肯定比b快。

package com.JUC.Lock8;

public class LockEvery {
    public static void main(String[] args) {
        Home home = new Home();

       new Thread(()->{
           home.h1();
       },"a").start();

        new Thread(()->{
            home.h2();
        },"b").start();
    }
}

class Home{

    public synchronized void h1(){
        System.out.println(Thread.currentThread().getName()+"去了第一间房子~");
        h2();//这里进入h2
    }

    public synchronized void h2(){
        System.out.println(Thread.currentThread().getName()+"去了第二间房子~");
    }
}

Lock和synchronized都是可重入的。

自旋锁

使用cas创造一个自旋锁。

package com.JUC.Lock8;

import java.util.concurrent.atomic.AtomicReference;

public class SpinLockDemo {
    AtomicReference<Thread> a = new AtomicReference<>();

    //加锁
    public void lock(){
        Thread thread = Thread.currentThread();
        //自旋锁
        System.out.println(Thread.currentThread().getName()+"=>开始拿");
        //加锁
        while (!a.compareAndSet(null,thread)){
            System.out.print(".");//这里就是证明没有获得锁的线程在这里阻塞了
        }
    }

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

测试类:

package com.JUC.Lock8;

import java.util.concurrent.TimeUnit;

public class TestSpinLock {
    public static void main(String[] args) {
        SpinLockDemo spinLockDemo = new SpinLockDemo();

        new Thread(()->{
            spinLockDemo.lock();
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                spinLockDemo.unLock();
            }
        },"a").start();

        new Thread(()->{
            spinLockDemo.lock();
            spinLockDemo.unLock();
        },"b").start();
    }
}
自造自旋锁

死锁

什么是死锁?

多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能运行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形.某一个同步块同时拥有“两个以上对象的锁”时,就可能会发生“死锁”的问题。

通俗的讲:就是两个线程运行时,都需要对面的资源进行完成指令,但是都等待对方的资源释放,从而形成停止执行的情况。

package com.szw;


public class DeadLock {
  public static void main(String[] args) {
      Makeup makeup = new Makeup(0,"白雪公主");
      Makeup makeup1 = new Makeup(1,"灰姑娘");
      makeup.start();
      makeup1.start();
  }
}

//口红
class Lipstick{
}

//镜子
class Mirror{
}

class Makeup extends Thread{
  int choice;//选择
  String girlName;//姓名

  Makeup(int choice,String girlName){
      this.choice = choice;
      this.girlName = girlName;
  }

  //化妆需要的镜子和口红都只有一份
  static Lipstick lipstick = new Lipstick();
  static Mirror mirror = new Mirror();

  @Override
  public void run() {
      makeup();
  }

  private void makeup(){
      if (choice == 0){
          synchronized (lipstick){
              System.out.println(this.girlName+"获得了口红");
              synchronized ((mirror)){
                  System.out.println(this.girlName+"获得了镜子");
              }
          }
      }else {
          synchronized (mirror){
              System.out.println(this.girlName+"获得了镜子");
              synchronized ((lipstick)){
                  System.out.println(this.girlName+"获得了口红");
              }
          }
      }
  }
}

怎么去排查这个问题?

1、先试用jps -l定位

先显示线程id

2、在使用jstack + 线程号 看堆栈信息

通过堆栈信息发现死锁问题
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,723评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,003评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,512评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,825评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,874评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,841评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,812评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,582评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,033评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,309评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,450评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,158评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,789评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,409评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,609评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,440评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,357评论 2 352