1.进程
进程是互相不独立,且拥有资源的最小单位。
2.线程
线程是进程内部的执行单位,进程的资源将共享给线程使用。
线程是系统独立调度的基本单位。
3.并发并行
并发是同一时刻cpu上有多个指令同时运行。
并行是同一时刻cpu上有多个指令在交替运行。
其中单核cpu看似能够实现并发,实则在微观中是以cpu的调度而轮流执行的,只是切换的速度在宏观角度来看很快
所以在宏观上被认为是并发执行的。
4.同步异步
同步是指需要等待结果返回才能接着执行。
异步是指某些指令可以不等待结果返回就可以接着往下运行。
5.创建线程的三种方式。
1.直接继承Thread类,实现其中的run方法。(可以继承也可以实现匿名内部类来使用)
缺点:无法继承其他类,拓展性有限,一般不用。
2.实现Runnable接口,实现其中的run方法。(可以继承也可以实现匿名内部类来使用)
其中,Thread类本身也是实现了Runnable接口的,所以可以直接继承Thread来创建线程。
是比较常用的方式。
3.callable类。(不太了解)
6.线程常用方法(背!)
public void start();启动线程,并调用run方法。
public void run();启动线程后调用该方法。
public void setname(String name)/getname();设置和获取该线程的名字。
public static Thread currentThread(); 获取当前线程,代码在哪个线程上执行就获取哪个线程的对象。
public static void sleep(Long time); 让线程休眠,从running状态进入到time waiting状态,睡眠结束后线程未必能立即执行,
建议使用TimeUnit的sleep()来替代 Thread的sleep以获取更好的可读性。
public static native void yield();也是让线程切换的一种方式,但是yield不一定会切换,
他会提示cpu应当执行一次cpu的竞争来让出目前线程的cup使用。依赖于cpu的任务调度器。
public void interrupt();中断线程,可以中断睡眠线程,并抛出异常,也可以中断运行线程,但需要手动加代码判断线程(下面两个方法)是否被打断了,才能停止线程。
两个方法容易混淆
public static boolean interrupted();这个是静态方法,用于判断当前线程是否被打断了,并清除打断标记,将标记记为false;
public boolean isInterrupted();判断线程是否被打断了,不改变打断标记。
public final void join();等待运行结束,哪个线程调用的方法就哪个线程等待,A线程调用B线程的join,A线程就需要等待B线程执行结束。
public final native boolean isAlive();判断线程是否存活。
7.sleep和yield
sleep是将线程的running状态置为time waiting状态,且期间,他并不会释放他拥有的对象锁,其他线程需要等待该线程睡眠结束,执行完代码后释放对象锁,才能获取对象锁对对象进行操作。可以通过interrupt()来打断睡眠,并抛出异常。
yield是一种轻度的线程让步机制,该方法会提示jvm该线程愿意让出cpu资源,而最终结果也是由jvm的任务调度器来决定。其依赖于操作系统和jvm的共同作用。但当前线程并不会释放任何锁资源,包括对象锁和类锁。
8.线程同步实现
使用join(),在方法中调用t1的join,t1线程则抢占到cpu资源后不再释放,直到结束。该方法会阻塞所有线程,因此可以实现线程同步,只需要对处理关键逻辑的线程进行join,就可以实现线程同步。
使用future(没了解);
9.public final void setDaemon(boolean on):如果是 true ,将此线程标记为守护线程
守护线程,若所有非守护线程全部执行结束,即使守护线程没有执行全部代码也会结束运行. jvm中的垃圾回收器就是一个守护线程.
10.线程调度
有协同式调度和抢占式调度.
协同式调度是指线程每次都需要协同好其他线程的完成时间,控制每一个线程的执行时间和执行顺序
优点:不会发生线程同步问题,相当于线程执行结束后才通知其他线程运行的一种串行化执行.
缺点:执行时间不可控,有可能发生无限阻塞.
抢占式调度
由Java来决定线程执行时间,通过线程的优先级.
优点:不会发生一个线程问题而所有线程都不可用的情况.
缺点:不能手动设定线程执行时间.
并不能通过设定的优先级来判断线程执行的先后顺序.
11.线程的六种状态.
new(创建态)
新创建一个线程,但还未启用.通过调用start方法来启用线程.只有线程对象,没有线程特征.
runable(就绪态/可运行态)
线程已启动,处于可运行状态,此时只需等待任务调度器调度就可以运行.此时已获得所需的计算机资源.
线程调用了 start() 方法后,就进入了可运行状态。这个状态包括两种子状态:
就绪(Ready): 线程准备运行,等待CPU分配时间片。
运行中(Running): 线程获得了CPU时间片,正在执行线程的代码。
blocked(阻塞态)
当一个线程想要获取一个对象锁但该对象锁被别的线程持有,则该线程进入阻塞态,直到该线程获得该对象锁,解除阻塞变成runable状态.
time waiting(有限等待)
线程调用带时间参数的方法就会进入time waiting状态,直到时间限制或接收到唤醒通知.
waiting(无限等待)
一个线程在等待另一个线程的唤醒,此状态不能自动唤醒,必须通过notify或notifyAll方法唤醒.
teminated(结束态)
run方法正常执行完成死亡,或因为没有捕获异常而死亡.
六种状态的互相转换:
new -> runnable
调用线程t.start()方法.此时线程t被加入线程调度器的队列中,等待计算机调度.
runnable -> running
任务调度器通过对应算法选择线程分配时间片,若选中线程t,为线程t分配时间片后可以开始执行.
running -> runnable
线程t所分配到的时间片已结束或被更高优先级的线程所抢占,则会转回可运行态等待下一次cpu分配时间片.
running/runnable -> blocked
线程t获取一个内部锁,但该锁被其他线程持有或则执行一些IO的阻塞操作,线程就会进入阻塞态.
running -> waiting/time waiting
线程t在执行中被一些方法中断进入等待状态 , 如:Object.wait()的无参或带参限时等待 , Thread.join()的无参或带参限时等待 , Thread.sleep()的无参或带参限时睡眠.值得一提的是yield()方法仅是提示计算机应进行一次cpu竞争,其表现形式并不是让线程中断,只是将进程从running变成了runable.
waiting/time waiting -> runnable
无限等待需要notify()/notifyAll()来转换状态,而有限等待可以等待时间限制解除或interrupt()方法中断等待,进入runnable状态.
任何状态 -> teminated
run方法执行结束.或run方法异常未被捕获而退出方法.
12.如何查看线程?你在什么系统上查看过线程?
windows
通过任务管理器可以查看线程和杀死线程
通过cmd命令窗口
调用tasklist查看线程 tasklist
taskkill 杀死线程. tasklist /IM xxx.exe /F 以对象映射名强制终止线程.
linux
ps -ef 查看所有进程
ps -fT -p <PID> 查看对应PID的所有线程
kill 杀死线程
top 按大写H切换是否显示线程
top -H -p <PID> 查看对应PID的所有线程
Java
jps 命令查看所有线程
jstack <PID> 查看某个PID的所有线程状态
jconsole 在Java图形化界面中查看某一个线程的运行状态
13.synchronized(同步锁)
自己的话:
synchronized是一个修改符级别的关键字,用于将对象、类、方法加上同步锁。在一个线程调用拥有同步锁的方法或者对象时,其他对象就若要获取相同的方法或对象就会被丢进monitor(管程)里的阻塞队列中,synchronized是一个不公平锁,所以在线程释放后,阻塞队列里需要竞争才会重新拿到该类的使用权。synchronized的锁还有优化方式,分别是偏向锁、轻量级锁、重量级锁,其锁的转换依靠的是锁粗化的机制。偏向锁是指,当加上synchronized关键字的类或方法被线程调用的时候,第一次调用的线程就不需要再次检验recordID,可以优化其使用。轻量级锁是指,两个或多个线程在没有发生争用或某一线程阻塞的情况下使用该对象或方法,则锁会优化成轻量级锁,轻量级锁不会生成一个重量级的monitor管程来记录阻塞。最后,当发生了多个线程调用同一个对象或方法时,为了记录所有的线程该以何种顺序使用该类,则需要将锁转换为重量级锁,而此时通过轻量级锁进入的线程不能正常的通过轻量级锁的方式退出该对象或方法。会重新记录自己的ID进入管程中,再退出重量级锁。基于上述的原因,synchronized保证了线程操作的原子性。
AI:
Java中的synchronized关键字是一种内置的同步锁,用于控制多线程对共享资源的访问,以避免数据不一致性和竞态条件。它提供了几种锁机制和原理,具体包括:
监视器锁(Monitor Lock):
synchronized的实现基于Java对象的监视器锁(Monitor)。每个对象都关联着一个监视器,这是实现同步的基础结构。
当线程进入synchronized代码块或方法时,它会自动获取对象的监视器锁。如果锁已被其他线程持有,那么当前线程将被阻塞,直到锁被释放。
监视器的作用
互斥(Mutual Exclusion): 确保任何时刻只有一个线程能够执行临界区(Critical Section)的代码,防止数据竞争和不一致性。
同步(Synchronization): 允许多个线程按照预定的顺序执行,比如通过wait-notify机制实现线程间的协作。
线程安全: 通过锁的管理,保障线程安全,避免数据损坏和竞态条件。
监视器的机制
对象头(Object Header): 每个Java对象都包含一个对象头,其中记录了对象的锁状态信息。这包括了偏向锁标记、轻量级锁指针或指向重量级锁的指针等。
锁的获取与释放:
1.当线程试图进入synchronized代码块或方法时,它会尝试获取对象的监视器锁。
2.对于偏向锁和轻量级锁,通常采用CAS操作尝试更新对象头中的锁标志位,而无需操作系统介入。
3.如果锁升级为重量级锁,线程会被阻塞并由操作系统管理,进入等待队列。
等待集(Entry Set / Wait Set):
当线程因无法获得锁而需要等待时,它会被加入到监视器的等待集中。对于轻量级锁,这个集合称为Entry Set;而对于重量级锁,未获取到锁的线程会被转移至操作系统的等待队列。
使用wait()方法的线程会被移到单独的Wait Set中,并释放锁,直到被notify()或notifyAll()唤醒。
自旋(Spinning): 在轻量级锁机制中,如果锁暂时不可用,线程会执行自旋操作,即在原地循环等待锁的释放,而不是立即挂起,这样可以减少线程上下文切换的开销。
锁升级与降级: 从偏向锁到轻量级锁再到重量级锁的升级路径,以及在条件允许时锁的降级,都是为了在性能和线程安全之间取得平衡。
内存屏障(Memory Barrier): 在进入和退出监视器区域时,会插入内存屏障来确保可见性和有序性,保证了线程间的数据一致性。
锁的升级与降级:
Java 6之后,为了提高性能,synchronized引入了偏向锁、轻量级锁和重量级锁的概念,这称为锁的升级与降级策略。
偏向锁(Biased Locking):
偏向锁假定大多数情况下锁只会被单一线程访问,因此在第一次线程获取锁时,它会将锁标记为“偏向”此线程。后续此线程再访问时,无需执行昂贵的加锁操作,只需检查Mark Word是否仍指向自己即可。
如果有其他线程尝试获取这个锁,偏向锁会被撤销,并可能升级为轻量级锁或重量级锁。
轻量级锁(Lightweight Locking):
当多个线程竞争偏向锁时,偏向锁会被升级为轻量级锁。轻量级锁通过自旋(Spin)来尝试获取锁,即线程在循环中不断尝试执行CAS(Compare-And-Swap)操作来获取锁,避免了线程上下文切换的开销。
自旋锁适合于线程持有锁时间很短的情况,能有效提升性能。但如果自旋时间过长(默认自旋次数可配置),则会消耗CPU资源,此时锁会被升级。
重量级锁(Heavyweight Locking):
当自旋操作超时或自旋过程中有更多线程请求锁时,轻量级锁会膨胀为重量级锁。这时,未获取到锁的线程会被阻塞,并放入操作系统的等待队列中,由操作系统负责线程的调度和唤醒。
重量级锁会导致线程上下文切换,性能开销较大,是最后的手段。
自适应自旋(Adaptive Spinning):
JVM可以根据之前的自旋结果(是否成功获取到锁)动态调整自旋的次数,以达到性能和资源消耗之间的最佳平衡。如果自旋经常成功,则增加自旋次数;反之,则减少。
可重入性:
synchronized锁是可重入的,意味着已经持有锁的线程可以再次获取同一把锁而不会造成死锁。这允许了递归调用或在同步代码块内部调用其他同步方法。
非公平锁:
synchronized默认实现的是非公平锁,即当锁释放时,等待的线程没有特定的排队顺序,任何一个等待线程都有机会直接尝试获取锁,这可能导致某些线程“插队”。
同步队列与等待队列:
当线程无法获取锁时,它会被加入到对象的入口等待队列(EntryList)中。
在重量级锁状态下,未获取到锁的线程最终可能会被转移到操作系统管理的等待队列中,等待被唤醒。
底层指令:
同步代码块通过monitorenter和monitorexit两条字节码指令实现。monitorenter在方法或代码块开始处,尝试获取锁;monitorexit在结束处,释放锁。
性能影响:
虽然synchronized提供了强大的同步能力,但其重量级特性,尤其是在涉及线程上下文切换时,可能会对性能产生一定影响,尤其是在高并发场景下。
14 死锁和活锁
死锁产生的原因有4个:
1.请求和保持条件:线程占用资源后如果还需要其他资源,若一直没有请求到资源,他也不会释放手里的资源。
2.循环等待条件:循环请求,另一个线程和本线程互相需要各自手里的资源,但两个线程都不释放。
3.互斥条件:一个资源只能被一个线程所持有。
4.不可抢占条件:其他线程不能抢夺已有资源的线程,必须等待释放。
破解死锁只需要将上述的四个条件中的一个破除则可以完成破解。
活锁产生的原因是:两个线程互相修改对方的终止条件,最终无法顺利终止。
查看锁可以利用cmd命令行的jdk原生jconsole图形化界面查看
也可以在终端中使用jps查看java程序端口,再输入jstack <PID> 查看对应的线程信息。
死锁信息像这样子:
Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x0000000000000000 (object 0x00000007e004a6f0, a java.lang.Object),
which is held by "Thread-2"
"Thread-2":
waiting to lock monitor 0x0000000000000000 (object 0x00000007e004a708, a java.lang.Object),
which is held by "Thread-1"