复习:进程与线程
进程
概念:
进程:在操作系统中,能够独立运行,并且作为资源分配的基本单位。它表示运行中的程序。
时间片:
操作系统(如Windows、Linux)的任务调度是采用时间片轮转的抢占式调度方式,也就是说一个任务执行一小段时间后强制暂停去执行下一个任务,每个任务轮流执行。
任务执行的一小段时间叫做时间片,任务正在执行时的状态叫运行状态,任务执行一段时间后强制暂停去执行下一个任务,被暂停的任务就处于就绪状态等待下一个属于它的时间片的到来。
并发与并行:
并发:多个进程在一 个CPU下采用时间片轮转的方式,在一段时间之内,让多个进程都得以推进,称之为并发。
表现形式:人肉眼看起来,所有进程都是在“同时”执行。但是,在CPU层面看到的是,进程不停的轮流执行的(时间片轮转调度的方式)--------(假同时)【单个cpu执行多任务在一个时间范围以时间片轮转调度方式】
并行:多个进程在多个CPU下分别,同时进行运行,这称之为并行。
表现形式:在一个时间点上,可以同时运行多个进程-------(真同时)】多CPU-个时间点同时执行多任务】
内核态、用户态
一般的操作系统(如Windows、Linux)对执行权限进行分级:用户态和内核态。
- 操作系统内核作为直接控制硬件设备的底层软件,权限最高,称为内核态,或核心态。 CPU可以访问内存所有数据, 包括外围设备, 例如硬盘, 网卡.
- 用户程序的权限最低,称为用户态。 CPU可以访问内存所有数据, 包括外围设备, 例如硬盘, 网卡.
进程状态圈
- 就绪:进程处于可运行的状态,只是CPU时间片还没有轮转到该进程,则该进程处于就绪状态。
- 运行:进程处于可运行的状态,且CPU时间片轮转到该进程,该进程正在执行代码,则该进程处于运行状态
- 阻塞:进程不具备运行条件,正在等待某个事件的完成。
线程
概念:
线程:是进程中的一个实例,作为系统调度和分派的基本单位。是进程中的一段序列,能够完成进程中的一个功能。
- 进程是系统分配资源的最小单位,线程是系统调度的最小单位。一个进程内的线程之间是可以共享资源的。
- 每个进程至少有一个线程存在,即主线程
- 线程也存在并发、并行(单个CPU时间片轮转、-个时间点,多个CPU.上的真同时)
- 多线程的优势:增加运行速度
创建线程:
方法1:继承Thread 类
可以通过继承 Thread 来创建一个线程类,该方法的好处是 this 代表的就是当前线程,不需要通过 Thread.currentThread() 来获取当前线程的引用。
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() 来获取当前线程的引用。
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "这里是线程运行的代码");
}
}
Thread t = new Thread(new MyRunnable());
t.start(); // 线程开始运行
线程api
start()和run()方法:
- run方法直接调用,不会启动线程,只是在当前main线程中,调用了run方法
- 线程启动时通过start方法启动的
public class Main{
public static void main(String[] args) {
MyThread myThread=new MyThread();
//myThread.start();
//run方法直接调用,不会启动线程,只是在当前main线程中,调用了run方法
myThread.run();
//new MyThread().start();
new Thread(new MyThread()).start();//线程启动时通过start方法启动的
}
}
class MyThread extends Thread{
@Override
public void run() {
//run是线程运行的时候执行的代码块
System.out.println(Thread.currentThread().getName());
}
}
进程的退出:
- 至少有一个非守护线程没有被销毁,进程就不会退出。
- 非守护线程一般可以成为工作线程,守护线程可以称为后台线程
setDaemon(true):设置线程为守护线程
public class Daemon {
public static void main(String[] args) {
Thread t=new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(9999999999L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
//设置线程为守护线程
t.setDaemon(true);
//t.start();
}
}
线程让步–yield()方法:
将当前线程由运行态—>就绪态
public class ThreadYield {
public static void main1(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}).start();
// while (Thread.activeCount()>1){
// Thread.yield();//将当前线程有运行态--->就绪态
// }
System.out.println(Thread.currentThread().getName());
}
}
线程的等待–join()方法:
当前线程:代码执行时,所在的线程
当前线程阻塞(运行态–>阻塞态)等待(满足一定条件),t线程(不做限制,自由调度)一定条件
以下条件哪个先执行完,就满足
- 传入时间(时间值+时间单位毫秒)
- 线程引用对象执行完毕
public static void without() throws InterruptedException {
Thread t=new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
});
t.start();
t.join();//当前线程等待,直到t线程执行完毕
System.out.println(Thread.currentThread().getName());
}
public static void with() throws InterruptedException {
Thread t=new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(5000);
System.out.println(Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
t.join(2000);
//当前线程main线程等待2秒钟就往下执行了(t线程等待时间更长)
System.out.println(Thread.currentThread().getName());
}
线程的等待–结合activeCount()+yield()方法:
//等待new Thread所有线程执行完毕,否则一直等待
while (Thread.activeCount()>1){
Thread.yield();//将当前线程有运行态--->就绪
}
线程中断:
- 通过 thread 对象调用 interrupt() 方法通知该线程停止运行 (初始为false)
- thread 收到通知的方式有两种:
- 如果线程调用了 wait/join/sleep 等方法而阻塞挂起,则以 InterruptedException 异常的形式通知,清除 中断标志
- 否则,只是内部的一个中断标志被设置,thread 可以通过
- Thread.interrupted() 判断当前线程的中断标志被设置,重置中断标志
- Thread.currentThread().isInterrupted() 判断指定线程的中断标志被设置,不清除中断标志
不是真实的直接中断,而是告诉某个线程,需要进行中断,具体是否要中断,由该线程自己来决定
public static void main1(String[] args) {
Thread thread=new Thread(new Runnable() {
@Override
public void run() {
//线程运行时,需要自行判断线程中断标志位
while(!Thread.currentThread().isInterrupted()){
System.out.println();
}
while (!Thread.interrupted()){
System.out.println(Thread.currentThread().getName());
}
}
});
thread.start();//中断标志位=false
thread.interrupt();//中断标志位=true
}
Thread thread=new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println(Thread.currentThread().isInterrupted());
Thread.sleep(30000);
//线程调用wait()/join()/sleep()方法阻塞当前线程,会直接抛出异常
//阻塞状态是,通过捕获及处理异常,来处理线程的中断逻辑
System.out.println(Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
//捕获到异常之后,线程的中断标志位,被重置
System.out.println(Thread.currentThread().isInterrupted());
}
}
});
thread.start();//中断标志位=false
thread.interrupt();//中断标志位=true
获取当前线程的引用:public static Thread currentThread();
休眠线程:public static void sleep(long millis) throws InterruptedException
线程安全
不安全的原因:
1.原子性
- 我们把一段代码想象成一个房间,每个线程就是要进入这个房间的人。如果没有任何机制保证,A进入房间之后,还 没有出来;B 是不是也可以进入房间,打断 A 在房间里的隐私。这个就是不具备原子性的。
- 那我们应该如何解决这个问题呢?是不是只要给房间加一把锁,A 进去就把门锁上,其他人是不是就进不来了。这样 就保证了这段代码的原子性了。
- 一条 java 语句不一定是原子的,也不一定只是一条指令
- 比如我们看到的 n++,其实是由三步操作组成的:
- 从内存把数据读到 CPU
- 进行数据更新
- 把数据写回到 CPU
- 比如new对象操作:
- 分配对象的内存
- 初始化对象
- 将对象赋值给变量
2.可见性
为了提高效率,JVM在执行过程中,会尽可能的将数据在工作内存中执行,但这样会造成一个问题,共享变量在多线 程之间不能及时看到改变,这个就是可见性问题。
3.代码顺序性
一段代码是这样的:
- 去前台取下 U 盘
- 去教室写 10 分钟作业
- 去前台取下快递
如果是在单线程情况下,JVM、CPU指令集会对其进行优化,比如,按 1->3->2的方式执行,也是没问题,可以少跑 一次前台。这种叫做指令重排序
synchronize
- synchronized的底层是使用操作系统的mutex lock实现的
- 当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。
- 运行的线程数量越多,性能下降越快(归还对象锁的时候,就有越多的线程不停的在被唤醒、阻状态切换)
- 同步代码执行时间越短,性能下降也较快
volatile
- 保证可见性
- 保证有序性
使用场景:可以结合线程加锁的一些手段,提高线程效率
private static volatile int count1=0;
public static void main(String[] args) {
//同时启动20个线程,每个线程对同一个变量执行操作,循环10000次,每次循环++操作
for(int i=0;i<20;i++){
new Thread(new Runnable() {
@Override
public void run() {
for(int j=0;j<10000;j++){
synchronized (volatileThread.class){
if(count<100000){
count++;
}
}
}
}
}).start();
}
while (Thread.activeCount()>1){
Thread.yield();
}
System.out.println(count);
}
线程通信
- wait()的作用是让当前线程进入等待状态,同时,wait()也会让当前线程释放它所持有的锁。“直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法”,当前线程被唤醒(进入“就绪状态”)
- notify()和notifyAll()的作用,则是唤醒当前对象上的等待线程;notify()是唤醒单个线程,而notifyAll()是唤醒所有的 线程。
- wait(long timeout)让当前线程处于“等待(阻塞)状态”,直到其他线程调用此对象的notify()方法或 notifyAll()方法,或者超过指定的时间量,当前线程被唤醒(进入“就绪状态”)。
wait()方法
- 方法wait()的作用是使当前执行代码的线程进行等待,wait()方法是Object类的方法,该方法是用来将当前线程 置入“预执行队列”中,并且在wait()所在的代码处停止执行,直到接到通知或被中断为止。
- wait()方法只能在同步方法中或同步块中调用。如果调用wait()时,没有持有适当的锁,会抛出异常。
- wait()方法执行后,当前线程释放锁,线程与其它线程竞争重新获取锁。
notify()方法
notify方法就是使停止的线程继续运行。
- 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对 其发出通知notify,并使它们重新获取该对象的对象锁。如果有多个线程等待,则有线程规划器随机挑选出一个呈wait状态的线程。
- 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。
notifyAll()方法
- 使用notifyAll()方法唤醒所有等待线程
代码举例:
1.while来判断,不要使用if(因为在判断代码中进行wait释放锁以后,其他线程会修改变量,再次wait被通知恢复的时候,条件已经不满足了)
2.使用notifyAll方法,通知所有wait被阻塞的线程
public class Sington {
//库存面包数量:上限100
public static volatile int sum;
public static void main(String[] args) {
//启动5个生产者,生产面包
for(int i=0;i<5;i++){
new Thread(new Producer(),"面包师傅"+i).start();
}
//启动消费者线程,消费面包
for(int i=0;i<20;i++){
new Thread(new Consumer(),"消费者"+i).start();
}
}
//默认生产者:面包师傅生产面包:一次生产3个面包,每个面包师傅生产20次
private static class Producer implements Runnable{
@Override
public void run() {
for(int i=0;i<20;i++){
synchronized (Sington.class){
//生产完后,库存大于100不行,所以库存在97以上不能生产
while (sum+3>100){
//释放对象锁,需要让其他线程进入同步代码块,当前线程需要进入阻塞状态
try {
Sington.class.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
sum+=3;//生产面包
Sington.class.notify();
System.out.println(Thread.currentThread().getName()+",生产了,库存为:"+sum);
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//默认消费者:消费面包:一次消费一个面包,消费者一直消费
private static class Consumer implements Runnable{
@Override
public void run() {
while (true){
synchronized (Sington.class){
//库存为0,不能继续消费,阻塞当前线程
//不能用if判断,
while (sum==0){
try {
Sington.class.wait();
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
sum--;
//notify和notifyAll都是通知调用wait()被阻塞的线程
//notify随机唤醒一个wait()阻塞的线程
//notifyAll唤醒全部wait()阻塞的线程
//在synchronized代码块结束,也就是释放对象锁之后,才会唤醒
//等于说,synchronized结束之后,wait()和synchronized代码行阻塞的线程,都会被唤醒
Sington.class.notifyAll();
//最好是notifyAll,唤醒所有
System.out.println(Thread.currentThread().getName()+",消费了,库存为:"+sum);
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
单例模式
单例模式有以下特点:
1、单例类只能有一个实例。
2、单例类必须自己创建自己的唯一实例。
3、单例类必须给所有其他对象提供这一实例。
饿汉模式:
class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
懒汉模式
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.ThreadPoolExecutor()方法参数意义:
ExecutorService pool = new ThreadPoolExecutor(//线程池---快递公司
3,// 核心线程数(正式员工):创建好线程池,正式员工就开始取快递
// 临时工雇佣:正式员工忙不过来,就会创建临时工
// 临时工解雇:空闲时间超出设置的时间范围,就解雇
5,// 最大线程数(最多数量的员工:正式员工+临时工)
30,// 时间数量
TimeUnit.SECONDS,// 时间单位(时间数量+时间单位表示一定范围的时间)
// 阻塞队列:存放包裹的仓库(存放任务的数据结构)
new ArrayBlockingQueue<>(1000),
// (了解)线程池创建Thread线程的工厂类。没有提供的话,就使用线程池内部默认的创建线程的方式
// new ThreadFactory() {
// @Override
// public Thread newThread(Runnable r) {
// return null;
// }
// },
// 拒绝策略:
// CallerRunsPolicy:谁(execute代码行所在的线程)让我(快递公司)送快递,不好意思,你自己去送
// AbortPolicy:直接抛出异常RejectedExecutionException
// DiscardPolicy:从阻塞队列丢弃最新的任务(队尾)
// DiscardOldestPolicy:从阻塞队列丢弃最旧的任务(队首)
new ThreadPoolExecutor.DiscardOldestPolicy()
);
- Executors.newxxx()方法
ExecutorService pool = Executors.newSingleThreadExecutor();//线程池的员工就是1个
ExecutorService pool = Executors.newFixedThreadPool(4);//正式员工数量为4,没有临时工
ScheduledExecutorService pool = Executors.newScheduledThreadPool(4);//正式员工
ExecutorService pool = Executors.newCachedThreadPool();//正式员工为0,临时工数量不限制
线程的优点
-
创建一个新线程的代价要比创建一个新进程小得多
-
与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
-
线程占用的资源要比进程少很多
-
能充分利用多处理器的可并行数量
-
在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
-
计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
-
I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。