线程
进程 在操作系统上(OS),一个独立运行的任务被称为进程 进程是可以并发执行(即多个进程可以同时运行)
线程是进程中 ,多个并发执行的任务逻辑。线程是进程的组成单位,一个进程至少要有一个线程 原因是进程任务实际的执行者是线程 类比 进程–小组 线程–小组成员 任务是分配给小组的 但实际执行小组任务的是小组成员
一个进程的任务实际上是又(1~n)个线程来完成的,对于java来说,JVM相当于操作系统上运行的进程,JVM一定会包含一个线程被称为主线程,而main函数就是由主线程来执行的
多线程在宏观上是并行,微观上是串行
一.线程得组成
1.CPU:一个线程进行执行时 需要使用CPU CPU具有时间片 哪个线程获得时间片哪个线程去使用CPU 使用CPU得时间由时间片决定 时间片的分发和管理由操作系统负责
2.数据:
每个线程拥有自己得JVM栈 栈空间独立:每个线程执行逻辑中方法得调用都是独立得
所有线程共享同一个堆空间 堆空间共享:每个线程中产生得数据如果是放置在堆空间中的 那么的都是共享的 比如创建得对象
二.线程的创建 非常重要
对于其他线程的创建与开启,一般要依赖于某个线程(主线程)所以创建线程开启线程的代码,往往要放置在主函数里
1.先创建任务对象 在创建线程对象
a.定义任务类 Runnable接口的实现类就是任务类
class MyTask implements Runnable{
@Override
public void run() {
// TODO Auto-generated method stub
//任务代码
for(char c='A';c<='Z';c++){
System.out.println(c);
}
}
}
b.创建任务对象
MyTask task1 = new MyTask();
前两步操作也可以使用Lambda与匿名内部类来完成
c.创建线程对象 并将任务提交给线程
Thread t1 = new Thread(task1);
d.开启线程 调用线程的start方法
t1.start();
2.创建一个自带任务的线程
a.自定义一个线程类 写一个类 继承Thread 重写父类的run方法 run方法中写的就是该自定义线程类 自带的任务
class MyThread extends Thread{
@Override
public void run(){
//自带的任务代码
for(char c='A';c<='Z';c++){
System.out.println(c);
}
}
}
b.创建自定义线程类对象 并开启线程
MyThread t1 = new MyThread();
t1.start();
这两步 可以使用匿名内部类直接搞定 但是不能用Lambda
三.线程的状态 面试常问
1.线程的官方状态
1.NEW
创建一个线程 而没有启动时
2.RUNNABLE
可以获得时间片 或 正在执行时 这两种状态都是RUNNABLE可运行状态
3.BLOCKED
阻塞状态,进入到阻塞状态线程会放弃时间片,且不再参与时间片的争夺
4.WAITING
无限期等待,当线程被其他线程加队时(执行了其他线程对象的join方法时) 会进入到该状态
5.TIMED_WAITING
有限期的等待 当线程执行了Thread.sleep方法时 会让当前线程进入到有限期等待
6.TERMINATED
当线程任务执行完毕时 进入到终止状态
2.改变线程状态的方法
a.Thread.sleep(毫秒数);必须掌握
如果某个线程在执行时 执行了该方法 那么该线程就会进入到睡眠,放弃自己的时间片,在有限时间中不在参与时间片的争夺 不会放弃锁标记
b.线程对象.join();可以不会
在一个线程任务中,让某个线程对象调用join时 会让调用该方法的线程加队到当前线程之前,当前线程进入到无限期等待,并放弃时间片,不在参与时间片的获取
例:比如线程A的任务中 执行 了B.join() 此时 A会主动放弃时间片,不参与到时间片的争夺,进入到无限期等待,什么时候B的任务执行完 什么时候A从无限期等待恢复到RUNNABLE状态
四.Java中的线程池
线程池 线程池存放了一定数量的线程 这些线程可以重复的被利用 不必频繁的创建与销毁
好处:不用频繁的创建和销毁线程,节省系统资源,提高效率
1.线程池的类型
a.线程池的顶级接口:Executor
b.线程池的常用类型:ExecutorService
c.线程池官方实现类:ThreadPoolExecutor
不推荐使用该类创建线程池实例 如果有公司要求那么按照公司规范进行创建
线程池构造的7个参数 需要在出去面试前 背会
2.创建线程池对象
a.获取java中接口类型的对象:
1.使用官方提供的实现类创建对象
2.自己书写实现类创建对象
3.调用官方的工厂方法,直接获取接口类型的对象,而不去自己手动创建
b.什么是工厂方法?
工厂方法的实现,是使用某种手段创建一个接口类型的对象,该方法会给调用者返回值一个接口类型实现类的对象,而让调用者忽略创建实现类对象的过程
c.获取线程池对象的两个常用线程池方法
线程池相关的工厂方法 都被放置在Executors的工具类中
static ExecutorService newCachedThreadPool()
会返回一个缓存机制的线程池:线程池在创建时不包含任何的线程,当有新任务提交时,会验证是否有闲置的线程,如果有就把任务给闲置线程,如果没有将新创建一个线程接收任务,一个线程在完成任务后,会等待60秒,当60等待期间没有新任务时,线程会被销毁 不会有任何任务等待
static ExecutorService newFixedThreadPool(int nThreads)
会返回固定线程数量的线程池:参数就是规定线程池在创建时,会创建几个线程,当有新任务提交时,会先查看是否有闲置线程,如果有就提交给闲置线程,如果没有不会创建新的线程,而是任务等待闲置线程 存在任务等待的情况
3.如何给线程池提交任务
1.创建一个线程池
2.使用线程池的submit方法 提交任务(提交任务对象)
ExecutorService pool1 = Executors.newCachedThreadPool();
//ExecutorService pool2 = Executors.newFixedThreadPool(2);
Runnable task1 = ()->{
for (int i = 1; i < 26; i++) {
System.out.println(i);
}
};
Runnable task2 = ()->{
for (char c = 'A' ; c < 'Z'; c++) {
System.out.println(c);
}
};
pool1.submit(task1);
pool1.submit(task2);
4.Callable接口 了解
Runnable接口的run方法 存在问题 不能给任务的发布者返回计算的结果,也不能抛出异常
JDK1.5推出了新的任务类型 Callable类型 Callable类型只能与线程池搭配不能与手动创建的线程搭配使用
创建Callable类型的任务类
class 类名 implements Callable<泛型>{
public 泛型 call()throws 异常....{
//任务代码
return 给任务发布者返回的数据;
}
}
a.如何获取Callable类型任务的 任务结果?
1.创建Callable类型的任务类
class MyTask2 implements Callable<Integer>{
@Override
public Integer call() throws Exception {
// TODO Auto-generated method stub
int sum = 0;
for(int i=1;i<=500;i++){
sum += i;
}
return sum;
}
}
class MyTask3 implements Callable<Integer>{
@Override
public Integer call() throws Exception {
// TODO Auto-generated method stub
int sum = 0;
for(int i=501;i<=1000;i++){
sum += i;
}
return sum;
}
}
2.创建Callable类型的任务对象
MyTask2 task2 = new MyTask2();
MyTask3 task3 = new MyTask3();
3.将Callable类型的任务对象提交给线程池
ExecutorService pool = Executors.newCachedThreadPool();
Future<Integer> f1 = pool.submit(task2);
Future<Integer> f2 = pool.submit(task3);
4.将任务提交给线程池后,会得到Future的未来对象,因为计算结果产生在未来
5.通过Future的get方法可以获取Callable类型的任务对象的计算结果
System.out.println("主线程在干其他事情..");
System.out.println("主线程需要其他线程的计算结果,等待中.....");
Integer integer = f1.get();
Integer integer2 = f2.get();
Integer result = integer+integer2;
System.out.println(result);
注意:当Callable类型的任务对象还未将结果计算完成时,未来对象的get方法会使得当前线程进入阻塞状态,等到获取到结果时 才能继续执行
五.线程安全问题
1.名词解释 必须记住 非常重要
a.临界资源:在多线程并发下,多个线程共享的某个数据 被称为临界资源
b.原子操作:多步操作被视为一个整体,在执行顺序上不可以被打破
c.线程不同步/不安全:在多线程并发下,原子操作被破坏,临界资源数据出现问题
d.线程的同步/安全:在多线程并发下,保证原子操作不被破坏,从而保证临界资源的数据安全
e.死锁:两个线程相互等待对方释放所有占据的互斥锁标记,从而使得两个线程都会进入到阻塞状态,使得程序无法向下执行
2.如何保证线程的同步 重要
Java的每一个对象都会有一个 互斥锁标记
a.使用同步代码块保证线程同步
synchronized(临界资源对象){
//原子操作
}
当一个线程,是第一个访问同步代码块的线程 此时该线程获取到临界资源的互斥锁标记 当原子操作执行完毕时会归还互斥锁标记
当一个线程想要执行原子操作 会先试图获取互斥锁标记,获取成功则执行原子操作,如果获取失败则进入阻塞状态
阻塞状态状态的线程会放弃时间片 不参与时间片的争夺
当一个线程释放了互斥锁标记时,JVM会通知所有处在阻塞状态并等待该互斥锁标记的所有线程,此时这些线程会等待互斥所标记的随机分配,哪个线程获取到该互斥锁标记,哪个线程回归Runnable状态 参与时间片争夺
b.使用同步方法来保证线程的同步
如果一个方法的内部全部都是原子操作 并且临界资源使用的是当前对象,那么我们可以直接把该方法声明为一个同步方法
语法:
修饰符 synchronized 方法返回值 方法名(参数表){
//原子操作
}
例:
public void push(String s) {
synchronized(this) {
str[size] = s;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
size++;
}
}
转为同步方法:
public synchronized void push(String s) {
str[size] = s;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
size++;
}
关于同步代码块与同步方法的使用位置:
同步代码块一般使用在任务代码中
同步方法,往往使用在临界资源的方法上
3.线程的状态图
6.对于集合是否线程安全,可变长字符串是否线程安全的解读
线程安全的实现类:往往内部方法都了同步操作
线程不安全的实现类:往往内部的方法没有做线程的同步操作
例如:
Vector为什么线程安全?ArrayList为什么线程不安全?
Vector中的方法都是同步方法,被synchronized修饰
ArrayList所有的方法都不是同步方法
4.线程间通讯 不是很重要
void wait()
当线程执行到 临界资源.wait()时,当前线程会立刻放弃时间片,放弃所有的互斥锁标记,进入到无限期等待
void notify()
当线程执行到 临界资源.notify(), JVM会去随机唤醒一个处于无限期等待且正在等待该临界资源的线程,唤醒后该线程会从 无限期等待进入到阻塞状态 从而等待JVM通知临界资源被释放然后准备争抢
void notifyAll()
当线程执行到 临界资源.notify(), JVM会去随机唤醒所有处于无限期等待且正在等待该临界资源的线程,唤醒后该线程会从 无限期等待进入到阻塞状态 从而等待JVM通知临界资源被释放然后准备争抢
5.lock锁
同步代码块以临界资源的互斥锁标记作为占据临界资源的标志
lock锁以自身为一个标记,先进行加锁的线程就占据lock锁,当解锁时释放lock锁
使用lock锁的步骤:
1.创建一个lock锁对象---使用实现类ReentrantLock获取lock锁对象
Lock lock = new ReentrantLock();
2.对原子操作的开始进行加锁, 在原子操作结束后进行解锁
public void push(String s) {
synchronized(this){
str[size] = s;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
size++;
}
}
使用lock锁替换
public void push(String s) {
lock.lock();
str[size] = s;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
size++;
lock.unlock();
}
lock锁常用方法:
1.lock()上锁
2.unlock()解锁
3.tryLock() 引用加锁 尝试获取lock对象如果lock被占据返回false 如果lock对象没有被占据则返回true并等效于lock()
lock锁的好处:使得锁具有更具的表现,代码可读性高,可以提高代码的灵活度,提高程序效率