进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个即是一个进程从创建、运行到消亡的过程。
在Java中,当我们启动 main 函数时,其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程。
如下图所示,在 windows 中通过查看任务管理器的方式,我们就可以清楚看到 window 当前运行的进程(.exe 文件的运行)
线程比进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间切换工作时,负担要比进程小的多,也正因如此,线程也被称作轻量级进程。
Java 天生就是多线程程序,我们可以通过 JMX 来看一下一个普通的 Java 程序有哪些线程。
代码如下:
package multiThread;
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
/**
* @author silentCow
* @Date 2020/9/5 10:33
*/
public class MultiThread {
public static void main(String[] args) {
// 获取 Java 线程管理 MXBean
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
// 不需要获取同步的 monitor 和 synchronizer 信息,仅获取线程和线程堆栈信息
ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
// 遍历线程信息,仅打印线程ID 和 线程名称信息
for (ThreadInfo threadInfo : threadInfos) {
System.out.println("["+threadInfo.getThreadId() +"] "+threadInfo.getThreadName());
}
}
}
输出结果:(输出内容可能不同,不用太纠结下面每个线程的作用,只用知道 main 线程执行main 方法即可)
[6] Monitor Ctrl-Break
[5] Attach Listener //添加事件
[4] Signal Dispatcher // 分发处理给 JVM 信号的线程
[3] Finalizer //调用对象 finalize 方法的线程
[2] Reference Handler //清除 reference 线程
[1] main //main 线程,程序入口
综上可以看出:一个 Java
程序的运行是 main
线程和多个其他线程同时运行。
从 JVM 角度说进程和线程之间的关系
详细内容请阅读:Java相关/可能是把Java内存区域讲的最清楚的一篇文章
从上图可以看出:一个进程可以有多个线程,多个线程共享进程的堆和方法区(JDK 1.8 之后的元空间)资源,但是每个线程有 自己的程序计数器、虚拟机栈 和 本地方法栈。
总结:线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正好相反。
扩展内容: 思考:为什么程序计数器、虚拟机栈和本地方法栈是线程私有的呢?为什么堆和方法区是线程共享的呢?
程序计数器主要有两个作用:
需要注意的是,如果执行的是 native 方法,那么程序计数器记录的是 undefined 地址,只有执行的是 Java 代码时程序计数器记录的才是下一条指令的地址。
所以,程序计数器私有主要是为了:线程切换后能够恢复到正确的执行位置。
所以,为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的。
堆和方法区都是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象(所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
先从总体上来说:
再深入到计算机底层来探讨:
并发编程的目的就是为了能提高程序的执行效率提高程序运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,比如:内存泄漏、上下文切换、死锁还有受限于硬件和软件的资源闲置问题。
点击跳转【Java面试】Java基础(下篇)/#30-线程有哪些基本状态?线程有哪些基本状态
多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。
概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。
上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。
Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。
线程死锁描述的是:多个线程同时被阻塞,它们中的一个或全部都在等待某个资源被释放。由于线程被无期限的阻塞,因此程序不可能正常终止。
如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。
下面通过一个例子来说明线程死锁,代码模拟了上图的死锁的情况 (代码来源于《并发编程之美》):
package deadLock;
/**
* @author silentCow
* @Date 2020/9/6 14:59
*/
public class DeadLockDemo {
private static Object resource1 = new Object();// 资源1
private static Object resource2 = new Object();// 资源2
public static void main(String[] args) {
new Thread(() -> {
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource2");
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
}
}
}, "线程 1").start();
new Thread(() -> {
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource1");
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
}
}
}, "线程 2").start();
}
}
输出:
Thread[线程 1,5,main]get resource1
Thread[线程 2,5,main]get resource2
Thread[线程 2,5,main]waiting get resource1
Thread[线程 1,5,main]waiting get resource2
线程 A 通过 synchronized (resource1)
获得 resource1
的监视器锁,然后通过Thread.sleep(1000)
;让线程 A 休眠 1s
为的是让线程 B 得到执行然后获取到 resource2
的监视器锁。线程 A 和线程 B 休眠结束了都开始企图请求获取对方的资源,然后这两个线程就会陷入互相等待的状态,这也就产生了死锁。上面的例子符合产生死锁的四个必要条件。
产生死锁必须具备的条件:
上面说了产生死锁的必备条件,那么为了避免死锁,只需破坏其中的一个条件即可:
线程 2 的代码修改成下面这样就不会产生死锁了
new Thread(() -> {
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource2");
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
}
}
}, "线程 2").start();
输出:
Thread[线程 1,5,main]get resource1
Thread[线程 1,5,main]waiting get resource2
Thread[线程 1,5,main]get resource2
Thread[线程 2,5,main]get resource1
Thread[线程 2,5,main]waiting get resource2
Thread[线程 2,5,main]get resource2
分析上面的代码为什么避免了死锁?
线程 1 首先获得到 resource1
的监视器锁,这时候线程 2 就获取不到了。然后线程 1 再去获取resource2
的监视器锁,可以获取到。然后线程 1 释放了对 resource1
、resource2
的监视器锁的占用,线程 2 获取到就可以执行了。这样就破坏了破坏循环等待条件,因此避免了死锁。
sleep
方法没有释放锁,而 wait
方法释放了锁wait
通常被用于线程间交互/通信,sleep
通常被用于暂停执行wait()
方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify()
或者notifyAll()
方法。sleep()
方法执行完成后,线程会自动苏醒。或者可以使用 wait(long timeout)
超时后线程会自动苏醒new
一个 Thread
,线程进入了新建状态;调用 start()
方法,会启动一个线程并使线程进入就绪状态,当分配到时间片后就可以运行了。start()
会执行线程 的响应准备工作,然后自动执行run()
方法的内容,这是真正的多线程工作。而直接执行 run()
方法,会把run()
方法当成一个 main
线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
总结: 调用 start 方法可启动线程并使线程进入就绪状态;而 run 方法只是 Thread 的一个普通方法,还是在主线程里执行。
synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
另外,在 Java 早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较⻓的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对synchronized较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
暂无,后期写
总结:synchronized
关键字加到 static
静态方法和 synchronized(class)
代码块上都是是给 Class
类上锁。synchronized
关键字加到实例方法上是给对象实例上锁。尽量不要使用synchronized(String a)
因为JVM
中,字符串常量池具有缓存功能!
暂 略
JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。
锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。
①、两者都是可重入锁
“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。
②、synchronized 依赖于 JVM ,而 ReentrantLock 依赖于 API
synchronized
是依赖于 JVM
实现的,前面我们也讲到了虚拟机团队在 JDK1.6
为 synchronized
关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReentrantLock
是JDK
层面实现的(也就是 API
层面,需要lock()
和unlock()
方法配合try/finally
语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。
③、ReentrantLock 比 synchronized 增加了一些高级功能
相比synchronized
,ReentrantLock
增加了一些高级功能。主要来说主要有三点:①等待可中断;②可实现公平锁;③可实现选择性通知(锁可以绑定多个条件)
lock.lockInterruptibly()
来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。ReentrantLock
可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。 ReentrantLock
默认情况是非公平的,可以通过 ReentrantLock
类的ReentrantLock(boolean fair)
构造方法来制定是否是公平的。synchronized
关键字与wait()
和notify()/notifyAll()
方法相结合可以实现等待/通知机制,ReentrantLock
类当然也可以实现,但是需要借助于Condition
接口与newCondition()
方法。Condition
是JDK1.5
之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个Lock
对象中可以创建多个Condition
实例(即对象监视器),线程对象可以注册在指定的Condition
中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。在使用notify()/notifyAll()
方法进行通知时,被通知的线程是由 JVM
选择的,用ReentrantLock
类结合Condition
实例可以实现“选择性通知”,这个功能非常重要,而且是Condition
接口默认提供的。而synchronized
关键字就相当于整个Lock
对象中只有一个Condition
实例,所有的线程都注册在它一个身上。如果执行notifyAll()
方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition
实例的signalAll()
方法只会唤醒注册在该Condition
实例中的所有等待线程。④、性能已不是选择标准
暂略
暂略
池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率
线程池提供了一种限制和管理资源(包括执行一个任务)。每个线程池还维护一些基本统计信息,例如已完成任务的数量。
使用线程池的好处:
Runnable
自Java 1.0
以来一直存在,但Callable
仅在Java 1.5
中引入,目的就是为了来处理Runnable
不支持的用例。Runnable
接口不会返回结果或抛出检查异常,但是Callable
接口可以。所以,如果任务不需要返回结果或抛出异常推荐使用Runnable
接口,这样代码看起来会更加简洁。
工具类``Executors可以实现
Runnable对象和
Callable对象之间的相互转换。(
Executors.callable(Runnable task)或
Executors.callable(Runnable task,Object resule)`)。
execute()
方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;submit()
方法用于提交需要返回值的任务。线程池会返回一个Future
类型的对象,通过这个Future
对象可以判断任务是否执行成功,并且可以通过Future
的get()
方法来获取返回值,get()
方法会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit)
方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。