三、线程
1、线程与进程
1.线程是进程内部的一个执行序列,一个进程中可以包含多个线程,每条线程执行不同的任务;
2.不同的进程使用不同的内存空间,而一个进程内部的所有线程共享一片相同的内存空间;
3.每个线程拥有单独的计数器与栈内存,并且能够共享进程内的资源。
2、线程的创建
- 继承Thread类,重写run方法,调用start
- 实现runnable接口,重写run方法,调用start;用实现Runnable接口的对象作为参数实例化一个Thread对象
- 实现callable接口,重写call方法,调用start方法;用实现接口的对象实例化一个FuturTask对象,再用futurtask对象实例化Thread对象,方法有返回值。
- 线程池,通过创建线程池,获得对象
3、Thread常用方法
静态方法:
- currentThread:当前执行的线程
- interrupt():返回当前执行的线程是否被中断
- sleep(long mills):让当前执行的线程睡眠多少毫秒
- yeild():让线程从运行状态转为就绪状态,让其他就绪状态的线程可能获取到 CPU 时间片,也有可能是调用 yield() 方法的线程再次获得
常用实例方法:
- getId:返回线程id
- getName:返回线程名字
- getPriority:返回线程优先级
- interrupt:线程中断
- isAlive:是否存活
- join((long mills)):让当前线程进入阻塞状态,直到join的这个线程执行完毕后才被唤醒
4、线程的生命周期
线程的生命状态分为新建(New)、就绪(Ready)、运行(Running)、阻塞(Blocked)和死亡(Dead)五种;
1、当使用new创建一个线程后,该线程就处于新建状态,java虚拟机为其分配内存。
2、当线程调用了Start方法后,线程就处于就绪状态,java虚拟机会为其创建方法调用栈和程序计数器,但此时线程还没有开始运行,等待获取CPU。
3、当处于就绪状态的线程获得了CPU之后,就会开始执行run方法体,此时线程处于运行状态。若线程数大于处理器数量时,依然会存在多个线程在同一CPU上轮换的现象,此时线程又会回到就绪状态。
4、线程在运行状态很可能会进入阻塞状态,情况:
- 线程调用了sleep方法主动放弃占用处理器资源
- 线程调用了阻塞式IO方法,在该方法返回前,线程会被阻塞
- 线程在试图获得一个被其他线程占用的同步锁
- 线程等待某个通知(notify)
对应得解除阻塞状态的情况:
- sleep的时间到了
- 阻塞时IO方法返回
- 获得了同步锁
- 等到了通知
此时线程就会由阻塞状态转为就绪状态
5、最后就是线程的死亡状态
- run方法或call方法执行完毕,线程正常结束
- 线程捕获到了一个异常或错误
- 调用了该线程的stop方法结束
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xFM0vkm7-1662014705151)(C:\Users\10591\AppData\Roaming\Typora\typora-user-images\1658320083600.png)]
5、Java多线程之间的通信方式
主要包括三种方式:wait、notify、notifyAll(Object类中的方法)
wait方法:让当前线程释放对象锁进入阻塞状态;
notify方法:唤醒一个正在等待相应对象锁的线程进入就绪状态;
notifyAll方法:用于唤醒所有正在等待获得对象锁的线程;
Sleep与wait的区别:
- sleep时thread类中的静态方法,而wait是object类的成员方法
- sleep可以在任何地方使用,wait只能在同步方法或同步代码块中使用
- sleep不会释放锁,而wait会释放锁,并需要notify或notifyall重新获得锁
6、 Sychnorized与Lock的区别
- 本质
- Sychnorized是java关键字,在JVM层面实现加锁与解锁;
- Lock是一个接口,在代码层面实现加锁与解锁
- 使用方法
- Sychnorized不需要手动地去释放锁,系统会自动让线程释放对锁的占用;可以使用在方法和代码块
- Lock需要手动地释放锁,否则会出现死锁现象;只能在方法中
- 等待锁
- Sychnorize拿不到锁会一直等待
- Lock可以设置获取锁的超时时间
- 锁是否公平:
- 默认都是不公平锁,ReentrantLock可以通过构造函数所传的true或false值来选择公平锁和非公平锁
- 中断
- Sychnorized:不可中断
- lock:可以中断
7、Syschnorized的底层原理
Syschnorized是通过进入与退出Monitor对象来实现方法的同步。
1、对于sychnorized修饰方法时,方法调用时会检查方法的ACC_SYNCHNORIZED访问标志是否被设置。如果被设置了,执行线程会先获取monitor,获取成功后才能执行方法体,执行完再释放monitor。在方法执行期间其他的线程无法在获取monitor对象
2、对于syschnorized修饰代码块时,是利用monitorenter与monitorexit两个字节码指令实现。当monitorenter指令尝试获得monitor,若monitor的进入数为0,则线程进入monitor,然后进入数+1;当执行monitorexit指令时,锁计数器-1;当计数器=0时,锁被释放。
8、AQS理解
**概念:**抽象队列同步器AQS是一个抽象类,主要通过继承的方式来使用,构建锁或则同步器。AQS内部有一个双向的同步队列,若线程竞争失败,那么AQS会把当前线程与等待状态信息构造成一个Node加入到同步队列中,同时阻塞该线程。当获取锁的线程释放之后,会从队列中唤醒一个线程。同时AQS中还有一个int成员state来表示同步状态。
**模式:**AQS有两种资源分享模式,一种是独占模式,另一种是共享模式
- 独占模式:只有一个线程能执行,ReentrantLock(表示线程池持有则重复获得锁的次数)
- 共享模式:多个线程可同时执行,Semaphore(剩余许可数量,0阻塞)、countdownlatch、cyclicbarrier、readwritelock
9、ThreadLocal
**概念:**ThreadLocal是线程私有的局部变量存储容器,用于储存线程私有变量。每个线程可以通过set、个体、存取变量,其他的线程无法访问。
场景:
1、为每一个线程分配一个JDBC链接Connection,保证每个线程都在各自的Connection上进行数据库操作。
2、管理session会话,将session保存在threadlocal中,使得线程多次处理绘画始终用的是同一个session
10、volatile
10.1 Volatile是什么
volatile是java虚拟机提供的轻量级的同步机制
- 保证可见性
- 禁止指令重排
- 不保证原子性
10.2 JMM是什么
概念:JMM本身是一种抽象的概念并不是真实存在,它描述了一组规范,定义了程序中各变量的访问方式
同步规定:
- 线程解锁前,必须把共享变量的值刷新回主内存
- 线程加锁前,必须读取主内存的最新值到自己的工作内存中
- 加锁解锁是同一把锁
三大特性:
可见性
原子性
有序性
可见性:
若没有可见性,当线程A修改了共享变量值但还没有存入内存中,另外的线程B也来拷贝此共享变量,但线程A的私有内存对线程B而言并不可见,这种工作内存与主内存同步延迟现象造就了可见性问题。解决:可以用volatile关键字修饰对象,当一个线程对此变量进行了修改,会被另一个线程感知,重新从主内存中获得最新的变量值
class Data { //int a = 0; 因为停了三秒保证此时main线程去取数据,取到的是0,所以一直在循环,当aaa线程操作完也在循环,因为不可见 volatile int a;// main线程停三秒发现a变了所以while就结束 public void add(){ a++; } } public class VolatileDemo { public static void main(String[] args) { Data data = new Data(); // 模拟操作,此时main在while中一直是0,在循环 new Thread(()->{ System.out.println(Thread.currentThread().getName()+",is coming..."); try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } data.add(); System.out.println(Thread.currentThread().getName()+",update value to:" + data.a); },"AAA").start(); while(data.a == 0){ } System.out.println(Thread.currentThread().getName() + "\t mission is over"); } }
原子性:
概念:不可分割、完整性,当某个线程在执行某个业务时,中间不能被加塞或被分割,需要整体完整,要么同时成功,要么同时失败。
解决:加synchnorized解决:重量级同步机制,不是很考虑;使用AtomicInteger解决
package cxj.intervew.standard.VolatileDemo; import java.util.concurrent.atomic.AtomicInteger; /** * @Author:CXJ 2022-06-2022/6/23 * @Describe: */ class MyData1 { //volatile int a = 0; AtomicInteger a = new AtomicInteger(); public void add() { //a++; a.getAndIncrement(); } } public class VolatileDemo02 { public static void main(String[] args) { MyData1 myData1 = new MyData1(); for (int i = 0; i < 200; i++) { new Thread(() -> { System.out.println(Thread.currentThread().getName() + "正在操作..."); for (int j = 0; j < 2000; j++) { myData1.add(); } }, "第" + i + "个").start(); } while (Thread.activeCount() > 2) { Thread.yield(); } System.out.println("a的值:" + myData1.a); } }
有序性:
计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令重。
- 在重排的时需要考虑指令之间的数据依赖性
- 多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的
所以禁止指令重排,可以避免在多线程环境下程序出现乱序的现象
10.3 线程安全获得保证
- 工作内存与主内存同步延迟现象导致的可见性问题—使用synchnorized或volatile关键字解决,使得一个线程修改后的变量对其他线程可见
- 对于指令重排导致的有序性问题-可用volatile关键字解决,因为volatile的另一个作用是禁止指令重排
11、CAS
- 概念:CAS是compare and swap,比较与交换的意思,判断内存某一个位置的值是否为预期值,如果是则更改这个值,这个过程就是原子的。
- 具体实现:例如当AutomicInteger对象在调用加一方法时,底层会调用Unsafe类得CAS方法,使用了三个基本参数:内存地址、预期值与新值;实现的是do-while方法,在do中获得当前内存位置得变量值,然后在while中判断若此时该位置得变量值与do中获取的变量值相同,则改变这个值,不一样则又会进入do语句中。
- 缺点:
- 若一直操作不成功,则一直循环,CPU开销大
- 只能保证一个变量的原子性
- 会引发ABA问题
12、ABA问题
概念:当线程1与线程2,读取了主内存中某个变量值时,线程2对这个变量值进行了一些操作,并把变量刷回了主内存中,然后线程2又读取当前变量修改为第一次读取的值,并刷回到主内存中;此时线程A进行CAS操作,对比先前所获取的值与现在主内存中的值,发现值没有改变,进行操作。
尽管线程1操作成功,但不代表整个过程没有问题
普通类的原子性:通过AtomicReference类提供了原子引用,把AtomicReference的泛性类设置为指定的引用类型进行实现,此时存入此引用类型变量就具有原子性
package automic; import java.util.concurrent.atomic.AtomicReference; class User { String userAge; int age; public User() { } public User(String userAge, int age) { this.userAge = userAge; this.age = age; } @Override public String toString() { return "User{" + "userAge='" + userAge + '\'' + ", age=" + age + '}'; } } public class AtomicReferences { public static void main(String[] args) { User cxj = new User("陈心娇",21); User cyj = new User("陈玉娇",24); AtomicReference<User> atomicReference = new AtomicReference<User>(); atomicReference.set(cxj); System.out.println(atomicReference.compareAndSet(cxj, cyj)); System.out.println(atomicReference.compareAndSet(cxj, cyj)); } } // true // false
解决:通过引入时间戳原子引用AtomicStampedReference解决,每进行一次stamp就会进行加1,此时若线程1与线程2获得了主内存中相同的某个变量值,与相同的stamp,然后线程1做了ABA操作,此时线程2把期望值与主内存中的值进行对比,虽然值没有变,但是stamp改变了,所以不允许交换,解决了ABA问题
package automic; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicStampedReference; public class ABADemo { // 传递两个值,一个是初始值,一个是初始版本号 static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1); public static void main(String[] args) { new Thread(()->{ int stamp = atomicStampedReference.getStamp(); System.out.println(Thread.currentThread().getName()+"当前版本号:"+stamp); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } atomicStampedReference.compareAndSet(100,102,stamp,stamp+1); atomicStampedReference.compareAndSet(102,101,stamp,stamp+1); System.out.println(Thread.currentThread().getName()+"当前版本号:"+atomicStampedReference.getStamp()); },"aaa").start(); new Thread(()->{ int stamp = atomicStampedReference.getStamp(); System.out.println(Thread.currentThread().getName()+"当前版本号:"+stamp); try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } boolean b = atomicStampedReference.compareAndSet(100, 104, stamp, stamp + 1); System.out.println(b); System.out.println(Thread.currentThread().getName()+"当前版本号:"+atomicStampedReference.getStamp()); },"bbb").start(); } }
13、锁
13.1 公平锁与非公平锁的区别
公平锁:
在并发环境下,每个线程在获得锁的时候会查看这个这个线程的等待队列,如果为空则立即获得锁,否则就排在等待队列的最后面等待获得锁;所以多个线程是以申请锁的顺序获得锁
非公平锁:
一上来就会尝试获得锁,如果获得锁失败,则会采用公平锁的方式,进入等待队列
sychnorized与reentrantlock都是非公平锁
13.2 可重入锁
线程在外层方法获得锁之后,内层方法会自动获得该锁
public class LockDemo { public synchronized void sendMs(){ System.out.println(Thread.currentThread().getName()+"\t sendMs"); sendEmail(); } public synchronized void sendEmail(){ System.out.println(Thread.currentThread().getName()+"\t sendEmail"); } public static void main(String[] args) { LockDemo lockDemo = new LockDemo(); new Thread(()->{ lockDemo.sendMs(); },"t1").start(); new Thread(()->{lockDemo.sendMs();},"t2").start(); } } t1 sendMs t1 sendEmail t2 sendMs t2 sendEmail
13.3 自旋锁
尝试获取锁的线程不会立即阻塞,而是会采用循环的方式去尝试获得锁。
好处:减少线程上下文切换的消耗
坏处:循环会消耗CPU
public class SpinLockDemo { AtomicReference<Thread> atomicReference = new AtomicReference<>(); // 尝试获得锁 public void myLock(){ Thread thread = Thread.currentThread(); System.out.println(Thread.currentThread().getName()+"is coming"); // 返回false就是,不是期望值,循环尝试获得锁 while(!atomicReference.compareAndSet(null,thread)){} } public void myUnlock(){ Thread thread = Thread.currentThread(); atomicReference.compareAndSet(thread,null); System.out.println(Thread.currentThread().getName()+"is exiting"); } public static void main(String[] args) { SpinLockDemo spinLockDemo = new SpinLockDemo(); new Thread(()->{ spinLockDemo.myLock(); try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } spinLockDemo.myUnlock(); },"t1").start(); new Thread(()->{ spinLockDemo.myLock(); spinLockDemo.myUnlock(); },"t2").start(); } }
13.4 读写锁
独占锁:该锁只能被一个线程所持有
共享锁:锁可以被多个线程持有
读写锁:可以共享读,但只能一个写,所以读写锁的读是个共享锁,而写是一个独占锁。Java中的ReentrantReadWriteLock就是一个读写锁。
package lockReverse; import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantReadWriteLock; class Cache { private volatile Map<String, Object> map = new HashMap<>(); private ReentrantReadWriteLock mylock = new ReentrantReadWriteLock(); public void put(String key, Object value) { mylock.writeLock().lock(); System.out.println(Thread.currentThread().getName() + ",正在写入...."); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } map.put(key, value); System.out.println(Thread.currentThread().getName() + ",已写完,value=" + value); mylock.writeLock().unlock(); } public void get(String key) { mylock.readLock().lock(); System.out.println(Thread.currentThread().getName() + ",正在读出...."); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } Object o = map.get(key); System.out.println(Thread.currentThread().getName() + ",已读完,value=" + o); mylock.readLock().unlock(); } } public class RedaWriteLockDemo { public static void main(String[] args) { Cache cache = new Cache(); for (int i = 0; i < 5; i++) { final int tempInt = i; new Thread(() -> { cache.put(tempInt + "", tempInt + ""); }, i + "").start(); } for (int i = 0; i < 5; i++) { final int tempInt = i; new Thread(() -> { cache.get(tempInt + ""); }, i + "").start(); } } }
13.5 乐观锁与悲观锁
乐观锁
概念:每次去拿数据的时候都认为别的线程不会修改修改数据,所以不会上锁,但是在更新的时候会判断一下在此期间该数据是否修改,可以使用版本号机制和CAS算法实现。
场景:乐观锁适用于多读的应用类型,这样可以提高吞吐量。
悲观锁
概念:每次去拿数据的时候都认为别的线程会修改数据,所以每次在拿数据的时候都会上锁,其他想拿数据进程会阻塞,直到获得锁。
14、同步器CountDownLatch、CyclicBarrier和Semaphore使用过吗?
CountDownLatch:
让一些线程堵塞,直达另一个线程完成操作后才被唤醒。
主要调用两个方法:通过countdown进行计数值的减减;通过await方法会让线程堵塞,直到计数值为0,被堵塞的线程才被唤醒。
public class CountDownLatchDemo { public static void main(String[] args) throws InterruptedException { CountDownLatch countDownLatch = new CountDownLatch(5); for(int i = 0; i < 5; i++){ new Thread(()->{ System.out.println(Thread.currentThread().getName()+",正在操作...."); countDownLatch.countDown(); },i+"").start(); } countDownLatch.await(); System.out.println(Thread.currentThread().getName()+"操作完毕,所有线程操作完毕...."); } }
CyclicBarrier:
先到达屏障的线程被阻塞,直到最后一个线程到达屏障,所有屏障阻塞的线程才继续进行
public class CyclicBarrierDemo { public static void main(String[] args) { CyclicBarrier cyclicBarrier = new CyclicBarrier(4,()->{ System.out.println("所有线程均已达到!"); }); for(int i = 0; i < 5; i++){ new Thread(()->{ System.out.println(Thread.currentThread().getName()+",正在操作...."); try { cyclicBarrier.await(); } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { e.printStackTrace(); } },i+"").start(); } } }
Semaphore:
允许n个任务同时访问某个资源,用于多线程抢夺资源的情况和并发线程数的控制
假设现目前只允许3个线程访问某资源,而有6个线程进行正在进行抢夺;当某三个线程抢夺了资源,并离开时,剩余未抢夺到资源的线程又去抢夺
public class SemaphoreDemo { public static void main(String[] args) { Semaphore semaphore = new Semaphore(3, false); for (int i = 0; i < 6; i++) { new Thread(() -> { try { semaphore.acquire(); System.out.println(Thread.currentThread().getName() + "抢到了资源"); TimeUnit.SECONDS.sleep(3); System.out.println(Thread.currentThread().getName() + "释放资源"); } catch (InterruptedException e) { e.printStackTrace(); } finally { semaphore.release(); } }, i + "").start(); } } }
15、阻塞队列
15.1 概念
阻塞队列首先是一个队列,好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程
- 当队列为空时,从队列中获取元素时会被阻塞
- 当队列为满时,从队列中存放元素时会被阻塞
15.2 BlockQueue分类
- ArrayBlockingQueue:由数组结构组成的有界阻塞队列,以先进先出的方式对元素进行排序
- LinkedBlockingQueue:由链表结构组成的队列,以先进先出的方式对元素进行排序
- SynchronousQueue:不存储元素的阻塞队列,只能放一个取一个
15.3 核心方法
抛异常 特定的值 阻塞 超时 add offer put offer(o, timeout, timeunit) remove poll take poll(timeout, timeunit) element peek
- 抛异常:当方法不能进行,则抛出异常
- 特定值:如果不能进行,返回false、null的特殊值
- 阻塞:如果不能进行,线程会被阻塞
- 超时:如果不能进行,在指定的时间能线程会被阻塞,超过了指定的时间,会返回一个特殊值
16、线程池
16.1 线程池的优势
概念:
线程池的工作主要是控制运行的线程数量。处理过程中将任务放入队列,在线程创建后启动这些任务,如果线程数量超过了线程池的最大数量,超出数量的线程排队等待,等其他线程执行完比之后,再从队列中取出任务进行执行。
优点:
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗
- 提高响应速度。当任务到达时,不需要等待线程创建就能立即执行
- 提高线程的管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配、调优和监控
16.2 线程池创建线程的三个常用方法
- Executors.newSingleThreadExecutor():单线程化的线程池,只用唯一的工作线程来执行任务,保证所有任务按照指定顺序执行。其中corePollSize’和maximumPoolSize都设置为1,使用的是LinkedBlockingQueue
- Executors.newFixedThreadPool(int):定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。其中corePoolSize和maxPoolSize值是相同的,使用的是LinkedBlockingQueue
- Executors.newCachedThreadPool():可缓存线程,如果线程池的长队超过处理需求,可灵活回收空闲的线程,若无可回收,则新建线程。其中corePoolSize为0,maxPoolSize为Integer.MAX_VALUE,使用的是sychronousQueue,也就是说当任务来了会创建线程,当线程空闲60s就会销毁线程
16.3 线程池的底层原理
- 在创建线程池后,等待任务请求
- 在调用execute方法添加一个请求时,线程会马上判断
- 正在运行的线程数量小于corePollSize,马上创建线程执行任务
- 正在运行的线程数量大于等于corePollsize,那么将任务存放于等待队列中
- 若此时队列已满,且运行的线程数量小于maxpoolsize,那么会创建线程执行该任务
- 若此时队列已满,且运行的线程数量等于maxpoolsize,那么线程会启动拒绝服务
- 当线程完成任务后,会从等待队列取出一个任务执行
- 当一个线程的空闲时间超过了keepaliveTime时,线程会判断:
- 若此时线程数量大于corepoolsize,线程就会被停用,最小回缩到corepoolsize的大小
16.4 拒绝服务
AbortPolicy(默认):直接抛出异常
CallerRunsPolicy:将默认任务退回到调用者本身,从而降低新任务的流量
DiscardOldestPolicy:抛弃等待队列中等待最几句的任务
DiscardPolicy:直接丢弃任务
17、死锁
概念:死锁是指两个或两个以上的线程在执行的过程中,因为抢夺资源而造成的一种互相等待的现象,若无外力干涉,他们都无法推进。
发生的条件:
- 互斥条件:资源是非共享模式
- 占有并等待:一个线程至少持有一个资源且正等待获取一个当前被别的线程持有的资源
- 非抢占:资源不能被抢夺,只能等待持有资源的进行完成任务后,才能获得
- 循环等待
解决死锁的方法:
- 预防:设置限制条件,破坏产生死锁的必要条件
- 避免:在资源分配过程中,根据资源使用情况提前做出预测
- 检测死锁:允许死锁的发生,通过系统检测之后采用一些措施,将死锁清除
- 解除:与检测配套
18、其他问题
1、run与start的区别:
run方法是线程的执行体,如果不是使用start调用run方法的话,run方法只是一个普通的方法;使用start调用的话,run方法就是以线程执行体处理。
2、线程是否可以重复启动?
只能对一个新建状态的线程调用start方法,否则会引发IlleagalThreadStateException异常。
3、如何实现线程的同步?
- 使用sychnorized关键字修饰方法或代码块,保证一个线程执行此方法时其他方法都在外面等待
- 使用Lock,加锁、解锁
- volatile关键字修饰变量
- 原子变量JUC下的atomic包