Java高并发知识点

线程基本操作

线程的创建

1、实现Runnabe接口,最常用。

Thread t1 = new Thread(new Runnable() {//匿名内部类写法
            @Override
            public void run() {
                System.out.println("t1");
            }
        });
t1.start();

2、继承Thread类

Thread t2 = new Thread(){//匿名内部类写法
    @Override
    public void run() {
        System.out.println("t2");

    }
};
t2.start();

3、使用FutureTask和Callable组合(并行模式中的Future模式有讲)

class C implements Callable<String>{//返回realData

    @Override
    public String call() throws Exception {
        return "t3";
    }
}

FutureTask<String> futureTask = new FutureTask(new C());
//启动线程
new Thread(futureTask).start();
//这里的realData只是个形式上的变量,当使用get方法时,会返回真正的变量给realData,若call方法还未执行完,则会阻塞
String realData = futureTask.get(1,TimeUnit.SECONDS);
System.out.println(realData);

不要调用Thread和Runnable的run方法,该方法只会在当前线程执行任务,并不会开启新线程。

终止线程

stop()方法可以终止线程,但是该方法太过于暴力。假设线程正在写入数据,强行把执行到一半的线程终止,可能会引发数据不一致的问题!(一半是旧数据,一半是新数据)

stop方法是Thread类的静态方法,目前已被弃用。

建议使用自定义符号的方法来中断线程:
基本思路为在方法中的某个位置循环符号位,当符号位表现为“终止线程”时,再进行退出!这样写的好处是,终止线程的时机是由程序员自己定义的!可以确保不会在其他位置退出。

public class StopThread {

    volatile static Boolean stop = false;

    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            while (!stop){
                //do something
                System.out.println("I'm working!");
            }
            System.out.println("exit!");
        }).start();
        Thread.sleep(500);
        stop = true;
    }
}

线程中断

Java自己提供的一套更加强大的终止线程的方法!

核心思想也是使用标记来判断是否需要中断,该标记由Java提供!

java提供了一套API来访问interrupt标记:

public void Thread.interrupt()//中断当前线程
public static boolean Thread.interrupted()//判断当前线程是否中断,并且清除中断状态
public boolean Thread.isInterrupted()//判断是否中断

interrupt()只会修改标记,并不会真的执行中断线程这一操作!这一操作应该是由程序员在读取到中断线程标记后自定义的!要在线程start后,才能中断,不然没有意义。

public class InterruptThread {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()->{
            while (true){
                if (Thread.currentThread().isInterrupted()){
                    System.out.println("interrupt!");
                    break;
                }
                System.out.println("I'm working!");
            }
        });
        t.start();
        Thread.sleep(500);
        t.interrupt();
    }
}

如果循环体中出现了类似wait()方法或者sleep()方法,则只能够通过中断来识别:

sleep休眠

sleep方法并不会释放持有的锁

public static native void sleep(long millis) throws InterruptedException;
//让线程休眠指定毫秒数
//如果在sleep时,该线程被中断,则会抛出异常并且清除中断标记!
public class InterruptThread {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()->{
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                System.out.println("该线程在休眠中被中断!");
            }
        });
        t.start();
        Thread.sleep(500);
        t.interrupt();
    }
}

等待(wait)和通知(notify)

这两个方法并不是在Thread类中,而是Object类中!

public final void wait() throws InterruptedException
public final native void notify()
public final native void notifyAll()

当你在线程A中调用obj.wait()方法后,线程A就会停止继续执行,转为等待状态。
线程A会一直等到其他线程调用了obj.notify()或者obj.notifyAll()为止!

使用wait和 notify需要注意的点:

  • 该线程必须是objects监视器的拥有者:必须使用synchronized获得调用wait或notify对象的监视器,否则会抛出IllegalMonitorStateException。
  • notify是随机唤醒一个wait的线程。
  • 使用wait方法后,该线程会放弃在obj上的监视器/锁。
  • 一个被notify的线程,需要等待重新获得obj上的监视器/锁。
  • wait时被中断,也会和sleep一样抛出InterruptedException并且清除中断标记。
  • 如果synchronized加在方法上,则可以使用this.wait() / wait() 来调用。
public class WaitAndNotify {

    static final Object obj = new Object();

    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            System.out.println("陷入等待");
            synchronized (obj){
                try {
                    obj.wait();
                } catch (InterruptedException e) {
                    System.out.println("陷入中断");
                }
                System.out.println("被唤醒");
            }
        }).start();

        Thread.sleep(1000);
        synchronized (obj){
            obj.notify();//唤醒线程。notifyAll,唤醒所有线程 
        }
    }
}

挂起(suspend)和继续执行(resume)

两个方法都被废弃。原因是suspend挂起后,线程不会释放锁(如果先前使用synchronize获取了锁的话),容易导致死锁。

需要获取锁后才能执行。

suspend和resume不需要锁对象obj来执行,而是线程本身的方法。

suspend会挂起线程直到该线程调用resume才会继续执行。

但是suspend挂起时,该线程并不会去释放锁资源!

public class SuspendAndResume {
    static final Object obj = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            synchronized (obj){
                System.out.println("suspend!");
                Thread.currentThread().suspend();
                System.out.println("resume!");
            }
        });
        Thread t2 = new Thread(()->{
            synchronized (obj){
                System.out.println("do something");
            }
        });
        t1.start();
        Thread.sleep(500);
        t2.start();
        Thread.sleep(500);
        t1.resume();
    }
}

以上代码输出为:

suspend!
resume!
do something

t2必须等待t1 resume后释放锁才能执行。

resume方法必须保证在suspend方法后执行才能正常唤醒线程,不然线程会一直挂起。然而在实际应用中不是很好保证这一顺序。因此可以使用Object.wait和Object.notify来代替,或者使用LockSupport。

线程等待结束(join)和谦让(yeild)

Join(实例方法,可以等待其他实例完成)

等待指定线程运行结束后,再继续执行。

public final void join() throws InterruptedException//无限等待
public final synchronized void join(long millis) //等待指定毫秒数

实例:

public class JoinAndYield {
    static int i = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()->{
            for (int j = 0; j < 100000; j++) {
                i++;
            }
        });
        t.start();
        t.join();//等待t完成
        System.out.println(i);
    }
}

Yeild(静态方法,只有我才能主动谦让)

让当前线程主动让出CPU,但还是会进行CPU资源的争夺。谁将获得CPU由操作系统决定。

Thread t1 = new Thread(()->{
            System.out.println("t1 do");
            Thread.yield();
            System.out.println("t1 continue to do");
        });

Thread t2 = new Thread(()->{
    System.out.println("t2 do");
});
t1.start();
t2.start();

输出结果:

t1 do
t2 do
t1 continue to do

守护线程

守护线程是一种特殊的线程,在后台默默地完成一些系统性的服务,比如垃圾回收线程、JIT线程。用户线程可以认为是工作线程。**如果用户线程全部结束,只剩下守护线程时,Java虚拟机会自动退出。**在默认情况下,创建的线程都是工作线程!mian方法线程也是工作线程。

public class DaemonThread {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()->{
            for (int i = 0; i < 100; i++) {
                try {
                    Thread.sleep(500);
                    System.out.println("I'm alive!");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.setDaemon(true);//一定要在start之前设置
        t.start();
        Thread.sleep(2000);
    }
}

输出结果:

I'm alive!
I'm alive!
I'm alive!

线程t并不会输出100次”I‘m alive“。因为main方法线程结束后,此时只剩下守护线程(包括t),JVM会自动退出。

线程优先级

java线程的实现,实际上是将java线程映射到原生操作系统的线程。所以设置Java线程的优先级实际上是设置原生操作系统中线程的优先级。这样会存在一个问题,那就是优先级并不是一一对应的,例如java优先级是1到10,共十个优先级。但是实际的操作系统中可能并没有十个级别,这样就会存在Java中的多个优先级映射在操作系统的一个优先级上。所以优先级并不能一定保证实际线程的优先度。

在Java中,使用1到10表示线程优先级。一般使用Thread类内置的静态标量来表示:

public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;
public class SetPriority {
    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            for (int i=0;i<10;i++){
                System.out.println(Thread.currentThread().getName());
            }
        });
        Thread t2 = new Thread(()->{
            for (int i=0;i<10;i++){
                System.out.println(Thread.currentThread().getName());
            }
        });
        t1.setPriority(Thread.MAX_PRIORITY);
        t2.setPriority(Thread.MIN_PRIORITY);
        t2.start();//先启动t2
        t1.start();
    }
}

输出结果:

Thread-1
Thread-1
Thread-1
Thread-0
Thread-0
Thread-0
Thread-0
Thread-0
Thread-0
...

可以看见t2执行一段时间后,CPU就被t1抢占。

synchronized

关键字synchronized的作用是实现线程间的同步。它的工作是对同步的代码加锁/获得监视器,使得每一次,只能有一个线程进入同步块。(可重入)

用法:

  • 指定加锁对象:对给定对象加锁,进入同步代码前要获得指定对象的锁。
  • 直接作用于实例方法:相当于对当前实例加锁,进入同步代码前要获得当前实例的锁。
  • 直接作用于静态方法:相当于对当前类(class)进行加锁,进入同步代码前要获得当前类的锁。

synchronized还可以保证可见性。

错误的加锁示例:

1、对不可变对象加锁

public class SynchronizedTest {

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new Error1());
        Thread t2 = new Thread(new Error1());
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(Error1.i);
    }
}

class Error1 implements Runnable{
    static Integer i = 0;

    @Override
    public void run() {
        for (int j=0;j<100000;j++){
            synchronized (i){
                i++;
            }
        }
    }
}

//输出为:185807,正常情况下应该是200000

明明加了synchronized进行同步,为何会出现错误?因为i是不可变对象,每次自增实际上是生成了新的对象并将引用赋值给i。这就导致在某一时刻可能两个线程要进行同步的锁不是同一个对象的锁。

2、锁的不是同一个对象

public class SynchronizedTest {

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new Error1());
        Thread t2 = new Thread(new Error1());
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(Error1.i);
    }
}

class Error1 implements Runnable{
    static Integer i = 0;

    private synchronized void increase(){
        i++;
    }
    @Override
    public void run() {
        for (int j=0;j<100000;j++){
            increase();
        }
    }
}
//实际输出:167675

明明方法加了synchronized,为什么没有实现同步?那是因为实例方法锁的是实例对象。而t1、t2是两个实例对象,其加锁/解锁互不影响。改正后如下:

Error1 error1 = new Error1();
Thread t1 = new Thread(error1);
Thread t2 = new Thread(error1);

也可以将increase方法声明为静态方法,这样加锁/解锁就是在唯一的类实例上进行。

JDK并发包

重入锁ReentrantLock

效果和synchronized关键字相同,可以起到完全替代的作用。

需要注意的是,获取锁后一定要保证能释放锁,不然会造成永久阻塞。

建议使用try-finally语句处理业务,在finally代码块中释放锁。

public class ReentrantLockTest implements Runnable{

    static ReentrantLock lock = new ReentrantLock();
    static int k = 0;

    @Override
    public void run() {
        for (int i = 0; i < 100000; i++) {
            lock.lock();
            try{
                k++;
            }finally { //保证锁一定会被释放,如果不这样写,业务代码出现异常后会直接终止代码,导致锁不被释放
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ReentrantLockTest rlt = new ReentrantLockTest();
        Thread t1 = new Thread(rlt);
        Thread t2 = new Thread(rlt);
        t1.start();t2.start();
        t1.join();t1.join();
        System.out.println(k);
    }
}

重入锁:

锁是可以反复进入的。一个线程连续两次获得同一把锁是允许的。如果是非重入锁,第二次获取锁时会被阻塞从而造成死锁(因为第一把锁已经不可能被解锁了)。

lock.lock();
lock.lock();
try{
    k++;
}finally { //保证所一定会被释放
    lock.unlock();
    lock.unlock();
}

重入锁还提供了一些高级功能:

1、中断响应

可以提供带中断的重入锁:

public class ReentrantLockTest implements Runnable{

    static ReentrantLock lock = new ReentrantLock();
    static int k = 0;

    @Override
    public void run() {
        while (true){
            try {
                lock.lockInterruptibly(); //在还未获得锁前被中断,则会抛出异常
                System.out.println("work");
                lock.unlock();
            } catch (InterruptedException e) {
                e.printStackTrace();
                System.out.println("中断!");
                break;
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ReentrantLockTest rlt = new ReentrantLockTest();
        Thread t1 = new Thread(rlt);
        t1.start();
        Thread.sleep(500);
        t1.interrupt();
    }
}

2、锁申请等待限时

使用tryLock方法可以指定等待锁的时间,成功时会持有锁并返回true,失败则返回false

public boolean tryLock(long timeout, TimeUnit unit)//指定等待时间
public boolean tryLock()//等待时间为0,即立即尝试获取锁,失败返回false
class TimeLock implements Runnable{
    static ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        try {
            if (lock.tryLock(2,TimeUnit.SECONDS)){
                System.out.println(Thread.currentThread().getName()+"获得锁");
                Thread.sleep(3000);
                lock.unlock();
            }else {
                System.out.println(Thread.currentThread().getName()+"获取锁失败");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        TimeLock timeLock = new TimeLock();
        Thread t1 = new Thread(timeLock);
        Thread t2 = new Thread(timeLock);
        t1.start();
        t2.start();
    }
}

输出:

Thread-0获得锁
Thread-1获取锁失败

3、公平锁

线程1首先请求锁A,接着线程2也请求了锁A。那么当锁A可用时,是哪个线程获得锁呢?

非公平锁:系统会从这个锁的等待队列中随机选取一个

公平锁:会按照先来后到的顺序,保证先到先得

公平锁不会产生“饥饿”现象,但是要求系统去维护一个有序队列,因此有更高的实现成本、更低的性能。

大多数锁默认是非公平的。

public ReentrantLock(boolean fair) //重入锁构造时可以指定是否是公平,默认false

重入锁的阻塞和唤醒

效果同wait和notify。

  • await和signal调用时需要持有对应得锁,调用后会释放锁。
  • 被唤醒后依然需要重新竞争锁才能继续执行。
class ReenterLockCondition implements Runnable{
    
    static ReentrantLock lock = new ReentrantLock();
    static Condition condition = lock.newCondition();
    
    @Override
    public void run() {
        try{
            lock.lock();
            System.out.println("被阻塞");
            condition.await(); //调用时要求持有锁,调用后会释放锁。唤醒后依然需要重新获得锁才能继续执行
            System.out.println("被唤醒");
        }catch (InterruptedException e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ReenterLockCondition r = new ReenterLockCondition();
        Thread t1 = new Thread(r);
        t1.start();
        Thread.sleep(1000);
        lock.lock();
        condition.signal(); // condition.signalAll();唤醒所有阻塞线程让他们竞争
        lock.unlock();
    }
}

允许多个线程访问:信号量(Semaphore)

从广义上讲,信号量是对锁的扩展。无论是内部锁synchronized还是重入锁ReentranrLock一次只允许一个线程访问一个资源,而信号量却可以指定多个线程,同时访问一个资源。

public class SemaphoreTest implements Runnable{

    final Semaphore semp = new Semaphore(2);

    @Override
    public void run() {
        try{
            semp.acquire();//semp.tryAcquire(5,TimeUnit.SECONDS);
            System.out.println(Thread.currentThread().getName()+"获得锁");
            Thread.sleep(2000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }finally {
            semp.release();//和lock一样必须保证锁被释放
        }
    }

    public static void main(String[] args) {
        SemaphoreTest st = new SemaphoreTest();
        Thread t1 = new Thread(st);
        Thread t2 = new Thread(st);
        Thread t3 = new Thread(st);
        t1.start();
        t2.start();
        t3.start(); //t3 2s后才能获得锁
    }
}

Semaphore底层实现

也是基于AQS的。信号量(Semaphore)允许多个线程同时访问共享资源。属于共享锁。共享锁不存在重入一说。

Semaphore也分公平和非公平:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8HJy01qX-1676523040793)(C:\Users\Misaki\AppData\Roaming\Typora\typora-user-images\image-20230129121528476.png)]

先来看非公平锁的实现。

为了方便测试,我们构造信号量时传入的permit为1,此时信号量就退化成了锁。让线程A、B先后获取信号量然后释放。

(1)初始化Semaphore。

可以看出在Semaphore中state表示着可以获取的许可的数量。

public Semaphore(int permits) {
    sync = new NonfairSync(permits);
}
// 实际上调用的是父类的初始化方法
Sync(int permits) {
    setState(permits);
}

(2)线程A尝试获取锁。

public void acquire() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}

// AQS中的实现。
public final void acquireSharedInterruptibly(int arg)
    throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    // 尝试获取锁,如果锁的数量不够,则会进入队列尾部阻塞
    if (tryAcquireShared(arg) < 0)
        // 尝试获取锁,失败则进入阻塞状态
        doAcquireSharedInterruptibly(arg);
}

tryAcquireShared(arg),Semaphore自身实现。

protected int tryAcquireShared(int acquires) {
    return nonfairTryAcquireShared(acquires);
}

//返回获取许可后,剩下的可用许可数。负数则表示许可不可用。
final int nonfairTryAcquireShared(int acquires) {
    // 循环,尝试获取指定数量的许可。
    for (;;) {
        int available = getState();
        int remaining = available - acquires;
        if (remaining < 0 ||
            compareAndSetState(available, remaining))
            return remaining;
    }
}

doAcquireSharedInterruptibly(arg),AQS实现。

// 添加节点到AQS队列中,当当前节点的前驱节点是head时,尝试获取锁。获取成功则将当前节点作为head。
// 否则根据状态,判断是否需要进入阻塞状态。
private void doAcquireSharedInterruptibly(int arg)
    throws InterruptedException {
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head) {
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

(3)线程B尝试获取许可,此时会被阻塞。

(4)线程A释放锁。AQS实现。

// AQS方法
public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}
// Semaphore重写。释放许可,state+=释放的许可。
protected final boolean tryReleaseShared(int releases) {
    for (;;) {
        int current = getState();
        int next = current + releases;
        if (next < current) // overflow
            throw new Error("Maximum permit count exceeded");
        if (compareAndSetState(current, next))
            return true;
    }
}
// 尝试唤醒队列中的第一个线程
private void doReleaseShared() {
    /*
         * Ensure that a release propagates, even if there are other
         * in-progress acquires/releases.  This proceeds in the usual
         * way of trying to unparkSuccessor of head if it needs
         * signal. But if it does not, status is set to PROPAGATE to
         * ensure that upon release, propagation continues.
         * Additionally, we must loop in case a new node is added
         * while we are doing this. Also, unlike other uses of
         * unparkSuccessor, we need to know if CAS to reset status
         * fails, if so rechecking.
         */
    // 如果第一个线程可以被唤醒,那么还会继续尝试唤醒后序的线程。
    for (;;) {
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                unparkSuccessor(h);
            }
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        if (h == head)                   // loop if head changed
            break;
    }
}

总结(实际上Semaphore逻辑和ReentrantLock逻辑差不多):
Semaphore分为公平和非公平。两者仅在初次获取锁的逻辑上有区别:

  • 公平锁:如果AQS阻塞队列中存在线程,那么放弃获取锁,直接将其添加到队列尾部;否则尝试获取锁。
  • 非公平锁:无论AQS阻塞队列中是否存在线程,都会去尝试获取锁,失败则添加到队列尾部。

释放锁逻辑:
会尝试唤醒阻塞队列中的第一个线程,如果第一个线程被唤醒后成功获取到锁,则会继续尝试唤醒下一个线程。

state变量在Semaphore中的语义为:当前许可的数量。

Semaphore使用时,最好一个线程就是需要一个锁/许可。不然会出问题:
假设:当前锁为0,阻塞队列 2-1。现在释放1个锁,队列中的第一个线程被唤醒后尝试获取锁失败(需要2个,但是只有一个),因此进入阻塞状态。而实际上我们知道,第二个线程是可以被唤醒并成功获取锁的。

public class TestAQS {
    public static void main(String[] args) throws InterruptedException {
        Semaphore semaphore = MyThread.semaphore;
        Thread t1 = new Thread(new MyThread(1));
        Thread t2 = new Thread(new MyThread(2));
        Thread t3 = new Thread(new MyThread(1));
        t1.start();
        t2.start();
        t3.start();
        MyThread.semaphore.release(1);
        // 只会有一个输出,t2、t3均被阻塞
    }
}

class MyThread implements Runnable{
    public static final ReentrantLock lock = new ReentrantLock(false);
    public static final Semaphore semaphore = new Semaphore(1);

    private int k;

    public MyThread(int k) {
        this.k = k;
    }

    @Override
    public void run() {
        try {
            semaphore.acquire(k);
            System.out.println("获得锁");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

ReadWriteLock读写锁

读写锁允许多个线程同时读,写和写、读和写需要相互等待和持有锁。

public class ReadWriteLock {
    static ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
    static Lock readLock = rwLock.readLock();
    static Lock writeLock = rwLock.writeLock();

    static class ReadThread implements Runnable{//模拟读
        @Override
        public void run() {
            try{
                readLock.lock();
                System.out.println("读取数据......");
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                readLock.unlock();
            }
        }
    }

    static class WriteThread implements Runnable{//模拟写
        @Override
        public void run() {
            try{
                writeLock.lock();
                System.out.println("写数据中......");
                Thread.sleep(2000); //写操作更加耗时
                System.out.println("写入数据完成!");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                writeLock.unlock();
            }
        }
    }

    public static void main(String[] args) {
        ReadThread readThread = new ReadThread();
        WriteThread writeThread = new WriteThread();
        Random random = new Random();
        for (int i = 0; i <= 20; i++) {
            double aDouble = random.nextDouble();
            if (aDouble<0.3){
                new Thread(writeThread).start();
            }else {
                new Thread(readThread).start();
            }
        }


    }
}

输出:

写数据中......
写入数据完成!
读取数据......
读取数据......
读取数据......
读取数据......
读取数据......
写数据中......
写入数据完成!
读取数据......
读取数据......
读取数据......
读取数据......
写数据中......
写入数据完成!
写数据中......
写入数据完成!
读取数据......
读取数据......
读取数据......
读取数据......
读取数据......
写数据中......
写入数据完成!
读取数据......
读取数据......

倒计数器:CountDownLatch

可以等待指定个数个线程完成后再开始运行,和多个join一起使用效果一样。不可重复使用,一旦用完就报废。

public class CountDownLatchTest implements Runnable{
    static CountDownLatch countDown = new CountDownLatch(2);

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"正在准备");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"准备完成");
        countDown.countDown();
    }

    public static void main(String[] args) throws InterruptedException {
        CountDownLatchTest countDownLatchTest = new CountDownLatchTest();
        Thread t1 = new Thread(countDownLatchTest);
        Thread t2 = new Thread(countDownLatchTest);
        t1.start();t2.start();
        countDown.await(); //效果和  t1.join();t2.join(); 一样
        System.out.println("全部准备完毕");
    }
}

CountDownLatch底层实现

场景:设置一个数量为3的CountDownLatch。每个业务线程处理完业务后,调用countDown()。主线程中调用await()等待各个线程执行完毕。

(1)CountDownLatch初始化。state为需要多少个线程调用countDown才能开始运行。

public CountDownLatch(int count) {
    if (count < 0) throw new IllegalArgumentException("count < 0");
    this.sync = new Sync(count);
}
Sync(int count) {
    setState(count);
}

(2)三个工作线程启动,正在工作。主线程运行到 await(),开始等待。

public void await() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}
// AQS
public final void acquireSharedInterruptibly(int arg)
    throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    // 当其他线程还未完成时,state=-1
    if (tryAcquireShared(arg) < 0)
        // 当前线程被加入阻塞队列
        doAcquireSharedInterruptibly(arg);
}
// state是否为0?1:-1
protected int tryAcquireShared(int acquires) {
    return (getState() == 0) ? 1 : -1;
}

(3)线程1完成,调用countDown()方法

public void countDown() {
    sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
    // 将state-1,返回state是否为0
    if (tryReleaseShared(arg)) {
        // 函数作用:尝试释放第一个线程,如果第一个线程释放成功,尝试释放后序线程。
        doReleaseShared();
        return true;
    }
    return false;
}
// countDownLatch重写。将state-1.返回state是否为0。
protected boolean tryReleaseShared(int releases) {
    // Decrement count; signal when transition to zero
    for (;;) {
        int c = getState();
        if (c == 0)
            return false;
        int nextc = c-1;
        if (compareAndSetState(c, nextc))
            return nextc == 0;
    }
}

(4)线程2、3均完成。

实际上流程是和(3)一致的,但是state此时为0,会执行doReleaseShared() 函数。此时主线程被唤醒。

(5)附加:countDownLatch是一次性的,如果用完后:

再调用 countDown函数()

protected boolean tryReleaseShared(int releases) {
    // Decrement count; signal when transition to zero
    for (;;) {
        int c = getState();
        // state已经为0时,无法再更改state
        if (c == 0)
            return false;
        int nextc = c-1;
        if (compareAndSetState(c, nextc))
            return nextc == 0;
    }
}

再调用await()函数

 if (tryAcquireShared(arg) < 0)
        // 当前线程被加入阻塞队列
        doAcquireSharedInterruptibly(arg);

// 此时state为0,不会被阻塞
// state是否为0?1:-1
protected int tryAcquireShared(int acquires) {
    return (getState() == 0) ? 1 : -1;
}

总结:

countDownLatch初始化设置的线程数,实际上就是设置的state。

当调用await() 时,会检查state是否为0。为0则正常执行,否则加入到AQS阻塞队列中。

当调用countDown()时,会使state-1。当state变为0时,会尝试唤醒阻塞队列中的线程。

countDownLatch中重写了几个方法,赋予其不同的意义:

  • tryAcquireShared()。获取锁,state为0时获取成功,其余失败。
  • tryReleaseShared()。释放锁,使state-1,state为0时,释放成功。

此外,countDownLatch是一次性的,原因是state的更改是不可逆的。当state为0时,调用countDown()不会使state的值发生变化。调用await()时,也不会被阻塞。

循环栅栏:CyclicBarrier

可重复使用的计数。CountDownLatch计数归零后,await便不再有阻塞效果。但是CyclicBarrier计数归零后,会开始重新计数。同时可以指定当计数完成时的操作,每当计数归零便会自动调用。

CyclicBarrier还可以传入一个线程实例,当计数归零时便会调用。

CyclicBarrier还可以设置阻塞时间,超时会抛出异常。

public class CyclicBarrierTest implements Runnable{

    static CyclicBarrier barrier = new CyclicBarrier(2,()->{
        System.out.println("所有线程准备完毕");
    });

    @Override
    public void run() {
        try{
            System.out.println(Thread.currentThread().getName()+"准备中...");
            Thread.sleep(500);
            System.out.println(Thread.currentThread().getName()+"准备完毕,等待其他线程");
            barrier.await(); //计数器归零时会继续往下执行
        }catch (BrokenBarrierException | InterruptedException e){
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        CyclicBarrierTest cyclicBarrierTest = new CyclicBarrierTest();
        for (int i = 0; i < 4; i++) {
            new Thread(cyclicBarrierTest).start();
            Thread.sleep(1000);
        }
    }
}

输出:

Thread-0准备中...
Thread-0准备完毕,等待其他线程
Thread-1准备中...
Thread-1准备完毕,等待其他线程
所有线程准备完毕
Thread-2准备中...
Thread-2准备完毕,等待其他线程
Thread-3准备中...
Thread-3准备完毕,等待其他线程
所有线程准备完毕

CyclicBarrier底层实现

CyclicBarrier核心是一个await()方法,每调用一次计数会减1。当计数不为0时,线程会被阻塞;当计数为0时,线程会被放行,并且计数会重置。

观察源码,发现CyclicBarrier是基于ReentrantLock实现的,而ReentrantLock是基于AQS实现的。

/** The lock for guarding barrier entry */
private final ReentrantLock lock = new ReentrantLock();
/** Condition to wait on until tripped */
private final Condition trip = lock.newCondition();
/** The number of parties */
private final int parties;

场景,设置一个计数为3的CyclicBarrier,每个线程处理完任务后,调用CyclicBarrier.await()进行阻塞。

(1)构造CyclicBarrier。

// parties是要设置的计数
// barrierAction会在计数清零时调用
public CyclicBarrier(int parties, Runnable barrierAction) {
    if (parties <= 0) throw new IllegalArgumentException();
    this.parties = parties;
    this.count = parties;
    this.barrierCommand = barrierAction;
}
// 只设置计数
public CyclicBarrier(int parties) {
    this(parties, null);
}

(2)调用await()函数

// 该函数有返回值,表示该线程是第几个到的(最后到的index为0)。
public int await() throws InterruptedException, BrokenBarrierException {
    try {
        return dowait(false, 0L);
    } catch (TimeoutException toe) {
        throw new Error(toe); // cannot happen
    }
}
// timed:是否是有超时设置
// nanos:超时时间设置
private int dowait(boolean timed, long nanos)throws InterruptedException, BrokenBarrierException,
TimeoutException {
    final ReentrantLock lock = this.lock;
    // 获取锁
    lock.lock();
    try {
        final Generation g = generation;
		// g.broken初始化为false
        if (g.broken)
            throw new BrokenBarrierException();
		// 如果当前线程被中断
        if (Thread.interrupted()) {
            // 将g.broken设置为true
            breakBarrier();
            throw new InterruptedException();
        }
		// 标志该线程到达barrier的顺序
        int index = --count;
        // 说明计数清零。
        if (index == 0) {  // tripped
            boolean ranAction = false;
            try {
                // 如果构造barrier时传入过线程实例,则运行该线程
                final Runnable command = barrierCommand;
                if (command != null)
                    command.run();
                ranAction = true;
                // 该函数中调用 trip.signalAll(),唤醒所有阻塞的线程
                // 同时重置计数
                // 更新成员变量 generation
                nextGeneration();
                return 0;
            } finally {
                if (!ranAction)
                    breakBarrier();
            }
        }
		
        // 死循环,知道被中断、屏障破坏、或者超时
        // loop until tripped, broken, interrupted, or timed out
        for (;;) {
            try {
                if (!timed)
                    // 使用的是Condition的等待
                    trip.await();
                else if (nanos > 0L)
                    // Condition的限时等待
                    nanos = trip.awaitNanos(nanos);
            } catch (InterruptedException ie) {
                if (g == generation && ! g.broken) {
                    breakBarrier();
                    throw ie;
                } else {
                    // We're about to finish waiting even if we had not
                    // been interrupted, so this interrupt is deemed to
                    // "belong" to subsequent execution.
                    Thread.currentThread().interrupt();
                }
            }

            if (g.broken)
                throw new BrokenBarrierException();
			// 表示是上一代的线程,和之后的generation无关。
            // 如果下一代的线程也被唤醒,则不会进入return语句,而是会继续执行循环--被阻塞。
            if (g != generation)
                return index;
			// 超时
            if (timed && nanos <= 0L) {
                breakBarrier();
                throw new TimeoutException();
            }
        }
    } finally {
        lock.unlock();
    }
}

nextGeneration(),唤醒所有阻塞线程并且重置计数。

private void nextGeneration() {
    // signal completion of last generation
    trip.signalAll();
    // set up next generation
    count = parties;
    generation = new Generation();
}

CyclicBarrier总结:

基于重入锁ReentrantLock以及其Condition的阻塞和唤醒。

CyclicBarrier将每一轮计数清空的过程设计为一代:Generation。每一个线程都有自己属于的一代。不同代之间的线程互不关联。

同一代中,调用await方法时,会使得计数-1。若减一后计数不为0,则被condition.await阻塞。若为0,则唤醒本代中所有被阻塞的线程。后序几代的线程若也被唤醒,但是因为不是当前代的线程,还是会进入阻塞状态。

简单来说,当一个线程调用await使得计数为零时,该线程会唤醒所有和它属于同一代的线程。并将CyclicBarrier的generation更新为下一代、重置计数。

CyclicBarrier和CoutDownLatch的不同

  • CB是基于ReentrantLock、CD是基于AQS。虽然ReentrantLock也是基于AQS。
  • CB减少计数和阻塞均在await函数中。而CD减少计数是countDown函数、阻塞是await函数。
  • CB可以重复使用。而CD是一次性的。
  • CB可以设置超时时间、设置计数清空时的回调函数(线程实例)。

抽象队列同步器(AQS)

是JDK中许多并发类实现的基础:ReentrantLock、Semaphore、CountDowLatch。

AQS维护了一个CLH队列(双向队列,带空的头节点),所有未竞争到锁的线程都会排队等待。AQS采用的是尾插法。

注:Java中的CLH队列并不是原生的CLH队列,而是其变种,线程有原自旋机制改为阻塞机制。

AQS有独占和共享两种模式:

  • 独占:只有一个线程能够持有锁
  • 共享:有多个线程可以持有锁

AQS有一个核心的成员变量:state,用来表示当前获取锁的个数。

AQS本身的设计就是可重入的。AQS继承了AbstractOwnableSynchronizer。

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

AQS在ReentrantLock中的应用

ReentrantLock中有两个静态类:NonfairSync和FairSync,他们都继承于Sync。而Sync继承于AbstractQueueSynchronizer。

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

首先我们以非公平锁的加锁、解锁方法为例,去阅读源码。我们设置了俩个线程A、B分别去获取锁,处理业务后再去释放锁。

需要注意ReentrantLock是独占锁,不是共享锁。后序代码讲解会从独占锁出发。

(1)线程A先获取锁

final void lock() {
    // CAS操作,如果当前state为0,说明没有其他线程获得锁,则去获取锁,并将state设置为1。
    if (compareAndSetState(0, 1))
        // 对于线程A,执行该语句。将线程A设置为独占线程
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}
// 该方法位于AbstractOwnableSynchronizer类中
// 独占锁只能有一个线程占有锁。而exclusiveOwnerThread变量标志着哪个线程正在占有锁。
protected final void setExclusiveOwnerThread(Thread thread) {
    exclusiveOwnerThread = thread;
}

此时,A获取完锁。state和exclusiveOwnerThread进行了更行。

由于没有其他线程获取锁失败而进入阻塞状态,因此AQS中队列的head和tail均为null。

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

(2)A正在处理业务,还未释放锁。此时B尝试获取锁。

final void lock() {
    // state已经不是0
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        // B会走到这一步。
        acquire(1);
}
// 继承自AbstractQueueSynchronizer
public final void acquire(int arg) {
    // 如果 获取锁失败 && 该线程出现异常需要被中断。
    if (!tryAcquire(arg) &&
        // 正常情况下,这一步无法获取锁时,会被阻塞
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        // 设置其为中断
        selfInterrupt();
}

tryAcquire(arg)最终调用的是AQS的nonfairTryAcquire方法。

// 尝试获取非公平锁,成功返回true;失败返回false
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    // state=0说明,当前锁已经释放,无线程占用
    if (c == 0) {
        // 尝试获取锁
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 当前线程已经获得过锁,可以再次获得。说明AQS本身设计就是可重入的。
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    // 获取锁失败
    return false;
}

addWaiter(Node.EXCLUSIVE)位于AQS中。会添加包含当前线程引用的节点到队列尾部。如果队列未初始化,则会进行初始化。

// 以 mode 的模式添加一个Node到队尾,返回这个Node的引用。如果队列为初始化,则会先初始化。
// mode的取值有两种:Node.EXCLUSIVE 独占;Node.SHARED 共享。
private Node addWaiter(Node mode) {
    // 该Node中包含了当前线程
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    Node pred = tail;
    // pred不为null,说明队列已经初始化过。
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 初始化链表/不断尝试添加空结点。
    enq(node);
    return node;
}

// 如果链表还未初始化,则初始化带空头节点的双向链表。并将node作为第一个节点。
// 如果链表已经初始化,则不断尝试往队列最后添加node节点
private Node enq(final Node node) {
    // 死循环。不断尝试直到成功。因为只执行一次的话,在多线程的情况下,并不能保证一定会成功。
    for (;;) {
        Node t = tail;
        // 链表为初始化,则进行初始化
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } 
        // 将node添加到
        else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

acquireQueued(node,arg)位于AQS中。死循环:如果node是第一个节点,那么尝试获取锁,成功则返回;如果node不是第一个节点,那么根据其前一个节点的状态,进入阻塞状态 / 清除前驱节点并继续尝试获取锁。

final boolean acquireQueued(final Node node, int arg) {
    //标志出队失败
    boolean failed = true;
    try {
        boolean interrupted = false;
        // 死循环
        for (;;) {
            final Node p = node.predecessor();
            // 如果当前节点是第一个节点,且获取锁成功。对于后续想要抢占锁的线程来说,这个条件短时间无法满足。
            if (p == head && tryAcquire(arg)) {
                // 将该节点出队
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            // 判断当前节点是否需要先阻塞。如果节点的前驱是取消状态,则会将其前驱出队。
            if (shouldParkAfterFailedAcquire(p, node) &&
                // 使用LockSupport阻塞当前线程,并返回当前线程是否被中断(中断恢复后才能返回,因此一定是false)
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        // 该节点可能在执行过程中抛出异常。此时如果没有将节点出队,会执行出队操作(取消掉锁的获取操作)。
        if (failed)
            cancelAcquire(node);
    }
}

正常情况下,B会因为LockSupport.park进入阻塞状态。

(3)线程A释放锁

public final boolean release(int arg) {
    // 如果锁被彻底释放
    if (tryRelease(arg)) {
        Node h = head;
        // 尝试从头唤醒线程。
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

// 尝试解锁,返回锁是否被释放。
protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    // 独占锁只有持有锁(监视器monitor)的线程能够解锁
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    // state=0时,说明该锁已经被放弃
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

以下的总结是针对于ReetrantLock的

使用state表示加锁的数量。state=0时,表示没有线程持有锁。exclusiveOwnerThread记录持有锁的线程,其可以再次加锁(可重入),此时state++。

非公平锁总结:

  • 每次获取锁失败时,会被添加至队列尾部进入阻塞状态。
  • 对于每个新的将要抢占锁的线程,无论队列中是否已经有阻塞的线程,都会自己先去尝试抢占锁。这可能会造成饥饿现象。

公平锁总结:

  • 公平锁和非公平锁的区别在,线程在抢占锁时,只有阻塞队列中没有线程的情况下,才允许尝试抢占。否则加入到队尾并阻塞。

也就是是说,两者仅在线程抢锁时有区别。解锁,线程唤醒时,是没有区别的。

唤醒线程时,从头开始唤醒。

两者代码仅有以下区别:

// 非公平锁
inal boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    // 直接尝试抢占
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

// 公平锁
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        // 如果阻塞队列中有节点,则放弃抢占
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
        }

线程阻塞工具类:LockSupport

LockSupport可以在线程内任意位置让线程阻塞。其弥补了resume()方法导致线程无法继续执行的情况。和Object.wait()方法相比,他不需要事先获得某个对象的锁。(被阻塞时不会释放锁)

public class LockSupportTest implements Runnable {
    static final Object obj = new Object();

    @Override
    public void run() {
        synchronized (obj){
            System.out.println("进入"+Thread.currentThread().getName());
            LockSupport.park();
            System.out.println(Thread.currentThread().getName()+"被唤醒");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        LockSupportTest lst = new LockSupportTest();
        Thread t1 = new Thread(lst);
        Thread t2 = new Thread(lst);
        t1.start();
        t2.start();
        LockSupport.unpark(t1);
        LockSupport.unpark(t2);
        t1.join();
        t2.join();
    }
}

即使unpark发生在park前,线程也能正常被唤醒,因为其底层是修改标志位实现的。当park时,如果发现已经使用了unpark方法,则不会对线程进行阻塞。

Guava和RateLimiter限流

一般化的限流算法有两种:漏桶算法和令牌桶算法。

漏桶算法:

利用一个缓存区,无论请求的速率如何,都先在缓存区内保存,然后以固定的流速流出缓存区进行处理。

令牌算法:

是一种反向的漏桶算法。在令牌算法中,桶中存放的不再是请求,而是令牌。处理程序只有拿到令牌后才能对请求进行处理。如果没有令牌,要么丢弃请求,要么等待可用的令牌。

RateLimiter正是采用了令牌桶算法。

//要先导入Guava包
public class RateLimiterTest {
    static RateLimiter limiter = RateLimiter.create(2);//每秒两个请求

    public static void main(String[] args) {
        while (true){
            limiter.acquire();
            System.out.println(Calendar.getInstance().get(Calendar.SECOND));
        }
    }
}

输出:

20
20
21
21
22
22
23
23

线程池

JDK提供了一套Executor框架

Executors.newFixedThreadPool(10)//返回线程数量固定的线程池
Executors.newSingleThreadExecutor();//返回只有一个线程的线程池
Executors.newCachedThreadPool();//返回一个可根据实际情况调整线程数量的线程池
Executors.newScheduledThreadPool(5);//返回一个ScheduledExecutorService对象,可以在某个固定的延时之后执行,或者周期性执行某个任务

基本用法:

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

        ExecutorService service = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 10; i++) {
            service.execute(()-> System.out.println(Thread.currentThread().getName()));
        }
    }
}

同时还可以自定义指定run方法前的before函数和执行后的after函数。

线程并发容器

Conllections 对集合进行包装

一种获得线程安全容器的方法是使用Collections对集合进行包装:

Map<String, String> map = Collections.synchronizedMap(new HashMap<>());
List<String> list = Collections.synchronizedList(new ArrayList<>());

Collections.synchronizedxxx可以对集合进行包装,获取线程安全类集合。实际上是使用了代理,对原本集合的操作都加了锁(在方法内加锁),因此读写的性能都很差。下面以线程安全的HashMap为例:

//包装后Map源码
private static class SynchronizedMap<K,V>
        implements Map<K,V>, Serializable {
        private static final long serialVersionUID = 1978198479659022715L;

        private final Map<K,V> m;     // 代理的map
        final Object      mutex;        // 使用该字段进行互斥
		
    	//可以看见对原本map的方法进行了上锁
        public int size() {
            synchronized (mutex) {return m.size();}
        }
        public boolean isEmpty() {
            synchronized (mutex) {return m.isEmpty();}
        }
    	......
}

这样的集合性能无疑是极差的。好在JDK提供了一系列高效的线程安全的集合。下面一一介绍。

ConcurrentHashMap:JDK自带的线程安全的集合

ConcurrentHashMap<String, String> hashMap = new ConcurrentHashMap<>();

以下针对jdk1.7

对于ConcurrentHashMap,其内部细分了若干个小的HashMap,称之为段(SEGMENT)。在默认情况下,分为16段。

如果要添加新的表项,并不是将整个HashMap加锁,而是首先根据hashcode得到该表项应该被存放到哪个段中,然后对该段加锁,完成put操作。如果多个线程同时进行put方法,只要被加入的表项不存放在同一个段中,线程间便可以做到真正的并行。

减小锁粒度会带来新的问题,当系统需要取得全局锁时,其消耗的资源比较多。例如得到Map的size需要给所有的段加锁。

1.8以后还是使用了桶+链表+红黑树的结构

此外,ConcurrentHashMap只对写、删除操作进行加锁(synchronize),对读操作是不进行加锁的。因此该集合要比使用Conllections得到的线程安全的包装集合的性能要好。

高效读写的队列:ConcurrentLinkedQueue

ConcurrentLinkedQueue应该算是在高并发环境中性能最好的队列。主要采用CAS无锁操作实现。

高效读取:CopyOnWriteArrayList

读取完全不用加锁,写入也不会阻塞读取操作。只有写入和写入之间需要进行同步等待。适合读多写少的场景。

具体实现:

1、写入操作会加锁ReentrantLock,但是读取不会加锁。

2、在进行写入操作时,会先拷贝原数据的副本,然后在副本上修改,修改完成后再赋值给原数据。
这样写操作就不会影响读操作。

下面是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();
    }
}

数据共享通道:BlockingQueue

实现消费者生产者模式。可到“并行模式”中的“生产者模式”查看使用。

阻塞方法:获取数据:take() 和 插入数据:put()。

无阻塞/阻塞指定时间方法:获取数据 poll() 和 插入数据offer()

// BlockingQueue和List一样是接口
// 初始化时需要制定容量和是否公平(默认为false)
// 如果是公平的,则按公平锁的实现方式,先到先得
BlockingQueue<String> bq = new ArrayBlockingQueue<String>(2);
// 获取队列的头元素,若在指定的时间内没有得到返回则会返回null
String poll = bq.poll(1, TimeUnit.SECONDS);
// 获取队列的头元素,若没有获取到(队列为空),则会阻塞
String take = bq.take();
// 插入元素,队列满时插入失败
bq.offer("a");
// 插入元素,队列满时会一直等待
bq.put("b");

主要是使用take和put两个会阻塞的方法实现生产者-消费者模式。

put方法实现细节:

public void put(E e) throws InterruptedException {
    checkNotNull(e);
    final ReentrantLock lock = this.lock;
    // 写操作需要加锁
    lock.lockInterruptibly();
    try {
        while (count == items.length)
            // 当队列满时,阻塞,并且会释放锁。await方法详见重入锁的阻塞。
            notFull.await(); //会调用notEmpty.signal();
        enqueue(e);
    } finally {
        lock.unlock();
    }
}

//offer方法于put方法实现细节在于
//offer方法不会一直阻塞。如果使用不带时间参数的offer方法,在队满时插入会失败直接返回false
//如果使用带时间参数的offer方法,在队满时,只会等待指定时间。时间过后仍然是队满则会返回false

take方法实现细节

public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    // 这里注意take方法读取数据也是要加锁的
    lock.lockInterruptibly();
    try {
        while (count == 0)
 			// 当队列为空时,阻塞,并且会释放锁
            notEmpty.await();// 会调用notFull.signal();
        return dequeue();
    } finally {
        lock.unlock();
    }
}
// poll方法和take方法的不同在于
// 如果使用不带时间参数的poll方法,队空时会直接返回null,不会阻塞等到队列有数据放入
// 如果使用带时间参数的poll方法,队空时会等待指定时间,若指定时间过后仍然是空,则会返回null

锁的优化

有助于提高锁性能的几点建议

1、减少锁的持有时间。

2、减少锁粒度。

1、2在代码中体现为只给有必要加锁的代码加锁

3、利用读写分离锁来替换独占锁。(在读多写少的场合使用读写锁可以有效提升系统的并发能力)

4、锁分离。(例如读写锁、LinkedBlockingQueue中取数和增加数据是由两个锁控制的:一个发生在队首、一个发生在队尾)

5、锁粗化。虚拟机在遇到一连串地对同一个锁不断进行请求和释放的操作时,便会把所有的锁操作整合成对锁的一次请求。

虚拟机对锁优化所做的努力

锁偏向

如果一个线程获得了锁,那么锁就进入偏向模式。当这个线程再次请求锁时,无须再做任何同步操作。这样就节省了大量有关锁申请的操作,从而提高了程序性能。

轻量级锁

传统的加锁方式为“重量级锁”。轻量级锁简单的将对象头部作为指针指向持有锁的线程堆栈的内部,来判断一个线程是否持有对象锁。如果获取锁成功则可以进入临界区。如果轻量级锁加锁失败,则表示其他线程抢先争夺到了锁,那么当前线程的锁请求就会膨胀为重量级锁。

自旋锁

锁膨胀后,为了避免线程真实地在操作系统层面挂起,虚拟机还会做最后地努力——自旋锁。虚拟机会让当前线程做几个空循环(自旋),在经过若干次循环后,如果可以得到锁,那么久顺利进入临界区。如果还是不能获得锁,才会真的将线程在操作系统层面挂起。

锁消除

JVM在JIT编译时,通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁。(通过逃逸分析来判断那些资源不存在竞争)。例如线程安全类集合Vector在进行操作时会加锁,当只有单线程使用vector时,不存在资源竞争,此时可以消除锁。

ThreadLocal线程专属资源(那种资源是线程专属的?现在来测试!)

ThreadLocal实际上是一个存放在当前线程的Map,可以存放每个线程互不干扰的变量。

现在测试线程共享数据。

1、多个Thread使用同一个Runnable实现者。(多个线程是共享ShareThread内的数据)

public class ThreadLocalTest {
    static class ShareThread implements Runnable{
        private volatile int num = 0;

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

        @Override
        public void run() {
            while (true){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+" : "+num);
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ShareThread t1 = new ShareThread();
        new Thread(t1).start();
        new Thread(t1).start();
        Thread.sleep(2000);
        t1.setNum(10); 
    }
}

输出:

Thread-0 : 0
Thread-1 : 0
Thread-0 : 10
Thread-1 : 10
Thread-0 : 10
Thread-1 : 10

2、多个线程使用不同的Runnable实现对象。(对象的实例数据不共享,类的数据是共享的)

下面是普通变量:

ShareThread t1 = new ShareThread();
ShareThread t2 = new ShareThread();
new Thread(t1).start();
new Thread(t2).start();
Thread.sleep(2000);
t1.setNum(10);

输出:

Thread-0 : 0
Thread-1 : 0
Thread-1 : 0
Thread-0 : 10
Thread-1 : 0

下面是静态变量:

private static volatile int num = 0;//将num变为静态变量
...
ShareThread t1 = new ShareThread();
ShareThread t2 = new ShareThread();
new Thread(t1).start();
new Thread(t2).start();
Thread.sleep(2000);
t1.setNum(10);

输出:

Thread-1 : 0
Thread-0 : 0
Thread-1 : 10
Thread-0 : 10
Thread-0 : 10
Thread-1 : 10

3、使用ThreadLocal。(数据不共享)

这里要注意,ThreadLocal只能在本线程内(run方法内)使用才有效。

public class ThreadLocalTest {
    static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
    static class ShareThread implements Runnable{
       
        @Override
        public void run() {
            threadLocal.set(new Random().nextInt(10));
            while (true){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+" : "+threadLocal.get());
            }
        }
    }

    public static void main(String[] args)  {
        ShareThread t1 = new ShareThread();
        new Thread(t1).start();
        new Thread(t1).start();
    }
}

输出:

Thread-0 : 6
Thread-1 : 3
Thread-0 : 6
Thread-1 : 3

我们可以看下ThreadLocal设置数据的源码:

public void set(T value) {
    Thread t = Thread.currentThread();//获取当前线程,所以只有在本线程内才有效
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

无锁CAS

无锁的策略使用一种叫比较交换(CAS,CompareAndSwap)

CAS算法包含三个参数CAS(old,expect,new),old代表要更新的变量,expect代表预期值,new代表新值。仅当old值等于expect值时,才会将old设置为new。否则就代表有其他线程做了更新,则当前线程什么也不做。

无锁存在的ABA问题

ABA问题:假设线程1读取变量值为A,进行计算等操作后,欲将新值赋给变量。但在这期间变量被多次修改如:从A改到B后又改到A。这时线程进行CAS操作时,会成功。(内涵语义是,该变量没有被其他线程修改过,实际却不是这样!)这就是ABA问题。

在大部分情况下ABA并不会造成影响,因为有的业务并不关心过程,而只关心结果。

如何解决ABA问题?附加额外数据,记录修改的操作步数(读取操作对数据无影响,不用记录)。这个额外数据可以是版本号,也可以是时间戳(实际上这就是乐观锁)。在进行CAS操作时,对象值和时间戳都必须满足期望值才能写入成功。

ABA问题举例:

  • 小明在提款机,提取了50元,因为提款机问题,有两个线程,同时把余额从100变为50
    线程1(提款机):获取当前值100,期望更新为50,
    线程2(提款机):获取当前值100,期望更新为50,
    线程1成功执行,线程2某种原因block了,这时,某人给小明汇款50
    线程3(默认):获取当前值50,期望更新为100,
    这时候线程3成功执行,余额变为100,
    线程2从Block中恢复,获取到的也是100,compare之后,继续更新余额为50!!!
    此时可以看到,实际余额应该为100(100-50+50),但是实际上变为了50(100-50+50-50)这就是ABA问题带来的成功提交。
  • 解决方法: 在变量前面加上版本号,每次变量更新的时候变量的版本号都+1,即A->B->A就变成了1A->2B->3A。

无锁的线程安全整数:AtomicInteger

AtomicInteger是可变的,并且是线程安全的,对其进行修改的任何操作都是使用CAS指令进行的。

public class AtomicIntegerTest implements Runnable {
    static AtomicInteger ai = new AtomicInteger(0);
    @Override
    public void run() {
        for (int i = 0; i < 100000; i++) {
            ai.incrementAndGet();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        AtomicIntegerTest test = new AtomicIntegerTest();
        Thread t1 = new Thread(test);
        Thread t2 = new Thread(test);
        Thread t3 = new Thread(test);
        t1.start();t2.start();t3.start();
        t1.join();t2.join();t3.join();

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

输出:

300000

可以看见即使没有做任何同步操作,也能正确输出,这也反映了AtomicInteger确实是线程安全的。

incrementAndGet()方法实现[JDK 1.8]:

public final int incrementAndGet() {
    // getAndAddInt() 返回的修改的值,所以这里要+
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

// Unsafe类是对指针进行操做的对象
// Unsafe类中的方法
public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        // 通过变量和偏移找到 对象中的value值(预期值)
        var5 = this.getIntVolatile(var1, var2);
        // 比较预期值和旧值是否相等,相等则替换为新值
        // 如果旧值被修改,则不断循环
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
	
    // 返回修改前的值
    return var5;
}

// native 方法,书上说该方法具备原子性
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

并行模式

生产者消费者模式

利用BlockQueue来实现

public class ConsumerAndProducer {
    static BlockingQueue blockingQueue = new ArrayBlockingQueue(3);
    static AtomicInteger ai = new AtomicInteger(0);
    static final int consumerNum = 2;
    static final int producerNum = 6;


    public static void main(String[] args) {
        Consumer consumer = new Consumer(blockingQueue);
        Producer producer = new Producer(ai,blockingQueue);
        ExecutorService service = Executors.newCachedThreadPool();
        for (int i = 0; i < consumerNum; i++) {
            service.execute(new Thread(consumer));
        }
        for (int i = 0; i < producerNum; i++) {
            service.execute(new Thread(producer));
        }
    }
}

class Consumer implements Runnable{
    private BlockingQueue<Data> blockingQueue;

    public Consumer(BlockingQueue<Data> blockingQueue) {
        this.blockingQueue = blockingQueue;
    }

    @Override
    public void run() {
        while (true){
            try{
                Data data = blockingQueue.take();
                System.out.println("消费者"+Thread.currentThread().getName()+"消费数据:"+data.getNum());
                Thread.sleep(800);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class Producer implements Runnable{
    private AtomicInteger ai;
    private BlockingQueue<Data> blockingQueue;

    public Producer(AtomicInteger ai, BlockingQueue<Data> blockingQueue) {
        this.ai = ai;
        this.blockingQueue = blockingQueue;
    }

    @Override
    public void run() {
        while (true){
            try {
                Data data = new Data(ai.incrementAndGet());
                blockingQueue.put(data);
                System.out.println("生产者"+Thread.currentThread().getName()+"产生数据:"+data.getNum());
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

final class Data{ //缓冲区内的数据
    private int num;

    public Data(int num) {
        this.num = num;
    }

    public int getNum() {
        return num;
    }
}

Future模式

核心思想:当要获得的数据要经过耗时的操作才能的得到时,可以开启其他线程获取RealData,然后立即返回FutureData。FutureData只是形式上的数据,当你使用他时,若RealData已经得到,那么程序会返回RealData,能够正常执行。当RealData还未得到时,当前线程会被阻塞,直到获取到RealData。

Guava中提供的Future利用了回调。

下面使用JDK的Future:

public class FutureModel {
    static class RealData implements Callable<String>{
        @Override
        public String call() throws Exception { //准备数据的线程
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < 10; i++) {
                sb.append(i);
                Thread.sleep(200);//模拟耗时操作
            }
            return sb.toString();
        }
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask<String> task = new FutureTask<String>(new RealData());
        new Thread(task).start();
        String s = task.get();
        System.out.println(s);
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值