Java线程

509 阅读10分钟

java.util.concurrent并发包

volatile

volatile是java虚拟机提供的轻量级的同步机制

三大特性:

  • 保证可见性
  • 不保证原子性
  • 禁止指令重排

若修饰的变量进行的操作不是线程安全的,会发生写覆盖,不能保证原子性

解决办法:

  • 方法上添加synchronized
  • 使用juc包下的AtomicInteger

JMM,java内存模型

三大特性
  • 可见性
  • 原子性
  • 有序性

是一种抽象的概念并不真实存在,他描述的是一组规则或规范,通过这组规范定义了程序中各个变量的访问方式

JMM关于同步规定
  • 线程解锁前,必须把共享变量的值刷新回住内存

  • 线程加锁前,必须读取主内存的最新值到自己的工作内存

  • 加锁解锁是同一把锁

线程中的通信通过主内存完成,每个线程只能将主内存的变量拷贝一份到自己的工作内存,然后进行操作变量,操作完成后,必须将变量重新写会主内存,并通知其他拷贝这变量的线程,这就是保证可见性

指令重排

多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测

内存屏障
你在哪些地方用到volatile
  • 单例模式
  • 读写锁,手写缓存的时候
  • CAS,juc包中
DCL双端检锁机制

同步锁代码块

public class SingletonDemo {

    private static SingletonDemo instance=null;

    private SingletonDemo(){

    }
  //在锁的前后都进行判断
    public static SingletonDemo getInstance(){
        if(instance==null){
            synchronized (SingletonDemo.class){
                if (instance==null){
                    instance=new SingletonDemo();
                }
            }
        }
        return instance;
    }
}

双端检锁机制不一定安全,因为存在指令重排的存在

instance=new SingletonDemo();

在底层字节码编译分成三步:

  • 分配对象内存空间
  • 初始化对象
  • 设置instance指向刚分配的内存空间,此时instance!=null

由于步骤2和3不存在数据依赖关系,所有这两步存在重排优化,可能会出现instance有指向地址,但是未初始化,得到的数据为空。

解决办法:

    private static volatile SingletonDemo instance=null;

CAS(比较并交换)

Compare-And-Swap,是一条CPU并发原语,是一种完全依赖于硬件的功能,属于操作系统用语范畴,并且原语的执行必须是连续的,在执行过程中不允许被打断,就不会造成数据不一致的问题,也就是线程安全的。

AtomicInteger atomicInteger=new AtomicInteger(5);//初始值
atomicInteger.compareAndSet(5,2019);//先于期望值(第一个参数)进行比较,若一样则进行修改,返回值boolean
底层
  • 自旋锁
  • unsafe类
Unsafe类

是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,相当于一个后门,基于该类可以直接操作特定内存的数据,其内部方法操作可以直接操作内存。

例如:

public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
				//var1:当前对象;var2:地址偏移量;var4:修改值;var5:当前内存中的值
  			//先根据当前对象的地址及偏移量获得内存中的值与var5进行比较,如果相同,则对数据进行处理,反之重新获得内存中的最新值,循环
        return var5;
    }
缺点
  • 循环时间长,开销大
  • 只能保证一个共享变量的原子操作
  • ABA问题
ABA问题

CAS算法实现一个重要前提需要去除内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差类会导致数据的变化。

解决办法:原子引用+新增一种机制,那就是修改版本号(类似时间戳)

原子引用

将对象变为原子性

User user=new User("z3",22);
User user1=new User("li4",25);
AtomicReference<User> atomicReference=new AtomicReference<>();
atomicReference.set(user);
atomicReference.compareAndSet(user,user1);

解决办法:

AtomicStampedReference<Integer> atomicStampedReference=new AtomicStampedReference<>(100,1);
atomicStampedReference.compareAndSet(100,101,1,2);//期望值,更新值,期望版本号,更新版本号

集合线程安全

ArrayList和HashSet、HashMap是线程不安全的,多个线程操作一个集合对象,会出现CuncurrentModificationException异常

解决方案

  • new Vector:方法上加synchronzied
  • Collections.synchronziedList/Collections.synchronziedSet:方法内加sunchronzied代码块
  • new CopyOnWriteArrayList/CopyOnWriteArraySet:用lock锁,现将集合复制到一份新的集合对象,再将新值复制到尾部,最后再将新的集合对象赋值给原来的集合对象;CopyOnWriteArraySet构造器底层其实是new了一个CopyOnWriteArrayList
  • new ConcurrentHashMap

多线程

创建线程

通过实现Runnable接口

public class ThreadDemo {
    Thread thread1=new Thread(new Thread1());
}
class Thread1 implements  Runnable{
    @Override
    public void run() {
        //线程体
    }
}

通过实现Callable接口

class MyCallable implements Callable{
    @Override
    public Object call() throws Exception {
        //线程体,有返回值
        return null;
    }

    public static void main(String[] args) {
        MyCallable myCallable = new MyCallable();
        FutureTask task = new FutureTask<>(myCallable);
        Thread thread = new Thread(task);
        thread.start();
    }
}

通过继承Thread

class MyThread extends Thread{
    @Override
    public void run() {
        //线程体
    }

    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
    }
}
class MyThread2 {
    public static void main(String[] args) {
        new Thread(()->{
            //线程体
        }).start();
    }
}

使用Executor框架创建线程池

class MyExecutor{
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                //线程体
            }
        });
        executorService.shutdown();
    }
}

Runnable和Callable区别

  • Callable中的方法是call,Runnable是run
  • Callable执行结束后有返回体,而Runnable没有
  • call方法可以抛出异常,run不行
  • 运行Callable可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future可以了解任务执行情况,可取消任务的执行,可获取执行结果

Executor线程池

使用理由

  • 降低资源小号
  • 提高响应速度
  • 提高线程可管理性

原理

  • 核心线程数:线程池的基本大小
  • 最大线程数:同一时刻线程池中线程的最大数
  • 任务队列:线程池中的数量已达到核心线程数,用任务队列来存储任务,就是排队

基于生产-消费模式实现的,任务提交方就是生产者,线程池就是消费者

可缓存线程池

如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程

同步队列,有则执行,无则创建线程执行;若线程空闲时间超过指定大小,默认60秒,则该线程被销毁

Executors.newCachedThreadPool();

定长线程池

可控制线程最大并发数,超出的线程会在队列等待

Executors.newFixedThreadPool(3);

定期或延时执行线程池

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

Executors.newScheduledThreadPool(3);

单线程化线程池

只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO,LIFO,优先级)执行

Executors.newSingleThreadExecutor();

任务执行流程

  • 线程池小于核心线程数时,新提交任务将创建一个新线程执行,即使此时线程池中存在空闲线程
  • 线程池达到核心线程数时,新提交的任务将被放到任务队列中,等待线程池调度
  • 当任务队列已满,且最大线程数>核心线程数时,新提交任务会创建新线程执行
  • 当提交任务数超过最大线程数时,新提交任务会由RejectedExecutorHandler处理
  • 当线程池中超过核心线程数,空闲时间达到keepAliveTime时,关闭空闲线程
  • 当设置allowCoreThreadTimeOut时,线程池中核心线程空闲时间达到keepAliveTime也将关闭

终止线程

  • 使用退出标记
  • 使用stop方法强行终止(不安全)
    • 立即停止全部任务,并抛出异常;会释放所有的锁,导致数据得不到同步
  • 使用interrupt方法中断线程
    • 不是马上停止,而是在线程中打了一个停止标记

线程生命周期

  • 初始化:被创建
  • 可运行/运行:调用start方法之后
  • 阻塞:只有synchronized才能导致阻塞
  • 等待
    • 调用wait方法,等到notify或者notifyAll方法才会回到运行状态;
    • 调用join方法,礼让线程
    • 调用sleep方法
  • 终止
    • 执行结束
    • interrupt方法
    • stop方法

sleep和wait区别

sleep wait
所属类 Thread类 Object类
锁资源处理 不释放锁 释放锁
使用范围 任何代码块 必须在同步方法或者同步代码块
如何结束等待 等待参数时间结束,若为空或0则无限 调用notify或者notifyAll方法或者等待时间结束

start和run的区别

start

调用start方法使得该线程开始执行,JVM会去调用该线程的run方法

调用run方法只是主线程调用了一个普通方法

线程间通信

  • volatile:多个线程同时监听一个共享变量时,当共享变量发生改变时,线程能够感知
  • wait()、notify()、notifyAll()
  • JUC包下的CountDownLatch
  • 基于LockSupport实现线程间的阻塞和唤醒:必须知道线程名

同步锁

synchronized
  • 修饰实例
  • 修饰方法
  • 修饰代码块
锁状态 优点 缺点 适用场景
偏向锁 加锁解锁额外消耗 如果竞争的线程多,会带来额外的锁撤销消耗 基本没有线程竞争锁的同步场景
轻量级锁 竞争的线程不会阻塞,使用自旋,提高程序响应速度 如果一直不能获取锁,长时间自旋会造成CPU消耗 少量锁竞争对象,且线程持有锁时间不长,追求响应速度
重量级锁 线程竞争不使用自旋,不会导致CPU空转消耗 线程阻塞,响应时间长 很多线程竞争锁,且锁持有时间长,追求吞吐量
Lock
  • lock方法:获取锁,如果锁被其他线程获得则进行等待
  • lockInterruptibly方法:如果线程正在等待锁,则这个线程可以响应中断;也就是两个线程通过这方法获取锁,其中一个获得,另一个等待,可以调用interrupt中断等待
  • tryLock方法:尝试获取锁;可以添加参数,若拿不到锁,则等待一段时间
  • unlock方法:解锁
区别
  • lock支持非阻塞的方式获取锁,能够响应中断;
  • lock必须手动获取释放锁
  • lock是公平锁或者非公平锁;synchronized只能是非公平锁
  • synchronized发生异常会自动释放锁;lock必须调用unlock才能释放
  • 都是可重入锁

死锁

锁相互嵌套,两个线程相互索取对方的锁

产生条件
  • 互斥条件:一个资源每次只能被一个线程使用
  • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
  • 不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺
  • 循环等待条件:若干进程之间行程一种头尾相接的循环等待资源关系
防止死锁
  • 减少同步代码块嵌套操作
  • 降低锁使用粒度,不要几个功能公用一把锁

公平锁与非公平锁

  • 公平锁:按照申请锁的顺序来获取锁
  • 非公平锁:不按照顺序获取锁,若没有获取到锁,就按照公平锁,有可能造成优先级反转和饥饿现象,吞吐量大于公平锁

可重入锁(递归锁)

同一线程外层函数获得锁之后,内层递归函数仍然能获取该锁,在同一线程在外层方法获取锁的时候,在进入内层方法是自动获取锁

线程可以进入任何一个它拥有的锁所同步的代码块

最大作用就是避免死锁

自旋锁

尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,减少线程上下文切换的消耗,但是增大对CPU的消耗

独占锁

该锁一次只能被一个线程占用,Lock和Synchronized都是独占锁

共享锁

该锁可以被多个线程锁持有

读写锁

ReadWriteLock

读:共享锁,ReadWriteLock.readLock()

写:独占锁,ReadWriteLock.writeLock()