多线程基础总结(包含实例)--适合初学者

222 阅读14分钟

1. 通过继承Thread类的方式创建新的线程时为什么要覆盖父类的run()方法呢?

记住三个关键字:线程 & 任务 & 启动

创建线程是为了创建一条新的执行路径,并在这个新路径去执行某任务(也就是在这个新路径去执行某代码),来达到和其他线程(比如主线程)中的任务同时运行的目的。

Question : 那个这个新路径的具体任务在哪儿体现呢?

Answer : 在run()方法中来体现的。run()方法就是封装自定义线程任务的函数。run()方法中定义的就是线程要运行的任务代码。

线程创建并覆盖run()方法之后,调用线程的start()方法来启动线程,线程启动了那是不是就要执行任务了呢,那任务在哪儿呢,在覆写的run()方法里呢。

start()方法的作用:让该线程开始执行;Java虚拟机调用该线程的run()方法,调用run()方法就是开始执行任务了。

2. 创建线程有两种方式,可不可以只提供继承Thread类这一种方式?

先直接说结论:不可以,因为Java语言不支持多继承。

回顾一下创建线程的两种方式:

  1. 创建一个Thread类的子类,并覆写父类中的run方法。
  2. 创建一个实现Runnable接口的类的实例对象,这个类主要就是实现Runnable接口中的run方法。然后使用Thread(Runnable runnable)构造方法创建线程,这里的runnable参数就是那个实现Runnable接口的类的实例对象。

假如我创建的一个类已经继承了一个Thread类之外的父类了,那这个类就不能在继承Thread类了,因为Java语言不支持多继承。那这样的话就没法创建线程了。

鉴于以上原因,Java还需要提供另外一种不是通过继承Thread类创建线程的方式来创建线程。

所以,第二种方式是必不可少的。

实现Runnable接口的方式创建线程的好处:

  1. 将线程的任务从线程的子类中分类出来,进行了单独的封装。或者说书,安装面向对象的方式将线程需要执行的任务进行了单独的封装这是一种思想。
  2. 避免了Java语言单继承的局限性。

3 线程安全问题的现象

if (num > 0)
{
    //|          |           |
    //|          |           |
    //线程1进来了,但是还没执行num--,CPU将执行权分给QQ音乐的线程了
    //|          | 线程2进来了,但是还没执行num--,CPU将执行权分给线程3了
    //|          |           |线程3进来了,但是还没执行num--,CPU将执行权分给eclipse线程了
    //|          |           |
    //|          |           |
    num--;
    System.out.println(num);
}

如上图,每个线程在进入if代码块的时候,num都是大于0的。

线程1进来了,但是还没执行num--,CPU将执行权分给QQ音乐的线程了;

线程2进来了,但是还没执行num--,CPU将执行权分给线程3了;

线程3进来了,但是还没执行num--,CPU将执行权分给eclipse线程了。

最终这3个线程都把num--和输出语句都执行了一遍,可能导致num的输出值变成负数了。

4. 线程安全问题产生的原因

产生前提

  1. 多个线程在操作共享的数据
  2. 操作共享数据的线程代码有多行

一句话说明产生原因: 当一个线程在执行操作共享数据的多条代码过程中,其他线程参与了运算, 就会导致线程安全问题的产生。

解决思路就是将多条操作共享数据的线程代码封装起来,当有线程在执行这些代码的时候, 其他线程时不可以参与运算的。 必须要当前线程把这些代码都执行完毕后,其他线程才可以参与运算。

比如用同步代码块就可以解决这个问题。

同步代码块的格式:

synchronized(对象)
{
	需要被同步的代码;
}

5. 同步的好处,同步的弊端,同步的前提

同步的好处:解决了线程的安全问题。

同步的弊端:相对降低了效率,因为同步外的线程的都会判断同步锁,这样就增大了资源开销。

同步的前提:同步中必须有多个线程并使用同一个锁。

6. 写一个死锁示例

说明下:下面的死锁使用的是同步嵌套的方式。

package javapractise;

public class DeadLock {

	public static void main(String[] args) {
		Task task0 = new Task();
		Task task1 = new Task();
		Thread t0 = new Thread(task0);
		Thread t1 = new Thread(task1);
		
		t0.start();
		task1.flag = false; // 
		t1.start();
	}
}

class MyLock {
	public static final Object LOCK_A = new Object();
	public static final Object LOCK_B = new Object();
}

class Task implements Runnable {
	public boolean flag = true;
	@Override
	public void run() {
		if (flag) {
			// 嵌套锁
			synchronized (MyLock.LOCK_A) {
				System.out.println(Thread.currentThread().getName() + "...if分支....第1行输出");
				synchronized (MyLock.LOCK_B) {
					System.out.println(Thread.currentThread().getName() + "...if分支....第2行输出");
				}
			}
		} else {
			// 嵌套锁
			synchronized (MyLock.LOCK_B) {
				System.out.println(Thread.currentThread().getName() + "...else分支....第1行输出");
				synchronized (MyLock.LOCK_A) {
					System.out.println(Thread.currentThread().getName() + "...else分支....第2行输出");
				}
			}
		}
	}
}
  • 运行结果1(死锁情况)
    if分支和else分支的第二行输出语句都没有打印,程序锁死了。
  • 运行结果2(正常情况)
    if分支和else分支的代码都运行完毕了

再写一个形式简单一点的

package javapractise;

public class DeadLock {
    // 创建两个对象,充当锁
	public static final Object LOCK_A = new Object();
	public static final Object LOCK_B = new Object();
	
	public static void main(String[] args) {
	    //  创建任务run0
		Runnable run0 = new Runnable() {
			@Override
			public void run() {
				synchronized (LOCK_A) {
					System.out.println(Thread.currentThread().getName() + "...01");
					synchronized (LOCK_B) {
						System.out.println(Thread.currentThread().getName() + "...02");
					}
				}
			}
		};
	    //  创建任务run1
		Runnable run1 = new Runnable() {
			@Override
			public void run() {
				synchronized (LOCK_B) {
					System.out.println(Thread.currentThread().getName() + "...03");
					synchronized (LOCK_A) {
						System.out.println(Thread.currentThread().getName() + "...04");
					}
				}
			}
		};
		// 创建2个线程,并启动线程
		Thread th0 = new Thread(run0);
		Thread th1 = new Thread(run1);
		th0.start();
		th1.start();
	}
}

  • 运行结果1(死锁情况)
  • 运行结果2(正常情况)

7. 非静态的同步函数使用的锁是什么? 静态的同步函数使用的锁又是什么?同步代码块的锁又是什么呢?

先说答案:

非静态同步函数使用的锁是this.

静态同步函数使用的锁是该函数所属的类的字节码文件对象(类名.class对象).

同步代码块使用的锁可以是任何对象.

非静态函数都有自己所持有this引用,而static函数不持有this。

同步函数仅仅是函数带了同步性,同步本身不带锁吧,那同步函数应该是函数带的锁吧,非静态函数都有自己所持有this引用,那就用this作为非静态函数的锁。而static函数不持有this,那就用函数所属的类的字节码文件对象作为锁吧。

8. 我们一般啊,使用同步代码块比使用同步函数要好。

9. 线程间通讯示例--wait/notify/notifyAll示例

线程间通讯,通过等待/唤醒机制实现

涉及的方法:

  • wait,让线程冻结,没有了执行权,也没有了执行资格
  • notify,唤醒线程池中的一个线程(任意),有了执行资格,等待CPU分配执行权
  • notifyAll,唤醒线程池中所有的线程,有了执行资格,等待CPU分配执行权

注意:

上面的这次方法都必须在同步中使用。因为这些方法都是操作线程状态的方法,操作线程状态的时候必须要明确到底操作的是哪个锁上的线程。如下,LOCK_B.notify()是无法唤醒在等待LOCK_A锁的线程的。

LOCK_A.wait();
LOCK_B.wait(); LOCK_B.notify();
/*
* 写两个线程,要求线程A给公共资源赋值一次,线程B就获取一次公共资源。也就是线程A赋值-->线程B获取-->线程A赋值-->线程B获取......
* 思路 : 线程间的通讯使用wait和notify方法
*/
public class ThreadWait {

   public static void main(String[] args) {
   	// 创建资源
   	Resource resource = new Resource();
   	// 创建任务
   	TaskA taskA = new TaskA(resource);
   	TaskB taskB = new TaskB(resource);
   	// 创建线程
   	Thread threadA = new Thread(taskA);
   	Thread threadB = new Thread(taskB);
   	// 线程启动
   	threadA.start();
   	threadB.start();
   }

}

class Resource {
   private String name;
   private String sex;
   private boolean flag = false;

   public void setNameAndSex(String name, String sex) {
   	synchronized (this) {
   		if (flag) { // 这里使用while更好
   			try {
   				this.wait(); // 冻结目前持有this锁的线程(这里指的是给name和sex赋值的线程)
   			} catch (InterruptedException e) {
   				e.printStackTrace();
   			}
   		}
   		this.name = name; // 给公共资源赋值
   		this.sex = sex;   // 给公共资源赋值
   		this.flag = true;  // 给name和sex赋值已经将完成了,将flag置为true,防止不停的赋值
   		this.notify(); // 唤醒在等待this锁的某个线程(这里指的是读取name和sex的线程)
   	}
   }

   public void getNameAndSex() {
   	synchronized (this) {
   		if (!flag) {  // 这里使用while更好
   			try {
   				this.wait(); // 冻结目前持有this锁的线程(这里指的是读取name和sex的线程)
   			} catch (InterruptedException e) {
   				e.printStackTrace();
   			}
   		}
   		System.out.println(name + "......" + sex); // 获取公共资源
   		this.flag = false; // 获取name和sex已经完成了,将flag置为false,防止不停的获取值
   		this.notify(); // 唤醒在的等待this锁的线程(这里指的是给name和sex赋值的线程)
   	}

   }
}

class TaskA implements Runnable {
   Resource res;
   boolean flag = false;

   TaskA(Resource res) {
   	this.res = res; // 任务A持有公共资源Resource
   }

   @Override
   public void run() {
   	int x = 0;
   	for (int i = 0; i < 50; i++) {
   		if (x == 0) {
   			res.setNameAndSex("Jack", "man");
   		} else {
   			res.setNameAndSex("丽丽", "女");
   		}
   		x = (x + 1) % 2; // 为了让交替着赋值为Jack和丽丽
   	}
   }
}

class TaskB implements Runnable {
   Resource res;

   TaskB(Resource res) { // 任务B也持有公共资源Resource
   	this.res = res;
   }

   @Override
   public void run() {
   	for (int i = 0; i < 50; i++) {
   		res.getNameAndSex();
   	}
   }
}

程序运行结果:

10. 为什么操作线程状态的wait/notify/notifyAll方法要定义在Object类中,而不定义在Thread类中?

我们知道这三个方法都是被锁对象调用的,比如LOCK.wait() / LOCK.notifyAll()。而任何对象都可以作为锁,任意对现象都可以调用的方法那得定义在Object类中。

11. 等待唤醒机制——生产者消费者模型01(wait/notifyAll)

这里介绍多生产者,多消费者的问题。

本例的代码有两个生产者,两个消费者,生产者生产一个,消费者消费一个。

问题1 : 标记判断为什么要使用while替换上一个例子中的if ?

if判断标记,只判断一次,会导致不该运行的线程运行了,会出现数据错误的情况。

什么时候会出现错误呢?比如某个时刻flag标记是true,那么生产者1线程进入wait等待,过了一会儿,别人把他唤醒了,他就直接往下执行代码了,但是由于是多线程,生产者2号线程在生产者1号线程被唤醒但还没来得及执行代码的时候又将flag改成了true,此时生产者1线程直接去执行下面的生产代码就有问题了。

while判断标记,沉睡的线程被唤醒后要在进行一次flag标记判断,解决了线程获取执行权后,是否真的应该运行!

问题2 : 为什么要使用notifyAll替换上一个例子中的notify

notify: 简单说,while判断标记 + notify会导致死锁。可以把下面的代码中notifyAll改成notify在电脑跑一下,肯定会下出现死锁的。

notifyAll解决了本方线程一定会唤醒对方线程的问题,因为他会把所有线程都唤醒。

代码演示

package p1.thread;

class Resource {
	private String name;
	private int count = 0;
	private boolean flag = false;

	public synchronized void produce(String name) {
		while (flag) // 标记判断为什么要使用while替换上一个例子中的if
			try {
				this.wait();
			} catch (InterruptedException e) {
			}

		this.name = name + count;
		count++;
		System.out.println(Thread.currentThread().getName() + "...生产..." + this.name);
		flag = true;
		notifyAll(); // 为什么要使用notifyAll替换上一个例子中的notify
	}

	public synchronized void consume() {
		while (!flag)
			try {
				this.wait();
			} catch (InterruptedException e) {
			}
		System.out.println("......" + Thread.currentThread().getName() + "...消费..." + this.name);
		flag = false;
		notifyAll();
	}
}

class Producer implements Runnable {
	private Resource r;

	Producer(Resource r) {
		this.r = r;
	}

	public void run() {
		while (true) {
			r.produce("手机");
		}
	}
}

class Consumer implements Runnable {
	private Resource r;

	Consumer(Resource r) {
		this.r = r;
	}

	public void run() {
		while (true) {
			r.consume();
		}
	}
}

class ProducerConsumerDemo {
	public static void main(String[] args) {
		
		Resource r = new Resource();
		
		Producer pro = new Producer(r);
		Consumer con = new Consumer(r);

		Thread t0 = new Thread(pro);
		Thread t1 = new Thread(pro);
		Thread t2 = new Thread(con);
		Thread t3 = new Thread(con);
		t0.start();
		t1.start();
		t2.start();
		t3.start();

	}
}

12. 等待唤醒机制——生产者消费者模型02(await/signal)

创建一个锁,再通过这个锁获取两组监视器,一组监视生产者,一组监视消费者

jdk1.5以后将锁封装成了对象

Lock接口: 它的出现替代了同步代码块或者同步函数。将同步的隐式锁操作变成显式锁操作。 同时操作更为灵活。可以一个锁上加上多组监视器

  1. lock() : 获取锁。

  2. unlock() : 释放锁,通常需要定义finally代码块中。

Condition接口:它的出现替代了Object中的wait notify notifyAll方法。

将这些监视器方法单独进行了封装,变成Condition监视器对象。可以和任意锁进行组合。

  • await() --> wait()
  • signal() --> signal()
  • signalAll() --> signalAll()

核心代码

	// 创建一个锁对象
	Lock lock = new ReentrantLock();

	// 通过已有的锁获取两组监视器,一组监视生产者,一组监视消费者
	Condition producer_con = lock.newCondition();
	Condition consumer_con = lock.newCondition();
	
	// code...
	producer_con.signal();
	// code...
	consumer_con.signal();

代码演示

package p2.thread;

import java.util.concurrent.locks.*;

public class ProducerConsumerDemo2 {
	public static void main(String[] args) {
		Resource r = new Resource();
		Producer pro = new Producer(r);
		Consumer con = new Consumer(r);

		Thread t0 = new Thread(pro);
		Thread t1 = new Thread(pro);
		Thread t2 = new Thread(con);
		Thread t3 = new Thread(con);
		t0.start();
		t1.start();
		t2.start();
		t3.start();

	}

}

class Resource {
	private String name;
	private int count = 1;
	private boolean flag = false;

	// 创建一个锁对象
	Lock lock = new ReentrantLock();

	// 通过已有的锁获取两组监视器,一组监视生产者,一组监视消费者
	Condition producer_con = lock.newCondition();
	Condition consumer_con = lock.newCondition();

	public void produce(String name) {
		lock.lock(); // 【获取锁】
		try {
			while (flag)
				try {
					producer_con.await();
				} catch (InterruptedException e) {
				}

			this.name = name + count;
			count++;
			System.out.println(Thread.currentThread().getName() + "...生产..." + this.name);
			flag = true;
			consumer_con.signal(); // 生产完一个商品后通知消费者进行消费
		} finally {
			lock.unlock(); // 【释放锁】,在finally释放,防止try语句中的代码发生异常时候没有进行释放锁
		}

	}

	public void consume() {
		lock.lock(); 【获取锁】
		try {
			while (!flag)
				try {
					consumer_con.await();
				} catch (InterruptedException e) {
				}
			System.out.println("........" + Thread.currentThread().getName() + "...消费..." + this.name);
			flag = false;
			producer_con.signal(); // 消费完一个商品后通知生产者进行生产
		} finally {
			lock.unlock(); // 【释放锁】,在finally释放,防止try语句中的代码发生异常时候没有进行释放锁
		}

	}
}

class Producer implements Runnable {
	private Resource r;

	Producer(Resource r) {
		this.r = r;
	}

	public void run() {
		while (true) {
			r.produce("烤鸭");
		}
	}
}

class Consumer implements Runnable {
	private Resource r;

	Consumer(Resource r) {
		this.r = r;
	}

	public void run() {
		while (true) {
			r.consume();
		}
	}
}

13. 上面两个例子分析一下

使用while判断标记 + notifyAll的形式可以实现线程之间通讯,而且不会死锁。那为什么还要搞一套Lock + condition的方式来实现同样的功能呢?

  • 可以选择性的是实现唤醒对方线程的效果。(notifyAll他会唤醒本方线程还会唤醒对方线程,而唤醒本方线程是没有意义的,因为flag标志不满足。
  • 对锁的造作更加灵活
  • 同步的隐式锁操作变成显式锁操作。

14. 造成死锁的常见方式:

  1. 锁嵌套
  2. while + notify(解决办法notify --> notifyAll)

15. sleep和wait的区别

  1. wait可指定时间,也可以不指定时间,而sleep必须执行时间
  2. 在同步时,对CPU的执行权和锁的处理方式不同。
  • wait 释放执行权,且释放锁(必须释放锁,如果不释放锁,怎么让别人来唤醒它呀)
  • sleep 释放执行权,但是不释放锁(它不需要被别人唤醒)

16. 线程结束方式

  1. stop()方法 已经废弃的方法,因为它不安全
  2. run方法的方法体执行完毕了就结束了

那么怎么来控制任务结束呢?

定义标记:控制循环通常用定义标记来完成。

但它也有一些解决不了的问题或场景,比如线程处于冻结状态,就无法读取标记,就一直被冻结中,线程任务就无法结束了。

针对以上情况,可以使用如下方法解决

  1. interrupt()方法

它会将线程从冻结状态强制性恢复到运行状态中,让线程强制具有CPU执行资格,但是由于是强制性的,会发生InterruptException异常。记得要处理这个异常。

就像,催眠师把你催眠了,然后他接了个电话出国了,那你就没办法被接触催眠了,这个时候使用interrupt()方法把你给唤醒了,相当于给你泼了一盆冷水,你就醒了,但是由于不是正常的叫醒的,你起来的把脑袋磕了一下,头一直疼。然后你还得赶紧赶紧揉一揉,处理这个异常。

  1. 守护线程 setDaemon()方法 将这个线程设为后台线程,前台线程结束了,后台线程就结束了。

setDaemon()用法:

线程1.setDaemon(); // 在线程启动之前就得设置
线程1.start();
  1. 临时加入线程 join()

线程a的方法体中的某一行有一句线程B.join(),那个线程a执行到这一行的时候就停止了,一直等线程B执行完自己才能继续执行。

可以比喻成插队。