11 多线程
线程安全 线程不安全 个人理解
A指向B C指向B 当C调用B改变其值,而A不知道B改变了,在使用的时候,以为还是原数据。从而导致线程不安全。
程序与进程与线程区别和联系
区别 | 程序 | 进程 | 线程 |
---|---|---|---|
根本区别 | 程序只是一组指令的有序集合,它本身没有任何运行的含义,它只是一个静态的实体。 | 作为资源分配的单位 | 调度和执行的单位 |
开销 | 有序集合(进程)所需要的资源 | 每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销 | 线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程有独立运行栈和程序计数器(PC),线程切换的开销小 |
所处环境 | cpu和内存 | 在操作系统中能同时运行多个任务(程序) | 在同一应用程序中有多个顺序流同时执行 |
分配内存 | 系统统一程序所需分配内存 | 系统在运行的时候会为每个进程分配不同的内存区域 | 线程间共享进程的所有资源,每个线程只有有自己的堆栈和局部变量。线程由CPU独立调度执行,在多CPU环境下就允许多个线程同时运行 |
包含关系 | 包含多个进程 | 没有线程的进程可以看作单线程,如果一个进程拥有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的 | 线程是进程的一部分,所以线程有的时候会被称为是轻量级进程或轻权进程 |
多线程的优点和缺点
优点
资源利用率更好;程序设计在某些情况下更简单;程序响应更快
缺点
设计更复杂,虽然有一些多线程应用程序比单线程的应用程序要简单,但其他的一般都更复杂。在多线程访问共享数据的时候,这部分代码需要特别的注意。线程之间的交互往 往非常复杂。不正确的线程同步产生的错误非常难以被发现,并且重现以修复。上下文切换的开销 当 CPU 从执行一个线程切换到执行另外一个线程的时候,它需要 先存储当前线程的本地的数据,程序 指针等,然后载入另一个线程的本地数据,程序指针 等,最后才开始执行。这种切换称为“上下文切 换”(“context switch”)。CPU 会在一 个上下文中执行一个线程,然后切换到另外一个上下文中执 行另外一个线程。上下文切换 并不廉价。如果没有必要,应该减少上下文切换的发生。
线程创建与使用
1)继承Thread类创建线程
2)实现Runnable接口创建线程
3)使用Callable和Future创建线程
4)使用线程池例如用Executor框架
继承Thread类创建线程
步骤:
- 新建类继承Thread类,并重写run()方法。
- 创建Thread类的子类对象
- 启动线程,调用start()方法
实现Runnable接口创建线程
步骤:
- 新建类实现Runnable类,并重写run()方法。
- 创建实现Runnable的类对象a,并再创建Thread对象,将传入。
- 启动线程,调用start()方法
使用Callable和Future创建线程
步骤:
- 新建类实现Callable类A,并重写call()方法。
- 创建A类对象a,再创建FutureTask(a)类对象b,再创建Thread(b)类对象c
- 启动线程,调用c.start()方法
使用线程池例(后期补充)
线程池的作用:
(1) 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗;
(2) 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行;
(3) 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
线程池可以应对突然大爆发量的访问,通过有限个固定线程为大量的操作服务,减少创建和销毁线程所需的时间。
池的一般作用:提供缓冲的作用。
常见的创建线程池:
newSingleThreadExecutor:一个单线程的线程池,可以用于需要保证顺序执行的场景,并且只有一个线程在执行。
newFixedThreadPool:一个固定大小的线程池,可以用于已知并发压力的情况下,对线程数做限制。
newCachedThreadPool:一个可以无限扩大的线程池,比较适合处理执行时间比较小的任务。
newScheduledThreadPool:可以延时启动,定时启动的线程池,适用于需要多个后台线程执行周期任务的场景。
newWorkStealingPool:一个拥有多个任务队列的线程池,可以减少连接数,创建当前可用cpu数量的线程来并行执行。
案例:以下是前三种
TestThread
public class TestThread extends Thread {
@Override
public void run() {
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread 实现多线程");
}
}
}
TestRunnable
public class TestRunnable implements Runnable {
@Override
public void run() {
while (true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Runnable 实现多线程");
}
}
}
TestCallable
public class TestCallable implements Callable {
@Override
public Object call() throws Exception {
for (int i = 0; i <10 ; i++) {
Thread.sleep(1000);
System.out.println("Callable 实现多线程 "+(i+1));
}
return null;
}
}
Test
public class Test {
public static void main(String[] args) {
TestThread testThread = new TestThread();
testThread.start();
TestRunnable testRunnable = new TestRunnable();
Thread thread=new Thread(testRunnable);
thread.start();
TestCallable testCallable = new TestCallable();
FutureTask<Integer> futureTask=new FutureTask<>(testCallable);
Thread thread1=new Thread(futureTask);
thread1.start();
}
}
线程的五种状态
新建状态
使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。
就绪状态
当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。
运行状态
如果就绪状态的线程获取 CPU 资源,就可以执行run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
阻塞状态
如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:
等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。
同步阻塞:线程在获取 synchronized同步锁失败(因为同步锁被其他线程占用)。
其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
死亡状态
一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。
线程常用的方法
sleep:抱着资源睡觉
sleep() 方法需要指定等待的时间,它可以让当前正在执行的线程在指定的时间内暂停执行,进入阻塞状态,该方法既可以让其他同优先级或者高优先级的线程得到执行的机会,也可以让低优先级的线程得到执行机会。但是 sleep() 方法不会释放“锁标志”,也就是说如果有 synchronized 同步块,其他线程仍然不能访问共享数据。
yield:让出cpu资源
yield() 方法和 sleep() 方法类似,也不会释放“锁标志”,区别在于,它没有参数,即 yield() 方法只是使当前线程重新回到可执行状态,所以执行yield() 的线程有可能在进入到可执行状态后马上又被执行。让出CPU的使用权,从运行态直接进入就绪态。让CPU重新挑选哪一个线程进入运行状态。
join:等另一个线程执行
方法会使当前线程等待调用 join() 方法的线程执行结束之后,才会继续往后执行。
属性方法:
线程安全是多线程编程时的计算机程序代码中的一个概念。在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。
线程同步synchronize
- 普通同步方法(实例方法),锁是当前实例对象 ,进入同步代码前要获得当前实例的锁
- 静态同步方法,锁是当前类的class对象 ,进入同步代码前要获得当前类对象的锁
- 同步方法块,锁是括号里面的对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
理解:获取同步对象的所有,并使其只能一个一个执行,每次在同步代码块中执行的只有一个,同步区域越大越安全,但效率就越低,相反同步区域越小,效率越高,但是越不安全。
死锁
死锁是指两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的线程称为死锁线程。
生产者消费者
实现原理线程通信,唤醒。
方法:
wait() 让当前线程等着,释放cpu执行权,在当前对象锁中的线程池中排队,将线程临时存储到了线程池中。
当前线程必须拥有此对象的监视器(锁),否则抛出java.lang.IllegalMonitorStateException线程唤醒notify() 唤醒等待的一个线程,让其他线程去执行调度
notifyAll(): 会唤醒线程池中所有的等待的线程。
这些方法必须使用在同步中,因为必须要标识wait、notify等方法所属的锁。同一个锁上的notify,只能唤醒改锁上wait的线程。默认是this.wait();this.notify();this.notifyAll()。
实现原理:
生产者 生产了 就等待 唤醒消费者
消费者 消费了 就等待 唤醒生产者