多线程学习
一、线程实现的三种方式:文字表述
1.继承thread类,并重写thread类中的run()
2.实现runnable接口,实现runnable接口中的run(),(因为runnable接口中只有一个抽象方法,abrast run(),并且抽象接口是必须被实现的),所以也可以采用lambda表达式的写法去启动线程,比如new Thread(()->System.out.println(“线程已启动”)).start,
3.通过线程池的方式
二、启动线程的三种代码实现方式
1.new Thread().start
2.new Thread(Runnable target).start
3.new Thread(Runnable target).start lambad表达式的写法,具体见下方代码块
public class fd {
// 实现线程的第一种方式,但是有局限性,因为java是单继承,继承了之后该类就不能再继承了
static class myThread extends Thread {
@Override
public void run() {
for (int i = 0; i <= 5; i++) {
try {
TimeUnit.MILLISECONDS.sleep(1);
System.out.println("线程正在执行");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 实现线程的第二种方式,实现runnable接口
static class runnableImpl implements Runnable {
@Override
public void run() {
System.out.println("实现runnable接口");
}
}
public static void main(String[] args) {
// 调用run方法并没有启动线程,仅仅只是调用了一个方法
new myThread().run();
// 通过继承Thread类的方式启动
new myThread().start();
// 通过实现runnable接口方式启动线程,只有
// Thread类中才有start()方法,
// 在源码中看到Thread类中有一个方法可以传入
// Runnable接口类型的参数,
// 所以可以通过下面这种方式进行启动
new Thread(new runnableImpl()).start();
// 这是通过lambda表达式的一种写法(lambda实际上
// 就是对接口的一种实现,可以直接作为参数进行传递,
// 是一种语法糖),
// 也就是说这里使用lambda表达式对Runnable接口
// 中的抽象方法进行了一个实现
// new Thread(() ->
// {
// for (int i = 0; i <= 5; i++) {
// try {
// TimeUnit.MICROSECONDS.sleep(1);
// System.out.println("runnable形式线程");
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
//
// }
// }).start();
for (int i = 0; i <= 5; i++) {
try {
TimeUnit.MICROSECONDS.sleep(1);
System.out.println("主程序正在执行");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
三、三种线程相关方法
-
Thread.sleep() 该方法指定该线程睡眠指定的时间,过了指定时间后,该线程回到就绪状态,等待cpu的调度,但不会释放锁
-
Thread.yield()当前线程让出一下cpu,让其他线程进来执行,当前线程让出一下cpu后直接回到就绪状态,也就是等待队列,也不会释放锁
-
Thread.join()将当前线程上的任务交给另一个线程进行执行,另一个线程执行完了回到当前线程。此处面试题:让t1,t2,t3三个线程有序执行。
public class ThreadLearn {
public static void main(String[] args) {
// testSleep();
Thread t1 = new Thread(new TestJoin(null));
Thread t2 = new Thread(new TestJoin(t1));
Thread t3 = new Thread(new TestJoin(t2));
t1.start();
t2.start();
t3.start();
}
public static class TestJoin implements Runnable {
private Thread thread;
public TestJoin(Thread target) {
this.thread = target;
}
@Override
public void run() {
if (thread!=null){
try {
thread.join();
System.out.println("thread join"+Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
else {
System.out.println("thread join"+Thread.currentThread().getName());
}
}
}
}
四、线程的6种状态
- new—新建
(new.start()启动线程)后 - Runnable运行状态—>运行状态包括就绪状态和执行状态
- TimedWating.有时间限制的等待状态
- Wating.无时间限制需要被唤醒的等待状态
- Blocked.阻塞状态,等待进入同步代码块的锁
- Teminater终止状态
五、关于线程的终止
- 线程执行完后自动终止
- stop() 已经被停用
- interrupt() 该方法也并不是真正的将线程停止,而是在当前线程至一个标记**(isInterrupt(),这个标记是Thread类中的一个静态方法),如果该标记为false,则线程为非中断状态,该标记为true,则线程为中断状态。并且这个标记并不会对当前线程造成任何影响,需要通过代码进行控制,也就是说在非阻塞的线程中,通过一直循环判断当前标记状态来确定当前线程是否需要进行中断,这里的中断指的就是停止运行,比如当前标记为false了,就不进入方法进行执行,线程就中断了,第二种就是阻塞的线程,比如线程中有wait(),join(),sleep()等方法时,就是阻塞的线程,在这三个方法的源码中都抛出了一个interruptexception,所以当当前线程是阻塞的线程,并且调用了interrupt方法时,由于只有运行时的线程才会有isInterrupt()的状态,所以此时会抛出异常,从而执行catch块中的代码,并将isInterrupt状态改为false(非中断状态),结论:当阻塞式的线程调用了中断方法时可以提前结束阻塞的时间,并将状态改为false
interrupt结论:如果仅仅是在当前线程调用了interrupt方法,但不做任何处理的话,线程并不会被中断,所以需要循环判断当前线程的状态标志,根据状态标志做出相应的处理,阻塞式线程会提前结束阻塞的时间,并在catch块中捕获异常,并将状态标志改为false
引发的思考?
interrupt()能否终止wait()?
答:可以,并抛出interrupt异常,wait,join,sleep操作都会被提前终止,并抛出异常,改中断标记为false
sleep是否可以被notify唤醒
答:不能,但是sleep可以被interrupt打断,从而达到快速唤醒的目的。wait必须被唤醒
六、synchronized锁的理解
- synchronized 锁的是对象,底层实现锁的是该对象对象头的前两位,一个对象的对象头是64位(如果是32位jvm就是32位)。
- 一个对象的组成为:对象头,实例数据,对其填充
- 加锁的三种方式:1.new Object,并对object加锁
public class SynchronizedLearn_01 {
int i =1;
Object o = new Object();
public void testSync(){
// 锁object对象,只有拿到这个锁之后,线程才能执行锁里面的代码
synchronized (o){
System.out.println(i);
}
}
}
- 第二种方式:给当前对象加锁
public class Sync_02 {
int i;
int b;
// 这个是给点钱对象加锁
public void getSync() {
synchronized (this) {
System.out.println(i);
}
}
// 上面这种写法可以变成这种
public synchronized void getSync_same(){
System.out.println("同样的效果");
}
}
- 第三种方式:给当前类加锁(每一个类通过类的加载器加载后,都会有一个当前类的对象)
public class Sync_03 {
static int i = 10;
// 这是就是类锁,将当前类的对象上锁
public synchronized static void getSync() {
i--;
}
// 等同于以下代码,在static方法里面是没有this的
public static void getSyncSame() {
synchronized (Sync_03.class) {
i--;
}
}
}
六-一、sync锁的相关问题
先说结论:同步方法和非同步方法在两个线程中是可以同时执行的。
- 同步方法和非同步方法是否可以同时调用
答:可以。非同步方法可以在同步方法的执行过程中被调用,因为非同步的方法不需要获取当前对象的锁
public class Sync_04 {
// 同步和非同步方法是否可以同时调用
public synchronized void getNum(){
System.out.println(Thread.currentThread().getName()+"线程t1开始执行");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"线程t1执行结束");
}
public void getNumNone(){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"非同步线程运行");
}
public static void main(String[] args) {
Sync_04 T = new Sync_04();
new Thread(T::getNum,"t1").start();
new Thread(T::getNumNone, "t2").start();
// 1.8之前的写法
// new Thread(new Runnable() {
// @Override
// public void run() {
// T.getNum();
// }
// }).start();
}
}
2.模拟问题:一个银行账户,只对写方法加锁,不对读方法加锁,这样行不行
答:这样会出现脏读,如果业务允许也可以。最好是能不加锁的地方就不加锁,加锁会使效率低100倍
public class Sync_Bank {
/**
* 模拟银行,只对写加锁,不对读加锁
*/
String name ;
double money;
//对写方法加锁
public synchronized void set(String name,double money){
this.name = name;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.money = money;
}
// 根据传入的名字拿到钱
public double get(String name){
return money;
}
public static void main(String[] args) {
Sync_Bank sb = new Sync_Bank();
new Thread(()->sb.set("张三",100000)).start();
System.out.println(sb.get("张三"));
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(sb.get("张三"));
}
}
3.模拟问题:sync是否是可重入锁?
答:必须是。可重入锁是指,同一个线程去拿到两个加了当前对象锁的方法,这时是可以执行的,如果不是可冲入锁,那么此时就死锁了(例子:在一个加了锁的方法里去调用加了同样锁的另一个方法)
public class Sync_Reentry {
// 可重入锁---在一个加锁的方法中,调用另一个加锁的方法
public synchronized void getReentry(){
System.out.println("第一个加锁的方法");
getReentry2();
}
public synchronized void getReentry2(){
System.out.println("第二个加锁的方法");
}
public static void main(String[] args) {
Sync_Reentry sr = new Sync_Reentry();
new Thread(sr::getReentry).start();
}
}
- 模拟问题:子类继承父类,sync(this)是否是同一把锁?
答:是的
- 模拟问题:程序抛出异常后,是否会释放锁?
答:是,但是在用catch块捕获异常后,就不会释放锁了。比如说当前线程t1释放锁之后,t2线程就会进来执行
public class Sync_TryCath {
// 程序抛出异常后,会释放锁
public synchronized void count(){
int i = 0;
while (true){
i++;
System.out.println(Thread.currentThread().getName()+"线程正在执行");
if(i==5){
try {
int s = i/0; // 这里抛出异常后会释放锁,但是用catch捕获后不会
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
Sync_TryCath st = new Sync_TryCath();
new Thread(st::count,"t1").start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(st::count,"t2").start();
}
}
sync原理
- 早起的时候每一次的获取锁都是重量级的,都需要去找操作系统内核,这时候从用户态切换到内核态势非常耗费资源的
- 后期引入锁升级的概念:
主要有以下几种锁: 1.偏向锁。2自旋锁(轻量级锁)。3重量级锁(os)
偏向锁
- 偏向锁是一种乐观锁,实际上是没有上锁的,只是记录了一下线程ID
- 大多数情况下,锁不仅不由多个线程竞争,往往都由一个线程来获取,所以引用偏向锁,当第一个线程获取锁时,会在Mark word中记录该线程的线程ID,并给一个一位的偏向锁标记。下一次该线程再来获取锁的时候只需要对比这个线程ID,就可以获取锁了
- 线程默认是不会释放偏向锁的,当有其他线程来获取锁时,首先在达到全局安全点时,先暂停持有偏向锁的线程,然后检查该线程是否存活(因为有可能该线程已经执行完毕,但并不会释放锁),如果该线程已经处于销毁状态,那么将对象头至为无锁状态01,然后重新偏向新的线程,如果该线程处于活跃状态,撤销偏向锁,升级为轻量锁,标志为00,此时轻量锁由原持有偏向锁的线程持有,而在竞争的线程会进入自旋状态,等待获取锁。
自旋锁
- 自旋锁主要是通过不断的循环去拿锁,默认循环10次
- 优点:不会让线程频繁的挂起,运行,一直都是用户态,不用切换到内核态
- 缺点:一直占用cpu
JDK1.6 自适应自旋锁
- 根据上一个拿到该锁的线程的执行时间判断,使线程自旋时间变长,如果其他线程拿到锁很少成功的话,就直接忽略自旋操作
重量级锁
- 争抢锁的线程从用户态转换成内核态,效率很低
问题:什么时候用自旋锁,什么时候用重量级锁?
答:加锁代码执行时间长,线程多的时候用重量级锁,执行时间短,线程数比较少的时候用自旋锁。(自旋锁一直占用cpu,重量级锁不占cpu,一直处于等待队列)
注意::在使用sync()的使用,锁的对象不能是String常量,Integer,Long,也就是说基本数据类型就别用了。Integer 的内部有特殊处理,每次值一改变就会产生新对象
锁优化
- 锁细化—>尽量让锁作用在较少的代码块上,让只需要加锁的代码加锁
- 锁粗话—>当对锁的竞争特别激烈的时候,让锁粗话,比如在一个方法中做了很多的锁细化,那么还不如让锁粗化,这样更能减少资源的浪费
volatile (只能保证线程的可见性,没有原子性,并不能代替sync)
Volatile主要有两个作用:
- 1.保证线程可见性 (尽量在简单的变量上使用volatile,在对象上使用volatile,并不会观察到对象中成员变量的值得改变,比如在new一个ArrayList后,list中的数据改变了,并不会被立刻发现,线程睡一秒之后才可以被发现,list中的成员被改变了)
- 2.禁止指令重排序 (原理是内存屏障)
保证线程可见性(一个线程将变量改变后,立刻被另一个线程发现)
- 比如定义了一个变量,有两个线程去用这个变量,java中变量是存储在堆内存中的,堆内存相当于是一个公共的区域,当一个线程想要使用这个变量,他会先去堆内存中复制一份这个变量,然后在自己的线程中去改变这个变量,改变后立刻写会堆内存。但是第二个线程在使用这个变量的时候,不一定什么时候去读取这个改变之后的变量,这期间可能还在用字的复制的副本变量。
- 本质上使用的是(原理)cpu的缓存一致性协议(MESI)不同的线程运行在不同的cpu上
public class V_01 {
//volatile 测试
// 这里在主线程中改为false,t1线程一直都没有发现,使用volatile就不会了,保证了线程的可见性
volatile boolean aBoolean = true;
void set(){
System.out.println("线程"+Thread.currentThread().getName()+"开始了");
while (aBoolean){
// try {
// TimeUnit.SECONDS.sleep(2);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
}
System.out.println("结束了");
}
public static void main(String[] args) {
V_01 v = new V_01();
new Thread(v::set,"t1").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
v.aBoolean = false;
}
禁止指令重排序
- 以前的cpu执行指令是一条一条的执行,现在是并发的执行,比如一条指令执行了一半,另一条也开始执行了,所以cpu会对指令进行一个重新排序的可能,重排序后,和我们本来的执行顺序有可能是不一样的
- 例子:单例模式(禁止指令重新排序的例子,以往经典面试题)
public class V_02 {
// 单例模式:类加载到内存后,只加载一个单例
// 这个是饿汉式,不管用没用到都先创建这个对象
private static final V_02 v02 = new V_02();
// 私有化构造方法
private V_02(){};
public static V_02 getIntance(){
System.out.println("m");
return v02;
}
public static void main(String[] args) {
V_02.getIntance();
}
}
public class V_03 {
// 懒汉式
private static V_03 v03;
private V_03() {
};
public static V_03 getInstance() {
if (v03 == null) {
v03 = new V_03();
System.out.println("我看到你了");
}
return v03;
}
public static void main(String[] args) {
V_03.getInstance();
}
}
线程安全的单例模式
- 这里涉及到的问题就是要不要加volatile,如果不加的话,正常压测也很难测出问题,但在理论上还是会存在执行重排序的问题,只是很难测出
- new 一个对象经过jvm编译后分为三个指令,1.申请一块内存(会给一个默认值) 2.给这个对象的成员变量初始化(就是你真正给的初始值) 3.把值给栈中的对象。所以在超高并发的情况下,有可能指令重排序后,第三步跑到了第二步的前面,在初始化后就给对象赋值了,也就是直接把初始化的值直接给了对象,这时就出问题了,所以要加volatile
public class V_04 {
// 线程安全的懒汉式
private volatile static V_04 v04;
// 私有构造方法
private V_04() {
}
// 提供一个对外的公用的访问方式,保证全局只有一个实例对象
public static V_04 getInstance() {
if (v04 == null) {
synchronized (V_04.class) {
if (v04 == null) {
v04 = new V_04();
}
}
}
return v04;
}
}
AtomicInteger CAS操作(这个目前还么有理解)
public class AtomicInteger_1 {
AtomicInteger count = new AtomicInteger(0);
void m() {
for (int i = 0; i < 1000; i++) {
count.incrementAndGet();//count++
}
}
public static void main(String[] args) {
AtomicInteger_1 a1 = new AtomicInteger_1();
List<Thread> threads = new ArrayList<>();
for (int i=0;i<10;i++){
threads.add(new Thread(a1::m));
}
threads.forEach(o->o.start());
// 不知道问什么,这里必须规定执行的顺序,不然值就是不对的
threads.forEach(o-> {
try {
o.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println(a1.count);
}
}
CAS(无锁优化 自旋)
- Compare And Set
- cas(V,Expected,NewValue)
if(V(要改变的变量)==E(预期值))
V=New
otherwise try again or fail - cas是cpu原语支持的,执行的过程不会被打断,不用担心线程的问题
ABA 问题(比如一个Integer值为1,被一个线程改成了2,又被一个线程改成了1,应该怎么解决)
- 答:加版本号cas(version) atomic类中是有这个类的。-----------ABA问题在基础数据类型上是没问题的,在对象类型是可能有问题的
所有的CAS操作都是unsafe这个类在支撑
unsafe类 == c,c++的指针
- unsafe 可以直接操作内存,直接生成类示例,直接操作类变量或实例变量,执行cas相关操作
- 该类是单例的,在jdk11之前是无法直接使用的,只能通过反射,但是在jdk11,可以直接调用
- 马士兵试了一下,在视频中调用了一下,发现一运行就报错
lock(可以代替sync的锁,cas操作的锁)
- lock中的reentrantLock(可重入锁)
- lock.lock()如同sync,获取不到锁会一直等待中,无法打断(interrept()),除非上一个线程释放锁
public class ReentrantLock_01 {
Lock lock = new ReentrantLock();
// 可重入锁,必须手动释放锁
void getNum() {
lock.lock();
try {
for (int i = 0; i < 10; i++) {
System.out.println(i);
TimeUnit.SECONDS.sleep(1);
if(i==2){
getNum2();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
void getNum2(){
lock.lock(); // sync(this)
System.out.println("线程2start");
lock.unlock();
}
public static void main(String[] args) {
ReentrantLock_01 r = new ReentrantLock_01();
new Thread(r::getNum).start();
}
- tryLock(尝试以设置的时间去获取锁,如果在固定时间获取不到,则返货false,并执行下面的代码,而不是像sync,获取不到锁就阻塞了,在等待时间内可以被interrept打断,并抛出异常)
public class TryLock01 {
Lock lock = new ReentrantLock();
void getNum() {
lock.lock();
try {
for (int i = 0; i < 10; i++) {
System.out.println(i);
TimeUnit.SECONDS.sleep(1);
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
void getNum2(){
boolean islock = false;
try {
islock = lock.tryLock(1, TimeUnit.SECONDS);// sync(this)
System.out.println(islock);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
if(islock)lock.unlock();
}
}
public static void main(String[] args) {
TryLock01 r = new TryLock01();
new Thread(r::getNum).start();
new Thread(r::getNum2).start();
}
}
- LOCK.lockInterruptibly():此方式会等待,当未获得锁的线程调用.interrupt()会被中断等待,并抛出InterruptedException异常,否则会与lock()一样始终处于等待中,直到线程A释放锁。
- - 问什么说lock是可中断锁,sync是不可中断锁?
答:sync在被打断时,非阻塞的线程只会在线程中做一个标记,其他什么也不会做,阻塞式的线程则会抛出一个异常,并将被打断的状态改为false。
lock中,使用interruptibly()方法获取锁,是可以被其他线程使用interrept()方法打断的,类似于sync中打断阻塞式的线程,不同处在于lock中使用interruptibly()获取锁后,也会抛出异常
- 公平锁(reentrantLock才有公平锁,sync没有)
公平锁:线程会先检查等待队列中是否还有其他线程,如果有则进入等待队列,如果没有直接取获取锁
非公平锁:不管等待队列中是否有线程,直接取获取锁
public class FairLock {
// 将参数设置为true则为公平锁,但是也不能完全保证公平
// 有可能一个线程执行完后,第二个线程还没有进入等待队列,第一个线程
// 就又进入队列了
final Lock lock = new ReentrantLock(true);
public void getName() {
for (int i = 0; i < 100; i++) {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "获得了锁");
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) {
FairLock fl = new FairLock();
new Thread(fl::getName,"t1").start();
new Thread(fl::getName,"t2").start();
}
}
countDownLatch(门闩)
- 相当于一个计数器,可以用来等所有线程都执行完了之后做一些操作。
public class CountDown_01 {
public static void main(String[] args) {
Thread[] ts = new Thread[100];
//new 一个countDownLatch
CountDownLatch cd = new CountDownLatch(ts.length);
for (int i=0;i<ts.length;i++){
// 每个线程执行完之后都减一
ts[i] = new Thread(()->cd.countDown());
}
for (int i=0;i<ts.length;i++){
ts[i].start();
}
try {
// 在这里拴住,阻塞,所有线程执行完了之后才可以往下走
cd.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("所有的都执行完了");
}
}
cyclicBarrier(循环栅栏)
- 当线程数满足条件时,执行相应的操作
public class CylicBarrier_01 {
public static void main(String[] args) {
// 第一种方式,在await下面写操作
// CyclicBarrier barrier = new CyclicBarrier(20);
// 这种方式,当有20个线程了之后,走第二个参数
CyclicBarrier barrier = new CyclicBarrier(20,()-> System.out.println("满了"));
for (int i=0;i<100;i++){
int finalI = i;
new Thread(()-> {
try {
barrier.await();
// 第一种方式不能用,是错误的
// System.out.println("满了"+ finalI);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}
cyclicBarrier进阶版phaser
- 在线程的某个阶段做一些事情
- 使用方法1.继承phaser类,重写onadvice方法2.详情在视频多线程与高并发(三)58分钟处
读写锁
- 读锁也叫共享锁
- 写锁也叫互斥锁
- 读写锁主要是一个线程在读的时候另一个线程也可以进来读,写锁主要是在修改数据的时候,其他线程都不能进行读写,这样可以比互斥锁极大的提高效率
public class ReaderWriterLock_01 {
// 互斥锁
static Lock lock = new ReentrantLock();
public static int value;
// 读写锁
static ReadWriteLock rw = new ReentrantReadWriteLock();
// 读锁
static Lock readLock = rw.readLock();
// 写锁
static Lock writer = rw.writeLock();
// 读的方法
public static void read(Lock lock){
lock.lock();
try {
TimeUnit.SECONDS.sleep(1);
System.out.println("read over");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
// 写的方法
public static void writer(Lock lock,int value){
lock.lock();
try {
int result = value;
TimeUnit.SECONDS.sleep(1);
System.out.println("writer over");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
public static void main(String[] args) {
Random random = new Random();
// 传入读锁
Runnable readThread = ()->ReaderWriterLock_01.read(readLock);
// 传入写锁
Runnable runnableWriter = ()->ReaderWriterLock_01.writer(writer,random.nextInt());
for (int i=0;i<2;i++){
new Thread(runnableWriter).start();
}
for (int i=0;i<18;i++){
new Thread(readThread).start();
}
}
}
sermaphore (信号)----用来控制同时执行的线程数
public class Semaphore_01 {
public static void main(String[] args) {
// 允许线程同时执行的个数,这里写两个就表示可以同时又有两个线程执行
// 如果写的是1,就需要一个线程执行完之后另一个线程才能执行
Semaphore s = new Semaphore(2);
new Thread(()-> {
try {
// 获得一个信号,上面给的参数就会减一个
s.acquire();
System.out.println("线程1执行了");
TimeUnit.SECONDS.sleep(3);
System.out.println("线程1又执行了");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
// 释放信号,释放后别的线程才能获取
s.release();
}
}).start();
new Thread(()-> {
try {
// 获得一个信号
s.acquire();
System.out.println("线程2执行了");
TimeUnit.SECONDS.sleep(3);
System.out.println("线程2又执行了");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
// 释放
s.release();
}
}).start();
}
}
exchanger(用于两个线程之间交换数据)
- 交换数据的过程是阻塞的,比如一个线程调用了exchange方法,那么他就阻塞住了,需要有另一个线程也exchange一下,才可以继续执行
多线程与高并发(四)
LockSupport(阻塞线程)
- LockSupport.park() 可以直接阻塞线程
- LockSupport.unpark(要唤醒的线程对象) 可以直接唤醒线程
- 注意事项:如果LockSupport.unpark在 LockSupport.park()之前执行,那么park将无效
LockSupport与wait,notify的区别?
- wait和notify必须在同步代码块(sync)中使用,不然很有可能,一个线程在还没有wait之前,notity就已经执行了,导致该线程一直被阻塞。
- notify 不能指定唤醒某一个线程
- LockSupport不需要在sync中使用,可以直接使用,可以直接唤醒想要唤醒的线程
- 注意:wait会释放锁,notify不释放锁
public class LockSupport_01 {
public static void main(String[] args) {
Thread t1 = new Thread(()->{
for (int i=0;i<10;i++){
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(i);
if(i==5){
// 阻塞线程
LockSupport.park();
}
}
});
t1.start();
try {
TimeUnit.SECONDS.sleep(12);
} catch (InterruptedException e) {
e.printStackTrace();
}
LockSupport.unpark(t1);
}
}
练习题:实现一个容器,提供两个方法,add,size 写两个线程,线程1添加10个元素到容器中,线程2实现监控元素的个数,当个数到5个是,线程2给出提示并结束
- 错误示范,这样并不能保证list中的数据被改变了之后立刻被其他线程发现,因为volatile并不能发现对象中成员变量的改变
public class practice {
/* 实现一个容器,提供两个方法,add,size
* 写两个线程,线程1添加10个元素到容器中,线程2实现监控元素的个数,当个数到5个是,线程2给出提示并结束
*/
volatile static List list = new ArrayList();
public static synchronized void add() {
list.add(new Object());
}
public static synchronized Integer size() {
return list.size();
}
public static void main(String[] args) {
// 添加
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
TimeUnit.SECONDS.sleep(1);
System.out.println(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
add();
}
}).start();
new Thread(()->{
while (true){
if(size()==5){
System.out.println("5个了");
break;
}
}}).start();
}
}
- 通过wait和notify来做真正的实现
- 注意,notify不会释放锁,所以需要再notify后wait一下,释放锁,监视线程才能执行,并且另一个线程需要再notify一下,唤醒添加的线程
public class practice_01 {
/* 实现一个容器,提供两个方法,add,size
* 写两个线程,线程1添加10个元素到容器中,线程2实现监控元素的个数,当个数到5个是,线程2给出提示并结束
*/
public static void main(String[] args) {
List list = new ArrayList();
final Object lock = new Object();
// 先搞一个监视线程
new Thread(()->{
synchronized (lock){
System.out.println("监视线程启动");
if(list.size()!=5) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("监视线程结束");
lock.notify();
}
},"t2").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
synchronized (lock){
for (int i = 0; i < 10; i++) {
System.out.println(i);
list.add(lock);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(list.size()==5){
lock.notify(); // notyfy后不会释放锁,所以需要再wait一下,释放锁
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
},"t1").start();
}
}
- 用countdownLatch(门闩)来实现,监视线程在集合长度!=5时,拴住,添加线程在集合长度==5时拴住,并剪掉一个门闩,让监视线程得以执行
public class CountLatch_practice {
/* 实现一个容器,提供两个方法,add,size
* 写两个线程,线程1添加10个元素到容器中,线程2实现监控元素的个数,当个数到5个是,线程2给出提示并结束
*/
public static void main(String[] args) {
// 搞一个门闩
CountDownLatch latch = new CountDownLatch(1);
CountDownLatch latch1 = new CountDownLatch(1);
// 搞一个list
List list = new ArrayList();
// 搞一个监视线程
new Thread(()->{
if(list.size()!=5){
try {
// 拴住,不能走
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("给出提示,加了5个了");
// 类似wait和notify的使用,让下面的线程继续执行
latch1.countDown();
}
}).start();
// 搞一个添加的线程
new Thread(()->{
for (int i = 0; i < 10; i++) {
list.add(new Object());
System.out.println("add"+i);
if(list.size()==5){
// 剪掉一个门闩,上面的线程就可以执行了
latch.countDown();
// 需要用两个门闩来控制,不然输出的时机会有问题
try {
latch1.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
}
- 用LockSupport来实现
public class LockSupprot_practice {
/* 实现一个容器,提供两个方法,add,size
* 写两个线程,线程1添加10个元素到容器中,线程2实现监控元素的个数,当个数到5个是,线程2给出提示并结束
*/
static Thread t1=null,t2=null;
public static void main(String[] args) {
List list = new ArrayList();
t2 = new Thread(()->{
// 监视线程
// 上来就把自己停止
LockSupport.park();
System.out.println("监视线程启动了");
System.out.println("见识到了");
// 唤醒添加线程
LockSupport.unpark(t1);
});
t1 = new Thread(()->{
for (int i = 0; i < 10; i++) {
list.add(new Object());
System.out.println(i);
if(list.size()==5){
// 唤醒监视线程
LockSupport.unpark(t2);
// 停止自己
LockSupport.park();
}
}
});
t2.start();
t1.start();
}
}
- 作业小题:用两个线程顺序打印A,1,B,2----Z,26
练习题:写一个固定容量的同步容器,拥有put和get方法,以及getCount方法,能支持2个生产者线程以及10个消费者线程的阻塞调用
- 第一种方式,使用sync,wait和notifyAll(有一个问题就是,所有阻塞的线程都都在同一个队列中的,当notifyAll时,会将生产和消费的线程全部唤醒加入到等待队列,有可能发生一种情况生产者线程唤醒后,又竞争得到了锁,有wait了一下,这样就浪费了资源)
public class Container<T> {
// 写一个固定容量的同步容器,拥有put和get方法,以及getCount方法,能支持2个
// 生产者线程以及10个消费者线程的阻塞调用,使用wait和notifyAll来实现
final private List<T> lists = new ArrayList<T>();
// 最多10个元素
final private int MAX = 10;
private int count = 0;
// 生产者,如果容器中达到最大容量,停止生产
public synchronized void put(T t){
while (lists.size()==MAX){ // 这里用while,当线程被唤醒之后再判断一下是不是满的
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
lists.add(t);
++count;
// 唤醒消费者线程
this.notifyAll();
}
// 消费者,如果容器中没有了,就停止消费
public synchronized T getCount(){
while (lists.size()==0){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 移除第一个
T t1 = lists.remove(0);
-- count;
// 唤醒生产者线程
this.notifyAll();
return t1;
}
public static void main(String[] args) {
Container<String> container = new Container<>();
// 启动消费者线程
for (int i = 0; i < 10; i++) {
new Thread(()->{
for(int j=0;j<5;j++){
System.out.println(container.getCount());
}
},"c"+i).start();
}
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 启动生产者线程
for (int i = 0; i < 2; i++) {
new Thread(()->{
for (int j=0;j<25;j++){
container.put(Thread.currentThread().getName()+" "+j);
}
},"p"+i).start();
}
}
}
- 使用lock锁,可以新建两个队列,让生产者停止自己的队列,唤醒消费者线程的队列。
public class ContainerLock<T> {
// 写一个固定容量的同步容器,拥有put和get方法,以及getCount方法,能支持2个
// 生产者线程以及10个消费者线程的阻塞调用,使用lock中的newCondition
Lock lock = new ReentrantLock();
// 新建一个等待队列
Condition producer = lock.newCondition();
// 再新建一个等待队列
Condition consumer = lock.newCondition();
LinkedList<T> list = new LinkedList<>();
// 最多放10个
final int MAX = 10;
// 放了几个
int count = 0;
// 生产者方法
public void put(T t) {
try {
lock.lock();
// 当容器满了,停止生产,
while (list.size() == MAX) {
// 在生产者队列进行阻塞
producer.await();
}
list.add(t);
++count;
// 唤醒消费者队列
consumer.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public T get() {
T t = null;
try {
lock.lock();
// 当容器里面空了,停止消费
while (list.size() == 0) {
// 在消费者队列中阻塞
consumer.await();
}
// 移除第一个
t = list.removeFirst();
count--;
// 唤醒生产者线程
producer.await();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
return t;
}
public static void main(String[] args) {
ContainerLock<String> c = new ContainerLock<>();
// 10个消费者线程
for (int i = 0; i < 10; i++) {
new Thread(()->{
for (int j=0;j<5;j++){
System.out.println(c.get()+j);
}
},"c"+i).start();
}
// 2个生产者线程
for (int i = 0; i < 2; i++) {
new Thread(()->{
for (int j = 0; j < 25; j++) {
c.put(Thread.currentThread().getName()+j);
}
},"p"+i).start();
}
}
}
源码阅读原则
- 读源码很难!理解别人思路!需要数据结构基础,设计模式基础
- 1.跑不起来不读(用debug跑起来,一层一层点进去)
- 2.解决问题就好-目的性
- 3.一条线索到底
- 4.无关细节略过
- 5.一般不读静态
- 6.一般读动态方法
- 需要画两种图:1.方法之间的调用图,具体见电脑桌面 2.类之间的类图。具体见电脑桌面
Lock源码解读(设计模式:模板模式templateMethod)
- 主要使用的是aqs,里面的大概流程cas+volatile state。通过 volatile state 判断当前线程是否获取了锁,如果没获取,加到一个双向链表的等待队列中
AQS(AbstractQueuedSynchronizer)抽象的队列同步器-----原理
- https://blog.csdn.net/u012881584/article/details/105886486?utm_medium=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.channel_param&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.channel_param(详见这个链接,说的很清楚)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-b4adulvK-1606805531182)(8F63E637A8164A6CBDC01517F9BE6834)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qIEeKVNW-1606805531187)(D59B277FBB774617A82A4688A9CC60EF)] - reentrantLock,reentrantReadWriteLock,countDownLath,Semaphore,CylicBarrier都是基于AQS来实现的
- AQS内部维护了一个volatile int state,和一个FIFO(先进先出)的双链表结构的线程等待队列(多线程竞争资源被阻塞时会进入此队列),通过CAS的方式进行锁的获取
- AQS内部分为独占锁(reentrantLock),共享锁(Semaphore,countDownLatch等)
- 由于AQS是一个抽象类,里面没有具体的实现,仅仅是抛出了异常,所以一般都由他的子类做具体的实现。
- 以ReentrantLock来举例,使用的就是子类sync.lock()(这个是互斥锁的方法,sync这个静态内部类中还有其他的分享锁,释放锁的方法)。当同时又三个线程并发抢占锁,线程一抢占成功,线程二,三抢占失败。具体过程为:
- 非公平锁的情况
- 三个线程都通过tryAcquire(int acquires)里面采用cas的方式进行锁的抢占,不论谁抢到了,都将state的状态改为1,并且设置对象独占锁线程为当前线程
- 如果state=0,就说明当前没有线程得到锁,则cas抢锁,抢到了则设置独占对象为当前线程,返回true。如果不为0,说明当前对象的锁已经被其他线程占有,接着判断占有锁的线程是否为当前线程,如果为当前线程则累加state的值,这就是可重入锁的具体实现,累加state值,释放锁的时候也需要依次递减state值。
- 如果即没抢到锁,得到锁的也不是当前线程,则执行addWaiter(Node node)(也就是将当前线程加入到一个双向链表结构的等待队列中,并挂起当前线程,等待其他线程释放锁来唤醒他),创建一个和当前线程绑定的node节点,node为双向链表结构。此时如果tail指针为空,通过一个for(;;)+cas的方式,一直循环直到把当前线程插入到双向链表尾部。这样的好处是可以代替sync锁住整个链表,采用cas(最后一个节点的方式),提高了效率
- addWaiter(Node node)—添加到队列尾部之后,如果他前面的节点是头节点(也就是已经获得锁的节点),就尝试以cas的方式去获取一下锁,获取到了直接返回,下一步阻塞当前线程节点,等待前置节点释放锁后来唤醒队列中的下一个节点
- 公平锁的情况(公平锁自己又实现了一个tryAquire()
- 会先判断state值,如果不为0且获取锁的线程不是当前线程,直接返回false代表获取锁失败,被加入等待队列。如果是当前线程则可重入获取锁。
- 如果state=0则代表此时没有线程持有锁,执行hasQueuedPredecessors()判断AQS等待队列中是否有元素存在,如果存在其他等待线程,那么自己也会加入到等待队列尾部,做到真正的先来后到,有序加锁
VarHandle
- 普通属性原子操作
- 比反射速度快,直接操作二进制码
- VarHandle可以获取任意变量的引用
- 如果要原子性地增加某个字段的值,除了用atomic类,还可以用varHandel,以cas的方式进行相加
- AQS中用的就是varHadle
ThreadLocal
- Spring 的声明式事物用的ThreadLocal(比如有多个数据库连接,spring的声明式事物可以把他们合成一个事物,这样就需要保证每个连接都是一样的,所以采用ThreadLocal,直接从本地map来拿连接,而不是从连接池来拿链接)
- 向一个对象中添加属性,保证只有添加的那个线程访问的到。其他线程访问不到
- 原理(源码)----->当向ThreadLocal中set一个对象的时候,首先获取当前线程,在Thread类中有一个map,将set的对象放到当前线程的map集合中
- ThreadLocal中的内存泄露问题
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YzjIVWYn-1606805531190)(CC856C54AD2F4D92A472C600A0CF1E21)]
- 内存泄露是指有一块内存永远无法被回收
- 在threadLocal.set中,实际上是向threadlocal中的一个map中插入对量,map中的key为this,也就是ThreadLocal,这里的key采取的就是弱引用,这样的好处就是当new ThreadLocal()这个强引用消失时,key的弱引用也会被回收,这时map中的key就会变成null,key变成null之后,就会导致value永远都访问不到,依旧存在内存泄露,所以在每次使用完ThreadLocal后都remove一下
- 详情图见电脑桌面
public class ThreadLocal_01 {
static ThreadLocal<Person> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
// 这个线程获取不到下面线程添加的对象
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(threadLocal.get());
}).start();
// 这个线程添加数据,只有这个线程能获取到
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
Person person = new Person();
person.name="ji";
threadLocal.set(person);
System.out.println(threadLocal.get().name+"t2");
}).start();
}
static class Person{
String name = "2";
}
}
Java中的四种引用
- 强弱软需
- 强引用----------->Object o = new Object(),只要存在当前引用,就不会被垃圾回收。如果o=null,则会被回收
- 软引用(可做缓存使用)----------> SoftReference<byte[]> m = new SoftReference(new byte[1024* 1024 *10])。当堆内存的大小够用时,即时调用System.gc()也不会被回收,当堆内存不够时,会自动回收软引用,回收后,m对象为null。
public class SoftRerfence_01 {
public static void main(String[] args) {
SoftReference<byte[]> m = new SoftReference<>(new byte[1024*1024*10]);
System.out.println(m.get());
System.gc();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(m.get());
//堆内存大小不够时,会自动回收软引用
byte[] b = new byte[1024*1024*15];
System.out.println(m.get());
}
}
- 弱引用------------->只要遇到垃圾回收就会被回收(可以获取到里面的值)。弱引用中的问题在上文ThreadLocal中
- 虚引用------------->直接被垃圾回收回收,根本获取不到里面的值,当对象被回收时,可以通过队列检测到,然后清理堆外内存(操作系统的内存)
多线程与高并发第六节(容器部分)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rG6kIkGn-1606805531193)(CF6DBA6040FE4E6FA404BFC65B1662DC)]
- 物理存储结构只有两种:1.数组-----2.链表
- Vector和hashtable是最原始的版本,所有的方法都带锁的,后来有了hashmap和Collections.sycnMap(new hashMap),后来又有了ConcurrentHashMap
Map 相关
- 向里面插入的效率上hashTable和Collections.sycnMap效率基本差不多,读取的时候ConcurrentHashMap效率最高,非常高
- hashMap是用hash表来实现的,是没有排序的,treeMap使用红黑数来实现的,是排好序的,迭代的效率比较高
- 只有concurrentHashMap,并没有concurrentTreeMap(在红黑树的结构下使用cas操作是非常复杂的),所有如果想要在高并发的情况下对map进行排序,需要使用ConcurrentSkipListMap()(通过跳表来实现)
- 跳表的数据结构图。-----》解释一波,正常的链表在数据特别多时查询是非常慢的,但是使用跳表,在已有的链表上抽出一些重要的节点,再形成一个链表,依次类推,查询的时候就可以依次从上向下查询,提高查询速度
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-j6QZkrrX-1606805531196)(FE260123AC0942F48F59E54281EF605D)]
List相关
- Vector是线程安全的
- 高并发情况用queue,queue是专门为高并发提供的,如果需要去重也可以用currentSet
- List 中的==CopyOnWriteArrayList()==适用于读特别多,写比较少的情况 是线程安全的,写时复制,简单来说,读的时候不加锁,在写的时候加锁,加锁后获取当前数组和当前数组的长度,然后copy一份将数组长度加一,在复制后的集合中添加元素,添加完成后再讲引用指向新的集合
- List 中的Collections.synchList(new ArrayList())
Queue相关
- Queue-> ConcurrentLinkedDeque
- queue.offer,queue.peek,queue.poll
public static void main(String[] args) {
// queue里面的方法都是线程安全的
Queue<String> queue = new ConcurrentLinkedDeque<>();
for (int i = 0; i < 10; i++) {
queue.offer("a"+i);// 相当于list中的add 区别是add填不进去了会抛异常,offer会给一个boolean的返回值
}
// peek 取出第一个元素但不会删掉值
System.out.println(queue.peek());
System.out.println(queue.size());
// poll 取出第一个元素会删掉值
System.out.println(queue.poll());
System.out.println(queue.size());
}
- BlockingQueue–>LinkedBlockQueue无界的阻塞的队列(天生的实现了生产者消费者模型)
- bqueue.put() 如果放满了就会等待(链表是可以一直往里面放的,除非内存满了)
- bqueue.take() 如果取不到了,就等待
- BlockingQueue–>ArrayBlockingQueue有界的阻塞队列
public static void main(String[] args) throws InterruptedException {
// 有界的阻塞队列
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(10);
for (int i = 0; i < 10; i++) {
blockingQueue.put("a"+i);
}
// 如果满了,就会阻塞住,等待消费者来消费
// blockingQueue.put("满了");
// blockingQueue.add("m");// 满了就会报异常
//boolean m = blockingQueue.offer("m");// 成功与否会有一个返回值
}
- BlockingQueue–>DelayQueue可按照时间排序的阻塞队列(用作时间调度)
- 添加到delayQueue队列中的元素需要实现Delayed接口,重写里面的两个方法,一个定义时间排序的规则,一个定义获取时间的方法
- DelayQueue底层实现为PriorityQueue,这个队列直接就可以排序
public class DelayQueue_01 {
static BlockingQueue<MyTack> tacks = new DelayQueue<>();
// 必须实现delayed方法
static class MyTack implements Delayed{
String name;
long runningTime;
MyTack(String name,long runningTime){
this.name = name;
this.runningTime = runningTime;
}
@Override
public long getDelay(TimeUnit unit) {
// 将毫秒转成微秒
return unit.convert(runningTime-System.currentTimeMillis(),TimeUnit.MICROSECONDS);
}
// 指定排序规则
@Override
public int compareTo(Delayed o) {
if(getDelay(TimeUnit.MICROSECONDS)<o.getDelay(TimeUnit.MICROSECONDS))
return -1;
else if (getDelay(TimeUnit.MILLISECONDS)>o.getDelay(TimeUnit.MILLISECONDS))
return 1;
else return 0;
}
@Override
public String toString() {
return "MyTack{" +
"name='" + name + '\'' +
", runningTime=" + runningTime +
'}';
}
}
public static void main(String[] args) throws InterruptedException {
// 获取当前系统时间,毫秒
long now = System.currentTimeMillis();
MyTack t1 = new MyTack("t1",now+1000);
MyTack t2 = new MyTack("t2",now+2000);
MyTack t3 = new MyTack("t3",now+1500);
MyTack t4 = new MyTack("t4",now+2500);
MyTack t5 = new MyTack("t5",now+500);
tacks.put(t1);
tacks.put(t2);
tacks.put(t3);
tacks.put(t4);
tacks.put(t5);
for (int i = 0; i < 5; i++) {
System.out.println(tacks.take());
}
}
}
- comparable使用方法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bdod2z5E-1606805531200)(193AF583CCC34040A2A8F99AE2CDC4FF)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zwi33Lmj-1606805531202)(3FACBBBF802C49C5A2769316A4844E17)] - priorityQueue----DelayQueue底层使用的就是priorityQueue
public static void main(String[] args) {
PriorityQueue<String> priorityQueue = new PriorityQueue<>();
priorityQueue.offer("a");
priorityQueue.offer("g");
priorityQueue.offer("c");
priorityQueue.offer("r");
for (int i = 0; i < 5; i++) {
System.out.println(priorityQueue.poll());
}
}
- blockingQueue----》SynchronousQueue() 容量为0的queue,用于手对手的线程交换
public static void main(String[] args) throws InterruptedException {
// 容量为0
BlockingQueue blockingQueue = new SynchronousQueue<>();
new Thread(()->{
try {
// 阻塞住,没有元素就一直等着
System.out.println(blockingQueue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
// 这里用add,offer方法添加都会报错
blockingQueue.put("aaa");
System.out.println(blockingQueue.size());
}
- TransferQueue------->LinkedTransferQueue(),这个queue的主要区别是有一个transfer方法,这个方法必须添加完之后等到结果才会走
public static void main(String[] args) {
TransferQueue<String> transferQueue = new LinkedTransferQueue<>();
// 搞一个阻塞的消费者线程,拿不到就一直阻塞这
new Thread(()->{
try {
System.out.println(transferQueue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
try {
// transfer也是添加,但是添加完必须被消费,这个线程才会结束
// put是添加完了,这个线程就结束了
transferQueue.transfer("aaa");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Map,List小结
- 只有一个线程就用hashmap,ArrayList,linkedlist
- 高并发情况,线程执行时间比较短,就用ConcurrentHashMap,ConcurrentLinkedQueue
- 代码执行时间特别长,并发量不高,用syncHashMap,syncList
面试题:queue和List区别
- queue主要是针对高并发的,这里面添加了对线程友好的api,比如offer(添加),peek(取出不删除),poll(取出删除)
- 还有blockingQueue阻塞的队列,这里面还有put(阻塞的添加),take(阻塞的取出)
线程池
- 5中状态
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fcPVVY8Z-1606805531204)(DA4C9B7E09304428A094F094C246EB0B)]
接口
- Executor
- ExecutorService
- Callable ---->这个接口和runnable比较像,区别就是这个接口有返回值,比如通过这个线程计算一个数,就可以异步的返回计算的结果
public static void main(String[] args) throws ExecutionException, InterruptedException {
Callable<String> callable = new Callable() {
@Override
public String call() throws Exception {
return "哈哈哈";
}
};
// 创建一个线程池
ExecutorService executorService = Executors.newCachedThreadPool();
// 将执行任务放提交到线程池中,什么时候执行由线程池决定,将返回结果存在future中,异步的
Future<String> submit = executorService.submit(callable);
// 阻塞的从future中获取结果
System.out.println(submit.get());
executorService.shutdown();
}
- FutureTask---->这个接口可以将任务的执行与返回的结果融于一身,这个接口可以将执行完任务的返回值也放到future中。也就是说既是一个runnable也是一个future
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<String> f = new FutureTask<>(()->{
return "哈哈";
});
new Thread(f).start();
System.out.println(f.get());// 阻塞获取futuretask里面的返回值
}
- CompletableFutrue----->可以对多任务进行集中管理,比如有三个任务需要三个任务都执行完,再获取结果,或者三个任务,只要有一个执行完就获取结果。或者对一个任务的结果进行处理。这里的每一步都是异步的
public class CompletableFuture_01 {
public static void main(String[] args) {
long start = System.currentTimeMillis();
// 把这三个任务异步的加到future中
CompletableFuture<Long> tb = CompletableFuture.supplyAsync(() -> getTaob());
CompletableFuture<Long> jd = CompletableFuture.supplyAsync(() -> getJd());
CompletableFuture<Long> pdd = CompletableFuture.supplyAsync(() -> getPdd());
// 必须这三个任务都执行完了,才能够结束
// CompletableFuture.allOf(tb,jd,pdd).join();
// 有一个执行完了就可以结束
CompletableFuture.anyOf(tb,jd,pdd).join();
long end = System.currentTimeMillis();
System.out.println(end-start);
// CompletableFuture.supplyAsync(()->getTaob()).thenApply(String::valueOf).thenApply(str->"price"+str).thenAccept(System.out::println);
}
public static long getTaob() {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
return 1;
}
public static long getJd(){
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
return 4;
}
public static long getPdd() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
return 8;
}
}
两种线程池
- ThreadPoolExecutor(所有的线程都用一个队列)
- ForkJoinPool(分解,汇总线程池。每个线程都有一个队列)
- 分解汇总的任务
- 用很少的线程可以执行很多的任务(子任务)TPE做不到先执行子任务
- cpu密集型
- ThreadPoolExecutor中7个核心参数(背过)
- corePoolSize 线程池中核心线程的个数
- maximumPoolSize 线程池中最大线程数
- keepAliveTime 线程池中无用的线程保持多场时间
- TimeUnit 时间单位,秒,毫秒,微秒
- BlockingQueue 可以放各种装任务的blockingqueue
- ThreadFactory 线程工厂,定义线程的产生方式,阿里开发手册中提出必须指定线程的名字,不然回溯的时候很费劲的发现问题,java中也提供了一种默认的线程生产方式
- RejectedExecutionHandler 拒绝策略,比如线程池中有两个核心线程,来了两个任务,把两个核心线程占了,再来任务时,新的任务加入到队列中,如果队列也满了,则新生产一个线程来处理队列之外的任务,如果达到线程池中的最大线程数,则执行拒绝策略
- jdk默认提供了4种拒绝策略:
- 1.AbortPolicy 抛异常
- 2.DiscardPolicy 扔掉,不抛异常
- 3.DiscardOldestPolicy 扔掉排队时间最长的
- 4.CallerRunsPolicy 调用者处理任务
- 还可以自定义策略,并且一般都会自定义策略,比如把订单消息保存到redis或者kafuka
public class ThreadPoolExecutor_01 {
/**
*
*/
static class Task implements Runnable {
private int i;
public Task(int i) {
this.i = i;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "Task" + i);
try {
// 一直阻塞住当前线程
System.in.read();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public String toString() {
return "Task{" +
"i=" + i +
'}';
}
}
public static void main(String[] args) {
//创建一个线程池
ThreadPoolExecutor tpe = new ThreadPoolExecutor(2, 4, 60,
TimeUnit.SECONDS, new ArrayBlockingQueue(4),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.DiscardOldestPolicy());
// 把执行器放到线程池中,也就是向线程池中添加了8个任务
for (int i = 0; i < 8; i++) {
tpe.execute(new Task(i));
}
System.out.println(tpe.getQueue());
// 再放一个
tpe.execute(new Task(100));
System.out.println(tpe.getQueue());
tpe.shutdown();
}
}
Executors - 线程池的工厂
- SingleThreadExcutors----单线程的线程池
为什么会有单线程的线程池?
线程池有任务队列,直接new Thread()还需要自己进行任务队列的管理
线程池有生命周期管理
- 线程池的底层实现就是new 了一个ThreadPoolExecutor(),核心数为1,最大线程数为1
ExecutorService s = Executors.newSingleThreadExecutor()
- CachedPool
- 这个线程池内部实现核心数为0,最大线程为Inter.MAX_VALUE,队列用的是SynchronousQueue。
- 也就是说这个线程池,来一个任务就会新建一个线程,这样也是不好的,任务不会堆积时用
- 用在线程来的比较急,来一个就必须处理一个
- FixedThreadPool 固定线程数的线程池。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
- ScheduledThreadPoolExecutor 定时任务的线程池 DelayedWorkQueue–可以定时的队列
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
- 并发和并行
- 并发指任务的提交
- 并行值任务执行
ForkJoinPool系列
- ForkJoinPool 可以把多个任务分波执行,比如1000个任务,分两波,或者4波,执行完了之后再汇总
- workStealingPool(每个线程把自己队列中的任务执行完之后,去别的队列尾偷一个过来执行)
ParallemStream 并行流
- 这个流底层使用的也是ForkJoinPool
JMH
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UxkW5Bmi-1606805531206)(1895243B131441AF939F1D0705325F31)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-egUfZkMP-1606805531207)(4923F6816FA14F3FA1E3B412BBCEE9E1)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9KM944s2-1606805531209)(3725033F993F4700B65D901C16364B39)]
public class PS {
static List<Integer> list = new ArrayList<>();
static {
Random random = new Random();
for (int i = 0; i < 1000; i++) {
list.add(100000+random.nextInt(100000));
}
}
static void foreach(){
list.forEach(v->isPrime(v));
}
static void parallel(){
list.parallelStream().forEach(PS::isPrime);
}
static boolean isPrime(int num){
for (int i = 2; i <num/2 ; i++) {
if(num%i==0)
return false;
}
return true;
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gwKn9wra-1606805531211)(7DD04ED7BF054498AA5D2703947C14B9)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YrOl5vmf-1606805531213)(C6BB5D03621545D4BDE0D7E4D7E4C74E)]
public class PSTest {
@Benchmark
@Warmup(iterations = 1,time = 3)// 预热--jvm对多次运行的程序会有个优化,而不是运行class文件,所以需要提前预热
@Fork(5)// 用多少线程去执行
@BenchmarkMode(Mode.Throughput)// 基准测试模式 Throughput吞吐量,看这个方法每秒钟可以执行多少次
@Measurement(iterations = 1,time = 3)// 调用这个方法多少次。一般调用很多次取平均值
public void testForEach(){
PS.foreach();
}
}
Disruptor(内存里用于存储数据的高速队列)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HcihzgPt-1606805531214)(94FC5D04B5FE4569927C0E5B7C8658C2)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7L8Va8rY-1606805531216)(9C435AC91E81475A8684D23516EE417A)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Vc9frfc0-1606805531219)(7212FAB7A79B493BA5D8E8F91B9E433E)]
- 如果环形队列已经满了,但是消费者还没来消费,它是有策略的,常见的一种就是阻塞式的,如果还没被消费,阻塞等待8分钟,等待消费者线程来唤醒,消费过后才会被覆盖
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3rpAG3KU-1606805531221)(A83D1C317D7B4EC788E8B6F73991A69B)]