Java多线程知识点整理
一、基本概念
1.程序、进程和线程的区别
①程序(program):是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。
②进程(process): 是程序的一次执行过程,或是正在运行的一个程序。动态过程:有它自身的产生、存在和消亡的过程。
——如:运行中的QQ,运行中的MP3播放器
程序是静态的,进程是动态的
③线程(thread): 进程可进一步细化为线程,是一个程序内部的一条执行路径。若一个程序可同一时间执行多个线程,就是支持多线程的。
2.何时需要多线程
① 程序需要同时执行两个或多个任务。
② 程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等。
③ 需要一些后台运行的程序时。
二、线程的常用创建方式
1.通过继承Thread的方式创建线程
创建方式:
① 自定义一个类并继承Thread
② 重写run方法
③ 在run方法中去实现需要在分线程中实现的功能
④ 创建Thread的子类的对象
⑤ 调用start方法
举例:
//1.自定义一个类并继承Thread
class MyThread extends Thread{
//2.重写run方法
@Override
public void run() {
//3.在run方法中实现需要在分线程中实现的功能
for (int i = 0; i < 100; i++) {
//Thread.currentThread():获取当前正在执行的线程。
//getName() : 获取线程的名字
System.out.println(Thread.currentThread().getName() + "杀毒中.....");
}
}
}
public class ThreadTest2 {
public static void main(String[] args) {
//4.创建Thread子类的对像
MyThread mt = new MyThread();
//5.调用start方法
mt.start();//start() :1.开启分线程 2.调用run方法
}
}
2.通过实现Runnable的方式创建线程
创建方式:
① 自定义类实现Runnable接口
② 重写run方法
③ 在run方法中去实现需要在分线程中实现的功能
④ 创建Runnable实现类的对象
⑤ 创建Thread类的对象,并将Runnable实现类的对象作为实参传给Thread的构造器
⑥ 调用start方法
举例:
//1.自定义一个类并实现Runnable接口
class MyRunnable implements Runnable{
//2.重写run方法
@Override
public void run() {
//3。在run方法中去实现需要在分线程中实现的功能
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " ==== " + i);
}
}
}
public class RunnableTest {
public static void main(String[] args) {
//4.创建Runnable接口实现类的对象
MyRunnable mr = new MyRunnable();
//5.创建Thread的对象并将Runnable接口实现类的对象作为实参传给Thread的构造器中
Thread t = new Thread(mr);
//6.调用start方法
t.start();
}
}
3.两种创建线程方式的区别和联系
区别:
① 继承Thread: 线程代码存放Thread子类run方法中。
② 实现Runnable:线程代码存在接口的子类的run方法。
联系:
① 避免了单继承的局限性
② 多个线程可以共享同一个接口实现类的对象,非常适合多个相同线程来处理同一份资源。
三、线程Thread类常用API方法
四、线程的生命周期
五、线程的同步
1.举例说明线程同步重要性
此处以三个窗口同步售票为例,总计100张票,多个线程同时运行时会出现错票、重票的现象------
发生问题的原因 :当某个线程正在操作共享数据时,当前正在操作共享数据时还没操作完毕,其它线程也进来操作该数据就发生了线程安全问题。
解决思路 :当某个线程在操作共享数据时,其它线程不能再操作该数据。
java中解决线程安全问题的方法: 1. 同步代码块 2. 同步方法
class MyRunnable implements Runnable{
private int ticketNumber = 100;
@Override
public void run() {
while(true){
if(ticketNumber > 0){//有票
try {
Thread.currentThread().sleep(45);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "===" + ticketNumber);
ticketNumber--;
}else{
return;
}
}
}
}
public class TicketTest {
public static void main(String[] args) {
//开启三个线程
MyRunnable mr = new MyRunnable();
new Thread(mr,"窗口1:").start();
new Thread(mr,"窗口2:").start();
new Thread(mr,"窗口3:").start();
}
}
2. 同步代码块
格式 :
synchronized(同步监视器/锁){
操作共享数据的代码;
}
说明:
① 同步监视器 :可以是任意类的对象但是多个线程使用的锁必须是同一把。
② 如果是继承Thread类的方式那么锁不可以使用this
如果是实现Runnable接口的方式那么锁可以使用this
举例:
//实现Runnable接口的方式
class MyRun implements Runnable{
private int ticketNumber = 100;
@Override
public void run() {
while(true){
synchronized (this) {//同步代码块,注意多个线程使用的锁必须是同一把
if (ticketNumber > 0) {//有票
System.out.println(Thread.currentThread().getName() + "===" + ticketNumber);
ticketNumber--;
} else {
return;
}
}
}
}
}
//继承Thread类方式
class MyThr extends Thread{
private static int ticketNumber = 100;
//锁
private static Object obj = new Object();
@Override
public void run() {
while(true){
synchronized (obj) {
if (ticketNumber > 0) {//有票
System.out.println(Thread.currentThread().getName() + "===" + ticketNumber);
ticketNumber--;
} else {
return;
}
}
}
}
}
3.同步方法
格式 :
权限修饰符 synchronized 返回值类型 方法名(形参列表){
操作共享数据的代码。
}
说明:
① 继承Thread的锁(同步方法用static修饰)是 : 当前类.class
② 实现Runnable的锁是 :this
举例:
class MyRun implements Runnable{
private int ticketNumber = 100;
@Override
public void run() {
while(true){
if(!saleTicket()){
return;//结束线程
}
}
}
/*
同步方法:
默认的锁是this
*/
public synchronized boolean saleTicket(){
if (ticketNumber > 0) {//有票
System.out.println(Thread.currentThread().getName() + "===" + ticketNumber);
ticketNumber--;
return true;
} else {
return false;
}
}
}
//开启三个线程
MyRun mr = new MyRun();
new Thread(mr,"窗口1:").start();
new Thread(mr,"窗口2:").start();
new Thread(mr,"窗口3:").start();
六、继承Thread和实现Runnable的区别
1.单继承多实现的角度:
实现Runnable的方式更好一些
2.同步代码块的角度 :
继承Thread的锁不可以使用this,实现Runnable的锁可以使用this
3.同步方法的角度:
继承Thread中同步方法必须使用static修饰。默认的锁是:当前类.class
实抽Runnable中同步方法的默认锁是this
4.共享资源的角度 :
如果是在继承Thread中声明的共享资源必须使用static修饰(多个线程使用的是同一份)
实现Runnable中声明的共享资源不需要使用static修饰
总结:实现Runnable的方式更好一些。
七、线程安全的单例设计模式
class Bank{
private Bank(){}
private static Bank bank = null;
public static Bank getInstance(){
if (bank == null) {//增加一次判断以提高效率
synchronized (Bank.class) {
if (bank == null) {
bank = new Bank();
}
}
}
return bank;
}
}
八、死锁
线程死锁的原因 :
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
解决方法:
专门的算法、原则
尽量减少同步资源的定义
举例:
new Thread() {
public void run() {
synchronized (s1) {
s2.append("A");
try {
Thread.sleep(45);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s2) {
s2.append("B");
System.out.print(s1);
System.out.print(s2);
}
}
}
}.start();
new Thread() {
public void run() {
synchronized (s2) {
s2.append("C");
try {
Thread.sleep(45);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s1) {
s1.append("D");
System.out.print(s2);
System.out.print(s1);
}
}
}
}.start();
九、线程通信
1.主要API
① wait(): 令当前线程挂起并放弃CPU、同步资源,使别的线程可访问并修改共享资源,而当前线程排队等候再次对资源的访问
② notify(): 唤醒正在排队等待同步资源的线程中优先级最高者结束等待
③ notifyAll (): 唤醒正在排队等待资源的所有线程结束等待
2.经典案例之:生产者与消费者
class Clerk { //售货员
private int product = 0;
/*
锁 :this
*/
public synchronized void addProduct() {
if (product >= 20) {//如果生产的数量已经达到上限那么暂停生产(wait)
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {//数量没有达到上限可以生产,生产完毕必须通知消费者消费
product++;
System.out.println("生产者生产了第" + product + "个产品");
notifyAll();//通知消费赶紧消费
}
}
/*
锁 :this
*/
public synchronized void getProduct() {
if (this.product <= 0) {//如果生产的数量已经达到下限,那么消费者将暂停消费(wait())
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
System.out.println("消费者取走了第" + product + "个产品");
product--;
notifyAll();
}
}
}
public class TestProduct {
public static void main(String[] args) {
Clerk clerk = new Clerk();
new Thread(new Runnable() {
@Override
public void run() {
while(true) {
clerk.addProduct();
}
}
},"生产者").start();
new Thread(new Runnable() {
@Override
public void run() {
while(true) {
clerk.getProduct();
}
}
},"消费者").start();
}
}
十、创建线程的另外两种方式
1.实现Callable
//1.自定义一个类并实现Callable接口
class MyCallable implements Callable<String>{
//2.重写call方法
@Override
public String call() throws Exception {
//3.在call方法中去实现需要在分线程中实现的功能同时返回需要返回的内容
for (int i = 0; i < 50; i++) {
System.out.println(Thread.currentThread().getName() + "===" + i);
}
return "aaa";
}
}
public class CallableTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//4.创建Callable接口的实现类的对象
MyCallable mc = new MyCallable();
//5.创建FutureTask的对象并将Callable接口的实现类的对象作为实参传到FutureTask的构造器中
FutureTask<String> ft = new FutureTask<>(mc);
//6.创建Thread类的对象并将FutureTask的对象作为实参传到Thread类的构造器中
new Thread(ft).start();
//获取线程返回的内容
String info = ft.get();//调用此方法会 阻塞当前线程直到获取到返回结果才会继续向下执行。
System.out.println("info===" + info);
for (int i = 0; i < 50; i++) {
System.out.println(Thread.currentThread().getName() + "===" + i);
}
}
}
2.线程池
① 概念:
因为线程的创建和销毁会消耗大量的资源所以有了线程池。
线程池可以帮助我们去管理线程。
② 分类:
Executors.newCachedThreadPool():
创建一个可根据需要创建新线程的线程池(有空闲的线程直接可以使用,如果没有创建新的线程)
Executors.newFixedThreadPool(n):
创建一个可重用固定线程数的线程池
Executors.newSingleThreadExecutor() :
创建一个只有一个线程的线程池
Executors.newScheduledThreadPool(n):
创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
③ API:
void execute(Runnable command) : 执行任务/命令,没有返回值,一般用来执行Runnable
Future submit(Callable task): 执行任务,有返回值,一般又来执行Callable
void shutdown() : 关闭连接池
④ 举例:
//创建线程池
ExecutorService es = Executors.newCachedThreadPool();
//从线程池中调用线程
es.submit(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println(Thread.currentThread().getName() + "====" + i);
}
}
});
es.submit(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println(Thread.currentThread().getName() + "====" + i);
}
}
});