引入
先理清几个易混淆的概念:易混淆:并发、并行、互斥、同步、异步,然后开始今天的学习吧~
文中如果有不对的地方欢迎各位小伙伴们指出,我会及时进行更正!!
进程、多线程
目前的CPU频率已经无法再有更大的提高空间,所以提高程序性能主要依靠多核和并行程序。而要了解 Java 的多线程,我们还得先来说一说多进程~
需要指出的是,在引入线程之后,线程是独立调度的基本单位,进程是拥有资源的基本单位。不仅进程之间可以并发执行,线程之间也可以并发执行,这使得操作系统具有更好的并发性。
多进程
我们都知道,操作系统(OS)会将时间划分为多个时间很短的时间片,在每个时间片内将CPU分配给某一个任务,当时间片结束,CPU将被自动回收,分配给另外的任务。
在CPU上,任务是按照串行依次运行(单核CPU),如果是多核,多个任务进程可以并行。
多进程的优点
- 可以同时运行多个任务;
- 程序因 IO 堵塞,可以释放CPU,让CPU为其它程序服务;
- 当系统有多个CPU,可以为多个程序同时服务
多进程的缺点
- 进程切换带来的系统开销代价大,降低了整体性能;(假设一个进程需要频繁使用IO,那么在阻塞的过程中,就需要来回切换进程)
- 进程之间的通信的实现也比较复杂,不好管理;
- 进程的创建和撤销都需要一定的时间代价
多线程
为了解决多进程带来的缺点,所以引进了多线程处理技术。我们可以通俗的类比于一个main()函数里面有很多内容,为此,我们在编写程序的时候,可以把它分为很多模块,一个模块就是一个函数。
这里是一样的道理,一个程序可以包括多个子任务,多个子任务可串/并行,每个子任务称之为一个线程!
如果一个子任务阻塞,CPU将会去调度另一个子任务进行工作,但CPU仍保留在本程序(进程)中,而不是被调度到别的程序(进程)去,这样就可以减少进程之间的切换从而大大提高本程序所获得CPU时间和利用率!
多进程和多线程对比
从以下几个方面来看:
多线程通讯更加高效,可以通过共享内存、消息队列等方式实现,而多进程间通信较为困难,需要采用IPC(进程间通信)技术;
多线程内存开销更小,因为多线程用的是统一进程的内存空间,而多进程必须为每个进程分配内存空间;
多线程的创建和销毁更迅速,因为线程创建只需要拷贝一份主程序的执行上下文,销毁时只需要回收相应资源即可,不需要操作系统介入,而多进程需要创建一个新的进程,销毁也需要释放多个进程所占用的内存资源 ,需要操作系统介入,增加了系统开销;
多线程在访问共享资源时需要进行锁定,避免多个线程同时修改同一份数据,而多进程可以通过消息队列等方式进行同步,但是需要注意进程间的数据一致性问题。
多线程实现
多线程的创建
在 Java 中进行多线程的创建,只有以下两种方式:
// 继承 Thread 类
public class Thread1 extends Thread{
public void run()
{
Sytem.out.printlin("hello");
}
}
// 实现 Runnable 接口
public class Thread2 implements Runnable{
public void run()
{
System.out.println("hello");
}
}
注:Java 的四个主要接口有:
- Clonable :用于对象克隆 ;
- Comparable:用于对象比较;
- Serializable:用于对象序列化;
- Runnable:用于对象线程化。
两种创建方式的比较
注:Thread里面,必须用 static 定义变量 ,才能实现变量共享
多线程的启动
上述说的两种创建都属于run方法,且Java里规定, 调动start就会触发run方法,它的底层是用JNI(Java Native Interface)【这个我会在JVM的学习中谈到 】, 提供了若干个API,可以使得Java程序调用C/C++程序!(Java 就是用 C/C++写出来的~~)
main 函数只是叫做主线程,其他新的派生出来的线程叫子线程,主线程终止了子线程可能还在,一定是得等到所有的线程都结束了,整个程序才算终止
// Thread
public class Thread1 extends Thread {
public void run() {
System.out.println("hello");
}
public static void main(String[] a) {
(new Thread1()).start();
}
}
// Runnable
public class Thread2 implements Runnable {
public void run() {
System.out.println("hello");
}
//实现 Runnable 的类必须要依靠 Thread 才可以启动
//不能直接对 Runnable 的对象进行 start 方法
public static void main(String[] a) {
(new Thread(new Thread2())).start();
}
}
多线程信息共享
在介绍多线程信息共享之前先说两个基础概念——粗粒度和细粒度。
- 粗粒度:子线程和子线程之间 和 main 线程之间缺乏交流
- 细粒度:线程之间有信息交流通讯(即同步协作)
那怎样才能让线程之间可以进行信息交流呢?
首先得要有可以共享的信息!因为 Java 的 JDK 库中暂时是不支持点对点的发送消息的(即线程与线程之间直接通信)【这个在 C/C++ 里面有一个并行库 MPI 可以直接做到!】
信息共享的方法
在 Java 中,达到信息共享的也有两种办法:
- 通过共享变量(static)实现信息共享。static 变量是这个类的所有对象都共享的一个变量
- 同一个Runnable类的成员变量。这样的共享变量在多个线程中实际上就是一个拷贝对象了。
下面是两个分别使用两种创建方式的具体的例子:
//会出现信息不一致的情况
public class ThreadDemo0 {
public static void main(String[] args) {
(new TestThread0()).start();
(new TestThread0()).start();
(new TestThread0()).start();
(new TestThread0()).start();
}
}
class TestThread0 extends Thread {
private int tickets = 100; //每个线程卖100张,没有共享
// private static int tickets = 100; // static 变量是共享的,所有的线程共享
public void run() {
while(true){
if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + " is selling ticket " + tickets);
--tickets;
} else {
break;
}
}
}
}
注:如果一个类是通过继承Thread 类,那么它的信息共享只能通过static变量
//仍然会出现信息不一致的问题
public class ThreadDemo1 {
public static void main(String[] args) {
TestThread1 t = new TestThread1();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
}
}
class TestThread1 implements Runnable {
private int tickets = 100;
public void run() {
while(true){
if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + " is selling ticket " + tickets);
--tickets;
} else {
break;
}
}
}
}
在上述例子中,TestThread1 只被创建一次,就是 t ,new Thread ( t ) 并没有创建新对象,只是将 t 包装成线程对象,然后启动 ,所以使用的是同一个TestThread1对象。
Java的内存模型
在前面的信息共享中,我们已经实现了多线程,但存在线程之间得到数据不一致的问题,原因有两个,一是由于每个线程都有自己的工作缓存副本(见上图—Java 的内存模型),二是由于关键步骤缺乏加锁限制。
- 工作缓存副本:线程运行时,会先从内存里面加载完数据,放到它自己的工作缓存里面,然后开始运算,运算完以后会将数据先写入到自己的线程缓存中,最后再更新到内存中。假设有一个线程修改了自己工作缓存的值,但是还没有传递到其他线程中去,这就会导致数据更新的不及时。
- 关键步骤缺乏加锁限制:关键步骤就是对数据进行更新的步骤,在上述例子中,tickets--就是一个关键步骤,当对同一个变量做修改操作的时候,我们需要对其进行加锁限制。
数据不一致问题解决
针对变量副本带来的问题,我们采用 volatile 关键字来修饰变量,从而保证不同线程对共享变量操作时的可见性。
public class ThreadDemo2 {
public static void main(String[] args) throws Exception {
TestThread2 t = new TestThread2();
t.start();
Thread.sleep(2000);
t.flag = false;
System.out.println("main thread is exiting");
}
}
class TestThread2 extends Thread {
boolean flag = true; //子线程不会停止
//volatile boolean flag = true; //用volatile 修饰的变量可以及时的在各线程里面通知
public void run() {
for(int i = 0; this.flag; ++i) {}
System.out.println("test thread3 is exiting");
}
}
那对于关键步骤缺乏加锁限制呢,我们可以编写代码实现线程互斥,也可以使用关键字 synchronized 来进行加锁,被它修饰的代码块/函数,一次只能允许一个线程进入,虽然使用简便,但是也加大了性能负担!
public class ThreadDemo3 {
public static void main(String[] args) {
TestThread3 t = new TestThread3();
(new Thread(t, "Thread-0")).start();
(new Thread(t, "Thread-1")).start();
(new Thread(t, "Thread-2")).start();
(new Thread(t, "Thread-3")).start();
}
}
class TestThread3 implements Runnable {
private volatile int tickets = 100; //多个线程共享的
String str = new String("");
public void run() {
do {
this.sale();
try {
Thread.sleep(100);
} catch (Exception var2) {
System.out.println(var2.getMessage());
}
} while(this.tickets > 0);
}
//同步代码块,一次只能一个线程进来
public synchronized void sale() {
if (this.tickets > 0) {
System.out.println(Thread.currentThread().getName() + " is saling ticket " + this.tickets--);
}
}
}
多线程管理
线程状态及其关系
要对多线程进行管理,还得知道线程有哪些状态。首先,线程的五个状态如下:(有的地方也会谈到七状态,包含了就绪挂起状态和阻塞挂起状态)
- NEW 刚创建(new)
- RUNNABLE 就绪态 (start)
- RUNNING 运行中(run)
- BLOCK 阻塞(sleep)
- TERMINATED 结束
它们之间的转换关系如图所示,注意,就绪是所有的资源都已备好,只等调度;等待(阻塞)是还在等待某项资源!
阻塞/唤醒的API
线程的创建、运行和终止的代码我们在前面已经讲述过,现在该开始学习线程的阻塞和唤醒的API~,早在前面我们也已经见过一个sleep函数了。
- sleep,线程自我休眠,时间一到,自己就会醒来(阻塞态和就绪态之间的转换)
- wai / notify / notifyAll,被动等待,需要别人来进行唤醒
- join,等待另一个线程结束
- interrupt,向另一个线程发送中断信号,该线程收到信号,会触发InterruptedException(可解除阻塞),并进行下一步处理。
这里需要注意的是,线程被动的暂停和终止,是依靠别的线程来拯救自己,并不会及时的释放资源,为了避免资源得不到立即释放,所以我们一个让线程主动暂停和终止!
具体的我们可以:
- 定期监测共享变量,如设置一个flag;
- 如果需要暂停或终止,先释放资源,再主动动作
【暂停:Thread.sleep(),休眠;终止:run方法结束,线程终止】
多线程死锁
多线程的死锁是指每个线程互相持有别人需要的锁(如哲学家吃面),这里简单写一个例子如下:
public class ThreadDemo5 {
public static Integer r1 = 1;
public static Integer r2 = 2;
public static void main(String[] args) throws InterruptedException {
TestThread51 t1 = new TestThread51();
t1.start();
TestThread52 t2 = new TestThread52();
t2.start();
}
}
class TestThread51 extends Thread {
public void run() {
//索要r1
synchronized(ThreadDemo5.r1) {
try {
TimeUnit.SECONDS.sleep(3);
//TimeUnit 是JDK 5引入的新类,提供了时间单位粒度和一些时间转换、计时和延迟等函数
} catch (InterruptedException var4) {
var4.printStackTrace();
}
//索要r2
synchronized(ThreadDemo5.r2) {
System.out.println("TestThread51 is running");
}
}
}
}
class TestThread52 extends Thread {
public void run() {
//索要r2
synchronized(ThreadDemo5.r2) {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException var4) {
var4.printStackTrace();
}
//索要r1
synchronized(ThreadDemo5.r1) {
System.out.println("TestThread52 is running");
}
}
}
}
可以看出它们彼此都拿到了对方的资源,从而导致线程阻塞。要想预防死锁,我们可以对资源进行等级排序 ,即把例子中的 TestThread52 也统一规定为先拿 r1,再拿 r2 。
守护(后台)线程
通过前面的学习,我们已经知道普通线程的结束,是run方法运行结束,而守护线程的结束,是run方法运行结束,或者main函数结束。你可以理解为守护线程的run方法要么先于main函数结束,要么main函数结束,整个程序结束。还是通过一个例子来加以说明:
public class ThreadDemo4 {
public static void main(String[] args) throws InterruptedException {
TestThread4 t = new TestThread4();
t.setDaemon(true); //定义守护线程(后台线程)
t.start();
Thread.sleep(2000);
System.out.println("main thread is exiting");
}
}
class TestThread4 extends Thread {
public void run() {
while(true) {
System.out.println("TestThread4 is running");
try {
Thread.sleep(1000);
} catch (InterruptedException var2) {
var2.printStackTrace();
}
}
}
}
注意:守护线程永远不要访问资源,如文件或者数据库;因为main函数结束的时候,它可能来不及释放资源!
接下来是高并发部分,篇幅原因,咱们明天再来打卡!