16 线程(基础)
1. 相关概念
进程: 进程是程序的一次执行过程,或是正在运行的一个程序。是多态过程,有它自身的产生、存在和消亡的过程。(比如我们使用QQ,就启动了一个进程,操作系统就会为该进程分配内存看见。当我们使用迅雷,又启动了一个进程,操作系统将为迅雷分配新的内存空间。进程一旦中断,空间将会被释放。)
线程:线程由进程创建的,是进程的一个实体。一个进程可以拥有多个线程。(比如一个QQ进程,可以同时打开多个聊天窗口,一个迅雷进程,可以同时下载多个文件。)
单线程:同一个时刻,只允许执行一个线程。
多线程:同一个时刻,可以执行多个线程。
并发:同一个时段,多个任务交替执行,造成一种“貌似同时”的错觉,简单的说,单核cpu实现的多任务就是并发。
并行:同一个时刻,多个任务同时执行。多核cpu可以实现并行。
2. 线程基本使用
2.1 创建线程的两种方式
- 2.1.1 继承 Thread 类,重写 run 方法
public static void main(String[] args) throws InterruptedException
{
// 创建 Cat 对象,Cat 已继承 Thread,实现 run 方法。可以当作线程使用
Cat cat = new Cat();
cat.start(); // 启动线程,最终会直接执行 Cat 的 run 方法
for (int i = 0; i < 10; i++) {
System.out.println("主线程 i = "+ i);
}
Thread.sleep(1000); // 让主线程也间断休眠
}
// 运行程序时,开启一个进程
// 执行 main 方法,此时进程会开启一个主线程(main 线程)
// 执行 Cat.start(),此时 main 线程会再开一个 子线程,Thread-0
// 主线程,不会阻塞,会继续执行fori。主线程和子线程在交替执行...
// 可以在终端输入jconsole监控线程执行情况。
Q:为什么是 start() 方法,不直接调 run()?
因为run方法是一个普通的方法,如果直接调,会作为一个普通方法串行来执行,并未真正启动一个线程。
此时,当run方法执行完毕,才会继续执行后面的语句。
源码分析
start() 方法里,调用 start0() 方法。
start0() 是本地方法,由 JVM 调用的。具体如何执行,取决于 CPU,也与操作系统、内存资源、IO资源等有关。
真正实现多线程的效果,是 start0(),而不是 run()。可以理解为,在 start0() 里面,用多线程的机制来调用 run 方法。
- 2.1.2 实现 Runnable 接口,重写 run 方法
Java 是单继承的,在某些情况下一个类可能以及继承了某个父类,这时再用继承 Thread 类方法来创建线程显然不可能了。
此时可通过另外一个方式创建线程,就是通过实现 Runnable 接口来创建线程。
public static void main(String[] args) throws InterruptedException
{
Dog dog = new Dog();// Dog 实现了 Runnable 接口,并重写了 run 方法。
// dog.start(); // 这里无法调用start编译报错。更不能改用run原因同上。
// 应当 创建一个 Thread 对象,把 dog 带进去。这里底层使用了设计模式 [代理模式]。
Thread t1 = new Thread(dog);
t1.start();
}
- 2.1.3 两种方式的区别
-
从 Java 的设计来看,通过继承 Thread 或者实现 Runnable 接口来创建线程本质上没有区别,因为二者在底层都是通过调用 start0() 来实现的。
-
实现 Runnable 接口方式更加适合多个线程共享一个资源的情况,并且避免了单继承的限制。
2.2 多线程执行
public static void main(String[] args) throws InterruptedException
{
Cat cat = new Cat();
cat.start(); // 启动第一个子线程
Dog dog = new Dog();
Thread t1 = new Thread(dog);
t1.start(); // 启动第二个子线程
for (int i = 0; i < 30; i++) // 主线程
{
System.out.println("main~~~~");
}
}
class Dog implements Runnable
{
@Override
public void run() {
int t = 0;
while (t < 50)
{
System.out.println("wo~~~~");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
class Cat extends Thread
{
@Override
public void run()
{
int time = 0;
while (time < 20)
{
System.out.println("miao~~~~");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
time += 1;
}
}
}
3. 线程终止
- 基本说明:
-
当线程完成任务后,会自动退出。
-
还可以通过使用变量来控制 run 方法退出的方式停止线程,即通知方式。
比如,子线程的run方法里是一个循环方法,可在子线程里设置loop变量控制循环,并设置setLoop方法控制loop的值。那么,在main线程里,只需要通过调用setLoop方法即可控制子线程退出run方法。
- 线程常用方法
- 注意事项和细节
- 线程状态
4. 线程的同步
- 定义:
-
在多线程编程里,一些敏感数据不允许被多个线程同时访问,此时就使用同步访问技术,保证数据在任何同一时刻,最多有一个线程访问,以保证数据的完整性。
-
也可以理解为,线程同步,即当有一个线程在对内存进行操作时,其它线程都不可以对这个内存地址进行操作,直到该线程完成操作,其它线程才能对该内存地址进行操作。
- 具体方法:
- 同步代码块
synchronized(对象) // 得到对象的锁,才能操作同步代码块
{
// 需要被同步的代码
}
- synchronized 还可以放在方法声明中,表示整个方法为同步方法
public synchronized void m()
{
// 需要被同步的代码
}
5. 互斥锁
- 基本介绍:
-
Java语言中,引入了对象互斥锁的概念,来保证共享数据操作的完整性。
-
每个对象都对应于一个可称为“互斥锁”的标记,这个标记用来保证在任意时刻,只能有一个线程访问该对象。
-
关键字 synchronized 来与对象的互斥锁联系。当某个对象用 synchronized 修饰时,表明该对象在任一时刻只能由一个线程访问。
-
同步的局限性:导致程序的执行效率要降低
-
同步方法(非静态的)的锁,其实是加在当前对象的。也可以是其它对象(要求是同一个对象,否则锁不上)
-
同步方法(静态的)的锁为当前类本身。
6. 线程的死锁
多个线程都占用了对方的锁资源,但不肯想让,导致了死锁,在编程时要避免死锁的发生。
- 释放锁:
- 当前线程的同步方法、同步代码块执行结束
- 当前线程在同步代码块、同步方法中遇到 break、return
- 当前线程在同步代码块、同步方法中出现了未处理的 error 或 exception,导致异常结束
- 当前线程在同步代码块、同步方法中执行了线程对象的 wait() 方法,当前线程暂停,并释放锁
- 下面操作不会释放锁:
- 线程执行同步代码块或同步方法时,程序调用 Thread.sleep()、Thread.yield() 方法暂停当前线程的执行,不会释放锁
- 线程执行同步代码块时,其它线程调用了该线程的 suspend() 方法将该线程挂起,该线程不会释放锁。(最好避免使用)