多线程基础

本文介绍了多线程的基础知识,包括线程的优点、创建线程的三种方式、线程常用方法如start()、join()、interrupt()、wait()、notify()。还探讨了线程安全问题,如synchronized和volatile关键字的作用,以及单例模式在多线程环境下的实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

认识线程

  • 进程:操作系统中一个程序的执行周期,进程是系统分配资源的最小单位
  • 线程:一个程序同时执行多个任务,每个任务就称为一个线程。线程是系统调度的最小单位。
  • 一个进程内的线程之间是可以共享资源的。
    每个进程至少有一个线程存在,即主线程
进程线程
根本区别进程是操作系统资源分配的基本单位线程是任务调度和执行的基本单位
开销方面每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
所处环境在操作系统中能同时运行多个进程(程序)而在同一个进程(程序)中有多个线程同时执行(通过CPU调度,在每个时间片中只有一个线程执行)
内存分配系统在运行的时候会为每个进程分配不同的内存空间除了CPU外,系统不会为线程分配内存(线程所使用的资源来自其所属进程的资源),线程组之间只能共享资源。
包含关系没有线程的进程可以看做是单线程的,如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。
线程优点:
  1. 创建一个新线程的代价要比创建一个新进程小得多
  2. 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
  3. 线程占用的资源要比进程少很多
  4. 能充分利用多处理器的可并行数量
  5. 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
  6. 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
  7. I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作
多线程的实现
  1. 继承Thread类来实现多线程:java.long.Thread是线程操作的核心类,新建线程直接继承Thread,然后覆写run()方法。

优点:可以直接调用start方法启动线程
缺点:java只能单继承,如果已经有了父类,不能用这种方法

class MyThread extends Thread {
	@Override
	public void run() {
	System.out.println("这里是线程运行的代码");
	}
}
MyThread t = new MyThread();
t.start(); // 线程开始运行

2.实现Runnable接口来实现多线程
通过实现 Runnable 接口,并且调用 Thread 的构造方法时将 Runnable 对象作为 target 参数传入来创建线程对象。
该方法的好处是可以规避类的单继承的限制;但需要通过 Thread.currentThread() 来获取当前线程的引用

优点:即使自己定义的线程类有了父类也可以实现接口,而且接口是多实现
缺点:需通过构造一个Thread把自己传进去,才能实现Thread的方法,代码复杂

class MyRunnable implements Runnable {
	@Override
	public void run() {
	System.out.println(Thread.currentThread().getName() + "这里是线程运行的代码");
	}
}
Thread t = new Thread(new MyRunnable());
t.start(); // 线程开始运行

3.实现Callable接口,需要重写call()方法

优点:可以抛出异常,有返回值
缺点:只有jdk1.5以后才支持,结合FuntureTask和Thread类一起使用,最后调用start启动线程

一般用第二种,实现Runnable接口,比较方便,扩展性高

线程常用方法

线程命名与取得
Thread t1 = new Thread();//创建线程对象
Thread t2 = new Thread(new MyRunnable());//使用 Runnable 对象创建线程对象
Thread t3 = new Thread("这是我的名字");//创建线程对象,并命名
Thread t4 = new Thread(new MyRunnable(), "这是我的名字");//使用 Runnable 对象创建线程对象,并命名
public final String getName()//取得线程名称
public final synchronized void setName(String name)//在创建线程之后设定线程名称
启动一个线程-start()

通过覆写 run 方法创建一个线程对象,但线程对象被创建出来并不意味着线程就开始运行了。
区别run()相当于线程的任务处理逻辑的入口方法,它由Java虚拟机在运行相应线程时直接调用,而不是由应用代码进行调用。
而start()的作用是启动相应的线程。启动一个线程实际是请求Java虚拟机运行相应的线程,而这个线程何时能够运行是由线程调度器决定的。start()调用结束并不表示相应线程已经开始运行,这个线程可能稍后运行,也可能永远也不会运行。
总结:调用start方法方可启动线程,而run方法只是thread类中的一个普通方法调用,还是在主线程里执行。

举个例子来看:

public static void main(String args[]) {
        Thread t = new Thread() {
            public void run() {
                pong();
            }
        };
        t.start();
        System.out.print("ping");
    }
 
    static void pong() {
        System.out.print("pang");
    }
    //结果为pingpang

t.start(); 该行代码相当于是启动线程,

public static void main(String args[]) {
        Thread t = new Thread() {
            public void run() {
                pong();
            }
        };
        t.run();
        System.out.print("ping");
    }
 
    static void pong() {
        System.out.print("pang");
    }
    //结果为pangping

t.run(); 该行代码相当于是使用t这个类中的run方法.

中断一个线程

1.通过共享的标记来进行沟通

2.调用 interrupt() 方法来通知(通知收到的更及时,即使线程正在 sleep 也可以马上收到)

  1. 通过 thread 对象调用 interrupt() 方法通知该线程停止运行
  2. thread 收到通知的方式有两种:

a. 如果线程调用了 wait/join/sleep 等方法而阻塞挂起,则以 InterruptedException异常的形式通知,清除中断标志.
b.否则,只是内部的一个中断标志被设置,thread 可以通过
Thread.interrupted() 判断当前线程的中断标志被设置,清除中断标志
Thread.currentThread().isInterrupted() 判断指定线程的中断标志被设置,不清除中断标志

等待一个线程-join()

线程间通信的一种方式,等待其他线程终止。join()会释放对象锁,如果主线程调用该方法,会让主线程休眠,让调用该方法的线程执行完毕后在恢复执行主线程。

获取当前线程引用

public static Thread currentThread();返回当前线程对象的引用

public class ThreadDemo {
	public static void main(String[] args) {
	Thread thread = Thread.currentThread();
	System.out.println(thread.getName());
	}
}
休眠当前线程

线程休眠:当前线程暂缓执行,等到预计时间后再回复执行。
因为线程的调度是不可控的,所以,这个方法只能保证休眠时间是大于
等于休眠时间的。
public static void sleep(long millis) throws InterruptedException 休眠当前线程 millis 毫秒。
线程休眠会立刻交出CPU,但不会释放锁。

public class ThreadDemo {
	public static void main(String[] args) throws InterruptedException {
	System.out.println(System.currentTimeMillis());
	Thread.sleep(3 * 1000);
	System.out.println(System.currentTimeMillis());
	}
}

多线程带来的的风险-线程安全

线程安全概念:

如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。

线程不安全的原因:
  1. 原子性::即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
  2. 可见性:可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
  3. 有序性:即程序执行的顺序按照代码的先后顺序执行

synchronized 关键字

作用:在多线程的环境下,控制synchronized代码段不被多个线程同时执行。synchronized既可以加在一段代码上,也可以加在方法上。

  • 同步代码块:在方法中使用synchronized(对象),一般可以锁定当前对象this,表示同一时刻只有一个线程能够进入同步代码块,但多个线程可同时进入方法。
  • 同步方法:在方法生命上加synchronized,表示此时只有一个线程进入同步方法。

使用

  • 对于同步方法,即修饰某个函数,这个时候锁对象是当前类的实例化对象
  • 对于同步静态方法,这个时候锁是当前类的Class对象
  • 对于同步代码块,锁是括号里的对象

volatile 关键字

修饰范围:只能是变量

  • volatile可以使变量在多个线程间可见。
  • 强制从公共堆栈中取得变量的值,而不是从线程私有数据栈中获取数据。
  • 增加了实例变量在多个线程之间的可见性,但是不支持原子性。所以不能保证线程安全。

通信-对象的等待集wait set

1.wait()的作用是让当前线程进入等待状态,同时,wait()也会让当前线程释放它所持有的锁。“直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法”,当前线程被唤醒(进入“就绪状态”)
2.notify()和notifyAll()的作用,则是唤醒当前对象上的等待线程;notify()是唤醒单个线程,而notifyAll()是唤醒所有的线程。
3.wait(long timeout)让当前线程处于“等待(阻塞)状态”,“直到其他线程调用此对象的notify()方法或 notifyAll() 方法,或者超过指定的时间量”,当前线程被唤醒(进入“就绪状态”)。

wait()方法

其实wait()方法就是使线程停止运行。从运行态回到阻塞态。
特点

  1. 方法wait()的作用是使当前执行代码的线程进行等待,wait()方法是Object类的方法,该方法是用来将当前线程置入“预执行队列”中,并且在wait()所在的代码处停止执行,直到接到通知或被中断为止。
  2. wait()方法只能在同步方法中或同步块中调用。如果调用wait()时,没有持有适当的锁,会抛出异常。
  3. wait()方法执行后,当前线程释放锁,线程与其它线程竞争重新获取锁
notify()方法

notify方法就是使停止的线程继续运行。

  1. 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。如果有多个线程等待,则有线程规划器随机挑选出一个
    呈wait状态的线程。
  2. 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。
notifyAll()方法

notifyAll方法可以一次唤醒所有的等待线程

wait和sleep对比
  1. wait 之前需要请求锁,而wait执行时会先释放锁,等被唤醒时再重新请求锁。这个锁是 wait 对像上的 monitorlock
  2. sleep 是无视锁的存在的,即之前请求的锁不会释放,没有锁也不会请求。
  3. wait 是 Object 的方法
  4. sleep 是 Thread 的静态方法

单例模式

饿汉模式

饿汉模式:在类加载时就完成了初始化,但是加载比较慢,获取对象比较快。不会出现线程安全问题 因为只产生了唯一实例。

class Singleton {
	private static Singleton instance = new Singleton();
	private Singleton() {}
	public static Singleton getInstance() {
		return instance;
	}
}

懒汉模式

懒汉模式:初始化不会被创建 只有在真正需要使用的时候才会创建实例。需要编写get同步方法,因为不确定会创建多少个实例而产生线程安全问题。

  • 单线程版
class Singleton {
	private static Singleton instance = null;
	private Singleton() {}
	public static Singleton getInstance() {
	if (instance == null) {
		instance = new Singleton();
	} 
	return instance;
	}
}
  • 多线程版,双重判断,性能高
class Singleton {
	private static volatile Singleton instance = null;
	private Singleton() {}
		public static Singleton getInstance() {
		if (instance == null) {
			synchronized (Singleton.class) {
				if (instance == null) {
					instance = new Singleton();
				}
			}
		} 
		return instance;
	}
}

保证线程安全的思路

一、使用没有共享资源的模型
二、适用共享资源只读,不写的模型

  1. 不需要写共享资源的模型
  2. 使用不可变对象

三、 直面线程安全

  1. 保证原子性
  2. 保证顺序性
  3. 保证可见性
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值