文章目录
1.线程
1)什么是进程和线程?
进程是程序运行资源分配的最小单位
线程是CPU调度的最小单位,必须依赖于进程而存在
线程无处不在
CPU核心数和线程数的关系
多核心:也指单芯片多处理器(Chip Multiprocessors,简称CMP),将大规模并行处理器中的SMP(对称多处理器)集成到同一芯片内,各个处理器并行执行不同的进程。这种依靠多个CPU同时并行地运行程序是实现高速计算的一个重要方向,成为并行处理。
多线程:Simultaneous Multithreading 简称SMT。让同一个处理器上的多个线程同步执行并共享处理器的资源。
核心数、线程数:目前主流的CPU都是多核的。增加核心数目就是增加线程数,因为操作系统是通过线程来执行任务的,一般情况下他们1:1对应关系,也就是说四核CPU一般拥有四个线程。但Intel引入超线程技术后,使核心数与线程数行程1:2的关系。
CPU时间片轮转机制
时间片轮转调度,又称RR调度。每个进程被分配一个时间段,称做它的时间片,即该进程允许运行的时间。
时间片设得太短会导致过多的进程切换,降低CPU效率,而设得太长又可能引起对短的交互请求的响应变差。
并发和并行
并发:交替执行
并行:同时执行
多线程的好处:
- 充分利用CPU的资源
- 加快响应用户的时间
- 可以使代码模块化、异步化、简单化
多线程程序的注意事项:
- 线程之间的安全性
- 线程之间的死锁
- 线程太多会将服务器资源耗尽形成死机宕机
2)线程启动与中止
启动线程的方式有几种?
有两种,类Thread、类Runnable
public class Th {
private static class UseThread extends Thread{
@Override
public void run() {
System.out.println("I am extends Thread");
}
}
private static class UseRunnable implements Runnable{
@Override
public void run() {
System.out.println("I am implements Runnable");
}
}
public static void main(String[] args) {
UseThread useThread = new UseThread();
useThread.start();
UseRunnable useRunnable = new UseRunnable();
new Thread(useRunnable).start();
}
}
Thread和Runnable的区别
Thread是java里对线程的唯一抽象,Runnable只是对任务(业务逻辑)的抽象。Thread可以接受任意一个Runnable的实例并执行。
线程终止 stop
暂停、恢复和停止对应线程Thread的API是suspend()、resume()、stop()。这些API已经过期了,不建议使用,原因是:以suspend()为例,在调用后,线程不会释放已经占有的资源(比如锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题。同样,stop()方法在终结一个线程时不会保证线程的资源正常释放,通常是没有给予线程完成资源释放工作的机会,因此会导致程序可能工作在不确定状态下。
中断interrupt
安全的中止则是其他线程通过调用某个线程A的interrupt()方法对其进行中断操作,中断好比其他线程对该线程打了个招呼,不代表A会立即停止自己的工作,同样的A线程完全可以不理会这种中断的请求。因为java里线程是协作式的,不是抢占式的。线程通过检查自身的中断标志位是否被置为true来进行响应。
线程通过方法isInterrupted()来进行判断是否被打断,也可以调用静态方法Thread.interrupted()来进行判断当前线程是否被中断,不过Thread.interrupted()会同时将终端标志位改写为false。
public class Th {
private static class UseThread extends Thread{
public UseThread(String name){
super(name);
}
@Override
public void run() {
System.out.println( Thread.currentThread().getName()+" interrupt flag="+isInterrupted());
// while (!isInterrupted()){
while (!Thread.interrupted()){
// while (true){
System.out.println("is running");
System.out.println( Thread.currentThread().getName()+" inner interrupt flag="+isInterrupted());
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread endThread = new UseThread("endThread");
endThread.start();
Thread.sleep(20);
endThread.interrupt(); // 终端线程,其实是设置线程的标识位true
}
}
实现接口Runnable的线程如何中断
public class Th {
private static class UseRunnable implements Runnable{
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()){
System.out.println( Thread.currentThread().getName()+" I am implements Runnable.");
}
System.out.println(Thread.currentThread().getName()+" interrupt flag is "+ Thread.currentThread().isInterrupted());
}
}
public static void main(String[] args) throws InterruptedException {
UseRunnable useRunnable = new UseRunnable();
Thread endThread = new Thread(useRunnable, "endThread");
endThread.start();
Thread.sleep(20);
endThread.interrupt();
}
}
如果一个线程处于了阻塞状态(如线程调用了 thread.sleep、thread.join、thread.wait 等),则在线程在检查中断标示时如果发现中断标示为 true,则会在这些阻塞方法调用处抛出 InterruptedException 异常,并且在抛出异常后会立即将线程的中断标示位清除,即重新设置为 false。
public class Th {
private static class UseThread extends Thread{
public UseThread(String name){
super(name);
}
@Override
public void run() {
System.out.println( Thread.currentThread().getName()+" interrupt flag="+isInterrupted());
while (!isInterrupted()){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName()+" in InterruptedException interrupt flag is "+ isInterrupted()); // false
// 可以做资源的释放 是否做中断线程 ---------
interrupt(); // 加这个才能真正的中断
e.printStackTrace();
}
System.out.println( Thread.currentThread().getName()+" interrupt flag="+isInterrupted());
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread endThread = new UseThread("endThread");
endThread.start();
Thread.sleep(20);
endThread.interrupt(); // 终端线程,其实是设置线程的标识位true
}
}
不建议自定义一个取消标志位来中止线程的运行。因为 run 方法里有阻塞调用时会无法很快检测到取消标志,线程必须从阻塞调用返回后,才会检查这个取消标志。这种情况下,使用中断会更好,因为,
一、一般的阻塞方法,如 sleep 等本身就支持中断的检查,
二、检查中断位的状态和检查取消标志位没什么区别,用中断位的状态还可以避免声明取消标志位,减少资源的消耗。
注意:处于死锁状态的线程无法被中断
public class Th {
private static class UseThread extends Thread{
private boolean cancel;
public boolean isCancel() {
return cancel;
}
public void setCancel(boolean cancel) {
this.cancel = cancel;
}
public UseThread(String name){
super(name);
}
@Override
public void run() {
System.out.println( Thread.currentThread().getName()+" interrupt flag="+isInterrupted());
while (cancel){
Thread.sleep(10); // 这个时候就会出现问题
}
while (!isInterrupted()){
System.out.println("is running");
System.out.println( Thread.currentThread().getName()+" inner interrupt flag="+isInterrupted());
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread endThread = new UseThread("endThread");
endThread.start();
Thread.sleep(20);
endThread.interrupt(); // 终端线程,其实是设置线程的标识位true
((UseThread)endThread).setCancel(true);
}
}
run()和start()
Thread类是Java里对线程概念的抽象,可以这样理解:通过new Thread()其实只是new出一个Thread的实例,还没有操作系统中真正的线程挂起钩来。只有执行了start()方法后,才实现了真正意义上的启动线程。
start()方法让一个线程进入就绪队列等待分配 cpu,分到 cpu 后才调用实现的 run()方法,start()方法不能重复调用,如果重复调用会抛出异常。
而 run 方法是业务逻辑实现的地方,本质上和任意一个类的任意一个成员方法并没有任何区别,可以重复执行,也可以被单独调用。
public class Th {
public static class ThreadRun extends Thread{
@Override
public void run() {
int i = 10;
while (i>0){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("I am "+Thread.currentThread().getName()+" and now the i="+i--);
}
}
}
public static void main(String[] args) {
ThreadRun threadRun = new ThreadRun();
threadRun.setName("threadRun");
// threadRun.start(); // I am threadRun and now the i=10
threadRun.run(); // I am main and now the i=10
}
}
join()方法
把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行。比如在线程B中调用了线程A的jion()方法,直到线程A执行完毕后,才会执行线程B。
public class Th {
private static class B extends Thread{
private Thread thread;
public B(Thread thread){
this.thread = thread;
}
public B(){}
@Override
public void run() {
System.out.println("B 开始排队...");
if (thread != null){
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" B结束");
}
}
static class C extends Thread{
@Override
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("C 开始排队...");
System.out.println(Thread.currentThread().getName()+" C结束");
}
}
public static void main(String[] args) throws Exception {
Thread a = Thread.currentThread();
Thread c = new C();
Thread b = new B(c);
b.start();
c.start();
System.out.println("A 开始排队...");
b.join();
System.out.println(Thread.currentThread().getName()+" A 结束");
}
}
执行结果为:
A 开始排队...
B 开始排队...
C 开始排队...
Thread-0 C结束
Thread-1 B结束
main A 结束
yield()方法
使当前线程让出CPU占有权,但让出的时间是不可设定的。也不会释放锁资源。注意:并不是每个线程都需要这个锁的,而且执行yield()的时候不一定就会持有锁,完全可以再释放锁后再调用yield方法。
所有执行yield()的线程有可能进入到就绪状态后就会被操作系统再次选中马上又被执行。
等待/通知机制wait() notify() notifyAll()
是指一个线程 A 调用了对象 O 的 wait()方法进入等待状态,而另一个线程 B调用了对象 O 的 notify()或者 notifyAll()方法,线程 A 收到通知后从对象 O 的 wait()方法返回,进而执行后续操作。上述两个线程通过对象 O 来完成交互,而对象上的 wait()和 notify/notifyAll()的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。
notify():
通知一个在对象上等待的线程,使其从 wait 方法返回,而返回的前提是该线程获取到了对象的锁,没有获得锁的线程重新进入 WAITING 状态。
notifyAll():
通知所有等待在该对象上的线程
wait()
调用该方法的线程进入 WAITING 状态,只有等待另外线程的通知或被中断才会返回.需要注意,调用 wait()方法后,会释放对象的锁
wait(long)
超时等待一段时间,这里的参数时间是毫秒,也就是等待长达n 毫秒,如果没有通知就超时返回
wait (long,int)
对于超时时间更细粒度的控制,可以达到纳秒
等待和通知的标准范式:
等待方遵循如下原则:
1)获取对象的锁。
2)如果条件不满足,那么调用对象的 wait()方法,被通知后仍要检查条件。
3)条件满足则执行对应的逻辑。
通知方遵循如下原则:
1)获得对象的锁。
2)改变条件。
3)通知所有等待在对象上的线程。
在调用 wait()、notify()系列方法之前,线程必须要获得该对象的对象级别锁,即只能在同步方法或同步块中调用 wait()方法、notify()系列方法,进入 wait()方法后,当前线程释放锁,在从 wait()返回前,线程与其他线程竞争重新获得锁,执行 notify()系列方法的线程退出调用了 notifyAll 的 synchronized代码块的时候后,他们就会去竞争。如果其中一个线程获得了该对象锁,它就会继续往下执行,在它退出 synchronized 代码块,释放锁后,其他的已经被唤醒的线程将会继续竞争获取该锁,一直进行下去,直到所有被唤醒的线程都执行完毕。
notify 和 notifyAll 应该用谁:
尽可能用 notifyall(),谨慎使用 notify(),因为 notify()只会唤醒一个线程,我们无法确保被唤醒的这个线程一定就是我们需要唤醒的线程
线程的优先级
在 Java 线程中,通过一个整型成员变量 priority 来控制优先级,优先级的范围从 1~10,在线程构建的时候可以通过 setPriority(int)方法来修改优先级,默认优先级是 5,优先级高的线程分配时间片的数量要多于优先级低的线程。
设置线程优先级时,针对频繁阻塞(休眠或者 I/O 操作)的线程需要设置较高优先级,而偏重计算(需要较多 CPU 时间或者偏运算)的线程则设置较低的优先级,确保处理器不会被独占。在不同的 JVM 以及操作系统上,线程规划会存在差异,有些操作系统甚至会忽略对线程优先级的设定。
守护线程
Daemon(守护)线程是一种支持型线程,因为它主要被用作程序中后台调度以及支持性工作。这意味着,当一个 Java 虚拟机中不存在非 Daemon 线程的时候,Java 虚拟机将会退出。可以通过调用 Thread.setDaemon(true)将线程设置为 Daemon 线程。我们一般用不上,比如垃圾回收线程就是 Daemon 线程。
Daemon 线程被用作完成支持性工作,但是在 Java 虚拟机退出时 Daemon 线程中的 finally 块并不一定会执行。在构建 Daemon 线程时,不能依靠 finally 块中的内容来确保执行关闭或清理资源的逻辑。
public class Th {
private static class B extends Thread{
@Override
public void run() {
try{
while (!isInterrupted()){
System.out.println(Thread.currentThread().getName()+" I am extends Thread.");
System.out.println(Thread.currentThread().getName()+" interrupt flag is "+isInterrupted());
}
} finally { // 守护线程中finally不一定起作用
System.out.println(" This is finally.......");
}
}
}
public static void main(String[] args) throws Exception {
// Thread b = new B();
// b.start();
// Thread.sleep(10);
// b.interrupt();
Thread b = new B();
b.setDaemon(true);
b.start();
Thread.sleep(2);
b.interrupt();
}
}
可见性
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
由于线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量,那么对于共享变量V,它们首先是在自己的工作内存,之后再同步到主内存。可是并不会及时的刷到主存中,而是会有一定时间差。很明显,这个时候线程 A 对变量 V 的操作对于线程 B 而言就不具备可见性了 。
要解决共享对象可见性这个问题,可以使用volatile关键字或者是加锁。
原子性
原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
因为操作系统做任务切换,可以发生在任何一条CPU 指令执行完!是 CPU 指令,而不是高级语言里的一条语句。比如count++,在java里就是一句话,但高级语言里一条语句往往需要多条 CPU 指令完成。其实count++至少包含了三个CPU指令!
2.线程间的共享
1)synchronized内置锁
Java 支持多个线程同时访问一个对象或者对象的成员变量,关键字synchronized 可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性,又称为内置锁机制。
实现原理:
Synchronized在JVM里的实现都是基于进入和退出Monitor对象来实现方法同步和代码块同步,虽然具体实现细节不一样,但是都可以通过成对的MonitorEnter和MonitorExit指令来实现。
对同步块,MonitorEnter指令插入在同步代码块的开始位置,当代码执行到该指令时,将会尝试获取该对象Monitor的所有权,即尝试获得该对象的锁,而monitorExit指令则插入在方法结束处和异常处,JVM保证每个MonitorEnter必须有对应的MonitorExit。
对同步方法,从同步方法反编译的结果来看,方法的同步并没有通过指令monitorenter和monitorexit来实现,相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标示符。
JVM就是根据该标示符来实现方法的同步的:当方法被调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。
对象锁和类锁:
对象锁是用于对象实例方法,或者一个对象实例上的,类锁是用于类的静态方法或者一个类的 class 对象上的。我们知道,类的对象实例可以有很多个,但是每个类只有一个 class 对象,所以不同对象实例的对象锁是互不干扰的,但是每个类只有一个类锁。
但是有一点必须注意的是,其实类锁只是一个概念上的东西,并不是真实存在的,类锁其实锁的是每个类的对应的 class 对象。类锁和对象锁之间也是互不干扰的。
synchronized用在同步块、方法上:
public class Th {
private long count = 0;
private Object object = new Object(); // 作为一个锁
public long getCount() {
return count;
}
public void setCount(long count) {
this.count = count;
}
// 用在同步块上
public void incCount(){
synchronized (object){
count++;
}
}
// 用在方法上
public synchronized void incCount2(){
count++;
}
// 用在同步块上,但是锁的是当前类的对象实例
public void incCount3(){
synchronized (this){
count++;
}
}
private static class Count extends Thread{
private Th th;
public Count(Th th){
this.th = th;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
th.incCount();
}
}
}
public static void main(String[] args) throws Exception {
Th th = new Th();
Count count1 = new Count(th);
Count count2 = new Count(th);
count1.start();
count2.start();
Thread.sleep(100);
System.out.println(th.count);
}
}
**对象锁:**锁的实例不一样时,可以并行。
public class Th {
private static class InstanceSyn extends Thread{
private Th th;
public InstanceSyn(Th th){
this.th = th;
}
@Override
public void run() {
System.out.println("TestInstance is running..."+th);
try {
th.instance1();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private synchronized void instance1() throws InterruptedException {
Thread.sleep(3000);
System.out.println("synInstance is going..."+this.toString());
Thread.sleep(3000);
System.out.println("synInstance ended..."+this.toString());
}
private synchronized void instance2() throws InterruptedException {
Thread.sleep(3000);
System.out.println("synInstance2 is going..."+this.toString());
Thread.sleep(3000);
System.out.println("synInstance2 ended..."+this.toString());
}
public static void main(String[] args) throws Exception {
Th instance1 = new Th();
Th instance2 = new Th();
Thread t1 = new InstanceSyn(instance1); // 锁的实例不一样时,可以并行
Thread t2 = new InstanceSyn(instance2); // 锁的实例不一样时,可以并行
t1.start();
t2.start();
Thread.sleep(1000);
}
}
类锁:
public class Th {
// Synchronized修饰静态的方法
private static synchronized void synClass() throws InterruptedException {
System.out.println(Thread.currentThread().getName()+" synClass going...");
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+" synClass end");
}
// 锁的对象是object,即object类的锁
private static Object object = new Object();
private static void synStatic() throws InterruptedException {
synchronized (object){
System.out.println(Thread.currentThread().getName()+" synStatic going...");
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+" synStatic end");
}
}
}
2)错误的加锁和原因分析
public class Th {
private static class UseRunnable implements Runnable{
Object object = new Object();
private Integer i;
public UseRunnable(Integer i){
this.i = i;
}
@Override
public void run() {
// synchronized (i){ // 这个加锁是锁不住的
synchronized (object){
System.out.println(Thread.currentThread().getName()+ "-@"+System.identityHashCode(i));
i++;
System.out.println(Thread.currentThread().getName()+ "------" + i + "-@"+System.identityHashCode(i));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+ "------" + i + "--@"+System.identityHashCode(i));
}
}
}
public static void main(String[] args) throws InterruptedException {
UseRunnable u = new UseRunnable(1);
for (int i = 0; i < 5; i++) {
new Thread(u).start();
}
}
}
虽然对 i 进行了加锁,但是没法锁住的:
i++其实是Integer.valueOf(this.i.intValue() + i);
返回的是一个新的Integer对象, 也就是每个线程实际加锁的是不同的Integer对象。
3)volatile,最轻量的同步机制
volatile 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
public class Th {
private volatile static boolean ready;
private static int number;
private static class PrintThread extends Thread{
@Override
public void run() {
System.out.println("PrintThread is running.......");
while(!ready);
System.out.println("number = "+number);
}
}
public static void main(String[] args) throws InterruptedException {
new PrintThread().start();
Thread.sleep(1000);
number = 51;
ready = true;
Thread.sleep(5000);
System.out.println("main is ended!");
}
}
不加 volatile 时,子线程无法感知主线程修改了 ready 的值,从而不会退出循环,而加了 volatile 后,子线程可以感知主线程修改了 ready 的值,迅速退出循环。
但是 volatile 不能保证数据在多个线程下同时写时的线程安全
volatile 最适用的场景:一个线程写,多个线程读。
特性:
可见性。对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
原子性:对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性。
volatile虽然能保证执行完及时把变量刷到主内存中,但对于count++这种非原子性、多指令的情况,由于线程切换,线程A刚把count=0加载到工作内存,线程B就可以开始工作了,这样就会导致线程A和B执行完的结果都是1,都写到主内存中,主内存的值还是1不是2
实现原理:
volatile关键字修饰的变量会存在一个“lock:”的前缀。
Lock前缀,Lock不是一种内存屏障,但是它能完成类似内存屏障的功能。Lock会对CPU总线和高速缓存加锁,可以理解为CPU指令级的一种锁。
同时该指令会将当前处理器缓存行的数据直接写会到系统内存中,且这个写回内存的操作会使在其他CPU里缓存了该地址的数据无效。
4)lock
void lock(): 上锁
void unlock():释放锁
boolean tryLock():用来尝试获取锁:成功获取则返回true;获取失败则返回false
public class LockTest {
private static Lock No13 = new ReentrantLock();//第一个锁
private static Lock No14 = new ReentrantLock();//第二个锁
//先尝试拿No13 锁,再尝试拿No14锁,No14锁没拿到,连同No13 锁一起释放掉
private static void fisrtToSecond() throws InterruptedException {
String threadName = Thread.currentThread().getName();
Random r = new Random();
while (true) {
if (No13.tryLock()) {
System.out.println(threadName
+ " get 13");
try {
if (No14.tryLock()) {
try {
System.out.println(threadName
+ " get 14");
System.out.println("fisrtToSecond do work------------");
break;
} finally {
No14.unlock();
}
}
} finally {
No13.unlock();
}
}
Thread.sleep(r.nextInt(3));
}
}
//先尝试拿No14锁,再尝试拿No13锁,No13锁没拿到,连同No14锁一起释放掉
private static void SecondToFisrt() throws InterruptedException {
String threadName = Thread.currentThread().getName();
Random r = new Random();
while (true) {
if (No14.tryLock()) {
System.out.println(threadName
+ " get 14");
try {
if (No13.tryLock()) {
try {
System.out.println(threadName
+ " get 13");
System.out.println("SecondToFisrt do work------------");
break;
} finally {
No13.unlock();
}
}
} finally {
No14.unlock();
}
}
Thread.sleep(r.nextInt(3));
}
}
private static class TestThread extends Thread {
private String name;
public TestThread(String name) {
this.name = name;
}
public void run() {
Thread.currentThread().setName(name);
try {
SecondToFisrt();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Thread.currentThread().setName("TestDeadLock");
TestThread testThread = new TestThread("SubTestThread");
testThread.start();
try {
fisrtToSecond();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
5)死锁
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁。
死锁是必然发生在多操作者(M>=2个)情况下,争夺多个资源(N>=2个,且N<=M)才会发生这种情况。很明显,单线程自然不会有死锁。
a) 多个操作者争夺多个资源
b) 争夺资源的顺序不对
c) 拿到资源不放手
学术化定义:
a)互斥条件
指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。
b)请求和保持条件
指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
c)不剥夺条件
指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
d)环路等待条件
指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。
只要打破四个必要条件之一就能有效预防死锁的发生:
打破互斥条件:改造独占性资源为虚拟资源,大部分资源已无法改造。
打破不可抢占条件:当一进程占有一独占性资源后又申请一独占性资源而无法满足,则退出原占有的资源。
打破占有且申请条件:采用资源预先分配策略,即进程运行前申请全部资源,满足则运行,不然就等待,这样就不会占有且申请。
打破循环等待条件:实现资源有序分配策略,对所有设备实现分类编号,所有进程只能采用按序号递增的形式申请资源。
避免死锁常见的算法有有序资源分配法、银行家算法。
6)活锁
两个线程在尝试拿锁的机制中,发生多个线程之间互相谦让,不断发生同一个线程总是拿到同一把锁,在尝试拿另一把锁时因为拿不到,而将本来已经持有的锁释放的过程。
A(拥有1)(争夺2)–(拥有1)(争夺2)–(拥有1)(争夺2)–(拥有1)(争夺2)
B(拥有2)(争夺1)–(拥有2)(争夺1)–(拥有2)(争夺1)–(拥有2)(争夺1)
就会造成一直很忙碌的情况,但没法跳出来
解决办法: 每个线程休眠随机数,错开拿锁的时间。
注:sleep会让出CPU,但不会释放锁,故不要在锁里面sleep。
7)自旋锁
原理:
如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。
但是线程自旋是需要消耗CPU的,说白了就是让CPU在做无用功,线程不能一直占用CPU自旋做无用功,所以需要设定一个自旋等待的最大时间。
如果持有锁的线程执行的时间超过自旋等待的最大时间仍没有释放锁,就会导致其它争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。
自旋锁的优缺点:
自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起操作的消耗!
但是如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适合使用自旋锁了,因为自旋锁在获取锁前一直都是占用cpu做无用功,占着XX不XX,线程自旋的消耗大于线程阻塞挂起操作的消耗,其它需要cpu的线程又不能获取到cpu,造成cpu的浪费。
自旋锁时间阈值:
自旋锁的目的是为了占着CPU的资源不释放,等到获取到锁立即进行处理。但是如何去选择自旋的执行时间呢?如果自旋执行时间太长,会有大量的线程处于自旋状态占用CPU资源,进而会影响整体系统的性能。因此自旋次数很重要
JVM对于自旋次数的选择,jdk1.5默认为10次,在1.6引入了适应性自旋锁,适应性自旋锁意味着自旋的时间不在是固定的了,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定,基本认为一个线程上下文切换的时间是最佳的一个时间。
JDK1.6中-XX:+UseSpinning开启自旋锁; JDK1.7后,去掉此参数,由jvm控制;
8)线程饥饿
线程的优先级低,一直等待,拿不到执行时间。
9)锁的状态
一共有四种状态,无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态,它会随着竞争情况逐渐升级。锁可以升级但不能降级,目的是为了提高获得锁和释放锁的效率。
3.ThreadLocal
ThreadLocal和Synchronized都用于解决多线程并发访问。可是ThreadLocal与Synchronized有本质的差别。Synchronized是利用锁的机制,使变量或代码块在某一时刻仅仅能被一个线程访问。而ThreadLocal为每个线程都提供了变量的副本,使得每个线程在某一时间访问到的并非同一个对象,这样就隔离了多个线程对数据的数据共享。
1)ThreadLocal的方法:
1)set(Object value)
设置当前线程的线程局部变量的值
2)get()
会返回当前线程所对应的线程局部变量
3)remove()
将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK5.0新增的方法。需要指出的是,当线程结束后,对应线程的局部变量将自动被垃圾回收,所以显示调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
4)initialValue()
返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第一次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。
private static ThreadLocal threadLocal = new ThreadLocal()
threadLocal 代表了一个能够存放Integer类型的ThreadLocal对象。此时不论什么一个线程能够并发访问这个变量,对它进行读写操作,都是线程安全的。
public class UseThreadLocal {
private static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>(){
@Override
protected Integer initialValue() {
return 1;
}
};
// 运行3个线程
public void StartThreadArray(){
Thread[] runs = new Thread[3];
for (int i = 0; i < runs.length; i++) {
runs[i] = new Thread(new TestThread(i));
}
for (int i = 0; i < runs.length; i++) {
runs[i].start();
}
}
// 测试线程,线程的工作是将ThreadLocal变量的值变化,并写回,看看线程之间是否会互相影响
public static class TestThread implements Runnable{
int id;
public TestThread(int id){
this.id = id;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ":start");
Integer s = threadLocal.get();
s = s + id;
threadLocal.set(s);
System.out.println(Thread.currentThread().getName() + ":" + threadLocal.get());
}
}
public static void main(String[] args) {
UseThreadLocal test = new UseThreadLocal();
test.StartThreadArray();
}
}
2)ThreadLocal实现解析
上面先取到当前线程,然后调用getMap方法获取对应的ThreadLocalMap,ThreadLocalMap是ThreadLocal的静态内部类,然后Thread类中有一个这样类型成员,所以getMap是直接返回Thread的成员。
ThreadLocal的内部类ThreadLocalMap源码:
可以看到有个Entry内部静态类,它继承了WeakReference,总之它记录了两个信息,一个是ThreadLocal<?>类型,一个是Object类型的值。getEntry方法则是获取某个ThreadLocal对应的值,set方法就是更新或赋值相应的ThreadLocal对应的值。
get方法,其实就是拿到每个线程独有的ThreadLocalMap。然后再利用ThreadLocal的当前实例,拿到Map中的相应的Entry,然后就可以拿到对应的值返回出去。当然,如果Map为空,还会进行map的创建,初始化等工作。
3)引发的内存泄露问题
引用:
Object o = new Object();
这个o,可以称之为对象引用,而new Object()可以称之为在内存中产生一个对象实例。
当写下o=null时,姿势表示o不再指向堆中object的对象实例,不代表这个对象实例不存在了。
强引用:
就是指在程序代码之中普遍存在的,类似“Object o = new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用等对象实例。
软引用:
是用来描述一些还有用但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象实例列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。在JDK1.2之后,提供了SoftReference类来实现软引用。
弱引用:
也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象实例只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象实例。在JDK1.2之后,提供了WeakReference来实现弱引用。
虚引用:
也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象实例是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为了一个对象设置虚引用关联的唯一目的就是能再这对象实例被收集器回收时收到一个系统通知。在JDK1.2之后,提供了PhantomReference类来实现虚引用。
内存泄露的现象
public class ThreadLocalOOM {
private static final int TASK_LOOP_SIZE = 500;
// 启用线程池,大小固定为5个线程
final static ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(
5,
5,
1,
TimeUnit.MINUTES,
new LinkedBlockingDeque<>());
static class LocalVariable {
private byte[] a = new byte[1024*1024*5];
}
final static ThreadLocal<LocalVariable> localVariable = new ThreadLocal<>();
public static void main(String[] args) {
for (int i = 0; i < TASK_LOOP_SIZE; i++) {
poolExecutor.execute(new Runnable() {
@Override
public void run() {
// 场景1
// System.out.println("use local variable.");
// 场景2
// new LocalVariable();
// System.out.println("use local variable.");
// 场景3
// localVariable.set(new LocalVariable());
// System.out.println("use local variable.");
// 场景4
localVariable.set(new LocalVariable());
System.out.println("use local variable.");
localVariable.remove();
}
});
}
System.out.println("pool execute over.");
}
}
并将堆内存大小设置为-Xmx256m
场景 1,首先任务中不执行任何有意义的代码,当所有的任务提交执行完成后,可以看见,我们这个应用的内存占用基本上为 25M 左右
场景 2,然后我们只简单的在每个任务中 new 出一个数组,执行完成后我们可以看见,内存占用基本和场景 1 同
场景 3,当我们启用了 ThreadLocal 以后,执行完成后我们可以看见,内存占用变为了 100M 左右
场景 4,于是,我们加入一行代码,再执行,看看内存情况:内存占用基本和场景 1 同。
这就充分说明,场景 3,当我们启用了 ThreadLocal 以后确实发生了内存泄漏
每个Thead维护一个ThreadLocalMap,这个映射表的key是ThreadLocal实例本身,value是真正需要存储的Object,也就是说ThreadLocal本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap获取value。观察ThreadLocalMap,这个map是使用ThreadLocal的弱引用作为key的,弱引用的对象再GC时会被回收。
因此使用了ThreadLocal后,引用链如图所示:图中的虚线表示弱引用
当把 threadlocal 变量置为 null 以后,没有任何强引用指向 threadlocal实例,所以 threadlocal 将会被 gc 回收。这样一来,ThreadLocalMap 中就会出现key 为 null 的 Entry,就没有办法访问这些 key 为 null 的 Entry 的 value,如果当前线程再迟迟不结束的话,这些 key 为 null 的 Entry 的 value 就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value,而这块 value 永远不会被访问到了,所以存在着内存泄露。
只有当前 thread 结束以后,current thread 就不会存在栈中,强引用断开,Current Thread、Map value 将全部被 GC 回收。最好的做法是不在需要使用ThreadLocal 变量后,都调用它的 remove()方法,清除数据。
场景 3 中,虽然线程池里面的任务执行完毕了,但是线程池里面的 5 个线程会一直存在直到 JVM 退出,我们 set 了线程的localVariable 变量后没有调用 localVariable.remove()方法,导致线程池里面的 5 个线程的 threadLocals 变量里面的 new LocalVariable()实例没有被释放。
其实考察 ThreadLocal 的实现,我们可以看见,无论是 get()、set()在某些时候,调用了 expungeStaleEntry 方法用来清除 Entry 中 Key 为 null 的 Value,但是这是不及时的,也不是每次都会执行的,所以一些情况下还是会发生内存泄露。只有 remove()方法中显式调用了 expungeStaleEntry 方法。
从表面上看内存泄漏的根源在于使用了弱引用,但是另一个问题也同样值得思考:为什么使用弱引用而不是强引用?
key 使用强引用:引用 ThreadLocal 的对象被回收了,但是 ThreadLocalMap还持有 ThreadLocal 的强引用,如果没有手动删除,ThreadLocal 的对象实例不会被回收,导致 Entry 内存泄漏。
key 使用弱引用:引用的 ThreadLocal 的对象被回收了,由于 ThreadLocalMap持有 ThreadLocal 的弱引用,即使没有手动删除,ThreadLocal 的对象实例也会被回收。value 在下一次 ThreadLocalMap 调用 set,get,remove 都有机会被回收。
比较两种情况,我们可以发现:由于 ThreadLocalMap 的生命周期跟 Thread 一样长,如果都没有手动删除对应 key,都会导致内存泄漏,但是使用弱引用可以多一层保障。
因此,ThreadLocal 内存泄漏的根源是:由于 ThreadLocalMap 的生命周期跟Thread 一样长,如果没有手动删除对应 key 就会导致内存泄漏,而不是因为弱引用。
总结:
JVM 利用设置 ThreadLocalMap 的 Key 为弱引用,来避免内存泄露。
JVM 利用调用 remove、get、set 方法的时候,回收弱引用。
当 ThreadLocal 存储很多 Key 为 null 的 Entry 的时候,而不再去调用 remove、get、set 方法,那么将导致内存泄漏。
使用线程池+ ThreadLocal 时要小心,因为这种情况下,线程是一直在不断的重复运行的,从而也就造成了 value 可能造成累积的情况。
4)错误使用ThreadLocal导致线程不安全
public class ThreadLocalUnsafe implements Runnable{
public static ThreadLocal<Number> value = new ThreadLocal<Number>() {};
public static Number number = new Number(0);
private static class Number {
public Number(int num) {
this.num = num;
}
private int num;
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
}
@Override
public String toString() {
return "Number [num=" + num + "]";
}
}
@Override
public void run() {
//每个线程计数加一
number.setNum(number.getNum()+1);
//将其存储到ThreadLocal中
value.set(number);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//输出num值
System.out.println(Thread.currentThread().getName()+"="+value.get().getNum());
}
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(new ThreadLocalUnsafe()).start();
}
}
}
Thread-2=5
Thread-1=5
Thread-0=5
Thread-3=5
Thread-4=5
为什么每个线程都输出 5?难道他们没有独自保存自己的 Number 副本吗?
为什么其他线程还是能够修改这个值?仔细考察 ThreadLocal 和 Thead 的代码,我们发现 ThreadLocalMap 中保存的其实是对象的一个引用,这样的话,当有其他线程对这个引用指向的对象实例做修改时,其实也同时影响了所有的线程持有的对象引用所指向的同一个对象实例。这也就是为什么上面的程序为什么会输出一样的结果:5 个线程中保存的是同一 Number 对象的引用,在线程睡眠的时候,其他线程将 num 变量进行了修改,而修改的对象 Number 的实例是同一份,因此它们最终输出的结果是相同的。
而上面的程序要正常的工作,应该的用法是让每个线程中的 ThreadLocal 都应该持有一个新的 Number 对象。
调用 yield() 、sleep()、wait()、notify()等方法对锁有何影响?
yield() 、sleep()被调用后,都不会释放当前线程所持有的锁。
调用 wait()方法后,会释放当前线程持有的锁,而且当前被唤醒后,会重新去竞争锁,锁竞争到后才会执行 wait 方法后面的代码。
调用 notify()系列方法后,对锁无影响,线程只有在 syn 同步代码执行完后才会自然而然的释放锁,所以 notify()系列方法一般都是 syn 同步代码的最后一行。
《wait/notify实现生产者和消费者程序》
采用多线程技术,例如wait/notify,设计实现一个符合生产者和消费者问题的程序,对某一个对象(枪膛)进行操作,其最大容量是20颗子弹,生产者线程是一个压入线程,它不断向枪膛中压入子弹,消费者线程是一个射出线程,它不断从枪膛中射出子弹。
请实现上面的程序。
public class MainTest {
// 子弹数
private Integer bulletNum = 0;
// 生产者 - 压入子弹
public synchronized void producerBullet(){
while (bulletNum >= 20){
try {
System.out.println(Thread.currentThread().getName() + " 子弹已经满了!");
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
bulletNum++;
System.out.println(Thread.currentThread().getName() + " 装入子弹,当前子弹数为:"+bulletNum);
// 唤醒消费者可以进行消费了
notifyAll();
}
// 消费者 - 射击
public synchronized void consumerBullet(){
while (bulletNum <= 0){
try {
System.out.println(Thread.currentThread().getName() + " 当前子弹数为0,没有子弹了!");
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
bulletNum--;
System.out.println(Thread.currentThread().getName() + " 当前字段数剩余:"+bulletNum);
// 唤醒生产者生产子弹
notifyAll();
}
// 生产者
public static class producer implements Runnable{
MainTest mainTest;
public producer(MainTest mainTest){
this.mainTest = mainTest;
}
@Override
public void run() {
while (true){
mainTest.producerBullet();
// 每1.5s生产压入子弹
try {
Thread.sleep(1500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 消费者
public static class consumer implements Runnable{
MainTest mainTest;
public consumer(MainTest mainTest){
this.mainTest = mainTest;
}
@Override
public void run() {
while (true){
mainTest.consumerBullet();
// 每1s消费一个子弹
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
MainTest mainTest = new MainTest();
// 10个生产者
for (int i = 0; i < 10; i++) {
new Thread(new producer(mainTest), "生产者"+i).start();
}
// 5个消费者
for (int i = 0; i < 5; i++) {
new Thread(new consumer(mainTest), "消费者"+i).start();
}
}
}
参考享学内容,如有错误,请指正。