Java多线程和高并发学习笔记3

单例模式

单例模式:就是某个类在整个系统中只能有一个实例对象可被获取和使用的代码模式,经典例子:JVM运行环境的Runtime类
设计单例模式的要点:
1、某个类只能有一个实例(构造器私有化)
2、它必须是自行创建这个实例(含有一个该类的静态变量来保存这个唯一的实例)
3、它必须自行向系统提供这个实例(向外提供获取该实例对象的方式:1、直接暴露 2、用静态变量的get方法去获取)
实现方式:
1、饿汉式:直接创建对象,不存在线程安全的问题
2、懒汉式:延迟创建对象,存在线程安全的问题,但是可以解决

首先我们来看饿汉式的三种创建方式:

第一种:

package offer;

public class Singleton1 {
    public static final Singleton1 INSTANCE = new Singleton1();

    private Singleton1() {

    }
}

第二种:通过枚举类创建

package offer;

public enum Singleton2 {
    INSTANCE
}

第三种:通过静态代码块创建

package offer;

public class Singleton3 {
    private String name;
    private int age;

    public static final Singleton3 INSTANCE;

    static {
        INSTANCE = new Singleton3("qxf", 27);
    }

    private Singleton3(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public static Singleton3 getINSTANCE() {
        return INSTANCE;
    }
}

测试类:

package offer;

public class Test {
    public static void main(String[] args) {
        Singleton1 singleton1 = Singleton1.INSTANCE;

        Singleton2 singleton2 = Singleton2.INSTANCE;

        Singleton3 singleton31 = Singleton3.INSTANCE;
        Singleton3 singleton32 = Singleton3.INSTANCE;
        System.out.println(singleton31.getName() + " " + singleton31.getAge());
        System.out.println(singleton31);
        System.out.println(singleton32);
    }
}

然后我们来看懒汉式的三种创建方式:

第一种:

package offer;

public class Singleton4 {
    private static Singleton4 instance;

    private Singleton4() {

    }

    public static Singleton4 getInstance() {
        if (instance == null) {

            try {
                // 这个地方线程休眠1s是为了测试多线程情况下的单例模式
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            instance = new Singleton4();
        }

        return instance;
    }
}

这种方式单线程没有事情,但是在多线程调用的情况下会有问题,因为如果第一个线程进入getInstance方法以后,判断instance==null为true,然后线程休眠1s,这个时候,第二个线程进来了,发现Singleton4还没有创建,判断instance==null还是为true,这个时候,又休眠1s,然后第一个线程被唤醒后开始执行,创建一个对象,过来1s,第二个线程也被唤醒,又创建了一个对象,这样就会有两个对象产生,导致多线程下没有办法保证只实例化一个对象。就不符合我们单例模式的初衷了。

第二种:

package offer;

public class Singleton4 {
    private static Singleton4 instance;

    private Singleton4() {

    }

    public static Singleton4 getInstance() {
        if (instance == null) {
            synchronized (Singleton4.class) {
                if (instance == null) {
                    try {
                        // 这个地方线程休眠1s是为了测试多线程情况下的单例模式
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    instance = new Singleton4();
                }
            }
        }

        return instance;
    }
}

首选,使用synchronized关键字修饰当前变量,然后将我们初始化对象操作放到同步方法块中保证操作的原子性,然后我们在外层再加一个判断,如果当前对象已经存在,则直接返回,加最外层的判断是为了考虑效率,防止线程阻塞

第三种:

package offer;

public class Singleton5 {
    private Singleton5() {

    }

    private static class Inner {
        private static final Singleton5 INSTANCE = new Singleton5();
    }

    public static Singleton5 getInstance() {
        return Inner.INSTANCE;
    }
}

这种方式就是在内部类加载的时候,才创建INSTANCE对象,因为静态内部类不会自动随着外部类的加载和初始化而初始化,它是要单独加载和初始化的。因为是在内部类加载和初始化的,因此是线程安全的。

类初始化

一个类要创建实例需要先加载并初始化该类

main方法所在的类需要先加载和初始化

一个子类要初始化需要先初始化父类

一个类的初始化就是执行<clinit>()方法

<clinit>()方法由静态类变量显示赋值代码(也就是private static int j = method())和静态代码块组成

一个类的实例初始化就是执行<init>()方法

<init>()可能由多个,有几个构造器就有几个<init>方法
<init>()方法由非静态实例变量显示赋值代码(也就是private static int j = method())和非静态代码块,对应构造器代码组成
super()方法一直都在<init>()方法的第一行,所以就会优先执行

方法的重写

哪些方法不可以被重写?

final方法
静态方法
private等子类中不可见的方法

对象的多态性

子类如果重写了父类的方法,通过子类对象调用的一定是子类重写过的代码

mysql的索引

mysql的索引是帮助mysql快速获取数据的数据结构,往往以索引文件的形式存储在硬盘上
优点:查询速度快
缺点:会降低更新表的速度,因为更新表的时候不但要保存数据,还要保存数据的索引值

哪些情况需要创建索引?

主键自动创建索引
频繁查询的字段需要创建索引
查询中与其他表关联的字段,外键需要建立索引
单键/组合索引的选择问题,组合索引性价比更高
查询中需要排序的字段,可以添加索引,能够提高排序的速度
查询中需要统计或者分组的字段,都需要建立索引

哪些情况下不要创建索引?

经常需要修改的字段不要添加索引
where条件里用不到的字段不创建索引

ArrayList

ArrayList的底层是一个数组,初始的默认大小是10,如果存满以后会扩容,扩容大小为原始大小的一半+原始大小
源码如下:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
ArrayList是线程不安全的,因为它的add方法没有加锁,例如:

package offer;

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

public class ListDemo {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        for (int i = 0; i <= 3; i++) {
            new Thread(() -> {
                list.add(UUID.randomUUID().toString().substring(0, 8));
                System.out.println(list);
            }, String.valueOf(i)).start();
        }

    }
}

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

可以看到,多线程的情况下,add方法添加多个元素,有可能会导致元素的丢失
如果我们把i改成30,就会报下面的异常:

Exception in thread "0" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:911)
	at java.util.ArrayList$Itr.next(ArrayList.java:861)
	at java.util.AbstractCollection.toString(AbstractCollection.java:461)
	at java.lang.String.valueOf(String.java:2994)
	at java.io.PrintStream.println(PrintStream.java:821)
	at offer.ListDemo.lambda$main$0(ListDemo.java:13)
	at java.lang.Thread.run(Thread.java:748)

上面这个异常,在多线程操作的情况下,是非常常见的

那么解决方案是什么?
1、使用Vector集合
2、使用Collections.synchronizedList(new ArrayList<>())
3、使用new CopyOnWriteArrayList<>()(写时复制,读写分离)

写时复制,读写分离怎么理解呢?
意思就是,如果一个线程拿到了当前集合,那么它在操作集合的同时,会复制一份当前的集合给其他的线程,且不允许其他的线程操作当前的集合,而复制的集合只允许其他的线程进行读操作,如果要进行写操作,那么必须等待当前线程执行完成写操作后,其他线程才可以对当前集合进行写操作。总结一句话就是写时复制,读写分离。

copyOnWriteArrayList的add方法源码:
在这里插入图片描述

HashSet

HashSet的底层是HashMap,但是为什么add方法只添加一个元素呢?
我们可以看源码:
在这里插入图片描述
我们add的值就是key,而value是一个名字叫PRESENT的空对象
在这里插入图片描述
同样,set集合也有自己的CopyOnWrite方法

Set<String> set = new CopyOnWriteArraySet<>();
set.add("1");

HashMap

HashMap处理多线程操作的时候,建议使用ConcurrentHashMap

Map<String, String> map = new ConcurrentHashMap<>();

公平锁,非公平锁,可重入锁(又名递归锁),自旋锁,独占锁(写锁)/ 共享锁(读锁)/ 互斥锁

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

例如,可重入锁本质上就是非公平的

Lock lock = new ReentrantLock();

在这里插入图片描述
只有我们传入参数fair为true时候,它才是一个公平锁

Lock lock = new ReentrantLock(true);

在这里插入图片描述
非公平锁的优点在于吞吐量比公平锁大,对于Synchronized也是一种非公平锁

可重入锁(又名递归锁):指的是同一线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码,在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。也就是说,线程可以进入任何一个它已经拥有的锁所同步着的代码块。
ReentrantLockSynchronized就是非公平的可重入锁。

例子如下:

package offer;

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

public class Phone implements Runnable {
    public synchronized void sendMsg() {
        System.out.println(Thread.currentThread().getId() + "\t send msg");
        sendEmail();
    }

    public synchronized void sendEmail() {
        System.out.println(Thread.currentThread().getId() + "\t send email");
    }

    Lock lock = new ReentrantLock();

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

    public void get() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getId() + "\t get method");
            set();
        } finally {
            lock.unlock();
        }
    }

    public void set() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getId() + "\t set method");
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Phone phone = new Phone();
        new Thread(phone::sendMsg, "thread1").start();
        new Thread(phone::sendMsg, "thread2").start();

        Thread.sleep(3000);

        System.out.println();
        System.out.println();

        Thread thread3 = new Thread(phone);
        Thread thread4 = new Thread(phone);

        thread3.start();
        thread4.start();
    }
}

输出结果如下:
在这里插入图片描述
我们把上面的代码改一下看看

    public void get() {
        lock.lock();
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getId() + "\t get method");
            set();
        } finally {
            lock.unlock();
            lock.unlock();
        }
    }

结果还是一样的,说明锁无论加几把,都不会影响结果

那我们再改一下代码:

    public void get() {
        lock.lock();
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getId() + "\t get method");
            set();
        } finally {
            lock.unlock();
        }
    }

我们会发现,线程会卡死,等待下一个线程,因为它拿了2把锁,但是只释放了一把锁,所以就会造成还有一把锁没有释放
在这里插入图片描述

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

下面是手动实现一个自旋锁:

package offer;

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

public class SpinLockDemo {
    // 原子引用线程
    AtomicReference<Thread> atomicReference = new AtomicReference<>();

    public void myLock() {
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName() + "\t come in");
        while (!atomicReference.compareAndSet(null, thread)) {
        }
    }

    public void myUnlock() {
        Thread thread = Thread.currentThread();
        atomicReference.compareAndSet(thread, null);
        System.out.println(thread.getName() + "\t unlock");
    }

    public static void main(String[] args) throws InterruptedException {
        SpinLockDemo demo = new SpinLockDemo();
        new Thread(() -> {
            try {
                demo.myLock();
                TimeUnit.SECONDS.sleep(5);
                demo.myUnlock();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "thread1").start();

        TimeUnit.SECONDS.sleep(1);

        new Thread(() -> {
            demo.myLock();
            demo.myUnlock();
        }, "thread2").start();
    }
}

结果运行如下:
在这里插入图片描述

多个线程可以同时读一个资源类,但是,如果有一个线程想去写共享资源,那么就不应该再有其他线程可以对该资源进行读或写
读-读能共存
读-写不能共存
写-写不能共存
写操作:原子+独占

首选我们来看一个不给读写加锁的例子:

package offer;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

public class MyCache {
    private final Map<String, Object> map = new HashMap<>();

    public void put(String key, Object val) throws InterruptedException {
        TimeUnit.MILLISECONDS.sleep(300);
        System.out.println(Thread.currentThread().getName() + "\t 正在写入 " + key);
        map.put(key, val);
        System.out.println(Thread.currentThread().getName() + "\t 写入完成 " + key);
    }

    public void get(String key) throws InterruptedException {
        TimeUnit.MILLISECONDS.sleep(300);
        System.out.println(Thread.currentThread().getName() + "\t 正在读取 " + key);
        Object val = map.get(key);
        System.out.println(Thread.currentThread().getName() + "\t 读取完成 " + val);
    }

    public static void main(String[] args) {
        MyCache cache = new MyCache();
        for (int i = 0; i < 5; i++) {
            final int temp = i;
            new Thread(() -> {
                try {
                    cache.put(String.valueOf(temp), temp + "");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }, String.valueOf(i)).start();
        }

        for (int i = 0; i < 5; i++) {
            final int temp = i;
            new Thread(() -> {
                try {
                    cache.get(String.valueOf(temp));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }, String.valueOf(i)).start();
        }
    }
}

结果如下:
在这里插入图片描述
我们会发现,读和写操作完全是乱序的,没有按照一直写或者一直读的顺序去执行

那么,这种情况,我们应该怎么解决呢?直接加一个Lock?恐怕是不太行,因为直接加Lock锁的话,会导致同时只有一个线程可以访问,也就是说,在同一个时间,只能有一个线程进行读或者写,这个和我们的初衷是不太一样的,所以,我们需要换一种锁来实现,也就是ReentrantReadWriteLock锁,它的作用就是在一个线程写入资源的时候,其他线程可以同时的去读取资源,这样能够满足我们的需求了,代码如下:

package offer;

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

public class MyCache {
    private final Map<String, Object> map = new HashMap<>();
    private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    public void put(String key, Object val) throws InterruptedException {
        readWriteLock.writeLock().lock();
        try {
            TimeUnit.MILLISECONDS.sleep(300);
            System.out.println(Thread.currentThread().getName() + "\t 正在写入 " + key);
            map.put(key, val);
            System.out.println(Thread.currentThread().getName() + "\t 写入完成 " + key);
        } finally {
            readWriteLock.writeLock().unlock();
        }
    }

    public void get(String key) throws InterruptedException {
        readWriteLock.readLock().lock();
        try {
            TimeUnit.MILLISECONDS.sleep(300);
            System.out.println(Thread.currentThread().getName() + "\t 正在读取 " + key);
            Object val = map.get(key);
            System.out.println(Thread.currentThread().getName() + "\t 读取完成 " + val);
        } finally {
            readWriteLock.readLock().unlock();
        }
    }

    public static void main(String[] args) {
        MyCache cache = new MyCache();
        for (int i = 0; i < 5; i++) {
            final int temp = i;
            new Thread(() -> {
                try {
                    cache.put(String.valueOf(temp), temp + "");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }, String.valueOf(i)).start();
        }

        for (int i = 0; i < 5; i++) {
            final int temp = i;
            new Thread(() -> {
                try {
                    cache.get(String.valueOf(temp));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }, String.valueOf(i)).start();
        }
    }
}

运行结果如下:
在这里插入图片描述
而且我们可以看出来,写入要按照顺序,不能有其他线程干扰,但是读取的时候可以不按照顺序

CountDownLatch

举个例子,有个教室,一共有7位同学,其中1位是班长,他负责每天晚自习后锁门,所以按道理他应该每天最后一个走,那么用代码模拟一下过程:

package offer;

public class CountDownLatchDemo {

    public static void main(String[] args) {
        for (int i = 1; i <= 6; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "\t 学生离开教室");
            }, String.valueOf(i)).start();
        }

        System.out.println(Thread.currentThread().getName() + "\t 班长离开教室");
    }
}

结果是什么?
在这里插入图片描述
我们会发现,班长提前走了,然后才是其他学生离开教室,那么这种情况,是不是就和我们的想法不符?那么,我们应该怎么杜绝这种情况的发送呢?
使用CountDownLatch

package offer;

import java.util.concurrent.CountDownLatch;

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

        for (int i = 1; i <= 6; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "\t 学生离开教室");
                countDownLatch.countDown();
            }, String.valueOf(i)).start();
        }

        try {
            // CountDownLatch只有为0的时候,才会释放当前锁
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(Thread.currentThread().getName() + "\t 班长离开教室");
    }
}

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

枚举类的使用

枚举在使用中,可以存储一些我们常用的数据,而不用从数据库中去查询,比如:

package offer;

public enum CountyEnum {
    ONE(1, "1"),
    TWO(2, "2");

    private int num;
    private String val;

    CountyEnum(int num, String val) {
        this.num = num;
        this.val = val;
    }

    public int getNum() {
        return num;
    }

    public void setNum(int num) {
        this.num = num;
    }

    public String getVal() {
        return val;
    }

    public void setVal(String val) {
        this.val = val;
    }

    public static CountyEnum getVal(int index) {
        CountyEnum[] countyEnums = CountyEnum.values();
        for (CountyEnum e : countyEnums) {
            if (index == e.getNum()) {
                return e;
            }
        }

        return null;
    }
}

那么我们怎么使用这个枚举?

package offer;

import java.util.Objects;
import java.util.concurrent.CountDownLatch;

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

        for (int i = 1; i <= 6; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "\t 学生离开教室");
                countDownLatch.countDown();
            }, Objects.requireNonNull(CountyEnum.getVal(i)).getVal()).start();
        }

        try {
            // CountDownLatch只有为0的时候,才会释放当前锁
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(Thread.currentThread().getName() + "\t 班长离开教室");
    }
}

这样就可以简化我们的代码

System.out.println(CountyEnum.ONE);
System.out.println(CountyEnum.ONE.getNum());
System.out.println(CountyEnum.ONE.getVal());

这三个分别打印了枚举的名称,key值,value值

CyclicBarrier

CyclicBarrier和我们上面提到的CountDownLatchDemo刚好相反,它是如果声明的次数减少到0的时候,才会执行里面声明的方法,代码如下:

package offer;

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

public class CyclicBarrierDemo {

    public static void main(String[] args) {
        CyclicBarrier barrier = new CyclicBarrier(6, () -> {
            System.out.println("所有子线程执行完成!");
        });

        for (int i = 0; i < 6; i++) {
            final int temp = i;
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "\t 获取到第" + temp + "个线程");
                try {
                    barrier.await();
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }, String.valueOf(i)).start();
        }
    }
}

运行结果如下:
在这里插入图片描述

Semaphore

Semaphore主要用于两个目的:一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制
下面的代码就模拟了多个线程抢占多个资源使用的情况:

package offer;

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

public class SemaphoreDemo {
    public static void main(String[] args) {
        // 模拟3个资源
        Semaphore semaphore = new Semaphore(3);

        // 模拟6个线程,但是只有3个线程可以拿到资源
        for (int i = 0; i < 6; i++) {
            new Thread(() -> {
                try {
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName() + "\t 获取到资源");
                    Random random = new Random();
                    int waitSecond = random.nextInt(3);
                    TimeUnit.SECONDS.sleep(waitSecond);
                    System.out.println(Thread.currentThread().getName() + "\t" + waitSecond + "秒后释放资源");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    // semaphore释放当前资源
                    semaphore.release();
                }
            }, String.valueOf(i)).start();
        }
    }
}

结果如下:
在这里插入图片描述
我们可以看到,我们使用Semaphore定义了3个资源的限制,那么每次也只有3个资源可以被其他线程抢到,如果没有抢到,那么就会等待,一旦有线程释放资源,那么其他线程就会去争抢这个资源

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值