一、线程概述
在我们看来,计算机可以同时执行多个任务,尽管是单核的CPU也能够做到,这是因为计算机的操作系统在执行多个任务的时,实际上就让CPU对多个任务轮流交替执行,但计算机执行速度很快,所以给人一种同时处理多任务的感觉。
进程
在操作系统中,一个独立执行的程序可以称做一个进程,也就是“正在运行的程序”。一个进程是由多个线程执行而成,一个进程至少有一个线程。
线程
我们知道,代码运行时都是自上而下的,这样的程序叫做单线程。
出现多段代码交替运行时,这样的程序叫做多线程,指一个进程在执行过程中可以产生多个单线程,这些单线程程序在运行时是相互独立的,它们可以并发执行,多线程意味着需要创建多个线程。
对比:对进程比多线程稳定,多进程中,一个进程的奔溃不会影响其他进程,而多线程中,任何一个线程奔溃都会直接导致整个进程奔溃。但是,进程的创建开销比较大,并且进程间的通信比线程的通信慢,为线程间通信就是读写同一个变量,速度很快。
二、线程创建
两种方式,一种继承java.lang包下的Thread类,一种实现java.lang.Runnable接口。
1、继承Thread类
public class example {
public static void main(String[] args) {
MyThread myThread = new MyThread();//创建继承类MyThread线程对象
myThread.start();//开启线程
//main方法线程输出
while (true){
System.out.println("main()方法在运行");
}
}
}
class MyThread extends Thread{
@Override
public void run() {
while (true){
System.out.println("MyThread类的run()方法在运行");
}
}
}
运行结果:
通过死循环打印,可以看出两个线程子在不断交叉执行。
2、实现Runnable接口
当一个类继承,比如说Student类继承了Person父类,那么就不能实现再继承Thread类,无法创建线程。因此Thread提供了另一个构造方法TThread(Runnable target),其中Runnable是一个接口,接口中只有一个run()方法。当通过Thread(Runnable target)构造方法创建线程对象时,只需为该方法传递一个实现Runnable接口的实例对象,这样创建的线程将调用实现了Runnable接口的类中的run()方法作为运行代码,而不需要调用Thread类中的run()方法。
public class example {
public static void main(String[] args) {
MyThread myThread = new MyThread(); //创建MyThread的实例对象
Thread thread = new Thread(myThread); //创建线程对象
thread.start(); // 开启线程,执行线程中的run()方法
while (true) {
System.out.println("main()方法在运行");
}
}
}
class MyThread1 implements Runnable {
@Override
public void run() {
while (true) {
System.out.println("MyThread类的run()方法运行");
}
}
}
区别
经典售票问题
继承Thread
public class example03 {
public static void main(String[] args) {
new Ticket().start();//第一个线程对象
new Ticket().start();//第二个线程对象
new Ticket().start();//第三个线程对象
new Ticket().start();//第四个线程对象
}
}
class Ticket extends Thread{
private int tickets = 100;
@Override
public void run() {
while (true){
if (tickets > 0){
System.out.println(Thread.currentThread().getName() + "正在售票:" + tickets-- + "张票");
}
}
}
}
结果
明显的,可以看到一张票被卖了两次,显然是不正确的。
Runnable接口
public class example04 {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(ticket,"窗口1").start();
new Thread(ticket,"窗口2").start();
new Thread(ticket,"窗口3").start();
new Thread(ticket,"窗口4").start();
}
}
class Ticket1 implements Runnable {
private int tickets = 100;
@Override
public void run() {
while (true) {
if (tickets > 0) {
System.out.print(Thread.currentThread().getName() + "正在售票:" + tickets-- + "张票" + " ");
}
}
}
}
运行结果四个窗口正常卖票,没有重复。这是因为总票数tickets是共享的,创建的四个线程里,每个线程都调用实现类Ticket1中的run()方法,这样就可以确保四个线程访问的是同一个tickets变量,共享100张车票。
线程创建简化
Thread t = new Thread(() -> {
//main方法代码
} });
public class example5 {
public static void main(String[] args) {
Thread thread = new Thread(()->{
while (true){
System.out.println("start new thread");
}
});
thread.start();
MyThread2 myThread = new MyThread2();//创建继承类MyThread2线程对象
myThread.start();//开启线程
}
}
class MyThread2 extends Thread{
@Override
public void run() {
while (true){
System.out.println("MyThread类的run()方法正在运行");
}
}
}
三、线程的生命周期以及状态转换
线程整个生命周期可以分为五个阶段,分别是新建状态(New)、就绪状态(Runnable)、运行状态(Running)、阻塞状态(Blocked)和死亡状态(Terminated),线程的不同状态表明了线程当前正在进行的活动。
1.新建状态(New)
创建一个线程对象后,该线程对象就处于新建状态,此时它不能运行,和其他Java对象一样,仅仅由Java虚拟机为其分配了内存,没有表现出任何线程的动态特征。
2.就绪状态(Runnable)
当线程对象调用了start()方法后,该线程就进入就绪状态。处于就绪状态的线程位于线程队列中,此时它只是具备了运行的条件,能否获得CPU的使用权并开始运行,还需要等待系统的调度。
3.运行状态(Running)
如果处于就绪状态的线程获得了CPU的使用权,并开始执行run()方法中的线程执行体,则该线程处于运行状态。一个线程启动后,它可能不会一直处于运行状态,当运行状态的线程使用完系统分配的时间后,系统就会剥夺该线程占用的CPU资源,让其他线程获得执行的机会。需要注意的是,只有处于就绪状态的线程才可能转换到运行状态。
4.阻塞状态(Blocked)
一个正在执行的线程在某些特殊情况下,如被人为挂起或执行耗时的输入/输出操作时,会让出CPU的使用权并暂时中止自己的执行,进入阻塞状态。线程从阻塞状态只能进入就绪状态,而不能直接进入运行状态,也就是说结束阻塞的线程需要重新进入可运行池中,等待系统的调度。
5.死亡状态(Terminated)
当线程调用stop()方法或run()方法正常执行完毕后,或者线程抛出一个未捕获的异常(Exception)、错误(Error),线程就进入死亡状态。一旦进入死亡状态,线程将不再拥有运行的资格,也不能再转换到其他状态。
四、线程调度
线程调度有两种模型,分别是分时调度模型和抢占式调度模型。
分时调度模型是指让所有的线程轮流获得CPU的使用权,并且平均分配每个线程占用的CPU的时间。抢占式调度模型是指让可运行池中优先级高的线程优先占用CPU,而对于优先级相同的线程,随机选择一个线程使其占用CPU,当它失去了CPU的使用权后,再随机选择其他线程获取CPU使用权。
Java虚拟机默认采用抢占式调度模型。
线程优先级
线程的优先级用1~10之间的整数来表示,数字越大优先级越高。
静态常量表示线程的优先级
通过Thread类的setPriority(int newPriority)方法进行设置,参数newPriority接收的是1~10之间的整数或者Thread类的三个静态常量。
public class example06 {
public static void main(String[] args) {
Thread min = new Thread(new MinPriority(),"优先级较低");
Thread max = new Thread(new MaxPriority(),"优先级较高");
min.setPriority(Thread.MIN_PRIORITY);
max.setPriority(Thread.MAX_PRIORITY);
min.start();
max.start();
}
}
class MaxPriority implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "正在输出" + i);
}
}
}
class MinPriority implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "正在输出" + i);
}
}
}
线程休眠
想使正在执行的线程暂停,将CPU让给别的线程,这时可以使用静态方法sleep(long millis),该方法可以让当前正在执行的线程暂停一段时间,进入休眠等待状态。这样其他的线程就可以得到执行的机会了。
public class example07 {
public static void main(String[] args) {
new Thread(new SleepThread()).start();
for (int i = 0; i < 10; i++) {
if (i == 5){
try {
Thread.sleep(2000);// 当前线程休眠2秒
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("主线程正在输出:" + i);
try {
Thread.sleep(500);// 当前线程休眠500毫秒
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
class SleepThread implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
if (i ==3){
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("SleepThread线程正在输出" + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
sleep()是静态方法,只能控制当前正在运行的线程休眠,而不能控制其他线程休眠。当休眠时间结束后,线程就会返回到就绪状态,而不是立即开始运行。
线程让步
public class example08 {
public static void main(String[] args) {
YieldThread threadA = new YieldThread("线程A");
YieldThread threadB = new YieldThread("线程B");
threadA.start();
threadB.start();
}
}
class YieldThread extends Thread{
public YieldThread(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 6; i++) {
System.out.println(Thread.currentThread().getName() + "---" + i);
if (i == 3){
System.out.println("线程让步");
Thread.yield();// 线程运行到此,作出让步
}
}
}
}
调用Thread的yield()方法,使当前线程暂停,这时另一个线程就会获得执行。
但在上述代码,当线程A==3,线程A做出让步后,线程B并没有获得执行权,虽然yield()可以让线程又“运行状态”到“就绪状态”,但是并不一定会让其他线程获得执行权。
线程插队
在Thread类中提供了一个join()方法,当在某个线程中调用其他线程的join()方法时,调用的线程将被阻塞,直到被join()方法加入的线程执行完成后它才会继续运行。
public class example09 {
public static void main(String[] args) {
Thread t = new Thread(new JoinThread(),"线程1");
t.start();
for (int i = 0; i < 6; i++) {
System.out.println(Thread.currentThread().getName() + "正在输出:" + i);
if (i == 2){
try {
t.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
class JoinThread implements Runnable{
@Override
public void run() {
for (int i = 0; i < 6; i++) {
System.out.println(Thread.currentThread().getName() + "正在输出:" + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
两个线程的循环体中都调用了Thread的sleep(500)方法,以实现两个线程的交替执行。当main线程中的循环变量为2时,调用t线程的join()方法,这时,t线程就会“插队”优先执行。从运行结果可以看出,当main线程输出2以后,线程一就开始执行,直到线程一执行完毕,main线程才继续执行。
五、多线程同步
多线程可以提高效率,但是多个线程去访问同一个资源,会引发一些安全问题。因此,需要实现多线程的同步,即限制某个资源在同一时刻只能被一个线程访问。
线程安全问题
在上面介绍Runnable接口时,引用了售票的例子,但售票的过程中会出现意外,下面通过使用sleep()方法进行模拟。
public class example10 {
public static void main(String[] args) {
SaleThread saleThread = new SaleThread();
new Thread(saleThread,"线程一").start();
new Thread(saleThread,"线程二").start();
new Thread(saleThread,"线程三").start();
new Thread(saleThread,"线程四").start();
}
}
class SaleThread implements Runnable{
private int tickets = 10;
@Override
public void run() {
while (tickets > 0){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "--卖出的票:" + tickets--);
}
}
}
出现了明显的线程安全问题,重复卖票和票号为负数。原因是在售票程序的while循环中添加了sleep()方法,由于线程有延迟,当票号减为1时,假设线程1此时出售1号票,对票号进行判断后,进入while循环,在售票之前通过sleep()方法让线程休眠,这时线程二会进行售票,由于此时票号仍为1,因此线程二也会进入循环,同理,四个线程都会进入while循环,休眠结束后,四个线程都会进行售票,这样就相当于将票号减了四次,结果中出现了0、-1、-2这样的票号。
同步代码块
线程安全问题其实就是由多个线程同时处理共享资源所导致的,要想解决线程安全问题,必须得保证在任何时刻只能有一个线程访问共享资源。
java中提供了同步机制,将共享资源的代码放在一个使用synchronized关键字修饰的代码块中,这个代码块被称作同步代码块。
格式:
synchronized(lock){ 操作共享资源代码块 }
lock是一个锁对象,是同步代码块的关键。lock可以是任意对象,它的作用是用来定义临界区,确保同一时刻只有一个线程可以执行被同步的代码块。 当一个线程进入synchronized块时,它会尝试获取lock对象的锁,如果锁被其他线程持有,那么该线程会被阻塞,直到锁被释放。只有当该线程成功获取到锁时,才能继续执行synchronized块中的代码。其他线程在获取到锁之前,将被阻塞在synchronized块外部,等待锁的释放。循环往复,直达共享资源被处理完成。这样保证同一时刻只有一个线程可以执行被同步的代码块,防止多个线程同时访问共享资源造成的数据不一致或冲突。
将售票进行修改,添加同步代码块
public class example10 {
public static void main(String[] args) {
SaleThread saleThread = new SaleThread();
new Thread(saleThread,"线程一").start();
new Thread(saleThread,"线程二").start();
new Thread(saleThread,"线程三").start();
new Thread(saleThread,"线程四").start();
}
}
class SaleThread implements Runnable{
private int tickets = 10;
Object lock = new Object(); // 定义任意一个对象,用作同步代码块的锁
@Override
public void run() {
while (true){
synchronized (lock){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
if (tickets > 0){
System.out.println(Thread.currentThread().getName() + "卖出的票:" + tickets--);
}else{
break;
}
}
}
}
将有关tickets变量的操作全部都放到同步代码块中。为了保证线程的持续执行,将同步代码块放在死循环中,直到ticket<0时跳出循环。从运行结果可以看出,售出的票不再出现0和负数的情况,这是因为售票的代码实现了同步,之前出现的线程安全问题得以解决。
注意:锁对象的创建代码不能放到run()方法中,否则每个线程运行到run()方法都会创建一个新对象,这样每个线程都会有一个不同的锁,每个锁都有自己的标志位,这样线程之间便不能产生同步的效果。
同步方法
在方法前面同样可以使用synchronized关键字来修饰,被修饰的方法为同步方法,它能实现和同步代码块同样的功能。
格式:
synchronized 返回值类型 方法名([参数1,...]){}
被synchronized修饰的方法在某一时刻只允许一个线程访问,访问该方法的其他线程都会发生阻塞,直到当前线程访问完毕后,其他线程才有机会执行该方法。
public class example11 {
public static void main(String[] args) {
TicketThread ticketThread = new TicketThread();
new Thread(ticketThread,"线程1").start();
new Thread(ticketThread,"线程2").start();
new Thread(ticketThread,"线程3").start();
new Thread(ticketThread,"线程4").start();
}
}
class TicketThread implements Runnable{
private int tickets = 10;
@Override
public void run() {
while (true){
saleTicket();
if (tickets <= 0){
break;
}
}
}
//定义同步方法saleTicket()
private synchronized void saleTicket(){
if ( tickets> 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "卖出的票:" + tickets--);
}
}
}

死锁问题
面试官:如果你能告诉我什么是死锁,我就录用你。
面试者:只要你录用我,我就告诉你什么是死锁。
....
上面对话就形成了一个死锁,两个线程都在等待对方的锁,这样造成了程序停滞,称为死锁。
六、线程通信
线程中不只是争夺锁,也有协作机制,当我们需要多个线程来共同完成一件任务,并且我们希望他们有规律的执行,那么多线程之间需要一些通信机制,可以协调它们的工作,以此实现多线程共同操作一份数据——等待唤醒机制。
在一个线程满足某个条件时,就进入等待状态(wait() / wait(time)), 等待其他线程执行完他们的指定代码过后再将其唤醒(notify());或可以指定 wait 的时间,等时间到了自动唤醒;在有多个线程进行等待时,如果需要,可以使用 notifyAll()来唤醒所有的等待线程。wait/notify 就是线程间的一种协作机制。
注意:被通知的线程被唤醒后也不一定能立即恢复执行,因为它当初中断的 地方是在同步块内,而此刻它已经不持有锁,所以它需要再次尝试去 获取锁(很可能面临其它线程的竞争),成功后才能在当初调用 wait 方法之后的地方恢复执行。
如果能获取锁,线程就从 WAITING 状态变成 RUNNABLE(可运行)状态。否则,线程就从 WAITING 状态又变成 BLOCKED(等待锁) 状态。
public class example13 {
public static void main(String[] args) {
Thread1 thread1 = new Thread1();
new Thread(thread1,"线程1").start();
new Thread(thread1,"线程2").start();
}
}
class Thread1 implements Runnable{
int i = 1;
@Override
public void run() {
while (true){
synchronized (this){
notify();
if(i <= 10){
System.out.println(Thread.currentThread().getName() + ":" + i++);
}else{
break;
}
try {
wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
}
从结果可以知道,两个线程交叉执行,当线程1打印完后进入等待状态,这时线程2对其进行唤醒,打印完后进行等待状态,线程1对其进行唤醒...直到结束。
注意:
wait 方法与 notify 方法必须要由同一个锁对象调用。因为:对应的锁对象可以通过 notify 唤醒使用同一个锁对象调用的 wait 方法后的线程。
wait 方法与 notify 方法是属于 Object 类的方法的。因为:锁对象可以是任意对象,而任意对象的所属类都是继承了 Object 类的。
wait 方法与 notify 方法必须要在同步代码块或者是同步函数中使用因为:必须 要通过锁对象调用这 2 个方法。否则会报java.lang.IllegalMonitorStateException 异常。