一,概念
进程:正在运行的程序
线程:是进程中的一个执行单元(一条执行路径),一个进程至少包含一条线程。如果一个进程包含多个线程,这种程序就叫多线程程序
并发:两件事同时进行
并行:两件事同时发生
线程的调度:
- 分时调度:所有线程轮流使用CPU,每个线程平均分配CPU的执行权
- 抢占式调度:优先让优先级高的线程执行,优先级相同时,CPU随机分配执行权,Java 中多线程的执行方式就是抢占式的
注:Java 程序在没有额外开启线程的情况下也有两个线程:主函数所在的主线程、垃圾回收线程
二,创建线程
2.1 继承 Thread 类
步骤:
- 定义类继承 Thread 类
- 重写 run() 方法,在 run() 方法中的功能就是线程要执行的功能
- 创建 Thread 的子类对象
- 调用 start() 方法开启线程,Java 虚拟机自动调用该线程的 run() 方法。
注:同一个线程对象不能重复调用 start(),否则会发生 IllegalThreadStateException
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 public class Test {
public static void main(String[] args) {
MyThread mt = new MyThread();
mt.start();
MyThread mt2 = new MyThread();
mt2.start();
}
}
class MyThread extends Thread{
public void run() {
for(int i = 1;i<=100;i++){
System.out.println(Thread.currentThread().getName()+"-----"+i);
}
}
}
2.2 实现 Runnable 接口
步骤:
- 定义类实现 Runnable 接口
- 重写 run() 方法,在 run() 方法中的功能就是线程要执行的功能
- 创建该实现类的对象
- 创建 Thread 对象,并将实现类的对象作为参数传递给 Thread 的构造函数
- 调用 start() 方法开启线程,Java 虚拟机自动调用该线程的 run() 方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 public class Test2 {
public static void main(String[] args) {
MyRunnable mr = new MyRunnable();
Thread t = new Thread(mr);
t.start();
Thread t2 = new Thread(mr);
t2.start();
}
}
class MyRunnable implements Runnable{
public void run() {
for(int i = 1;i<=100;i++){
System.out.println(Thread.currentThread().getName()+"-----"+i);
}
}
}
注:建议优先选用实现 Runnable 的方式,因为避免了单继承的局限性
2.3 使用匿名内部类创建线程并开启
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 new Thread(){
public void run() {
for(int i = 1;i<=100;i++){
System.out.println(Thread.currentThread().getName()+"-----"+i);
}
}
}.start();
new Thread(new Runnable(){
public void run() {
for(int i = 1;i<=100;i++){
System.out.println(Thread.currentThread().getName()+"-----"+i);
}
}
}).start();
三,线程安全问题
什么是线程安全:如果多个线程并发执行,这些线程操作共享数据,运行后的结果与单线程运行后的结果是相同的,就称为线程是安全的。
以下代码出现了线程安全问题:限制了ticket > 0的条件,但是最终输出的ticket出现的0和负数,因为某条线程在执行的过程中被其他线程抢夺了CPU的执行权。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 class MyThread extends Thread{
static int ticket = 100;
public void run() {
for(;;){
if(ticket > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"正在卖第"+ (ticket)+"号票");
ticket--;
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 class MyRunnable implements Runnable{
int ticket = 100;
public void run() {
for(;;){
if(ticket > 0){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"正在卖第"+ (ticket)+"号票");
ticket--;
}
}
}
}
四,同步技术
关键字:
synchronized
4.1 同步代码块
格式:
1
2
3 synchronized(锁对象){
// 可能发生线程安全问题的代码
}注:
- 同步代码块中的锁对象(同步锁)可以是任意类型的
- 必须保证多个线程使用的锁对象是同一个
- 锁对象的作用:将同步代码块锁定,同一时间只允许让一个线程进入同步代码块,只要有一个线程抢夺到了CPU的执行权,只有当该线程执行完毕后,其他线程才能继续抢夺CPU的执行权,否则就处于等待状态。
4.2 同步函数
格式:
1
2
3 public synchronized 返回类型 方法名(参数列表){
// 可能发生线程安全问题的代码
}注:
同步函数中有锁对象吗?有,同步函数中的锁对象是 this
如果使用的是继承 Thread 的方式创建线程,同步函数必须是静态的
静态同步函数的锁对象是谁?锁是
类名.class
二进制字节码文件对象
4.3 Lock 锁
概念:Lock 是一个接口,在 JDK1.5 出现,Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作。此实现允许更灵活的结构。
方法:
- lock():获取锁
- unlock():释放锁
步骤:
- 在成员位置创建 Lock 接口的实现类对象 ReentrantLock
- 在可能发生线程安全问题的代码前调用 lock()
- 在可能发生线程安全问题的代码后调用 unlock()
同步技术的原理:
同步技术中使用到的锁对象,这个锁对象也称为同步锁
多个线程一起抢夺CPU的执行权
获取到锁对象的线程,进入同步,当这个线程执行完同步中的代码后,释放锁;
没有获取到锁对象的线程,会处于阻塞状态,等待同步中的线程执行完毕后释放锁;
使用同步技术会影响程序的执行效率:
判断锁、获取锁、释放锁
五,线程池
概念:线程池的本质就是一个容器,在该容器中存放着若干线程对象,当有任务需要使用线程对象时,直接从池中获取线程对象,当任务执行完毕后,将使用完的线程对象归还到池中,从而提高了线程对象的复用性,减少了线程对象的创建。
线程池的好处:
- 减低了资源的消耗。减少了创建和销毁线程的次数,每个线程对象都可以被复用
- 提高了响应速度
- 提高了对线程的管理
核心类:Executors,线程池的工厂类,用于生产线程池
Executors 类中提供了生产线程池的静态方法 newFixedThreadPool(int nThreads),创建一个有固定数量线程对象的线程池,返回 ExecutorService 接口的实现类。
ExecutorService 接口中提供获取线程对象的功能 submit(Runnable r),调用此功能会开启线程并执行 Runnable 实现类中的 run() 方法
1
2
3
4
5
6
7
8
9 MyRunnable1 mr1 = new MyRunnable1();
MyRunnable2 mr2 = new MyRunnable2();
MyRunnable3 mr3 = new MyRunnable3();
ExecutorService service = Executors.newFixedThreadPool(2);
service.submit(mr1);
service.submit(mr2);
service.submit(mr3);
service.shutdown();// 关闭线程池注:
- 如果要开启线程执行的任务超过池中线程对象的数量,没有执行的任务会等待其他任务执行完毕归还线程对象再执行它的任务
- 即使线程对象已经全部归还,线程池仍然处于开启状态,因为线程池在等待可能还有其他任务需要使用池中对象,我们可以手动调用 shutdown() 来强制关闭池,一旦池被关闭了,再从池中获取连接对象就会发生 RejectedExecutionException。