0x01.线程的概述
- 并发:指两个或多个事件在同一个时间段内发生。
- 并行:指两个或多个事件在同一时刻发生(同时发生)。
- 进程:是指一个内存中运行的应用程序。
- 线程:线程是进程中的一个执行单元,负责当前进程中程序的执行。
- 一个程序运行后至少有一个进程,一个进程中可以包含多个线程 。
0x02.线程的调度
- 分时调度: 所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
- 抢占式调度: 优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个。
- Java使用的线程调度为为抢占式调度。
- CPU的一个核在任一时刻只能执行一个线程,CPU用抢占式调度模式在多个线程间进行着高速的切换,使得我们感觉像是在同时运行。
- 线程程序并不能提高程序的运行速度,但能够提高程序运行效率。
0x03.多线程
- 多线程指的是多个线程并发运行。
- Java使用
java.lang.Thread
类代表线程。 - 多线程执行时,在栈内存中,每一个执行线程都有一片自己所属的栈内存空间。
- 当执行线程的任务结束了,线程自动才在栈内存中释放。
- 当所有的执行线程都结束了,进程才结束。
- 在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。
启动多线程的方法:
- 创建一个类并继承Thread类。
- 重写该类的run()方法,run()方法称为线程执行体。
- 创建线程对象。
- 调用start()方法来启动该线程。
0x04.Thread类
常用构造方法:
public Thread()
:创建或分配一个新的线程对象。public Thread(String name)
:分配线程对象的同时为线程取一个名字。public Thread(Runnable target)
:分配一个带有指定目标新的线程对象。public Thread(Runnable target,String name)
:分配一个带有指定目标新的线程对象并取一个名字。
常用方法:
public void start()
:线程开始执行。(JVM调用run()
方法)public void run()
:线程需要执行的代码块。public static void sleep(long millis)
:暂停该线程指定的时间(毫秒)。public static Thread currentThread()
:获取当前正在执行的线程对象的引用。public String getName()
:获取线程名字。
0x05.Runnable接口
- 采用
java.lang.Runnable
事常见的一种创建线程的方法。 - 所有的多线程代码都是通过运行
Thread
的start()
方法来运行的。 Thread
线程负责执行其target
的run()
方法。
使用方法:
- 创建一个类并实现
Runnable
接口。 - 重写
run()
方法。 - 创建
Runnable
实现类的实例。 - 以此实例作为
Thread
的target
来创建Thread
对象。 - 调用
Thread
对象的start()
方法执行线程。
相比直接使用Thread的优点:
- 适合多个相同的程序代码的线程去共享同一个资源。一个类继承Thread,则不适合资源共享。如果实现了Runable接口,则很容易的实现资源共享。
- 避免单继承的局限性。
- 增加程序的健壮性。
- 可以放入线程池。
0x06.举例创建多线程
1.通过继承Thread类创建多线程:
创建类并继承Thread类:
public class Thread01 extends Thread {
public Thread01(String name){
super(name);
}
public void run(){
for(int i=0;i<10000;i++){
System.out.println("线程2正在执行");
}
}
}
创建多线程:
public class Main {
public static void main(String[] args) {
System.out.println("Main开始!!!");
Thread01 t01 = new Thread01("第一个线程");
t01.start();
for (int i = 0; i < 10000; i++) {
System.out.println("主线程正在执行");
}
}
}
2.通过实现Runnable接口:
创建类并实现Runnable接口:
public class Runnable01 implements Runnable {
@Override
public void run(){
for(int i=0;i<10000;i++){
System.out.println("线程2正在执行");
}
}
}
创建多线程:
public class Main {
public static void main(String[] args) {
System.out.println("Main开始!!!");
Runnable01 ru = new Runnable01();
Thread t01=new Thread(ru,"线程1");
t01.start();
for (int i = 0; i < 10000; i++) {
System.out.println("主线程正在执行");
}
}
}
3.通过匿名内部类创建多线程:
public class Main {
public static void main(String[] args) {
Runnable r =new Runnable(){
public void run(){
for(int i=0;i<10000;i++){
System.out.println("线程2正在执行");
}
}
};
new Thread(r).start();
for (int i = 0; i < 10000; i++) {
System.out.println("主线程正在执行");
}
}
}
0x07.线程安全
- 线程安全问题一般是由全局变量及静态变量引起的。
- 若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的。
- 若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。
线程同步机制(synchronized)
同步机制的三种方式:
- 同步代码块。
- 同步方法。
- 锁机制。
同步代码块:
synchronized
关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。- 同步锁: 可以理解为在对象上标记的一把锁.
- 锁的对象可以是任意类型。
- 多个线程对象 要使用同一把锁。
- 在任何时候,最多允许一个线程拥有同步锁,谁有锁就可以进入执行代码,其他的线程只能等待。
- 对于非
static
方法,同步锁就是this
。 - 对于
static
方法,使用当前方法所在类的字节码对象(类名.class)
作为同步锁。
格式:
synchronized(同步锁){
...//代码块
}
同步方法:
- 使用
synchronized
修饰的方法,叫做同步方法。 - 保证任何时候只有一个进程可以进入该方法。
格式:
public synchronized void method(){
...//代码块
}
Lock锁机制:
java.util.concurrent.locks.Lock
机制提供的比synchronized
代码块和synchronized
方法更广泛的锁定操作。- 同步代码块/同步方法具有的功能Lock都有。
Lock锁
也叫同步锁,加锁与释放锁的过程被方法化了。public void lock()
:加同步锁。public void unlock()
:释放同步锁。
代码简要实现线程同步机制:
同步代码块:
public class Runnable01 implements Runnable {
Object lock = new Object();
int num;//假设是某个具有特殊意义的数字
@Override
public void run(){
synchronized (lock) {
num++;
}
}
}
同步方法:
public class Runnable01 implements Runnable {
int num;//假设是某个具有特殊意义的数字
@Override
public void run(){
addnum();
}
public synchronized void addnum(){
num++;
}
}
Lock锁机制:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Runnable01 implements Runnable {
Lock lock = new ReentrantLock();
int num;//假设是某个具有特殊意义的数字
@Override
public void run(){
lock.lock();
num++;
lock.unlock();
}
}
0x08.线程所处状态
java.lang.Thread.State
这个枚举中给出了线程所处的六种状态。
线程所处状态 | 状态说明 |
---|---|
NEW (新建状态) |
线程刚被创建,但是还没有启动,没有调用start 方法。 |
Runnable (可运行状态) |
表示线程可以在JVM中运行,可能正在运行自己代码,也可能没有。 |
Blocked (锁阻塞状态) |
当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked 状态;当该线程持有锁时,该线程将变成Runnable 状态。 |
Waiting (无限等待状态) |
一个线程在等待另一个线程执行一个唤醒时,该线程进入Waiting 状态。进入这个状态后是不能自动唤醒,必须等待另一个线程调用notify 或者notifyAll 方法才能够唤醒。 |
TimedWaiting (计时等待状态) |
同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting 状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、 Object.wait 。 |
Teminated (被终止状态) |
可能因为run 方法正常退出而死亡,也可能因为没有捕获的异常终止了run 方法而死亡。 |
0x09.等待唤醒机制
- 等待唤醒机制是多个线程间的一种协作机制。
- 一个线程进行了规定操作后,就进入等待状态(
wait()
)。 - 待其他线程执行完他们的指定代码过后再将其唤醒(
notify()
)。 otifyAll()
用来唤醒所有的等待线程。- 一个进程如果能获取锁,线程就会从
WAITING
状态变成RUNNABL
E 状态; - 一个进程如果不能获取锁,从
wait set
出来,又进入entry set
,线程就从WAITING
状态又变成BLOCKED
状态。 wait
方法与notify
方法必须要由同一个锁对象调用。wait
方法与notify
方法属于Object
类的方法。wait
方法与notify
方法必须要在同步代码块或者是同步方法中使用。
0x10.线程池
- 线程池: 是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。
- 线程池的顶级接口是
java.util.concurrent.Executor
。 Executor
是一个执行线程的工具。java.util.concurrent.ExecutorService
是线程池接口。
使用线程池的优点:
- 提高响应速度。
- 降低资源消耗。
- 提高线程的可管理性。
创建并使用线程池:
- 创建线程池对象。
- 创建
Runnable
接口子类对象。 - 提交
Runnable
接口子类对象。 - 关闭线程池。
常使用的创建方法: public static ExecutorService newFixedThreadPool(int nThreads)
:返回线程池对象。(创建有界线程池,池中的线程个数可以指定最大数量)public Future<?> submit(Runnable task)
:获取线程池中的某一个线程对象,并执行。Future
接口用来记录线程任务执行完毕后产生的结果。
代码举例:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Runnable01 implements Runnable {
Lock lock = new ReentrantLock();
int num;//假设是某个具有特殊意义的数字
@Override
public void run(){
lock.lock();
num++;
lock.unlock();
}
}
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Main {
public static void main(String[] args) {
// 创建线程池对象,包含5个线程对象
ExecutorService service = Executors.newFixedThreadPool(5);
Runnable01 r = new Runnable01(); // 创建Runnable实例对象
// 从线程池中获取线程对象,然后调用Runnable01中的run()
service.submit(r);
service.submit(r);//继续获取
service.shutdown();//关闭线程池
}
}