juc并发学习笔记

JUC

什么是JUC?

JUC就是java包中java.uril.concurrent的包以及它的子类。我们以前学的Callable接口就是这个包下的一个接口

线程和进程

进程:一个程序,就比如QQ.exe,Music.exe ,一个进程通常包含多个线程,至少包含一个

线程:程序执行的最小单位,Java中默认有两个线程,分别是Main线程和GC线程

java真的可以自己开启线程吗?

public synchronized void start() {
    /**
     * This method is not invoked for the main method thread or "system"
     * group threads created/set up by the VM. Any new functionality added
     * to this method in the future may have to also be added to the VM.
     *
     * A zero status value corresponds to state "NEW".
     */
    if (threadStatus != 0)
        throw new IllegalThreadStateException();

    /* Notify the group that this thread is about to be started
     * so that it can be added to the group's list of threads
     * and the group's unstarted count can be decremented. */
    group.add(this);

    boolean started = false;
    try {
        start0();
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
            /* do nothing. If start0 threw a Throwable then
              it will be passed up the call stack */
        }
    }
}

private native void start0();

通过原码得到,java调用start()方法,在start方法中调用native(本地)方法start0(),所以java本身是不能自己开线程的。

获取CPU的核数(可以同时支持多少条线程执行)

System.out.println(Runtime.getRuntime().availableProcessors());

wait/sleep的区别

  1. 来自不同的类:wait来自Object类,sleep来自Thread类
  2. 锁的释放:sleep不会释放锁,wait会释放锁
  3. 使用的范围不同:wait必须在同步代码块中,sleep可以在任何地方
  4. 是否需要捕获异常:wait不需要捕获异常,sleep必须补货异常(除中断异常之外)

Lock锁

synchronized

public class TestSynchronized {

    public static void main(String[] args) {
        //并发,多线程操作一个资源类,将资源类丢入线程
        Ticket ticket = new Ticket();

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

}

//资源类 OOP
class Ticket {
    //属性
    private int ticketNum = 20;

    //方法
    public synchronized void sold() {
        if (ticketNum <= 0) return;
        System.out.println(Thread.currentThread().getName() + "买到了第" + ticketNum-- + "张票,还剩余" + ticketNum + "张票");
    }
}

lock

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

public class TestLock {
    public static void main(String[] args) {
        Ticket1 ticket = new Ticket1();
        new Thread(() -> {for(int i = 0; i < 30; i++) ticket.sold();}, "A").start();
        new Thread(() -> {for(int i = 0; i < 30; i++) ticket.sold();}, "B").start();
        new Thread(() -> {for(int i = 0; i < 30; i++) ticket.sold();}, "C").start();
    }

}

/**
 * Lock三部曲
 * new ReentrantLock();
 * lock.lock();
 * finally --> lock.unlock();
 */
class Ticket1 {
    private int ticketNum = 20;

    private static Lock lock = new ReentrantLock();

    public synchronized void sold() {
        lock.lock();
        try {
            if (ticketNum <= 0) return;
            System.out.println(Thread.currentThread().getName() + "买到了第" + ticketNum-- + "张票,还剩余" + ticketNum + "张票");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

    }
}

介绍完lock和synchronized之后,需要了解一下公平锁和非公平锁

synchronized和lock的区别

  1. synchronized是一个内置关键字,而lock是一个类
  2. synchronized无法获取锁的状态,而lock可以判断是否获取到了锁
  3. synchronized会自动释放锁(在被锁的代码块执行完毕之后),而lock必须手动释放锁,通常在finally语句块中释放
  4. synchronized是重入锁,不可以中断的,非公平锁;lock是重入锁,可以判断锁,是否公平可以手动设置
  5. synchronized在一个线程获得锁并且还没有释放锁的时候,其他线程就只能等待拿到锁的线程释放锁;lock当持有锁的对象长期不释放锁时,正在等待的线程可以选择放弃等待
  6. synchronized适合锁少量代码的同步问题,lock适合锁大量代码的同步问题

生产者消费者问题

面试:八大排序,单例模式,生产者消费者,死锁

synchronized版本

public class Synchronized_test {

    public static void main(String[] args) {
        Data data = new Data();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                data.increase();
            }
        }, "A").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                data.decrease();
            }
        }, "B").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                data.increase();
            }
        }, "C").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                data.decrease();
            }
        }, "D").start();
    }

}

/**
 * 三部:判断等待,事务,通知唤醒
 */
class Data{

    private int num = 0;

    public synchronized void increase(){
        //使用while是为了防止虚假唤醒
        while (num != 0){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        num += 1;
        System.out.println(Thread.currentThread().getName() + "--->" + num);
        //通知其他线程
        this.notifyAll();
    }

    public synchronized void decrease(){
        while (num == 0){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        num -= 1;
        System.out.println(Thread.currentThread().getName() + "--->" + num);
        this.notifyAll();
    }

}

这里说一下虚假唤醒:就是当一条线程条件满足并且唤醒其他线程的时候,可能会将一些无用的线程也唤醒。就比如说买东西,现在商场没有货物,线程都在阻塞,突然进入了一件商品,所有的人都会来买这个东西,但是只有一个人能买到,其他人唤醒是没用的。

在if块使用wait是非常危险的,因为这个条件只会判断一次,如果第一次判断时线程进入了阻塞,那么当线程唤醒的时候就不会再进行判断。假设,A,C线程同时去生产产品,但是只有一个线程会拿到锁,当A线程拿到锁之后,C线程进来就会进入阻塞,此时A线程再进来也会被阻塞,那么这个时候就有两条生产者线程被阻塞了。如果此时到达一个消费者线程,将产品消耗,并且唤醒其他线程,AC都会唤醒,如果是使用的if判断,则两个线程会轮流生产一个产品,就导致产品会有两个;若果使用的是while判断,当一个生产者生产一个产品后,另一个线程又会再次进入阻塞。所以在有使用到wait的地方,应使用while进行条件判断。

在这里插入图片描述

其实重点是wait()导致当前线程挂起并释放锁,立刻会有其他线程抢占锁,如果这个抢占到锁的线程依然满足等待条件,就会导致这两个线程均挂起。如果某一时刻被唤醒,那么这两个线程将会轮流执行if语句之后的语句从而产生问题

juc版本

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

public class Juc {

    public static void main(String[] args) {
        //声明资源类,并将资源类丢入线程进行运行
        Data1 data = new Data1();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                data.increase();
            }
        }, "A").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                data.decrease();
            }
        }, "B").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                data.increase();
            }
        }, "C").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                data.decrease();
            }
        }, "D").start();
    }
}

class Data1 {
    private int num = 0;

    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();
	//增加
    public void increase() {
        //基本的lock使用步骤,
        lock.lock();
        try {
            //业务代码
            //使用while是为了防止虚假唤醒
            while (num != 0) {
                //等待
                condition.await();
            }
            num += 1;
            System.out.println(Thread.currentThread().getName() + "--->" + num);
            condition.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
	//减少
    public void decrease() {
        lock.lock();
        try {
            //使用while是为了防止虚假唤醒
            while (num == 0) {
                condition.await();
            }
            num -= 1;
            System.out.println(Thread.currentThread().getName() + "--->" + num);
            condition.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

如我们看到的一样,上面这些方法都只能随机的让线程运行,怎么能让线程按照我们所想的顺序进行执行呢。

引入lock后的执行步骤:

  1. 加锁
  2. 判断等待
  3. 执行业务
  4. 唤醒线程
  5. finally —> 释放锁

Condition ----> 精准的通知唤醒线程

这种按我们所想的来规定线程的执行顺序,就好比操作系统中的线程前驱,在一个线程执行前进行V操作,在一个线程执行后执行P操作。

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

public class Test3 {
    public static void main(String[] args) {
        //资源类丢入线程执行
        Data2 data2 = new Data2();

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

class Data2{

    private Lock lock = new ReentrantLock();

    private Condition c1 = lock.newCondition();
    private Condition c2 = lock.newCondition();
    private Condition c3 = lock.newCondition();
    private int flag = 1;
    //A线程
    public void printA(){
        lock.lock();
        try {
            while (flag != 1){
                //A线程阻塞
                c1.await();
            }
            flag = 2;
            System.out.println(Thread.currentThread().getName()+"线程执行");
            //A线程执行完之后唤醒B线程
            c2.signal();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

    //B线程
    public void printB(){
        lock.lock();
        try{
            while(flag != 2){
                c2.await();
            }
            System.out.println(Thread.currentThread().getName() + "线程执行");
            flag = 3;
            c3.signal();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

    //C线程
    public void printC(){
        lock.lock();
        try{
            while(flag != 3){
                c3.await();
            }
            System.out.println(Thread.currentThread().getName() + "线程执行");
            flag = 1;
            c1.signal();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
}

锁现象

8个锁问题:https://www.cnblogs.com/itiaotiao/p/12651573.html

首先明确一个问题,线程的执行与它的先后调用顺序有关系吗?

是无关的,线程的执行与它的先后调用顺序无关,而是取决于哪一个线程先拿到了锁,哪一个线程先执行。

synchronized锁的是方法的调用者,有几个对象就有几个锁,普通方法不受锁的影响

staitc synchronized锁的是对象的class文件,全局唯一

如果有一个静态同步方法,一个普通同步方法,一个对象,创建两条线程分别调用两个方法,执行顺序会是什么情况呢?

由于普通同步方法和静态同步方法锁的对象不同,所以两个线程不需要等待对方释放锁就能执行。这个时候就要看谁的执行时间或者时延来判断谁先出结果。

总结

只要弄清楚锁的是对象本身还是对象的class文件,就能弄清楚锁的问题。两个锁之间互不影响。

安全的集合

List不安全

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

//java.util.ConcurrentModificationException
public class TestArrayList {
    public static void main(String[] args) {
        List<String> list = new CopyOnWriteArrayList<>();
        /**
         * 在高并发下如果使用线程不安全的集合,会抛出ConcurrentModificationException异常
         * 解决办法:
         * 1.使用Collections.synchronizedList()     ---> List<String> list = Collections.synchronizedList(new ArrayList<>());
         * 2.使用Vector       --->  List<String> list = new Vector<>();
         * 3.使用JUC中的CopyOnWriteArrayList       ---> List<String> list = new CopyOnWriteArrayList<>();
         */

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

    }
}

CopyOnWriteArrayList:写入时复制,简称COW,是计算机程序设计领域的一种优化策略,在写入的时候避免覆盖,造成数据的丢失。

CopyOnWriteArrayList add方法原码:

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

Vector add方法原码:

public synchronized boolean add(E e) {
    modCount++;
    ensureCapacityHelper(elementCount + 1);
    elementData[elementCount++] = e;
    return true;
}

CopyOnWriteArrayList中的数组是private transient volatile Object[] array;这种形式的,被transient 和 volatile修饰的,而Vector中的数组只是一个简单的数组:protected Object[] elementData;,两者很明显是不同的。

既然CopyOnWriteArrayList是线程安全的,Vector也是线程安全的,那么为什么不使用Vector而使用CopyOnWriteArrayList?

首先,我们可以从方法名就可以看出,Vector的add方法使用的synchronized来进行同步,而CopyOnWriteArrayList使用了lock来实现同步。而synchronized的效率是比lock的效率要低的。

其实,在《深入理解JVM虚拟机》中也说了,java的线程是映射到操作系统的原生内核级线程之上的,如果要阻塞或者唤醒一条线程,则需要操作系统来进行协助,这就需要造成用户态和核心态之间的切换,这种状态的切换是需要耗费很多的处理器时间,尤其是对于代码特别简单的同步块,状态转换消耗的时间甚至会比代码块执行的时间长。

Set不安全

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

//java.util.ConcurrentModificationException
public class TestSet {
    public static void main(String[] args) {
//        Set<String> set = new HashSet<>();
//        Set<String> set = Collections.synchronizedSet(new HashSet<>());
        Set<String> set = new CopyOnWriteArraySet<>();
        for (int i = 0; i < 30; i++) {
            new Thread(() -> {
                set.add(UUID.randomUUID().toString().substring(0,3));
                System.out.println(set.toString());
            }).start();
        }
    }
}

补充知识:HashSet底层是什么?

透过原码可以知道,初始化HashSet的时候其实就是初始化了一个HashMap对象:

public HashSet() {
    map = new HashMap<>();
}

而set.add()也正是调用了map的put方法:

public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}

CopyOnWriteHashArraySet初始化的时候其实也是创建了一个CopyOnWriteArrayList对象:

public CopyOnWriteArraySet() {
    al = new CopyOnWriteArrayList<E>();
}

Map不安全

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

public class TestMap {

    public static void main(String[] args) {
//        java.util.ConcurrentModificationException
//        Map<String, String> map = new HashMap<>();	注意负载因子和默认大小
//        Map<String,String> map = Collections.synchronizedMap(new HashMap<>());
        Map<String,String> map = new ConcurrentHashMap<>();
        for (int i = 0; i < 30; i++) {
            new Thread(() -> {
                map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0, 4));
                System.out.println(map);
            }).start();
        }
    }
}

Callable接口

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

public class TestCallable {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //FutureTask实现RunnableFuture接口,RunnableFuture接口继承Runnable接口,所以在线程启动的时候可以将FutureTask传递给Thread
        //而FutureTask的构造方法需要传入一个Callable接口
        MyThread myThread = new MyThread();
        FutureTask<Integer> futureTask = new FutureTask<>(myThread);
        //一个FutureTask只执行一次
        new Thread(futureTask).start();
        new Thread(futureTask).start();
        //futureTask.get()方法会出现阻塞,尽量放在最后
        System.out.println(futureTask.get());
    }
}

class MyThread implements Callable<Integer>{
    @Override
    public Integer call() throws Exception {
        System.out.println("call方法执行!");
        return 1024;
    }
}

futureTask.get()原码

public V get() throws InterruptedException, ExecutionException {
    int s = state;
    if (s <= COMPLETING)
        s = awaitDone(false, 0L);
    return report(s);
}

FutureTask只执行一次的原因

在FutureTask中的run方法中这样写的:

if (state != NEW ||
    !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                 null, Thread.currentThread()))
    return;

通过这个state来判断任务的执行状态,只有当state是NEW的状态并且CAS比较安全的时候才能执行。

常用的辅助类

CountDownLatch

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

import java.util.concurrent.CountDownLatch;

public class CountDownLatchTest {
    public static void main(String[] args) throws InterruptedException {
        //CountDownLatch(int count) ,传入一个计数器的值
        CountDownLatch latch = new CountDownLatch(7);

        for (int i = 0; i < 7; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName());
                //让计数器减一
                latch.countDown();
            }, String.valueOf(i)).start();
        }

        //只有latch中的计数器为0的时候才能执行下面的代码
        //如果此时计数器的值不为0,那么当前线程会被禁用进行线程调度,并处于休眠状态
        //直到计数器为0,或者其他线程中断当前线程
        latch.await();
        System.out.println("线程执行完毕");

    }
}

CyclicBarrier

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

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

public class CyclicBarrierTest {
    public static void main(String[] args) {
        /**
         * CyclicBarrier有两种构造方法
         * 1.CyclicBarrier(int parties)
         * 2.CyclicBarrier(int parties, Runnable barrierAction)
         * parties理解为当有多少个线程到达await语句后,才会执行Runnable的语句
         * 而Runnable就是当线程的执行数量到达parties之后执行的run方法
         */
        c cyclicBarrier = new CyclicBarrier(7,() -> System.out.println(Thread.currentThread().getName() + "线程最后一个到达"));

        MyCyclicBarrier myCyclicBarrier = new MyCyclicBarrier(cyclicBarrier);

        for (int i = 0; i < 7; i++) {
            new Thread(() -> {
                myCyclicBarrier.execute();
            }).start();
        }

    }
}

class MyCyclicBarrier {

    private CyclicBarrier barrier;

    public MyCyclicBarrier(CyclicBarrier barrier) {
        this.barrier = barrier;
    }

    public void execute() {
        try {
            System.out.println(Thread.currentThread().getName() + "到达了A");
            /**
             * 如果当前线程不是最后一个线程,那么它将被禁用以进行线程调度,并且处于休眠状态,直到发生下面事情之一:
             * 1.最后一个线程到达 
             * 2.一些其他线程当前线程为interrupts ; 
             * 3.一些其他线程interrupts其他等待线程之一; 
             * 4.一些其他线程在等待屏障时超时; 
             * 5.其他一些线程在这个屏障上调用reset() 。
             */
            barrier.await();
            System.out.println(Thread.currentThread().getName() + "冲破了A");
            TimeUnit.SECONDS.sleep(2);
            System.out.println(Thread.currentThread().getName() + "到达了B");
            barrier.await();
            System.out.println(Thread.currentThread().getName() + "冲破了B");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (BrokenBarrierException e) {
            e.printStackTrace();
        }
    }
}

CyclicBarrier使用场景

用于多线程计算数据,最后合并计算结果

static int num = 0;
CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> {System.out.println(Thread.currentThread().getName() + "线程最后一个到达");
    System.out.println(num);});

for (int i = 0; i < 7; i++) {
    new Thread(() -> {
        try {
            num++;
            cyclicBarrier.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (BrokenBarrierException e) {
            e.printStackTrace();
        }
    }).start();
}

Semaphore(信号量)

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

public class SemaphoreTest {
    public static void main(String[] args) {
        //信号量 permit表示同时最多可以运行的线程数量,在线程开始前使用acquire()方法,在线程执行完之后使用release()方法
        //可以用来做限流
        Semaphore semaphore = new Semaphore(3);
        for (int i = 0; i < 7; i++) {
            new Thread(() -> {
                try {
                    //获取,如果执行的线程已经达到极限,则让当前线程停止
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName() + "到达");
                    TimeUnit.SECONDS.sleep(1);
                    System.out.println(Thread.currentThread().getName() + "离开");
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    //释放,将自身持有的许可证释放,信号量 + 1,然后唤醒等待的线程
                    //没有要求发布许可证的线程必须通过调用acquire()获取该许可证
                    semaphore.release();
                }
            }).start();
        }
    }
}

读写锁

在我们从数据库中读写数据的时候,对于读操作来说,可以让多个人同时从数据库读取数据;对于写操作来说,一个时刻只能让一个线程写入数据。这就需要我们的读写锁来进行控制。

假设有一个类,创建多个线程对类中的数据进行修改,如果一个线程在修改数据的时候有另一个线程进行修改数据,那么就会造成数据的丢失。

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteLockTest {
    public static void main(String[] args) {
        ReadWriteUnlock readWrite = new ReadWriteUnlock();
        //线程同时写数据
        for (int i = 0; i < 5; i++) {
            //在创建线程的时候,相当于创建了一个实现类,是不能访问到变量i的
            // 如果想要在线程中访问到这个i,就需要创建一个常量,然后将这个常量传入
            final int temp = i;
            new Thread(() -> readWrite.update(temp)).start();
        }
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //线程同时读数据
        for (int i = 0; i < 5; i++) {
            new Thread(() -> System.out.println(readWrite.getNum())).start();
        }
    }
}

//不加读写锁
class ReadWriteUnlock {
    private int num = 0;


    public void update(int num) {
        System.out.println(Thread.currentThread().getName() + "修改前的值为:" + this.num);
        this.num = num;
        System.out.println(Thread.currentThread().getName() + "修改后的值为:" + this.num);
    }


    public int getNum() {
        return num;
    }

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

//加读写锁
class ReadWrite{

    private int num = 0;

    private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    public void update(int num){
        readWriteLock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "修改前的值为:" + this.num);
            this.num = num;
            System.out.println(Thread.currentThread().getName() + "修改后的值为:" + this.num);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readWriteLock.writeLock().unlock();
        }
    }


    public int getNum() {
        return num;
    }

    public void setNum(int num) {
        readWriteLock.readLock().lock();
        try {
            this.num = num;
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readWriteLock.readLock().unlock();
        }
    }
}

不加读写锁:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Q3ZAJztr-1640419535816)(C:/Users/26794/AppData/Roaming/Typora/typora-user-images/image-20211212195732366.png)]
加读写锁:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-m2IsQ2dz-1640419535818)(C:/Users/26794/AppData/Roaming/Typora/typora-user-images/image-20211212195816008.png)]**

独占锁:相当于写锁,一次只能被一个线程占有

共享锁:相当于读锁,多个线程同时占有

阻塞队列

什么情况下使用阻塞队列:多线程并发处理,线程池

**[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0LLlWhdf-1640419535821)(C:/Users/26794/AppData/Roaming/Typora/typora-user-images/image-20211212211552546.png)]**

虽然BlockingQueue继承自Queue,但是BlockingQueue与Set和List是同级的

四组API

  1. 抛出异常 —> add 和 remove
  2. 不抛出异常,有返回值 ----> offer 和 poll
  3. 一直等待 ----> put 和 take
  4. 超时等待 ----> offfer(带参数) 和 poll(带参数)
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.TimeUnit;

public class QueueTest {
    public static void main(String[] args) throws InterruptedException {
        test4();
    }

    //抛出异常  add 和 remove
    public static void test1(){
        //capacity:队列长度
        ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(3);

        queue.add("A");
        queue.add("B");
        queue.add("C");
        //超出队列长度抛出:java.lang.IllegalStateException: Queue full 异常
        //queue.add("D");

        //获取队首元素
        System.out.println(queue.element());

        queue.remove();
        queue.remove();
        queue.remove();
        //队列为空时抛出异常:java.util.NoSuchElementException 异常
        //queue.remove();
    }

    //不抛出异常,有返回值
    public static void test2(){
        ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(3);

        System.out.println(queue.offer("A"));
        System.out.println(queue.offer("B"));
        System.out.println(queue.offer("C"));
        //队列满时输出false
        //System.out.println(queue.offer("D"));

        //获取队首元素
        System.out.println(queue.peek());

        System.out.println(queue.poll());
        System.out.println(queue.poll());
        System.out.println(queue.poll());
        //队列为空是输出null
        //System.out.println(queue.poll());
    }

    //一直等待
    public static void test3() throws InterruptedException {
        ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(3);

        queue.put("A");
        queue.put("B");
        queue.put("C");
        //当队列满时一直等待
        //queue.put("D");

        System.out.println(queue.take());
        System.out.println(queue.take());
        System.out.println(queue.take());
        //当队列空时一直等待
        System.out.println(queue.take());
    }

    //等待超时
    public static void test4() throws InterruptedException {
        ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(3);

        System.out.println(queue.offer("A", 2, TimeUnit.SECONDS));
        System.out.println(queue.offer("B", 2, TimeUnit.SECONDS));
        System.out.println(queue.offer("C", 2, TimeUnit.SECONDS));
        //当队列为满时,等待规定的时间后,如果还没有添加进队列,就不会继续等待
        //System.out.println(queue.offer("D", 2, TimeUnit.SECONDS));

        System.out.println(queue.poll(2, TimeUnit.SECONDS));
        System.out.println(queue.poll(2, TimeUnit.SECONDS));
        System.out.println(queue.poll(2, TimeUnit.SECONDS));
        //当队列为空时,等待规定的时间后,如果队列中还没有元素,就不会继续等待
        //System.out.println(queue.poll(2, TimeUnit.SECONDS));
    }
}

同步队列(SynchronousQueue)

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

public class SynQueueTest {
    public static void main(String[] args) {
        //同步队列不存储元素,put了一个值之后,必须用take取出来,否则就不能存储元素
        BlockingQueue<String> queue = new SynchronousQueue<>();

        //存储线程
        new Thread(() ->{
            try {
                System.out.println(Thread.currentThread().getName() + "存放了第1个数");
                queue.put("1");
                System.out.println(Thread.currentThread().getName() + "存放了第2个数");
                queue.put("2");
                System.out.println(Thread.currentThread().getName() + "存放了第3个数");
                queue.put("3");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"Thread-1").start();

        //取出线程
        new Thread(() -> {
            try {
                System.out.println(Thread.currentThread().getName() + "取得了第" +queue.take() + "个数");
                System.out.println(Thread.currentThread().getName() + "取得了第" +queue.take() + "个数");
                System.out.println(Thread.currentThread().getName() + "取得了第" +queue.take() + "个数");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"Thread-2").start();
    }
}

之所以同步队列不存储元素,是因为它的put方法中是这样写的,大概可以看到有一个Thread.interrupted()方法:

public void put(E e) throws InterruptedException {
    if (e == null) throw new NullPointerException();
    if (transferer.transfer(e, false, 0) == null) {
        Thread.interrupted();
        throw new InterruptedException();
    }
}

线程池

由于频繁的开启/关闭线程会造成性能的浪费,所以可以通过一个线程池来管理这些线程,将不用的线程放入线程池中,在下次使用的时候调用即可。避免了线程的频繁创建和销毁带来的性能消耗。也便于对线程进行管理。

三大方法

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

public class ThreadPollTest {
    public static void main(String[] args) {
//        ExecutorService threadPool = Executors.newSingleThreadExecutor();     一个线程池只有一个线程
//        ExecutorService threadPool = Executors.newFixedThreadPool(5);         一个线程池有固定的线程个数
        ExecutorService threadPool = Executors.newCachedThreadPool();           //一个线程池的线程个数是不固定的

        //使用线程池后就不用传统的开启线程的方法了
        for (int i = 0; i < 100; i++) {
            threadPool.execute(() -> System.out.println(Thread.currentThread().getName() + " ok!"));
        }

        //线程池使用后要记得关闭
        threadPool.shutdown();
    }
}

7大参数

线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式。因为用Executors创建线程的线程池对象有如下弊端:

  1. FixedThreadPoolSingleThreadPool允许的请求队列长度为Integer.MAX_VALUE,可能会造成大量请求的堆积,从而导致OOM(OutOfMemory)
  2. CachedThreadPoolScheduledThreadPool允许创建的线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM

TheradPoolExecutor构造方法以及七大参数:

public ThreadPoolExecutor(int corePoolSize,	//核心线程数
                          int maximumPoolSize,	//最多线程数
                          long keepAliveTime,	//空闲线程存活时间
                          TimeUnit unit,	//空闲线程存活时间单位
                          BlockingQueue<Runnable> workQueue,	//工作队列
                          ThreadFactory threadFactory,	//线程工厂
                          RejectedExecutionHandler handler)	//拒绝策略

执行流程:

  1. 当一个任务被提交到线程池中,如果此时核心线程没有被全部使用,则这个线程会开始执行。
  2. 如果此时核心线程都在被使用,那么这个线程会进入工作队列中
  3. 如果此时工作队列为空,则将线程添加到队列中;
  4. 如果此时队列已满,并且核心线程都在被使用,则会创建一个线程,然后从工作队列中取出一个线程进行执行,再将新来的线程添加到工作队列中。
  5. 线程池不会无限制的创建线程,它有一个上限即maximumPoolSize,当创建的线程数到达maximumPoolSize这个数时,即使此时队列已满也不会再创建线程。
  6. 如果此时核心线程等于maximumPoolSize并且工作队列也是满的,此时再来一个线程,那么就会触发线程池的拒绝策略。
  7. 如果线程池中存在空闲线程,并且这个空闲线程存活了keepAliveTime个unit,那么这个线程就会被回收
import java.util.concurrent.*;

public class ThreadPollTest {
    public static void main(String[] args) {
        ExecutorService threadPool = new ThreadPoolExecutor(2,
                5,
                2,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.DiscardOldestPolicy());

        //线程池最大负载 = maximumPoolSize + capacity,当到达的线程超过这个数的时候就会使用到拒绝策略

        //使用线程池后就不用传统的开启线程的方法了
        for (int i = 0; i < 9; i++) {
            threadPool.execute(() -> System.out.println(Thread.currentThread().getName() + " ok!"));
        }

        //线程池使用后要记得关闭
        threadPool.shutdown();
    }
} 

四种拒绝策略

四种拒绝策略都在ThreadPoolEcecutor中有对应的静态类,在使用的时候,通过new ThreadPoolExecutor.();即可*

  1. AbortPolicy:丢弃任务并抛出RejectedExecutionException异常
  2. CallerRunPolicy:不处理此任务,并将此任务交给提交任务的线程执行
  3. DiscardPolicy:丢弃任务但是不抛出异常,如果此时工作队列已满,则后续提交的任务都会被丢弃
  4. DiscardOldestPolicy:抛弃进入队列的最早的那个任务,然后尝试把这次提交的任务加入队列

I/O密集型和CPU密集型(调优)

如何设置最大的线程池大小,需要根据I/O密集型和CPU密集型来设置最大线程的大小

  • 对于CPU密集型,你的电脑是几核就设置几条线程
  • 对于I/O密集型,需要判断你的I/O线程数量,然后让最大线程数大于I/O线程的数量

如何得知电脑是几条线程:System.out.println(Runtime.getRuntime().availableProcessors());

四大函数式接口

函数型接口

import java.util.function.Function;

public class FunctionTest {
    public static void main(String[] args) {
/*        Function<String,String> function = new Function<String, String>() {
            @Override
            public String apply(String s) {
                return s;
            }
        };*/
        Function<String,String> function = (str) -> {return str;};

        System.out.println(function.apply("abc"));
    }
}

函数型接口泛型:Function<T, R>

applay()方法:R apply(T t);

断定型接口

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 s) {
                return s.isEmpty();
            }
        };*/

        Predicate<String> predicate = (str) -> {return str.isEmpty();};

        System.out.println(predicate.test(""));
    }
}

断定型接口泛型:Predicate<T>

test()方法:boolean test(T t)

消费型接口

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 s) {
                System.out.println(s);
            }
        };*/
        Consumer<String> consumer = (str) -> {
            System.out.println(str);
        };

        consumer.accept("str");
    }
}

消费型接口泛型:Consumer<T>

accept()方法:void accept(T t);

供给型接口

import java.util.function.Supplier;

public class SupplierTest {
    public static void main(String[] args) {
/*
        Supplier<String> supplier = new Supplier<String>() {
            @Override
            public String get() {
                return "ok!";
            }
        };
*/
        Supplier<String> supplier = () -> {return "ok!";};

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

Supplier接口泛型:Supplier<T>

get()方法:T get();

Stream流式计算

注意:

  1. Stream不会自己存储元素
  2. Stream不会改变原对象。相反,它们会返回一个持有记过的新Stream
  3. Stream操作是延迟更新的。这就意味着它们会等到需要的结果才会执行
import java.util.Arrays;
import java.util.List;

public class StreamTest {
    public static void main(String[] args) {
        User u1 = new User(1,"a",12);
        User u2 = new User(2,"b",14);
        User u3 = new User(3,"c",16);
        User u4 = new User(4,"d",18);
        User u5 = new User(5,"e",20);
        User u6 = new User(6,"f",22);
        List<User> list = Arrays.asList(u1, u2, u3, u4, u5, u6);
        list.stream()
                .filter((user) -> {return user.getId() % 2 == 0;})  //过滤id为2的用户
                .filter((user) -> {return user.getAge() > 15;}) //过滤年龄大于15的用户
                //<R> Stream<R> map(Function<? super T,? extends R> mapper)  之后的stream类型就是返回值的类型
                .map((user) -> {return user.getName().toUpperCase();})  //将用户名转为大写,这句执行完之后stream中的元素就为String类型了
                .sorted((o1,o2) -> {return o2.compareTo(o1);})//将用户名逆序排序
                .limit(1)   //只输出一个用户
                .forEach(System.out::println);
    }
}

并行流

ForkJoin(还需了解)

ForkJoin的核心思想就是将一个大任务拆分成多个子任务进行并行处理,最后将子任务结果合并成最后的计算结果。在处理大量数据的时候使用ForkJoin能提升性能。就比如分而治之。

ForkJoin特点:工作窃取

工作窃取是指当前某个线程中没有可执行的任务的时候,从其他线程的任务中窃取任务执行,以充分利用线程的计算能力,减少线程因为获取不到任务而造成的空闲浪费。在ForkJoinPool中工作任务的队列都采用双端队列的容器。图片转自

img

工作线程worker1,worker2,worker3都是从队列头部获取元素,从队列尾部放入元素。当worker3中没有任务时,就会从其他线程中窃取,这样使得worker3不再空闲。

使用工作窃取,可以避免我们在不断的fork过程中,可能会让某些worker一直等待join而浪费性能。

使用步骤

  1. 创建ForkJoin线程池
  2. **调用线程池的execute(异步)方法或者submit(同步) **

在这里插入图片描述

  1. 要怎么传递Runnable,Callable,可以使用lambda表达式,也可以编写实现类实现接口然后重写对应的方法,再将类传入参数。
  2. 要传入FutureTask,我们可以编写具体的类继承FutureTask或者FutureTask的子类,然后将自己编写的类传入。

**[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aS97wsuq-1640419535826)(C:/Users/26794/AppData/Roaming/Typora/typora-user-images/image-20211213201524372.png)]**

RecusiveAction的方法没有返回值,``RecusiveTask`的方法有返回值,根据具体情况选择。

import java.util.concurrent.*;

public class ForkJoinTest {
    public static void main(String[] args)  {
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        try {
            ForkJoinTask<Long> submit = forkJoinPool.submit(new MyForkJoinTest(0, 10000));
            Long sum = submit.get();
            System.out.println(sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        } finally {
            forkJoinPool.shutdown();
        }

    }
}

//计算startValue到endValue之间的数的和
class MyForkJoinTest extends RecursiveTask<Long> {

    private long startValue;
    private long endValue;

    private final long BOUNDARY = 1000L;

    public MyForkJoinTest(long startValue, long endValue) {
        this.startValue = startValue;
        this.endValue = endValue;
    }


    @Override
    protected Long compute() {
        long sum = 0;
        if (endValue - startValue <= BOUNDARY) {
            for (long i = startValue; i <= endValue; i++) {
                sum += i;
            }
        } else {
            MyForkJoinTest task1 = new MyForkJoinTest(startValue, (startValue + endValue) / 2);
            task1.fork();   //拆分任务,把任务加入线程队列
            MyForkJoinTest task2 = new MyForkJoinTest((startValue + endValue) / 2 + 1, endValue);
            task2.fork();
            //当使用join方法后,这个方法会阻塞调用方,直到任务做出结果
            sum = task1.join() + task2.join();
        }
        System.out.println("本次执行的ForkJoinTask的区间为 :" + startValue + "-->" + endValue);
        return sum;
    }
}

不同方法计算0->100000000000的和所用时间

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

public class ParallelStream {
    public static void main(String[] args) {
        //test1();    //46999
        test2();    //21700
        //test3();    //34376
    }

    //传统方式
    public static void test1() {
        long sum = 0;
        long start = System.currentTimeMillis();
        for (long i = 0; i <= 100000000000L; i++) {
            sum += i;
        }
        long end = System.currentTimeMillis();
        System.out.println("sum = " + sum + "time = " + (end - start));
    }

    //并行流式计算
    public static void test2() {
        long start = System.currentTimeMillis();
        //rangeClosed为闭区间,range为左闭右开区间
        long sum = LongStream.rangeClosed(0, 100000000000L).parallel().sum();
        long end = System.currentTimeMillis();
        System.out.println("sum = " + sum + "time = " + (end - start));
    }

    //ForkJoin测试
    public static void test3(){

        ForkJoinPool pool = new ForkJoinPool();
        try {
            long start = System.currentTimeMillis();
            ForkJoinTask<Long> task = pool.submit(new ForkJoin(0, 100000000000L));
            Long sum = task.get();
            long end = System.currentTimeMillis();
            System.out.println("sum = " + sum + "time = " + (end - start));
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }finally {
            pool.shutdown();
        }

    }
}

class ForkJoin extends RecursiveTask<Long>{

    private long startValue;
    private long endValue;

    private final long BOUNDARY = 1000L;

    public ForkJoin(long startValue, long endValue) {
        this.startValue = startValue;
        this.endValue = endValue;
    }

    @Override
    protected Long compute() {
        long sum = 0;
        //如果jiexia
        if (endValue - startValue <= BOUNDARY) {
            for (long i = startValue; i <= endValue; i++) {
                sum += i;
            }
        } else {
            ForkJoin task1 = new ForkJoin(startValue, (startValue + endValue) / 2);
            task1.fork();
            ForkJoin task2 = new ForkJoin((startValue + endValue) / 2 + 1, endValue);
            task2.fork();
            sum = task1.join() + task2.join();
        }
        return sum;
    }
}

ForkJoinPool分析

ForkJoinPool是一个运行ForkJoinTask的线程池,与其他线程池不同,主要通过工作窃取来执行:ForkJoinPool中的所有线程都会查找并执行由其他线程创建的提交到此线程池的任务,如果不存在这些任务,则发生阻塞。默认情况下,ForkJoinPool是使用默认构造函数构造的,但是可以有三个参数来设置这些参数。

ForkJoinPool(

int parallelism, //并行度级别

ForkJoinPool.ForkJoinWorkerThreadFactory factory, //线程生产的工厂类

Thread.UncaughtExceptionHandler handler, //拒绝策略

boolean asyncMode)

执行流程:

异步回调

JMM

JVM(Java Memory Model),java内存模型的主要目的是定义程序中各种变量的访问规则。这里的变量包括实力字段,静态字段和构成数组的对象,但是不包括局部变量与方法参数。

java内存模型

Volatile

  1. 保证可见性:一个线程中修改共享变量的值,在其他线程中是立即可知的
  2. 不保证原子性
  3. 进制指令重排:为了提高处理器计算单元的执行效率,会按照一定的规则对指令进行优化,但是在某些情况下,会带来一些逻辑问题。简单来说就是代码的执行顺序可能并不是按照你代码写的那样执行的。

Atmoic

单例模式

饿汉式

//饿汉式会一上来就把类中的东西都加载完毕,甚至一些没用的内存也会进行加载,所有要对代码进行优化。
public class HungryMan {

    private HungryMan(){}

    //如果类中有多个空间,一上来就创建内存会造成内存空间的浪费,所以要进行修改
    private byte[] bytes = new byte[1024*1024];
    private byte[] bytes1 = new byte[1024*1024];
    private byte[] bytes2 = new byte[1024*1024];
    private byte[] bytes3 = new byte[1024*1024];

    
    private static final HungryMan HUNGRY_MAN = new HungryMan();

    public static HungryMan getInstance(){
        return HUNGRY_MAN;
    }

}

懒汉式

//懒汉式单例
public class LazyMan {
    private LazyMan(){
        System.out.println(Thread.currentThread().getName()+"ok");
    }

    private static LazyMan LAZY_MAN;


    public static LazyMan getInstance(){

        //单线程下单例可以
        if(LAZY_MAN == null){
            LAZY_MAN = new LazyMan();
        }
        return LAZY_MAN;
    }

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

这种懒汉式在单线程下是安全的,但是在多线程下并不安全。可以使用双重锁检测。

package signal;

//懒汉式单例
public class LazyMan {
    private LazyMan(){
        System.out.println(Thread.currentThread().getName()+"ok");
    }

    //volatile防止指令重排
    private static volatile LazyMan lazyMan;


    public static LazyMan getInstance(){
        /*
        单线程下创建单例
        if(LAZY_MAN == null){
            LAZY_MAN = new LazyMan();
        }
        return LAZY_MAN;*/

        //多线程下创建,加锁
        //双重检测锁模式的 懒汉单例模式 DCL懒汉式
        if(lazyMan == null){
            synchronized(LazyMan.class){
                if(lazyMan == null)
                    /*
                    这里还要进行一次判断的原因是,当两个线程都通过了第一个if判断时,当第一个线程拿到锁,new完对象后将锁归还
                    第二个线程也拿到锁,如果不加判断此时还会创建一个对象,就不满足单例要求
                     */
                    lazyMan = new LazyMan();
                    /*
                    * 不是原子操作
                    * 1.分配内存空间
                    * 2.执行构造方法,初始化对象
                    * 3.把这个对象指向这个空间
                    * 会发生指令重排,也就是执行顺序可能为123,也可能为132
                    * 当一个线程在这快的执行顺序为132并且执行到了3,如果此时,另一个线程进入方法第一次判断lazyMan会显示不为空,就会直接返回对象
                    * 因为此时的对象还没有创建完成,返回的对象就是虚无的
                    * 想要避免指令重排,在变量前加上volatile即可
                    * */
            }
        }
        return lazyMan;
    }

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

静态内部类

public class Holder {

    static class InnerClass{
        private InnerClass(){}

        private static final Holder HOLDER = new Holder();
    }

    private Holder(){}

    public static Holder getInstance(){
        return InnerClass.HOLDER;
    }
}

上面的单例都是不安全的,都可以通过反射来进行破坏,

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;

//懒汉式单例
public class LazyMan {

    private static boolean jksdnfvjksdawd = false;

    private LazyMan(){
        synchronized (LazyMan.class){
            if(!jksdnfvjksdawd){
                //第一次创建对象
                jksdnfvjksdawd = true;
            }else{
                throw new RuntimeException("不要通过反射破坏单例");
            }
        }
    }

/*    private LazyMan(){
        synchronized (LazyMan.class){
            if(lazyMan != null){
                throw new RuntimeException("不要通过反射破坏单例");
            }
        }
    }*/


    private static volatile LazyMan lazyMan;


    public static LazyMan getInstance(){
        /*
        单线程下创建单例
        if(LAZY_MAN == null){
            LAZY_MAN = new LazyMan();
        }
        return LAZY_MAN;*/

        //多线程下创建,加锁
        //双重检测锁模式的 懒汉单例模式 DCL懒汉式
        if(lazyMan == null){
            synchronized(LazyMan.class){
                if(lazyMan == null)
                    /*
                    这里还要进行一次判断的原因是,当两个线程都通过了第一个if判断时,当第一个线程拿到锁,new完对象后将锁归还
                    第二个线程也拿到锁,如果不加判断此时还会创建一个对象,就不满足单例要求
                     */
                    lazyMan = new LazyMan();
                    /*
                    * 不是原子操作
                    * 1.分配内存空间
                    * 2.执行构造方法,初始化对象
                    * 3.把这个对象指向这个空间
                    * 会发生指令重排,也就是执行顺序可能为123,也可能为132
                    * 当一个线程在这快的执行顺序为132并且执行到了3,如果此时,另一个线程进入方法第一次判断lazyMan会显示不为空,就会直接返回对象
                    * 因为此时的对象还没有创建完成,返回的对象就是虚无的
                    * 想要避免指令重排,在变量前加上volatile即可
                    * */
            }
        }
        return lazyMan;
    }

    public static void main(String[] args) throws Exception {
/*
        LazyMan lazyMan1 = LazyMan.getInstance();
        Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);	破坏构造方法私有性
        LazyMan lazyMan2 = declaredConstructor.newInstance();
        System.out.println(lazyMan1 == lazyMan2);   //false
        //如何处理,在构造方法那块进行改造   21行构造方法
*/

        //如果两个对象都使用反射进行破坏
        Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);
        Field field = LazyMan.class.getDeclaredField("jksdnfvjksdawd"); //获取对应变量名的Field
        field.setAccessible(true);  //将值设置为true,可以被外部访问
        LazyMan lazyMan3 = declaredConstructor.newInstance();
        field.set(lazyMan3,false); //修改变量值
        LazyMan lazyMan4 = declaredConstructor.newInstance();
//        System.out.println(lazyMan3 == lazyMan4);   //false

        //如何处理,在类中定义一个变量或者一个秘钥
        System.out.println(lazyMan3 == lazyMan4);   //抛出错误          修改变量值后结果为false

        //但是无论你怎么改,都会破解出来,如果通过反射获取到你这个变量或者秘钥并进行修改,那么这个单例也就被破坏了
        
    }
}

那么怎么能不让通过反射破坏呢,通过newInstance()方法可以看到原码:

if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");

很明显,如果我们的类是一个枚举类,那么就不能通过反射来修改,会抛出异常

public enum EnumSignal {
    INSTANCE;


    public EnumSignal getInstance(){
        return INSTANCE;
    }
}

class Test{
    public static void main(String[] args) {
        EnumSignal enumSignal1 = EnumSignal.INSTANCE;
        EnumSignal enumSignal2 = EnumSignal.INSTANCE;
        System.out.println(enumSignal1 == enumSignal2);		//true
        
        //Constructor<EnumSignal> constructor = EnumSignal.class.getDeclaredConstructor(null);	//枚举类型的构造方法不是无参构造
        /*抛出错误,但不是“Cannot reflectively create enum objects”,而是“NoSuchMethodException”,原因是为什么呢,EnumSignal的class文件中构造方法是无参构造啊,而我们通过反射获取无参构造却会抛出一个异常,我们可以通过专业的分析工具过去枚举类的class文件进行反编译得到源文件,就可以得知其构造方法的参数并不是无参构造
        */
        //试图破坏枚举类型
        Constructor<EnumSignal> constructor = EnumSignal.class.getDeclaredConstructor(String.class, int.class);
        constructor.setAccessible(true);
        EnumSignal enumSignal3 = constructor.newInstance();
        EnumSignal enumSignal4 = constructor.newInstance();
        System.out.println(enumSignal3 == enumSignal4);
        //抛出异常:IllegalArgumentException: Cannot reflectively create enum objects
        
    }
}

理解CAS

什么是CAS?

CAS其实是靠硬件实现的一种功能。硬件可以保证某些从语义上看起来是需要多次操作的行为只通过一条处理器指令就能完成。

CAS,即compareAndSwap,比较并交换,如果期望值和旧值相同,就交换。(比较当前工作内存中的值和主内存中的值是否相同)

在前面说过,volatile不保证原子性,因为一个赋值操作在字节码中对应的是多条指令。另外,还说了Atomic可以保证操作的原子性,那么是怎么保证原子性的呢?就是通过CAS。

在AtomicInteger中要对一个数进行加1,需要调用getAndIncrement方法,而getAndIncrement方法中是通过调用unsafe.getAndAddInt(this, valueOffset, 1);来进行加1。

unsafe是什么?由于java无法操作内存,但是java可以通过调用c++来操作内存。而unsafe和c++一样也可以操作内存,unsafe中有native方法。

private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset //获取这个字段相对于这个对象的起始地址的偏移地址
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

getAndIncrement方法:

public final int getAndIncrement() {
    	//this表示这个对象,valueOffset是对象内某个字段在内存中的偏移值
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }

getAndAddInt方法:

public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    //自旋锁
    do {
        //获取var1中内存地址偏移值为var2的值
        //由于这里不加锁,另一个线程可能会修改var1的值
        var5 = this.getIntVolatile(var1, var2);
        //如果这个对象var1中内存地址偏移值var2的值和var5的值相等,就将值设置为var5  + var4
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
    //!this.compareAndSwapInt(var1, var2, var5, var5 + var4)是一个native方法,就是调用内存来处理

    return var5;
}

缺点:

  1. 循环会耗时
  2. 一次性只能保证一个共享变量的原子性
  3. ABA问题

测试AtomicInteger:

import java.util.concurrent.atomic.AtomicInteger;

public class CASTest {
    public static void main(String[] args) {
        //原子类型的整形,所有的操作都是原子类型的
        AtomicInteger integer = new AtomicInteger();

        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                for (int j = 0; j < 10; j++) {
                    integer.getAndIncrement();
                }
            }).start();
        }

        while (Thread.activeCount() > 2){
            //如果线程总数大于2(jvm中有两个默认线程,main线程和gc线程),就让当前线程(也就是main线程)由执行态,变为就绪态。
            Thread.yield();
        }

        System.out.println(integer.get());
    }
}

ABA问题

ABA问题:一个线程在某一时刻或者一个变量的值,然而在这一刻另一个线程到来也获得了这个值并且进行了修改,然后又将这个值改了回去。当第一个继续执行的时候,就会发现当前值和自己读的值是一样的,实际上已经发生了改变。

如何解决ABA问题 ----> 原子引用

举个例子:如果你女朋友跟别人跑了,之后又和你复合,虽然女朋友还是那个女朋友,但是中间发生了什么就不知道了。

原子引用

带版本号的原子操作,在进行旧值和新值的比较的时候,也要对版本号进行比较。

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

public class ABATest {
    public static void main(String[] args) {
        /**
         * 小贴士:
         * 如果这里的泛型是我们的包装类,比如Integer,Long,Character等,有些包装类是有缓存机制的,就比如Integer的缓存范围是[-128,127],
         * 如果旧值和期望的值是在缓存范围之中的,那么cas就会成功
         * 如果不在范围之中,由于比较的是包装类,是一个对象,比较的是引用,肯定是相同的,也就cas就会失败
         */
        AtomicStampedReference<Integer> reference = new AtomicStampedReference<>(20,1);

        new Thread(() -> {
            //获取版本号
            int stamp = reference.getStamp();
            System.out.println("a1 -> " +stamp);
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //将值进行修改并且将版本号加1,观察是否修改成功
            System.out.println(reference.compareAndSet(20, 22, reference.getStamp(), reference.getStamp() + 1));
            //输出修改后的版本号
            System.out.println("a2 -> " + reference.getStamp());
            //将值修改回去,并且版本号加1
            System.out.println(reference.compareAndSet(22, 20, reference.getStamp(), reference.getStamp() + 1));
            //输出还原后的版本号
            System.out.println("a3 -> " + reference.getStamp());


        },"A").start();

        new Thread(() -> {
            int stamp = reference.getStamp();
            System.out.println("b1 -> " + stamp);
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            //在线程a修改完并且恢复这个值之后,观察能否修改成功
            System.out.println(reference.compareAndSet(20, 60, stamp, stamp + 1));


        },"B").start();
    }
}

各种锁的理解

悲观锁(先取锁,再访问)

  1. 悲观锁总认为只要不去才去正确的同步措施,那就一定会发生问题,无论共享的数据是否会出现竞争,它都会进行加锁。
  2. 在对一条数据进行修改的时候,为了避免在修改过程中被其他线程修改,最好的办法就是对该数据进行加锁。这种在修改数据之前就加锁的操作,就称为悲观锁。
  3. 悲观锁具有很强的独占性,它指的是数据对外界的修改持保守态度。

悲观锁的实现:

  1. 关系型数据库使用的锁机制,比如行锁,表锁,读锁,写锁等,都是在操作之前进行加锁的
  2. java中的:synchronized,lock

悲观锁的分类:

  1. 共享锁:多个事务/线程对于同一数据可以共享一把锁,都能访问到数据,但是只能用来读取数据,不能修改数据
  2. 排它锁:多个事务/线程对于同一数据只能有一个事务/线程持有锁,同一时刻只有一个能修改数据

乐观锁

乐观锁是一种基于冲突检测的乐观并发策略,通俗就是不管风险,先进行操作,如果没有其他线程争用共享数据,那就操作成功。如果共享数据被争用,产生冲突,就进行其他措施,常用的措施就是不断尝试,直到没有出现争用。乐观锁不会刻意使用锁机制,而是一考数据本身来保证数据的正确性。

乐观锁的实现:

  1. cas:Java 中java.util.concurrent.atomic包下面的原子变量使用了乐观锁的一种 CAS 实现方式。
  2. 版本号控制(原子引用):一般一个数据都有一个版本号,当数据被修改时,版本号就加1。当线程A修改数据时,如果此时的版本号与之前读到的版本号一致,那么就可以修改数据,否则重试更新操作,知道操作成功。

乐观并发控制相信事务之间的数据竞争的概率是比较小的,因此尽可能直接做下去,直到提交的时候才去锁定,所以不会产生任何锁和死锁。

公平锁,非公平锁

公平锁:多个线程等待一个锁的时候,必须按照锁的时间顺序依次获得锁,也就是排队

非公平锁:非公平锁不保证排队,即后来的线程可以先获得锁

可重入锁

给每个锁添加一个锁的计数值和锁的持有者两个信息,计数值为0表示没有线程拥有这个锁,如果计数值不为0,如果线程和这个锁的持有者相同。就可以重复进入

同步代码块对于同一线程是可重入的,这意味着同一线程反复进入同步代码块也不会出现自己把自己锁死的情况。

synchronized锁的重入锁始终是一个锁,lock锁调用几个lock方法就有几个锁

自旋锁

自旋锁在AtomicInteger中已经见过。

public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}

由于互斥同步(synchronized和lock)对性能影响最大的是阻塞的实现,线程的挂起和恢复都需要转入内核态进行处理,性能较低。

自旋锁:如果一个处理器上能有多个线程并行执行,我们可以让后面请求锁的线程“稍等一会”并不放弃处理器,看看持有锁的线程是否会很快释放锁,如果很快释放锁,也就不需要阻塞和线程的挂起。

缺点:如果一个线程持有锁的时间过长,而后面又有线程在忙循环(自旋),就会浪费处理器的时间。

自己写的锁,简单测试自旋锁

import java.util.concurrent.atomic.AtomicReference;

public class SpinLockTest {

    private AtomicReference<Thread> reference = new AtomicReference<>();

    public void lock() {

        Thread thread = Thread.currentThread();

        System.out.println(thread.getName() + " --> lock");

        /**
         * 自旋锁
         * 如果当前没有线程拥有锁,即AtomicReference中的值为null,就让线程拿到锁去做别的事,其他线程到达的时候就会进入循环而被阻塞
         *
         */
        //加锁
        while (!reference.compareAndSet(null, thread)) {
        }

    }

    public void unlock() {

        Thread thread = Thread.currentThread();
        System.out.println(thread.getName() + " --> unlock");
        //解锁
        //如果旧值和期望的值相同,就将值变为null,表示解锁
        reference.compareAndSet(thread, null);
    }

}
import java.util.concurrent.TimeUnit;

public class LockDemo {
    public static void main(String[] args) {
        SpinLockTest lockTest = new SpinLockTest();

        new Thread(() -> {
            lockTest.lock();
            try {
                TimeUnit.SECONDS.sleep(2);
                System.out.println(Thread.currentThread().getName() + " --> 执行");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lockTest.unlock();
            }
        }, "T1").start();

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

        new Thread(() -> {
            lockTest.lock();
            try {
                System.out.println(Thread.currentThread().getName() + " --> 执行");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lockTest.unlock();
            }
        }, "T2").start();
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值