认识线程
- 进程:操作系统中一个程序的执行周期,进程是系统分配资源的最小单位
- 线程:一个程序同时执行多个任务,每个任务就称为一个线程。线程是系统调度的最小单位。
- 一个进程内的线程之间是可以共享资源的。
每个进程至少有一个线程存在,即主线程
进程 | 线程 | |
---|---|---|
根本区别 | 进程是操作系统资源分配的基本单位 | 线程是任务调度和执行的基本单位 |
开销方面 | 每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销 | 线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。 |
所处环境 | 在操作系统中能同时运行多个进程(程序) | 而在同一个进程(程序)中有多个线程同时执行(通过CPU调度,在每个时间片中只有一个线程执行) |
内存分配 | 系统在运行的时候会为每个进程分配不同的内存空间 | 除了CPU外,系统不会为线程分配内存(线程所使用的资源来自其所属进程的资源),线程组之间只能共享资源。 |
包含关系 | 没有线程的进程可以看做是单线程的,如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的 | 线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。 |
线程优点:
- 创建一个新线程的代价要比创建一个新进程小得多
- 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
- 线程占用的资源要比进程少很多
- 能充分利用多处理器的可并行数量
- 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
- 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
- I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作
多线程的实现
- 继承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 也可以马上收到)
- 通过 thread 对象调用 interrupt() 方法通知该线程停止运行
- 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());
}
}
多线程带来的的风险-线程安全
线程安全概念:
如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。
线程不安全的原因:
- 原子性::即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
- 可见性:可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
- 有序性:即程序执行的顺序按照代码的先后顺序执行
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()方法就是使线程停止运行。从运行态回到阻塞态。
特点:
- 方法wait()的作用是使当前执行代码的线程进行等待,wait()方法是Object类的方法,该方法是用来将当前线程置入“预执行队列”中,并且在wait()所在的代码处停止执行,直到接到通知或被中断为止。
- wait()方法只能在同步方法中或同步块中调用。如果调用wait()时,没有持有适当的锁,会抛出异常。
- wait()方法执行后,当前线程释放锁,线程与其它线程竞争重新获取锁
notify()方法
notify方法就是使停止的线程继续运行。
- 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。如果有多个线程等待,则有线程规划器随机挑选出一个
呈wait状态的线程。 - 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。
notifyAll()方法
notifyAll方法可以一次唤醒所有的等待线程
wait和sleep对比
- wait 之前需要请求锁,而wait执行时会先释放锁,等被唤醒时再重新请求锁。这个锁是 wait 对像上的 monitorlock
- sleep 是无视锁的存在的,即之前请求的锁不会释放,没有锁也不会请求。
- wait 是 Object 的方法
- 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;
}
}
保证线程安全的思路
一、使用没有共享资源的模型
二、适用共享资源只读,不写的模型
- 不需要写共享资源的模型
- 使用不可变对象
三、 直面线程安全
- 保证原子性
- 保证顺序性
- 保证可见性