Java 多线程基础

Java 语言内置了多线程支持:一个 Java 程序实际上是一个 JVM 进程,主线程来执行 main() 方法,在 main() 方法内部,我们可以启动多个线程;

线程的创建方式:

  1. 继承 Thread 覆写 run() 方法,创建 Thread 实例, new MyThread();
  2. 实现 Runnable 接口,new Thread(new MyRunnable());
  3. 实现 Callable 接口;重写 call() 方法;
    相比 run() ,可以有返回值;
    方法可以抛异常;
    支持泛型的返回值;
    需要借助 FutureTask 类获取返回结果;
  4. 使用线程池;

继承关系:
FutureTask 类 —实现–》RunnableFuture 接口 —继承—》 Runnable 和 Future 接口;

class NumThread implements Callable<Integer>{
	@override
	public Integer call() throws Exception{}
}

psvm{
	NumThread numThread = new NumThread();
	FutureTask<Integer> futureTask = new FutureTask<>(numThread);
	new Thread(futureTask).start();
}

设置线程优先级
Thread.setPriority(int n) # 1-10, 默认是5;
操作系统对高优先级线程;

Java 线程的状态:

  • New :新创建的线程,尚未执行;
  • Runnable:运行中的线程,正在执行 run() 方法;
  • Blocked:运行中的线程,因为某些操作被阻塞而挂起;
  • Waiting:运行中的线程,因为某些操作在等待中;
  • Timed Waiting :运行中的线程,因为执行 sleep() 方法正在计时等待;
  • Terminated:线程已终止,因为 run() 方法执行完毕;

线程终止的原因:

  1. 正常终止:run() 方法执行到 return 语句返回;
  2. 意外终止:run方法因为未捕获的异常导致线程终止;
  3. 强制终止:对线程的Thread实例调用 stop()方法强制终止;

join()
等待该线程结束,然后才继续往下执行自身线程;
有一个重载方法 join(long) 可以指定一个等待时间,超过时间后就不再等待;

线程池

线程池接口:public interface ExecutorService extends Executor
ExecutorService : 真正的线程池接口,常见子类:ThreadPoolExecutor;

​ void execute( Runnable command ):执行任务/命令,没有返回值,一般用来执行Runnable;

​ Futuresubmit(Callable task):执行任务,有返回值,一般用来执行 Callable;

​ void shutdown() :关闭连接池;

Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池;

  • FixedThreadPool:线程数固定的线程池;
  • CachedThreadPool:线程数根据任务动态调整的线程池;
  • SingleThreadExecutor:仅单线程执行的线程池。

线程池的核心参数:
ThreadPoolExecutor(

​ int corePoolSize, # 核心线程数;

​ int maximumPoolSize, # 最大线程数

​ long keepAliveTime # 非核心线程的空闲时间,直到只剩下 corePoolSize 个线程为止;

​ TimeUnit unit, # 时间单位

​ BlockingQueue workQueue, # 工作队列

​ ThreadFactory threadFactory # 线程工厂 ,通常使用默认的工厂:Executors.defaultThreadFactory()

​ RejectedExecutionHandler handler # 拒绝策略,队列满了,并且工作线程大于等于线程池的最大线程数,如何拒绝新来的任务;
)

提交任务 —》 核心线程池 --》核心线程池满了之后,任务被放入任务队列 --》 任务队列已满,扩容线程池 --》扩容至最大线程数仍然有任务提交进来,就执行拒绝策略;

不使用 jdk 提供的线程池的原因:

​ 请求队列长度为 Integer 的最大值;

​ 最大线程数量为 int 最大值; 很容易内存溢出;

四种拒绝策略:

​ AbortPolicy(默认):直接抛出 RejectedExecutionException异常阻止系统正常运行;

​ CallerRunsPolicy: “调用者运行”,将任务回退到调用者;

​ DiscardOldestPolicy:抛弃队列中等待最久的任务,将当前任务加入队列;

​ DiscardPolicy:默默丢弃无法处理的任务;

ThreadLocal

在一个线程中,横跨若干方法调用,需要传递的对象,我们称之为上下文 Context;

给每个方法增加一个 context 参数非常麻烦,如果调用链中有无法修改源码的第三方库,参数就无法传递;

Java 标准库提供了 ThreadLocal , 它可以在一个线程中传递同一个对象;

ThreadLocal 通常是以静态字段的形式进行初始化:

static ThreadLocal<User> threadLocal = new ThreadLocal<>();
try{
    threadLocal.set(user);
    threadLocal.get();
}finally{
    threadLocal.remove();
}

ThreadLocal 可以看成是一个全局 Map<Thread,Object>, 线程获取 ThreadLocal 变量时,总是使用 Thread自身(Thread.currentThread())作为 key;
注意:ThreadLocal 一定要在 finally 中清除;

中断线程

中断线程就是其它线程给该线程发一个信号,该线程收到信号后结束执行 run() 方法,使得自身线程能立刻结束运行;

方式一:对目标线程调用 interrupt()

在其它线程中对目标线程调用 interrupt() 方法,目标线程需要反复使用 isInterrupted() 检测自身状态是否是 interrupted 状态,如果是,就立刻结束运行;

interrupt() 方法仅仅向目标线程发出了“中断请求”,至于目标线程是否能立刻响应,要看具体代码;

如果目标线程通过 join() (或者sleep)等方法处于等待状态,对目标线程调用 interrupt(),join方法会立刻抛出 InterruptedException ;

因此目标线程只要捕获到 join() 方法抛出的 InterruptedException, 就说明有其他线程对其调用了 interrupt()方法,通常情况下该线程应该立刻结束运行;

总结:目标线程检测到 isInterrupted()true 或者捕获了 InterruptedException 都应该立刻结束自身线程;

方式二:设置标志位

psvm{
    HelloThread t = new HelloThread();
    t.start();
    Thread.sleep(1);
    t.running = false; //设置标志位为 false
}


class HelloThread extends Thread{
    public volatile boolean running = true;
    public void run(){
        while(running){
            
        }
    }
}

注意:线程间共享变量需要使用 volatile 关键字标记,确保每个线程都能读取到更新后的变量值

Java 内存模型中,变量的值保存在主内存中,当线程访问变量时,它会先获取一个副本,并保存在自己的工作内存中;如果线程修改了变量的值,虚拟机会在某个时刻把修改后的值回写到主内存,但是,这个时间是不确定的

volatile 关键字的作用是告诉虚拟机:

​ 每次访问变量时,总是获取主内存的最新值;

​ 每次修改变量后,立刻回写到主内存;

守护进程

对于定时任务,我们经常设置为守护线程;当其它非守护线程结束时,JVM 进程结束;
守护线程是指为其他线程服务的线程。在JVM中,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出;JVM退出时,不会关心守护线程是否已结束;

设置守护线程:
在调用 start() 方法前,调用 setDaemon(true) 把该线程标记为守护线程;

注意
守护线程不能持有任何需要关闭的资源,例如打开文件等,因为虚拟机退出时,守护线程没有任何机会来关闭文件,这会导致数据丢失

线程同步

在多线程模型下,对共享变量进行读写时,必须保证:一个线程执行时,其它线程必须等待;

加锁和解锁之间的代码块我们称之为临界区(Critical Section),任何时候临界区最多只能有一个线程执行;

synchronized

Java 中提供了 synchronized 关键字对一个对象(必须是线程间的共享实例)进行加锁;
synchronized 保证了代码块在任意时刻最多只有一个线程能执行。
synchronized 语句块结束后
会自动释放锁

使用 synchronized 的时候,不必担心抛出异常。因为无论是否有异常,都会在synchronized结束处正确释放锁;
两种原子操作:

​ 1、基本类型赋值(long 和 double 除外),int n =m ;

​ 2、引用类型赋值: List list = anotherList;

如果是多行赋值语句,就必须保证是同步操作;

同步方法

把 synchronized 逻辑封装到一个方法中;
synchronized修饰的方法就是同步方法,它表示整个方法都必须this实例加锁
如果一个类被设计为允许多线程正确访问,那么这个类就是 线程安全的, 如 StringBuffer;

# 下面两种写法等价
public void add(int n ){
	synchronized(this){
		count += n;
	}
}

public synchronized void add(int n){
	count += n ;
}

###  对 static 方法添加 synchronized , 锁住的是该类的 Class 实例;
public synchronized static void test(int n){}

可重入锁

能被同一个线程反复获取的锁,就叫做可重入锁
synchronized 是可重入锁,每获取一次锁内部计数器就加1,每退出 synchronized 块,记录 -1,减到0的时候,才会真正释放锁;

多线程协调运行

多线程协调运行的原则就是:当条件不满足时,线程进入等待状态;当条件满足时,线程被唤醒,继续执行任务;

synchronized+Wait/Notify

wait()方法必须在当前获取的锁对象上调用

调用 wait方法后,线程进入等待状态,wait 方法不会返回,直到被其他线程唤醒后,wait才会返回,继续执行下一条语句;

特点:

  • wait 是定义在 Object 类的一个 native 方法;
  • 必须在 synchronized 块中才能调用 wait 方法;
  • 调用wait() 方法时,会释放线程获得的锁
  • 被唤醒后(wait方法返回),线程会重新尝试获取锁
  • 在相同的锁对象上调用 notify() 就可以唤醒等待的线程,然后从 wait() 方法返回;
  • 使用notifyAll()将唤醒所有当前正在this锁等待的线程,而notify()只会唤醒其中一个(具体哪个依赖操作系统,有一定的随机性);
  • 要在while()循环中调用wait(),而不是if语句;被唤醒重新获取锁后要重新判断一次

ReentrantLock + Condition(await/signalAll)

并发包: java.util.concurrent ;
java.util.concurrent.locks 提供的 ReentrantLock (Lock接口的实现类)用于替代 synchronized加锁;
ReentrantLock默认是非公平锁,需要创建的时候设置为true标识公平锁。

与synchronized的区别:
1、synchronized 是java语言层面的语法(需要jvm实现),锁是自动添加和释放的,不需要考虑异常,而 ReentrantLock 是 java 代码实现的锁(API层面的互斥锁),必须手动加锁,并在 finally 中正确释放锁(加锁和解锁的次数要一致);
2、synchronized不可响应中断,一个线程获取不到锁就一直等着;ReentrantLock可以响应中断, 并且ReentrantLock 可以尝试获取锁:
3、ReentrantLock还可以实现公平锁。所谓公平锁,也就是在锁上等待时间最长的线程将获得锁的使用权。通俗的理解就是谁排队时间最长谁先执行获取锁。而synchronized只能是非公平锁。

补充:

  • synchronized:偏向锁(偏向第一个线程,效率最高) —> 如果有线程竞争升级为轻量级锁(自旋锁) —>
    自旋10次升级为重量级锁(悲观锁)
  • ReentrantLock可以尝试获取锁

lock.tryLock(1,TimeUnit.SECONDS);
在尝试获取锁的时候,等待1秒。如果1秒后仍未获取到锁,tryLock()返回false,程序就可以做一些额外处理,而不是无限等待下去。

Condition
synchronized 可以配合 wait 和 notify 来实现线程同步;

ReentrantLock 使用 Condition 对象来实现线程同步;

Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
condition.await(); 
condition.signalAll()
# await() 可以在等待指定时间后,如果没有被其它线程通过 signal()signalAll()唤醒,可以自己醒来;
condition.await(1,TimeUnit.SECOND)

注意:
唤醒线程从await()返回后需要重新获得锁。
使用Condition时,引用的Condition对象必须从Lock实例的newCondition()返回,这样才能获得一个绑定了Lock实例的Condition实例。

BlockingQueue

java.util.concurrent下的一个接口(Queue的子接口),是为了解决多线程中数据高效安全传输而提出的。

好处:
​ 我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue都给你一手包办了;

提供了生产者-消费者模型的一种实现方案;

被阻塞的情况主要有如下两种:

  1. 当队列满了的时候进行入队列操作
  2. 当队列空了的时候进行出队列操作

接口的实现类
BlockingQueue 接口:获取元素时可能会让线程变成等待状态 ;java.util.concurrent

  1. ArrayBlockingQueue : 由数组结构组成的有界阻塞队列。
  2. SynchronousQueue:不存储元素的阻塞队列,也即单个元素的队列;
  3. LinkedBlockingQueue:由链表结构组成的有界阻塞队列。
  4. PriorityBlockingQueue: 支持优先级排序的无界阻塞队列;

常用方法:

抛异常true/false/null阻塞阻塞+超时退出返回true/false
插入add(e)offerputoff(e,time,unit)
移除removepolltakepoll(time,unit)
检查elementpeek//

ReadWriteLock

允许多个线程同时读,但只要有一个线程在写,其它线程就必须等待;
适用于读多写少的场景;

ReadWriteLock rwlock = new ReentrantReadWriteLock();
Lock rlock = rwlock.readLock();
Lock wlock = rwlock.writeLock();

锁降级
锁降级就是从写锁降级成为读锁。在当前线程拥有写锁的情况下,再次获取到读锁,随后释放写锁的过程就是锁降级。

public void test(){
    rwlock.writeLock().lock();
    System.out.println("获取到写锁。。。。");
    rwlock.readLock().lock();
    System.out.println("获取到读锁----------");
    rwlock.writeLock().unlock();
    System.out.println("释放写锁==============");
    rwlock.readLock().unlock();
    System.out.println("释放读锁++++++++++++++++");
}

读写锁的特点:
1、支持公平/非公平策略
2、支持可重入

  • 同一读线程在获取了读锁后还可以获取读锁
  • 同一写线程在获取了写锁之后既可以再次获取写锁又可以获取读锁
    3、支持锁降级,不支持锁升级:写锁里面可以获取读锁,读锁里面不能获取写锁
    4、Condition条件支持
    写锁可以通过newCondition()方法获取Condition对象。但是读锁是没法获取Condition对象,读锁调用newCondition()方法会直接抛出UnsupportedOperationException
    5、读写锁如果使用不当,很容易产生“饥饿”问题:
    在读线程非常多,写线程很少的情况下,很容易导致写线程“饥饿”,虽然使用“公平”策略可以一定程度上缓解这个问题,但是“公平”策略是以牺牲系统吞吐量为代价的。
    “饥饿”问题的解释:
    当线程 A 持有读锁读取数据时,线程 B 要获取写锁修改数据就只能到队列里排队。此时又来了线程 C 读取数据,那么线程 C 就可以获取到读锁,而要执行写操作线程 B 就要等线程 C 释放读锁。由于该场景下读操作远远大于写的操作,此时可能会有很多线程来读取数据而获取到读锁,那么要获取写锁的线程 B 就只能一直等待下去,最终导致饥饿。
    ————————————————

Concurrent 集合

java.util.concurrent 包提供的并发集合类:
List ArrayList CopyOnWriteArrayList
Map HashMap ConcurrentHashMap
Set HashSet CopyOnWriteArraySet
Queue LinkedList ArrayBlockingQueue/LinkedBlockingQueue

Atomic

java.util.concurrent.atomic 包提供了一组原子操作的封装类;
AtomicInteger

​ 增加值并返回新值: int addAndGet(int delta);

​ 加1后返回新值: int incrementAndGet();

​ 获取当前值: int get();

​ 用 CAS 方式设置: int compareAndSet(int expect, int update);

​ 如果AtomicInteger的当前值是expect,那么就更新为update,返回true。如果AtomicInteger的当前值不是expect,就什么也不干,返回false

Atomic 类通过“无锁” 的方式实现的线程安全访问。主要利用了 CAS: Compare and Set;

Semaphore

信号量的两个作用:

  • 多个共享资源的互斥使用;
  • 用来控制并发线程数;

信号量中的两种操作:

acquire(获取):当线程调用 acquire 操作时,它要么成功获取信号量(信号量减一),要么一直等下去;,直到有线程 释放信号量;

release (释放):会将信号量的值加1,然后唤醒等待线程;

public class SemaphoreDemo {
    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(4);
        for (int i = 1; i <=6 ; i++) {
            new Thread(()->{
                try {
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName()+"\t抢到了车位");
                    TimeUnit.SECONDS.sleep(3);
                    System.out.println(Thread.currentThread().getName()+"\t离开了车位");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    semaphore.release();
                }
            },String.valueOf(i)).start();
        }

    }
}

CountDownLatch

两个方法:
countDown() # 计数器减1;
await() # 只有计数器减为 0 ,main线程才能结束,否则要一直等待;
一个场景:所有人都离开了教室才打扫卫生;

public class CountDownLatchDemo {

    public static void main(String[] args) throws InterruptedException {

        CountDownLatch countDownLatch = new CountDownLatch(6);

        for (int i = 1; i <=6 ; i++) {
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+"\t离开教室");
                //计数器减一
                countDownLatch.countDown();
            },String.valueOf(i)).start();
        }
        //只有计数器减为 0 ,main线程才能结束,否则要一直等待;
        countDownLatch.await();
        System.out.println("main 线程结束");

    }

}

CyclicBarrier

机器7颗龙珠才召唤神龙;
一个线程完成任务后,就调用 barrier.await(), 当完成任务的线程达到一个数量后就执行总任务(相当于满足某条件下的一个回调);

public class CyclicBarrierDemo {
    public static void main(String[] args) {
        CyclicBarrier barrier = new CyclicBarrier(7, () -> {
            System.out.println("集齐龙珠召唤神龙");
        });


        for (int i = 1; i <=7; i++) {
            final int temInt = i;
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+"\t收集到第:" + temInt + "颗龙珠");
                try {
                    barrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            },String.valueOf(i)).start();
        }
    }
}

面试

JMM : java 内存模型,主要是为了解决代码在不同的硬件生产商和操作系统下,内存访问逻辑的差异带来的线程安全问题;

目的:让同样的代码在不同平台下能够达到相同的访问结果;

线程访问主内存中的共享变量,会先在本地内存(工作内存)中保存一个副本;

工作内存线程独享;工作内存负责与线程交互,也负责与主内存交互;

JMM对共享内存的操作做了如下规定:

​ 线程对共享内存的所有操作都必须在自己的工作内存中进行;

​ 不同线程无法直接访问其他线程工作内存中的变量,因此共享变量的值传递需要通过主内存完成;

volatile变量,用来确保将工作内存中变量的更新操作通知到其他线程

CAS

CAS : Compare And Swap,

是解决多线程并发安全问题的一种乐观锁算法;因为它在对共享变量更新之前,会先比较当前值是否与更新前的值一致,如果一致则更新,如果不一致则循环执行(称为自旋锁),直到当前值与更新前的值一致为止,才执行更新。

缺点:

开销大:在并发量比较高的情况下,如果反复尝试更新某个变量,却又一直更新不成功,会给CPU带来较大的压力

CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性;

AQS

抽象队列同步器、它是实现同步器的基础组件(框架)

juc下面Lock(ReentrantLock、ReentrantReadWriteLock等)的实现以及一些并发工具类(Semaphore、CountDownLatch、CyclicBarrier等)就是通过AQS来实现的。

AQS维护了一个volatile语义(支持多线程下的可见性)的共享资源变量state和一个FIFO(first-in-first-out)线程等待队列(多线程竞争state资源被阻塞时,会进入此队列)。

线程会获取state的状态值,如果是 0 就尝试通过 CompareAndSetState(0,1)设置为1,设置成功就获取到了锁;

AQS将大部分的同步逻辑均已经实现好,继承的自定义同步器只需要实现state的获取(acquire)和释放(release)的逻辑代码就可以,主要有以下方法:

  • tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
  • tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
  • tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
  • tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。
  • isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值