前言
介绍多线程基本概念
1. 并行和并发
并行:多个CPU实例后者多台机器同时执行处理逻辑,是真正的同时
并发:通过CPU调度算法,让用户看上去同时执行,实际上从CPU层面不是真正的同时(操作系统将CPU时间片分配给每一个进程,给人并行处理的感觉)。
时间片是分时操作系统分配给每个正在运行的进程微观上的一段CPU时间(在抢占内核中是:从进程开始运行直到被抢占的时间)。
2. 进程和线程
-
进程
进程就是正在运行的程序,是指在系统中正在运行的一个应用程序,是系统资源分配和调用的基本单位,在内存中有其完备的数据空间和代码空间,拥有完整的虚拟空间地址。一个进程所拥有的数据和变量只属于它自己。对于操作系统来说,一个任务就是一个进程(Process)。 -
线程
线程是进程内相对独立的可执行单元,所以也被称为轻量进程(lightweight processes);是操作系统进行任务调度的基本单元。它与父进程的其它线程共享该进程所拥有的全部代码空间和全局变量,但拥有独立的堆栈(即局部变量对于线程来说是私有的)。 -
联系
一个程序至少有一个进程,一个进程至少拥有一个线程(主线程)。
一个线程必须有一个父进程。多个进程可以并发执行;一个线程可以创建和撤销另一个线程;同一个进程中的多个线程之间可以并发执行。 -
区别
进程和线程的主要差别在于它们是不同的操作系统资源管理方式。
进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响;
而线程只是一个进程中的不同执行路径。线程有自己的堆栈(就是栈)和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。 -
通信方式
进程间通信主要包括管道、系统IPC(包括消息队列,信号量,共享存储)、SOCKET,具体说明参考linux进程间通信方式。进程间通信其实是指分属于不同进程的线程之间的通讯,所以进程间的通信方法同样适用于线程间的通信。
但对应归于同一进程的不同线程来说,使用全局变量进行通信效率会更高,即使用共享内存模型来通信。在JVM规定多个线程进行通讯是通过共享变量进行的,而Java内存模型规定了有主内存是所有线程共享的,而各个线程又有自己的工作内存,线程只能访问自己的工作内存中数据。 -
意义
多进程的意义:提高CPU的使用率,可以同时执行多个任务
多线程的意义:提高应用程序的使用效率
3. 创建线程的方法
// 1. 创建类继承Thread类,复写run方法,调用start()开启线程
new Thread(){
@Override public void run(){
}
}.start();
//2. 创建类实现Runnable接口,复写run方法,创建它的实例对象并将它作为参数用来创建Thread实例后启动
new Thread(new Runnable(){
@Override public void run(){
}
}).start();
//3. 使用Callable
final ExecutorService threadPool = Executors.newFixedThreadPool(2);
Future<Integer> f = threadPool.submit(new Callable<Integer>() {
@Override
public Integer call() {
try {
Thread.sleep(5 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return 10;
}
});
System.out.println(f.get()); //一直等待任务结束再打印
4. 线程调度、优先级、相关方法
线程调度是指按照特定机制为多个线程分配CPU的使用权。
-
线程调度模型
- 分时调度模型:所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间片
- 抢占式调度模型:优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的CPU时间片相对多一些。
Java使用的抢占式调度模型。
-
线程名
public final String getName()
:获取线程名称
public final void setName(String name)
:设置线程名称
Thread.currentThread()
:获取当前线程 -
优先级
Java将优先级分为10个级别,MIN_PRIORITY为1,MAX_PRIORITY为10;NORM_PRIORITY为5。
public final int getPriority()
:获取线程优先级
public final void setPriority(int priority)
:设置线程优先级 -
Thread.sleep(mills)
:休眠 -
void join()
:别的线程等待该线程终止后开始执行
t1.start();
t1.join(); //当前调用线程进入等待状态,即等待t1执行完毕,main线程才会往下走,执行t2、t3
t2.start():
t3.start();
-
Thread.yield()
:礼让
暂停当前线程执行对象,等待其他线程。**再查下???**让多个线程执行的更和谐,并不能保证你一次我一次 -
void setDaemon()
:守护线程、后台线程,
在启动前调用。
当正在运行的线程都是守护线程时,JVM虚拟机退出。即主线程结束,其内的所有线程都是守护线程时,则退出。- JVM中存在两种线程:用户线程(User Thread)和守护线程(Daemon Thread)。
所谓守护线程,是指在程序运行的时候在后台提供一种通用服务的线程,比如垃圾回收线程就是一个很称职的守护者,并且这种线程并不属于程序中不可或缺的部分。
因 此,当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程。反过来说,只要任何非守护线程还在运行,程序就不会终止。
在守护线程中产生的线程也是守护线程。
- JVM中存在两种线程:用户线程(User Thread)和守护线程(Daemon Thread)。
-
void interrupt()
:中断线程。
通过调用一个线程的 interrupt() 来中断该线程,如果该线程处于阻塞、限期等待或者无限期等待状态,那么就会抛出 InterruptedException,从而提前结束该线程。但是不能中断 I/O 阻塞和 synchronized 锁阻塞。 -
isInterrupted()
:- 如果该线程已经处于终结状态,调用
Thread.currentThread().isInterrupted()
为false; - 如果该线程在等待、阻塞等状态中,抛出InterruptedException之后,此时调用调用
Thread.currentThread().isInterrupted()
也为false; - 如果一个线程的 run() 方法执行一个无限循环,并且没有执行 sleep() 等会抛出 InterruptedException 的操作,那么调用线程的 interrupt() 方法就无法使线程提前结束。但是调用 interrupt() 方法会设置线程的中断标记,此时调用 interrupted() 方法会返回 true。因此可以在循环体中使用 interrupted() 方法来判断线程是否处于中断状态,从而提前结束线程。
- 如果该线程已经处于终结状态,调用
-
void stop()
:结束线程,@Deprecated,不安全。
离开线程有两种常用的方法:- 抛出InterruptedException(在阻塞操作时抛出,比如sleep())
- 用Thread.interrupted()检查是否发生中断。详细请看后文连接。
-
线程组
默认情况下所有线程都属于main线程组。
ThreadGroup可以同一控制该组内所有线程
5. 线程生命周期
- Thread.State枚举类的状态:
-
New 新创建
使用new操作符创建的新线程,还没有调用start()方法,此时的状态是new。 -
Runnable 可运行的
调用start方法,线程处于runnable状态。
一个可运行的线程可能正在运行也可能没有运行,这取决于操作系统给线程提供运行的时间。
一旦一个线程开始运行,它不必始终保持运行。 -
Blocked 被阻塞、Waiting 等待、Timed Waiting 计时等待
当线程处于被阻塞或等待状态时,它暂时不活动,它不允许任何代码且消耗最少的资源,知道线程调度器重新激活它。
细节取决于它是怎样达到非活动状态的:- 当一个线程试图获取一个内部的内部锁(非java.util.concurrent中的锁),而该锁被其他线程持有,则该线程进入阻塞状态;当所有其他线程释放锁,并且线程调度器允许本线程持有它时,该线程将进入非阻塞状态。
- 当线程等待另一个线程通知调度器以一个条件时,它自己进入等待状态。在调用Object.wait方法或Thread.join方法时,或者是等待java.util.concurent库中的Lock或Condition时,就会出现这种情况。
与被阻塞状态有很大不同 - 有几个方法有一个超时参数,调用它们导致线程进入计时等待状态。这一状态一直保持到超时期满或者接收到适当的通知。
带有超时参数的方法有:Thread.sleep、Object.wait、Thread.join、Lock.tryLock、Condition.await;
-
Terminated 被终止
因run方法正常退出而自然终止
因为一个没有被捕获的异常而意外终止
-
6. 线程安全
-
线程安全概念
线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用;不会出现数据不一致或者数据污染。
线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。 -
出现线程安全问题的原因
- 是否是多线程环境
- 是否有共享数据
- 是否有多条语句操作共享数据
-
使用等待唤醒机制保证操作的原子性,解决多条语句操作共享数据时的不同步情况。多个线程共同使用一把锁。
同步的好处:解决了多线程的安全问题
同步弊端:效率低;如果出现了同步嵌套,容易产生死锁问题。
7. 等待唤醒机制
-
wait() 和 sleep()区别
wait()释放执行锁,sleep()不释放锁;
wait()需要被notify()、notifyAll()方法唤醒,否则一直等待;sleep()时间到后自动醒 -
等待唤醒的实现方式
- 使用同步关键字、wait()和notify()、notifyAll()等方法完成,这些都要在锁对象上调用,唤起在此监视器上的所有线程的相关方法。
- 使用Lock和Condition,显示锁和该锁上的监视器来完成
-
示例
不同线程对同一资源的操作,多生产者多消费者模型。
1. 使用synchronized
//同步代码块,object必须是同一个对象,即多个线程使用同一把锁
synchronized(object){
//需要同步的代码,代码执行完后该线程释放锁,重新抢CPU的执行权
//线程必须具有该锁和CPU的执行权的才能执行
}
synchronized(obj){ //同步代码块
}
//如果一个方法一进去就是被同步了,就可以使用同步方法。
synchronized void function(){//同步方法,同步方法使用的锁是this
}
static synchronized void function(){ //静态同步方法使用的锁是该类的.class对象。
synchronized(类名 .class){
}
2. 使用ReentrantLock
void function(){
lock.lock(); //加锁
try{
//...
}finally{
lock.unlock();//释放锁
}
}
3. 比较
-
锁的实现
synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的。 -
性能
新版本 Java 对 synchronized 进行了很多优化,锁主要存在四中状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,性能逐一递减。所以synchronized 与 ReentrantLock 大致相同。 -
等待可中断
当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。
ReentrantLock 可中断,而 synchronized 不行。 -
公平锁
公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。
synchronized 中的锁是非公平的,ReentrantLock 默认情况下也是非公平的,但是也可以是公平的。 -
锁绑定多个条件
一个 ReentrantLock 可以同时绑定多个 Condition 对象。
4. 死锁
指多个线程因抢夺资源而产生的互相等待的现象。
当两个以上的运算单元,双方都在等待对方停止运行,以获取系统资源,但是没有一方提前退出时,就称为死锁。
或所有线程都进入等待状态。
- 简单示例:
同步的嵌套容易造成死锁(两个线程、两个锁,线程互相持有对方锁);
多生成多消费代码中全部线程进入wait状态也是死锁的一个示例。=
参考: