【java】多线程详解

狂神老师的视频地址:https://space.bilibili.com/95256449

1、定义

1.1、线程和进程的区别

进程:就是程序一次运行过程,或者是正在运行的程序,是动态的。
线程:进程的一部分,是程序内部运行的一条路径,是CPU调度和执行的单位

1.2 小知识

  • 在程序运行时,即使没有创建线程,后台也会有多个线程,例如main()即主线程,gc线程
  • 线程会带来额外的开销,如CPU调度时间,并发控制开销
  • 每个线程在自己的工作内存交互,内存控制不当会导致数据不一致

2、实现方式

  • 继承Thread类,重写run方法
  • 实现Runable接口,其实Thread类也是实现了Runable接口,但是这个方法有个好处,就是只能继承一个类但是接口可以实现多个啊
  • 实现Callable接口,这个有个好处就是支持返回值,而且可以抛出异常

继承Thread

package com.company;

public class Thread_test extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(getName() + i);
        }
    }

    public static void main(String[] args) {
        new Thread_test().start();
        new Thread_test().start();
    }
}

实现Runnable

public class Runable_test implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(getName() + i);
        }
    }


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

实现Callable

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


public class TestPoolThreadCallable implements Callable {
    int a,b;

    public TestPoolThreadCallable(int a, int b) {
        this.a = a;
        this.b = b;
    }

    @Override
    public Integer call() throws Exception {
        return new Integer(a/b);
    }

    public static void main(String[] args) {
        FutureTask ft = new FutureTask(new TestPoolThreadCallable(1,1)); //FutureTask也是实现了Runable接口
        new Thread(ft).start();
        System.out.println(ft.get());
    }
}

3、线程之间的通信和状态

3.1、常用方法:

start() :      启动新的线程,最终调用run()方法

sleep():       让对象等待,进入超时等待状态,将执行机会给别的线程,但是锁是依旧存在的,休眠时间后自动恢复就绪态

yield():      让当前线程让步,把执行机会让给别的优先级更高的线程,执行完后还是就绪态,不一定成功。

join():       等待本线程终止后执行别的线程,其他线程阻塞,可以理解为插队

interrupt():   中断当前的线程 #不建议使用

setPriority():设置优先级,数字越高优先级越高。

注:
sleep()方法有的时候还用于模拟网络延迟,放大问题的发生性,比如线程不安全的时候,如果没有延迟,可能一个线程直接跑完了再跑别的线程了
真正需要延时的话,其实一般不用sleep(),用的是TimeUnit.SECONDS.sleep()

3.2、通信方式:

wait()方法:     进入锁住的区域后阻塞等待,
notify()方法:   通知一个线程 锁被释放了,优先通知优先级高的
notifyall()方法,通知所有线程 锁被释放了

sleep() 和 wait() 的区别:

  • sleep不会释放锁,但是wait会
  • sleep可以在任何地方声明,但是wait必须在同步代码块里
  • sleep需要捕获异常(超时)

举一个例子演示线程之间的通信:

  • 生产者消费者模式:

假如仓库中智能存放一个产品,生产者将产品放入仓库中,消费者再将仓库中产品取走消费

如果仓库中有产品,消费者可以消费,否则停止消费并等待,知道仓库中再次放入产品

如果仓库没有产品,生产者放入产品到仓库,否则停止生产,知道仓库中产品被消费者取走为止

管程法

  • 生产者:负责生产数据的模块(可能是方法、对象、线程、进程)
  • 消费者:负责处理数据的模块(可能是方法、对象、线程、进程)
  • 缓冲区:消费者不能直接使用生产者的数据,他们之间有个缓冲区

生产者将生产好的数据放入缓冲区,消费者从缓冲区拿出数据

完整代码如下:

点击查看代码
//测试:生产者消费者模型


//生产者,消费者,产品,缓冲区
public class TestPC {
    public static void main(String[] args) {
        SynContainer container = new SynContainer();
        new Producer(container).start();
        new Consumer(container).start();
    }
}


//生产者
class Producer extends Thread {
    SynContainer container;
    public Producer(SynContainer container) {
        this.container = container;
    }


    //生成
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("生成了-->" + i + "只鸡");
            container.push(new Chicken(i));
        }
    }


}


//消费者
class Consumer extends Thread {
    SynContainer container;


    public Consumer(SynContainer container) {
        this.container = container;
    }


    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("消费了-->" + container.pop().id + "只鸡");
        }
    }
}


//产品
class Chicken {
    int id; //产品编号


    public Chicken(int id) {
        this.id = id;
    }
}


//缓冲区
class SynContainer {
    //容器大小
    Chicken []chickens = new Chicken[10];
    //计数
    int count = 0;


    //生产者放入产品
    public synchronized void push(Chicken chicken) {
        //如果容器满了,就需要等待消费者消费
        if(count == chickens.length) {
            //通知消费者消费,生产等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        chickens[count++] = chicken;
        //通知消费者消费
        this.notifyAll();
    }


    //消费者消费产品
    public synchronized Chicken pop() {
        //判断能否消费
        if (count == 0) {
            //等待生产者生成,消费者等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        count--;
        Chicken chicken = chickens[count];
        //通知生产者生成
        this.notifyAll();
        return chicken;
    }
}

运行结果如下:

点击查看代码
生成了-->0只鸡
生成了-->1只鸡
消费了-->0只鸡
生成了-->2只鸡
消费了-->1只鸡
生成了-->3只鸡
生成了-->4只鸡
生成了-->5只鸡
生成了-->6只鸡
生成了-->7只鸡
生成了-->8只鸡
生成了-->9只鸡
生成了-->10只鸡
生成了-->11只鸡
生成了-->12只鸡
生成了-->13只鸡
消费了-->2只鸡
消费了-->12只鸡
生成了-->14只鸡
消费了-->13只鸡
消费了-->14只鸡

3.3、线程状态

说到通信方式就不得不讲线程的状态

线程生命周期的阶段

  • NEW : 新建,当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态

  • RUNNABLE : 运行 当就绪的线程被调度并获得CPU资源时,便进入运行状态,run方法定义了线程的操作和功能

  • WAIT : 等待 需要其他线程通知或中断

  • TIME_WAIT : 超时等待,不同于等待的就是可以在指定时间自行返回

  • BLOCK : 阻塞表示线程阻塞于锁

  • TERMINATED :死亡状态 线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束

测试:

  public class TestState {
      public static void main(String[] args) throws InterruptedException {
          Thread thread= new Thread(() -> {
              for (int i = 0; i < 5; i++) {
                  try {
                      Thread.sleep(100);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
              }
              System.out.println("----结束----");
          });

          //观察状态
          Thread.State state = thread.getState();
          System.out.println(state);

          //观察启动后
          thread.start(); //启动线程
          state = thread.getState();
          System.out.println(state);

          while (state != Thread.State.TERMINATED) {
              Thread.sleep(100);
              state = thread.getState();
              System.out.println(state);
          }
      }
  }

console:

NEW
RUNNABLE
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
RUNNABLE
RUNNABLE
----结束----
TERMINATED

4、线程同步

线程同步之前先记录一下企业实际开发的时候,是怎么先多线程的:

其实并不是继承Thread或者实现Runnable,因为耦合性太强,实际的资源类(实际操作类)写成一个类

创建多线程的时候 run() 方法里面具体写资源类的操作,这样就实现了降低耦合性。

4.1、锁不同的分类

  • 公平锁/非公平锁 :区别就是有没有先来后到,按不按申请顺序,很明显syncronized锁是非公平的,因为它可以设置优先级 (一般默认都是非公平)
public ReentrantLock() {
        sync = new NonfairSync();
    }

public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
  • 可重入锁:又叫递归锁,即一个线程在外层获取到锁的时候,进入内层方法的时候会自动获取锁

注意:Synchronized 递归的时候是一把锁,而lock递归的时候是两把锁

  • 独享锁/共享锁:更好理解了,就是看能不能被一个线程独占

  • 互斥锁/读写锁:就是上面独享锁/共享锁的具体表现

  • 乐观锁/悲观锁:这是一种思维,具体看这个笔记 乐观锁,悲观锁以及对应实现

  • 分段锁:简而言之就是分段加锁,它的实例有 ConcurrentHashMap,可以看这篇笔记 Map 常见类型和使用场景

  • 偏向锁/轻量级锁/重量级锁:

偏向锁:就是一段代码一直被一个线程访问,有锁竞争的时候不用,就会用到轻量级锁

轻量级锁:就是一个线程访问的时候,其他线程锁一直自旋的形式获取锁不会阻塞,提高性能

重量级锁:就是另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。(在java 1.6之后就优化了synchronized锁的分类, 即:无锁 --> 偏向锁 --> 轻量级锁 --> 重量级锁 方向是单向的,可以升级不能降级)

  • 自旋锁(CAS) 自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。但是如果说大量锁在竞争一个锁,那性能会很低
public class SpinlockDemo {
    AtomicReference<Object> atomicReference = new AtomicReference<>();

    public void myLock() {
        Thread thread = Thread.currentThread();

        while (!atomicReference.compareAndSet(null,thread)) {  //如果不为空就一直等待

        }
    }
}

4.2、Synchronized和锁

两者的区别:

Synchronized适合锁少量的代码同步问题,Lock适合锁大量的同步代码。

Synchronized用法:

修饰方法时,锁住的是对象实例,不同的对象实例锁不住,当然如果是静态方法那就是锁的对象了,因为静态方法和对象无关嘛

修饰代码块时,这部分代码块会被锁定,不过需要指定对象或一个类,指定对象:synchronized(this) 指定类:synchronized(Test.class)

注:1. synchronized修饰静态方法或者类的时候,锁住的是类。2. 不能修饰接口 3. 不能继承(待议)

这里有八个问题,来辅助深入理解线程锁

1、犹如Synchronized的例子交替+-,是不是一定是incre先执行,如果incre的run方法中加了延迟呢?

答:一定是incre先执行,因为Synchronized修饰的是方法,换句话说就是锁住的是类对象,而代码中只有一个对象,incre先拿到了锁

2、如果代码里加了一个没有synchronized修饰的方法呢?同样上面的场景,如果给incre加个延时,执行顺序呢?

答:很显然,如果方法没有被锁住,那么incre延时了,这个方法就会先执行

3、和第二个问题类似,如果有两个Data对象,那么执行顺序会怎么样?

答:因为锁住的是对象,有两个那么就看谁先到谁先执行了,如果incre加了延时,就是decre先执行

4、如果incre和decre都变成static方法呢?

答:犹如上Synchronized使用方法里说的,修饰静态方法锁住的是类,所以一定是incre先执行,和延迟无关

5、如果incre的是静态,decre不是呢?

答:如果一个锁锁住的是类,一个锁锁住的是对象,那其实也是两个锁,看执行时间

这几个问题,考察Synchronized用法,更深入了解锁锁住的是什么。

例子,两个线程交替加减:

public class Test {
    public static void main(String[] args) {
        Data data = new Data();
        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "A").start();
        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "B").start();
    }
}

class Data {
    private int number = 0;

    public synchronized void increment() throws InterruptedException {
        if (number != 0) {  //应该用while,这里用if是为了引入下面的虚假唤醒的问题
            this.wait();
        }
        number++;
        System.out.println(Thread.currentThread().getName() + "-->" + number);
        this.notifyAll();
    }

    public synchronized void decrement() throws InterruptedException {
        if (number == 0) {  //应该用while,这里用if是为了引入下面的虚假唤醒的问题
            this.wait();
        }
        number--;
        System.out.println(Thread.currentThread().getName() + "-->" + number);
        this.notifyAll();
    }
}

输出

A-->1
B-->0
A-->1
B-->0
A-->1
B-->0
A-->1
B-->0
A-->1
B-->0

但是上面的代码也存在虚假唤醒的问题,如果把线程数增加到四个,就会发现出现了2,3,4这样的异常数据

简单来说,当number=0的时候,A和B同时进入同步代码快,同时加一

线程被唤醒,而不会被通知,中断或超时,防范措施就是等待应该出现在循环中!

把等待放到while后:

A-->1
B-->0
A-->1
B-->0
A-->1
B-->0
C-->1
B-->0
A-->1
B-->0
C-->1
D-->0
A-->1
D-->0
C-->1
D-->0
C-->1
D-->0
C-->1
D-->0

Process finished with exit code 0

Lock的用法

Lock的典型实现类,ReentrantLock,它是一个:默认非公平但可实现公平的,悲观,独享,互斥,可重入,重量级锁。

#阻塞的时候获取锁
tryLock() #尝试获取锁
tryLock(long timeout, TimeUnit unit)

#设置可以被打断
public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
}
#打断线程
interrupt()

condition的出现,可以更精准的控制业务代码的执行,例如多线程按照指定顺序执行:

点击查看代码
public class Test {
    public static void main(String[] args) {
        Data2 data2 = new Data2();
        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                data2.printA();
            }
        }, "A").start();

        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                data2.printA();
            }
        }, "B").start();

        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                data2.printA();
            }
        }, "C").start();
    }
}

class Data2 {

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

    public void printA() {
        lock.lock();
        try {
            //业务代码
            while (number != 1) {
                condition1.await();
            }
            System.out.println(Thread.currentThread().getName() + "-->" + number);
            number = 2;
            condition2.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void printB() {
        lock.lock();
        try {
            //业务代码
            while (number != 2) {
                condition2.await();
            }
            System.out.println(Thread.currentThread().getName() + "-->" + number);
            number = 3;
            condition3.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void printC() {
        lock.lock();
        try {
            //业务代码
            while (number != 3) {
                condition3.await();
            }
            System.out.println(Thread.currentThread().getName() + "-->" + number);
            number = 1;
            condition1.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

4.3、ThreadLocal

ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

看下面的例子,说明线程的ThreadLocal互不影响

public class TestThreadLocal {
    private static ThreadLocal<String> localVar = new ThreadLocal<String>();

    public static void getLocal() {
        System.out.println(localVar.get());
        localVar.remove();
    }

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            localVar.set("A");
            getLocal();
            System.out.println(localVar.get());
        }).start();
        Thread.sleep(1000);
        new Thread(() -> {
            localVar.set("B");
            getLocal();
            System.out.println(localVar.get());
        }).start();
    }
}

console

A
null
B
null

5、锁的实践

5.1、集合并发

ArrayList,LinkedList,HashTap 这些常用的集合,都不是线程安全的,如果放到线程里,就有可能报 ConcurrentModificationException (并发修改异常)

那么怎么解决实现线程安全呢?

  • Vector是线程安全的,不过已经弃用了
  • Clooections.SynchronizedList(new ArratList())
  • CopyOnWriteArrayList() 这才是真正常用的线程安全集合

CopyOnWriteArrayList

// 独占锁
final transient ReentrantLock lock = new ReentrantLock();

// 存放元素的数组
private transient volatile Object[] array;

我们仔细来分析一下上面两个属性,这两个思想是 CopyOnWriteArrayList 的核心 。

lock:ReentrantLock,独占锁,利用锁,插入的时候线复制一份,插入完毕后再复制回去。
array:存放数据的数组,关键是被volatile修饰了,被volatile修饰,就保证了可见性,也就是一个线程修改后,其他线程立即可见。

同时可以看到array数组是被transient修饰的,因为CopyOnWriteArrayList自己实现了线程安全的writeObject readObject

CopyOnWriteList不需要扩容(复制过来修改再复制回去),同时读操作没有加锁,效率也高,Vector就被替代了。

CopyOnWriteArraySet

这里对标的就是HashSet了,其实HashSet底层就是HashMap,当然HashMap也是线程不安全的

而CopyOnWriteArraySet其实底层就是CopyOnWriteArrayList

ConcurrnthashMap

JDK也设计了线程安全的HashTable

相对比hashMap来说,HashTable设计是用来应用线程安全的场景的,所有的get()和put()方法都是synchronized的,但是也有一个问题就是效率不高,每个线程操作的时候,其他线程都会堵塞。所以又出来一个ConcurrenthHashMap。

JDK1.8屏蔽了JDK1.7中的Segment概念,直接使用「Node数组+链表+红黑树」的数据结构来实现,并发控制采用 「Synchronized + CAS机制」来确保安全性

5.2、Callable

@FunctionalInterface
public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}

Callable和Runnable没有直接关系,通过FutureTask关联

 /**
     * Creates a {@code FutureTask} that will, upon running, execute the
     * given {@code Callable}.
     *
     * @param  callable the callable task
     * @throws NullPointerException if the callable is null
     */
    public FutureTask(Callable<V> callable) {
        if (callable == null)
            throw new NullPointerException();
        this.callable = callable;
        this.state = NEW;       // ensure visibility of callable
    }

这个接口它可以打印出来线程的结果,抛出异常

public class Test {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyThread myThread = new MyThread();
        FutureTask futureTask = new FutureTask<>(myThread);
        new Thread(futureTask).start();
        System.out.println(futureTask.get());  //这个get方法可能会产生阻塞
    }
}

class MyThread implements Callable {

    @Override
    public Object call() throws Exception {
        System.out.println("call()");
        return 1024;
    }
}

console:

call()
1024

5.3、常用的辅助类

CountDownLatch

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

`countDownLatch.countDown()` :计数器-1
`countDownLatch.await()` :等待计数归零,在执行后面的动作
public class Test {
    public static void main(String[] args) throws InterruptedException {
        //总数是6,必须要执行任务的时候再使用
        CountDownLatch countDownLatch = new CountDownLatch(6);
        for (int i = 0; i < 6; i++) {
            new Thread(()->{
                System.out.println(Thread.currentThread().getName() + "Go out");
                countDownLatch.countDown(); //计数-1
            },String.valueOf(i)).start();
        }
        countDownLatch.await(); //等待计数归零
        System.out.println("close door");
    }
}

console

0Go out
2Go out
1Go out
3Go out
4Go out
5Go out
close door

CyclicBarrier

cyclicBarrier.await():加法计数器
cyclicBarrier.reset(): 重置计数器
cyclicBarrier.getNumberWaiting: 获得CyclicBarrier阻塞的线程数量

一个同步帮助,允许一组线程相互等待,以达到一个共同的障碍点。如果没达到这个障碍点,线程都不会往下执行

public class Test {
    public static void main(String[] args) throws InterruptedException {
        CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> {  //等到满足计数器为7,执行召唤神龙线程
            System.out.println("召唤神龙成功");
        });
        for (int i = 1; i <= 7; i++) {
            final int temp = i;
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "收集了" + temp + "颗龙珠");
                try {
                    cyclicBarrier.await(); //加法计数器
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

Console:

Thread-5收集了6颗龙珠
Thread-2收集了3颗龙珠
Thread-3收集了4颗龙珠
Thread-6收集了7颗龙珠
Thread-1收集了2颗龙珠
Thread-4收集了5颗龙珠
召唤神龙成功

CountDownLatch和CyclicBarrier的区别

  • CountDownLatch会阻塞主线程,CyclicBarrier不会阻塞主线程,只会阻塞子线程。
  • 某线程中断CyclicBarrier会抛出异常,避免了所有线程无限等待。

Semaphore

public class Test {
    public static void main(String[] args) throws InterruptedException {
        //线程数量,这里就是停车位数,限流
        Semaphore semaphore = new Semaphore(3);
        for (int i = 0; i < 6; i++) {
            new Thread(() -> {
                try {
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName() + "拿到车位");
                    TimeUnit.SECONDS.sleep(2);
                    System.out.println(Thread.currentThread().getName() + "离开车位");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    semaphore.release();
                }
            }, String.valueOf(i)).start();
        }
    }
}

Console

0拿到车位
2拿到车位
1拿到车位
0离开车位
1离开车位
2离开车位
4拿到车位
3拿到车位
5拿到车位
5离开车位
4离开车位
3离开车位

原理
semaphore.acquire() : 获得,假如资源已经满了,等待,等待被释放位置
semaphore.release() :释放,会将当前的信号量释放+1,然后唤醒等待的线程

作用:多个共享资源互斥的使用!并发限流,控制最大线程数

5.4、读写锁

ReentrantReadWriteLock

实现了读不加锁,写加锁。和ReentrantLock很像也有Sync,而且WriteLock和ReadLcok都是依赖于它的。

这里的读锁就是共享锁,写锁就是独享锁

public class TestReentrantReadWriteLock {
    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    private ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
    private ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();


    public void read() {
        try {
            readLock.lock();
            System.out.println(Thread.currentThread().getName() + ":读取内容");
            Thread.sleep(3000);
            System.out.println(Thread.currentThread().getName() + ":读取内容完毕");
        } catch (Exception e) {
            System.out.println(e.getCause());
        } finally {
            readLock.unlock();
        }
    }


    public void write() {
        try {
            writeLock.lock();
            System.out.println(Thread.currentThread().getName() + ":写入内容");
            Thread.sleep(3000);
            System.out.println(Thread.currentThread().getName() + ":写入内容完毕");
        } catch (Exception e) {
            System.out.println(e.getCause());
        } finally {
            writeLock.unlock();
        }
    }


    public static void main(String[] args) {
        final TestReentrantReadWriteLock test = new TestReentrantReadWriteLock();
        for (int i = 0; i < 3; i++) {
            new Thread(()->{
                test.read();
            }).start();
        }
        for (int i = 0; i < 3; i++) {
            new Thread(()->{
                test.write();
            }).start();
        }
    }
}

Console

Thread-0:读取内容
Thread-1:读取内容
Thread-2:读取内容
Thread-2:读取内容完毕
Thread-0:读取内容完毕
Thread-1:读取内容完毕
Thread-3:写入内容
Thread-3:写入内容完毕
Thread-4:写入内容
Thread-4:写入内容完毕
Thread-5:写入内容
Thread-5:写入内容完毕

6、线程池

线程池的优势:帮你管理线程,重复利用,减少线程的重复创建,控制最大并发数。

6.1、阻塞队列BlockingQueue

类结构:

从下图可以了解阻塞队列的类结构,还有其他的双端队列和非阻塞队列:

API文档中阻塞队列的介绍:

阻塞队列四组API

接口实现类的详细介绍:

  • ArrayBlockingQueue: 基于数组实现的有界的阻塞队列,该队列按照FIFO(先进先出)原则对队列中的元素进行排序。
  • LinkedBlockingQueue: 基于链表实现的阻塞队列,该队列按照FIFO(先进先出)原则对队列中的元素进行排序。
  • SynchronousQueue: 和其他的阻塞队列不同,不存储元素,put一个元素得先take一个元素。
  • PriorityBlockingQueue: 具有优先级的无限阻塞队列。
  • 我们还能够通过实现BlockingQueue接口来自定义我们所需要的阻塞队列。

这里记录一下SynchronousQueue的例子:

public class TestBlockQueue {
    public static final BlockingQueue BLOCKING_QUEUE = new SynchronousQueue();

    public static void main(String[] args) {
        BlockingQueue BLOCKING_QUEUE = new SynchronousQueue();
        new Thread(() ->{
            try {
                System.out.println(Thread.currentThread().getName()+ "压入元素1");
                BLOCKING_QUEUE.put("1");
                System.out.println(Thread.currentThread().getName()+ "压入元素2");
                BLOCKING_QUEUE.put("2");
                System.out.println(Thread.currentThread().getName()+ "压入元素3");
                BLOCKING_QUEUE.put("3");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();

        new Thread(() ->{
            try {
                Thread.sleep(3);
                System.out.println("获取元素" + BLOCKING_QUEUE.take());
                Thread.sleep(3);
                System.out.println("获取元素" + BLOCKING_QUEUE.take());
                Thread.sleep(3);
                System.out.println("获取元素" + BLOCKING_QUEUE.take());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
}

console

Thread-0压入元素1
获取元素1
Thread-0压入元素2
获取元素2
Thread-0压入元素3
获取元素3

6.2、线程池使用

  • 三个方法,七个参数,四个拒绝策略

请看源码关于Excutors这个类的描述,就是为了创建Excutor实例的工厂类

Factory and utility methods for {@link Executor}, {@link
ExecutorService}, {@link ScheduledExecutorService}, {@link
ThreadFactory}, and {@link Callable} classes defined in this
package. This class supports the following kinds of methods:
  • 通过Excutors创建线程池(三个方法)
// Executors 工具类、三大方法
public class TestThreadPool {
    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newSingleThreadExecutor();  //单个线程
        //ExecutorService threadPool = Executors.newFixedThreadPool(10);   //固定线程池大小
        //ExecutorService threadPool = Executors.newCachedThreadPool();    //可伸缩,空闲线程不占空间

        for (int i = 0; i < 10; i++) {
            threadPool.execute(()-> {
                System.out.println(Thread.currentThread().getName() +  " ok");
            });
        }
    }
}

注:这里老师讲了一个注意事项就是阿里的开发手册里面不允许使用Executors创建线程池,通过ThreadPoolExecutor的方式来创建,更明确线程池的运行规则,避免资源耗尽的风险

说明:Executors创建线程的弊端如下:

1) SingleThreadExecutor和FixedThreadPool
允许的请求队列长度是Integer.Max_Value;可能会堆积大量的请求,导致OOM。

2) CachedThreadPool()和ScheduledThreadPool:
允许创建的线程数Integer.Max_Value;可能会创建大量的请求,导致OOM。

  • 通过ThreadPoolExecutor创建线程池(七个参数)

ThreadPoolExcutor类

public ThreadPoolExecutor(int corePoolSize,            //核心线程数
                          int maximumPoolSize,         //线程池最大线程数
                          long keepAliveTime,          //无人调用超时释放时间
                          TimeUnit unit,               //时间单位
                          BlockingQueue<Runnable> workQueue,  //阻塞队列
                          ThreadFactory threadFactory,        //线程工厂,用来创建线程的,一般不用动
                          RejectedExecutionHandler handler) { //拒绝策略
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
}

private static final RejectedExecutionHandler defaultHandler =
    new AbortPolicy();

一个情景解释这几个参数的作用:银行有两个24小时窗口(核心线程数),如果排队大厅(阻塞队列)人满为患,则启动备用窗口(总共有最大线程数-核心线程数)。如果这个时候还不够,那么处理门外的客户(拒绝策略)。如果多开的窗口等待了一段时间(超时释放时间)还没人来,就关闭。

  • RejectedExecutionHandler (四个拒绝策略)

1)AbortPolicy :阻塞队列也满了,抛出异常
2)CallerRunsPolicy :让创建线程的线程(例如main线程)办理
3)DiscardOldestPolicy :队列满了,尝试和最早的线程竞争,看竞争结果,也不抛出异常
4)DiscardPolicy :直接放弃,不抛出异常,也不处理,

6.3、线程池的一些常用函数

class MyRunnable implements Runnable {

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "执行");
    }
}

class MyCallable implements Callable {

    @Override
    public Object call() throws Exception {
        System.out.println(Thread.currentThread().getName() + "执行");
        return "hello!";
    }
}

public class ThreadPoolTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService threadPool = Executors.newFixedThreadPool(10);
        //execute(Runnable) 异步执行
        threadPool.execute(new MyRunnable());
        //submit(Runnable) 异步执行,返回一个Future,如果任务执行完成,future.get()方法会返回一个null
        Future<?> submit = threadPool.submit(new MyRunnable());
        System.out.println(submit.get());
        //submit(Callable) 异步执行,返回一个Future,有返回结果
        Future<?> submit1 = threadPool.submit(new MyCallable());
        System.out.println(submit1.get());
        //shutdown() 等待执行完毕关闭,shutdownNow()试图阻止所有积极执行任务
        threadPool.shutdown();
    }
}
posted @ 2022-06-03 12:01  吴承勇  阅读(109)  评论(0编辑  收藏  举报