Skip to content

1 进程与线程

进程(Process):

每个独立运行的程序都对应一个进程。进程是资源分配的最小单位,占用独立的内存空间和系统资源

线程(Thread):

CPU调度和分派的基本单位,程序执行过程中的最小单元

例如:迅雷是个进程,里面的多个下载任务属于线程

二者区别

进程是资源分配的基本单位,线程是CPU调度的基本单位; 建立一个进程,CPU会分配独立的内存空间,同一进程中线程则共享进程中的数据,而进程之间是独立的内容空间; 一个进程可包含多个线程,线程依附于进程存在

主线程:main()方法即为主线程入口,是产生其他子线程的线程

2 多线程实现与启动

方法
public void start()启动线程
public static Thread currentThread()返回当前正在执行的线程对象的引用
public final boolean isAlive()测试线程是否处于活动状态
public final String getName()返回该线程名称
public final void setName(String name)设置线程名称
static void sleep(long millis)线程睡眠,使线程转到阻塞状态。millis设定睡眠的时间,以毫秒为单位。当睡眠结束,转为就绪(Runnable)状态。
static void yield()线程礼让,暂停当前正在执行的线程对象,把执行机会让给其他线程
void join()线程插队(线程加入),在当前线程中调用另一个线程对象的join()方法,当前线程转为阻塞状态,直到另一个线程运行结束,当前线程转为就绪状态。
void wait()线程等待,导致当前的线程等待,直到其他线程调用此对象的 notify()方法或notifyAll()方法唤醒
void notify()线程唤醒,唤醒在此对象监视器上等待的单个线程。如果有多个线程在此对象上等待,则会选择唤醒其中一个,选择是任意的。
void notifyAll()线程唤醒,唤醒在此对象监视器上等待的所有线程

wait()、notify()、notifyAll() 这3个方法是Object类的方法,针对的是对象

3个方法只能在同步(synchronized)方法或代码块中调用(持有对象锁),否则报IllegalMonitorStateException异常

2.1 继承Thread类

2.1.1 基本语法

java
修饰符 class 类名 extends Thread {  
    @Override
    public void run() {
        线程中的逻辑
    }  
}

启动线程:

线程对象名.start()

2.2.2 代码演示

java
// 自定义线程类
public class MyThread extends Thread {

	// 线程执行的核心逻辑,写在run方法里
	@Override
	public void run() {
		// TODO Auto-generated method stub
		for(int i = 0; i < 100; i++) {
//			System.out.println("子线程:" + i);
			// Thread.currentThread() 获取当前线程对象 getName() 获取线程名
			System.out.println(Thread.currentThread().getName() + ":" + i);
		}
	}
}
public class App {

	// main()方法程序的入口,也称为主线程
	public static void main(String[] args) {
		// TODO Auto-generated method stub

		System.out.println("主线程");
		
		// 多个线程"同时"执行
		
		// 创建线程对象
		MyThread thread = new MyThread();
		// 启动线程,并不一定会立即执行,start后先进入就绪状态,不一定能立马获取cpu资源
		thread.start();
		
		MyThread thread2 = new MyThread();
		thread2.start();
		
		System.out.println("主线程结束");
		
		
	}

}

2.2 实现Runnable接口

2.2.1 基本语法

java
public class 类名 implements Runnable {
    @Override
    public void run() {
        线程中的逻辑
    }  
}

调用

java
// 创建该类的实例作为参数传递给线程实例,然后启动:
Thread th = new Thread(new 实现Runnable接口的类());
th.start();

2.2.2 代码演示

java
public class MyRunnable implements Runnable {

	@Override
	public void run() {
		// TODO Auto-generated method stub
		
		for(int i = 0; i < 100; i++) {
			System.out.println(Thread.currentThread().getName() + ":" + i);
		}
	}
}
public class App2 {

	public static void main(String[] args) {
		// TODO Auto-generated method stub

		MyRunnable myRunnable = new MyRunnable();
		// Thread()的参数是Runnable对象
		Thread thread = new Thread(myRunnable);
		thread.start();
		
		Thread thread2 = new Thread(myRunnable);
		thread2.start();
		
		System.out.println("主线程结束");
		
	}

}

Thread和Runnable比较:

继承Thread类编写简单,可直接操作线程适用于单继承

实现Runnable接口避免单继承局限性,便于共享资源

2.2.3 使用匿名内部类写法

java
Thread thread3 = new Thread(new Runnable() {
			
    @Override
    public void run() {
        // TODO Auto-generated method stub
        for(int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }
});
thread3.start();

3 线程状态

3.1 线程的生命周期

Java中线程的生命周期包括6种状态:

新建 New

可运行 Runnable

阻塞 Blocked

等待 Waiting

计时等待 Timed wating

终止 Terminated

3.2 线程状态转换

1)新建状态

​ 使用new创建一个新的线程对象时,该线程处于创建状态。处于创建状态的线程只是一个空的线程对象,系统不为它分配资源。

2)可运行状态

​ 调用线程的start()方法,线程处于可运行状态(JDK5后,就绪和运行统称为可运行状态)。一个可运行的线程可能正在运行,也可能没有运行。

我们可以这样理解,线程调度依赖操作系统提供的服务,抢占式调度系统给每一个可运行的线程一个时间片来执行任务。调用start()后,线程被调度系统选中,此时处于就绪状态(ready);就绪状态的线程在获得CPU使用权后变为运行中状态(running)。

3)阻塞状态

​ 当一个线程试图获取一个内部对象锁,而这个锁被其他线程占用(使用了synchronzied),该线程就会被阻塞。当其他线程释放了这个锁,并且该线程持有了这个锁,该线程进入可运行状态。

4)等待状态

线程进入等待状态(比如wait()),需要等待另一个线程执行唤醒通知,否则会一直处于等待状态。

5)计时等待状态

有些方法有超时参数,调用这些方法,线程进入计时等待状态。

6)终止(Dead)

​ run()方法执行完成,或因异常退出,该线程结束

img

3.3 线程优先级

Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程。

优先级高的线程获得执行机会的概率更大。

Java线程的优先级用整数表示,取值范围是 1~10,

Thread 类有以下三个静态常量:

static int MAX_PRIORITY:最高优先级,取值为 10。

static int MIN_PRIORITY:最低优先级,取值为 1。

static int NORM_PRIORITY:默认优先级,取值为 5。

Thread类的setPriority()和getPriority()方法分别用来设置和获取线程的优先级。

4 线程调度

4.1 线程睡眠

时长单位: 毫秒

在哪个线程中调用该方法,哪个线程就会进入阻塞状态,睡眠时间到了,线程进入就绪状态

java
public class App3 {

	public static void main(String[] args) {
		
		System.out.println("启动主线程");
		
		MyThread2 t = new MyThread2();
		t.start();
		
		// 线程睡眠  时长单位: 毫秒
		// 线程将纳入阻塞状态,时间到了,进入就绪状态
		try {
			Thread.sleep(5000);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
		System.out.println("主线程结束");
	}
}
// 一个类文件中可以些多个类,但是只能有一个public类,而且public类名要和类文件同名
class MyThread2 extends Thread {
	@Override
	public void run() {
		// TODO Auto-generated method stub
		for(int i = 0; i < 100; i++) {
			if(i == 50) {
				try {
					// 在哪个线程中调用该方法,哪个线程就会休眠
					Thread.sleep(6000);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
			System.out.println(Thread.currentThread().getName() + ":" + i);
		}
	}
}

4.2 线程插队 (了解)

当前线程中,另一个线程对象调用join()方法

则,当前线程进入阻塞状态,

另一个线程进入执行状态

java
public class App4 {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		
		System.out.println("启动主线程");
		
		SubThread subThread = new SubThread();
		subThread.start();
		
		
		for(int i = 0; i < 1000; i++) {
			if(i == 100) {
				try {
					// 效果上来说,相当于执行插队操作
					// i == 500, 有可能main还占着cpu,某个线程调用join方法后,
					// 该线程相当于进行了插队,强制获取到了cpu的资源,原来的线程进入到阻塞状态
					// 本例中使用无参的join方法,这种情况,子线程逻辑全部执行完,主线程才会继续执行
					// subThread.join();
					// 带时间的方法,单位毫秒,表示阻塞指定时间后,结束阻塞
					subThread.join(1);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
			System.out.println(Thread.currentThread().getName() + ":" + i);
		}
		
//		for(int i = 0; i < 1000; i++) {
//			
//			System.out.println(Thread.currentThread().getName() + ":" + i);
//		}
		
		
	}

}

class SubThread extends Thread {
	
	@Override
	public void run() {
		// TODO Auto-generated method stub
		for(int i = 0; i < 500; i++) {
			System.out.println(Thread.currentThread().getName() + ":" + i);
		}
	}
}

4.3 线程礼让 (了解)

yield()是Thread中的静态方法

调用yield的线程进入就绪状态,其他线程获得执行机会

但是,执行yield后,只是提供了这样的可能性,并不一定能保证其他线程一定执行。因为执行yield的线程也会进入就绪状态,他们都会参与cpu资源的竞争

java
public class App5 {

	public static void main(String[] args) {
		// TODO Auto-generated method stub

		Producer producer = new Producer();
		Customer customer = new Customer();
		
		producer.start();
		customer.start();
		
	}

}

class Producer extends Thread {
	
	@Override
	public void run() {
		// TODO Auto-generated method stub
		for(int i = 0; i < 500; i++) {
			if(i == 200) {
				// 当前线程,进入就绪状态
				Thread.yield();
			}
			System.out.println(Thread.currentThread().getName() + ":" + i);
		}
	}
}

class Customer extends Thread {
	
	@Override
	public void run() {
		// TODO Auto-generated method stub
		for(int i = 0; i < 500; i++) {
			if(i == 200) {
				// 当前线程,进入就绪状态
				Thread.yield();
			}
			System.out.println(Thread.currentThread().getName() + ":" + i);
		}
	}
}

join()方法和yield()方法的主要区别是什么?

​ join 使当前线程进入阻塞状态

​ yield使当前线程进入就绪状态

5 线程同步

5.1 线程安全问题

1)操作同一银行账户

img

2)抢票大战

比如50个人抢100张票

java
public class App7 {

	public static void main(String[] args) {
		// TODO Auto-generated method stub

		Ticket ticket = new Ticket();
		for(int i = 0; i < 50; i++) {
			Thread t = new Thread(ticket);
			t.start();
		}

	}

}

class Ticket implements Runnable {

	// 共享的资源
	private int num = 100;
	
	@Override
	public void run() {
		// TODO Auto-generated method stub
		num--;
		System.out.println("剩余票数:" + num);
	}
	
}

当线程体内操作成员变量(共享数据),多个线程同时执行时,就可能出现多线程数据安全问题。

3)如何解决多线程中的数据安全问题

java中提供了一个同步机制(锁)。

即让操作共享数据的代码在某一时间段,只被一个线程执行(锁住),在执行过程中,其他线程不可以参与进来,这样共享数据就能同步了。

简单来说,就是给代码加把“锁”,而且,多个线程只能使用一把“锁”。

如何加锁呢?借助synchronized关键字

5.2 同步方法

synchronized位于方法声明中

java
public class App7 {

	public static void main(String[] args) {
		// TODO Auto-generated method stub

		Ticket ticket = new Ticket();
		for(int i = 0; i < 50; i++) {
			Thread t = new Thread(ticket);
			t.start();
		}

	}

}

class Ticket implements Runnable {

	// 共享的资源
	private int num = 100;
	
	public void setNum(int num) {
		this.num = num;
	}
	
	@Override
	public void run() {
		// TODO Auto-generated method stub
		show();
	}
	
	// 同步方法, synchronized修饰方法,相当于对方法加锁
	// 锁只能有一把,synchronized修饰的方法,用的锁是 this对象
	public synchronized void show() {
		num--;
		System.out.println("剩余票数:" + num);
	}
	
}

5.3 同步代码块

synchronized位于对象前,限定某一代码块

java
public class App {

	public static void main(String[] args) {
		// TODO Auto-generated method stub

		Ticket t = new Ticket();
		for(int i = 0; i < 50; i++) {
			Thread th = new Thread(t);
			th.start();
		}
	}

}

class Ticket implements Runnable {

	private int num = 100;
	
	@Override
	public void run() {
		// TODO Auto-generated method stub
		// 同步代码块 synchronized(表示锁的对象) {} ,
        // 多个Thread使用同一个Runnable对象时,"锁"通常可以使用this
		synchronized (this) {
		// Ticket.class,返回Class类型的对象,当前先了解一下
//		synchronized (Ticket.class) {
			num--;
			System.out.println("剩余票数:" + num);
		}
	}
	
}

5.4 同步机制的优劣

同步的好处:

同步的出现解决了多线程的安全问题。

同步的弊端:

当线程相当多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率。(性能和线程安全不可兼得)

6 线程池

6.1 什么是线程池

线程池是指在初始化一个多线程应用程序过程中创建一个线程集合,然后在需要执行新的任务时重用这些线程而不是新建一个线程(提高线程复用,减少性能开销)。

线程池中的每个线程都有被分配一个任务,一旦任务已经完成了,线程回到池子中然后等待下一次分配任务。

img

6.2 Executors工厂类创建线程池 了解

newFixedThreadPool()定长线程池池中包含固定数目的线程,空闲线程一直保留。只有核心线程,线程数量固定,任务队列为LinkedBlockingQueue
newScheduledThreadPool()定时线程池用于调度执行的固定线程池。执行定时或周期性任务。核心线程数量固定,非核心线程数量无限,执行完闲置 10ms 后回收,任务队列为DelayedWorkQueue
newCachedThreadPool()可缓存线程池无核心线程,非核心线程数量无限,执行完闲置 60s 后回收,任务队列为SynchronousQueue。
newSingleThreadExecutor()单线程化线程池只有一个线程的池,会顺序执行提交的任务。只有 1 个核心线程,无非核心线程,任务队列为LinkedBlockingQueue

FixedThreadPool 和 SingleThreadExecutor:主要问题是堆积的请求处理队列均采用 LinkedBlockingQueue,可能会耗费非常大的内存,甚至 OOM。

CachedThreadPool 和 ScheduledThreadPool:主要问题是线程数最大数是 Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至 OOM。

线程池执行任务时,可以采用两种方法:

execute(): 没有返回值,无法判断任务是否执行成功

submit():会返回Future对象,通过该对象判断任务是否执行成功

java
package com.glls.threadpool;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
 */
public class Executors01 {

	public static void main(String[] args) {
		// TODO Auto-generated method stub

		schedule3();
	}
	
	public static void fixed() {
		ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(3);
		newFixedThreadPool.execute(new Runnable() {
			
			@Override
			public void run() {
				// TODO Auto-generated method stub
				System.out.println("haha");
			}
		});
	}
	
	public static void cache() {
		ExecutorService cachedPool = Executors.newCachedThreadPool();
		cachedPool.execute(new Runnable() {
			
			@Override
			public void run() {
				// TODO Auto-generated method stub
				System.out.println("haha");
			}
		});
	}
	
	public static void single() {
		ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();
		newSingleThreadExecutor.execute(new Runnable() {
			
			@Override
			public void run() {
				// TODO Auto-generated method stub
				System.out.println("houhou");
			}
		});
	}
	
	public static void schedule() {
		ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(3);
		// 延迟指定时间执行
		newScheduledThreadPool.schedule(new Runnable() {
			
			@Override
			public void run() {
				// TODO Auto-generated method stub
				System.out.println("houhouhouhou");
			}
		}, 5, TimeUnit.SECONDS);
	}
	
	public static void schedule2() {
		ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(3);
		
		// 延迟指定时间后,间隔固定时间执行任务
		// 比如延迟2秒,间隔5秒执行任务
		// 指的是“以固定的频率”执行,period(周期)指的是两次成功执行之间的时间。
		// 上一个任务开始的时间计时,一个period后,检测上一个任务是否执行完毕,
		// 如果上一个任务执行完毕,则当前任务立即执行,如果上一个任务没有执行完毕,则需要等上一个任务执行完毕后立即执行
		// 当前任务执行时间大于等于间隔时间,任务执行后立即执行下一次任务。相当于连续执行了。
		newScheduledThreadPool.scheduleAtFixedRate(new Runnable() {
			
			@Override
			public void run() {
				// TODO Auto-generated method stub
				System.out.println("hehehehe");
			}
		}, 2, 5, TimeUnit.SECONDS);
	}
	
	public static void schedule3() {
		ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(3);
		
		// 延迟指定时间后,间隔固定时间执行任务
		// 指的是“以固定的延时”执行,delay(延时)指的是一次执行终止和下一次执行开始之间的延迟
		newScheduledThreadPool.scheduleWithFixedDelay(new Runnable() {
			
			@Override
			public void run() {
				// TODO Auto-generated method stub
				System.out.println("hehehehe");
			}
		}, 2, 5, TimeUnit.SECONDS);
	}

}

6.3 ThreadPoolExecutor创建线程池

创建线程池的推荐方式

1)相关构造方法

java
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue){}

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory) {}
 
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          RejectedExecutionHandler handler) {}
 
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {}
corePoolSize核心线程数。默认情况下,核心线程会一直存活,但是当将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收
maximumPoolSize线程池所能容纳的最大线程数。
keepAliveTime线程闲置超时时长。如果超过该时长,非核心线程就会被回收。如果将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收
unit指定 keepAliveTime 参数的时间单位。常用的有:TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分)
workQueue任务队列。通过线程池的 execute() 方法提交的 Runnable 对象将存储在该参数中。其采用阻塞队列实现。
threadFactory线程工厂。用于指定为线程池创建新线程的方式
handler拒绝策略。当达到最大线程数时需要执行的策略

2)任务队列(workQueue)

任务队列是基于阻塞队列实现的,即采用生产者消费者模式,在 Java 中需要实现 BlockingQueue 接口。常见的阻塞队列如下:

ArrayBlockingQueue一个由数组结构组成的有界阻塞队列(数组结构可配合指针实现一个环形队列)
LinkedBlockingQueue一个由链表结构组成的有界阻塞队列,在未指明容量时,容量默认为 Integer.MAX_VALUE
PriorityBlockingQueue一个支持优先级排序的无界阻塞队列,对元素没有要求,可以实现 Comparable 接口也可以提供 Comparator 来对队列中的元素进行比较。跟时间没有任何关系,仅仅是按照优先级取任务
DelayQueue类似于PriorityBlockingQueue,是二叉堆实现的无界优先级阻塞队列。要求元素都实现 Delayed 接口,通过执行时延从队列中提取任务,时间没到任务取不出来
SynchronousQueue一个不存储元素的阻塞队列,消费者线程调用 take() 方法的时候就会发生阻塞,直到有一个生产者线程生产了一个元素,消费者线程就可以拿到这个元素并返回;生产者线程调用 put() 方法的时候也会发生阻塞,直到有一个消费者线程消费了一个元素,生产者才会返回

3)拒绝策略(handler)

当线程池的线程数达到最大线程数时,需要执行拒绝策略。拒绝策略需要实现 RejectedExecutionHandler 接口,并实现 rejectedExecution(Runnable r, ThreadPoolExecutor executor) 方法。Executors 框架已经为我们实现了 4 种拒绝策略:

AbortPolicy(默认)丢弃任务并抛出 RejectedExecutionException 异常。
CallerRunsPolicy由调用线程处理该任务
DiscardPolicy丢弃任务,但是不抛出异常。可以配合这种模式进行自定义的处理方式
DiscardOldestPolicy丢弃队列最早的未处理任务,然后重新尝试执行任务
java
package com.glls.threadpool;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 *
 */
public class Executors02 {

	public static void main(String[] args) {
		// TODO Auto-generated method stub

		Task task = new Task();
        // 创建线程池对象
		ThreadPoolExecutor pool = new ThreadPoolExecutor(
				3, 
				10, 
				1000, 
				TimeUnit.MILLISECONDS, 
				new SynchronousQueue<Runnable>(),
				Executors.defaultThreadFactory(),
				new ThreadPoolExecutor.AbortPolicy());
        for(int i = 0; i < 3; i++) {
            pool.execute(task);
        }   
	}
}

class Task implements Runnable{
    
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}

6.4 线程池任务调度流程

img

附录

操作系统层面线程的状态

5种状态

创建、就绪、运行、阻塞、终止

img

img

单例设计模式

单例,即只有一个实例,该模式的作用是保证程序中某个类的对象只有一个。

在程序运行过程中,某些类,在程序中只需要存在一个对象实例就可以满足需求,不必每次请求都创建新的对象。此时,可以使用单例模式。

比如:存储程序的配置信息的类

懒汉式

java
public class Singleton{
	private static Singleton singleton = null;
	private Singleton(){}

	 // 确保线程同步
	public synchronized static Singleton getInstance(){		
		if(singleton == null) {
			singleton = new Singleton();
        }
		return singleton;
	}
}

饿汉式

java
public class Singleton{
    // 类加载的时候进行初始化
    private static final Singleton singleton = new Singleton();
    private Singleton (){}

    public static Singleton getInstance(){
        return singleton;
    }
}