java多线程

多线程

概述

串行 并行 并发

串行sequential 是排队进行任务,比如接力

并行parallel 所有任务同时进行,一边打游戏一边听歌

并发concurrent 是在一个任务等待的时候,开始进行另外一个任务,

线程的生命周期

1646310520596

• 新建状态:
使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。

• 就绪状态:
当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。

• 运行状态:
如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。

• 阻塞状态:
如果一个线程执行了sleep(睡眠)、yield 等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:

• 同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。

• 其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。

• 等待状态:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。可以通过join()转换成就绪

• 死亡状态:
一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。

线程安全

  • 有序性: 代码的执行顺序, 和编写的顺序可能没有保障
  • 可见性: 一个线程对数据进行更新 , 另外一个线程可能读新数据也可能读旧数据
  • 原子性: 操作可能无法做到不可分割

有硬件原因: 每个线程拥有自己的堆内存和方法区

多线程编程

线程的创建

在java中,创建一个线程就是创建一个Thread类的对象。

继承Thread

重写run()方法中的代码就是子线程要执行的代码

 
public class Machine extends Thread{
    private int a =1 ;
    @Override
    public void run(){
        //继承Thread类,覆盖run()方法
        a++;
        System.out.println(a);
        sleep(); //进入睡眠状态
    }

使用线程的start()方法来启动该线程,告诉jvm相应的线程准备好了,具体什么时候运行,则由线程调度器决定。并且如果有多个线程启动,线程的执行顺序并不一定。

public static void main(String args[]){
    Machine machine = new Machine();
    machine.start(); //启动线程 会自动执行run方法

    machine.interrupt(); //中断machine线程的睡眠
    yield(); //线程让步
}

实现Runnable接口

有些时候类已经有父类了,就可以实现runnable接口

public class Machine implements Runnable{
    private int a =0;
    @Override
    public void run(){
        // 重载Runnable接口
        a++;
        System.out.println(a);
	}	
}
 

public static void main(String args[]){
    Machine machine = new Machine();
    // 调用Thread 定义的构造方法,传入runnable接口的实现类,
    Thread t1 = new Thread(machine);
    // 启动线程
    t1.start();
}
 

使用匿名内部类,在Tread的构造函数中定义匿名类。

public static void main(String args[]){
    Thread t2 = new Thread(new Runnable(){
        @Override
        public void run(){
           System.out.println('匿名内部类'); 
        } 
    });
    t2.start();

}

常用方法

静态方法
public static Thread currentThread()返回对当前正在执行该方法的线程对象的引用。
public static void sleep(long millisec)在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)。
public static void yield()暂停当前正在执行的线程对象,优先给其他线程执行。

静态方法通过Thead.方法名直接使用。

Thead.currentThread() 返回的当前线程,其实就是调用该方法的线程,在主线程中可以调用子线程的run方法,执行该方法的是主线程而非子线程

public static void main(String args[]){
    // 主线程调用Machine构造方法
    Machine machine = new Machine();
    // 主线程调用Thread 定义的构造方法,传入runnable接口的实现类
    Thread t1 = new Thread(machine);
    // 启动线程 t1线程去执行它的run方法
    t1.start();
    // 调用t1类的run方法,此方法由当前线程(main)执行,不开启新线程
    t1.run();
    
}

实例方法
public final void setDaemon(boolean on)将该线程标记为守护线程(true)或用户线程(false)。
public final void setName(String name)改变线程名称,使之与参数 name 相同。
public final boolean isAlive()测试线程是否处于活动状态(已启动未终止)。
public final void setPriority(int priority)更改线程的优先级(1-10)。一般不设置
public void interrupt()中断线程。

守护线程

为其他线程提供服务,当虚拟机中只有守护线程时,java虚拟机会停止运行

public void interrupt() 并不是真正的中断线程,只是给线程一个标记,可以在线程里拿到这个标记,停止线程

public static void main(String args[]){

    Machine machine = new Machine();
    Thread t1 = new Thread(thread1);
    t1.start();
    t1.interrupt();// 给子线程中断标记
    
}
public class thread1 implements Runnable{
    private int a =0;
    @Override
    public void run(){
		while(true){
            // 如果子线程的中断标志为true,则中断
			if(this.isInterrupted()){
				System.out.println("byebye");
				break;
			}
			System.out.println("工作中");
		}
	}	
}
 

线程同步

通过锁对共享数据的并发访问转换成串行访问,一个线程持有锁才能访问共享数据,一次只能有一个线程持有锁,保证了操作的原子性和有序性,锁的获得和释放动作,隐含着冲刷处理器缓存的动作,保证了可见性。

对数据的访问和修改都需要添加锁,进行同步

可重入:一个线程持有一个锁时,还能申请该锁,称为可重入

争用与调用:内部锁为公平锁,显示lock锁支持公平也支持不公平锁。

粒度:衡量可保护的数据的大小(代码的长度?),大=粗,小=细,过粗会导致申请锁进行多余的等待,过细会增加锁调度的开销。

乐观和悲观

乐观锁认为别人不会操作数据,只会对数据提交时进行验证是否修改,(一般通过加版本号的方式)如果修改则回退该操作。

悲观锁认为别人会修改数据,一次只给一个人锁,释放锁之前其他人不允许操作。

一般想要同步代码。将锁对象设置为static,因为只有使用同一个锁对象才能同步。

公平锁和非公平锁:

  • 公平锁 : 按照时间顺序, 先到先得. 会导致线程饥饿. 性能高 synchronized

  • 非公平锁 : 随机从阻塞队列中选择一个线程. 性能低 ReentrantLock可以在创建锁对象时传true 设置为公平锁

内部锁 synchronized

synchronized关键字修饰,是排他锁,一次只能有一个线程持有 , 获得锁对象的线程, 才能执行锁对象对应的代码

多个线程必须要获取同一个锁对象时 , 才会串行的等待。 多个线程等待同一把锁,叫做同步。不管任何方法只要用的一个锁,同一时间所有只能由一个线程访问。

相当于多个房间(方法或者代码块)都用了一种锁(锁对象),只有拿到唯一一把钥匙的人(线程)才能进入。

修饰代码块

synchronized(锁对象){
	代码块
}

修饰方法

方法example拥有锁 默认对象锁为this

public synchronized void example(){
	代码块
}

修饰代码块的粒度细,效率高

修饰方法的粒度粗,效率低

补充:

  • 某个线程出现异常,会将锁对象释放

举例

public class Test {
	public static void main(String[] args) {
        Test test = new Test();
        new Thread(new Runnable() {
            @Override
            public void run() {
                test.mm();
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                test.mm();
            }
        }).start();
    }
    public void mm(){
//        this指的就是test实例对象,新建的两个线程都是调用test对象的方法,其中一个线程获得test对象的			锁,另外的线程就会进入等待区等待代码执行完,对象锁释放。
        synchronized (this){
            for(int i =0;i<100;i++){
                System.out.println(Thread.currentThread().getName()+i);
            }
        }
    }

以上this指的就是Test实例对象,新建的两个线程都是调用test这个对象的方法,其中一个线程获得test对象的锁,另外的线程就会进入等待区等待代码执行完,对象锁释放。这时会依次执行for循环。

使用this,当两个线程调用的不是同一个对象时,因为使用的不是一把锁,则会并发交替进行。如下

public class Test {
    public static void main(String[] args) {
        Test test = new Test();
        Test test2 = new Test();
        new Thread(new Runnable() {
            @Override
            public void run() {
                // 该线程调用test对象,拿到test对象的锁
                test.mm();
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                // 该对象调用test2对象,拿到test2对象的锁
                test2.mm();
            }
        }).start();
    }

    public void mm(){
//        this指的就是调用该方法的实例对象
        synchronized (this){
            for(int i =0;i<100;i++){
                System.out.println(Thread.currentThread().getName()+i);
            }
        }
    }

使用this作为锁对象, 只有调用同一个实例对象的线程会排队等待

所以有时会使用常量对象作为锁对象 , 所有调用该方法的线程都会排队等待

    public final static Object OBJ = new Object();

    public void mm(){
        synchronized (OBJ){
            for(int i =0;i<100;i++){
                System.out.println(Thread.currentThread().getName()+i);
            }
        }
    }

多个方法用同一把锁 , 也得等待这个锁的释放

public class Test {

    public static void main(String[] args) {
        Test test = new Test();
        Test test2 = new Test();
        new Thread(new Runnable() {
            @Override
            public void run() {
                // obj锁的方法
                test.mm();
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                // obj锁的方法
                test2.mm2();
            }
        }).start();
    }

    public final static Object OBJ = new Object();
    public void mm(){
//       OBJ锁
        synchronized (OBJ){
            for(int i =0;i<100;i++){
                System.out.println(Thread.currentThread().getName()+i);
            }
        }
    }

    public void mm2(){
//        OBJ锁
        synchronized (OBJ){
            for(int i =0;i<100;i++){
                System.out.println(Thread.currentThread().getName()+i);
            }
        }
    }
}

死锁

当多线程顺序中,获得多个锁的顺序不一致,就会导致死锁。

当需要获得多个锁,所有线程获得锁的顺序一致,就能避免死锁。

volatile

volatile作用使变量在多个线程之间可见。强制线程从公共内存中读取变量的值,而不是从工作内存中读取。

没有volatile时,有时候一个线程修改了一个值,另一个线程无法读到,使用volatile修饰变量,则其他线程能够读到。

volatile 可以保证数据的可见性,不能保证原子性,只解决可见性。

synchronized 可以保证数据的原子性和数据的可见性,解决的是多个线程之间的同步性。

    public final volatile Object OBJ = new Object();

Lock 显示锁

Lock为可重入锁,即一个线程获得对象锁后,还能再次获得同一个对象锁,这就是可重入性,

lock显示锁有几个子类,ReentrantLock(),

ReentrantLock

当一个线程获得锁时,对于其他方法中同一个lock对象,其他线程无法获得锁,就无法执行其他方法,例如当t1线程进入test方法,获得lock锁,则其他线程无法调用test2,因为lock是可重入锁,所以t1可以在释放锁之前调用test2。

一般情况下,在try代码块中获得锁,在finally代码块中释放锁。

使用**.lock()**方法,当获得锁后,即使线程被调用.interrupt()方法也不会被中断

使用**.lockInterruptibly()**方法,可以被.interrupt()方法中断

// 定义锁(非公平锁)
static Lock lock = new ReentrantLock();
// 定义锁(公平锁)
static Lock lock = new ReentrantLock(true);

public void test(){
    lock.lock(); // 获得锁,其他线程就会停留尝试获得锁的地方,不会被.interrupt()方法中断
    lock.lockInterruptibly() //  获得锁,可以被.interrupt()方法中断

	代码
	test2();
	lock.unlock(); // 释放锁,随机一个线程获得锁
     
}
static public void test2(){
    lock.lock(); // 

	代码

	lock.unlock(); // 
     
}

对于synchronized内部锁,如果一个线程开始等待锁,只有两种结果,要么获得锁,要么继续等待,所以会产生死锁。

对于ReentrantLock可重入锁来说,还可以在等待过程中,使用interrupt中端线程, 来解决死锁问题

// 定义锁
static Lock lock = new ReentrantLock();

static public void test2(){
    lock.lockInterruptibly(); // 

	while(1){
        // 假设发生死锁
    }

	lock.unlock(); // 
     
}

    // 定义线程类
    static class subThread extends Thread{
        @Override
        public void run() {
			test2();
        }
    }

    public static void main(String[] args) {
        Thread t1 = new subThread();
        Thread t2 = new subThread();
        t1.start();
        t2.start();
        
        Thread.sleep(3000);
        if(t1.isAlive()){t1.interrupt()};
    }

tryLock()

tryLock(Long time, TimeUnit unit) 的作用是, 在给定时间内锁没有被其他线程持有, 当前也没有被中断, 则获得该锁, 获得锁则返回true, 指定时间内没有获得锁, 则不再等待, 返回false.

lock.tryLock(3,TimeUnit.SECONDS) // 最多等待3s
lock.tryLock()  // 锁被持有,直接放弃

以下代码, 一个线程拿到锁之后, 另一个线程最多等待3秒, 超时之后不再等待, 打印我不等了

// 定义锁
static Lock lock = new ReentrantLock();

static public void test2(){
    // 最多等待3秒
    if (lock.tryLock(3,TimeUnit.SECONDS)){
        执行4秒的任务
        Thread.sleep(4000);
    }else{
        System.out.println("我不等了");
    }
	lock.unlock(); // 
     
}

    // 定义线程类
    static class subThread extends Thread{
        @Override
        public void run() {
			test2();
        }
    }

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

lock.isHeldByCurrentThread() 当前线程是否持有锁

lock.getHoldCount() 当前锁被持有的次数

lock.getQueueLength() 等待锁的线程的预估数

lock.getWaitQueueLength(Condition) 该condition进入等待的线程的预估数(调用wait()方法的数量)

lock.hasQueuedThead(Thread) 该线程是否在等待该锁

lock.hasQueuedTheads() 是否有线程在等待该锁

lock.hasWaiters(Condition) 该条件该条件是否有线程在等待

lock.isFair() 是否为公平锁

lock.isLocked() 当前锁是否被线程持有

ReentrantReadWriteLock 读写锁

是改进的排它锁, 共享/排它锁. 允许多个线程同时读取, 但只允许一个线程对数据进行更新.

读锁可以由多个线程持有 , 写锁只允许一个线程持有. 但读锁和写锁不能同时被任何线程持有.

在读锁被持有时, 其他线程可以获得读锁, 但无法获得写锁. 在写锁被线程持有时, 其他线程无法获得任何锁

readLock()与write()返回的是同一个锁的不同角色

//获得读写锁
ReadWriteLock rwLock = new ReentrantReadWriteLock();
// 获得读锁
Lock readLock = rwLock.readLock()
// 获得写锁
Lock writeLock = rwLock.writeLock();

// 读数据
readLock.lock(); // 拿读锁 
try{
	读数据

}finally{
	readlock.unlock();
}

// 写数据
writeLock.lock(); // 拿读锁
try{
	读数据

}finally{
	readlock.unlock();
}

常用原子类

**AtomicInteger类 **

使用 .getAndIncrement()方法可以线程安全自增

AtomicInteger atomicInteger = new AtomicInteger();
atomicInteger.getAndIncrement();


CAS

可以把 read - modify - write 操作, 变得线程安全

例如 i++

线程从主内存拿到变量10放到线程的工作内存 , 对线程进行加1操作, 再写入到主内存

在这个操作过程中, 可能另一个线程也拿到变量, 就会出现两个线程都将值11写入,与预期不符

CAS会在写入到主内存之前, 验证主内存的值和读取到本线程内存的值是否相等, 相等则写入, 不相等则撤销这次操作.

CASCounter casCounter = new CASCounter();
casCounter.incrementAndGet();

但会碰到ABA问题, 数据A变成B再变回A, 会被误认为没有改变.

基于CAS, 改进ABA问题, 有了一些原子变量类

1646574837301

线程通信

等待/通知机制 wait和notify

在一个A线程中, 需要满足条件才能继续执行, 稍后其他的线程B更新条件使得A线程的条件得到满足, 可以将A线程暂停, 直到A线程条件得到满足再将A线程唤醒.

synchronized实现

Object()类的wait()方法可以使执行当前代码的线程等待, 转入阻塞状态 进入等待池 直到接到通知或被中断为止.

  • wait()方法只能在同步代码块当中, 由锁对象调用, 所以必须获得锁才能用wait()方法
  • wait()方法被调用, 会释放锁
  • wait(long) 在long时间内没有被唤醒, 则自动唤醒

Object()类的notify()可以唤醒线程 , 也必须在同步代码块中由锁对象调用, notify()调用后, 会等待当前同步代码块执行完毕才会释放锁对象 如果有多个等待的线程 , 则只能随机唤醒其中的一个. notifyAll() 可以唤醒所有线程.

通知过早

当线程还没有进入等待状态, 使用notify会打乱运行逻辑

当线程处于等待状态时 , 会被本线程的interruput()方法中断, 也会释放锁对象

public class Test {

    public static void main(String[] args) {
        String lock = "test";
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized(lock){
                    System.out.println("t1开始执行");
                    System.out.println("t1开始等待");
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("t1等待结束");
                }
            }
        });

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized(lock){
                    System.out.println("t2开始唤醒");
                    lock.notify(); //释放lock的锁,
                    System.out.println("等待t2同步代码块执行");
                }
            }
        });

        t1.start();
        try {
            Thread.sleep(3_000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t2.start();
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hhehAvzQ-1647140240771)(多线程与高并发/img/1646623949915.png)]

Lock显示锁实现

使用newContition()方法返回Condition对象, Condition对象可以选择唤醒的线程.

await(): 使当前线程等待, 同时释放锁, 当其他线程调用signal()时, 线程会重新获得锁并继续执行.

signal(): 唤醒一个当前condition对象等待队列的线程

调用以上两个方法时, 需要持有相关的lock锁.

不同线程可以使用同一个lock对象,通过使用不同的condition对象,来指定唤醒的的对象的等待队列的线程,

    // 定义锁
    static Lock lock = new ReentrantLock();
    // 获得Condition对象
    static Condition conditionA = lock.newCondition();
    static Condition conditionB = lock.newCondition();
    
    public void waitA(){
    	lock.lock();
    	conditionA.wait()
    	lock.unlock();
    }
    public void waitB(){
    	lock.lock();
    	conditionB.wait()
    	lock.unlock();
    }

	// 假设A B线程两个线程的run()方法为执行waitA(),waitB();
	// 唤醒执行waitA的线程	
	lock.lock();
	conditionA.signal()
    lock.unlock();
	// 唤醒执行waitB的线程
	lock.lock();
	conditionB.signal()
    lock.unlock();

以下例子,子线程在获得锁之后等待,释放锁,主线程睡眠3s后获得锁,唤醒子线程

public class Test {
    // 定义锁
    static Lock lock = new ReentrantLock();
    // 获得Condition对象
    static Condition condition = lock.newCondition();

    // 定义线程类
    static class SubThread extends Thread{
        @Override
        public void run() {
            lock.lock(); //获得锁
            try {
                condition.await();  // 进入等待状态
                System.out.println("解锁");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock(); //释放锁
            }
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        SubThread subThread = new SubThread();
        subThread.start();
        // 线程睡眠
        Thread.sleep(3_000);
        // 先要获取锁之后,才能唤醒
        lock.lock();
        try {
            condition.signal();
        }finally {
            lock.unlock();
        }
    }
}

生产者消费者模式

负责产生数据的为生产者, 负责使用数据的模块是消费者. 先有数据才能使用, 没有数据时,消费者需要等待.

在多生产者多消费者情况下, 使用if作为条件, 会因为等待状态时其他线程改变了条件, 导致消费者取空, 所以要使用while判断条件, 在被唤醒之后再判断一次条件. 此外, 使用notify会导致假死, 所有线程都处于等待状态. 所以要使用notifyAll().

管道实现线程通信

在java.io包中的PipeStream用于线程之间传送数据, 使用PipedInputStream 和 PipedOutputStream管道字节流在线程之间传递数据.

Out写入数据传出去, in读取数据

public class Test {

    public static void main(String[] args) {
        PipedInputStream inputStream = new PipedInputStream();
        PipedOutputStream outputStream = new PipedOutputStream();
        try {
            // 将管道连接
            inputStream.connect(outputStream);
        } catch (IOException e) {
            e.printStackTrace();
        }
        new Thread(new Runnable() {
            @Override
            public void run() {
                writeData(outputStream);
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                readData(inputStream);
            }
        }).start();
    }
    // 写入数据
    public static void writeData(PipedOutputStream out){
        try {
            for (int i=0; i<100; i++) {
                // 把i转成字符串
                String data = "" + i;
                //  将data写入到管道中
                out.write(data.getBytes(StandardCharsets.UTF_8));
            }
            // 关闭管道
            out.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // 读取数据
    public static void readData(PipedInputStream in){
        // 创建byte数组 保存byte
        byte[] bytes = new byte[128];
        try {
            int len = in.read(bytes);  //将字节读入数组,没有读到则返回-1
            while (len!=-1){
                // 把前len个字节转换成字符串打印
                System.out.println("读 :"+new String(bytes,0,len));
                len = in.read(bytes);  //继续读
            }
            in.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


}

ThreadLocal

ThreadLocal可以给每个线程一个自己的值. 这个例子并不完美, 当多个线程共用一个对象的时候可以使用ThreadLocal.

原理大概是, 每个线程都有一个ThreadLocalmap, key为ThreadLocal对象, value为set的值

ThreadLocal未设置初始值时 , 所get的为null,可以重写 initialValue()方法, 定义默认值

public class Test {
    // 定义ThreadLocal对象
    static ThreadLocal threadLocal = new ThreadLocal();

    // 定义线程类
    static class subThread extends Thread{
        @Override
        public void run() {
            for(int i = 0; i< 20; i++){
                // 将值与线程关联
                threadLocal.set(Thread.currentThread().getName()+": "+i);
                // 获取线程相关联的值
                System.out.println(threadLocal.get());
            }
        }
    }
    
    public static void main(String[] args) {
        Thread t1 = new subThread();
        Thread t2 = new subThread();
        t1.start();
        t2.start();
    }

}

修改ThreadLocal的默认值

    static class SubThreadLocal extends ThreadLocal{
        @Override
        protected Object initialValue() {
            return 1;
        }
    }

    // 定义ThreadLocal对象
    static ThreadLocal threadLocal = new SubThreadLocal();

join()

线程管理

线程组

使用线程组定义一组相关的线程, 或者子线程组, 类似于文件夹

Thread类创建线程时可以指定线程组 , 不指定则默认为父类所在的线程组

现在已经淘汰阿

守护线程

调用线程组的 setDaemon(true) 可以把线程组设置为守护线程组. 守护线程组没用活动线程, 则销毁该守护线程组, 守护线程组中可以有守护线程或者活动线程

public class Test {
    public static void main(String[] args) {
        // 定义线程组
        ThreadGroup threadGroup = new ThreadGroup("group");
        // 设置为守护线程
        threadGroup.setDaemon(true);

        // 添加三个非线程
        for (int i=0;i<3;i++){
            new Thread(threadGroup, new Runnable() {
                @Override
                public void run() {
                    while (true){
                        System.out.println("这是线程"+Thread.currentThread().getName());
                    }
                }
            }).start();
        }
    }
}

捕获线程的异常

线程产生异常会优先调用 线程设置的回调接口>线程组设置的回调接口>System.err

线程设置回调接口

        // 设置线程的全局回调接口
        Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
            @Override
            public void uncaughtException(Thread t, Throwable e) {
                System.out.println("发生了异常");
            }
        });

注入Hook钩子线程

在JVM退出时, 会执行Hook线程, 为了防止进程重复启动, 会在程序启动时创建.lock文件, 校验程序是否启动, 在退出时删除该文件, 注意要避免在Hook线程中进行复杂的操作.

    public static void main(String[] args) {
        Runtime.getRuntime().addShutdownHook(new Thread(){
            @Override
            public void run() {
                System.out.println("JVM退出,会启动当前线程");
            }
        });

线程池

核心

线程池是有效使用线程的一种方式 , 线程池内部预先创建一定数量的工作线程 , 客户端任务作为对象提交给线程 , 线程池将任务缓存在工作队列中 , 线程池的工作线程不断地从队列中取出任务并执行

https://www.jianshu.com/p/210eab345423

ThreadPoolExecutor

public ThreadPoolExecutor(int corePoolSize,(必
                          int maximumPoolSize,(必
                          long keepAliveTime,(必
                          TimeUnit unit,(必
                          BlockingQueue<Runnable> workQueue,(必
                          ThreadFactory threadFactory,(选
                          RejectedExecutionHandler handler(选)
    
  // 一共最多有七个参数

  • int corePoolSize 该线程池中核心线程数最大值
    线程池新建线程的时候,如果当前线程总数小于corePoolSize,则新建的是核心线程,如果超过则新建的是非核心线程,核心线程默认情况下会一直存活在线程池中,即使这个核心线程啥也不干(闲置状态)。

    如果指定ThreadPoolExecutor的allowCoreThreadTimeOut这个属性为true,那么核心线程如果不干活(闲置状态)超过一定时间(long keepAliveTime),就会被销毁掉

  • int maximumPoolSize 最大线程数

  • long keepAliveTime 非核心线程存活时长

    一个非核心线程,如果不干活(闲置状态)的时长超过这个参数所设定的时长,就会被销毁掉

如果allowCoreThreadTimeOut这个属性为true , 这个时长也会作用于核心线程

  • TimeUnit unit 存活时长的单位

  • BlockingQueue workQueue

    当所有的核心线程都在干活时,新添加的任务会被添加到这个队列中等待处理,如果队列满了,则新建非核心线程执行任务 , 以下是常见的使用对象

  • SynchronousQueue:( 直接摇人) 这个队列接收到任务的时候,会直接提交给线程处理,而不保留它,如果所有线程都在工作就新建一个线程来处理这个任务!所以为了保证不出现<线程数达到了maximumPoolSize而不能新建线程>的错误,使用这个类型队列的时候,maximumPoolSize一般指定成Integer.MAX_VALUE,即无限大

  • LinkedBlockingQueue:(只用核心线程 队列无界) 这个队列接收到任务的时候,如果当前线程数小于核心线程数,则新建线程(核心线程)处理任务;如果当前线程数等于核心线程数,则进入队列等待。由于这个队列没有最大值限制,即所有超过核心线程数的任务都将被添加到队列中,这也就导致了maximumPoolSize的设定失效,因为总线程数永远不会超过corePoolSize

  • ArrayBlockingQueue:(优先核心, 队满摇人 队列有界) 传入参数限定队列的长度,接收到任务的时候,如果没有达到corePoolSize的值,则新建线程(核心线程)执行任务,如果达到了,则入队等候,如果队列已满,则新建线程(非核心线程)执行任务,又如果总线程数到了maximumPoolSize,并且队列也满了,则发生错误

  • DelayQueue:(入队等候) 队列内元素必须实现Delayed接口,这就意味着你传进去的任务必须先实现Delayed接口。这个队列接收到任务时,首先先入队,只有达到了指定的延时时间,才会执行任务

  • PriorityBlockingQueue: 以上都是先进先出执行任务,这个根据优先级来执行任务

  • ThreadFactory threadFactory 线程工厂 创建线程的方式, 可以自定义

  • RejectedExecutionHandler handler 异常处理, 拒绝策略,当工作队列发生错误时,启用

TimeUnit的单位

NANOSECONDS : 1微毫秒 = 1微秒 / 1000
MICROSECONDS : 1微秒 = 1毫秒 / 1000
MILLISECONDS : 1毫秒 = 1秒 /1000
SECONDS : 秒
MINUTES : 分
HOURS : 小时
DAYS : 天

添加任务

 threadPoolExecutor.execute(Runnable command) 
    public static void main(String[] args) {
        // 创建5个线程的线程池
        ExecutorService threadPool= new ThreadPoolExecutor(5,
                10,
                60L,
                TimeUnit.MINUTES,
                new ArrayBlockingQueue<Runnable>(20));

        for (int i=0; i<30; i++){
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        System.out.println(Thread.currentThread()+"启动");
                        Thread.sleep(1_000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }

常见四种线程池

  • CachedThreadPool() 可缓存线程池

    可缓存线程池:适合耗时短,数量多的任务

    1. 线程数无限制
    2. 有空闲线程则复用空闲线程,若无空闲线程则新建线程
    3. 一定程序减少频繁创建/销毁线程,减少系统开销

    创建方法

    ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
    

    实现:由SynchronousQueue队列实现

    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
    
  • FixedThreadPool()

    定长线程池:

    1. 可控制线程最大并发数(同时执行的线程数)
    2. 超出的线程会在队列中等待

    创建方法

//nThreads => 最大线程数即maximumPoolSize
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(int nThreads);

//threadFactory => 创建线程的方法,
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(int nThreads, 											ThreadFactory threadFactory);

​ 源码: 由LinedBlockingQueue队列实现, 有界队列

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}
  • ScheduledThreadPool()

    定长线程池:

    1. 支持定时及周期性任务执行。

    创建方法:

    
    ExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(int corePoolSize);
    

    源码:使用

    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }
    
    //ScheduledThreadPoolExecutor():
    public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE,
              DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
              new DelayedWorkQueue());
    }
    
  • SingleThreadExecutor()

    单线程化的线程池:

    1. 有且仅有一个工作线程执行任务
    2. 所有任务按照指定顺序执行,即遵循队列的入队出队规则

    创建方法:

    ExecutorService singleThreadPool = Executors.newSingleThreadPool();
    

    源码:使用的LinkedBlockingQueue队列

    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }
    
  • ForkJoinPool()

拒绝策略

RejectedExecutionHandler handler 拒绝策略,当工作队列发生错误时,例如新来一个任务使得线程超出线程池允许最大线程数量,的处理方法

拒绝策略:

  • AbortPolicy: 抛出异常(默认)
  • CallerRunsPolicy:在调用者线程中允许被放弃的任务
  • DiscardOldestPolicy:丢弃最早的任务(马上要被执行的),尝试执行添加的任务
  • DiscardPolicy:丢弃这个无法处理的任务

自定义拒绝策略

 public static void main(String[] args) {

        // 创建5个线程的线程池
        ExecutorService fixedThreadPool= new ThreadPoolExecutor(1,
                1,
                60L,
                TimeUnit.MINUTES,
                new ArrayBlockingQueue<Runnable>(20),
                new Handler());

        for (int i=0; i<30; i++){
            fixedThreadPool.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        System.out.println(Thread.currentThread()+"启动");
                        Thread.sleep(1_000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }
	// 实现该接口 即可
    public static class Handler implements RejectedExecutionHandler{

        @Override
        public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
            // r为出错请求的任务,executor为当前线程池
            System.out.println("处理出错的任务");
        }
    }

ThreadFactory

线程池中的线程,由ThreadFactory创建,它只有一个用来创建线程的方法。

猜测:任务自动放入到了r的run方法中,可以在newThread中实现一些在线程创建时自定义的功能,例如设置为守护线程,使主线程结束时,所有线程结束

        // 创建线程池,自定义线程的创建
        ExecutorService fixedThreadPool= new ThreadPoolExecutor(1,
                1,
                60L,
                TimeUnit.MINUTES,
                new ArrayBlockingQueue<Runnable>(20),
                new ThreadFactory() {
                    @Override
                    public Thread newThread(Runnable r) {
                        Thread t = new Thread(r);
                        System.out.println("创建了一个线程");
                        return t;
                    }
                },
        new Handler());

监控线程池

ThreadPoolExecutor有以下实例方法可以监控线程池

方法作用
getActiveCount()线程池中正在执行任务的线程数量
getCompletedTaskCount()线程池已完成的任务数量,该值小于等于taskCount
getCorePoolSize()线程池的核心线程数量
getLargestPoolSize()线程池曾经创建过的最大线程数量。通过这个数据可以知道线程池是否满过,也就是达到了maximumPoolSize
getMaximumPoolSize()线程池的最大线程数量
getPoolSize()线程池当前的线程数量
getTaskCount()线程池已经执行的和未执行的任务总数

可以重写ThreadPoolExecutor类的以下方法对增加额外功能

方法含义
shutdown()线程池延迟关闭时(等待线程池里的任务都执行完毕),统计已执行任务、正在执行任务、未执行任务数量
shutdownNow()线程池立即关闭时,统计已执行任务、正在执行任务、未执行任务数量
beforeExecute(Thread t, Runnable r)任务执行之前,记录任务开始时间,startTimes这个HashMap以任务的hashCode为key,开始时间为值
afterExecute(Runnable r, Throwable t)任务执行之后,计算任务结束时间。统计任务耗时、初始线程数、核心线程数、正在执行的任务数量、已完成任务数量、任务总数、队列里缓存的任务数量、池中存在的最大线程数、最大允许的线程数、线程空闲时间、线程池是否关闭、线程池是否终止信息

线程池死锁

当线程池中执行A的同时,又提交了任务B,任务B进入等待队列中,如果任务A的结束需要等待任务B的完成,就有可能造成死锁。

同一个线程池提交相互独立的任务,相互联系的任务提交到不同的线程池。

锁的优化

  1. 减少锁持有的时间
  2. 减少锁的粒度
  3. 使用读写分离锁来代替独占锁
  4. 锁分离,可以同时执行的操作,使用不同的锁
  5. 粗锁化:将多次请求锁整合成一次

JVM对锁的优化

锁偏向:

如果一个线程获得了锁,那么这个线程再次请求锁时,无需再做同步操作(忽略synchronized关键字),提高了程序的性能,JVM偏向给获得过锁的线程再次给锁。

对象的对象头中,Mark Word 中保存了锁信息 ,是否为偏向锁为1,锁标值为01时,表示该锁为锁偏向状态。 若为可偏向状态,则测试Mark Word中的线程ID是否与当前线程相同,若相同,则直接执行同步代码。

偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁状态的线程才会释放锁 。出现竞争时,会升级成为轻量级锁。

1647067412181

轻量级锁:

在没有多线程竞争的情况下,使用轻量级锁能够减少性能消耗 。

理解:会把对象头中的mark word复制到锁记录中,然后把mark word变成指向锁记录的指针,锁记录中的owner指针指向锁对象的mark word。

如果失败了,就会验证mark word是否指向当前线程的锁记录,如果是就不需要再获取锁了,直接执行同步代码。如果不是就膨胀为重量级锁。

重量级锁:互斥锁

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值