基本概念
程序 (program) 是为完成特定任务、用某种语言编写的一组指令的集合 。即指 一段静态的代码,静态对象。
进程 (process) 是程序的一次执行过程,或是正在运行的一个程序。是 一个动态的过程 :有它自身的产生、存在和消亡的过程 。——生命周期
- 程序是静态的,进程是动态的
- 进程作为资源分配的单位, 系统在运行时会为每个进程分配不同的内存区域
线程 (thread),进程可进一步细化为线程,是一个程序内部的一条执行路径。
- 若 一个进程同一时间并行执行多 个线程,就是支持多线程的
- 线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器 ( pc),线程切换的开销小
- 一个进程中的多个线程共享相同的内存单元 / 内存地址空间—>它们从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来==安全的隐患==。
- 一个 Java 应用程序 java.exe ,其实至少有三个线程 main() 主线程, gc()垃圾回收线程,异常处理线程。当然如果发生异常,会影响主线程 。
并行与并发:
- 并行: 多 个 CPU 同时执行多个任务。比如:多个人同时做不同的事 。
- 并发: 一 个 CPU (采用时间片) 同时执行多个任务。比如:秒杀、多个人做同一件事。
使用多线程的优点
背景:以单核 CPU 为例, 只使用单个线程先后完成多个任务(调用多个方法),肯定比用多个线程来完成用的时间更短,为何仍需多线程呢?
多线程程序的优点:
- 提高应用程序的响应。对图形化界面更有意义,可增强用户体验。
- 提高计算机系统 CPU 的利用率
- 改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改
何时需要多线程:
- 程序需要同时执行两个或多个任务。
- 程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等。
- 需要一些后台运行的程序时。
线程的创建和使用
java.lang.Thread
Thread 类的特性
- 构造器
- Thread() 创建新的Thread 对象
- Thread(String threadname) 创建线程并指定线程实例名
- Thread(Runnable target) 指定创建线程的目标对象,它实现了Runnable 接口中的run方法
- Thread(Runnable target, String name) 创建新的 Thread 对象
- 每个线程都是通过某个特定 Thread 对象的 run() 方法来完成操作的,经常把 run() 方法的主体称为线程体
- 通过该 Thread 对象的 start() 方法来启动这个线程,而非直接调用 run()
- Thread类的有关方法
- void start(): 启动线程,并执行对象的 run() 方法
- run(): 线程在被调度时执行的操作
- String getName(): 返回线程的名称
- void setName (String) 设置该线程名称,也可用带参构造器设置线程名字
- static Thread currentThread(): 静态方法,返回当前线程 。在Thread子类中就是this ,通常用于主线程和 Runnable 实现类
- static void yield():线程让步;暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程;若队列中没有同优先级的线程,忽略此方法。
- 当某个程序执行流(如主线程)中调用其他线程(如子线程)的 join() 方法时,调用线程(主线程)将被阻塞,直到 join() 方法加入的 join 线程(子线程)执行完为止(子线程执行完毕再返回主线程);低优先级的线程也可以获得执行。
- static void sleep(long milli): 指定时间毫秒;令当前活动线程在指定时间段内放弃对 CPU 控制,使其他线程有机会被执行,时间到后重排队。抛出 InterruptedException 异常
- stop(): 强制线程生命期结束,不推荐使用
- boolean isAlive(): 返回 boolean ,判断线程是否还活着
- wait() 令当前线程挂起并放弃 CPU 、 同步资源并等待, 使别的线程可访问并修改共享资源,而当前线程排队等候其他线程调用 notify() 或 notifyAll() 方法唤醒,唤醒后等待重新获得对监视器的所有权后才能继续执行。
创建方式一:继承Thread类
- 创建继承于Thread的子类
- 重写run()方法,将此线程执行的操作声明在run()中
- 创建子类的对象
- 通过此对象调用start()方法:启动线程,并自动调用run()方法
注意点:
- 如果 自己手动调用 run() 方法,那么就只是普通方法,没有启动多线程模式。
- run 方法由 JVM 调用,什么时候调用,执行的过程控制都有操作系统的 CPU
调度决定 。 - 想要启动多线程,必须调用 start 方法 。
- 一 个线程对象只能调用一次 start() 方法启动,如果重复调用了,则将抛出的异常“ “IllegalThreadStateException”。
线程的优先级、调度
线程的优先级等级:
- MAX_PRIORITY:10
- MIN PRIORITY:1
- NORM_PRIORITY:5 默认优先级
- getPriority() 返回线程优先值
- setPriority(int newPriority) 改变线程的优先级
- 线程创建时继承父线程的优先级
- 低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用
调度策略:
时间片
抢占式:高优先级的线程抢占CPU;高优先级的线程高概率的情况下被执行,并不意味高优先级的线程执行完后再执行低优先级的线程。
Java 的调度方法:
- 同优先级线程组成先进先出队列(先到先服务),使用时间片策略
- 对高优先级,使用优先调度的抢占式策略
创建方式二:实现 Runnable 接口
- 定义子类 ,实现 Runnable 接口。
- 子类中重写 Runnable 接口中的 run 方法。
- 通过 Thread 类含参构造器创建线程对象。
- 将 Runnable 接口的子类对象作为实际参数传递给 Thread 类的构造器中 。
- 调用 Thread 类的 start 方法:开启线程调用,Runnable 子类接口的 run 方法。
比较两种创建方式
开发中,优先选择实现Runnable接口的方式;
- 没有类的单继承的局限性
- 更适合来处理多个线程有共享数据的情况
两者联系:Thread也是 implements Runnable;都需要重写run方法
补充:线程的分类:
Java
中的线程分为两类:一种是 守护线程 ,一种是 用户线程 。
- 它们在几乎每个方面都是相同的,唯一的区别是判断 JVM 何时离开。
- 守护线程是用来服务用户线程的,通过在 start() 方法前调用thread.setDaemon (true 可以把一个用户线程变成一个守护线程。
- Java 垃圾回收就是一个典型的守护线程。
- 若 JVM 中都是守护线程,当前 JVM 将 退出 。
- 形象理解: 兔死狗烹,鸟尽弓藏
线程的生命周期
要
想实现 多 线程 必须在主线程中创建新的线程对象 。 Java 语言使用 Thread 类
及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态:
- 新建: 当 一个 Thread 类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
- 就绪: 处于新建状态的线程被 start() 后,将进入线程队列等待 CPU 时间片,此时它已具备了运行的条件 ,只是没分配到 CPU 资源
- 运行: 当就绪的线程被调度并获得 CPU 资源时便进入运行状态, run() 方法定义了线程的操作和功能
- 阻塞: 在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中止自己的执行,进入阻塞状态
- 死亡: 线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束
线程的同步
当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误 。
解决办法:对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。
方式一:同步代码块
synchronized(同步监视器){
// 需要同步的代码,即操作共享数据的代码
}
同步监视器:俗称“锁”。任何一个类的对象都可以充当锁
要求多个线程==必须共用同一把锁==
/* 继承Thread类的方式,需要把锁声明为static,因每个子线程均new一个线程对象;或者使用 当前类 的 对象 实现Runnable的方式,每个子线程共用同一个Runnable实现类;或者使用this */ class Window2 extends Thread{ private static int ticket = 100; private static Object obj = new Object(); @Override public void run() { while(true){ //正确的 //synchronized (obj){ synchronized (Window2.class){ // Class clazz = Window2.class,Window2.class只会加载一次 // 类是一个对象 window1.class只加载一次,也是唯一的 // 错误的方式:this代表着t1,t2,t3三个对象 // synchronized (this){ if(ticket > 0){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(getName() + ":卖票,票号为:" + ticket); ticket--; }else{ break; } } } } } public class WindowTest2 { public static void main(String[] args) { Window2 t1 = new Window2(); Window2 t2 = new Window2(); Window2 t3 = new Window2(); t1.setName("窗口1"); t2.setName("窗口2"); t3.setName("窗口3"); t1.start(); t2.start(); t3.start(); } }
class Window1 implements Runnable{ private int ticket = 100; Object obj = new Object(); @Override public void run() { while(true){ synchronized (this){ // 此时的this:唯一的Window1的对象 { if (ticket > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket); ticket--; } else { break; } } } } } public class WindowTest1 { public static void main(String[] args) { Window1 w = new Window1(); Thread t1 = new Thread(w); Thread t2 = new Thread(w); Thread t3 = new Thread(w); t1.setName("窗口1"); t2.setName("窗口2"); t3.setName("窗口3"); t1.start(); t2.start(); t3.start(); } }
方式二:同步方法
// synchronized 还可以放在方法声明中,表示整个方法为同步方法
public synchronized void show (String name){
…
}
implements Runnable的方式
class Window3 implements Runnable {
private int ticket = 100;
@Override
public void run() {
while (true) {
show();
}
}
private synchronized void show(){ //同步监视器:this
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
ticket--;
}
}
}
继承Thread的方法
同步方式要求是==静态==的
class Window4 extends Thread {
private static int ticket = 100;
@Override
public void run() {
while (true) {
show();
}
}
private static synchronized void show(){//同步监视器:Window4.class
//private synchronized void show(){ //同步监视器:t1,t2,t3。此种解决方式是错误的
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
ticket--;
}
}
}
同步的方式解决了线程的安全问题;但操作同步代码时,只能有一个线程参与,其他线程等待,相当于一个单线程的过程,效率低.
同步方法仍然涉及同步监视器,只是不需要显式地声明;非静态的同步方法,同步监视器为this;静态的同步方法,同步监视器为 当前类本身 (类.class)
使用同步机制将单例模式中的懒汉式改写为线程安全的:
class Bank{
private Bank(){}
private static Bank instance = null;
public static Bank getInstance(){
//方式一:效率稍差
// synchronized (Bank.class) {
// if(instance == null){
// instance = new Bank();
// }
// return instance;
// }
//方式二:效率更高
if(instance == null){
synchronized (Bank.class) {
if(instance == null){
instance = new Bank();
}
}
}
return instance;
}
}
死锁
- 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程 的 死锁
- 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于
阻塞状态,无法继续
//死锁的演示
class A {
public synchronized void foo(B b) { //同步监视器:A类的对象:a
System.out.println("当前线程名: " + Thread.currentThread().getName()
+ " 进入了A实例的foo方法"); // ①
// try {
// Thread.sleep(200);
// } catch (InterruptedException ex) {
// ex.printStackTrace();
// }
System.out.println("当前线程名: " + Thread.currentThread().getName()
+ " 企图调用B实例的last方法"); // ③
b.last();
}
public synchronized void last() {//同步监视器:A类的对象:a
System.out.println("进入了A类的last方法内部");
}
}
class B {
public synchronized void bar(A a) {//同步监视器:b
System.out.println("当前线程名: " + Thread.currentThread().getName()
+ " 进入了B实例的bar方法"); // ②
// try {
// Thread.sleep(200);
// } catch (InterruptedException ex) {
// ex.printStackTrace();
// }
System.out.println("当前线程名: " + Thread.currentThread().getName()
+ " 企图调用A实例的last方法"); // ④
a.last();
}
public synchronized void last() {//同步监视器:b
System.out.println("进入了B类的last方法内部");
}
}
public class DeadLock implements Runnable {
A a = new A();
B b = new B();
public void init() {
Thread.currentThread().setName("主线程");
// 调用a对象的foo方法
a.foo(b);
System.out.println("进入了主线程之后");
}
public void run() {
Thread.currentThread().setName("副线程");
// 调用b对象的bar方法
b.bar(a);
System.out.println("进入了副线程之后");
}
public static void main(String[] args) {
DeadLock dl = new DeadLock();
new Thread(dl).start();
dl.init();
}
}
/*
当前线程名: 主线程 进入了A实例的foo方法
当前线程名: 主线程 企图调用B实例的last方法
进入了B类的last方法内部
进入了主线程之后
当前线程名: 副线程 进入了B实例的bar方法
当前线程名: 副线程 企图调用A实例的last方法
进入了A类的last方法内部
进入了副线程之后
*/
// 取消注释后
/*
当前线程名: 主线程 进入了A实例的foo方法
当前线程名: 副线程 进入了B实例的bar方法
当前线程名: 副线程 企图调用A实例的last方法
当前线程名: 主线程 企图调用B实例的last方法 // 这里卡住了
*/
Lock (锁)
从 JDK 5.0 开始 Java 提供了更强大的线程同步机制,通过显式定义同步锁对象来实现同步。同步锁使用 Lock 对象充当 。
java.util.concurrent.locks.Lock 接口是控制多个线程对共享资源进行访问的工具。 锁提供了对共享资源的独占访问,每次只能有一个线程对 Lock 对象加锁,==线程开始访问共享资源之前应先获得 Lock 对象==。
ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义, 在实现线程安全的控制中,比较常用的是 ReentrantLock 可以显式加锁、释放锁。对多个线程,lock应是唯一的。
class A {
private final ReentrantLock lock = new ReenTrantLock();
// 对多个线程应是唯一的
public void m(){
lock.lock();
try{
//保证线程安全的代码
}
finally{
lock.unlock();
}
}
}
// 注意:如果同步代码有异常,要将unlock() 写入 finally 语句块
synchronized 与 Lock 的对比
- Lock 是显式锁(手动开启和关闭锁,别忘记关闭锁), synchronized 是隐式锁,出了作用域自动释放
- Lock 只有代码块锁, synchronized 有代码块锁和方法锁
- 使用 Lock 锁, JVM 将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
- 优先使用顺序:① Lock ②同步代码块(已经进入了方法体,分配了相应资源) ③同步方法(在方法体之外)
线程的通信
class Number implements Runnable{
private int number = 1;
private Object obj = new Object();
@Override
public void run() {
while(true){
synchronized (obj) {
obj.notify();
// notify()唤醒正在排队等待同步资源的线程中优先级最高者结束等待
// notifyAll()唤醒正在排队等待资源的所有线程结束等待
if(number <= 100){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + number);
number++;
try {
//使得调用如下wait()方法的线程进入阻塞状态
//自动释放锁
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
break;
}
}
}
}
}
public class CommunicationTest {
public static void main(String[] args) {
Number number = new Number();
Thread t1 = new Thread(number);
Thread t2 = new Thread(number);
t1.setName("线程1");
t2.setName("线程2");
t1.start();
t2.start();
}
}
wait () 与 notify() 和 notifyAll()
- 这三个方法只有在 synchronized 方法或 synchronized 代码块中才能使用
- 这三个方法的调用者必须是同步代码块或同步方法的同步监视器,否则会报java.lang.IllegalMonitorStateException 异常。
- 因为这三个方法必须由锁对象调用,而任意对象都可以作为 synchronized 的同步锁,因此这三个方法只能在 Object 类中声明 。
sleep()和wait()方法异同
相同点:都可以使当前线程进入阻塞状态
不同:
a. 两个方法声明的位置不同,Thread类中声明sleep()方法;Object类中声明wait()方法。
b. 调用范围不同,sleep()可以在任何场景下调用;wait()必须使用在同步代码块或同步方法中
c. sleep不会释放锁,wait释放锁
JDK5.0新增线程的创建方式
新增方式一:实现Callable 接口
- 创建一个实现Callable的实现类
- 重写call方法,将此线程需要执行的操作声明在call()中
- 创建Callable接口实现类的对象
- 将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
- 将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
- 获取Callable中call方法的返回值。get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。
//1.创建一个实现Callable的实现类
class NumThread implements Callable{
//2.实现call方法,将此线程需要执行的操作声明在call()中
@Override
public Object call() throws Exception {
int sum = 0;
for (int i = 1; i <= 100; i++) {
if(i % 2 == 0){
System.out.println(i);
sum += i;
}
}
return sum;
}
}
public class ThreadNew {
public static void main(String[] args) {
//3.创建Callable接口实现类的对象
NumThread numThread = new NumThread();
//4.将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
FutureTask futureTask = new FutureTask(numThread);
//5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
new Thread(futureTask).start();
try {
//6.获取Callable中call方法的返回值
//get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。
Object sum = futureTask.get();
System.out.println("总和为:" + sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
如何理解实现Callable接口的方式创建多线程比实现Runnable接口创建多线程方式强大:
- call()可以有返回值
- call()可以抛出异常,被外面的操作捕获,获取异常的信息
- Callable是支持泛型的
新增方式二:使用线程池
背景: 经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大 。
思路: 提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复 利用。类似生活中的公共交通工具。
好处:
- 提高响应速度 (减少了创建新线程的时间)
- 降低资源消耗 (重复利用线程池中线程,不需要每次都创建)
- 便于线程管理
- corePoolSize :核心池的 大小
- maximumPoolSize :最大线程数
- keepAliveTime :线程没有任务时最多保持多长时间后会终止
JDK 5.0 起提供了线程池相关 API:ExecutorService 和 Executors
ExecutorService :真正的线程池接口。常见子类 ThreadPoolExecutor
- void execute(Runnable command) :执行任务 命令,没有返回值,一般用来执行Runnable
Future submit(Callable task):执行任务,有返回值,一般又来执行Callable - void shutdown() :关闭连接池
Executors :工具类、线程池的工厂类,用于创建并返回不同类型的线程池
- Executors.newCachedThreadPool ():创建一个可根据需要创建新线程的线程池
- Executors.newFixedThreadPool(n); 创建一个可重用固定线程数的线程池
- Executors.newSingleThreadExecutor () :创建一个只有一个线程的线程池
- Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运
行命令或者定期地执行。
class NumberThread implements Runnable{
@Override
public void run() {
for(int i = 0;i <= 100;i++){
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
}
class NumberThread1 implements Runnable{
@Override
public void run() {
for(int i = 0;i <= 100;i++){
if(i % 2 != 0){
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
}
public class ThreadPool {
public static void main(String[] args) {
//1. 提供指定线程数量的线程池
ExecutorService service = Executors.newFixedThreadPool(10);
ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
//设置线程池的属性
// System.out.println(service.getClass());
// service1.setCorePoolSize(15);
// service1.setKeepAliveTime();
//2.执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
service.execute(new NumberThread());//适合适用于Runnable
service.execute(new NumberThread1());//适合适用于Runnable
// service.submit(Callable callable);//适合使用于Callable 返回值可以用FutureTask接收
//3.关闭连接池
service.shutdown();
}
}